fire_and_forget 0.2.0 → 0.3.0

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