Merge lp:~spud/spud/unused-schemas into lp:spud
- unused-schemas
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 483 |
Proposed branch: | lp:~spud/spud/unused-schemas |
Merge into: | lp:spud |
Diff against target: |
416 lines (+291/-20) 7 files modified
diamond/diamond/choice.py (+4/-0) diamond/diamond/interface.py (+5/-0) diamond/diamond/schema.py (+11/-18) diamond/diamond/schemauseage.py (+102/-0) diamond/diamond/tree.py (+5/-1) diamond/diamond/useview.py (+156/-0) diamond/gui/gui.glade (+8/-1) |
To merge this branch: | bzr merge lp:~spud/spud/unused-schemas |
Related bugs: | |
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Simon Funke | Approve | ||
Patrick Farrell | Pending | ||
Review via email:
|
Commit message
Description of the change
Compare multiple xml files to one schema to determine what parts of the schema are not used.
- 477. By Fraser Waters
-
Bug fix and don't show 'comments'

Patrick Farrell (pefarrell) wrote : | # |
Fraser: I have a few comments.
Firstly, 'useage' should be spelled 'usage'.
Secondly, the "Input list" on the top of the gui window is a bit confusing: a list of what?
Thirdly, I expected it to open a directory to find all the flml files beneath, but I can't seem to do that: I can only open a file.
Fourth, it takes a very long time: I suspect that this is some algorithmic flaw. To be honest, I can't see any theoretical reason why this shouldn't take more than, say, 10 seconds; if it's taking longer than this it seems to me that we have the wrong algorithm implemented. Even if it does take very long, the UI freezes while it is working, which will lead users to think that diamond has crashed.
Fifth, can you please make it remember which directory you opened before, instead of defaulting to the cwd?
Finally, I ran it on 1material_
Traceback (most recent call last):
File "/usr/lib/
useview.
File "/usr/lib/
self.
File "/usr/lib/
self.
File "/usr/lib/
tree = schema.read(path)
File "/usr/lib/
doc = etree.parse(
File "lxml.etree.pyx", line 2942, in lxml.etree.parse (src/lxml/
File "parser.pxi", line 1528, in lxml.etree.
File "parser.pxi", line 1557, in lxml.etree.
File "parser.pxi", line 1457, in lxml.etree.
File "parser.pxi", line 997, in lxml.etree.
File "parser.pxi", line 564, in lxml.etree.
File "parser.pxi", line 645, in lxml.etree.
File "parser.pxi", line 583, in lxml.etree.
IOError: Error reading file '<?xml version='1.0' encoding=

Patrick Farrell (pefarrell) wrote : | # |
Simon: "The diff feature was successfully tested against all flml's in the fluidity test directory. "
The diff feature has nothing to do with this feature/merge request?

Fraser Waters (fraser-waters08) wrote : | # |
Currently (and this is in the manual) the input file its asking for is a file with a list of paths to check. We figured this is slightly more adaptable than just selecting every .suffix file, but that can be done as well if needs be.
Question four, the algorithm is pretty simple. Make a set of every element in the schema, then open each file and for every element in that file remove it from the set.
(This is slight more efficent than building a new set from each file and than calculating set differences) I can't think of any way this can be improved we HAVE to look at every schema element and we HAVE to look at every element in every file.
The UI freeze is also mentioned in the manual. We'll have to pull that off to another thread and then callback to UI to fix that, can probably be done but I'll have to look up how to do threads and callbacks first.
Final question, that's not the input it was expecting but there should be error handeling around it so that it at least doesn't crash.

Patrick Farrell (pefarrell) wrote : | # |
Ah, well in that case it is good that I didn't read the manual first, so that I could find where it was counterintuitive :-)
I think we should take in a directory. In practice, this will only ever be run on all the flml files in the tests/ or longtests/ directories, so it makes sense to do it that way.
I made a file
[pef@aislinn:
/data/pfarrell/
and ran it on that. I got:
Traceback (most recent call last):
File "/usr/lib/
useview.
File "/usr/lib/
self.
File "/usr/lib/
self.
File "/usr/lib/
iter = self.treestore.
KeyError: '/*/*[278]/*/*'

Fraser Waters (fraser-waters08) wrote : | # |
Could I have that file please? If it's not in the mapping it means somehow usage traversal found it but useview traversal didn't...
If it's always going to be every .suffix in a directory I can change it to that easy enough, give me a few minutes.

Patrick Farrell (pefarrell) wrote : | # |
While file? I pasted the contents of the list, and the referenced flml file lives in the fluidity trunk (tests/

Fraser Waters (fraser-waters08) wrote : | # |
Ah I see, sorry not enough coffee in the blood yet. I'll check it out.

Fraser Waters (fraser-waters08) wrote : | # |
Oki that's fixed, it was just a problem with comment nodes (we were ignoring them at one point and trying to look them up later)

Patrick Farrell (pefarrell) wrote : | # |
Great. It now works when I call it on fluidity's tests directory. And the results are /very/ interesting. Fantastic work!
Quick query: is it possible to offer a command-line option that only does the schema usage on a specified directory (i.e. doesn't bring up the main interface)?

Patrick Farrell (pefarrell) wrote : | # |
I did some profiling where I ran it on the tests/ directory, and the results are pretty interesting.
About 2/3 of the runtime is spent in xpath lookups. As a simple first optimisation, I memoised schema.
Exercise for the interested reader (i.e. Fraser): identify all the other points where xpath lookups are happening and cache it. I tried memoising schema.tree.xpath, but because the class is written in C, I can't overwrite the function (unfortunately). Caching the usages at
/usr/lib/
and
/usr/lib/
would knock off about another ~170s or so.
Here's the profile for the r492:
{method 'xpath' of 'lxml.etree.
{method 'xpath' of 'lxml.etree.

Fraser Waters (fraser-waters08) wrote : | # |
command-line option: yes, how about -u [DIR]?
I'm not suprised at those profile results, xpath lookups was my first guess.

Patrick Farrell (pefarrell) wrote : | # |
Yes, -u is fine. I have got the time down from 577s to 268s -- hopefully you can make further improvements. I've been running it like this:
python -m cProfile -o statfile /usr/bin/diamond test.flml
and then visualising the stats with RunSnakeRun (.deb packages in /scratch/pfarrell)

Fraser Waters (fraser-waters08) wrote : | # |
-u is in. Also added some caching in other places. The bulk of time is now spent in schema.Read
Next up, async UseView and DiffView and change the manual to match the new UseView behaviour

Fraser Waters (fraser-waters08) wrote : | # |
And that's all in, DiffView and UseView won't freeze Diamond anymore

Patrick Farrell (pefarrell) wrote : | # |
"Parseing schema" should be "Parsing schema".
Nice work! Down from 577s to 160s. I told you you could make it faster :-)
Now, for the coup-de-grace: Can you add a -ub (-u, batch) option that doesn't open any GUI or gtk window, but just makes diamond exit with 0 (if all the schema is used) and 1 (if any part of the schema is unused)? I want to add a test that all of the schema is used ... :-)

Patrick Farrell (pefarrell) wrote : | # |
And while I'm adding last-minute feature requests: can -ub print out to the terminal some readable description of the schema entries that are not used by the tests?
Thanks for all the hard work you have done over the 10 weeks; the whole group and I are very impressed.

Fraser Waters (fraser-waters08) wrote : | # |
Readable, do you want the schemaname (the /*/*//[58] format) or some kind of named path (Like element (fludity_
The later will be better for humans the former for machines.
Preview Diff
1 | === modified file 'diamond/diamond/choice.py' |
2 | --- diamond/diamond/choice.py 2011-07-29 09:18:28 +0000 |
3 | +++ diamond/diamond/choice.py 2011-09-07 14:03:34 +0000 |
4 | @@ -58,6 +58,10 @@ |
5 | def _on_set_attr(self, node, attr, value): |
6 | self.emit("on-set-attr", attr, value) |
7 | |
8 | + def get_attrs(self): |
9 | + """Get all attributes""" |
10 | + return self.get_current_tree().get_attrs() |
11 | + |
12 | def set_default_active(self): |
13 | self.active = True |
14 | if self.cardinality == '?' or self.cardinality == '*': |
15 | |
16 | === modified file 'diamond/diamond/interface.py' |
17 | --- diamond/diamond/interface.py 2011-09-07 12:31:36 +0000 |
18 | +++ diamond/diamond/interface.py 2011-09-07 14:03:34 +0000 |
19 | @@ -50,6 +50,7 @@ |
20 | import datawidget |
21 | import diffview |
22 | import sliceview |
23 | +import useview |
24 | |
25 | from lxml import etree |
26 | |
27 | @@ -144,6 +145,7 @@ |
28 | "on_slice": self.on_slice, |
29 | "on_diff": self.on_diff, |
30 | "on_diffsave": self.on_diffsave, |
31 | + "on_finduseage": self.on_finduseage, |
32 | "on_group": self.on_group, |
33 | "on_ungroup": self.on_ungroup} |
34 | |
35 | @@ -773,6 +775,9 @@ |
36 | else: |
37 | dialogs.error(self.main_window, "No save to diff against.") |
38 | |
39 | + def on_finduseage(self, widget = None): |
40 | + useview.UseView(self.s, self.filename) |
41 | + |
42 | def on_slice(self, widget = None): |
43 | if not self.selected_node.is_sliceable(): |
44 | self.statusbar.set_statusbar("Cannot slice on this element.") |
45 | |
46 | === modified file 'diamond/diamond/schema.py' |
47 | --- diamond/diamond/schema.py 2011-08-02 11:29:07 +0000 |
48 | +++ diamond/diamond/schema.py 2011-09-07 14:03:34 +0000 |
49 | @@ -55,17 +55,17 @@ |
50 | 'interleave': self.cb_group, |
51 | 'name': self.cb_name, |
52 | 'text': self.cb_text, |
53 | - 'anyName' : self.cb_anyname, |
54 | - 'nsName' : self.cb_nsname, |
55 | - 'except' : self.cb_except, |
56 | + 'anyName' : self.cb_anyname, |
57 | + 'nsName' : self.cb_nsname, |
58 | + 'except' : self.cb_except, |
59 | 'ignore' : self.cb_ignore, |
60 | 'notAllowed' : self.cb_notallowed} |
61 | - |
62 | + |
63 | self.lost_eles = [] |
64 | self.added_eles = [] |
65 | self.lost_attrs = [] |
66 | self.added_attrs = [] |
67 | - |
68 | + |
69 | return |
70 | |
71 | def element_children(self, element): |
72 | @@ -145,19 +145,12 @@ |
73 | if isinstance(eid, tree.Tree) or isinstance(eid, choice.Choice): |
74 | eidtree = eid |
75 | eid = eid.schemaname |
76 | - |
77 | - if eid == ":start": |
78 | - try: |
79 | - node = self.tree.xpath('/t:grammar/t:start', namespaces={'t': 'http://relaxng.org/ns/structure/1.0'})[0] |
80 | - except: |
81 | - debug.deprint("No valid start node found. Are you using a library Relax-NG file like spud_base.rng?", 0) |
82 | - sys.exit(0) |
83 | - else: |
84 | - xpath = self.tree.xpath(eid) |
85 | - if len(xpath) == 0: |
86 | - debug.deprint("Warning: no element with XPath %s" % eid) |
87 | - return None |
88 | - node = xpath[0] |
89 | + |
90 | + xpath = self.tree.xpath(eid) |
91 | + if len(xpath) == 0: |
92 | + debug.deprint("Warning: no element with XPath %s" % eid) |
93 | + return None |
94 | + node = xpath[0] |
95 | |
96 | node = self.to_tree(node) |
97 | |
98 | |
99 | === added file 'diamond/diamond/schemauseage.py' |
100 | --- diamond/diamond/schemauseage.py 1970-01-01 00:00:00 +0000 |
101 | +++ diamond/diamond/schemauseage.py 2011-09-07 14:03:34 +0000 |
102 | @@ -0,0 +1,102 @@ |
103 | +#!/usr/bin/env python |
104 | + |
105 | +# This file is part of Diamond. |
106 | +# |
107 | +# Diamond is free software: you can redistribute it and/or modify |
108 | +# it under the terms of the GNU General Public License as published by |
109 | +# the Free Software Foundation, either version 3 of the License, or |
110 | +# (at your option) any later version. |
111 | +# |
112 | +# Diamond is distributed in the hope that it will be useful, |
113 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
114 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
115 | +# GNU General Public License for more details. |
116 | +# |
117 | +# You should have received a copy of the GNU General Public License |
118 | +# along with Diamond. If not, see <http://www.gnu.org/licenses/>. |
119 | + |
120 | +from cStringIO import StringIO |
121 | +from lxml import etree |
122 | + |
123 | +from schema import Schema |
124 | + |
125 | +RELAXNGNS = "http://relaxng.org/ns/structure/1.0" |
126 | +RELAXNG = "{" + RELAXNGNS + "}" |
127 | + |
128 | +def find_fullset(tree): |
129 | + """ |
130 | + Given a schema tree pulls out xpaths for every element. |
131 | + """ |
132 | + |
133 | + def traverse(node): |
134 | + |
135 | + if node.tag == RELAXNG + "element" or (node.tag == RELAXNG + "choice" and all(n.tag != RELAXNG + "value" for n in node)): |
136 | + fullset.add(tree.getpath(node)) |
137 | + |
138 | + elif node.tag == RELAXNG + "ref": |
139 | + node = tree.xpath('/t:grammar/t:define[@name="' + node.get("name") + '"]', namespaces={'t': RELAXNGNS})[0] |
140 | + |
141 | + for child in node: |
142 | + traverse(child) |
143 | + |
144 | + start = tree.xpath("/t:grammar/t:start", namespaces={'t': RELAXNGNS})[0] |
145 | + |
146 | + root = start[0] |
147 | + |
148 | + fullset = set() |
149 | + traverse(root) |
150 | + return fullset |
151 | + |
152 | +def find_useset(tree): |
153 | + """ |
154 | + Given a diamond xml tree pulls out scehama paths for every element and attribute. |
155 | + """ |
156 | + |
157 | + def traverse(node): |
158 | + if node.active: |
159 | + useset.add(node.schemaname) |
160 | + |
161 | + for child in node.get_children(): |
162 | + traverse(child) |
163 | + |
164 | + useset = set() |
165 | + traverse(tree) |
166 | + return useset |
167 | + |
168 | +def find_unusedset(schema, paths): |
169 | + """ |
170 | + Given the a diamond schema and a list of paths to xml files |
171 | + find the unused xpaths. |
172 | + """ |
173 | + def traverse(node): |
174 | + if node.active: |
175 | + unusedset.discard(node.schemaname) |
176 | + |
177 | + for child in node.get_children(): |
178 | + traverse(child) |
179 | + |
180 | + unusedset = find_fullset(schema.tree) |
181 | + |
182 | + for path in paths: |
183 | + tree = schema.read(path) |
184 | + traverse(tree) |
185 | + |
186 | + return unusedset |
187 | + |
188 | +def strip(tag): |
189 | + return tag[tag.index("}") + 1:] |
190 | + |
191 | +def node_name(node): |
192 | + """ |
193 | + Returns a name for this node. |
194 | + """ |
195 | + tagname = node.get("name") if "name" in node.keys() else strip(node.tag) |
196 | + name = None |
197 | + |
198 | + for child in node: |
199 | + if child.tag == RELAXNG + "attribute": |
200 | + if "name" in child.keys() and child.get("name") == "name": |
201 | + for grandchild in child: |
202 | + if grandchild.tag == RELAXNG + "value": |
203 | + name = " (" + grandchild.text + ")" |
204 | + return tagname + (name if name else "") |
205 | |
206 | === modified file 'diamond/diamond/tree.py' |
207 | --- diamond/diamond/tree.py 2011-07-30 04:36:33 +0000 |
208 | +++ diamond/diamond/tree.py 2011-09-07 14:03:34 +0000 |
209 | @@ -53,7 +53,7 @@ |
210 | |
211 | # Any children? |
212 | if children is None: |
213 | - self.children = copy.copy([]) |
214 | + self.children = [] |
215 | else: |
216 | self.children = children |
217 | |
218 | @@ -114,6 +114,10 @@ |
219 | (datatype, curval) = self.attrs[attr] |
220 | return curval |
221 | |
222 | + def get_attrs(self): |
223 | + """Get all attributes""" |
224 | + return self.attrs |
225 | + |
226 | def set_data(self, data): |
227 | (invalid, data) = self.valid_data(self.datatype, data) |
228 | if invalid: |
229 | |
230 | === added file 'diamond/diamond/useview.py' |
231 | --- diamond/diamond/useview.py 1970-01-01 00:00:00 +0000 |
232 | +++ diamond/diamond/useview.py 2011-09-07 14:03:34 +0000 |
233 | @@ -0,0 +1,156 @@ |
234 | +#!/usr/bin/env python |
235 | + |
236 | +# This file is part of Diamond. |
237 | +# |
238 | +# Diamond is free software: you can redistribute it and/or modify |
239 | +# it under the terms of the GNU General Public License as published by |
240 | +# the Free Software Foundation, either version 3 of the License, or |
241 | +# (at your option) any later version. |
242 | +# |
243 | +# Diamond is distributed in the hope that it will be useful, |
244 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
245 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
246 | +# GNU General Public License for more details. |
247 | +# |
248 | +# You should have received a copy of the GNU General Public License |
249 | +# along with Diamond. If not, see <http://www.gnu.org/licenses/>. |
250 | + |
251 | +import gobject |
252 | +import gtk |
253 | + |
254 | +import schemauseage |
255 | + |
256 | +RELAXNGNS = "http://relaxng.org/ns/structure/1.0" |
257 | +RELAXNG = "{" + RELAXNGNS + "}" |
258 | + |
259 | +class UseView(gtk.Window): |
260 | + def __init__(self, schema, path): |
261 | + gtk.Window.__init__(self) |
262 | + self.__add_controls() |
263 | + |
264 | + dialog = gtk.FileChooserDialog(title = "Input list", |
265 | + action = gtk.FILE_CHOOSER_ACTION_OPEN, |
266 | + buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) |
267 | + |
268 | + if path: |
269 | + dialog.set_current_folder(path) |
270 | + response = dialog.run() |
271 | + if response != gtk.RESPONSE_OK: |
272 | + dialog.destroy() |
273 | + return |
274 | + |
275 | + filename = dialog.get_filename() |
276 | + dialog.destroy() |
277 | + |
278 | + paths = [line.strip() for line in open(filename) if line.strip()] |
279 | + if not paths: |
280 | + return |
281 | + |
282 | + self.__update(schema, paths) |
283 | + self.show_all() |
284 | + |
285 | + def __add_controls(self): |
286 | + self.set_title("Unused schema entries") |
287 | + self.set_default_size(800, 600) |
288 | + |
289 | + scrolledwindow = gtk.ScrolledWindow() |
290 | + scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) |
291 | + |
292 | + self.treeview = gtk.TreeView() |
293 | + |
294 | + self.treeview.get_selection().set_mode(gtk.SELECTION_SINGLE) |
295 | + |
296 | + # Node column |
297 | + celltext = gtk.CellRendererText() |
298 | + column = gtk.TreeViewColumn("Node", celltext) |
299 | + column.set_cell_data_func(celltext, self.set_celltext) |
300 | + |
301 | + self.treeview.append_column(column) |
302 | + |
303 | + # 0: The node tag |
304 | + # 1: Used (0 == Not used, 1 = Child not used, 2 = Used) |
305 | + self.treestore = gtk.TreeStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT) |
306 | + self.treeview.set_model(self.treestore) |
307 | + self.treeview.set_enable_search(False) |
308 | + |
309 | + scrolledwindow.add(self.treeview) |
310 | + self.add(scrolledwindow) |
311 | + |
312 | + def __set_treestore(self, node, iter = None, type = None): |
313 | + if node.tag == RELAXNG + "element": |
314 | + name = schemauseage.node_name(node) |
315 | + if name == "comment": |
316 | + return #early out to skip comment nodes |
317 | + |
318 | + tag = name + (type if type else "") |
319 | + child_iter = self.treestore.append(iter, [tag, 2]) |
320 | + self.mapping[self.tree.getpath(node)] = self.treestore.get_path(child_iter) |
321 | + type = None |
322 | + elif node.tag == RELAXNG + "choice" and all(n.tag != RELAXNG + "value" for n in node): |
323 | + tag = "choice" + (type if type else "") |
324 | + child_iter = self.treestore.append(iter, [tag, 2]) |
325 | + self.mapping[self.tree.getpath(node)] = self.treestore.get_path(child_iter) |
326 | + type = None |
327 | + elif node.tag == RELAXNG + "optional": |
328 | + child_iter = iter |
329 | + type = " ?" |
330 | + elif node.tag == RELAXNG + "oneOrMore": |
331 | + child_iter = iter |
332 | + type = " +" |
333 | + elif node.tag == RELAXNG + "zeroOrMore": |
334 | + child_iter = iter |
335 | + type = " *" |
336 | + elif node.tag == RELAXNG + "ref": |
337 | + node = self.tree.xpath('/t:grammar/t:define[@name="' + node.get("name") + '"]', namespaces={'t': RELAXNGNS})[0] |
338 | + child_iter = iter |
339 | + elif node.tag == RELAXNG + "group" or node.tag == RELAXNG + "interleave": |
340 | + child_iter = iter |
341 | + else: |
342 | + return |
343 | + |
344 | + for child in node: |
345 | + self.__set_treestore(child, child_iter, type) |
346 | + |
347 | + def __set_useage(self, useage): |
348 | + for xpath in useage: |
349 | + iter = self.treestore.get_iter(self.mapping[xpath]) |
350 | + self.treestore.set_value(iter, 1, 0) |
351 | + |
352 | + def __floodfill(self, iter, parent = 2): |
353 | + """ |
354 | + Floodfill the tree with the correct useage. |
355 | + """ |
356 | + if parent == 0: #parent is not used |
357 | + self.treestore.set_value(iter, 1, 0) #color us not used |
358 | + |
359 | + useage = self.treestore.get_value(iter, 1) |
360 | + |
361 | + child = self.treestore.iter_children(iter) |
362 | + while child is not None: |
363 | + change = self.__floodfill(child, useage) |
364 | + if change != 2 and useage == 2: |
365 | + self.treestore.set(iter, 1, 1) |
366 | + child = self.treestore.iter_next(child) |
367 | + |
368 | + return self.treestore.get_value(iter, 1) |
369 | + |
370 | + |
371 | + def __update(self, schema, paths): |
372 | + self.tree = schema.tree |
373 | + self.start = self.tree.xpath('/t:grammar/t:start', namespaces={'t': RELAXNGNS})[0] |
374 | + self.mapping = {} |
375 | + |
376 | + self.__set_treestore(self.start[0]) |
377 | + self.__set_useage(schemauseage.find_unusedset(schema, paths)) |
378 | + self.__floodfill(self.treestore.get_iter_root()) |
379 | + |
380 | + def set_celltext(self, column, cell, model, iter): |
381 | + tag, useage = model.get(iter, 0, 1) |
382 | + cell.set_property("text", tag) |
383 | + |
384 | + if useage == 0: |
385 | + cell.set_property("foreground", "red") |
386 | + elif useage == 1: |
387 | + cell.set_property("foreground", "indianred") |
388 | + else: |
389 | + cell.set_property("foreground", "black") |
390 | |
391 | === modified file 'diamond/gui/gui.glade' |
392 | --- diamond/gui/gui.glade 2011-08-23 12:37:37 +0000 |
393 | +++ diamond/gui/gui.glade 2011-09-07 14:03:34 +0000 |
394 | @@ -227,7 +227,7 @@ |
395 | <signal name="activate" handler="on_diff"/> |
396 | </widget> |
397 | </child> |
398 | - <child> |
399 | + <child> |
400 | <widget class="GtkMenuItem" id="menuitemDiffSave"> |
401 | <property name="visible">True</property> |
402 | <property name="label" translatable="yes">Diff against last save</property> |
403 | @@ -235,6 +235,13 @@ |
404 | </widget> |
405 | </child> |
406 | <child> |
407 | + <widget class="GtkMenuItem" id="menuitemFindUseage"> |
408 | + <property name="visible">True</property> |
409 | + <property name="label" translatable="yes">Find schema useage</property> |
410 | + <signal name="activate" handler="on_finduseage"/> |
411 | + </widget> |
412 | + </child> |
413 | + <child> |
414 | <widget class="GtkCheckMenuItem" id="display_properties"> |
415 | <property name="visible">True</property> |
416 | <property name="tooltip" translatable="yes">Display the option properties on the right hand side of the main window.</property> |
This is a very useful feature.
The code looks good;
The diff feature was successfully tested against all flml's in the fluidity test directory.
Documentation will be provided in a later merge.
=> good to go!