ctioga 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +340 -0
- data/ctioga/bin/ctable +28 -0
- data/ctioga/bin/ctioga +37 -0
- data/ctioga/doc/ctable.1 +156 -0
- data/ctioga/doc/ctioga.1 +2363 -0
- data/ctioga/examples/README +46 -0
- data/ctioga/examples/ctioga.gnuplot +4 -0
- data/ctioga/examples/ctioga_within_tioga.rb +53 -0
- data/ctioga/examples/ctiogarc.rb +24 -0
- data/ctioga/examples/include_1.rb +15 -0
- data/ctioga/examples/noise.dat +100 -0
- data/ctioga/examples/noise.rb +13 -0
- data/ctioga/examples/trig.csv +100 -0
- data/ctioga/examples/trig.dat +100 -0
- data/ctioga/examples/trig.rb +14 -0
- data/ctioga/examples/trigh.dat +100 -0
- data/ctioga/examples/trigh.rb +10 -0
- data/ctioga/examples/tutorial +763 -0
- data/ctioga/examples/tutorial.sh +269 -0
- data/ctioga/tests/README +14 -0
- data/ctioga/tests/axes.sh +40 -0
- data/ctioga/tests/basic.sh +11 -0
- data/ctioga/tests/draw.sh +24 -0
- data/ctioga/tests/histograms.sh +14 -0
- data/ctioga/tests/insets.sh +41 -0
- data/ctioga/tests/layouts.sh +29 -0
- data/ctioga/tests/legends.sh +113 -0
- data/ctioga/tests/styles.sh +43 -0
- data/ctioga/tests/test_style.sh +8 -0
- data/ctioga/tests/tests.sh +24 -0
- data/ctioga/tests/text_backend.sh +83 -0
- data/ctioga/tests/tioga_defaults.rb +18 -0
- data/lib/CTioga/axes.rb +904 -0
- data/lib/CTioga/backends.rb +88 -0
- data/lib/CTioga/boundaries.rb +224 -0
- data/lib/CTioga/ctable.rb +134 -0
- data/lib/CTioga/curve_style.rb +246 -0
- data/lib/CTioga/debug.rb +199 -0
- data/lib/CTioga/dimension.rb +133 -0
- data/lib/CTioga/elements.rb +17 -0
- data/lib/CTioga/elements/base.rb +84 -0
- data/lib/CTioga/elements/containers.rb +578 -0
- data/lib/CTioga/elements/curves.rb +368 -0
- data/lib/CTioga/elements/tioga_primitives.rb +440 -0
- data/lib/CTioga/layout.rb +595 -0
- data/lib/CTioga/legends.rb +29 -0
- data/lib/CTioga/legends/cmdline.rb +187 -0
- data/lib/CTioga/legends/item.rb +164 -0
- data/lib/CTioga/legends/style.rb +257 -0
- data/lib/CTioga/log.rb +73 -0
- data/lib/CTioga/movingarrays.rb +131 -0
- data/lib/CTioga/partition.rb +271 -0
- data/lib/CTioga/plot_style.rb +230 -0
- data/lib/CTioga/plotmaker.rb +1677 -0
- data/lib/CTioga/shortcuts.rb +69 -0
- data/lib/CTioga/structures.rb +82 -0
- data/lib/CTioga/styles.rb +140 -0
- data/lib/CTioga/themes.rb +581 -0
- data/lib/CTioga/themes/classical.rb +82 -0
- data/lib/CTioga/themes/demo.rb +63 -0
- data/lib/CTioga/themes/fits.rb +91 -0
- data/lib/CTioga/themes/mono.rb +33 -0
- data/lib/CTioga/tioga.rb +32 -0
- data/lib/CTioga/utils.rb +173 -0
- data/lib/MetaBuilder/Parameters/dates.rb +38 -0
- data/lib/MetaBuilder/Parameters/lists.rb +132 -0
- data/lib/MetaBuilder/Parameters/numbers.rb +69 -0
- data/lib/MetaBuilder/Parameters/strings.rb +86 -0
- data/lib/MetaBuilder/Parameters/styles.rb +75 -0
- data/lib/MetaBuilder/Qt4/Parameters/dates.rb +51 -0
- data/lib/MetaBuilder/Qt4/Parameters/numbers.rb +65 -0
- data/lib/MetaBuilder/Qt4/Parameters/strings.rb +106 -0
- data/lib/MetaBuilder/Qt4/parameter.rb +172 -0
- data/lib/MetaBuilder/Qt4/parameters.rb +9 -0
- data/lib/MetaBuilder/descriptions.rb +603 -0
- data/lib/MetaBuilder/factory.rb +101 -0
- data/lib/MetaBuilder/group.rb +57 -0
- data/lib/MetaBuilder/metabuilder.rb +10 -0
- data/lib/MetaBuilder/parameter.rb +374 -0
- data/lib/MetaBuilder/parameters.rb +11 -0
- data/lib/MetaBuilder/qt4.rb +8 -0
- data/lib/SciYAG/Backends/backend.rb +379 -0
- data/lib/SciYAG/Backends/binner.rb +168 -0
- data/lib/SciYAG/Backends/cache.rb +102 -0
- data/lib/SciYAG/Backends/dataset.rb +158 -0
- data/lib/SciYAG/Backends/descriptions.rb +469 -0
- data/lib/SciYAG/Backends/filters.rb +25 -0
- data/lib/SciYAG/Backends/filters/average.rb +134 -0
- data/lib/SciYAG/Backends/filters/cumulate.rb +37 -0
- data/lib/SciYAG/Backends/filters/filter.rb +70 -0
- data/lib/SciYAG/Backends/filters/norm.rb +39 -0
- data/lib/SciYAG/Backends/filters/smooth.rb +63 -0
- data/lib/SciYAG/Backends/filters/sort.rb +43 -0
- data/lib/SciYAG/Backends/filters/strip.rb +34 -0
- data/lib/SciYAG/Backends/filters/trim.rb +64 -0
- data/lib/SciYAG/Backends/gnuplot.rb +131 -0
- data/lib/SciYAG/Backends/math.rb +108 -0
- data/lib/SciYAG/Backends/mdb.rb +462 -0
- data/lib/SciYAG/Backends/multitext.rb +96 -0
- data/lib/SciYAG/Backends/source.rb +64 -0
- data/lib/SciYAG/Backends/text.rb +339 -0
- data/lib/SciYAG/backends.rb +16 -0
- metadata +191 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
# numbers.rb : Qt methods for numbers
|
2
|
+
# Copyright (C) 2006 Vincent Fourmond
|
3
|
+
#
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
17
|
+
|
18
|
+
|
19
|
+
module MetaBuilder
|
20
|
+
|
21
|
+
# The module ParameterTypes should be used for all subclasses of
|
22
|
+
# ParameterType, to keep the place clean and tidy.
|
23
|
+
module ParameterTypes
|
24
|
+
|
25
|
+
# An integer
|
26
|
+
class IntegerParameter
|
27
|
+
|
28
|
+
def qt4_create_input_widget(parent = nil, target = nil, style = nil)
|
29
|
+
return Qt4MB::IntegerInputWidget.new(parent, self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
module Qt4MB
|
36
|
+
|
37
|
+
# A spinbox to choose Integers.
|
38
|
+
class IntegerInputWidget < Qt::SpinBox
|
39
|
+
|
40
|
+
# Emitted when the value is changed. Connected to
|
41
|
+
# editingFinished().
|
42
|
+
signals 'value_changed()'
|
43
|
+
|
44
|
+
# Creates a Generic widget. +type+ is the ParameterType child
|
45
|
+
# instance for whom we should do conversion.
|
46
|
+
def initialize(parent, type)
|
47
|
+
super(parent)
|
48
|
+
@type = type
|
49
|
+
connect(self, SIGNAL('valueChanged(int)'),
|
50
|
+
SIGNAL('value_changed()'))
|
51
|
+
set_range(-(2**30), 2**30)
|
52
|
+
end
|
53
|
+
|
54
|
+
def text
|
55
|
+
return @type.type_to_string(value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def text=(t)
|
59
|
+
self.value=(@type.string_to_type(t))
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# numbers.rb : Qt methods for strings
|
2
|
+
# Copyright (C) 2006 Vincent Fourmond
|
3
|
+
#
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
17
|
+
|
18
|
+
|
19
|
+
module MetaBuilder
|
20
|
+
|
21
|
+
# The module ParameterTypes should be used for all subclasses of
|
22
|
+
# ParameterType, to keep the place clean and tidy.
|
23
|
+
module ParameterTypes
|
24
|
+
|
25
|
+
# An integer
|
26
|
+
class FileParameter
|
27
|
+
|
28
|
+
def qt4_create_input_widget(parent = nil, target = nil, style = nil)
|
29
|
+
return Qt4MB::FileInputWidget.new(parent, self)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Directly wraps to Qt::FileDialog.get_open_file_name;
|
33
|
+
# the _title_ is therefore ignored...
|
34
|
+
def qt4_get(parent, title, label, default, target = nil)
|
35
|
+
|
36
|
+
file = Qt::FileDialog.get_open_file_name(parent, label,
|
37
|
+
File.dirname(default),
|
38
|
+
filter)
|
39
|
+
if file.nil?
|
40
|
+
raise CancelInput
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
module Qt4MB
|
49
|
+
|
50
|
+
# A widget with a nice Browse box to choose
|
51
|
+
class FileInputWidget < Qt::Widget
|
52
|
+
|
53
|
+
# Emitted when the value is changed. Connected to
|
54
|
+
# editingFinished().
|
55
|
+
signals 'value_changed()'
|
56
|
+
|
57
|
+
# When the button is clicked
|
58
|
+
slots 'button_clicked()'
|
59
|
+
|
60
|
+
# Creates a Generic widget. _type_ is the FileParameter
|
61
|
+
# instance for whom we should do conversion.
|
62
|
+
def initialize(parent, type)
|
63
|
+
super(parent)
|
64
|
+
@type = type
|
65
|
+
@layout = Qt::HBoxLayout.new(self)
|
66
|
+
|
67
|
+
@line_edit = Qt::LineEdit.new
|
68
|
+
@layout.add_widget(@line_edit)
|
69
|
+
connect(@line_edit, SIGNAL('editingFinished()'),
|
70
|
+
SIGNAL('value_changed()'))
|
71
|
+
|
72
|
+
@button = Qt::PushButton.new("Browse")
|
73
|
+
@layout.add_widget(@button)
|
74
|
+
connect(@button, SIGNAL('clicked()'),
|
75
|
+
SLOT('button_clicked()'))
|
76
|
+
end
|
77
|
+
|
78
|
+
def button_clicked
|
79
|
+
file = @type.qt4_get(parent, "dummy",
|
80
|
+
"Select file", text)
|
81
|
+
self.text=file
|
82
|
+
rescue
|
83
|
+
# Nothing, just don't set the current value
|
84
|
+
end
|
85
|
+
|
86
|
+
def text
|
87
|
+
return @line_edit.text
|
88
|
+
end
|
89
|
+
|
90
|
+
def text=(t)
|
91
|
+
@line_edit.setText(t)
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
def value
|
96
|
+
return text
|
97
|
+
end
|
98
|
+
|
99
|
+
def value=(v)
|
100
|
+
self.text=v
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# parameter.rb : Qt support for parameters.
|
2
|
+
# Copyright (C) 2006 Vincent Fourmond
|
3
|
+
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
module MetaBuilder
|
22
|
+
|
23
|
+
# An exception raised when the user cancels an input dialog box.
|
24
|
+
class CancelInput < Exception
|
25
|
+
end
|
26
|
+
|
27
|
+
class ParameterType
|
28
|
+
|
29
|
+
# Creates a widget suitable for editing the value and returns it.
|
30
|
+
# The widget will be a child of _parent_ (can be nil).
|
31
|
+
# The _target_ is provided for the Parameter system. It should contain
|
32
|
+
# the value of the target whose parameter we should set. This can be
|
33
|
+
# used for instance when the value has dynamic content depending on
|
34
|
+
# some internals of _target_. Reimplementations should not
|
35
|
+
# fail if it is +nil+. The _style_ parameter
|
36
|
+
# is for now a placeholder.
|
37
|
+
#
|
38
|
+
# The created widget must provide the following:
|
39
|
+
# * <tt>text=|text</tt>: a writer|reader pair for the string value;
|
40
|
+
# * <tt>value=|value</tt>: a writer|reader pair for the real value;
|
41
|
+
# * <tt>value_changed()</tt> : a signal for when the value has changed.
|
42
|
+
# should be used in the same way as Qt::LineEdit::editingFinished
|
43
|
+
# in the sense that it should only trigger on complete output.
|
44
|
+
# * <tt>label=</tt>: a way to set the label. Label could be basically
|
45
|
+
# anything accepted by Qt; if it is +nil+ or +false+, the display is
|
46
|
+
# switched off. The label display must be switched off by default.
|
47
|
+
#
|
48
|
+
def qt4_create_input_widget(parent = nil, target = nil, style = nil)
|
49
|
+
return Qt4MB::GenericInputWidget.new(parent, self)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Pops up a Qt4 dialog box to ask for the value of the parameter.
|
53
|
+
# * _parent_ is the parent widget;
|
54
|
+
# * _title_ is the title of the Dialog Box
|
55
|
+
# * _label_ is the label of the widget used for querying
|
56
|
+
# * _default_ is the default value (of the appropriate type).
|
57
|
+
# * _target_ is the target for the parameters system. Defaults
|
58
|
+
# to +nil+.
|
59
|
+
#
|
60
|
+
# Returns directly a value in the right type. The default
|
61
|
+
# implementation uses the #qt4_create_input_widget function.
|
62
|
+
# As +false+ or +nil+ are appropriate return values in some
|
63
|
+
# cases, #qt4_get raises a CancelInput exception. You *must*
|
64
|
+
# handle it if you don't want nasty surprises...
|
65
|
+
|
66
|
+
def qt4_get(parent, title, label, default, target = nil)
|
67
|
+
return Qt4MB::GenericDialog.get_type(parent, self,
|
68
|
+
title, label, default, target)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Converts from the type to a QVariant
|
72
|
+
def type_to_variant(value)
|
73
|
+
return Qt::Variant.new(value)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Converts from a QVariant to the appropriate (Ruby) type.
|
77
|
+
def variant_to_type(variant)
|
78
|
+
return variant.value
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
class Parameter
|
84
|
+
end
|
85
|
+
|
86
|
+
# This module holds all the classes descending from the Qt4 classes.
|
87
|
+
module Qt4MB
|
88
|
+
|
89
|
+
|
90
|
+
# THIS MUST BE REWRITTEN TO COMPLY WITH THE NEW SPECS !!!
|
91
|
+
# A generic input for the types. Based on Qt::LineEdit.
|
92
|
+
class GenericInputWidget < Qt::LineEdit
|
93
|
+
|
94
|
+
# Emitted when the value is changed. Connected to
|
95
|
+
# editingFinished().
|
96
|
+
signals 'value_changed()'
|
97
|
+
|
98
|
+
# Creates a Generic widget. _type_ is the ParameterType child
|
99
|
+
# instance for whom we should do conversion.
|
100
|
+
def initialize(parent, type)
|
101
|
+
super(parent)
|
102
|
+
@type = type
|
103
|
+
connect(self, SIGNAL('editingFinished()'),
|
104
|
+
SIGNAL('value_changed()'))
|
105
|
+
end
|
106
|
+
|
107
|
+
def text=(t)
|
108
|
+
setText(t)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns the value currently displayed
|
112
|
+
def value
|
113
|
+
return @type.string_to_type(text)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Sets the display.
|
117
|
+
def value=(val)
|
118
|
+
self.text = @type.type_to_string(val)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class GenericDialog < Qt::Dialog
|
123
|
+
|
124
|
+
# Creates a generic input dialog for the type _type_.
|
125
|
+
def initialize(parent, type, title, label, val, target)
|
126
|
+
super(parent)
|
127
|
+
self.window_title = title
|
128
|
+
vbox = Qt::VBoxLayout.new(self)
|
129
|
+
|
130
|
+
hbox = Qt::HBoxLayout.new
|
131
|
+
@label = Qt::Label.new(label)
|
132
|
+
hbox.add_widget(@label)
|
133
|
+
@widget = type.qt4_create_input_widget(nil, target)
|
134
|
+
hbox.add_widget(@widget)
|
135
|
+
@widget.value = val
|
136
|
+
vbox.add_layout(hbox)
|
137
|
+
|
138
|
+
hbox = Qt::HBoxLayout.new
|
139
|
+
# OK and Cancel buttons
|
140
|
+
button = Qt::PushButton.new("OK")
|
141
|
+
connect(button, SIGNAL('clicked()'),
|
142
|
+
SLOT('accept()'))
|
143
|
+
hbox.add_widget(button)
|
144
|
+
button = Qt::PushButton.new("Cancel")
|
145
|
+
connect(button, SIGNAL('clicked()'),
|
146
|
+
SLOT('reject()'))
|
147
|
+
hbox.add_widget(button)
|
148
|
+
vbox.add_layout(hbox)
|
149
|
+
# and that's all !
|
150
|
+
end
|
151
|
+
|
152
|
+
def exec
|
153
|
+
res = super
|
154
|
+
if res > 0
|
155
|
+
return @widget.value
|
156
|
+
else
|
157
|
+
raise CancelInput, "User cancelled input"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# A convenience wrapper around the functionnality. Creates a
|
162
|
+
# GenericDialog and executes it.
|
163
|
+
def self.get_type(parent, type, title, label, val, target)
|
164
|
+
dialog = new(parent, type, title, label, val, target)
|
165
|
+
return dialog.exec
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# parameters.rb: A file grouping all the require necessary to have a
|
2
|
+
# working parameter system.
|
3
|
+
# This file is copyright 2006 by Vincent Fourmond, but you can do whatever
|
4
|
+
# you want with it.
|
5
|
+
|
6
|
+
require 'MetaBuilder/Qt4/parameter'
|
7
|
+
require 'MetaBuilder/Qt4/Parameters/numbers'
|
8
|
+
require 'MetaBuilder/Qt4/Parameters/strings'
|
9
|
+
require 'MetaBuilder/Qt4/Parameters/dates'
|
@@ -0,0 +1,603 @@
|
|
1
|
+
# descriptions.rb : The basics of the MetaBuilder system
|
2
|
+
# Copyright (C) 2006 Vincent Fourmond
|
3
|
+
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
17
|
+
|
18
|
+
module MetaBuilder
|
19
|
+
|
20
|
+
|
21
|
+
# The Description class is a meta-information class that records several
|
22
|
+
# informations about the class:
|
23
|
+
#
|
24
|
+
# * a basic name, code-like, which is used mainly for internal
|
25
|
+
# purposes;
|
26
|
+
# * a long name, more explanatory, in proper English (or any
|
27
|
+
# other language. By the way, it should definitely be
|
28
|
+
# translated in a production environment);
|
29
|
+
# * a description itself, some small text describing the nature
|
30
|
+
# of the class;
|
31
|
+
# * a list of all the Parameters that are important for the class,
|
32
|
+
# and enough to recreate the state of an object.
|
33
|
+
#
|
34
|
+
# This class is fairly general, and can be subclassed to fit specific
|
35
|
+
# needs. An example is in the SciYAG/Backend system, where the description
|
36
|
+
# of a backend is derived from Description.
|
37
|
+
#
|
38
|
+
# To make use of the Description system for a class, use the following:
|
39
|
+
#
|
40
|
+
# class SomeClass
|
41
|
+
# extend MetaBuilder::DescriptionExtend
|
42
|
+
# include MetaBuilder::DescriptionInclude
|
43
|
+
#
|
44
|
+
# describe 'someclass', 'Some nice class', <<EOD
|
45
|
+
# The description of a nice class
|
46
|
+
# EOD
|
47
|
+
#
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# Descriptions can be used in two completely different manners:
|
51
|
+
#
|
52
|
+
# * you can use a single Description to facilitate the interface
|
53
|
+
# with the user to query|save parameters;
|
54
|
+
# * you can use the Description system to describe a whole set of
|
55
|
+
# classes providing similar functions and sharing a base ancestor,
|
56
|
+
# such as series of input|output plugins, database formats,
|
57
|
+
# themes... In this case, the Description system can act as a real
|
58
|
+
# plugin factory, recording every new subclass of the base one
|
59
|
+
# and providing facilities to prompt the user.
|
60
|
+
#
|
61
|
+
# Please note that if you want to use the facilities to dynamically
|
62
|
+
# create objects at run-time, the classes used by describe
|
63
|
+
# *should not need any parameter for #initialize*.
|
64
|
+
#
|
65
|
+
#
|
66
|
+
# Description provides the following facilities:
|
67
|
+
#
|
68
|
+
# * a #save_state system, returning a hash containing the current
|
69
|
+
# parameters of an object; it's state can be returned to using
|
70
|
+
# #restore_state. Please note that the parameters will
|
71
|
+
# be set in the same order as they appear in the class. Moreover,
|
72
|
+
# if you want the #restore_state to work properly, you must ensure
|
73
|
+
# that setting all the parameters restore the state, i.e. you didn't
|
74
|
+
# forget anything.
|
75
|
+
# * facilities to prepare OptionParser to work on a described class (see
|
76
|
+
# #option_parser_fill, #option_parser_banner, #option_parser_options)
|
77
|
+
# * facilities to create widgets, dialog boxes and so on to query and
|
78
|
+
# modify the contents of a described class instance.
|
79
|
+
|
80
|
+
|
81
|
+
class Description
|
82
|
+
# The Class to instantiate.
|
83
|
+
attr_accessor :object_class
|
84
|
+
|
85
|
+
# The name of the class (short, code-like)
|
86
|
+
attr_accessor :name
|
87
|
+
|
88
|
+
# (text) description !
|
89
|
+
attr_accessor :description
|
90
|
+
|
91
|
+
# Long name, the one for public display
|
92
|
+
attr_accessor :long_name
|
93
|
+
|
94
|
+
# The parameter list. The parameters are added when they are found
|
95
|
+
# in the class description, and will be used in the order found
|
96
|
+
# in this list to recreate the state; beware if one parameter is
|
97
|
+
# depending on another one.
|
98
|
+
attr_reader :param_list
|
99
|
+
|
100
|
+
# A hash index on the (short) name and the symbols of the parameters,
|
101
|
+
# for quick access. None of those should overlap.
|
102
|
+
attr_reader :param_hash
|
103
|
+
|
104
|
+
|
105
|
+
# The list of groups
|
106
|
+
attr_accessor :group_list
|
107
|
+
|
108
|
+
# A hash indexing the groups by their name
|
109
|
+
attr_accessor :group_hash
|
110
|
+
|
111
|
+
# Initializes a Description
|
112
|
+
def initialize(cls, name, long_name, description = "")
|
113
|
+
@object_class = cls
|
114
|
+
@name = name
|
115
|
+
@long_name = long_name
|
116
|
+
@description = description
|
117
|
+
@param_list = []
|
118
|
+
@param_hash = {}
|
119
|
+
@init_param_list = []
|
120
|
+
|
121
|
+
@group_list = []
|
122
|
+
@group_hash = {}
|
123
|
+
|
124
|
+
# There is not default group.
|
125
|
+
@current_group = nil
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
# Adds a group to the list, and switches to it
|
130
|
+
def add_group(group)
|
131
|
+
@group_list << group
|
132
|
+
@group_hash[group.name] = group
|
133
|
+
@current_group = group
|
134
|
+
end
|
135
|
+
|
136
|
+
# Switches to the named group
|
137
|
+
def switch_to_group(name)
|
138
|
+
@current_group = @group_hash.fetch(name)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Adds a new parameter to the description
|
142
|
+
def add_param(param)
|
143
|
+
@param_list << param
|
144
|
+
|
145
|
+
# Update the parameter hash, for easy access.
|
146
|
+
@param_hash[param.reader_symbol] = param
|
147
|
+
@param_hash[param.writer_symbol] = param
|
148
|
+
@param_hash[param.name] = param
|
149
|
+
|
150
|
+
# Update the current group, if necessary
|
151
|
+
@current_group.add_parameter(param) unless @current_group.nil?
|
152
|
+
end
|
153
|
+
|
154
|
+
# Small set of functions dealing with OptionParsers
|
155
|
+
|
156
|
+
# Fills an OptionParser with all the parameters the of the class.
|
157
|
+
# _instance_ is the instance of the class we want to
|
158
|
+
# parametrize, _parser_ is an OptionParser, and if _uniquify_ is set,
|
159
|
+
# the description name is prefixed to the parameter name.
|
160
|
+
def option_parser_fill(parser, instance, uniquify = true, groups = false)
|
161
|
+
option_parser_banner(parser, instance)
|
162
|
+
option_parser_options(parser, instance, uniquify, groups)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Fills a parser with options for all parameters. _parser_, _instance_
|
166
|
+
# and _uniquify_ have the same meaning as in #option_parser_option.
|
167
|
+
# If _grouping_ is set to _true_, the options are organized according
|
168
|
+
# to group listing.
|
169
|
+
def option_parser_options(parser, instance, uniquify = true,
|
170
|
+
grouping = false)
|
171
|
+
if grouping
|
172
|
+
for group in @group_list
|
173
|
+
parser.separator group.long_name
|
174
|
+
for param in group.parameter_list
|
175
|
+
option_parser_option(parser, param, instance, uniquify)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
else
|
179
|
+
for param in @param_list
|
180
|
+
option_parser_option(parser, param, instance, uniquify)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Adds an option for the parameter _param_ to the OptionParser _parser_.
|
186
|
+
# The corresponding parameter will be set in _instance_. If _uniquify_
|
187
|
+
# is set, the description name if prefixed to the option.
|
188
|
+
# _param_ can be either a Parameter or a string|symbol registered
|
189
|
+
# in #param_hash.
|
190
|
+
def option_parser_option(parser, param, instance, uniquify = true)
|
191
|
+
raise "The instance is not of the right class" unless
|
192
|
+
instance.is_a? @object_class
|
193
|
+
if not param.is_a?(Parameter)
|
194
|
+
param = param_hash.fetch(param)
|
195
|
+
end
|
196
|
+
if uniquify
|
197
|
+
param_name = "#{@name}-#{param.name}"
|
198
|
+
else
|
199
|
+
param_name = "#{param.name}"
|
200
|
+
end
|
201
|
+
param.option_parser_option(parser, param_name, instance)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Subclass Description and reimplement this method to have your own banner.
|
205
|
+
def banner(instance)
|
206
|
+
return false
|
207
|
+
end
|
208
|
+
|
209
|
+
# Ouputs an OptionParser banner based on the return value
|
210
|
+
# of the #banner method.
|
211
|
+
def option_parser_banner(parser, instance)
|
212
|
+
if b = banner(instance)
|
213
|
+
parser.separator b
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Creates a hash representing the state of an instance of the
|
218
|
+
# described class at a given time. It can be fed to #restore_state
|
219
|
+
# and #recreate_state to restore the state to an already existing
|
220
|
+
# instance or to create a new one from scratch.
|
221
|
+
#
|
222
|
+
# This function saves the raw parameters, as there is not much
|
223
|
+
# interest in converting to String and back from it.
|
224
|
+
#
|
225
|
+
# It saves a duplicate of the values, not the values themselves,
|
226
|
+
# to make sure it really represents the state of the objects at
|
227
|
+
# a given time.
|
228
|
+
def save_state(instance)
|
229
|
+
ret = {}
|
230
|
+
for param in @param_list
|
231
|
+
val = param.get_raw(instance)
|
232
|
+
ret[param.name] = begin # Necessary as some classes can't be duped.
|
233
|
+
val.dup
|
234
|
+
rescue
|
235
|
+
val # Typical case: nil, which still is a
|
236
|
+
# valid value
|
237
|
+
end
|
238
|
+
end
|
239
|
+
return ret
|
240
|
+
end
|
241
|
+
|
242
|
+
# Puts the _target_ into the _state_ returned by #save_state.
|
243
|
+
# The parameters are set in the order in which they are found
|
244
|
+
# in the class declaration.
|
245
|
+
def restore_state(state, target)
|
246
|
+
for param in @param_list
|
247
|
+
if state.has_key?(param.name)
|
248
|
+
param.set_raw(target, state[param.name])
|
249
|
+
end
|
250
|
+
end
|
251
|
+
return target
|
252
|
+
end
|
253
|
+
|
254
|
+
# Returns the default state of an freshly created instance.
|
255
|
+
# This way, you can create a well-defined state of one
|
256
|
+
# instance via parameters set thus:
|
257
|
+
#
|
258
|
+
# state = desc.default_state.update({'param' -> "My Value"})
|
259
|
+
def default_state
|
260
|
+
return save_state(instantiate)
|
261
|
+
end
|
262
|
+
|
263
|
+
# Creates a new (default) instance of the #object_class associated
|
264
|
+
# with this Description. Although this method takes additional
|
265
|
+
# parameters that are fed to the new method of the class, its
|
266
|
+
# use is strongly discouraged.
|
267
|
+
def instantiate(*a)
|
268
|
+
return @object_class.new(*a)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Creates a new instance of the #object_class associated with
|
272
|
+
# this description and fills its state
|
273
|
+
def recreate_state(state)
|
274
|
+
obj = instantiate
|
275
|
+
restore_state(state, obj)
|
276
|
+
return obj
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
280
|
+
|
281
|
+
# This module should be used in conjunction with DescriptionExtend to
|
282
|
+
# provide Descriptions to a class or a family of classes:
|
283
|
+
#
|
284
|
+
# class NiceClass
|
285
|
+
# extend DescriptionExtend
|
286
|
+
# include DescriptionInclude
|
287
|
+
# # real code now
|
288
|
+
# end
|
289
|
+
#
|
290
|
+
# The DescriptionInclude in itself provide some small facilities: a mehod
|
291
|
+
# to access directly the #description of an object (and its #long_name),
|
292
|
+
# a series of methods
|
293
|
+
# to set|get parameters based on their name (#parameter, #get_param,
|
294
|
+
# #get_param_raw, #set_param, #set_param_raw) and some methods to fill
|
295
|
+
# an OptionParser (#option_parser_fill, #option_parser_options,
|
296
|
+
# #option_parser_banner)
|
297
|
+
|
298
|
+
module DescriptionInclude
|
299
|
+
|
300
|
+
# Returns the description associated with the class.
|
301
|
+
def description
|
302
|
+
return self.class.description
|
303
|
+
end
|
304
|
+
|
305
|
+
|
306
|
+
# Returns the long name of the class
|
307
|
+
def long_name
|
308
|
+
return description.long_name
|
309
|
+
end
|
310
|
+
|
311
|
+
# Small functions to deal with the Parameter objects associated with
|
312
|
+
# the class. They are absolutely not necessary to actually access the
|
313
|
+
# data, but can come in really useful at some points.
|
314
|
+
|
315
|
+
# Returns the Parameter object associated with the named parameter
|
316
|
+
def parameter(param)
|
317
|
+
return description.param_hash.fetch(param)
|
318
|
+
end
|
319
|
+
|
320
|
+
# Query the value of a parameter, returns a String
|
321
|
+
def get_param(p)
|
322
|
+
return parameter(p).get(self)
|
323
|
+
end
|
324
|
+
|
325
|
+
# Query the value of a parameter, returns the actual value
|
326
|
+
def get_param_raw(p)
|
327
|
+
return parameter(p).get_raw(self)
|
328
|
+
end
|
329
|
+
|
330
|
+
# Sets the value of a paramete according to a String
|
331
|
+
def set_param(param, str)
|
332
|
+
return parameter(param).set(self, str)
|
333
|
+
end
|
334
|
+
|
335
|
+
# Sets directly the value of a parameter
|
336
|
+
def set_param_raw(param, val)
|
337
|
+
return parameter(param).set_raw(self, val)
|
338
|
+
end
|
339
|
+
|
340
|
+
# OptionParser related methods.
|
341
|
+
|
342
|
+
# Fills an OptionParser with their parameters. Most probably, the
|
343
|
+
# default implementation should do for most cases. _uniquify_ asks
|
344
|
+
# if we should try to make the command-line options as unique as
|
345
|
+
# reasonable to do. If _groups_ is true, organize parameters
|
346
|
+
# in groups.
|
347
|
+
def option_parser_fill(parser, uniquify = true, groups = false)
|
348
|
+
description.option_parser_fill(parser, self,
|
349
|
+
uniquify, groups)
|
350
|
+
end
|
351
|
+
|
352
|
+
# Provides only a banner for the current class to the OptionParser
|
353
|
+
# _parser_.
|
354
|
+
def option_parser_banner(parser)
|
355
|
+
description.parser_banner(parser, self)
|
356
|
+
end
|
357
|
+
|
358
|
+
# Prepares an option parser for this instance. See
|
359
|
+
# Description#option_parser_options
|
360
|
+
def option_parser_options(parser, uniquify = true, groups = false)
|
361
|
+
description.option_parser_options(parser, self, uniquify, groups)
|
362
|
+
end
|
363
|
+
|
364
|
+
# SaveState and the like:
|
365
|
+
|
366
|
+
# Calls Description#save_state on this object
|
367
|
+
def save_state
|
368
|
+
return description.save_state(self)
|
369
|
+
end
|
370
|
+
|
371
|
+
# Restores a previously saved state. (Well, it doesn't need to
|
372
|
+
# be previously saved, you can make it up if you like).
|
373
|
+
def restore_state(state)
|
374
|
+
description.restore_state(state, self)
|
375
|
+
end
|
376
|
+
|
377
|
+
end
|
378
|
+
|
379
|
+
# This module should be used with +extend+ to provide the class with
|
380
|
+
# descriptions functionnalities. You also need to +include+
|
381
|
+
# DescriptionInclude. Please not that all the *instance* methods
|
382
|
+
# defined here will become *class* methods in the class you extend.
|
383
|
+
#
|
384
|
+
# This module defines several methods to add a description (#describe) to a
|
385
|
+
# class, to add parameters (#param, #param_noaccess) and to import
|
386
|
+
# parameters from parents (#inherit_parameters).
|
387
|
+
#
|
388
|
+
# Factories can be created using the #craete_factory statement.
|
389
|
+
# This makes the
|
390
|
+
# current class the factory repository for all the subclasses. It creates
|
391
|
+
# a factory class method returning the base factory.
|
392
|
+
# You can use #register_class to register the current class into the
|
393
|
+
# base factory class.
|
394
|
+
module DescriptionExtend
|
395
|
+
|
396
|
+
# The functions for factory handling.
|
397
|
+
|
398
|
+
# Makes this class the factory class for all subclasses.
|
399
|
+
# It creates four class methods: base_factory, that always
|
400
|
+
# points to the closest factory in the hierarchy
|
401
|
+
# and three methods used internally.
|
402
|
+
#
|
403
|
+
# If _auto_ is true, the subclasses are all automatically
|
404
|
+
# registered to the factory. If _register_self_ is true
|
405
|
+
# the class itself is registered. It is probably not a good
|
406
|
+
# idea, so it is off by default.
|
407
|
+
def create_factory(auto = true, register_self = false)
|
408
|
+
cls = self
|
409
|
+
# we create a temporary module so that we can use
|
410
|
+
# define_method with a block and extend this class with it
|
411
|
+
mod = Module.new
|
412
|
+
mod.send(:define_method, :factory) do
|
413
|
+
return cls
|
414
|
+
end
|
415
|
+
mod.send(:define_method, :private_description_list) do
|
416
|
+
return @registered_descriptions
|
417
|
+
end
|
418
|
+
mod.send(:define_method, :private_description_hash) do
|
419
|
+
return @registered_descriptions_hash
|
420
|
+
end
|
421
|
+
# Creates an accessor for the factory class
|
422
|
+
mod.send(:define_method, :private_auto_register) do
|
423
|
+
@auto_register_subclasses
|
424
|
+
end
|
425
|
+
self.extend(mod)
|
426
|
+
|
427
|
+
# Creates the necessary arrays|hashes to handle the registered
|
428
|
+
# classes:
|
429
|
+
@registered_descriptions = []
|
430
|
+
@registered_descriptions_hash = {}
|
431
|
+
|
432
|
+
@auto_register_subclasses = auto
|
433
|
+
end
|
434
|
+
|
435
|
+
# Checks if the class has a factory
|
436
|
+
def has_factory?
|
437
|
+
return self.respond_to?(:factory)
|
438
|
+
end
|
439
|
+
|
440
|
+
# Returns the base description if there is one, or nil if there isn't
|
441
|
+
def base_description
|
442
|
+
if has_factory?
|
443
|
+
return factory.description
|
444
|
+
else
|
445
|
+
return nil
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
# Returns the description list of the factory. Raises an exception if
|
450
|
+
# there is no factory
|
451
|
+
def factory_description_list
|
452
|
+
raise "Must have a factory" unless has_factory?
|
453
|
+
return factory.private_description_list
|
454
|
+
end
|
455
|
+
|
456
|
+
# Returns the description hash of the factory. Raises an exception if
|
457
|
+
# there is no factory
|
458
|
+
def factory_description_hash
|
459
|
+
raise "Must have a factory" unless has_factory?
|
460
|
+
return factory.private_description_hash
|
461
|
+
end
|
462
|
+
|
463
|
+
# Returns the factory description with the given name
|
464
|
+
def factory_description(name)
|
465
|
+
raise "Must have a factory" unless has_factory?
|
466
|
+
return factory_description_hash.fetch(name)
|
467
|
+
end
|
468
|
+
|
469
|
+
# Returns the Class object associated with the given
|
470
|
+
# name in the factory
|
471
|
+
def factory_class(name)
|
472
|
+
return factory_description(name).object_class
|
473
|
+
end
|
474
|
+
|
475
|
+
# Registers the given description to the factory. If no description
|
476
|
+
# is given, the current class is registered.
|
477
|
+
def register_class(desc = nil)
|
478
|
+
raise "One of the superclasses should have a 'factory' statement" unless
|
479
|
+
has_factory?
|
480
|
+
desc = description if desc.nil?
|
481
|
+
factory_description_list << desc
|
482
|
+
factory_description_hash[desc.name] = desc
|
483
|
+
end
|
484
|
+
|
485
|
+
# Returns the Description of the class.
|
486
|
+
def description
|
487
|
+
return @description
|
488
|
+
end
|
489
|
+
|
490
|
+
# Sets the description of the class. It is probably way better to use
|
491
|
+
# #describe, or write your own class method in the base class in the
|
492
|
+
# case of a family of classes.
|
493
|
+
def set_description(desc)
|
494
|
+
@description = desc
|
495
|
+
if has_factory? and factory.private_auto_register
|
496
|
+
register_class
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
|
501
|
+
# Registers an new parameter, with the following properties:
|
502
|
+
# * _writer_ is the name of the method used to write that parameter;
|
503
|
+
# * _reader_ the name of the method that returns its current value;
|
504
|
+
# * _name_ is a short code-like name of the parameter (typically
|
505
|
+
# lowercase);
|
506
|
+
# * _long_name_ is a more descriptive name, properly capitalized and
|
507
|
+
# localized if possible;
|
508
|
+
# * _type_ is it's type. Please see the MetaBuilder::ParameterType
|
509
|
+
# for a better description of what is a type;
|
510
|
+
# * _desc_ is a proper (short) description of the parameter,
|
511
|
+
# something that would fit on a What's this box, for instance.
|
512
|
+
# * _attrs_ are optional parameters that may come useful, see
|
513
|
+
# Parameter#attributes documentation.
|
514
|
+
#
|
515
|
+
# You might want to use the #param_reader, #param_writer, and
|
516
|
+
# #param_accessor facilities that create the respective accessors
|
517
|
+
# in addition. A typical example would be:
|
518
|
+
#
|
519
|
+
# param :set_size, :size, 'size', "Size", {:type => :integer},
|
520
|
+
# "The size !!"
|
521
|
+
#
|
522
|
+
def param(writer, reader, name, long_name, type,
|
523
|
+
desc = "", attrs = {})
|
524
|
+
raise "Use describe first" if description.nil?
|
525
|
+
param = MetaBuilder::Parameter.new(name, writer, reader,
|
526
|
+
long_name,
|
527
|
+
type, desc, attrs)
|
528
|
+
description.add_param(param)
|
529
|
+
return param
|
530
|
+
end
|
531
|
+
|
532
|
+
# The same as #param, but creates a attr_reader in addition
|
533
|
+
def param_reader(writer, reader, name, long_name, type,
|
534
|
+
desc = "", attrs = {})
|
535
|
+
p = param(writer, reader, name, long_name, type, desc, attrs)
|
536
|
+
attr_reader reader
|
537
|
+
return p
|
538
|
+
end
|
539
|
+
|
540
|
+
# The same as #param, except that _writer_ is made from _symbol_ by
|
541
|
+
# appending a = at the end. An attr_writer is created for the _symbol_.
|
542
|
+
def param_writer(symbol, name, long_name, type,
|
543
|
+
desc = "", attrs = {})
|
544
|
+
writer = (symbol.to_s + '=').to_sym
|
545
|
+
p = param(writer, symbol, name, long_name, type, desc, attrs)
|
546
|
+
attr_writer symbol
|
547
|
+
return p
|
548
|
+
end
|
549
|
+
|
550
|
+
# The same as #param_writer, except that an attr_writer is created
|
551
|
+
# for the _symbol_ instead of only a attr_writer. The most useful of
|
552
|
+
# the four methods. Typical use:
|
553
|
+
#
|
554
|
+
# param_accessor :name, 'name', "Object name", {:type => :string},
|
555
|
+
# "The name of the object"
|
556
|
+
def param_accessor(symbol, name, long_name, type,
|
557
|
+
desc = "", attrs = {})
|
558
|
+
writer = (symbol.to_s + '=').to_sym
|
559
|
+
p = param(writer, symbol, name, long_name, type, desc, attrs)
|
560
|
+
attr_accessor symbol
|
561
|
+
return p
|
562
|
+
end
|
563
|
+
|
564
|
+
# Creates a description attached to the current class. It needs to be
|
565
|
+
# used before anything else.
|
566
|
+
def describe(name, longname = name, desc = "")
|
567
|
+
d = Description.new(self, name, longname, desc)
|
568
|
+
set_description(d)
|
569
|
+
end
|
570
|
+
|
571
|
+
|
572
|
+
# Imports the given parameters directly from the parent class.
|
573
|
+
# This function is quite naive and will not look further than
|
574
|
+
# the direct #superclass.
|
575
|
+
def inherit_parameters(*names)
|
576
|
+
if self.superclass.respond_to?(:description)
|
577
|
+
parents_params = self.superclass.description.param_hash
|
578
|
+
for n in names
|
579
|
+
if parents_params.key?(n)
|
580
|
+
description.add_param(parents_params[n])
|
581
|
+
else
|
582
|
+
warn "Param #{n} not found"
|
583
|
+
end
|
584
|
+
end
|
585
|
+
else
|
586
|
+
warn "The parent class has no description"
|
587
|
+
end
|
588
|
+
end
|
589
|
+
|
590
|
+
# Switches to the given ParameterGroup. If it doesn't exist, it is
|
591
|
+
# created with the given parameters.
|
592
|
+
def group(name, long_name = name, desc = nil)
|
593
|
+
if not description.group_hash.has_key?(name)
|
594
|
+
group = ParameterGroup.new(name, long_name, desc)
|
595
|
+
description.add_group(group)
|
596
|
+
end
|
597
|
+
description.switch_to_group(name)
|
598
|
+
end
|
599
|
+
|
600
|
+
end
|
601
|
+
|
602
|
+
end
|
603
|
+
|