json 1.1.1 → 1.1.2

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.

@@ -10,8 +10,8 @@
10
10
  static VALUE mJSON, mExt, cParser, eParserError, eNestingError;
11
11
  static VALUE CNaN, CInfinity, CMinusInfinity;
12
12
 
13
- static ID i_json_creatable_p, i_json_create, i_create_id, i_chr, i_max_nesting,
14
- i_allow_nan;
13
+ static ID i_json_creatable_p, i_json_create, i_create_id, i_create_additions,
14
+ i_chr, i_max_nesting, i_allow_nan;
15
15
 
16
16
  #define MinusInfinity "-Infinity"
17
17
 
@@ -113,11 +113,13 @@ static char *JSON_parse_object(JSON_Parser *json, char *p, char *pe, VALUE *resu
113
113
  %% write exec;
114
114
 
115
115
  if (cs >= JSON_object_first_final) {
116
- VALUE klassname = rb_hash_aref(*result, json->create_id);
117
- if (!NIL_P(klassname)) {
118
- VALUE klass = rb_path2class(StringValueCStr(klassname));
119
- if RTEST(rb_funcall(klass, i_json_creatable_p, 0)) {
120
- *result = rb_funcall(klass, i_json_create, 1, *result);
116
+ if (RTEST(json->create_id)) {
117
+ VALUE klassname = rb_hash_aref(*result, json->create_id);
118
+ if (!NIL_P(klassname)) {
119
+ VALUE klass = rb_path2class(StringValueCStr(klassname));
120
+ if RTEST(rb_funcall(klass, i_json_creatable_p, 0)) {
121
+ *result = rb_funcall(klass, i_json_create, 1, *result);
122
+ }
121
123
  }
122
124
  }
123
125
  return p + 1;
@@ -473,6 +475,9 @@ static char *JSON_parse_string(JSON_Parser *json, char *p, char *pe, VALUE *resu
473
475
  * * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
474
476
  * defiance of RFC 4627 to be parsed by the Parser. This option defaults to
475
477
  * false.
478
+ * * *create_additions*: If set to false, the Parser doesn't create
479
+ * additions even if a matchin class and create_id was found. This option
480
+ * defaults to true.
476
481
  */
477
482
  static VALUE cParser_initialize(int argc, VALUE *argv, VALUE self)
478
483
  {
@@ -487,8 +492,6 @@ static VALUE cParser_initialize(int argc, VALUE *argv, VALUE self)
487
492
  if (len < 2) {
488
493
  rb_raise(eParserError, "A JSON text must at least contain two octets!");
489
494
  }
490
- json->max_nesting = 19;
491
- json->allow_nan = 0;
492
495
  if (!NIL_P(opts)) {
493
496
  opts = rb_convert_type(opts, T_HASH, "Hash", "to_hash");
494
497
  if (NIL_P(opts)) {
@@ -503,13 +506,32 @@ static VALUE cParser_initialize(int argc, VALUE *argv, VALUE self)
503
506
  } else {
504
507
  json->max_nesting = 0;
505
508
  }
509
+ } else {
510
+ json->max_nesting = 19;
506
511
  }
507
512
  tmp = ID2SYM(i_allow_nan);
508
513
  if (st_lookup(RHASH(opts)->tbl, tmp, 0)) {
509
514
  VALUE allow_nan = rb_hash_aref(opts, tmp);
510
- if (RTEST(allow_nan)) json->allow_nan = 1;
515
+ json->allow_nan = RTEST(allow_nan) ? 1 : 0;
516
+ } else {
517
+ json->allow_nan = 0;
518
+ }
519
+ tmp = ID2SYM(i_create_additions);
520
+ if (st_lookup(RHASH(opts)->tbl, tmp, 0)) {
521
+ VALUE create_additions = rb_hash_aref(opts, tmp);
522
+ if (RTEST(create_additions)) {
523
+ json->create_id = rb_funcall(mJSON, i_create_id, 0);
524
+ } else {
525
+ json->create_id = Qnil;
526
+ }
527
+ } else {
528
+ json->create_id = rb_funcall(mJSON, i_create_id, 0);
511
529
  }
512
530
  }
531
+ } else {
532
+ json->max_nesting = 19;
533
+ json->allow_nan = 0;
534
+ json->create_id = rb_funcall(mJSON, i_create_id, 0);
513
535
  }
514
536
  json->current_nesting = 0;
515
537
  /*
@@ -527,7 +549,6 @@ static VALUE cParser_initialize(int argc, VALUE *argv, VALUE self)
527
549
  json->len = len;
528
550
  json->source = ptr;
529
551
  json->Vsource = source;
530
- json->create_id = rb_funcall(mJSON, i_create_id, 0);
531
552
  return self;
532
553
  }
533
554
 
@@ -556,7 +577,7 @@ static VALUE cParser_parse(VALUE self)
556
577
  }
557
578
  }
558
579
 
559
- static JSON_Parser *JSON_allocate()
580
+ inline static JSON_Parser *JSON_allocate()
560
581
  {
561
582
  JSON_Parser *json = ALLOC(JSON_Parser);
562
583
  MEMZERO(json, JSON_Parser, 1);
@@ -611,6 +632,7 @@ void Init_parser()
611
632
  i_json_creatable_p = rb_intern("json_creatable?");
612
633
  i_json_create = rb_intern("json_create");
613
634
  i_create_id = rb_intern("create_id");
635
+ i_create_additions = rb_intern("create_additions");
614
636
  i_chr = rb_intern("chr");
615
637
  i_max_nesting = rb_intern("max_nesting");
616
638
  i_allow_nan = rb_intern("allow_nan");
@@ -62,6 +62,13 @@ require 'json/common'
62
62
  # you
63
63
  # require 'json/add/core'
64
64
  #
65
+ # After requiring this you can, e. g., serialise/deserialise Ruby ranges:
66
+ #
67
+ # JSON JSON(1..10) # => 1..10
68
+ #
69
+ # To find out how to add JSON support to other or your own classes, read the
70
+ # Examples section below.
71
+ #
65
72
  # To get the best compatibility to rails' JSON implementation, you can
66
73
  # require 'json/add/rails'
67
74
  #
@@ -125,11 +132,6 @@ require 'json/common'
125
132
  # json = JSON.generate [1, 2, {"a"=>3.141}, false, true, nil, 4..10]
126
133
  # # => "[1,2,{\"a\":3.141},false,true,null,\"4..10\"]"
127
134
  #
128
- # It's also possible to call the #to_json method directly.
129
- #
130
- # json = [1, 2, {"a"=>3.141}, false, true, nil, 4..10].to_json
131
- # # => "[1,2,{\"a\":3.141},false,true,null,\"4..10\"]"
132
- #
133
135
  # To create a valid JSON text you have to make sure, that the output is
134
136
  # embedded in either a JSON array [] or a JSON object {}. The easiest way to do
135
137
  # this, is by putting your values in a Ruby Array or Hash instance.
@@ -145,10 +147,10 @@ require 'json/common'
145
147
  # or arbitrary classes. In this case the json library falls back to call
146
148
  # Object#to_json, which is the same as #to_s.to_json.
147
149
  #
148
- # It's possible to extend JSON to support serialization of arbitrary classes by
150
+ # It's possible to add JSON support serialization to arbitrary classes by
149
151
  # simply implementing a more specialized version of the #to_json method, that
150
- # should return a JSON object (a hash converted to JSON with #to_json)
151
- # like this (don't forget the *a for all the arguments):
152
+ # should return a JSON object (a hash converted to JSON with #to_json) like
153
+ # this (don't forget the *a for all the arguments):
152
154
  #
153
155
  # class Range
154
156
  # def to_json(*a)
@@ -159,15 +161,15 @@ require 'json/common'
159
161
  # end
160
162
  # end
161
163
  #
162
- # The hash key 'json_class' is the class, that will be asked to deserialize the
164
+ # The hash key 'json_class' is the class, that will be asked to deserialise the
163
165
  # JSON representation later. In this case it's 'Range', but any namespace of
164
166
  # the form 'A::B' or '::A::B' will do. All other keys are arbitrary and can be
165
- # used to store the necessary data to configure the object to be deserialized.
167
+ # used to store the necessary data to configure the object to be deserialised.
166
168
  #
167
169
  # If a the key 'json_class' is found in a JSON object, the JSON parser checks
168
170
  # if the given class responds to the json_create class method. If so, it is
169
171
  # called with the JSON object converted to a Ruby hash. So a range can
170
- # be deserialized by implementing Range.json_create like this:
172
+ # be deserialised by implementing Range.json_create like this:
171
173
  #
172
174
  # class Range
173
175
  # def self.json_create(o)
@@ -175,7 +177,7 @@ require 'json/common'
175
177
  # end
176
178
  # end
177
179
  #
178
- # Now it possible to serialize/deserialize ranges as well:
180
+ # Now it possible to serialise/deserialise ranges as well:
179
181
  #
180
182
  # json = JSON.generate [1, 2, {"a"=>3.141}, false, true, nil, 4..10]
181
183
  # # => "[1,2,{\"a\":3.141},false,true,null,{\"json_class\":\"Range\",\"data\":[4,10,false]}]"
@@ -5,6 +5,7 @@ unless Object.const_defined?(:JSON) and ::JSON.const_defined?(:JSON_LOADED) and
5
5
  ::JSON::JSON_LOADED
6
6
  require 'json'
7
7
  end
8
+ require 'date'
8
9
 
9
10
  class Time
10
11
  def self.json_create(object)
@@ -54,7 +55,7 @@ class DateTime
54
55
  'H' => hour,
55
56
  'M' => min,
56
57
  'S' => sec,
57
- 'of' => offset,
58
+ 'of' => offset.to_s,
58
59
  'sg' => sg,
59
60
  }.to_json(*args)
60
61
  end
@@ -28,6 +28,12 @@ class Object
28
28
  end
29
29
  end
30
30
 
31
+ class Symbol
32
+ def to_json(*a)
33
+ to_s.to_json(*a)
34
+ end
35
+ end
36
+
31
37
  module Enumerable
32
38
  def to_json(*a)
33
39
  to_a.to_json(*a)
@@ -115,6 +115,9 @@ module JSON
115
115
  # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
116
116
  # defiance of RFC 4627 to be parsed by the Parser. This option defaults
117
117
  # to false.
118
+ # * *create_additions*: If set to false, the Parser doesn't create
119
+ # additions even if a matchin class and create_id was found. This option
120
+ # defaults to true.
118
121
  def parse(source, opts = {})
119
122
  JSON.parser.new(source, opts).parse
120
123
  end
@@ -131,6 +134,9 @@ module JSON
131
134
  # * *allow_nan*: If set to true, allow NaN, Infinity, and -Infinity in
132
135
  # defiance of RFC 4627 to be parsed by the Parser. This option defaults
133
136
  # to true.
137
+ # * *create_additions*: If set to false, the Parser doesn't create
138
+ # additions even if a matchin class and create_id was found. This option
139
+ # defaults to true.
134
140
  def parse!(source, opts = {})
135
141
  opts = {
136
142
  :max_nesting => false,
@@ -161,6 +167,9 @@ module JSON
161
167
  # * *allow_nan*: true if NaN, Infinity, and -Infinity should be
162
168
  # generated, otherwise an exception is thrown, if these values are
163
169
  # encountered. This options defaults to false.
170
+ # * *max_nesting*: The maximum depth of nesting allowed in the data
171
+ # structures from which JSON is to be generated. Disable depth checking
172
+ # with :max_nesting => false, it defaults to 19.
164
173
  #
165
174
  # See also the fast_generate for the fastest creation method with the least
166
175
  # amount of sanity checks, and the pretty_generate method for some
@@ -1,1293 +1,1363 @@
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)", ?v, &method(:paste_node_appending))
463
- add_item("Paste node (inserting before)", ?V,
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)", ?C,
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
- # Find a string in all nodes' contents and select the found node in the
548
- # treeview.
549
- def find(item)
550
- search = ask_for_find_term or return
551
- begin
552
- @search = Regexp.new(search)
553
- rescue => e
554
- Editor.error_dialog(self, "Evaluation of regex /#{search}/ failed: #{e}!")
555
- return
556
- end
557
- iter = model.get_iter('0')
558
- iter.recursive_each do |i|
559
- if @iter
560
- if @iter != i
561
- next
562
- else
563
- @iter = nil
564
- next
565
- end
566
- elsif @search.match(i[CONTENT_COL])
567
- set_cursor(i.path, nil, false)
568
- @iter = i
569
- break
570
- end
571
- end
572
- end
573
-
574
- # Repeat the last search given by #find.
575
- def find_again(item)
576
- @search or return
577
- iter = model.get_iter('0')
578
- iter.recursive_each do |i|
579
- if @iter
580
- if @iter != i
581
- next
582
- else
583
- @iter = nil
584
- next
585
- end
586
- elsif @search.match(i[CONTENT_COL])
587
- set_cursor(i.path, nil, false)
588
- @iter = i
589
- break
590
- end
591
- end
592
- end
593
-
594
- # Sort (Reverse sort) all elements of the selected array by the given
595
- # expression. _x_ is the element in question.
596
- def sort(item)
597
- if current = selection.selected
598
- if current.type == 'Array'
599
- parent = current.parent
600
- ary = Editor.model2data(current)
601
- order, reverse = ask_for_order
602
- order or return
603
- begin
604
- block = eval "lambda { |x| #{order} }"
605
- if reverse
606
- ary.sort! { |a,b| block[b] <=> block[a] }
607
- else
608
- ary.sort! { |a,b| block[a] <=> block[b] }
609
- end
610
- rescue => e
611
- Editor.error_dialog(self, "Failed to sort Array with #{order}: #{e}!")
612
- else
613
- Editor.data2model(ary, model, parent) do |m|
614
- m.insert_before(parent, current)
615
- end
616
- model.remove(current)
617
- expand_collapse(parent)
618
- window.change
619
- toplevel.display_status("Array has been sorted.")
620
- end
621
- else
622
- toplevel.display_status("Only Array nodes can be sorted!")
623
- end
624
- else
625
- toplevel.display_status("Select an Array to sort first!")
626
- end
627
- end
628
-
629
- # Create the menu.
630
- def create
631
- title = MenuItem.new('Edit')
632
- title.submenu = menu
633
- add_item('Find', ?f, &method(:find))
634
- add_item('Find Again', ?g, &method(:find_again))
635
- add_separator
636
- add_item('Sort', ?S, &method(:sort))
637
- title
638
- end
639
- end
640
-
641
- class OptionsMenu
642
- include MenuExtension
643
-
644
- # Collapse/Expand all nodes by default.
645
- def collapsed_nodes(item)
646
- if expanded
647
- self.expanded = false
648
- collapse_all
649
- else
650
- self.expanded = true
651
- expand_all
652
- end
653
- end
654
-
655
- # Toggle pretty saving mode on/off.
656
- def pretty_saving(item)
657
- @pretty_item.toggled
658
- window.change
659
- end
660
-
661
- attr_reader :pretty_item
662
-
663
- # Create the menu.
664
- def create
665
- title = MenuItem.new('Options')
666
- title.submenu = menu
667
- add_item('Collapsed nodes', nil, CheckMenuItem, &method(:collapsed_nodes))
668
- @pretty_item = add_item('Pretty saving', nil, CheckMenuItem,
669
- &method(:pretty_saving))
670
- @pretty_item.active = true
671
- window.unchange
672
- title
673
- end
674
- end
675
-
676
- # This class inherits from Gtk::TreeView, to configure it and to add a lot
677
- # of behaviour to it.
678
- class JSONTreeView < Gtk::TreeView
679
- include Gtk
680
-
681
- # Creates a JSONTreeView instance, the parameter _window_ is
682
- # a MainWindow instance and used for self delegation.
683
- def initialize(window)
684
- @window = window
685
- super(TreeStore.new(Gdk::Pixbuf, String, String))
686
- self.selection.mode = SELECTION_BROWSE
687
-
688
- @expanded = false
689
- self.headers_visible = false
690
- add_columns
691
- add_popup_menu
692
- end
693
-
694
- # Returns the MainWindow instance of this JSONTreeView.
695
- attr_reader :window
696
-
697
- # Returns true, if nodes are autoexpanding, false otherwise.
698
- attr_accessor :expanded
699
-
700
- private
701
-
702
- def add_columns
703
- cell = CellRendererPixbuf.new
704
- column = TreeViewColumn.new('Icon', cell,
705
- 'pixbuf' => ICON_COL
706
- )
707
- append_column(column)
708
-
709
- cell = CellRendererText.new
710
- column = TreeViewColumn.new('Type', cell,
711
- 'text' => TYPE_COL
712
- )
713
- append_column(column)
714
-
715
- cell = CellRendererText.new
716
- cell.editable = true
717
- column = TreeViewColumn.new('Content', cell,
718
- 'text' => CONTENT_COL
719
- )
720
- cell.signal_connect(:edited, &method(:cell_edited))
721
- append_column(column)
722
- end
723
-
724
- def unify_key(iter, key)
725
- return unless iter.type == 'Key'
726
- parent = iter.parent
727
- if parent.any? { |c| c != iter and c.content == key }
728
- old_key = key
729
- i = 0
730
- begin
731
- key = sprintf("%s.%d", old_key, i += 1)
732
- end while parent.any? { |c| c != iter and c.content == key }
733
- end
734
- iter.content = key
735
- end
736
-
737
- def cell_edited(cell, path, value)
738
- iter = model.get_iter(path)
739
- case iter.type
740
- when 'Key'
741
- unify_key(iter, value)
742
- toplevel.display_status('Key has been changed.')
743
- when 'FalseClass'
744
- value.downcase!
745
- if value == 'true'
746
- iter.type, iter.content = 'TrueClass', 'true'
747
- end
748
- when 'TrueClass'
749
- value.downcase!
750
- if value == 'false'
751
- iter.type, iter.content = 'FalseClass', 'false'
752
- end
753
- when 'Numeric'
754
- iter.content = (Integer(value) rescue Float(value) rescue 0).to_s
755
- when 'String'
756
- iter.content = value
757
- when 'Hash', 'Array'
758
- return
759
- else
760
- fail "Unknown type found in model: #{iter.type}"
761
- end
762
- window.change
763
- end
764
-
765
- def configure_value(value, type)
766
- value.editable = false
767
- case type
768
- when 'Array', 'Hash'
769
- value.text = ''
770
- when 'TrueClass'
771
- value.text = 'true'
772
- when 'FalseClass'
773
- value.text = 'false'
774
- when 'NilClass'
775
- value.text = 'null'
776
- when 'Numeric', 'String'
777
- value.text ||= ''
778
- value.editable = true
779
- else
780
- raise ArgumentError, "unknown type '#{type}' encountered"
781
- end
782
- end
783
-
784
- def add_popup_menu
785
- menu = PopUpMenu.new(self)
786
- menu.create
787
- end
788
-
789
- public
790
-
791
- # Create a _type_ node with content _content_, and add it to _parent_
792
- # in the model. If _parent_ is nil, create a new model and put it into
793
- # the editor treeview.
794
- def create_node(parent, type, content)
795
- iter = if parent
796
- model.append(parent)
797
- else
798
- new_model = Editor.data2model(nil)
799
- toplevel.view_new_model(new_model)
800
- new_model.iter_first
801
- end
802
- iter.type, iter.content = type, content
803
- expand_collapse(parent) if parent
804
- iter
805
- end
806
-
807
- # Ask for a hash key, value pair to be added to the Hash node _parent_.
808
- def ask_for_hash_pair(parent)
809
- key_input = type_input = value_input = nil
810
-
811
- dialog = Dialog.new("New (key, value) pair for Hash", nil, nil,
812
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
813
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
814
- )
815
-
816
- hbox = HBox.new(false, 5)
817
- hbox.pack_start(Label.new("Key:"))
818
- hbox.pack_start(key_input = Entry.new)
819
- key_input.text = @key || ''
820
- dialog.vbox.add(hbox)
821
- key_input.signal_connect(:activate) do
822
- if parent.any? { |c| c.content == key_input.text }
823
- toplevel.display_status('Key already exists in Hash!')
824
- key_input.text = ''
825
- else
826
- toplevel.display_status('Key has been changed.')
827
- end
828
- end
829
-
830
- hbox = HBox.new(false, 5)
831
- hbox.add(Label.new("Type:"))
832
- hbox.pack_start(type_input = ComboBox.new(true))
833
- ALL_TYPES.each { |t| type_input.append_text(t) }
834
- type_input.active = @type || 0
835
- dialog.vbox.add(hbox)
836
-
837
- type_input.signal_connect(:changed) do
838
- value_input.editable = false
839
- case ALL_TYPES[type_input.active]
840
- when 'Array', 'Hash'
841
- value_input.text = ''
842
- when 'TrueClass'
843
- value_input.text = 'true'
844
- when 'FalseClass'
845
- value_input.text = 'false'
846
- when 'NilClass'
847
- value_input.text = 'null'
848
- else
849
- value_input.text = ''
850
- value_input.editable = true
851
- end
852
- end
853
-
854
- hbox = HBox.new(false, 5)
855
- hbox.add(Label.new("Value:"))
856
- hbox.pack_start(value_input = Entry.new)
857
- value_input.text = @value || ''
858
- dialog.vbox.add(hbox)
859
-
860
- dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
861
- dialog.show_all
862
- self.focus = dialog
863
- dialog.run do |response|
864
- if response == Dialog::RESPONSE_ACCEPT
865
- @key = key_input.text
866
- type = ALL_TYPES[@type = type_input.active]
867
- content = value_input.text
868
- return @key, type, content
869
- end
870
- end
871
- return
872
- ensure
873
- dialog.destroy
874
- end
875
-
876
- # Ask for an element to be appended _parent_.
877
- def ask_for_element(parent = nil, default_type = nil, value_text = @content)
878
- type_input = value_input = nil
879
-
880
- dialog = Dialog.new(
881
- "New element into #{parent ? parent.type : 'root'}",
882
- nil, nil,
883
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
884
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
885
- )
886
- hbox = HBox.new(false, 5)
887
- hbox.add(Label.new("Type:"))
888
- hbox.pack_start(type_input = ComboBox.new(true))
889
- default_active = 0
890
- types = parent ? ALL_TYPES : CONTAINER_TYPES
891
- types.each_with_index do |t, i|
892
- type_input.append_text(t)
893
- if t == default_type
894
- default_active = i
895
- end
896
- end
897
- type_input.active = default_active
898
- dialog.vbox.add(hbox)
899
- type_input.signal_connect(:changed) do
900
- configure_value(value_input, types[type_input.active])
901
- end
902
-
903
- hbox = HBox.new(false, 5)
904
- hbox.add(Label.new("Value:"))
905
- hbox.pack_start(value_input = Entry.new)
906
- value_input.text = value_text if value_text
907
- configure_value(value_input, types[type_input.active])
908
-
909
- dialog.vbox.add(hbox)
910
-
911
- dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
912
- dialog.show_all
913
- self.focus = dialog
914
- dialog.run do |response|
915
- if response == Dialog::RESPONSE_ACCEPT
916
- type = types[type_input.active]
917
- @content = case type
918
- when 'Numeric'
919
- Integer(value_input.text) rescue Float(value_input.text) rescue 0
920
- else
921
- value_input.text
922
- end.to_s
923
- return type, @content
924
- end
925
- end
926
- return
927
- ensure
928
- dialog.destroy if dialog
929
- end
930
-
931
- # Ask for an order criteria for sorting, using _x_ for the element in
932
- # question. Returns the order criterium, and true/false for reverse
933
- # sorting.
934
- def ask_for_order
935
- dialog = Dialog.new(
936
- "Give an order criterium for 'x'.",
937
- nil, nil,
938
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
939
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
940
- )
941
- hbox = HBox.new(false, 5)
942
-
943
- hbox.add(Label.new("Order:"))
944
- hbox.pack_start(order_input = Entry.new)
945
- order_input.text = @order || 'x'
946
-
947
- hbox.pack_start(reverse_checkbox = CheckButton.new('Reverse'))
948
-
949
- dialog.vbox.add(hbox)
950
-
951
- dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
952
- dialog.show_all
953
- self.focus = dialog
954
- dialog.run do |response|
955
- if response == Dialog::RESPONSE_ACCEPT
956
- return @order = order_input.text, reverse_checkbox.active?
957
- end
958
- end
959
- return
960
- ensure
961
- dialog.destroy if dialog
962
- end
963
-
964
- # Ask for a find term to search for in the tree. Returns the term as a
965
- # string.
966
- def ask_for_find_term
967
- dialog = Dialog.new(
968
- "Find a node matching regex in tree.",
969
- nil, nil,
970
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
971
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
972
- )
973
- hbox = HBox.new(false, 5)
974
-
975
- hbox.add(Label.new("Regex:"))
976
- hbox.pack_start(regex_input = Entry.new)
977
- regex_input.text = @regex || ''
978
-
979
- dialog.vbox.add(hbox)
980
-
981
- dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
982
- dialog.show_all
983
- self.focus = dialog
984
- dialog.run do |response|
985
- if response == Dialog::RESPONSE_ACCEPT
986
- return @regex = regex_input.text
987
- end
988
- end
989
- return
990
- ensure
991
- dialog.destroy if dialog
992
- end
993
-
994
- # Expand or collapse row pointed to by _iter_ according
995
- # to the #expanded attribute.
996
- def expand_collapse(iter)
997
- if expanded
998
- expand_row(iter.path, true)
999
- else
1000
- collapse_row(iter.path)
1001
- end
1002
- end
1003
- end
1004
-
1005
- # The editor main window
1006
- class MainWindow < Gtk::Window
1007
- include Gtk
1008
-
1009
- def initialize(encoding)
1010
- @changed = false
1011
- @encoding = encoding
1012
- super(TOPLEVEL)
1013
- display_title
1014
- set_default_size(800, 600)
1015
- signal_connect(:delete_event) { quit }
1016
-
1017
- vbox = VBox.new(false, 0)
1018
- add(vbox)
1019
- #vbox.border_width = 0
1020
-
1021
- @treeview = JSONTreeView.new(self)
1022
- @treeview.signal_connect(:'cursor-changed') do
1023
- display_status('')
1024
- end
1025
-
1026
- menu_bar = create_menu_bar
1027
- vbox.pack_start(menu_bar, false, false, 0)
1028
-
1029
- sw = ScrolledWindow.new(nil, nil)
1030
- sw.shadow_type = SHADOW_ETCHED_IN
1031
- sw.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
1032
- vbox.pack_start(sw, true, true, 0)
1033
- sw.add(@treeview)
1034
-
1035
- @status_bar = Statusbar.new
1036
- vbox.pack_start(@status_bar, false, false, 0)
1037
-
1038
- @filename ||= nil
1039
- if @filename
1040
- data = read_data(@filename)
1041
- view_new_model Editor.data2model(data)
1042
- end
1043
- end
1044
-
1045
- # Creates the menu bar with the pulldown menus and returns it.
1046
- def create_menu_bar
1047
- menu_bar = MenuBar.new
1048
- @file_menu = FileMenu.new(@treeview)
1049
- menu_bar.append @file_menu.create
1050
- @edit_menu = EditMenu.new(@treeview)
1051
- menu_bar.append @edit_menu.create
1052
- @options_menu = OptionsMenu.new(@treeview)
1053
- menu_bar.append @options_menu.create
1054
- menu_bar
1055
- end
1056
-
1057
- # Sets editor status to changed, to indicate that the edited data
1058
- # containts unsaved changes.
1059
- def change
1060
- @changed = true
1061
- display_title
1062
- end
1063
-
1064
- # Sets editor status to unchanged, to indicate that the edited data
1065
- # doesn't containt unsaved changes.
1066
- def unchange
1067
- @changed = false
1068
- display_title
1069
- end
1070
-
1071
- # Puts a new model _model_ into the Gtk::TreeView to be edited.
1072
- def view_new_model(model)
1073
- @treeview.model = model
1074
- @treeview.expanded = true
1075
- @treeview.expand_all
1076
- unchange
1077
- end
1078
-
1079
- # Displays _text_ in the status bar.
1080
- def display_status(text)
1081
- @cid ||= nil
1082
- @status_bar.pop(@cid) if @cid
1083
- @cid = @status_bar.get_context_id('dummy')
1084
- @status_bar.push(@cid, text)
1085
- end
1086
-
1087
- # Opens a dialog, asking, if changes should be saved to a file.
1088
- def ask_save
1089
- if Editor.question_dialog(self,
1090
- "Unsaved changes to JSON model. Save?")
1091
- if @filename
1092
- file_save
1093
- else
1094
- file_save_as
1095
- end
1096
- end
1097
- end
1098
-
1099
- # Quit this editor, that is, leave this editor's main loop.
1100
- def quit
1101
- ask_save if @changed
1102
- if Gtk.main_level > 0
1103
- destroy
1104
- Gtk.main_quit
1105
- end
1106
- nil
1107
- end
1108
-
1109
- # Display the new title according to the editor's current state.
1110
- def display_title
1111
- title = TITLE.dup
1112
- title << ": #@filename" if @filename
1113
- title << " *" if @changed
1114
- self.title = title
1115
- end
1116
-
1117
- # Clear the current model, after asking to save all unsaved changes.
1118
- def clear
1119
- ask_save if @changed
1120
- @filename = nil
1121
- self.view_new_model nil
1122
- end
1123
-
1124
- def check_pretty_printed(json)
1125
- pretty = !!((nl_index = json.index("\n")) && nl_index != json.size - 1)
1126
- @options_menu.pretty_item.active = pretty
1127
- end
1128
- private :check_pretty_printed
1129
-
1130
- # Open the data at the location _uri_, if given. Otherwise open a dialog
1131
- # to ask for the _uri_.
1132
- def location_open(uri = nil)
1133
- uri = ask_for_location unless uri
1134
- uri or return
1135
- data = load_location(uri) or return
1136
- view_new_model Editor.data2model(data)
1137
- end
1138
-
1139
- # Open the file _filename_ or call the #select_file method to ask for a
1140
- # filename.
1141
- def file_open(filename = nil)
1142
- filename = select_file('Open as a JSON file') unless filename
1143
- data = load_file(filename) or return
1144
- view_new_model Editor.data2model(data)
1145
- end
1146
-
1147
- # Save the current file.
1148
- def file_save
1149
- if @filename
1150
- store_file(@filename)
1151
- else
1152
- file_save_as
1153
- end
1154
- end
1155
-
1156
- # Save the current file as the filename
1157
- def file_save_as
1158
- filename = select_file('Save as a JSON file')
1159
- store_file(filename)
1160
- end
1161
-
1162
- # Store the current JSON document to _path_.
1163
- def store_file(path)
1164
- if path
1165
- data = Editor.model2data(@treeview.model.iter_first)
1166
- File.open(path + '.tmp', 'wb') do |output|
1167
- if @options_menu.pretty_item.active?
1168
- output.puts JSON.pretty_generate(data)
1169
- else
1170
- output.write JSON.unparse(data)
1171
- end
1172
- end
1173
- File.rename path + '.tmp', path
1174
- @filename = path
1175
- toplevel.display_status("Saved data to '#@filename'.")
1176
- unchange
1177
- end
1178
- rescue SystemCallError => e
1179
- Editor.error_dialog(self, "Failed to store JSON file: #{e}!")
1180
- end
1181
-
1182
- # Load the file named _filename_ into the editor as a JSON document.
1183
- def load_file(filename)
1184
- if filename
1185
- if File.directory?(filename)
1186
- Editor.error_dialog(self, "Try to select a JSON file!")
1187
- nil
1188
- else
1189
- @filename = filename
1190
- if data = read_data(filename)
1191
- toplevel.display_status("Loaded data from '#@filename'.")
1192
- end
1193
- display_title
1194
- data
1195
- end
1196
- end
1197
- end
1198
-
1199
- # Load the data at location _uri_ into the editor as a JSON document.
1200
- def load_location(uri)
1201
- data = read_data(uri) or return
1202
- @filename = nil
1203
- toplevel.display_status("Loaded data from '#{uri}'.")
1204
- display_title
1205
- data
1206
- end
1207
-
1208
- # Read a JSON document from the file named _filename_, parse it into a
1209
- # ruby data structure, and return the data.
1210
- def read_data(filename)
1211
- open(filename) do |f|
1212
- json = f.read
1213
- check_pretty_printed(json)
1214
- if @encoding && !/^utf8$/i.match(@encoding)
1215
- iconverter = Iconv.new('utf8', @encoding)
1216
- json = iconverter.iconv(json)
1217
- end
1218
- return JSON::parse(json, :max_nesting => false)
1219
- end
1220
- rescue => e
1221
- Editor.error_dialog(self, "Failed to parse JSON file: #{e}!")
1222
- return
1223
- end
1224
-
1225
- # Open a file selecton dialog, displaying _message_, and return the
1226
- # selected filename or nil, if no file was selected.
1227
- def select_file(message)
1228
- filename = nil
1229
- fs = FileSelection.new(message).set_modal(true).
1230
- set_filename(Dir.pwd + "/").set_transient_for(self)
1231
- fs.signal_connect(:destroy) { Gtk.main_quit }
1232
- fs.ok_button.signal_connect(:clicked) do
1233
- filename = fs.filename
1234
- fs.destroy
1235
- Gtk.main_quit
1236
- end
1237
- fs.cancel_button.signal_connect(:clicked) do
1238
- fs.destroy
1239
- Gtk.main_quit
1240
- end
1241
- fs.show_all
1242
- Gtk.main
1243
- filename
1244
- end
1245
-
1246
- # Ask for location URI a to load data from. Returns the URI as a string.
1247
- def ask_for_location
1248
- dialog = Dialog.new(
1249
- "Load data from location...",
1250
- nil, nil,
1251
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
1252
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
1253
- )
1254
- hbox = HBox.new(false, 5)
1255
-
1256
- hbox.add(Label.new("Location:"))
1257
- hbox.pack_start(location_input = Entry.new)
1258
- location_input.width_chars = 60
1259
- location_input.text = @location || ''
1260
-
1261
- dialog.vbox.add(hbox)
1262
-
1263
- dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
1264
- dialog.show_all
1265
- dialog.run do |response|
1266
- if response == Dialog::RESPONSE_ACCEPT
1267
- return @location = location_input.text
1268
- end
1269
- end
1270
- return
1271
- ensure
1272
- dialog.destroy if dialog
1273
- end
1274
- end
1275
-
1276
- class << self
1277
- # Starts a JSON Editor. If a block was given, it yields
1278
- # to the JSON::Editor::MainWindow instance.
1279
- def start(encoding = nil) # :yield: window
1280
- encoding ||= 'utf8'
1281
- Gtk.init
1282
- @window = Editor::MainWindow.new(encoding)
1283
- @window.icon_list = [ Editor.fetch_icon('json') ]
1284
- yield @window if block_given?
1285
- @window.show_all
1286
- Gtk.main
1287
- end
1288
-
1289
- attr_reader :window
1290
- end
1291
- end
1292
- end
1293
- # vim: set et sw=2 ts=2:
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
1363
+ # vim: set et sw=2 ts=2: