The requested changes are too big and content was truncated. Show full diff
@@ -1,11 +1,13 | |||
|
1 | 1 | ## CHANGELOG: |
|
2 | 2 | |
|
3 | 3 | ### 3.0 |
|
4 | * Python 3.x compatible | |
|
5 |
* New architecture with multiprocessing |
|
|
6 |
* Add @MPDecorator for multiprocessing |
|
|
4 | * Python 3.x & 2.X compatible | |
|
5 | * New architecture with multiprocessing support | |
|
6 | * Add @MPDecorator for multiprocessing Operations (Plots, Writers and Publishers) | |
|
7 | 7 | * Added new type of operation `external` for non-locking operations |
|
8 | 8 | * New plotting architecture with buffering/throttle capabilities to speed up plots |
|
9 | * Clean controller to optimize scripts (format & optype are no longer required) | |
|
10 | * New GUI with dinamic load of Units and operations (use Kivy framework) | |
|
9 | 11 | |
|
10 | 12 | ### 2.3 |
|
11 | 13 | * Added support for Madrigal formats (reading/writing). |
@@ -1,1 +0,0 | |||
|
1 | from viewcontroller import * No newline at end of file |
@@ -1,39 +1,325 | |||
|
1 | 1 | #!/usr/bin/env python |
|
2 | 2 | import os |
|
3 | 3 | import sys |
|
4 | import ast | |
|
5 | ||
|
6 | from schainpy.controller import Project | |
|
7 | from schainpy.cli import cli | |
|
4 | 8 | from schainpy.utils import log |
|
5 | 9 | |
|
6 | 10 | try: |
|
7 | from PyQt4 import QtCore, QtGui | |
|
8 |
from |
|
|
11 | import kivy | |
|
12 | from kivy.app import App | |
|
13 | from kivy.uix.label import Label | |
|
14 | from kivy.uix.boxlayout import BoxLayout | |
|
15 | from kivy.uix.gridlayout import GridLayout | |
|
16 | from kivy.uix.textinput import TextInput | |
|
17 | from kivy.uix.button import Button | |
|
18 | from kivy.uix.dropdown import DropDown | |
|
19 | from kivy.uix.togglebutton import ToggleButton | |
|
20 | from kivy.uix.popup import Popup | |
|
21 | from kivy.uix.filechooser import FileChooserListView | |
|
9 | 22 | except: |
|
10 | 23 | log.error( |
|
11 |
'You should install |
|
|
24 | 'You should install kivy module in order to run the GUI.') | |
|
12 | 25 | sys.exit() |
|
13 | 26 | |
|
14 | from schainpy.gui.viewcontroller.initwindow import InitWindow | |
|
15 | from schainpy.gui.viewcontroller.basicwindow import BasicWindow | |
|
16 | from schainpy.gui.viewcontroller.workspace import Workspace | |
|
17 | 27 | |
|
28 | DEFAULTS = { | |
|
29 | 'path': os.path.expanduser('~'), | |
|
30 | 'startDate': '2018/01/01', | |
|
31 | 'endDate': '2020/01/01', | |
|
32 | 'startTime': '00:00:00', | |
|
33 | 'endTime': '23:59:59', | |
|
34 | 'online': '1', | |
|
35 | 'delay': '30', | |
|
36 | 'walk': '1', | |
|
37 | 'show': '1', | |
|
38 | 'zmin': '10', | |
|
39 | 'zmax': '40', | |
|
40 | } | |
|
41 | ||
|
42 | ||
|
43 | class MainLayout(BoxLayout): | |
|
44 | def __init__(self, **kwargs): | |
|
45 | super(MainLayout, self).__init__(**kwargs) | |
|
46 | ||
|
47 | self.workspace = os.path.join(os.path.expanduser('~'), 'workspace/scripts') | |
|
48 | self.current_unit_id = None | |
|
49 | self._units = [] | |
|
50 | self.project = Project() | |
|
51 | self.project.setup(id='1', name='test', description='') | |
|
52 | ||
|
53 | self.sidebar_left = BoxLayout(orientation='vertical', size_hint_x=0.4, spacing=5) | |
|
54 | self.body = BoxLayout(orientation='vertical', spacing=5) | |
|
55 | self.sidebar_right = BoxLayout(orientation='vertical', size_hint_x=0.6, spacing=5) | |
|
56 | ||
|
57 | bt_prj = Button(text='Project') | |
|
58 | bt_prj.bind(on_press=self.show_project) | |
|
59 | self.sidebar_left.add_widget(bt_prj) | |
|
60 | ||
|
61 | bt_add_unit = Button(text='Add Unit') | |
|
62 | bt_add_unit.bind(on_press=self.select_unit) | |
|
63 | self.sidebar_left.add_widget(bt_add_unit) | |
|
64 | ||
|
65 | bt_add_operation = Button(text='Add Operation') | |
|
66 | bt_add_operation.bind(on_press=self.select_operation) | |
|
67 | self.sidebar_left.add_widget(bt_add_operation) | |
|
68 | ||
|
69 | bt_import = Button(text='Import') | |
|
70 | bt_import.bind(on_press=self.load) | |
|
71 | self.sidebar_left.add_widget(bt_import) | |
|
72 | ||
|
73 | bt_export = Button(text='Export') | |
|
74 | bt_export.bind(on_press=self.export) | |
|
75 | self.sidebar_left.add_widget(bt_export) | |
|
76 | ||
|
77 | bt_run = Button(text='Run') | |
|
78 | bt_run.bind(on_press=self.run) | |
|
79 | self.sidebar_left.add_widget(bt_run) | |
|
80 | ||
|
81 | bt_stop = Button(text='Stop') | |
|
82 | bt_stop.bind(on_press = self.stop) | |
|
83 | self.sidebar_left.add_widget(bt_stop) | |
|
84 | ||
|
85 | bt_exit = Button(text = 'Exit', height = 40, size_hint_y = None, background_color=(1, 0, 0, 1)) | |
|
86 | bt_exit.bind(on_press=App.get_running_app().stop) | |
|
87 | self.sidebar_left.add_widget(bt_exit) | |
|
88 | ||
|
89 | self.add_widget(self.sidebar_left) | |
|
90 | self.add_widget(self.body) | |
|
91 | self.add_widget(self.sidebar_right) | |
|
92 | ||
|
93 | def update_body(self): | |
|
94 | ||
|
95 | self._units = [] | |
|
96 | self.body.clear_widgets() | |
|
97 | self.sidebar_right.clear_widgets() | |
|
98 | ||
|
99 | for unit in self.project.getUnits(): | |
|
100 | box = GridLayout(cols=3) | |
|
101 | bt = ToggleButton(text=unit.name, group='units') | |
|
102 | bt._obj = unit | |
|
103 | bt.bind(on_press=self.show_parameters) | |
|
104 | box.add_widget(bt) | |
|
105 | self._units.append(bt) | |
|
106 | ||
|
107 | for operation in unit.operations: | |
|
108 | bt_op = Button(text = operation.name, background_color=(1, 0.5, 0, 1)) | |
|
109 | bt_op._id = unit.id | |
|
110 | bt_op._obj = operation | |
|
111 | bt_op.bind(on_press=self.show_parameters) | |
|
112 | box.add_widget(bt_op) | |
|
113 | ||
|
114 | self.body.add_widget(box) | |
|
115 | ||
|
116 | print(self.project) | |
|
117 | ||
|
118 | def show_parameters(self, instance): | |
|
119 | ||
|
120 | obj = instance._obj | |
|
121 | self.current_unit_id = obj.id | |
|
122 | self.sidebar_right.clear_widgets() | |
|
123 | ||
|
124 | if obj and obj.parameters: | |
|
125 | self._params = {} | |
|
126 | ||
|
127 | for key, value in obj.getParameters().items(): | |
|
128 | self.sidebar_right.add_widget(Label(text=key)) | |
|
129 | text = TextInput(text=value, multiline=False) | |
|
130 | self._params[key] = text | |
|
131 | self.sidebar_right.add_widget(text) | |
|
132 | ||
|
133 | bt_save = Button(text = 'Save', height = 40, size_hint_y = None, background_color=(0, 1, 0, 1)) | |
|
134 | bt_save._obj = obj | |
|
135 | if hasattr(instance, '_id'): | |
|
136 | bt_save._id = instance._id | |
|
137 | self.current_unit_id = None | |
|
138 | bt_save.bind(on_press=self.save_parameters) | |
|
139 | self.sidebar_right.add_widget(bt_save) | |
|
140 | ||
|
141 | bt_delete = Button(text = 'Delete', height = 40, size_hint_y = None, background_color=(1, 0, 0, 1)) | |
|
142 | bt_delete._obj = obj | |
|
143 | if hasattr(instance, '_id'): | |
|
144 | bt_delete._id = instance._id | |
|
145 | self.current_unit_id = obj.id | |
|
146 | bt_delete.bind(on_press=self.delete_object) | |
|
147 | self.sidebar_right.add_widget(bt_delete) | |
|
148 | ||
|
149 | def save_parameters(self, instance): | |
|
150 | ||
|
151 | obj = instance._obj | |
|
152 | params = {} | |
|
153 | for key in self._params: | |
|
154 | if self._params[key]: | |
|
155 | params[key] = self._params[key].text | |
|
156 | ||
|
157 | if hasattr(instance, '_id'): | |
|
158 | unit = self.project.getProcUnit(instance._id) | |
|
159 | op = unit.getOperation(obj.id) | |
|
160 | op.update(**params) | |
|
161 | else: | |
|
162 | unit = self.project.getProcUnit(obj.id) | |
|
163 | unit.update(**params) | |
|
164 | ||
|
165 | def delete_object(self, instance): | |
|
166 | ||
|
167 | obj = instance._obj | |
|
168 | ||
|
169 | if hasattr(instance, '_id'): | |
|
170 | unit = self.project.getProcUnit(instance._id) | |
|
171 | unit.removeOperation(obj.id) | |
|
172 | else: | |
|
173 | self.project.removeProcUnit(obj.id) | |
|
174 | ||
|
175 | self.project.updateId(self.project.id) | |
|
176 | self.update_body() | |
|
177 | ||
|
178 | def show_project(self, instance): | |
|
179 | ||
|
180 | self.sidebar_right.clear_widgets() | |
|
181 | self._params = {} | |
|
182 | for label in ['Id', 'Name', 'Description']: | |
|
183 | self.sidebar_right.add_widget(Label(text=label)) | |
|
184 | text = TextInput(text=getattr(self.project, label.lower()), multiline=False) | |
|
185 | self._params[label] = text | |
|
186 | self.sidebar_right.add_widget(text) | |
|
187 | ||
|
188 | self.sidebar_right.add_widget(Label(text='Workspace')) | |
|
189 | text = TextInput(text=getattr(self, 'workspace'), multiline=False) | |
|
190 | self._params['Workspace'] = text | |
|
191 | self.sidebar_right.add_widget(text) | |
|
192 | ||
|
193 | bt_save = Button(text = 'Save', height = 40, size_hint_y = None, background_color=(0, 1, 0, 1)) | |
|
194 | bt_save.bind(on_press = self.save_project_parameters) | |
|
195 | self.sidebar_right.add_widget(bt_save) | |
|
196 | ||
|
197 | def save_project_parameters(self, instance): | |
|
198 | ||
|
199 | for label in ['Id', 'Name', 'Description']: | |
|
200 | setattr(self.project, label.lower(), self._params[label].text) | |
|
201 | ||
|
202 | setattr(self, 'workspace', self._params['Workspace'].text) | |
|
203 | ||
|
204 | def select_unit(self, instance): | |
|
205 | ||
|
206 | self.sidebar_right.clear_widgets() | |
|
207 | bt_main = Button(text = 'Select Unit', height = 40, size_hint_y = None) | |
|
208 | dropdown = DropDown() | |
|
209 | ||
|
210 | for unit in cli.getProcs(): | |
|
211 | ||
|
212 | btn = Button(text = unit, size_hint_y = None, height = 40) | |
|
213 | btn.bind(on_release = lambda btn: dropdown.select(btn.text)) | |
|
214 | dropdown.add_widget(btn) | |
|
215 | ||
|
216 | bt_main.bind(on_release = dropdown.open) | |
|
217 | dropdown.bind(on_select = lambda instance, x: setattr(bt_main, 'text', x)) | |
|
218 | ||
|
219 | bt_add = Button(text = 'Add', height = 40, size_hint_y = None, background_color=(0, 1, 0, 1)) | |
|
220 | bt_add.bind(on_press = lambda instance: self.add_unit(bt_main.text)) | |
|
221 | ||
|
222 | self.sidebar_right.add_widget(bt_main) | |
|
223 | self.sidebar_right.add_widget(bt_add) | |
|
224 | ||
|
225 | def add_unit(self, s): | |
|
226 | ||
|
227 | if s: | |
|
228 | if 'Reader' in s: | |
|
229 | unit = self.project.addReadUnit(name=s) | |
|
230 | else: | |
|
231 | *_, last = self.project.getUnits() | |
|
232 | unit = self.project.addProcUnit(name=s, inputId=last.id) | |
|
233 | ||
|
234 | keys = cli.getArgs(unit.name) | |
|
235 | values = [DEFAULTS[key] if key in DEFAULTS else '' for key in keys] | |
|
236 | unit.update(**dict(zip(keys, values))) | |
|
237 | self.update_body() | |
|
238 | ||
|
239 | def select_operation(self, instance): | |
|
240 | ||
|
241 | self.sidebar_right.clear_widgets() | |
|
242 | btns = [bt.state == 'down' for bt in self._units] | |
|
243 | if True in btns: | |
|
244 | bt_main = Button(text = 'Select Operation', height = 40, size_hint_y = None) | |
|
245 | dropdown = DropDown() | |
|
246 | ||
|
247 | for unit in cli.getOperations(): | |
|
248 | ||
|
249 | btn = Button(text = unit, size_hint_y = None, height = 40) | |
|
250 | btn.bind(on_release = lambda btn: dropdown.select(btn.text)) | |
|
251 | dropdown.add_widget(btn) | |
|
252 | ||
|
253 | bt_main.bind(on_release = dropdown.open) | |
|
254 | dropdown.bind(on_select = lambda instance, x: setattr(bt_main, 'text', x)) | |
|
255 | ||
|
256 | bt_add = Button(text = 'Add', height = 40, size_hint_y = None, background_color=(0, 1, 0, 1)) | |
|
257 | bt_add.bind(on_press = lambda instance: self.add_operation(bt_main.text)) | |
|
258 | ||
|
259 | self.sidebar_right.add_widget(bt_main) | |
|
260 | self.sidebar_right.add_widget(bt_add) | |
|
261 | else: | |
|
262 | self.sidebar_right.add_widget(Label(text='Select Unit')) | |
|
263 | ||
|
264 | def add_operation(self, s): | |
|
265 | ||
|
266 | if s: | |
|
267 | unit = self.project.getProcUnit(self.current_unit_id) | |
|
268 | op = unit.addOperation(name=s) | |
|
269 | keys = cli.getArgs(op.name) | |
|
270 | values = [DEFAULTS[key] if key in DEFAULTS else '' for key in keys] | |
|
271 | op.update(**dict(zip(keys, values))) | |
|
272 | self.update_body() | |
|
273 | ||
|
274 | def run(self, instance): | |
|
275 | ||
|
276 | if self.project and self.project.is_alive(): | |
|
277 | self.sidebar_right.clear_widgets() | |
|
278 | self.sidebar_right.add_widget(Label(text='Project running')) | |
|
279 | else: | |
|
280 | if self.project.exitcode is None: | |
|
281 | self.project.start() | |
|
282 | else: | |
|
283 | self.project = self.project.clone() | |
|
284 | self.project.start() | |
|
285 | ||
|
286 | def stop(self, instance): | |
|
287 | ||
|
288 | if self.project and self.project.is_alive(): | |
|
289 | self.project.kill() | |
|
290 | log.error('Project Stopped by user', 'GUI') | |
|
291 | else: | |
|
292 | self.sidebar_right.clear_widgets() | |
|
293 | self.sidebar_right.add_widget(Label(text='Project not running')) | |
|
294 | ||
|
295 | def load(self, instance): | |
|
296 | ||
|
297 | self.sidebar_right.clear_widgets() | |
|
298 | textinput = FileChooserListView( | |
|
299 | path=self.workspace, size_hint=(1, 1), dirselect=False, filters=['*.xml']) | |
|
300 | ||
|
301 | self.sidebar_right.add_widget(textinput) | |
|
302 | bt_open = Button(text = 'Open', height = 40, size_hint_y = None, background_color=(0, 1, 0, 1)) | |
|
303 | bt_open.textinput = textinput | |
|
304 | bt_open.bind(on_press = self.load_file) | |
|
305 | self.sidebar_right.add_widget(bt_open) | |
|
18 | 306 | |
|
19 | def main(): | |
|
307 | def load_file(self, instance): | |
|
20 | 308 | |
|
21 | app = QtGui.QApplication(sys.argv) | |
|
309 | self.project.readXml(instance.textinput.selection[0]) | |
|
310 | self.update_body() | |
|
22 | 311 | |
|
23 | Welcome = InitWindow() | |
|
312 | def export(self, instance): | |
|
24 | 313 | |
|
25 | if not Welcome.exec_(): | |
|
26 | sys.exit(-1) | |
|
314 | filename = os.path.join(self.workspace, '{}.xml'.format(self.project.name)) | |
|
315 | self.project.writeXml(filename) | |
|
316 | log.success('File created: {}'.format(filename), 'GUI') | |
|
27 | 317 | |
|
28 | WorkPathspace = Workspace() | |
|
29 | if not WorkPathspace.exec_(): | |
|
30 | sys.exit(-1) | |
|
31 | 318 | |
|
32 | MainGUI = BasicWindow() | |
|
33 | MainGUI.setWorkSpaceGUI(WorkPathspace.dirComBox.currentText()) | |
|
34 | MainGUI.show() | |
|
35 | sys.exit(app.exec_()) | |
|
319 | class SignalChainApp(App): | |
|
320 | def build(self): | |
|
321 | return MainLayout(spacing=10) | |
|
36 | 322 | |
|
37 | 323 | |
|
38 | 324 | if __name__ == "__main__": |
|
39 | main() | |
|
325 | SignalChainApp().run() No newline at end of file |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed | |
This diff has been collapsed as it changes many lines, (1765 lines changed) Show them Hide them |
|
1 | NO CONTENT: file was removed | |
This diff has been collapsed as it changes many lines, (2179 lines changed) Show them Hide them |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: file was removed | |
The requested commit or file is too big and content was truncated. Show full diff |
General Comments 0
You need to be logged in to leave comments.
Login now