weft-qda 0.9.6 → 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/lib/weft.rb +16 -1
  2. data/lib/weft/WEFT-VERSION-STRING.rb +1 -1
  3. data/lib/weft/application.rb +17 -74
  4. data/lib/weft/backend.rb +6 -32
  5. data/lib/weft/backend/sqlite.rb +222 -164
  6. data/lib/weft/backend/sqlite/category_tree.rb +52 -48
  7. data/lib/weft/backend/sqlite/database.rb +57 -0
  8. data/lib/weft/backend/sqlite/upgradeable.rb +7 -0
  9. data/lib/weft/broadcaster.rb +90 -0
  10. data/lib/weft/category.rb +139 -47
  11. data/lib/weft/codereview.rb +160 -0
  12. data/lib/weft/coding.rb +74 -23
  13. data/lib/weft/document.rb +23 -10
  14. data/lib/weft/exceptions.rb +10 -0
  15. data/lib/weft/filters.rb +47 -224
  16. data/lib/weft/filters/indexers.rb +137 -0
  17. data/lib/weft/filters/input.rb +118 -0
  18. data/lib/weft/filters/output.rb +101 -0
  19. data/lib/weft/filters/templates.rb +80 -0
  20. data/lib/weft/filters/win32backtick.rb +246 -0
  21. data/lib/weft/query.rb +169 -0
  22. data/lib/weft/wxgui.rb +349 -294
  23. data/lib/weft/wxgui/constants.rb +43 -0
  24. data/lib/weft/wxgui/controls.rb +6 -0
  25. data/lib/weft/wxgui/controls/category_dropdown.rb +192 -0
  26. data/lib/weft/wxgui/controls/category_tree.rb +314 -0
  27. data/lib/weft/wxgui/controls/document_list.rb +97 -0
  28. data/lib/weft/wxgui/controls/multitype_control.rb +37 -0
  29. data/lib/weft/wxgui/{inspectors → controls}/textcontrols.rb +235 -64
  30. data/lib/weft/wxgui/dialogs.rb +144 -41
  31. data/lib/weft/wxgui/error_handler.rb +116 -36
  32. data/lib/weft/wxgui/exceptions.rb +7 -0
  33. data/lib/weft/wxgui/inspectors.rb +61 -208
  34. data/lib/weft/wxgui/inspectors/category.rb +19 -16
  35. data/lib/weft/wxgui/inspectors/codereview.rb +90 -132
  36. data/lib/weft/wxgui/inspectors/document.rb +12 -8
  37. data/lib/weft/wxgui/inspectors/imagedocument.rb +56 -56
  38. data/lib/weft/wxgui/inspectors/query.rb +284 -0
  39. data/lib/weft/wxgui/inspectors/script.rb +147 -23
  40. data/lib/weft/wxgui/lang/en.rb +69 -0
  41. data/lib/weft/wxgui/sidebar.rb +90 -432
  42. data/lib/weft/wxgui/utilities.rb +70 -91
  43. data/lib/weft/wxgui/workarea.rb +150 -43
  44. data/share/icons/category.ico +0 -0
  45. data/share/icons/category.xpm +109 -0
  46. data/share/icons/codereview.ico +0 -0
  47. data/share/icons/codereview.xpm +54 -0
  48. data/share/icons/d_and_c.xpm +126 -0
  49. data/share/icons/document.ico +0 -0
  50. data/share/icons/document.xpm +70 -0
  51. data/share/icons/project.ico +0 -0
  52. data/share/icons/query.ico +0 -0
  53. data/share/icons/query.xpm +56 -0
  54. data/{lib/weft/wxgui → share/icons}/search.xpm +0 -0
  55. data/share/icons/weft.ico +0 -0
  56. data/share/icons/weft.xpm +62 -0
  57. data/share/icons/weft16.ico +0 -0
  58. data/share/icons/weft32.ico +0 -0
  59. data/share/templates/category_plain.html +18 -0
  60. data/share/templates/codereview_plain.html +18 -0
  61. data/share/templates/document_plain.html +13 -0
  62. data/share/templates/document_plain.txt +7 -0
  63. data/test/001-document.rb +55 -36
  64. data/test/002-category.rb +81 -6
  65. data/test/003-code.rb +8 -4
  66. data/test/004-application.rb +13 -34
  67. data/test/005-query_review.rb +139 -0
  68. data/test/006-filters.rb +54 -42
  69. data/test/007-output_filters.rb +113 -0
  70. data/test/009a-backend_sqlite_basic.rb +95 -24
  71. data/test/009b-backend_sqlite_complex.rb +43 -62
  72. data/test/009c_backend_sqlite_bench.rb +5 -10
  73. data/test/053-doc_inspector.rb +46 -0
  74. data/test/055-query_window.rb +50 -0
  75. data/test/all-tests.rb +1 -0
  76. data/test/test-common.rb +19 -0
  77. data/test/testdata/empty.qdp +0 -0
  78. data/test/testdata/simple with space.pdf +0 -0
  79. data/test/testdata/simple.pdf +0 -0
  80. data/weft-qda.rb +40 -7
  81. metadata +74 -14
  82. data/lib/weft/wxgui/category.xpm +0 -26
  83. data/lib/weft/wxgui/document.xpm +0 -25
  84. data/lib/weft/wxgui/inspectors/search.rb +0 -265
  85. data/lib/weft/wxgui/mondrian.xpm +0 -44
  86. data/lib/weft/wxgui/weft16.xpm +0 -31
@@ -0,0 +1,43 @@
1
+ require 'uri'
2
+
3
+ module QDA
4
+ module GUI
5
+ # save some typing
6
+ DEF_POS = Wx::DEFAULT_POSITION
7
+ DEF_SIZE = Wx::DEFAULT_SIZE
8
+
9
+ # Some versions of WxRuby on OS X seem not to have Wx::RUBY_PLATFORM
10
+ # correctly defined
11
+ if not defined?(Wx::RUBY_PLATFORM) and ::RUBY_PLATFORM =~ /powerpc/
12
+ Wx::RUBY_PLATFORM = 'WXMAC'
13
+ end
14
+
15
+ # What window layout strategy should we use?
16
+ if defined? ::WEFT_MDI_MODE
17
+ WINDOW_MODE = :MDI
18
+ elsif defined? ::WEFT_MULTIFRAME_MODE
19
+ WINDOW_MODE = :MULTIFRAME
20
+ end
21
+
22
+ if defined? ::WEFT_CRASH_REPORTING
23
+ REPORT_CRASHES = true
24
+ CRASH_REPORT_URL = URI.parse('http://www.pressure.to/cgi-bin/weft-crash-report.rb')
25
+ else
26
+ REPORT_CRASHES = false
27
+ end
28
+
29
+ # use windows type icons by preference on windows, otherwise settle for
30
+ # XPMs (which don't do multi-resolution).
31
+ # Also - use MDI style by default on windows, otherwise use
32
+ # multiframe style
33
+ IconType = Struct.new(:extension, :constant)
34
+ if Wx::RUBY_PLATFORM == 'WXMSW'
35
+ ICON_EXTS = [ IconType[ :ico, Wx::BITMAP_TYPE_ICO ] ,
36
+ IconType[ :xpm, Wx::BITMAP_TYPE_XPM ] ]
37
+ WINDOW_MODE = :MDI if not defined?(WINDOW_MODE)
38
+ else
39
+ ICON_EXTS = [ IconType[ :xpm, Wx::BITMAP_TYPE_XPM ] ]
40
+ WINDOW_MODE = :MULTIFRAME if not defined?(WINDOW_MODE)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,6 @@
1
+ # controls are little customised WxGUI widgets that aren't frames or dialogs
2
+ lib_patts = $LOAD_PATH.map { | path | /^#{Regexp.escape(path)}\// }
3
+ Dir.glob( __FILE__.sub(/\.rb$/, File::SEPARATOR + '*.rb' ) ).each do | f |
4
+ lib_patts.find { | x | f.sub!(x, '') }
5
+ require f
6
+ end
@@ -0,0 +1,192 @@
1
+ module QDA::GUI
2
+ class CategoryDropDown < Wx::ComboBox
3
+ include QDA::Subscriber
4
+ include ListLikeItemData
5
+
6
+ MAXIMUM_DROPDOWN_LENGTH = 7
7
+ attr_accessor :sticky
8
+
9
+ # +locked+ if true will prevent the dropdown from responding to
10
+ # global category focus events.
11
+ def initialize(app, parent, text_box = nil, locked = false)
12
+ super(parent, -1, '', Wx::DEFAULT_POSITION,
13
+ Wx::DEFAULT_SIZE, [])
14
+ @locked = locked
15
+ @client_data = {}
16
+ @text_box = text_box
17
+ @sticky = false
18
+
19
+ @app = app
20
+
21
+ evt_kill_focus() do | e |
22
+ find_and_add_categories()
23
+ e.skip()
24
+ end
25
+
26
+ evt_text_enter(self.get_id) do | e |
27
+ find_and_add_categories()
28
+ end
29
+
30
+ evt_combobox(self.get_id) do | e |
31
+ e.skip()
32
+ on_item_selected(e)
33
+ end
34
+
35
+ subscribe(@app, :focus_category, :category_deleted, :category_changed)
36
+ if @app.current_category
37
+ set_active_category(@app.current_category)
38
+ end
39
+ end
40
+
41
+ def redraw()
42
+ delete(0) while count.nonzero?
43
+ data.each_with_index do | item, i |
44
+ append(item.name, nil)
45
+ end
46
+ set_selection(0)
47
+ end
48
+
49
+ def append_item(cat)
50
+ push_item_data(cat)
51
+ append(cat.name, nil)
52
+ end
53
+
54
+ def prepend_item(the_cat)
55
+ unshift_item_data(the_cat)
56
+ redraw()
57
+ end
58
+
59
+ def update_item(the_cat)
60
+ if i = value_to_ident(the_cat)
61
+ set_item_data(i, the_cat)
62
+ redraw()
63
+ end
64
+ end
65
+
66
+ def remove_item(the_cat)
67
+ if i = value_to_ident(the_cat)
68
+ delete(i)
69
+ data.delete_at(i)
70
+ if count.zero?
71
+ set_selection(0)
72
+ end
73
+ end
74
+ end
75
+
76
+ # is this dropdown responding to global :focus_category events?
77
+ def locked?
78
+ @locked ? true : false
79
+ end
80
+
81
+ # make this dropdown receive global :focus_category events
82
+ def lock
83
+ @locked = true
84
+ end
85
+
86
+ # prevent this dropdown responding to global :focus_category events
87
+ def unlock
88
+ @locked = false
89
+ end
90
+
91
+
92
+ # highlight text coded by the category on the way.
93
+ def set_selection(idx)
94
+ super(idx)
95
+ on_item_selected(nil)
96
+ end
97
+
98
+ # highlight text coded by the newly-selected category
99
+ def on_item_selected(e)
100
+ if @text_box
101
+ if category = current_category
102
+ @text_box.highlight_codingtable(category.codes)
103
+ else
104
+ @text_box.unhighlight()
105
+ end
106
+ end
107
+ end
108
+
109
+ def delete_first()
110
+ delete(0)
111
+ data.shift
112
+ end
113
+
114
+ def set_active_category(category)
115
+ # clear the first node unless it is sticky because it has been used,
116
+ # or if the category is already in the list
117
+ delete_first unless @sticky or value_to_ident(category)
118
+
119
+ trim()
120
+ prepend_item(category)
121
+ @sticky = false
122
+ set_selection(0)
123
+ end
124
+
125
+ def trim(to_length = MAXIMUM_DROPDOWN_LENGTH)
126
+ # clear any remaining excess items
127
+ while count > to_length
128
+ data.delete_at(count - 1)
129
+ delete(count - 1)
130
+ end
131
+ end
132
+
133
+ def find_and_add_categories()
134
+ typed_text = get_value()
135
+ return if find_string( typed_text )
136
+ return if typed_text.empty?
137
+ matches = @app.app.get_categories_by_path(typed_text)
138
+ matches.each do | cat |
139
+ prepend_item(cat) unless cat.parent.nil?
140
+ end
141
+ trim(matches.length) if count > MAXIMUM_DROPDOWN_LENGTH
142
+ set_selection( 0 )
143
+ end
144
+
145
+ # add the newly-focused category to this dropdown and highlight
146
+ # its text.
147
+ def receive_focus_category(cat)
148
+ set_active_category(cat) unless locked?
149
+ end
150
+
151
+ # if a category is deleted it should be removed from the list
152
+ def receive_category_deleted(cat)
153
+ remove_item(cat)
154
+ end
155
+
156
+ def receive_category_changed(cat)
157
+ update_item(cat)
158
+ end
159
+
160
+
161
+ def on_blur(e)
162
+ find_and_add_categories()
163
+ e.skip()
164
+ end
165
+
166
+ # missing in wxruby, but part of WxWidgets
167
+ # this version differs by returning nil rather than -1 on failure
168
+ def find_string(str)
169
+ ( 0 ... count ).each do | i |
170
+ return i if str == get_string(i)
171
+ end
172
+ return nil
173
+ end
174
+
175
+
176
+ # TODO - some visual cue to indicate that no category matched find-first
177
+ def set_broken(bool)
178
+
179
+ end
180
+
181
+ def set_warning(bool)
182
+
183
+ end
184
+ # returns the Category object associated with the currently
185
+ # selected item in the drop down.
186
+ def current_category
187
+ if curr_sel = get_selection() and curr_sel >= 0
188
+ return get_client_data( curr_sel )
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,314 @@
1
+ module QDA::GUI
2
+ # the category tree list for the side panel
3
+ class CategoryTree < Wx::TreeCtrl
4
+ CATEGORY_TREE_STYLE = Wx::TR_HIDE_ROOT|Wx::TR_HAS_BUTTONS|
5
+ Wx::TR_LINES_AT_ROOT|Wx::TR_EDIT_LABELS
6
+
7
+ include HashLikeItemData
8
+ include QDA::Subscriber
9
+
10
+ attr_reader :root_id
11
+
12
+ # a +locked+ control is one that is not editable, and does not
13
+ # affect the selection of categories in other widgetes.
14
+ def initialize(weft_client, parent, locked = false)
15
+ @client = weft_client
16
+ @locked = locked
17
+ # a hash whose keys are the dbids which are expanded
18
+ @expanded = {}
19
+ super(parent, -1, Wx::DEFAULT_POSITION, Wx::DEFAULT_SIZE,
20
+ CATEGORY_TREE_STYLE)
21
+ my_id = self.get_id()
22
+ evt_tree_item_activated(my_id) { | e | on_item_activated(e) }
23
+
24
+ if ! @locked
25
+ evt_tree_sel_changed(my_id) { | e | on_item_selected(e) }
26
+ evt_tree_end_drag(my_id) { | e | on_drag_end(e) }
27
+ evt_tree_begin_drag(my_id) { | e | on_drag_begin(e) }
28
+ evt_tree_begin_label_edit(my_id) { | e | on_edit_label_begin(e) }
29
+ evt_tree_end_label_edit(my_id) { | e | on_edit_label_end(e) }
30
+ evt_tree_key_down(my_id) { | e | on_key_down(e) }
31
+ evt_tree_item_expanded(my_id) { | e | on_item_expanded(e) }
32
+ evt_tree_item_collapsed(my_id) { | e | on_item_collapsed(e) }
33
+ end
34
+
35
+ subscribe(@client, :category_deleted, :category_changed, :category_added)
36
+ end
37
+
38
+ # for faked-up item data
39
+ def append_item(parent, text, data = nil)
40
+ id = super(parent, text, -1, -1)
41
+ set_item_data(id, data)
42
+ set_item_bold(id) if parent == @root_id
43
+ id
44
+ end
45
+
46
+ # for faked-up item data
47
+ def prepend_item(parent, text, img = -1, sel_img = -1, data = nil)
48
+ id = super(parent, text, img, sel_img)
49
+ set_item_data(id, data)
50
+ return id
51
+ end
52
+
53
+ # for faked-up item data
54
+ def delete(id)
55
+ super(id)
56
+ del_cat = data.delete(id)
57
+ if del_cat.parent and old_parent_id = value_to_ident(del_cat.parent)
58
+ set_item_data( old_parent_id,
59
+ @client.app.get_category(del_cat.parent.dbid) )
60
+ end
61
+ return nil
62
+ end
63
+
64
+ def populate(children)
65
+ # the root isn't shown, so the top level of +children+ is what
66
+ # appears at the base of the tree.
67
+ @root_id = add_root('ROOT')
68
+ append_recursively(@root_id, children)
69
+ refresh()
70
+ end
71
+
72
+ # returns an array of category ids
73
+ # NOT USED?
74
+ def expanded_items()
75
+ @expanded.keys
76
+ end
77
+
78
+ # Opens the category nodes corresponding to the category ids in +catids+
79
+ def expand_items(catids)
80
+ catids.keys.each do | catid |
81
+ if itemid = value_to_ident(catid)
82
+ # this is done here to prevent it being seen as a change by evt handler
83
+ @expanded[catid] = true
84
+ expand( itemid )
85
+ end
86
+ end
87
+ end
88
+
89
+ def append_recursively(parent, children)
90
+ children.each do | child_cat |
91
+ name = child_cat.name.empty? ? 'DEFAULT' : child_cat.name
92
+
93
+ id = append_item(parent, name, child_cat )
94
+ # remember this for later use
95
+ @search_id = id if name == 'SEARCHES'
96
+ @codes_id = id if name == 'CATEGORIES' or name == 'CODES'
97
+
98
+ append_recursively(id, child_cat.children)
99
+ end
100
+ end
101
+
102
+ # get the currently active category
103
+ def get_current_category()
104
+ if curr_sel = get_selection()
105
+ return nil if curr_sel == 0
106
+ category = get_item_data( curr_sel )
107
+ return nil if category.nil? # important for GTK
108
+ return nil if category.parent.nil? # don't return root nodes
109
+ category
110
+ end
111
+ end
112
+ alias :selected_category :get_current_category
113
+
114
+ def on_item_expanded(evt)
115
+ cat = get_item_data(evt.item)
116
+ return unless cat
117
+ return if @expanded[cat.dbid] # prevents futile re-saving if already open
118
+ @expanded[cat.dbid] = true
119
+ @client.app.save_preference('TreeLayout', @expanded)
120
+ end
121
+
122
+ def on_item_collapsed(evt)
123
+ cat = get_item_data(evt.item)
124
+ return unless cat
125
+ @expanded.delete(cat.dbid)
126
+ @client.app.save_preference('TreeLayout', @expanded)
127
+ end
128
+
129
+ def on_edit_label_begin(evt)
130
+ # TODO - this should really talk to the underlying category,
131
+ # rather than just assuming bold = locked
132
+ evt.veto() if bold?(evt.item)
133
+ end
134
+
135
+ def on_edit_label_end(evt)
136
+ new_text = evt.label
137
+ if new_text == ""
138
+ evt.veto()
139
+ return
140
+ end
141
+
142
+ old_text = get_item_text( evt.item )
143
+ # don't bother saving if the text isn't changed
144
+ return if old_text == new_text
145
+ c_cdata = get_item_data( evt.item )
146
+ category = @client.app.get_category( c_cdata.dbid )
147
+ Wx::BusyCursor.busy do
148
+ begin
149
+ category.name = new_text
150
+ @client.app.save_category( category)
151
+ rescue QDA::BadNameError
152
+ category.name = old_text
153
+ ErrorDialog.display( Lang::BAD_CATEGORY_NAME_TITLE,
154
+ Lang::BAD_CATEGORY_NAME_WARNING )
155
+ evt.veto()
156
+ rescue QDA::NotUniqueNameError
157
+ category.name = old_text
158
+ ErrorDialog.display( Lang::DUPLICATE_CATEGORY_NAME_TITLE,
159
+ Lang::DUPLICATE_CATEGORY_NAME_WARNING )
160
+ evt.veto()
161
+ end
162
+ end
163
+
164
+ set_item_data(evt.item, category )
165
+ end
166
+
167
+ def on_drag_begin(evt)
168
+ @drag_subject = evt.item
169
+ evt.allow()
170
+ end
171
+
172
+ # relocates or merges the coding of the draggee to the drag target
173
+ def on_drag_end(evt)
174
+ @drag_target = evt.item
175
+ # control_down is a property of a mouse event, so this doesn't work
176
+ # p "CONTROL IS DOWN" if evt.control_down
177
+ move(@drag_subject, @drag_target)
178
+
179
+ @drag_subject = nil
180
+ @drag_target = nil
181
+ end
182
+
183
+ def dont_move(from)
184
+ select_item(from) if from != 0
185
+ end
186
+
187
+
188
+ # moves the item identifed by the tree id +from+ to be the last child of
189
+ # the category with the tree id +to+
190
+ def move(from, to)
191
+ return dont_move(from) unless from and to
192
+ return dont_move(from) if from == 0 or to == 0
193
+ return dont_move(from) if from == to
194
+
195
+ movee = get_item_data( from )
196
+ destination = get_item_data( to )
197
+ # don't move root nodes
198
+ return dont_move(from) if not movee.parent
199
+ # ignore if no move
200
+ return dont_move(from) if movee.parent == destination
201
+ # don't attach to descendants
202
+ return dont_move(from) if destination.is_descendant_of?(movee)
203
+ # complain if a child with this name already attached to parent
204
+ if destination[movee.name]
205
+ ErrorDialog.display( Lang::DUPLICATE_CATEGORY_NAME_TITLE,
206
+ Lang::DUPLICATE_CATEGORY_NAME_WARNING )
207
+ return dont_move(from)
208
+ end
209
+
210
+ Wx::BusyCursor.busy do
211
+ movee.parent = destination
212
+ @client.app.save_category( movee )
213
+ end
214
+ end
215
+
216
+ # Moves the tree item identified by +from+ so that it is attached the parent
217
+ # identified by +new_parent+. Does nothing if that is already the location
218
+ # of the node.
219
+ def move_item(from, to)
220
+ # don't alter if unchanged
221
+ return from if get_item_parent(from) == to
222
+ new_child = append_item(to, get_item_text(from), get_item_data(from))
223
+ child = get_first_child(from)[0]
224
+ while child != 0
225
+ move_item(child, new_child)
226
+ child = get_first_child(from)[0]
227
+ end
228
+ delete( from )
229
+ new_child
230
+ end
231
+
232
+ # when we're asked to add an item. This will be attached to the
233
+ # currently selected category in the tree, or the default 'CATEGORIES'
234
+ # category if no item is selected
235
+ def on_create_item()
236
+ @client.on_add_category()
237
+ end
238
+
239
+ def on_item_selected(event)
240
+ if valid_selection?(event)
241
+ @client.current_category = get_item_data( event.get_item )
242
+ else
243
+ @client.current_category = nil
244
+ end
245
+ end
246
+
247
+ def on_item_activated(event)
248
+ item_id = event.get_item()
249
+ category = get_item_data(item_id)
250
+ return nil if category.parent.nil?
251
+ Wx::BusyCursor.busy() do
252
+ category = @client.app.get_category(category.dbid, true)
253
+ if category != nil
254
+ @client.on_category_open(category)
255
+ end
256
+ end
257
+ end
258
+
259
+ def valid_selection?( event )
260
+ # wxruby varies across platforms in how it indicates "no item
261
+ # selected" - on windows, get_selection returns 0, on Linux, a
262
+ # weird large integer id. Returning nil is the "correct" future behaviour
263
+ item_id = event.get_item()
264
+ if item_id.nil? or item_id == 0 or is_bold(item_id)
265
+ return false
266
+ elsif not get_item_data( item_id )
267
+ return false
268
+ else
269
+ return true
270
+ end
271
+ end
272
+
273
+ def delete_selection()
274
+ @client.on_delete_category()
275
+ end
276
+
277
+ def on_key_down(evt)
278
+ case evt.key_code()
279
+ when 127 # DEL
280
+ delete_selection()
281
+ end
282
+ end
283
+
284
+ def receive_category_deleted(cat)
285
+ # may not include this item if it's a partial subtree
286
+ if tree_id = value_to_ident(cat)
287
+ delete( tree_id )
288
+ end
289
+ end
290
+
291
+ def receive_category_changed(cat)
292
+ # may not include this item if it's a partial subtree
293
+ if tree_id = value_to_ident(cat)
294
+ # it's maybe moved
295
+ tree_id = move_item(tree_id, value_to_ident(cat.parent) )
296
+ set_item_text( tree_id, cat.name)
297
+ set_item_data( tree_id, cat)
298
+ end
299
+ end
300
+
301
+ def receive_category_added(cat)
302
+ if cat.parent.nil?
303
+ p_id = @root_id
304
+ else
305
+ p_id = value_to_ident(cat.parent)
306
+ end
307
+ return if not p_id
308
+ append_item( p_id, cat.name, cat)
309
+ expand(p_id)
310
+ id = value_to_ident(cat)
311
+ select_item(id)
312
+ end
313
+ end
314
+ end