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.
- data/CHANGES +16 -0
- data/Rakefile +80 -75
- data/TODO +1 -1
- data/VERSION +1 -1
- data/bin/edit_json.rb +11 -11
- data/data/prototype.js +2764 -1095
- data/ext/json/ext/generator/generator.c +17 -4
- data/ext/json/ext/parser/parser.c +95 -73
- data/ext/json/ext/parser/parser.rl +34 -12
- data/lib/json.rb +14 -12
- data/lib/json/add/core.rb +2 -1
- data/lib/json/add/rails.rb +6 -0
- data/lib/json/common.rb +9 -0
- data/lib/json/editor.rb +1363 -1293
- data/lib/json/pure/parser.rb +7 -3
- data/lib/json/version.rb +1 -1
- data/tests/runner.rb +2 -0
- data/tests/test_json_addition.rb +19 -0
- data/tests/test_json_rails.rb +114 -0
- metadata +87 -85
@@ -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,
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
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");
|
data/lib/json.rb
CHANGED
@@ -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
|
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
|
-
#
|
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
|
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
|
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
|
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
|
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]}]"
|
data/lib/json/add/core.rb
CHANGED
@@ -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
|
data/lib/json/add/rails.rb
CHANGED
data/lib/json/common.rb
CHANGED
@@ -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
|
data/lib/json/editor.rb
CHANGED
@@ -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", ?
|
461
|
-
add_item("Copy node", ?
|
462
|
-
add_item("Paste node (appending)", ?
|
463
|
-
add_item("Paste node (inserting before)", ?
|
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)", ?
|
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
|
-
#
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
if @
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
#
|
645
|
-
def
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
#
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
@
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
else
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
end
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
dialog
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
dialog
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
dialog.
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
980
|
-
|
981
|
-
|
982
|
-
|
983
|
-
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
|
1040
|
-
|
1041
|
-
|
1042
|
-
|
1043
|
-
|
1044
|
-
|
1045
|
-
|
1046
|
-
|
1047
|
-
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
@
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
1093
|
-
|
1094
|
-
|
1095
|
-
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
#
|
1110
|
-
def
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1123
|
-
|
1124
|
-
def
|
1125
|
-
|
1126
|
-
@
|
1127
|
-
|
1128
|
-
|
1129
|
-
|
1130
|
-
|
1131
|
-
#
|
1132
|
-
def
|
1133
|
-
|
1134
|
-
|
1135
|
-
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1145
|
-
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1149
|
-
|
1150
|
-
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
1154
|
-
|
1155
|
-
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1159
|
-
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
1196
|
-
end
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
1211
|
-
|
1212
|
-
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
1216
|
-
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
|
1225
|
-
|
1226
|
-
|
1227
|
-
|
1228
|
-
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
1232
|
-
|
1233
|
-
|
1234
|
-
|
1235
|
-
|
1236
|
-
|
1237
|
-
|
1238
|
-
|
1239
|
-
|
1240
|
-
|
1241
|
-
|
1242
|
-
|
1243
|
-
|
1244
|
-
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
1249
|
-
|
1250
|
-
|
1251
|
-
|
1252
|
-
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
1260
|
-
|
1261
|
-
|
1262
|
-
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
1268
|
-
|
1269
|
-
|
1270
|
-
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1287
|
-
|
1288
|
-
|
1289
|
-
|
1290
|
-
|
1291
|
-
|
1292
|
-
|
1293
|
-
|
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:
|