Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
T
tuneit
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Ximper Linux
tuneit
Commits
ca1ae92c
Commit
ca1ae92c
authored
Feb 23, 2025
by
Roman Alifanov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
a new type of `custom` section with callbacks and interaction of one widget with other widgets
It has not been fully tested, it may be unstable
parent
861914f8
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
360 additions
and
32 deletions
+360
-32
__init__.py
src/settings/sections/__init__.py
+2
-0
base.py
src/settings/sections/base.py
+1
-2
custom.py
src/settings/sections/custom.py
+79
-0
custom_setting.py
src/settings/setting/custom_setting.py
+207
-0
setting.py
src/settings/setting/setting.py
+11
-8
BaseWidget.py
src/settings/setting/widgets/BaseWidget.py
+10
-2
BooleanWidget.py
src/settings/setting/widgets/BooleanWidget.py
+3
-0
ChoiceWidget.py
src/settings/setting/widgets/ChoiceWidget.py
+14
-5
EntryWidget.py
src/settings/setting/widgets/EntryWidget.py
+10
-5
FileChooser.py
src/settings/setting/widgets/FileChooser.py
+8
-8
NumStepper.py
src/settings/setting/widgets/NumStepper.py
+6
-0
RadioChoiceWidget.py
src/settings/setting/widgets/RadioChoiceWidget.py
+9
-2
No files found.
src/settings/sections/__init__.py
View file @
ca1ae92c
from
.classic
import
ClassicSection
from
.custom
import
CustomSection
class
SectionFactory
:
def
__init__
(
self
):
self
.
sections
=
{
'classic'
:
ClassicSection
,
'custom'
:
CustomSection
,
}
def
create_section
(
self
,
section_data
,
module
):
...
...
src/settings/sections/base.py
View file @
ca1ae92c
class
BaseSection
():
def
__init__
(
self
,
section_data
,
module
):
self
.
section_data
=
section_data
self
.
name
=
module
.
get_translation
(
section_data
[
'name'
])
self
.
weight
=
section_data
.
get
(
'weight'
,
0
)
self
.
page
=
section_data
.
get
(
'page'
)
\ No newline at end of file
src/settings/sections/custom.py
0 → 100644
View file @
ca1ae92c
from
gi.repository
import
Adw
from
..setting.custom_setting
import
CustomSetting
from
.base
import
BaseSection
import
logging
class
CustomSection
(
BaseSection
):
def
__init__
(
self
,
section_data
,
module
):
super
()
.
__init__
(
section_data
,
module
)
self
.
logger
=
logging
.
getLogger
(
f
"{self.__class__.__name__}[{self.name}]"
)
self
.
settings
=
[
CustomSetting
(
s
,
module
,
self
)
for
s
in
section_data
.
get
(
'settings'
,
[])]
self
.
settings_dict
=
{
s
.
orig_name
:
s
for
s
in
self
.
settings
}
self
.
module
=
module
self
.
module
.
add_section
(
self
)
self
.
_callback_buffer
=
[]
def
create_preferences_group
(
self
):
group
=
Adw
.
PreferencesGroup
(
title
=
self
.
name
,
description
=
self
.
module
.
name
)
not_empty
=
False
for
setting
in
self
.
settings
:
try
:
row
=
setting
.
create_row
()
if
row
:
print
(
f
"Adding a row for setting: {setting.name}"
)
group
.
add
(
row
)
not_empty
=
True
except
Exception
as
e
:
self
.
logger
.
error
(
f
"Error creating row for {setting.orig_name}: {str(e)}"
)
self
.
_process_buffered_callbacks
()
return
group
if
not_empty
else
None
def
handle_callback
(
self
,
action
,
target
,
value
):
self
.
logger
.
debug
(
f
"handled callback action={action}, target={target}, value={value}"
)
try
:
if
target
not
in
self
.
settings_dict
:
self
.
_callback_buffer
.
append
((
action
,
target
,
value
))
self
.
logger
.
debug
(
f
"Buffering callback for {target}"
)
return
self
.
_apply_callback
(
action
,
target
,
value
)
except
Exception
as
e
:
self
.
logger
.
error
(
f
"Callback handling error: {str(e)}"
)
def
_apply_callback
(
self
,
action
,
target
,
value
):
setting
=
self
.
settings_dict
[
target
]
if
action
==
'set'
:
setting
.
set_value
(
value
)
elif
action
==
'visible'
:
if
setting
.
row
:
setting
.
row
.
set_visible
(
value
.
lower
()
==
'true'
)
elif
action
==
'enabled'
:
if
setting
.
row
:
setting
.
row
.
set_sensitive
(
value
.
lower
()
==
'true'
)
else
:
self
.
logger
.
warning
(
f
"Unknown callback action: {action}"
)
def
_process_buffered_callbacks
(
self
):
while
self
.
_callback_buffer
:
action
,
target
,
value
=
self
.
_callback_buffer
.
pop
(
0
)
try
:
if
target
in
self
.
settings_dict
:
self
.
_apply_callback
(
action
,
target
,
value
)
else
:
self
.
logger
.
warning
(
f
"Unknown target after processing buffer: {target}"
)
except
Exception
as
e
:
self
.
logger
.
error
(
f
"Error processing buffered callback: {str(e)}"
)
def
get_all_values
(
self
):
return
{
setting
.
orig_name
:
setting
.
_current_value
for
setting
in
self
.
settings
}
src/settings/setting/custom_setting.py
0 → 100644
View file @
ca1ae92c
from
..searcher
import
SearcherFactory
from
.widgets
import
WidgetFactory
import
logging
import
subprocess
class
CustomSetting
:
def
__init__
(
self
,
setting_data
,
module
,
section
):
self
.
_
=
module
.
get_translation
self
.
module
=
module
self
.
section
=
section
self
.
logger
=
logging
.
getLogger
(
f
"CommandSetting[{setting_data['name']}]"
)
self
.
name
=
self
.
_
(
setting_data
[
'name'
])
self
.
orig_name
=
setting_data
[
'name'
]
self
.
type
=
setting_data
[
'type'
]
self
.
default
=
setting_data
.
get
(
'default'
,
''
)
self
.
help
=
self
.
_
(
setting_data
.
get
(
'help'
,
''
))
self
.
_current_value
=
None
self
.
get_command
=
setting_data
.
get
(
'get_command'
)
self
.
set_command
=
setting_data
.
get
(
'set_command'
)
self
.
gtype
=
setting_data
.
get
(
'gtype'
,
's'
)
if
len
(
self
.
gtype
)
>
2
:
self
.
gtype
=
self
.
gtype
[
0
]
else
:
self
.
gtype
=
self
.
gtype
self
.
search_target
=
setting_data
.
get
(
'search_target'
)
self
.
params
=
{
**
setting_data
.
get
(
'params'
,
{}),
'module_path'
:
module
.
path
}
self
.
widget
=
None
self
.
row
=
None
self
.
map
=
setting_data
.
get
(
'map'
)
if
self
.
map
is
None
:
if
self
.
search_target
is
not
None
:
self
.
map
=
SearcherFactory
.
create
(
self
.
search_target
)
.
search
()
else
:
self
.
map
=
self
.
_default_map
()
if
isinstance
(
self
.
map
,
list
)
and
'choice'
in
self
.
type
:
self
.
map
=
{
item
.
title
():
item
for
item
in
self
.
map
}
if
isinstance
(
self
.
map
,
dict
)
and
'choice'
in
self
.
type
:
self
.
map
=
{
self
.
_
(
key
)
if
isinstance
(
key
,
str
)
else
key
:
value
for
key
,
value
in
self
.
map
.
items
()
}
def
_default_map
(
self
):
if
self
.
type
==
'boolean'
:
# Дефолтная карта для булевых настроек
return
{
True
:
True
,
False
:
False
}
if
'choice'
in
self
.
type
:
# Дефолтная карта для выборов
map
=
{}
range
=
self
.
_get_backend_range
()
if
range
is
None
:
return
{}
for
var
in
range
:
print
(
var
)
map
[
var
[
0
]
.
upper
()
+
var
[
1
:]]
=
var
return
map
if
self
.
type
==
'number'
:
map
=
{}
range
=
self
.
_get_backend_range
()
if
range
is
None
:
return
{}
map
[
"upper"
]
=
range
[
1
]
map
[
"lower"
]
=
range
[
0
]
# Кол-во после запятой
map
[
"digits"
]
=
len
(
str
(
range
[
0
])
.
split
(
'.'
)[
-
1
])
if
'.'
in
str
(
range
[
0
])
else
0
# Минимальное число с этим количеством
map
[
"step"
]
=
10
**
-
map
[
"digits"
]
if
map
[
"digits"
]
>
0
else
0
return
map
return
{}
def
create_row
(
self
):
try
:
self
.
widget
=
WidgetFactory
.
create_widget
(
self
)
if
self
.
widget
:
self
.
row
=
self
.
widget
.
create_row
()
return
self
.
row
except
Exception
as
e
:
self
.
logger
.
error
(
f
"Error creating row: {str(e)}"
)
return
None
def
get_value
(
self
):
if
self
.
_current_value
is
None
:
self
.
_current_value
=
self
.
_execute_get_command
()
return
self
.
_current_value
def
set_value
(
self
,
value
):
success
=
self
.
_execute_set_command
(
value
)
if
success
:
self
.
_current_value
=
value
self
.
_update_widget
()
def
_get_selected_row_index
(
self
):
current_value
=
self
.
_get_backend_value
()
return
list
(
self
.
map
.
values
())
.
index
(
current_value
)
if
current_value
in
self
.
map
.
values
()
else
0
def
_get_default_row_index
(
self
):
return
list
(
self
.
map
.
values
())
.
index
(
self
.
default
)
if
self
.
default
in
self
.
map
.
values
()
else
None
def
_execute_get_command
(
self
):
if
not
self
.
get_command
:
return
self
.
default
try
:
cmd
=
self
.
_format_command
(
self
.
get_command
)
result
=
subprocess
.
run
(
cmd
,
check
=
True
,
stdout
=
subprocess
.
PIPE
,
stderr
=
subprocess
.
PIPE
,
text
=
True
,
shell
=
True
)
return
self
.
_process_output
(
result
.
stdout
)
except
subprocess
.
CalledProcessError
as
e
:
self
.
logger
.
error
(
f
"Get command failed: {e.stderr}"
)
return
self
.
default
def
_execute_set_command
(
self
,
value
):
if
not
self
.
set_command
:
return
False
try
:
cmd
=
self
.
_format_command
(
self
.
set_command
,
value
)
result
=
subprocess
.
run
(
cmd
,
check
=
True
,
stdout
=
subprocess
.
PIPE
,
stderr
=
subprocess
.
PIPE
,
text
=
True
,
shell
=
True
)
self
.
_process_output
(
result
.
stdout
)
return
True
except
subprocess
.
CalledProcessError
as
e
:
self
.
logger
.
error
(
f
"Set command failed: {e.stderr}"
)
return
False
def
_format_command
(
self
,
template
,
value
=
None
):
variables
=
{
'value'
:
value
,
**
self
.
params
,
**
self
.
section
.
get_all_values
()
}
return
template
.
format
(
**
variables
)
def
_process_output
(
self
,
output
):
lines
=
[]
for
line
in
output
.
split
(
'
\n
'
):
line
=
line
.
strip
()
if
line
.
startswith
(
'CALLBACK:'
):
self
.
_handle_callback
(
line
)
else
:
lines
.
append
(
line
)
return
'
\n
'
.
join
(
lines
)
.
strip
()
def
_handle_callback
(
self
,
line
):
try
:
_
,
action
,
target
,
value
=
line
.
split
(
':'
,
3
)
self
.
section
.
handle_callback
(
action
.
strip
(),
target
.
strip
(),
value
.
strip
()
)
except
ValueError
:
self
.
logger
.
error
(
f
"Invalid callback format: {line}"
)
def
_update_widget
(
self
):
if
self
.
widget
:
self
.
widget
.
update_display
()
@property
def
current_value
(
self
):
return
self
.
get_value
()
def
_get_backend_value
(
self
):
return
self
.
get_value
()
def
_set_backend_value
(
self
,
value
):
self
.
set_value
(
value
)
\ No newline at end of file
src/settings/setting/setting.py
View file @
ca1ae92c
...
...
@@ -33,6 +33,8 @@ class Setting:
self
.
default
=
setting_data
.
get
(
'default'
)
self
.
gtype
=
setting_data
.
get
(
'gtype'
,
[])
self
.
_current_value
=
None
self
.
search_target
=
setting_data
.
get
(
'search_target'
,
None
)
self
.
map
=
setting_data
.
get
(
'map'
)
...
...
@@ -120,15 +122,15 @@ class Setting:
return
list
(
self
.
map
.
values
())
.
index
(
self
.
default
)
if
self
.
default
in
self
.
map
.
values
()
else
None
def
_get_backend_value
(
self
):
value
=
None
backend
=
self
.
_get_backend
()
if
backend
:
value
=
backend
.
get_value
(
self
.
key
,
self
.
gtype
)
if
value
is
None
:
if
self
.
_current_value
is
None
:
backend
=
self
.
_get_backend
()
value
=
self
.
default
return
value
if
backend
:
value
=
backend
.
get_value
(
self
.
key
,
self
.
gtype
)
or
self
.
default
self
.
_current_value
=
value
return
self
.
_current_value
def
_get_backend_range
(
self
):
backend
=
self
.
_get_backend
()
...
...
@@ -139,6 +141,7 @@ class Setting:
backend
=
self
.
_get_backend
()
if
backend
:
backend
.
set_value
(
self
.
key
,
convert_by_gvariant
(
value
,
self
.
gtype
),
self
.
gtype
)
self
.
_current_value
=
value
def
_get_backend
(
self
):
if
self
.
root
is
True
:
...
...
src/settings/setting/widgets/BaseWidget.py
View file @
ca1ae92c
...
...
@@ -21,9 +21,17 @@ class BaseWidget:
reveal_child
=
False
,
halign
=
Gtk
.
Align
.
END
)
def
update_display
(
self
):
raise
NotImplementedError
(
"update_display method should be implemented in the subclass"
)
def
set_visible
(
self
,
visible
:
bool
):
self
.
row
.
set_visible
(
visible
)
def
set_enabled
(
self
,
enabled
:
bool
):
self
.
row
.
set_sensitive
(
enabled
)
def
create_row
(
self
):
raise
NotImplementedError
(
"
Метод create_row должен быть реализован в подклассе
"
)
raise
NotImplementedError
(
"
create_row method should be implemented in the subclass
"
)
def
_on_reset_clicked
(
self
,
button
):
raise
NotImplementedError
(
"
Метод _on_reset_clicked должен быть реализован в подклассе
"
)
raise
NotImplementedError
(
"
_on_reset_clicked method should be implemented in the subclass
"
)
src/settings/setting/widgets/BooleanWidget.py
View file @
ca1ae92c
...
...
@@ -26,6 +26,9 @@ class BooleanWidget(BaseWidget):
self
.
switch
.
set_active
(
is_active
)
self
.
_update_reset_visibility
()
def
update_display
(
self
):
self
.
_update_initial_state
()
def
_on_boolean_toggled
(
self
,
switch
,
_
):
value
=
self
.
setting
.
map
.
get
(
True
)
if
switch
.
get_active
()
else
self
.
setting
.
map
.
get
(
False
)
self
.
setting
.
_set_backend_value
(
value
)
...
...
src/settings/setting/widgets/ChoiceWidget.py
View file @
ca1ae92c
...
...
@@ -7,14 +7,13 @@ class ChoiceWidget(BaseWidget):
self
.
row
=
Adw
.
ActionRow
(
title
=
self
.
setting
.
name
,
subtitle
=
self
.
setting
.
help
)
self
.
dropdown
=
Gtk
.
DropDown
.
new_from_strings
(
items
)
self
.
dropdown
.
set_halign
(
Gtk
.
Align
.
CENTER
)
self
.
dropdown
.
set_valign
(
Gtk
.
Align
.
CENTER
)
self
.
_set_dropdown_width
(
items
)
self
.
_update_dropdown_selection
()
self
.
dropdown
.
set_selected
(
self
.
setting
.
_get_selected_row_index
())
self
.
dropdown
.
connect
(
"notify::selected"
,
self
.
_on_choice_changed
)
control_box
=
Gtk
.
Box
(
spacing
=
6
,
orientation
=
Gtk
.
Orientation
.
HORIZONTAL
)
...
...
@@ -25,6 +24,14 @@ class ChoiceWidget(BaseWidget):
self
.
_update_reset_visibility
()
return
self
.
row
def
update_display
(
self
):
self
.
_update_dropdown_selection
()
self
.
_update_reset_visibility
()
def
_update_dropdown_selection
(
self
):
current_index
=
self
.
setting
.
_get_selected_row_index
()
self
.
dropdown
.
set_selected
(
current_index
)
def
_set_dropdown_width
(
self
,
items
):
layout
=
self
.
dropdown
.
create_pango_layout
(
""
)
width
=
0
...
...
@@ -39,17 +46,19 @@ class ChoiceWidget(BaseWidget):
def
_on_choice_changed
(
self
,
dropdown
,
_
):
selected
=
dropdown
.
get_selected
()
selected_value
=
list
(
self
.
setting
.
map
.
values
())[
selected
]
if
selected
<
0
or
selected
>=
len
(
self
.
setting
.
map
):
return
selected_value
=
list
(
self
.
setting
.
map
.
values
())[
selected
]
self
.
setting
.
_set_backend_value
(
selected_value
)
self
.
_update_reset_visibility
()
def
_on_reset_clicked
(
self
,
button
):
default_value
=
self
.
setting
.
_get_default_row_index
()
if
default_value
is
not
None
:
self
.
dropdown
.
set_selected
(
default_value
)
with
self
.
dropdown
.
handler_block_by_func
(
self
.
_on_choice_changed
):
self
.
dropdown
.
set_selected
(
default_value
)
self
.
setting
.
_set_backend_value
(
self
.
setting
.
default
)
self
.
_update_reset_visibility
()
...
...
src/settings/setting/widgets/EntryWidget.py
View file @
ca1ae92c
...
...
@@ -9,7 +9,7 @@ class EntryWidget(BaseWidget):
self
.
entry
=
Gtk
.
Entry
()
self
.
entry
.
set_halign
(
Gtk
.
Align
.
CENTER
)
self
.
entry
.
set_text
(
s
elf
.
setting
.
_get_backend_value
()
or
""
)
self
.
entry
.
set_text
(
s
tr
(
self
.
setting
.
_get_backend_value
()
or
""
)
)
self
.
entry
.
connect
(
"activate"
,
self
.
_on_text_changed
)
...
...
@@ -30,6 +30,12 @@ class EntryWidget(BaseWidget):
return
self
.
row
def
update_display
(
self
):
with
self
.
entry
.
handler_block_by_func
(
self
.
_on_text_changed
):
current_value
=
self
.
setting
.
_get_backend_value
()
self
.
entry
.
set_text
(
str
(
current_value
)
if
current_value
is
not
None
else
""
)
self
.
_update_reset_visibility
()
def
_on_text_changed
(
self
,
entry
):
new_value
=
entry
.
get_text
()
...
...
@@ -41,14 +47,13 @@ class EntryWidget(BaseWidget):
default_value
=
self
.
setting
.
default
self
.
setting
.
_set_backend_value
(
default_value
)
self
.
entry
.
set_text
(
str
(
default_value
))
self
.
entry
.
set_text
(
str
(
default_value
)
if
default_value
is
not
None
else
""
)
self
.
_update_reset_visibility
()
def
_update_reset_visibility
(
self
):
current_value
=
self
.
entry
.
get_text
()
or
""
default_value
=
self
.
setting
.
default
current_value
=
self
.
entry
.
get_text
()
default_value
=
str
(
self
.
setting
.
default
)
if
self
.
setting
.
default
is
not
None
else
""
has_default
=
self
.
setting
.
default
is
not
None
is_default
=
current_value
==
default_value
...
...
src/settings/setting/widgets/FileChooser.py
View file @
ca1ae92c
...
...
@@ -51,7 +51,7 @@ class FileChooser(BaseWidget):
row
.
add_suffix
(
control_box
)
self
.
_
update_display
()
self
.
update_display
()
self
.
_update_reset_visibility
()
...
...
@@ -66,7 +66,7 @@ class FileChooser(BaseWidget):
self
.
setting
.
_set_backend_value
(
default_value
)
self
.
_
update_display
()
self
.
update_display
()
self
.
_update_reset_visibility
()
...
...
@@ -91,11 +91,9 @@ class FileChooser(BaseWidget):
else
False
)
def
_
update_display
(
self
):
def
update_display
(
self
):
current
=
self
.
setting
.
_get_backend_value
()
self
.
_update_reset_visibility
()
if
current
and
isinstance
(
current
,
str
)
and
current
.
startswith
(
"file://"
):
current
=
current
[
7
:]
self
.
value_separated
=
True
...
...
@@ -107,6 +105,8 @@ class FileChooser(BaseWidget):
else
:
self
.
_update_single_file_display
(
current
)
self
.
_update_reset_visibility
()
def
_on_button_clicked
(
self
,
button
):
dialog
=
Gtk
.
FileDialog
()
...
...
@@ -173,7 +173,7 @@ class FileChooser(BaseWidget):
if
file
:
self
.
setting
.
_set_backend_value
(
file
.
get_path
())
self
.
_
update_display
()
self
.
update_display
()
except
Exception
as
e
:
print
(
f
"File selection error: {e}"
)
...
...
@@ -183,7 +183,7 @@ class FileChooser(BaseWidget):
if
file_list
:
paths
=
[
f
.
get_path
()
for
f
in
file_list
]
self
.
setting
.
_set_backend_value
(
paths
)
self
.
_
update_display
()
self
.
update_display
()
except
Exception
as
e
:
print
(
f
"Multiple files selection error: {e}"
)
...
...
@@ -192,7 +192,7 @@ class FileChooser(BaseWidget):
folder
=
dialog
.
select_folder_finish
(
result
)
if
folder
:
self
.
setting
.
_set_backend_value
(
folder
.
get_path
())
self
.
_
update_display
()
self
.
update_display
()
except
Exception
as
e
:
print
(
f
"Folder selection error: {e}"
)
...
...
src/settings/setting/widgets/NumStepper.py
View file @
ca1ae92c
...
...
@@ -44,6 +44,12 @@ class NumStepper(BaseWidget):
return
row
def
update_display
(
self
):
current_value
=
self
.
setting
.
_get_backend_value
()
with
self
.
spin
.
handler_block_by_func
(
self
.
_on_num_changed
):
self
.
spin
.
set_value
(
float
(
current_value
))
self
.
_update_reset_visibility
()
def
_on_num_changed
(
self
,
widget
):
selected_value
=
widget
.
get_value
()
...
...
src/settings/setting/widgets/RadioChoiceWidget.py
View file @
ca1ae92c
...
...
@@ -84,6 +84,13 @@ class RadioChoiceWidget(BaseWidget):
return
main_box
def
update_display
(
self
):
current_value
=
self
.
setting
.
_get_backend_value
()
for
value
,
radio
in
self
.
radio_buttons
.
items
():
with
radio
.
handler_block_by_func
(
self
.
_on_toggle
):
radio
.
set_active
(
value
==
current_value
)
self
.
_update_reset_visibility
()
def
_on_toggle
(
self
,
button
,
value
):
if
button
.
get_active
():
self
.
setting
.
_set_backend_value
(
value
)
...
...
@@ -97,8 +104,8 @@ class RadioChoiceWidget(BaseWidget):
self
.
setting
.
_set_backend_value
(
default_value
)
if
default_value
in
self
.
radio_buttons
:
self
.
radio_buttons
[
default_value
]
.
set_active
(
True
)
with
self
.
radio_buttons
[
default_value
]
.
handler_block_by_func
(
self
.
_on_toggle
):
self
.
radio_buttons
[
default_value
]
.
set_active
(
True
)
self
.
_update_reset_visibility
()
def
_update_reset_visibility
(
self
):
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment