mirror of
https://github.com/airstruck/luigi.git
synced 2025-11-18 12:25:06 +00:00
add keyboard focus
This commit is contained in:
@@ -17,6 +17,9 @@ local style = {
|
||||
align = 'center middle',
|
||||
width = 48,
|
||||
},
|
||||
toolButton_focused = {
|
||||
slices = 'defer',
|
||||
},
|
||||
toolButton_not_hovered = {
|
||||
slices = false,
|
||||
},
|
||||
@@ -62,7 +65,7 @@ local mainForm = { title = "Test window", id = 'mainWindow', type = 'panel',
|
||||
{ type = 'sash', width = 4, },
|
||||
{ type = 'panel', id = 'rightSideBox', width = 200,
|
||||
{ type = 'panel', text = 'A slider', align = 'bottom', height = 24, padding = 4 },
|
||||
{ type = 'slider', height = 32, margin = 4, id = 'slidey', },
|
||||
{ type = 'slider', height = 32, margin = 4, id = 'slidey', value = 0 },
|
||||
{ type = 'panel', text = 'A stepper', align = 'bottom', height = 24, padding = 4 },
|
||||
{ type = 'stepper', height = 32, margin = 4, options = {
|
||||
{ value = 1, text = 'Thing One' },
|
||||
@@ -93,8 +96,8 @@ layout.leftSideBox:addChild {
|
||||
align = 'middle right'
|
||||
}
|
||||
|
||||
layout.slidey:onPressDrag(function (event)
|
||||
layout.progressBar.value = layout.slidey.value
|
||||
layout.slidey:onChange(function (event)
|
||||
layout.progressBar:setValue(event.value)
|
||||
layout.progressBar:reshape()
|
||||
end)
|
||||
|
||||
|
||||
@@ -18,9 +18,18 @@ function Event:bind (target, callback)
|
||||
end
|
||||
|
||||
local eventNames = {
|
||||
'Reshape', 'Display', 'KeyPress', 'KeyRelease', 'TextInput', 'Move',
|
||||
'Enter', 'Leave', 'PressEnter', 'PressLeave',
|
||||
'PressStart', 'PressEnd', 'PressDrag', 'PressMove', 'Press',
|
||||
'Reshape', -- widget's dimensions changed
|
||||
'Display', -- widget is being drawn
|
||||
'KeyPress', 'KeyRelease', -- keyboard key pressed/released
|
||||
'TextInput', -- text is entered
|
||||
'Move', -- cursor moves, no button pressed
|
||||
'Enter', 'Leave', -- cursor enters/leaves widget, no button pressed
|
||||
'PressEnter', 'PressLeave', -- cursor enters/leaves widget, button pressed
|
||||
'PressStart', 'PressEnd', -- cursor or accelerator key press starts/ends
|
||||
'PressDrag', -- pressed cursor moves, targets originating widget
|
||||
'PressMove', -- pressed cursor moves, targets widget at cursor position
|
||||
'Press', -- cursor is pressed and released on same widget
|
||||
'Change', -- widget's value changed via Widget:setValue
|
||||
}
|
||||
|
||||
local weakKeyMeta = { __mode = 'k' }
|
||||
|
||||
@@ -14,15 +14,6 @@ function Input:constructor (layout)
|
||||
self.passedWidgets = setmetatable({}, weakValueMeta)
|
||||
end
|
||||
|
||||
function Input:bubbleEvent (eventName, widget, data)
|
||||
local event = Event[eventName]
|
||||
for ancestor in widget:getAncestors(true) do
|
||||
local result = event:emit(ancestor, data)
|
||||
if result ~= nil then return result end
|
||||
end
|
||||
return event:emit(self.layout, data)
|
||||
end
|
||||
|
||||
function Input:handleDisplay ()
|
||||
local root = self.layout.root
|
||||
if root then Renderer:render(root) end
|
||||
@@ -31,40 +22,23 @@ end
|
||||
|
||||
function Input:handleKeyPress (key, x, y)
|
||||
local widget = self.layout.focusedWidget or self.layout:getWidgetAt(x, y)
|
||||
local result = self:bubbleEvent('KeyPress', widget, {
|
||||
target = widget,
|
||||
local result = widget:bubbleEvent('KeyPress', {
|
||||
key = key, x = x, y = y
|
||||
})
|
||||
if result ~= nil then return result end
|
||||
|
||||
local acceleratedWidget = self.layout.accelerators[key]
|
||||
|
||||
if acceleratedWidget then
|
||||
acceleratedWidget.hovered = true
|
||||
self:handlePressStart(key, x, y, acceleratedWidget, key)
|
||||
end
|
||||
end
|
||||
|
||||
function Input:handleKeyRelease (key, x, y)
|
||||
local widget = self.layout.focusedWidget or self.layout:getWidgetAt(x, y)
|
||||
local result = self:bubbleEvent('KeyRelease', widget, {
|
||||
target = widget,
|
||||
local result = widget:bubbleEvent('KeyRelease', {
|
||||
key = key, x = x, y = y
|
||||
})
|
||||
if result ~= nil then return result end
|
||||
|
||||
local acceleratedWidget = self.layout.accelerators[key]
|
||||
|
||||
if acceleratedWidget then
|
||||
acceleratedWidget.hovered = false
|
||||
self:handlePressEnd(key, x, y, acceleratedWidget, key)
|
||||
end
|
||||
end
|
||||
|
||||
function Input:handleTextInput (text, x, y)
|
||||
local widget = self.layout.focusedWidget or self.layout:getWidgetAt(x, y)
|
||||
self:bubbleEvent('TextInput', widget, {
|
||||
target = widget,
|
||||
widget:bubbleEvent('TextInput', {
|
||||
text = text, x = x, y = y
|
||||
})
|
||||
end
|
||||
@@ -78,21 +52,18 @@ function Input:handleMove (x, y)
|
||||
end
|
||||
widget.hovered = true
|
||||
end
|
||||
self:bubbleEvent('Move', widget, {
|
||||
target = widget,
|
||||
widget:bubbleEvent('Move', {
|
||||
oldTarget = previousWidget,
|
||||
x = x, y = y
|
||||
})
|
||||
if widget ~= previousWidget then
|
||||
if previousWidget then
|
||||
self:bubbleEvent('Leave', previousWidget, {
|
||||
target = previousWidget,
|
||||
previousWidget:bubbleEvent('Leave', {
|
||||
newTarget = widget,
|
||||
x = x, y = y
|
||||
})
|
||||
end
|
||||
self:bubbleEvent('Enter', widget, {
|
||||
target = widget,
|
||||
widget:bubbleEvent('Enter', {
|
||||
oldTarget = previousWidget,
|
||||
x = x, y = y
|
||||
})
|
||||
@@ -106,15 +77,13 @@ function Input:handlePressedMove (x, y)
|
||||
local originWidget = self.pressedWidgets[button]
|
||||
local passedWidget = self.passedWidgets[button]
|
||||
if originWidget then
|
||||
self:bubbleEvent('PressDrag', originWidget, {
|
||||
target = originWidget,
|
||||
originWidget:bubbleEvent('PressDrag', {
|
||||
newTarget = widget,
|
||||
button = button,
|
||||
x = x, y = y
|
||||
})
|
||||
if (widget == passedWidget) then
|
||||
self:bubbleEvent('PressMove', widget, {
|
||||
target = widget,
|
||||
widget:bubbleEvent('PressMove', {
|
||||
origin = originWidget,
|
||||
button = button,
|
||||
x = x, y = y
|
||||
@@ -122,16 +91,14 @@ function Input:handlePressedMove (x, y)
|
||||
else
|
||||
originWidget.pressed = (widget == originWidget) or nil
|
||||
if passedWidget then
|
||||
self:bubbleEvent('PressLeave', passedWidget, {
|
||||
target = passedWidget,
|
||||
passedWidget:bubbleEvent('PressLeave', {
|
||||
newTarget = widget,
|
||||
origin = originWidget,
|
||||
button = button,
|
||||
x = x, y = y
|
||||
})
|
||||
end
|
||||
self:bubbleEvent('PressEnter', widget, {
|
||||
target = widget,
|
||||
widget:bubbleEvent('PressEnter', {
|
||||
oldTarget = passedWidget,
|
||||
origin = originWidget,
|
||||
button = button,
|
||||
@@ -148,8 +115,8 @@ function Input:handlePressStart (button, x, y, widget, accelerator)
|
||||
widget.pressed = true
|
||||
self.pressedWidgets[button] = widget
|
||||
self.passedWidgets[button] = widget
|
||||
self:bubbleEvent('PressStart', widget, {
|
||||
target = widget,
|
||||
self.layout:tryFocus(widget)
|
||||
widget:bubbleEvent('PressStart', {
|
||||
button = button,
|
||||
accelerator = accelerator,
|
||||
x = x, y = y
|
||||
@@ -160,15 +127,13 @@ function Input:handlePressEnd (button, x, y, widget, accelerator)
|
||||
local widget = widget or self.layout:getWidgetAt(x, y)
|
||||
local originWidget = self.pressedWidgets[button]
|
||||
originWidget.pressed = nil
|
||||
self:bubbleEvent('PressEnd', widget, {
|
||||
target = widget,
|
||||
widget:bubbleEvent('PressEnd', {
|
||||
origin = originWidget,
|
||||
accelerator = accelerator,
|
||||
button = button, x = x, y = y
|
||||
})
|
||||
if (widget == originWidget) then
|
||||
self:bubbleEvent('Press', widget, {
|
||||
target = widget,
|
||||
widget:bubbleEvent('Press', {
|
||||
button = button,
|
||||
accelerator = accelerator,
|
||||
x = x, y = y
|
||||
|
||||
131
luigi/layout.lua
131
luigi/layout.lua
@@ -12,7 +12,6 @@ local Layout = Base:extend()
|
||||
local weakValueMeta = { __mode = 'v' }
|
||||
|
||||
function Layout:constructor (data)
|
||||
self.widgets = setmetatable({}, weakValueMeta)
|
||||
self.accelerators = {}
|
||||
self:setStyle()
|
||||
self:setTheme()
|
||||
@@ -21,8 +20,134 @@ function Layout:constructor (data)
|
||||
self.isManagingInput = false
|
||||
self.hooks = {}
|
||||
self.root = Widget(self, data or {})
|
||||
|
||||
self:addDefaultHandlers ()
|
||||
end
|
||||
|
||||
-- focus a widget if it's focusable, and return success
|
||||
function Layout:tryFocus (widget)
|
||||
if widget.canFocus then
|
||||
if self.focusedWidget then
|
||||
self.focusedWidget.focused = false
|
||||
end
|
||||
widget.focused = true
|
||||
self.focusedWidget = widget
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- get the next widget, cycling back around to root (depth first)
|
||||
function Layout:getNextWidget (widget)
|
||||
if #widget.children > 0 then
|
||||
return widget.children[1]
|
||||
end
|
||||
for ancestor in widget:eachAncestor(true) do
|
||||
local nextWidget = ancestor:getNext()
|
||||
if nextWidget then return nextWidget end
|
||||
end
|
||||
return self.root
|
||||
end
|
||||
|
||||
-- get the last child of the last child of the last child of the...
|
||||
local function getGreatestDescendant (widget)
|
||||
while #widget.children > 0 do
|
||||
local children = widget.children
|
||||
widget = children[#children]
|
||||
end
|
||||
return widget
|
||||
end
|
||||
|
||||
-- get the previous widget, cycling back around to root (depth first)
|
||||
function Layout:getPreviousWidget (widget)
|
||||
if widget == self.root then
|
||||
return getGreatestDescendant(widget)
|
||||
end
|
||||
for ancestor in widget:eachAncestor(true) do
|
||||
local previousWidget = ancestor:getPrevious()
|
||||
if previousWidget then
|
||||
return getGreatestDescendant(previousWidget)
|
||||
end
|
||||
if ancestor ~= widget then return ancestor end
|
||||
end
|
||||
return self.root
|
||||
end
|
||||
|
||||
-- focus next focusable widget (depth first)
|
||||
function Layout:focusNextWidget ()
|
||||
local widget = self.focusedWidget or self.root
|
||||
local nextWidget = self:getNextWidget(widget)
|
||||
|
||||
while nextWidget ~= widget do
|
||||
if self:tryFocus(nextWidget) then return end
|
||||
nextWidget = self:getNextWidget(nextWidget)
|
||||
end
|
||||
end
|
||||
|
||||
-- focus previous focusable widget (depth first)
|
||||
function Layout:focusPreviousWidget ()
|
||||
local widget = self.focusedWidget or self.root
|
||||
local previousWidget = self:getPreviousWidget(widget)
|
||||
|
||||
while previousWidget ~= widget do
|
||||
if self:tryFocus(previousWidget) then return end
|
||||
previousWidget = self:getPreviousWidget(previousWidget)
|
||||
end
|
||||
end
|
||||
|
||||
-- handlers for keyboard accelerators and tab focus
|
||||
function Layout:addDefaultHandlers ()
|
||||
self:onKeyPress(function (event)
|
||||
|
||||
-- tab / shift-tab cycles focused widget
|
||||
if event.key == 'tab' then
|
||||
if love.keyboard.isDown('lshift', 'rshift') then
|
||||
self:focusPreviousWidget()
|
||||
else
|
||||
self:focusNextWidget()
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- space / enter presses focused widget
|
||||
local widget = self.focusedWidget
|
||||
if widget and (event.key == 'return' or event.key == 'space') then
|
||||
self.input:handlePressStart(event.key, event.x, event.y,
|
||||
widget, event.key)
|
||||
return
|
||||
end
|
||||
|
||||
-- accelerators
|
||||
local acceleratedWidget = self.accelerators[event.key]
|
||||
|
||||
if acceleratedWidget then
|
||||
acceleratedWidget.hovered = true
|
||||
self.input:handlePressStart(event.key, event.x, event.y,
|
||||
acceleratedWidget, event.key)
|
||||
end
|
||||
end)
|
||||
|
||||
self:onKeyRelease(function (event)
|
||||
|
||||
-- space / enter presses focused widget
|
||||
local widget = self.focusedWidget
|
||||
if widget and (event.key == 'return' or event.key == 'space') then
|
||||
self.input:handlePressEnd(event.key, event.x, event.y,
|
||||
widget, event.key)
|
||||
return
|
||||
end
|
||||
|
||||
-- accelerators
|
||||
local acceleratedWidget = self.accelerators[event.key]
|
||||
|
||||
if acceleratedWidget then
|
||||
acceleratedWidget.hovered = false
|
||||
self.input:handlePressEnd(event.key, event.x, event.y,
|
||||
acceleratedWidget, event.key)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- set the style from a definition table or function
|
||||
function Layout:setStyle (rules)
|
||||
if type(rules) == 'function' then
|
||||
rules = rules()
|
||||
@@ -30,6 +155,7 @@ function Layout:setStyle (rules)
|
||||
self.style = Style(rules or {}, { 'id', 'style' })
|
||||
end
|
||||
|
||||
-- set the theme from a definition table or function
|
||||
function Layout:setTheme (rules)
|
||||
if type(rules) == 'function' then
|
||||
rules = rules()
|
||||
@@ -37,6 +163,7 @@ function Layout:setTheme (rules)
|
||||
self.theme = Style(rules or {}, { 'type' })
|
||||
end
|
||||
|
||||
-- show the layout (hooks all appropriate love events and callbacks)
|
||||
function Layout:show ()
|
||||
local root = self.root
|
||||
local width = root.width
|
||||
@@ -55,6 +182,7 @@ function Layout:show ()
|
||||
root:reshape()
|
||||
end
|
||||
|
||||
-- hide the layout (unhooks love events and callbacks)
|
||||
function Layout:hide ()
|
||||
if not self.isManagingInput then
|
||||
return
|
||||
@@ -89,7 +217,6 @@ function Layout:addWidget (widget)
|
||||
if widget.key then
|
||||
self.accelerators[widget.key] = widget
|
||||
end
|
||||
table.insert(self.widgets, widget)
|
||||
end
|
||||
|
||||
-- event stuff
|
||||
|
||||
@@ -69,7 +69,7 @@ function Style:eachName (object)
|
||||
end
|
||||
return function ()
|
||||
if not checkLookupProp() then return end
|
||||
local specialName = getSpecialName { 'pressed', 'hovered' }
|
||||
local specialName = getSpecialName { 'pressed', 'focused', 'hovered' }
|
||||
if specialName then return specialName end
|
||||
lookupPropIndex = lookupPropIndex + 1
|
||||
return lookupProp[lookupPropIndex]
|
||||
|
||||
@@ -9,29 +9,23 @@ return function (config)
|
||||
local highlight = config.highlight or { 180, 180, 255 }
|
||||
|
||||
return {
|
||||
panel = {
|
||||
background = backColor,
|
||||
},
|
||||
button = {
|
||||
align = 'center middle',
|
||||
padding = 6,
|
||||
slices = RESOURCE .. 'button.png',
|
||||
minimumWidth = 24,
|
||||
minimumHeight = 24
|
||||
minimumHeight = 24,
|
||||
canFocus = true
|
||||
},
|
||||
button_hovered = {
|
||||
slices = RESOURCE .. 'button_hovered.png'
|
||||
},
|
||||
button_focused = {
|
||||
slices = RESOURCE .. 'button_focused.png',
|
||||
},
|
||||
button_pressed = {
|
||||
slices = RESOURCE .. 'button_pressed.png',
|
||||
},
|
||||
text = {
|
||||
align = 'left middle',
|
||||
slices = RESOURCE .. 'text.png',
|
||||
padding = 6,
|
||||
minimumWidth = 24,
|
||||
minimumHeight = 24
|
||||
},
|
||||
sash = {
|
||||
background = lineColor
|
||||
},
|
||||
@@ -44,6 +38,9 @@ return function (config)
|
||||
minimumWidth = 24,
|
||||
minimumHeight = 24
|
||||
},
|
||||
panel = {
|
||||
background = backColor,
|
||||
},
|
||||
progress = {
|
||||
slices = RESOURCE .. 'button_pressed.png',
|
||||
padding = 0,
|
||||
@@ -59,6 +56,17 @@ return function (config)
|
||||
},
|
||||
stepper = {
|
||||
},
|
||||
text = {
|
||||
align = 'left middle',
|
||||
slices = RESOURCE .. 'text.png',
|
||||
padding = 6,
|
||||
minimumWidth = 24,
|
||||
minimumHeight = 24,
|
||||
canFocus = true,
|
||||
},
|
||||
text_focused = {
|
||||
slices = RESOURCE .. 'text_focused.png',
|
||||
},
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
BIN
luigi/theme/light/button_focused.png
Normal file
BIN
luigi/theme/light/button_focused.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 597 B |
Binary file not shown.
|
Before Width: | Height: | Size: 388 B After Width: | Height: | Size: 375 B |
BIN
luigi/theme/light/text_focused.png
Normal file
BIN
luigi/theme/light/text_focused.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 551 B |
@@ -9,6 +9,7 @@ Event.injectBinders(Widget)
|
||||
Widget.isWidget = true
|
||||
|
||||
Widget.typeDecorators = {
|
||||
button = require(ROOT .. 'widget.button'),
|
||||
progress = require(ROOT .. 'widget.progress'),
|
||||
sash = require(ROOT .. 'widget.sash'),
|
||||
slider = require(ROOT .. 'widget.slider'),
|
||||
@@ -34,7 +35,7 @@ local function new (Widget, layout, self)
|
||||
if value ~= nil then return value end
|
||||
local style = self.layout.style
|
||||
value = style and style:getProperty(self, property)
|
||||
if value ~= nil then return value end
|
||||
if value ~= nil and value ~= 'defer' then return value end
|
||||
local theme = self.layout.theme
|
||||
return theme and theme:getProperty(self, property)
|
||||
end
|
||||
@@ -56,7 +57,29 @@ local function new (Widget, layout, self)
|
||||
return self
|
||||
end
|
||||
|
||||
function Widget:bubbleEvent (eventName, data)
|
||||
local event = Event[eventName]
|
||||
data = data or {}
|
||||
data.target = self
|
||||
for ancestor in self:eachAncestor(true) do
|
||||
local result = event:emit(ancestor, data)
|
||||
if result ~= nil then return result end
|
||||
end
|
||||
return event:emit(self.layout, data)
|
||||
end
|
||||
|
||||
function Widget:setValue (value)
|
||||
local oldValue = self.value
|
||||
self.value = value
|
||||
|
||||
self:bubbleEvent('Change', {
|
||||
value = value,
|
||||
oldValue = oldValue,
|
||||
})
|
||||
end
|
||||
|
||||
function Widget:getPrevious ()
|
||||
if not self.parent then return end
|
||||
local siblings = self.parent.children
|
||||
for i, widget in ipairs(siblings) do
|
||||
if widget == self then return siblings[i - 1] end
|
||||
@@ -64,6 +87,7 @@ function Widget:getPrevious ()
|
||||
end
|
||||
|
||||
function Widget:getNext ()
|
||||
if not self.parent then return end
|
||||
local siblings = self.parent.children
|
||||
for i, widget in ipairs(siblings) do
|
||||
if widget == self then return siblings[i + 1] end
|
||||
@@ -76,7 +100,6 @@ function Widget:addChild (data)
|
||||
|
||||
table.insert(self.children, child)
|
||||
child.parent = self
|
||||
layout:addWidget(child)
|
||||
|
||||
return child
|
||||
end
|
||||
@@ -236,7 +259,7 @@ function Widget:isAt (x, y)
|
||||
return (x1 < x) and (x2 > x) and (y1 < y) and (y2 > y)
|
||||
end
|
||||
|
||||
function Widget:getAncestors (includeSelf)
|
||||
function Widget:eachAncestor (includeSelf)
|
||||
local instance = includeSelf and self or self.parent
|
||||
return function()
|
||||
local widget = instance
|
||||
|
||||
3
luigi/widget/button.lua
Normal file
3
luigi/widget/button.lua
Normal file
@@ -0,0 +1,3 @@
|
||||
return function (self)
|
||||
|
||||
end
|
||||
@@ -1,6 +1,11 @@
|
||||
return function (self)
|
||||
|
||||
self.value = 0.5
|
||||
local function clamp (value)
|
||||
return value < 0 and 0 or value > 1 and 1 or value
|
||||
end
|
||||
|
||||
self:setValue(clamp(self.value or 0.5))
|
||||
self.step = self.step or 0.01
|
||||
self.flow = 'x' -- TODO: support vertical slider
|
||||
|
||||
local spacer = self:addChild()
|
||||
@@ -13,22 +18,32 @@ return function (self)
|
||||
}
|
||||
|
||||
local function unpress ()
|
||||
thumb.pressed = false
|
||||
thumb.pressed = false -- don't make the thumb appear pushed in
|
||||
return false -- don't press thumb on focused keyboard activation
|
||||
end
|
||||
|
||||
thumb:onPressStart(unpress)
|
||||
thumb:onPressEnter(unpress)
|
||||
|
||||
thumb:onKeyPress(function (event)
|
||||
local key = event.key
|
||||
if key == 'left' or key == 'down' then
|
||||
self:setValue(clamp(self.value - self.step))
|
||||
self:reshape()
|
||||
elseif event.key == 'right' or key == 'up' then
|
||||
self:setValue(clamp(self.value + self.step))
|
||||
self:reshape()
|
||||
end
|
||||
end)
|
||||
|
||||
local function press (event)
|
||||
local x1, y1, x2, y2 = self:getRectangle(true, true)
|
||||
self.value = (event.x - x1) / (x2 - x1)
|
||||
if self.value < 0 then self.value = 0 end
|
||||
if self.value > 1 then self.value = 1 end
|
||||
self:setValue(clamp((event.x - x1) / (x2 - x1)))
|
||||
self:reshape()
|
||||
self.layout:tryFocus(thumb)
|
||||
end
|
||||
|
||||
self:onPressStart(press)
|
||||
|
||||
self:onPressDrag(press)
|
||||
|
||||
self:onEnter(function (event)
|
||||
|
||||
@@ -14,6 +14,7 @@ return function (self)
|
||||
type = 'text',
|
||||
align = 'middle center',
|
||||
margin = 0,
|
||||
canFocus = false,
|
||||
}
|
||||
|
||||
local increment = self:addChild {
|
||||
@@ -31,7 +32,7 @@ return function (self)
|
||||
local function updateValue ()
|
||||
if not self.options then return end
|
||||
local option = self.options[self.index]
|
||||
self.value = option.value
|
||||
self:setValue(option.value)
|
||||
view.text = option.text
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user