fjson 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/TODO ADDED
File without changes
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.4.1
Binary file
@@ -0,0 +1,9 @@
1
+ class Class
2
+ # Returns true, if this class can be used to create an instance
3
+ # from a serialised JSON string. The class has to implement a class
4
+ # method _json_create_ that expects a hash as first parameter, which includes
5
+ # the required data.
6
+ def json_creatable?
7
+ respond_to?(:json_create)
8
+ end
9
+ end
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,19 @@
1
+ module Kernel
2
+ # Outputs _objs_ to STDOUT as JSON strings in the shortest form, that is in
3
+ # one line.
4
+ def j(*objs)
5
+ objs.each do |obj|
6
+ puts JSON::unparse(obj)
7
+ end
8
+ nil
9
+ end
10
+
11
+ # Ouputs _objs_ to STDOUT as JSON strings in a pretty format, with
12
+ # indentation and over many lines.
13
+ def jj(*objs)
14
+ objs.each do |obj|
15
+ puts JSON::pretty_unparse(obj)
16
+ end
17
+ nil
18
+ end
19
+ end
Binary file
Binary file
@@ -0,0 +1,23 @@
1
+ class String
2
+ # Raw Strings are JSON Objects (the raw bytes are stored in an array for the
3
+ # key "raw"). The Ruby String can be created by this class method.
4
+ def self.json_create(o)
5
+ o['raw'].pack('C*')
6
+ end
7
+
8
+ # This method creates a raw object, that can be nested into other data
9
+ # structures and will be unparsed as a raw string.
10
+ def to_json_raw_object
11
+ {
12
+ 'json_class' => self.class.name,
13
+ 'raw' => self.unpack('C*'),
14
+ }
15
+ end
16
+
17
+ # This method should be used, if you want to convert raw strings to JSON
18
+ # instead of UTF-8 strings, e. g. binary data (and JSON Unicode support is
19
+ # enabled).
20
+ def to_json_raw(*args)
21
+ to_json_raw_object.to_json(*args)
22
+ end
23
+ end
Binary file
Binary file
data/lib/fjson.rb ADDED
@@ -0,0 +1,190 @@
1
+ # = json - JSON library for Ruby
2
+ #
3
+ # == Description
4
+ #
5
+ # == Author
6
+ #
7
+ # Brian Takita <mailto:brian.takita@gmail.com>
8
+ # Florian Frank <mailto:flori@ping.de>
9
+ #
10
+ # == License
11
+ #
12
+ # This is free software; you can redistribute it and/or modify it under the
13
+ # terms of the GNU General Public License Version 2 as published by the Free
14
+ # Software Foundation: www.gnu.org/copyleft/gpl.html
15
+ #
16
+ # == Download
17
+ #
18
+ # The latest version of this library can be downloaded at
19
+ #
20
+ # * http://rubyforge.org/frs?group_id=953
21
+ #
22
+ # Online Documentation should be located at
23
+ #
24
+ # * http://fjson.rubyforge.org
25
+ #
26
+ # == Examples
27
+ #
28
+ # To create a JSON string from a ruby data structure, you
29
+ # can call JSON.unparse like that:
30
+ #
31
+ # json = JSON.unparse [1, 2, {"a"=>3.141}, false, true, nil, 4..10]
32
+ # # => "[1,2,{\"a\":3.141},false,true,null,\"4..10\"]"
33
+ #
34
+ # It's also possible to call the #to_json method directly.
35
+ #
36
+ # json = [1, 2, {"a"=>3.141}, false, true, nil, 4..10].to_json
37
+ # # => "[1,2,{\"a\":3.141},false,true,null,\"4..10\"]"
38
+ #
39
+ # To get back a ruby data structure, you have to call
40
+ # JSON.parse on the JSON string:
41
+ #
42
+ # JSON.parse json
43
+ # # => [1, 2, {"a"=>3.141}, false, true, nil, "4..10"]
44
+ #
45
+ # Note, that the range from the original data structure is a simple
46
+ # string now. The reason for this is, that JSON doesn't support ranges
47
+ # or arbitrary classes. In this case the json library falls back to call
48
+ # Object#to_json, which is the same as #to_s.to_json.
49
+ #
50
+ # It's possible to extend JSON to support serialization of arbitray classes by
51
+ # simply implementing a more specialized version of the #to_json method, that
52
+ # should return a JSON object (a hash converted to JSON with #to_json)
53
+ # like this (don't forget the *a for all the arguments):
54
+ #
55
+ # class Range
56
+ # def to_json(*a)
57
+ # {
58
+ # 'json_class' => self.class.name,
59
+ # 'data' => [ first, last, exclude_end? ]
60
+ # }.to_json(*a)
61
+ # end
62
+ # end
63
+ #
64
+ # The hash key 'json_class' is the class, that will be asked to deserialize the
65
+ # JSON representation later. In this case it's 'Range', but any namespace of
66
+ # the form 'A::B' or '::A::B' will do. All other keys are arbitrary and can be
67
+ # used to store the necessary data to configure the object to be deserialized.
68
+ #
69
+ # If a the key 'json_class' is found in a JSON object, the JSON parser checks
70
+ # if the given class responds to the json_create class method. If so, it is
71
+ # called with the JSON object converted to a Ruby hash. So a range can
72
+ # be deserialized by implementing Range.json_create like this:
73
+ #
74
+ # class Range
75
+ # def self.json_create(o)
76
+ # new(*o['data'])
77
+ # end
78
+ # end
79
+ #
80
+ # Now it possible to serialize/deserialize ranges as well:
81
+ #
82
+ # json = JSON.unparse [1, 2, {"a"=>3.141}, false, true, nil, 4..10]
83
+ # # => "[1,2,{\"a\":3.141},false,true,null,{\"json_class\":\"Range\",\"data\":[4,10,false]}]"
84
+ # JSON.parse json
85
+ # # => [1, 2, {"a"=>3.141}, false, true, nil, 4..10]
86
+ #
87
+ # JSON.unparse always creates the shortes possible string representation of a
88
+ # ruby data structure in one line. This good for data storage or network
89
+ # protocols, but not so good for humans to read. Fortunately there's
90
+ # also JSON.pretty_unparse that creates a more readable output:
91
+ #
92
+ # puts JSON.pretty_unparse([1, 2, {"a"=>3.141}, false, true, nil, 4..10])
93
+ # [
94
+ # 1,
95
+ # 2,
96
+ # {
97
+ # "a": 3.141
98
+ # },
99
+ # false,
100
+ # true,
101
+ # null,
102
+ # {
103
+ # "json_class": "Range",
104
+ # "data": [
105
+ # 4,
106
+ # 10,
107
+ # false
108
+ # ]
109
+ # }
110
+ # ]
111
+ #
112
+ # There are also the methods Kernel#j for unparse, and Kernel#jj for
113
+ # pretty_unparse output to the console, that work analogous to Kernel#p and
114
+ # Kernel#pp.
115
+ #
116
+
117
+ require 'strscan'
118
+ dir = File.dirname(__FILE__)
119
+ require File.expand_path("#{dir}/json_ext")
120
+ require File.expand_path("#{dir}/parser")
121
+ require File.expand_path("#{dir}/state_ext")
122
+ require File.expand_path("#{dir}/extensions/object_ext")
123
+ require File.expand_path("#{dir}/extensions/hash_ext")
124
+ require File.expand_path("#{dir}/extensions/array_ext")
125
+ require File.expand_path("#{dir}/extensions/integer_ext")
126
+ require File.expand_path("#{dir}/extensions/float_ext")
127
+ require File.expand_path("#{dir}/extensions/string_ext")
128
+ require File.expand_path("#{dir}/extensions/string")
129
+ require File.expand_path("#{dir}/extensions/true_class_ext")
130
+ require File.expand_path("#{dir}/extensions/false_class_ext")
131
+ require File.expand_path("#{dir}/extensions/nil_class_ext")
132
+ require File.expand_path("#{dir}/extensions/kernel")
133
+ require File.expand_path("#{dir}/extensions/class")
134
+
135
+
136
+ # This module is the namespace for all the JSON related classes. It also
137
+ # defines some module functions to expose a nicer API to users, instead
138
+ # of using the parser and other classes directly.
139
+ module JSON
140
+ module ModuleMethods
141
+ # Switches on Unicode support, if _enable_ is _true_. Otherwise switches
142
+ # Unicode support off.
143
+ def support_unicode=(enable)
144
+ @support_unicode = enable
145
+ end
146
+
147
+ # Returns _true_ if JSON supports unicode, otherwise _false_ is returned.
148
+ def support_unicode?
149
+ !!@support_unicode
150
+ end
151
+
152
+ # Convert _string_ from UTF16 (big endian) encoding to UTF8 encoding and
153
+ # return it.
154
+ def utf16_to_utf8(string)
155
+ bytes = '' << string[0, 2].to_i(16) << string[2, 2].to_i(16)
156
+ JSON::UTF16toUTF8.iconv(bytes)
157
+ end
158
+
159
+ # Parse the JSON string _source_ into a Ruby data structure and return it.
160
+ def parse(source)
161
+ Parser.new(source).parse
162
+ end
163
+
164
+ # Unparse the Ruby data structure _obj_ into a single line JSON string and
165
+ # return it. _state_ is a JSON::State object, that can be used to configure
166
+ # the output further.
167
+ def unparse(obj, state = nil)
168
+ obj.to_json(JSON::State.from_state(state))
169
+ end
170
+
171
+ # Unparse the Ruby data structure _obj_ into a JSON string and return it.
172
+ # The returned string is a prettier form of the string returned by #unparse.
173
+ def pretty_unparse(obj)
174
+ state = JSON::State.new(
175
+ :indent => ' ',
176
+ :space => ' ',
177
+ :object_nl => "\n",
178
+ :array_nl => "\n"
179
+ )
180
+ obj.to_json(state)
181
+ end
182
+ end
183
+ extend ModuleMethods
184
+ include ModuleMethods
185
+ JSON.support_unicode = true # default, hower it's possible to switch off full
186
+ # unicode support, if non-ascii bytes should be
187
+ # just passed through.
188
+ end
189
+
190
+ # vim: set et sw=2 ts=2:
@@ -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: