json 0.4.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of json might be problematic. Click here for more details.

@@ -0,0 +1,21 @@
1
+ /* XPM */
2
+ static char * Array_xpm[] = {
3
+ "16 16 2 1",
4
+ " c None",
5
+ ". c #000000",
6
+ " ",
7
+ " ",
8
+ " ",
9
+ " .......... ",
10
+ " . . ",
11
+ " . . ",
12
+ " . . ",
13
+ " . . ",
14
+ " . . ",
15
+ " . . ",
16
+ " . . ",
17
+ " . . ",
18
+ " .......... ",
19
+ " ",
20
+ " ",
21
+ " "};
@@ -0,0 +1,21 @@
1
+ /* XPM */
2
+ static char * False_xpm[] = {
3
+ "16 16 2 1",
4
+ " c None",
5
+ ". c #FF0000",
6
+ " ",
7
+ " ",
8
+ " ",
9
+ " ...... ",
10
+ " . ",
11
+ " . ",
12
+ " . ",
13
+ " ...... ",
14
+ " . ",
15
+ " . ",
16
+ " . ",
17
+ " . ",
18
+ " . ",
19
+ " ",
20
+ " ",
21
+ " "};
@@ -0,0 +1,21 @@
1
+ /* XPM */
2
+ static char * Hash_xpm[] = {
3
+ "16 16 2 1",
4
+ " c None",
5
+ ". c #000000",
6
+ " ",
7
+ " ",
8
+ " ",
9
+ " . . ",
10
+ " . . ",
11
+ " . . ",
12
+ " ......... ",
13
+ " . . ",
14
+ " . . ",
15
+ " ......... ",
16
+ " . . ",
17
+ " . . ",
18
+ " . . ",
19
+ " ",
20
+ " ",
21
+ " "};
@@ -0,0 +1,73 @@
1
+ /* XPM */
2
+ static char * Key_xpm[] = {
3
+ "16 16 54 1",
4
+ " c None",
5
+ ". c #110007",
6
+ "+ c #0E0900",
7
+ "@ c #000013",
8
+ "# c #070600",
9
+ "$ c #F6F006",
10
+ "% c #ECE711",
11
+ "& c #E5EE00",
12
+ "* c #16021E",
13
+ "= c #120900",
14
+ "- c #EDF12B",
15
+ "; c #000033",
16
+ "> c #0F0000",
17
+ ", c #FFFE03",
18
+ "' c #E6E500",
19
+ ") c #16021B",
20
+ "! c #F7F502",
21
+ "~ c #000E00",
22
+ "{ c #130000",
23
+ "] c #FFF000",
24
+ "^ c #FFE711",
25
+ "/ c #140005",
26
+ "( c #190025",
27
+ "_ c #E9DD27",
28
+ ": c #E7DC04",
29
+ "< c #FFEC09",
30
+ "[ c #FFE707",
31
+ "} c #FFDE10",
32
+ "| c #150021",
33
+ "1 c #160700",
34
+ "2 c #FAF60E",
35
+ "3 c #EFE301",
36
+ "4 c #FEF300",
37
+ "5 c #E7E000",
38
+ "6 c #FFFF08",
39
+ "7 c #0E0206",
40
+ "8 c #040000",
41
+ "9 c #03052E",
42
+ "0 c #041212",
43
+ "a c #070300",
44
+ "b c #F2E713",
45
+ "c c #F9DE13",
46
+ "d c #36091E",
47
+ "e c #00001C",
48
+ "f c #1F0010",
49
+ "g c #FFF500",
50
+ "h c #DEDE00",
51
+ "i c #050A00",
52
+ "j c #FAF14A",
53
+ "k c #F5F200",
54
+ "l c #040404",
55
+ "m c #1A0D00",
56
+ "n c #EDE43D",
57
+ "o c #ECE007",
58
+ " ",
59
+ " ",
60
+ " .+@ ",
61
+ " #$%&* ",
62
+ " =-;>,') ",
63
+ " >!~{]^/ ",
64
+ " (_:<[}| ",
65
+ " 1234567 ",
66
+ " 890abcd ",
67
+ " efghi ",
68
+ " >jkl ",
69
+ " mnol ",
70
+ " >kl ",
71
+ " ll ",
72
+ " ",
73
+ " "};
@@ -0,0 +1,21 @@
1
+ /* XPM */
2
+ static char * False_xpm[] = {
3
+ "16 16 2 1",
4
+ " c None",
5
+ ". c #000000",
6
+ " ",
7
+ " ",
8
+ " ",
9
+ " ... ",
10
+ " . . ",
11
+ " . . ",
12
+ " . . ",
13
+ " . . ",
14
+ " . . ",
15
+ " . . ",
16
+ " . . ",
17
+ " . . ",
18
+ " ... ",
19
+ " ",
20
+ " ",
21
+ " "};
@@ -0,0 +1,28 @@
1
+ /* XPM */
2
+ static char * Numeric_xpm[] = {
3
+ "16 16 9 1",
4
+ " c None",
5
+ ". c #FF0000",
6
+ "+ c #0000FF",
7
+ "@ c #0023DB",
8
+ "# c #00EA14",
9
+ "$ c #00FF00",
10
+ "% c #004FAF",
11
+ "& c #0028D6",
12
+ "* c #00F20C",
13
+ " ",
14
+ " ",
15
+ " ",
16
+ " ... +++@#$$$$ ",
17
+ " .+ %& $$ ",
18
+ " . + $ ",
19
+ " . + $$ ",
20
+ " . ++$$$$ ",
21
+ " . + $$ ",
22
+ " . + $ ",
23
+ " . + $ ",
24
+ " . + $ $$ ",
25
+ " .....++++*$$ ",
26
+ " ",
27
+ " ",
28
+ " "};
@@ -0,0 +1,96 @@
1
+ /* XPM */
2
+ static char * String_xpm[] = {
3
+ "16 16 77 1",
4
+ " c None",
5
+ ". c #000000",
6
+ "+ c #040404",
7
+ "@ c #080806",
8
+ "# c #090606",
9
+ "$ c #EEEAE1",
10
+ "% c #E7E3DA",
11
+ "& c #E0DBD1",
12
+ "* c #D4B46F",
13
+ "= c #0C0906",
14
+ "- c #E3C072",
15
+ "; c #E4C072",
16
+ "> c #060505",
17
+ ", c #0B0A08",
18
+ "' c #D5B264",
19
+ ") c #D3AF5A",
20
+ "! c #080602",
21
+ "~ c #E1B863",
22
+ "{ c #DDB151",
23
+ "] c #DBAE4A",
24
+ "^ c #DDB152",
25
+ "/ c #DDB252",
26
+ "( c #070705",
27
+ "_ c #0C0A07",
28
+ ": c #D3A33B",
29
+ "< c #020201",
30
+ "[ c #DAAA41",
31
+ "} c #040302",
32
+ "| c #E4D9BF",
33
+ "1 c #0B0907",
34
+ "2 c #030201",
35
+ "3 c #020200",
36
+ "4 c #C99115",
37
+ "5 c #080704",
38
+ "6 c #DBC8A2",
39
+ "7 c #E7D7B4",
40
+ "8 c #E0CD9E",
41
+ "9 c #080601",
42
+ "0 c #040400",
43
+ "a c #010100",
44
+ "b c #0B0B08",
45
+ "c c #DCBF83",
46
+ "d c #DCBC75",
47
+ "e c #DEB559",
48
+ "f c #040301",
49
+ "g c #BC8815",
50
+ "h c #120E07",
51
+ "i c #060402",
52
+ "j c #0A0804",
53
+ "k c #D4A747",
54
+ "l c #D6A12F",
55
+ "m c #0E0C05",
56
+ "n c #C8C1B0",
57
+ "o c #1D1B15",
58
+ "p c #D7AD51",
59
+ "q c #070502",
60
+ "r c #080804",
61
+ "s c #BC953B",
62
+ "t c #C4BDAD",
63
+ "u c #0B0807",
64
+ "v c #DBAC47",
65
+ "w c #1B150A",
66
+ "x c #B78A2C",
67
+ "y c #D8A83C",
68
+ "z c #D4A338",
69
+ "A c #0F0B03",
70
+ "B c #181105",
71
+ "C c #C59325",
72
+ "D c #C18E1F",
73
+ "E c #060600",
74
+ "F c #CC992D",
75
+ "G c #B98B25",
76
+ "H c #B3831F",
77
+ "I c #C08C1C",
78
+ "J c #060500",
79
+ "K c #0E0C03",
80
+ "L c #0D0A00",
81
+ " ",
82
+ " .+@# ",
83
+ " .$%&*= ",
84
+ " .-;>,')! ",
85
+ " .~. .{]. ",
86
+ " .^/. (_:< ",
87
+ " .[.}|$12 ",
88
+ " 345678}90 ",
89
+ " a2bcdefgh ",
90
+ " ijkl.mno ",
91
+ " <pq. rstu ",
92
+ " .]v. wx= ",
93
+ " .yzABCDE ",
94
+ " .FGHIJ ",
95
+ " 0KL0 ",
96
+ " "};
@@ -0,0 +1,21 @@
1
+ /* XPM */
2
+ static char * TrueClass_xpm[] = {
3
+ "16 16 2 1",
4
+ " c None",
5
+ ". c #0BF311",
6
+ " ",
7
+ " ",
8
+ " ",
9
+ " ......... ",
10
+ " . ",
11
+ " . ",
12
+ " . ",
13
+ " . ",
14
+ " . ",
15
+ " . ",
16
+ " . ",
17
+ " . ",
18
+ " . ",
19
+ " ",
20
+ " ",
21
+ " "};
@@ -0,0 +1,1195 @@
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
+
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
+ # All JSON primitive types
20
+ ALL_TYPES = %w[TrueClass FalseClass Numeric String Array Hash NilClass].sort
21
+
22
+ # The Nodes necessary for the tree representation of a JSON document
23
+ ALL_NODES = (ALL_TYPES + %w[Key]).sort
24
+
25
+ # Returns the Gdk::Pixbuf of the icon named _name_ from the icon cache.
26
+ def Editor.fetch_icon(name)
27
+ @icon_cache ||= {}
28
+ unless @icon_cache.key?(name)
29
+ path = File.dirname(__FILE__)
30
+ @icon_cache[name] = Gdk::Pixbuf.new(File.join(path, name + '.xpm'))
31
+ end
32
+ @icon_cache[name]
33
+ end
34
+
35
+ # Opens an error dialog on top of _window_ showing the error message
36
+ # _text_.
37
+ def Editor.error_dialog(window, text)
38
+ dialog = MessageDialog.new(window, Dialog::MODAL,
39
+ MessageDialog::ERROR,
40
+ MessageDialog::BUTTONS_CLOSE, text)
41
+ dialog.run
42
+ ensure
43
+ dialog.destroy if dialog
44
+ end
45
+
46
+ # Opens a yes/no question dialog on top of _window_ showing the error
47
+ # message _text_. If yes was answered _true_ is returned, otherwise
48
+ # _false_.
49
+ def Editor.question_dialog(window, text)
50
+ dialog = MessageDialog.new(window, Dialog::MODAL,
51
+ MessageDialog::QUESTION,
52
+ MessageDialog::BUTTONS_YES_NO, text)
53
+ dialog.run do |response|
54
+ return Gtk::Dialog::RESPONSE_YES === response
55
+ end
56
+ ensure
57
+ dialog.destroy if dialog
58
+ end
59
+
60
+ # Convert the tree model starting from Gtk::TreeIter _iter_ into a Ruby
61
+ # data structure and return it.
62
+ def Editor.model2data(iter)
63
+ case iter.type
64
+ when 'Hash'
65
+ hash = {}
66
+ iter.each { |c| hash[c.content] = Editor.model2data(c.first_child) }
67
+ hash
68
+ when 'Array'
69
+ array = Array.new(iter.n_children)
70
+ iter.each_with_index { |c, i| array[i] = Editor.model2data(c) }
71
+ array
72
+ when 'Key'
73
+ iter.content
74
+ when 'String'
75
+ iter.content
76
+ when 'Numeric'
77
+ content = iter.content
78
+ if /\./.match(content)
79
+ content.to_f
80
+ else
81
+ content.to_i
82
+ end
83
+ when 'TrueClass'
84
+ true
85
+ when 'FalseClass'
86
+ false
87
+ when 'NilClass'
88
+ nil
89
+ else
90
+ fail "Unknown type found in model: #{iter.type}"
91
+ end
92
+ end
93
+
94
+ # Convert the Ruby data structure _data_ into tree model data for Gtk and
95
+ # returns the whole model. If the parameter _model_ wasn't given a new
96
+ # Gtk::TreeStore is created as the model. The _parent_ parameter specifies
97
+ # the parent node (iter, Gtk:TreeIter instance) to which the data is
98
+ # appended, alternativeley the result of the yielded block is used as iter.
99
+ def Editor.data2model(data, model = nil, parent = nil)
100
+ model ||= TreeStore.new(Gdk::Pixbuf, String, String)
101
+ iter = if block_given?
102
+ yield model
103
+ else
104
+ model.append(parent)
105
+ end
106
+ case data
107
+ when Hash
108
+ iter.type = 'Hash'
109
+ data.sort.each do |key, value|
110
+ pair_iter = model.append(iter)
111
+ pair_iter.type = 'Key'
112
+ pair_iter.content = key.to_s
113
+ Editor.data2model(value, model, pair_iter)
114
+ end
115
+ when Array
116
+ iter.type = 'Array'
117
+ data.each do |value|
118
+ Editor.data2model(value, model, iter)
119
+ end
120
+ when Numeric
121
+ iter.type = 'Numeric'
122
+ iter.content = data.to_s
123
+ when String, true, false, nil
124
+ iter.type = data.class.name
125
+ iter.content = data.nil? ? 'null' : data.to_s
126
+ else
127
+ iter.type = 'String'
128
+ iter.content = data.to_s
129
+ end
130
+ model
131
+ end
132
+
133
+ # The Gtk::TreeIter class is reopened and some auxiliary methods are added.
134
+ class Gtk::TreeIter
135
+ include Enumerable
136
+
137
+ # Traverse each of this Gtk::TreeIter instance's children
138
+ # and yield to them.
139
+ def each
140
+ n_children.times { |i| yield nth_child(i) }
141
+ end
142
+
143
+ # Recursively traverse all nodes of this Gtk::TreeIter's subtree
144
+ # (including self) and yield to them.
145
+ def recursive_each(&block)
146
+ yield self
147
+ each do |i|
148
+ i.recursive_each(&block)
149
+ end
150
+ end
151
+
152
+ # Remove the subtree of this Gtk::TreeIter instance from the
153
+ # model _model_.
154
+ def remove_subtree(model)
155
+ while current = first_child
156
+ model.remove(current)
157
+ end
158
+ end
159
+
160
+ # Returns the type of this node.
161
+ def type
162
+ self[TYPE_COL]
163
+ end
164
+
165
+ # Sets the type of this node to _value_. This implies setting
166
+ # the respective icon accordingly.
167
+ def type=(value)
168
+ self[TYPE_COL] = value
169
+ self[ICON_COL] = Editor.fetch_icon(value)
170
+ end
171
+
172
+ # Returns the content of this node.
173
+ def content
174
+ self[CONTENT_COL]
175
+ end
176
+
177
+ # Sets the content of this node to _value_.
178
+ def content=(value)
179
+ self[CONTENT_COL] = value
180
+ end
181
+ end
182
+
183
+ # This module bundles some method, that can be used to create a menu. It
184
+ # should be included into the class in question.
185
+ module MenuExtension
186
+ include Gtk
187
+
188
+ # Creates a Menu, that includes MenuExtension. _treeview_ is the
189
+ # Gtk::TreeView, on which it operates.
190
+ def initialize(treeview)
191
+ @treeview = treeview
192
+ @menu = Menu.new
193
+ end
194
+
195
+ # Returns the Gtk::TreeView of this menu.
196
+ attr_reader :treeview
197
+
198
+ # Returns the menu.
199
+ attr_reader :menu
200
+
201
+ # Adds a Gtk::SeparatorMenuItem to this instance's #menu.
202
+ def add_separator
203
+ menu.append SeparatorMenuItem.new
204
+ end
205
+
206
+ # Adds a Gtk::MenuItem to this instance's #menu. _label_ is the label
207
+ # string, _klass_ is the item type, and _callback_ is the procedure, that
208
+ # is called if the _item_ is activated.
209
+ def add_item(label, klass = MenuItem, &callback)
210
+ item = klass.new(label)
211
+ item.signal_connect(:activate, &callback)
212
+ menu.append item
213
+ item
214
+ end
215
+
216
+ # This method should be implemented in subclasses to create the #menu of
217
+ # this instance. It has to be called after an instance of this class is
218
+ # created, to build the menu.
219
+ def create
220
+ raise NotImplementedError
221
+ end
222
+
223
+ def method_missing(*a, &b)
224
+ treeview.__send__(*a, &b)
225
+ end
226
+ end
227
+
228
+ # This class creates the popup menu, that opens when clicking onto the
229
+ # treeview.
230
+ class PopUpMenu
231
+ include MenuExtension
232
+
233
+ # Change the type or content of the selected node.
234
+ def change_node(item)
235
+ if current = selection.selected
236
+ parent = current.parent
237
+ old_type, old_content = current.type, current.content
238
+ if ALL_TYPES.include?(old_type)
239
+ @clipboard_data = Editor.model2data(current)
240
+ type, content = ask_for_element(parent, current.type,
241
+ current.content)
242
+ if type
243
+ current.type, current.content = type, content
244
+ current.remove_subtree(model)
245
+ toplevel.display_status("Changed a node in tree.")
246
+ window.change
247
+ end
248
+ else
249
+ toplevel.display_status(
250
+ "Cannot change node of type #{old_type} in tree!")
251
+ end
252
+ end
253
+ end
254
+
255
+ # Cut the selected node and its subtree, and save it into the
256
+ # clipboard.
257
+ def cut_node(item)
258
+ if current = selection.selected
259
+ if current and current.type == 'Key'
260
+ @clipboard_data = {
261
+ current.content => Editor.model2data(current.first_child)
262
+ }
263
+ else
264
+ @clipboard_data = Editor.model2data(current)
265
+ end
266
+ model.remove(current)
267
+ window.change
268
+ toplevel.display_status("Cut a node from tree.")
269
+ end
270
+ end
271
+
272
+ # Copy the selected node and its subtree, and save it into the
273
+ # clipboard.
274
+ def copy_node(item)
275
+ if current = selection.selected
276
+ if current and current.type == 'Key'
277
+ @clipboard_data = {
278
+ current.content => Editor.model2data(current.first_child)
279
+ }
280
+ else
281
+ @clipboard_data = Editor.model2data(current)
282
+ end
283
+ window.change
284
+ toplevel.display_status("Copied a node from tree.")
285
+ end
286
+ end
287
+
288
+ # Paste the data in the clipboard into the selected Array or Hash by
289
+ # appending it.
290
+ def paste_node_appending(item)
291
+ if current = selection.selected
292
+ if @clipboard_data
293
+ case current.type
294
+ when 'Array'
295
+ Editor.data2model(@clipboard_data, model, current)
296
+ expand_collapse(current)
297
+ when 'Hash'
298
+ if @clipboard_data.is_a? Hash
299
+ parent = current.parent
300
+ hash = Editor.model2data(current)
301
+ model.remove(current)
302
+ hash.update(@clipboard_data)
303
+ Editor.data2model(hash, model, parent)
304
+ if parent
305
+ expand_collapse(parent)
306
+ elsif @expanded
307
+ expand_all
308
+ end
309
+ window.change
310
+ else
311
+ toplevel.display_status(
312
+ "Cannot paste non-#{current.type} data into '#{current.type}'!")
313
+ end
314
+ else
315
+ toplevel.display_status(
316
+ "Cannot paste node below '#{current.type}'!")
317
+ end
318
+ else
319
+ toplevel.display_status("Nothing to paste in clipboard!")
320
+ end
321
+ else
322
+ toplevel.display_status("Append a node into the root first!")
323
+ end
324
+ end
325
+
326
+ # Paste the data in the clipboard into the selected Array inserting it
327
+ # before the selected element.
328
+ def paste_node_inserting_before(item)
329
+ if current = selection.selected
330
+ if @clipboard_data
331
+ parent = current.parent or return
332
+ parent_type = parent.type
333
+ if parent_type == 'Array'
334
+ selected_index = parent.each_with_index do |c, i|
335
+ break i if c == current
336
+ end
337
+ Editor.data2model(@clipboard_data, model, parent) do |m|
338
+ m.insert_before(parent, current)
339
+ end
340
+ expand_collapse(current)
341
+ toplevel.display_status("Inserted an element to " +
342
+ "'#{parent_type}' before index #{selected_index}.")
343
+ window.change
344
+ else
345
+ toplevel.display_status(
346
+ "Cannot insert node below '#{parent_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
+ # Append a new node to the selected Hash or Array.
357
+ def append_new_node(item)
358
+ if parent = selection.selected
359
+ parent_type = parent.type
360
+ case parent_type
361
+ when 'Hash'
362
+ key, type, content = ask_for_hash_pair(parent)
363
+ key or return
364
+ iter = create_node(parent, 'Key', key)
365
+ iter = create_node(iter, type, content)
366
+ toplevel.display_status(
367
+ "Added a (key, value)-pair to '#{parent_type}'.")
368
+ window.change
369
+ when 'Array'
370
+ type, content = ask_for_element(parent)
371
+ type or return
372
+ iter = create_node(parent, type, content)
373
+ window.change
374
+ toplevel.display_status("Appendend an element to '#{parent_type}'.")
375
+ else
376
+ toplevel.display_status("Cannot append to '#{parent_type}'!")
377
+ end
378
+ else
379
+ type, content = ask_for_element
380
+ type or return
381
+ iter = create_node(nil, type, content)
382
+ window.change
383
+ end
384
+ end
385
+
386
+ # Insert a new node into an Array before the selected element.
387
+ def insert_new_node(item)
388
+ if current = selection.selected
389
+ parent = current.parent or return
390
+ parent_parent = parent.parent
391
+ parent_type = parent.type
392
+ if parent_type == 'Array'
393
+ selected_index = parent.each_with_index do |c, i|
394
+ break i if c == current
395
+ end
396
+ type, content = ask_for_element(parent)
397
+ type or return
398
+ iter = model.insert_before(parent, current)
399
+ iter.type, iter.content = type, content
400
+ toplevel.display_status("Inserted an element to " +
401
+ "'#{parent_type}' before index #{selected_index}.")
402
+ window.change
403
+ else
404
+ toplevel.display_status(
405
+ "Cannot insert node below '#{parent_type}'!")
406
+ end
407
+ else
408
+ toplevel.display_status("Append a node into the root first!")
409
+ end
410
+ end
411
+
412
+ # Recursively collapse/expand a subtree starting from the selected node.
413
+ def collapse_expand(item)
414
+ if current = selection.selected
415
+ if row_expanded?(current.path)
416
+ collapse_row(current.path)
417
+ else
418
+ expand_row(current.path, true)
419
+ end
420
+ else
421
+ toplevel.display_status("Append a node into the root first!")
422
+ end
423
+ end
424
+
425
+ # Create the menu.
426
+ def create
427
+ add_item("Change node", &method(:change_node))
428
+ add_separator
429
+ add_item("Cut node", &method(:cut_node))
430
+ add_item("Copy node", &method(:copy_node))
431
+ add_item("Paste node (appending)", &method(:paste_node_appending))
432
+ add_item("Paste node (inserting before)",
433
+ &method(:paste_node_inserting_before))
434
+ add_separator
435
+ add_item("Append new node", &method(:append_new_node))
436
+ add_item("Insert new node before", &method(:insert_new_node))
437
+ add_separator
438
+ add_item("Collapse/Expand node (recursively)",
439
+ &method(:collapse_expand))
440
+
441
+ menu.show_all
442
+ signal_connect(:button_press_event) do |widget, event|
443
+ if event.kind_of? Gdk::EventButton and event.button == 3
444
+ menu.popup(nil, nil, event.button, event.time)
445
+ end
446
+ end
447
+ signal_connect(:popup_menu) do
448
+ menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME)
449
+ end
450
+ end
451
+ end
452
+
453
+ # This class creates the File pulldown menu.
454
+ class FileMenu
455
+ include MenuExtension
456
+
457
+ # Clear the model and filename, but ask to save the JSON document, if
458
+ # unsaved changes have occured.
459
+ def new(item)
460
+ window.clear
461
+ end
462
+
463
+ # Open a file and load it into the editor. Ask to save the JSON document
464
+ # first, if unsaved changes have occured.
465
+ def open(item)
466
+ window.file_open
467
+ end
468
+
469
+ # Revert the current JSON document in the editor to the saved version.
470
+ def revert(item)
471
+ window.instance_eval do
472
+ @filename and file_open(@filename)
473
+ end
474
+ end
475
+
476
+ # Save the current JSON document.
477
+ def save(item)
478
+ window.file_save
479
+ end
480
+
481
+ # Save the current JSON document under the given filename.
482
+ def save_as(item)
483
+ window.file_save_as
484
+ end
485
+
486
+ # Quit the editor, after asking to save any unsaved changes first.
487
+ def quit(item)
488
+ window.quit
489
+ end
490
+
491
+ # Create the menu.
492
+ def create
493
+ title = MenuItem.new('File')
494
+ title.submenu = menu
495
+ add_item('New', &method(:new))
496
+ add_item('Open', &method(:open))
497
+ add_item('Revert', &method(:revert))
498
+ add_separator
499
+ add_item('Save', &method(:save))
500
+ add_item('Save As', &method(:save_as))
501
+ add_separator
502
+ add_item('Quit', &method(:quit))
503
+ title
504
+ end
505
+ end
506
+
507
+ # This class creates the Edit pulldown menu.
508
+ class EditMenu
509
+ include MenuExtension
510
+
511
+ # Find a string in all nodes' contents and select the found node in the
512
+ # treeview.
513
+ def find(item)
514
+ search = ask_for_find_term or return
515
+ begin
516
+ @search = Regexp.new(search)
517
+ rescue => e
518
+ Editor.error_dialog(self, "Evaluation of regex /#{search}/ failed: #{e}!")
519
+ return
520
+ end
521
+ iter = model.get_iter('0')
522
+ iter.recursive_each do |i|
523
+ if @iter
524
+ if @iter != i
525
+ next
526
+ else
527
+ @iter = nil
528
+ next
529
+ end
530
+ elsif @search.match(i[CONTENT_COL])
531
+ set_cursor(i.path, nil, false)
532
+ @iter = i
533
+ break
534
+ end
535
+ end
536
+ end
537
+
538
+ # Repeat the last search given by #find.
539
+ def find_again(item)
540
+ @search or return
541
+ iter = model.get_iter('0')
542
+ iter.recursive_each do |i|
543
+ if @iter
544
+ if @iter != i
545
+ next
546
+ else
547
+ @iter = nil
548
+ next
549
+ end
550
+ elsif @search.match(i[CONTENT_COL])
551
+ set_cursor(i.path, nil, false)
552
+ @iter = i
553
+ break
554
+ end
555
+ end
556
+ end
557
+
558
+ # Sort (Reverse sort) all elements of the selected array by the given
559
+ # expression. _x_ is the element in question.
560
+ def sort(item)
561
+ if current = selection.selected
562
+ if current.type == 'Array'
563
+ parent = current.parent
564
+ ary = Editor.model2data(current)
565
+ order, reverse = ask_for_order
566
+ order or return
567
+ begin
568
+ block = eval "lambda { |x| #{order} }"
569
+ if reverse
570
+ ary.sort! { |a,b| block[b] <=> block[a] }
571
+ else
572
+ ary.sort! { |a,b| block[a] <=> block[b] }
573
+ end
574
+ rescue => e
575
+ Editor.error_dialog(self, "Failed to sort Array with #{order}: #{e}!")
576
+ else
577
+ Editor.data2model(ary, model, parent) do |m|
578
+ m.insert_before(parent, current)
579
+ end
580
+ model.remove(current)
581
+ expand_collapse(parent)
582
+ window.change
583
+ toplevel.display_status("Array has been sorted.")
584
+ end
585
+ else
586
+ toplevel.display_status("Only Array nodes can be sorted!")
587
+ end
588
+ else
589
+ toplevel.display_status("Select an Array to sort first!")
590
+ end
591
+ end
592
+
593
+ # Create the menu.
594
+ def create
595
+ title = MenuItem.new('Edit')
596
+ title.submenu = menu
597
+ add_item('Find', &method(:find))
598
+ add_item('Find Again', &method(:find_again))
599
+ add_separator
600
+ add_item('Sort', &method(:sort))
601
+ title
602
+ end
603
+ end
604
+
605
+ class OptionsMenu
606
+ include MenuExtension
607
+
608
+ # Collapse/Expand all nodes by default.
609
+ def collapsed_nodes(item)
610
+ if expanded
611
+ self.expanded = false
612
+ collapse_all
613
+ else
614
+ self.expanded = true
615
+ expand_all
616
+ end
617
+ end
618
+
619
+ # Toggle pretty saving mode on/off.
620
+ def pretty_saving(item)
621
+ @pretty_item.toggled
622
+ window.change
623
+ end
624
+
625
+ attr_reader :pretty_item
626
+
627
+ # Create the menu.
628
+ def create
629
+ title = MenuItem.new('Options')
630
+ title.submenu = menu
631
+ add_item('Collapsed nodes', CheckMenuItem, &method(:collapsed_nodes))
632
+ @pretty_item = add_item('Pretty saving', CheckMenuItem,
633
+ &method(:pretty_saving))
634
+ @pretty_item.active = true
635
+ window.unchange
636
+ title
637
+ end
638
+ end
639
+
640
+ # This class inherits from Gtk::TreeView, to configure it and to add a lot
641
+ # of behaviour to it.
642
+ class JSONTreeView < Gtk::TreeView
643
+ include Gtk
644
+
645
+ # Creates a JSONTreeView instance, the parameter _window_ is
646
+ # a MainWindow instance and used for self delegation.
647
+ def initialize(window)
648
+ @window = window
649
+ super(TreeStore.new(Gdk::Pixbuf, String, String))
650
+ self.selection.mode = SELECTION_BROWSE
651
+
652
+ @expanded = false
653
+ self.headers_visible = false
654
+ add_columns
655
+ add_popup_menu
656
+ end
657
+
658
+ # Returns the MainWindow instance of this JSONTreeView.
659
+ attr_reader :window
660
+
661
+ # Returns true, if nodes are autoexpanding, false otherwise.
662
+ attr_accessor :expanded
663
+
664
+ private
665
+
666
+ def add_columns
667
+ cell = CellRendererPixbuf.new
668
+ column = TreeViewColumn.new('Icon', cell,
669
+ 'pixbuf' => ICON_COL
670
+ )
671
+ append_column(column)
672
+
673
+ cell = CellRendererText.new
674
+ column = TreeViewColumn.new('Type', cell,
675
+ 'text' => TYPE_COL
676
+ )
677
+ append_column(column)
678
+
679
+ cell = CellRendererText.new
680
+ cell.editable = true
681
+ column = TreeViewColumn.new('Content', cell,
682
+ 'text' => CONTENT_COL
683
+ )
684
+ cell.signal_connect(:edited, &method(:cell_edited))
685
+ append_column(column)
686
+ end
687
+
688
+ def unify_key(iter, key)
689
+ return unless iter.type == 'Key'
690
+ parent = iter.parent
691
+ if parent.any? { |c| c != iter and c.content == key }
692
+ old_key = key
693
+ i = 0
694
+ begin
695
+ key = sprintf("%s.%d", old_key, i += 1)
696
+ end while parent.any? { |c| c != iter and c.content == key }
697
+ end
698
+ iter.content = key
699
+ end
700
+
701
+ def cell_edited(cell, path, value)
702
+ iter = model.get_iter(path)
703
+ case iter.type
704
+ when 'Key'
705
+ unify_key(iter, value)
706
+ toplevel.display_status('Key has been changed.')
707
+ when 'FalseClass'
708
+ value.downcase!
709
+ if value == 'true'
710
+ iter.type, iter.content = 'TrueClass', 'true'
711
+ end
712
+ when 'TrueClass'
713
+ value.downcase!
714
+ if value == 'false'
715
+ iter.type, iter.content = 'FalseClass', 'false'
716
+ end
717
+ when 'Numeric'
718
+ iter.content = (Integer(value) rescue Float(value) rescue 0).to_s
719
+ when 'String'
720
+ iter.content = value
721
+ when 'Hash', 'Array'
722
+ return
723
+ else
724
+ fail "Unknown type found in model: #{iter.type}"
725
+ end
726
+ window.change
727
+ end
728
+
729
+ def configure_value(value, type)
730
+ value.editable = false
731
+ case type
732
+ when 'Array', 'Hash'
733
+ value.text = ''
734
+ when 'TrueClass'
735
+ value.text = 'true'
736
+ when 'FalseClass'
737
+ value.text = 'false'
738
+ when 'NilClass'
739
+ value.text = 'null'
740
+ when 'Numeric', 'String'
741
+ value.text ||= ''
742
+ value.editable = true
743
+ else
744
+ raise ArgumentError, "unknown type '#{type}' encountered"
745
+ end
746
+ end
747
+
748
+ def add_popup_menu
749
+ menu = PopUpMenu.new(self)
750
+ menu.create
751
+ end
752
+
753
+ public
754
+
755
+ # Create a _type_ node with content _content_, and add it to _parent_
756
+ # in the model. If _parent_ is nil, create a new model and put it into
757
+ # the editor treeview.
758
+ def create_node(parent, type, content)
759
+ iter = if parent
760
+ model.append(parent)
761
+ else
762
+ new_model = Editor.data2model(nil)
763
+ toplevel.view_new_model(new_model)
764
+ new_model.iter_first
765
+ end
766
+ iter.type, iter.content = type, content
767
+ expand_collapse(parent) if parent
768
+ iter
769
+ end
770
+
771
+ # Ask for a hash key, value pair to be added to the Hash node _parent_.
772
+ def ask_for_hash_pair(parent)
773
+ key_input = type_input = value_input = nil
774
+
775
+ dialog = Dialog.new("New (key, value) pair for Hash", nil, nil,
776
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
777
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
778
+ )
779
+
780
+ hbox = HBox.new(false, 5)
781
+ hbox.pack_start(Label.new("Key:"))
782
+ hbox.pack_start(key_input = Entry.new)
783
+ key_input.text = @key || ''
784
+ dialog.vbox.add(hbox)
785
+ key_input.signal_connect(:activate) do
786
+ if parent.any? { |c| c.content == key_input.text }
787
+ toplevel.display_status('Key already exists in Hash!')
788
+ key_input.text = ''
789
+ else
790
+ toplevel.display_status('Key has been changed.')
791
+ end
792
+ end
793
+
794
+ hbox = HBox.new(false, 5)
795
+ hbox.add(Label.new("Type:"))
796
+ hbox.pack_start(type_input = ComboBox.new(true))
797
+ ALL_TYPES.each { |t| type_input.append_text(t) }
798
+ type_input.active = @type || 0
799
+ dialog.vbox.add(hbox)
800
+
801
+ type_input.signal_connect(:changed) do
802
+ value_input.editable = false
803
+ case ALL_TYPES[type_input.active]
804
+ when 'Array', 'Hash'
805
+ value_input.text = ''
806
+ when 'TrueClass'
807
+ value_input.text = 'true'
808
+ when 'FalseClass'
809
+ value_input.text = 'false'
810
+ when 'NilClass'
811
+ value_input.text = 'null'
812
+ else
813
+ value_input.text = ''
814
+ value_input.editable = true
815
+ end
816
+ end
817
+
818
+ hbox = HBox.new(false, 5)
819
+ hbox.add(Label.new("Value:"))
820
+ hbox.pack_start(value_input = Entry.new)
821
+ value_input.text = @value || ''
822
+ dialog.vbox.add(hbox)
823
+
824
+ dialog.show_all
825
+ dialog.run do |response|
826
+ if response == Dialog::RESPONSE_ACCEPT
827
+ @key = key_input.text
828
+ type = ALL_TYPES[@type = type_input.active]
829
+ content = value_input.text
830
+ return @key, type, content
831
+ end
832
+ end
833
+ return
834
+ ensure
835
+ dialog.destroy
836
+ end
837
+
838
+ # Ask for an element to be appended _parent_.
839
+ def ask_for_element(parent = nil, default_type = nil, value_text = @content)
840
+ type_input = value_input = nil
841
+
842
+ dialog = Dialog.new(
843
+ "New element into #{parent ? parent.type : 'root'}",
844
+ nil, nil,
845
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
846
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
847
+ )
848
+ hbox = HBox.new(false, 5)
849
+ hbox.add(Label.new("Type:"))
850
+ hbox.pack_start(type_input = ComboBox.new(true))
851
+ default_active = 0
852
+ ALL_TYPES.each_with_index do |t, i|
853
+ type_input.append_text(t)
854
+ if t == default_type
855
+ default_active = i
856
+ end
857
+ end
858
+ type_input.active = default_active
859
+ dialog.vbox.add(hbox)
860
+ type_input.signal_connect(:changed) do
861
+ configure_value(value_input, ALL_TYPES[type_input.active])
862
+ end
863
+
864
+ hbox = HBox.new(false, 5)
865
+ hbox.add(Label.new("Value:"))
866
+ hbox.pack_start(value_input = Entry.new)
867
+ value_input.text = value_text if value_text
868
+ configure_value(value_input, ALL_TYPES[type_input.active])
869
+
870
+ dialog.vbox.add(hbox)
871
+
872
+ dialog.show_all
873
+ dialog.run do |response|
874
+ if response == Dialog::RESPONSE_ACCEPT
875
+ type = ALL_TYPES[type_input.active]
876
+ @content = case type
877
+ when 'Numeric'
878
+ Integer(value_input.text) rescue Float(value_input.text) rescue 0
879
+ else
880
+ value_input.text
881
+ end.to_s
882
+ return type, @content
883
+ end
884
+ end
885
+ return
886
+ ensure
887
+ dialog.destroy if dialog
888
+ end
889
+
890
+ # Ask for an order criteria for sorting, using _x_ for the element in
891
+ # question. Returns the order criterium, and true/false for reverse
892
+ # sorting.
893
+ def ask_for_order
894
+ dialog = Dialog.new(
895
+ "Give an order criterium for 'x'.",
896
+ nil, nil,
897
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
898
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
899
+ )
900
+ hbox = HBox.new(false, 5)
901
+
902
+ hbox.add(Label.new("Order:"))
903
+ hbox.pack_start(order_input = Entry.new)
904
+ order_input.text = @order || 'x'
905
+
906
+ hbox.pack_start(reverse_checkbox = CheckButton.new('Reverse'))
907
+
908
+ dialog.vbox.add(hbox)
909
+
910
+ dialog.show_all
911
+ dialog.run do |response|
912
+ if response == Dialog::RESPONSE_ACCEPT
913
+ return @order = order_input.text, reverse_checkbox.active?
914
+ end
915
+ end
916
+ return
917
+ ensure
918
+ dialog.destroy if dialog
919
+ end
920
+
921
+ # Ask for a find term to search for in the tree. Returns the term as a
922
+ # string.
923
+ def ask_for_find_term
924
+ dialog = Dialog.new(
925
+ "Find a node matching regex in tree.",
926
+ nil, nil,
927
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
928
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
929
+ )
930
+ hbox = HBox.new(false, 5)
931
+
932
+ hbox.add(Label.new("Regex:"))
933
+ hbox.pack_start(regex_input = Entry.new)
934
+ regex_input.text = @regex || ''
935
+
936
+ dialog.vbox.add(hbox)
937
+
938
+ dialog.show_all
939
+ dialog.run do |response|
940
+ if response == Dialog::RESPONSE_ACCEPT
941
+ return @regex = regex_input.text
942
+ end
943
+ end
944
+ return
945
+ ensure
946
+ dialog.destroy if dialog
947
+ end
948
+
949
+ # Expand or collapse row pointed to by _iter_ according
950
+ # to the #expanded attribute.
951
+ def expand_collapse(iter)
952
+ if expanded
953
+ expand_row(iter.path, true)
954
+ else
955
+ collapse_row(iter.path)
956
+ end
957
+ end
958
+ end
959
+
960
+ # The editor main window
961
+ class MainWindow < Gtk::Window
962
+ include Gtk
963
+
964
+ def initialize(encoding)
965
+ @changed = false
966
+ @encoding = encoding
967
+ super(TOPLEVEL)
968
+ display_title
969
+ set_default_size(800, 600)
970
+ signal_connect(:delete_event) { quit }
971
+
972
+ vbox = VBox.new(false, 0)
973
+ add(vbox)
974
+ #vbox.border_width = 0
975
+
976
+ @treeview = JSONTreeView.new(self)
977
+ @treeview.signal_connect(:'cursor-changed') do
978
+ display_status('')
979
+ end
980
+
981
+ menu_bar = create_menu_bar
982
+ vbox.pack_start(menu_bar, false, false, 0)
983
+
984
+ sw = ScrolledWindow.new(nil, nil)
985
+ sw.shadow_type = SHADOW_ETCHED_IN
986
+ sw.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
987
+ vbox.pack_start(sw, true, true, 0)
988
+ sw.add(@treeview)
989
+
990
+ @status_bar = Statusbar.new
991
+ vbox.pack_start(@status_bar, false, false, 0)
992
+
993
+ @filename ||= nil
994
+ if @filename
995
+ data = read_data(@filename)
996
+ view_new_model Editor.data2model(data)
997
+ end
998
+ end
999
+
1000
+ # Creates the menu bar with the pulldown menus and returns it.
1001
+ def create_menu_bar
1002
+ menu_bar = MenuBar.new
1003
+ @file_menu = FileMenu.new(@treeview)
1004
+ menu_bar.append @file_menu.create
1005
+ @edit_menu = EditMenu.new(@treeview)
1006
+ menu_bar.append @edit_menu.create
1007
+ @options_menu = OptionsMenu.new(@treeview)
1008
+ menu_bar.append @options_menu.create
1009
+ menu_bar
1010
+ end
1011
+
1012
+ # Sets editor status to changed, to indicate that the edited data
1013
+ # containts unsaved changes.
1014
+ def change
1015
+ @changed = true
1016
+ display_title
1017
+ end
1018
+
1019
+ # Sets editor status to unchanged, to indicate that the edited data
1020
+ # doesn't containt unsaved changes.
1021
+ def unchange
1022
+ @changed = false
1023
+ display_title
1024
+ end
1025
+
1026
+ # Puts a new model _model_ into the Gtk::TreeView to be edited.
1027
+ def view_new_model(model)
1028
+ @treeview.model = model
1029
+ @treeview.expanded = true
1030
+ @treeview.expand_all
1031
+ unchange
1032
+ end
1033
+
1034
+ # Displays _text_ in the status bar.
1035
+ def display_status(text)
1036
+ @cid ||= nil
1037
+ @status_bar.pop(@cid) if @cid
1038
+ @cid = @status_bar.get_context_id('dummy')
1039
+ @status_bar.push(@cid, text)
1040
+ end
1041
+
1042
+ # Opens a dialog, asking, if changes should be saved to a file.
1043
+ def ask_save
1044
+ if Editor.question_dialog(self,
1045
+ "Unsaved changes to JSON model. Save?")
1046
+ if @filename
1047
+ file_save
1048
+ else
1049
+ file_save_as
1050
+ end
1051
+ end
1052
+ end
1053
+
1054
+ # Quit this editor, that is, leave this editor's main loop.
1055
+ def quit
1056
+ ask_save if @changed
1057
+ destroy
1058
+ Gtk.main_quit
1059
+ true
1060
+ end
1061
+
1062
+ # Display the new title according to the editor's current state.
1063
+ def display_title
1064
+ title = TITLE.dup
1065
+ title << ": #@filename" if @filename
1066
+ title << " *" if @changed
1067
+ self.title = title
1068
+ end
1069
+
1070
+ # Clear the current model, after asking to save all unsaved changes.
1071
+ def clear
1072
+ ask_save if @changed
1073
+ @filename = nil
1074
+ self.view_new_model nil
1075
+ end
1076
+
1077
+ # Open the file _filename_ or call the #select_file method to ask for a
1078
+ # filename.
1079
+ def file_open(filename = nil)
1080
+ filename = select_file('Open as a JSON file') unless filename
1081
+ data = load_file(filename) or return
1082
+ view_new_model Editor.data2model(data)
1083
+ end
1084
+
1085
+ # Save the current file.
1086
+ def file_save
1087
+ if @filename
1088
+ store_file(@filename)
1089
+ else
1090
+ file_save_as
1091
+ end
1092
+ end
1093
+
1094
+ # Save the current file as the filename
1095
+ def file_save_as
1096
+ filename = select_file('Save as a JSON file')
1097
+ store_file(filename)
1098
+ end
1099
+
1100
+ # Store the current JSON document to _path_.
1101
+ def store_file(path)
1102
+ if path
1103
+ data = Editor.model2data(@treeview.model.iter_first)
1104
+ File.open(path + '.tmp', 'wb') do |output|
1105
+ json = if @options_menu.pretty_item.active?
1106
+ JSON.pretty_unparse(data)
1107
+ else
1108
+ JSON.unparse(data)
1109
+ end
1110
+ output.write json
1111
+ end
1112
+ File.rename path + '.tmp', path
1113
+ @filename = path
1114
+ toplevel.display_status("Saved data to '#@filename'.")
1115
+ unchange
1116
+ end
1117
+ rescue SystemCallError => e
1118
+ Editor.error_dialog(self, "Failed to store JSON file: #{e}!")
1119
+ end
1120
+
1121
+ # Load the file named _filename_ into the editor as a JSON document.
1122
+ def load_file(filename)
1123
+ if filename
1124
+ if File.directory?(filename)
1125
+ Editor.error_dialog(self, "Try to select a JSON file!")
1126
+ return
1127
+ else
1128
+ data = read_data(filename)
1129
+ @filename = filename
1130
+ toplevel.display_status("Loaded data from '#@filename'.")
1131
+ display_title
1132
+ return data
1133
+ end
1134
+ end
1135
+ end
1136
+
1137
+ def check_pretty_printed(json)
1138
+ pretty = !!((nl_index = json.index("\n")) && nl_index != json.size - 1)
1139
+ @options_menu.pretty_item.active = pretty
1140
+ end
1141
+ private :check_pretty_printed
1142
+
1143
+ # Read a JSON document from the file named _filename_, parse it into a
1144
+ # ruby data structure, and return the data.
1145
+ def read_data(filename)
1146
+ json = File.read(filename)
1147
+ check_pretty_printed(json)
1148
+ if @encoding && !/^utf8$/i.match(@encoding)
1149
+ iconverter = Iconv.new('utf8', @encoding)
1150
+ json = iconverter.iconv(json)
1151
+ end
1152
+ JSON::parse(json)
1153
+ rescue JSON::JSONError => e
1154
+ Editor.error_dialog(self, "Failed to parse JSON file: #{e}!")
1155
+ return
1156
+ rescue SystemCallError => e
1157
+ quit
1158
+ end
1159
+
1160
+ # Open a file selecton dialog, displaying _message_, and return the
1161
+ # selected filename or nil, if no file was selected.
1162
+ def select_file(message)
1163
+ filename = nil
1164
+ fs = FileSelection.new(message).set_modal(true).
1165
+ set_filename(Dir.pwd + "/").set_transient_for(self)
1166
+ fs.signal_connect(:destroy) { Gtk.main_quit }
1167
+ fs.ok_button.signal_connect(:clicked) do
1168
+ filename = fs.filename
1169
+ fs.destroy
1170
+ Gtk.main_quit
1171
+ end
1172
+ fs.cancel_button.signal_connect(:clicked) do
1173
+ fs.destroy
1174
+ Gtk.main_quit
1175
+ end
1176
+ fs.show_all
1177
+ Gtk.main
1178
+ filename
1179
+ end
1180
+ end
1181
+
1182
+ # Starts a JSON Editor. If a block was given, it yields
1183
+ # to the JSON::Editor::MainWindow instance.
1184
+ def Editor.start(encoding = nil) # :yield: window
1185
+ encoding ||= 'utf8'
1186
+ Gtk.init
1187
+ window = Editor::MainWindow.new(encoding)
1188
+ window.icon_list = [ Editor.fetch_icon('json') ]
1189
+ yield window if block_given?
1190
+ window.show_all
1191
+ Gtk.main
1192
+ end
1193
+ end
1194
+ end
1195
+ # vim: set et sw=2 ts=2: