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