~ubuntu-branches/ubuntu/trusty/ruby1.9/trusty

« back to all changes in this revision

Viewing changes to lib/json/editor.rb

  • Committer: Bazaar Package Importer
  • Author(s): Stephan Hermann
  • Date: 2008-01-24 11:42:29 UTC
  • mfrom: (1.1.9 upstream)
  • Revision ID: james.westby@ubuntu.com-20080124114229-jw2f87rdxlq6gp11
Tags: 1.9.0.0-2ubuntu1
* Merge from debian unstable, remaining changes:
  - Robustify check for target_os, fixing build failure on lpia.

Show diffs side-by-side

added added

removed removed

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