diff --git a/frontend/ui/widget/inputdialog.lua b/frontend/ui/widget/inputdialog.lua
new file mode 100644
index 000000000..90859e0a2
--- /dev/null
+++ b/frontend/ui/widget/inputdialog.lua
@@ -0,0 +1,109 @@
+require "ui/widget/container"
+require "ui/widget/inputtext"
+
+InputDialog = InputContainer:new{
+ title = "",
+ input = "",
+ input_hint = "",
+ buttons = nil,
+ input_type = nil,
+
+ width = nil,
+ height = nil,
+
+ title_face = Font:getFace("tfont", 22),
+ input_face = Font:getFace("cfont", 20),
+
+ title_padding = scaleByDPI(5),
+ title_margin = scaleByDPI(2),
+ input_padding = scaleByDPI(10),
+ input_margin = scaleByDPI(10),
+ button_padding = scaleByDPI(14),
+}
+
+function InputDialog:init()
+ self.title = FrameContainer:new{
+ padding = self.title_padding,
+ margin = self.title_margin,
+ bordersize = 0,
+ TextWidget:new{
+ text = self.title,
+ face = self.title_face,
+ width = self.width,
+ }
+ }
+ self.input = InputText:new{
+ text = self.input,
+ hint = self.input_hint,
+ face = self.input_face,
+ width = self.width * 0.9,
+ input_type = self.input_type,
+ scroll = false,
+ parent = self,
+ }
+ local button_table = ButtonTable:new{
+ width = self.width,
+ button_font_face = "cfont",
+ button_font_size = 20,
+ buttons = self.buttons,
+ zero_sep = true,
+ }
+ local title_bar = LineWidget:new{
+ --background = 8,
+ dimen = Geom:new{
+ w = button_table:getSize().w + self.button_padding,
+ h = scaleByDPI(2),
+ }
+ }
+
+ self.dialog_frame = FrameContainer:new{
+ radius = 8,
+ bordersize = 3,
+ padding = 0,
+ margin = 0,
+ background = 0,
+ VerticalGroup:new{
+ align = "left",
+ self.title,
+ title_bar,
+ -- input
+ CenterContainer:new{
+ dimen = Geom:new{
+ w = title_bar:getSize().w,
+ h = self.input:getSize().h,
+ },
+ self.input,
+ },
+ -- buttons
+ CenterContainer:new{
+ dimen = Geom:new{
+ w = title_bar:getSize().w,
+ h = button_table:getSize().h,
+ },
+ button_table,
+ }
+ }
+ }
+
+ self[1] = CenterContainer:new{
+ dimen = Geom:new{
+ w = Screen:getWidth(),
+ h = Screen:getHeight() - self.input:getKeyboardDimen().h,
+ },
+ self.dialog_frame,
+ }
+ UIManager.repaint_all = true
+ UIManager.full_refresh = true
+end
+
+function InputDialog:onShowKeyboard()
+ self.input:onShowKeyboard()
+end
+
+function InputDialog:getInputText()
+ return self.input:getText()
+end
+
+function InputDialog:onClose()
+ self.input:onCloseKeyboard()
+end
diff --git a/frontend/ui/widget/inputtext.lua b/frontend/ui/widget/inputtext.lua
new file mode 100644
index 000000000..17893e521
--- /dev/null
+++ b/frontend/ui/widget/inputtext.lua
@@ -0,0 +1,137 @@
+require "ui/graphics"
+require "ui/widget/text"
+require "ui/widget/keyboard"
+require "ui/widget/container"
+
+InputText = InputContainer:new{
+ text = "",
+ hint = "demo hint",
+ charlist = {}, -- table to store input string
+ charpos = 1,
+ input_type = nil,
+
+ width = nil,
+ height = nil,
+ face = Font:getFace("cfont", 22),
+
+ padding = 5,
+ margin = 5,
+ bordersize = 2,
+
+ parent = nil, -- parent dialog that will be set dirty
+ scroll = false,
+}
+
+function InputText:init()
+ self:StringToCharlist(self.text)
+ self:initTextBox()
+ self:initKeyboard()
+end
+
+function InputText:initTextBox()
+ local bgcolor = nil
+ local fgcolor = nil
+ if self.text == "" then
+ self.text = self.hint
+ bgcolor = 0.0
+ fgcolor = 0.5
+ else
+ bgcolor = 0.0
+ fgcolor = 1.0
+ end
+ local text_widget = nil
+ if self.scroll then
+ text_widget = ScrollTextWidget:new{
+ text = self.text,
+ face = self.face,
+ bgcolor = bgcolor,
+ fgcolor = fgcolor,
+ width = self.width,
+ height = self.height,
+ }
+ else
+ text_widget = TextBoxWidget:new{
+ text = self.text,
+ face = self.face,
+ bgcolor = bgcolor,
+ fgcolor = fgcolor,
+ width = self.width,
+ height = self.height,
+ }
+ end
+ self[1] = FrameContainer:new{
+ bordersize = self.bordersize,
+ padding = self.padding,
+ margin = self.margin,
+ text_widget,
+ }
+ self.dimen = self[1]:getSize()
+end
+
+function InputText:initKeyboard()
+ local keyboard_layout = 2
+ if self.input_type == "number" then
+ keyboard_layout = 3
+ end
+ self.keyboard = VirtualKeyboard:new{
+ layout = keyboard_layout,
+ inputbox = self,
+ }
+end
+
+function InputText:onShowKeyboard()
+ UIManager:show(self.keyboard)
+end
+
+function InputText:onCloseKeyboard()
+ UIManager:close(self.keyboard)
+end
+
+function InputText:getKeyboardDimen()
+ return self.keyboard.dimen
+end
+
+function InputText:addChar(char)
+ table.insert(self.charlist, self.charpos, char)
+ self.charpos = self.charpos + 1
+ self.text = self:CharlistToString()
+ self:initTextBox()
+ UIManager:setDirty(self.parent, "partial")
+end
+
+function InputText:delChar()
+ self.charpos = self.charpos - 1
+ table.remove(self.charlist, self.charpos)
+ self.text = self:CharlistToString()
+ self:initTextBox()
+ UIManager:setDirty(self.parent, "partial")
+end
+
+function InputText:getText()
+ return self.text
+end
+
+function InputText:StringToCharlist(text)
+ if text == nil then return end
+ -- clear
+ self.charlist = {}
+ self.charpos = 1
+ local prevcharcode, charcode = 0
+ for uchar in string.gfind(text, "([%z\1-\127\194-\244][\128-\191]*)") do
+ charcode = util.utf8charcode(uchar)
+ if prevcharcode then -- utf8
+ self.charlist[#self.charlist+1] = uchar
+ end
+ prevcharcode = charcode
+ end
+ self.text = self:CharlistToString()
+ self.charpos = #self.charlist+1
+end
+
+function InputText:CharlistToString()
+ local s, i = ""
+ for i=1, #self.charlist do
+ s = s .. self.charlist[i]
+ end
+ return s
+end
diff --git a/frontend/ui/widget/keyboard.lua b/frontend/ui/widget/keyboard.lua
new file mode 100644
index 000000000..9e90da9fb
--- /dev/null
+++ b/frontend/ui/widget/keyboard.lua
@@ -0,0 +1,298 @@
+require "ui/font"
+require "ui/widget/text"
+require "ui/widget/image"
+require "ui/widget/group"
+require "ui/widget/container"
+
+VirtualKey = InputContainer:new{
+ key = nil,
+ icon = nil,
+ label = nil,
+
+ keyboard = nil,
+ callback = nil,
+
+ width = nil,
+ height = nil,
+ bordersize = 2,
+ face = Font:getFace("infont", 22),
+}
+
+function VirtualKey:init()
+ if self.label == "Sym" or self.label == "ABC" then
+ self.callback = function () self.keyboard:setLayout(self.key or self.label) end
+ elseif self.label == "Shift" then
+ self.callback = function () self.keyboard:setLayout(self.key or self.label) end
+ elseif self.label == "IM" then
+ self.callback = function () self.keyboard:setLayout(self.key or self.label) end
+ elseif self.label == "Backspace" then
+ self.callback = function () self.keyboard:delChar() end
+ else
+ self.callback = function () self.keyboard:addChar(self.key) end
+ end
+
+ local label_widget = nil
+ if self.icon then
+ label_widget = ImageWidget:new{
+ file = self.icon,
+ }
+ else
+ label_widget = TextWidget:new{
+ text = self.label,
+ face = self.face,
+ }
+ end
+ self[1] = FrameContainer:new{
+ margin = 0,
+ bordersize = self.bordersize,
+ background = 0,
+ radius = 5,
+ padding = 0,
+ CenterContainer:new{
+ dimen = Geom:new{
+ w = self.width - 2*self.bordersize,
+ h = self.height - 2*self.bordersize,
+ },
+ label_widget,
+ },
+ }
+ self.dimen = Geom:new{
+ w = self.width,
+ h = self.height,
+ }
+ --self.dimen = self[1]:getSize()
+ if Device:isTouchDevice() then
+ self.ges_events = {
+ TapSelect = {
+ GestureRange:new{
+ ges = "tap",
+ range = self.dimen,
+ },
+ },
+ DoubleTapSelect = {
+ GestureRange:new{
+ ges = "double_tap",
+ range = self.dimen,
+ },
+ },
+ }
+ end
+end
+
+function VirtualKey:onTapSelect()
+ self[1].invert = true
+ if self.callback then
+ self.callback()
+ end
+ UIManager:scheduleIn(0.08, function() self:invert(false) end)
+ return true
+end
+
+function VirtualKey:onDoubleTapSelect()
+ self[1].invert = true
+ if self.callback then
+ self.callback() -- once
+ self.callback() -- twice
+ end
+ UIManager:scheduleIn(0.08, function() self:invert(false) end)
+ return true
+end
+
+function VirtualKey:invert(invert)
+ self[1].invert = invert
+ UIManager.update_region_func = function()
+ DEBUG("update key region", self[1].dimen)
+ return self[1].dimen
+ end
+ UIManager:setDirty(self.keyboard, "partial")
+end
+
+VirtualKeyboard = InputContainer:new{
+ is_always_active = true,
+ inputbox = nil,
+ KEYS = {}, -- table to store layouts
+ min_layout = 2,
+ max_layout = 9,
+ layout = 2,
+ shiftmode = false,
+ symbolmode = false,
+ utf8mode = false,
+
+ width = 600,
+ height = 256,
+ bordersize = 2,
+ padding = 2,
+ key_padding = scaleByDPI(6),
+}
+
+function VirtualKeyboard:init()
+ self.KEYS = {
+ -- first row
+ {
+ { "Q", "q", "1", "!", "Я", "я", "1", "!", },
+ { "W", "w", "2", "?", "Ж", "ж", "2", "?", },
+ { "E", "e", "3", "|", "Е", "е", "3", "«", },
+ { "R", "r", "4", "#", "Р", "р", "4", "»", },
+ { "T", "t", "5", "@", "Т", "т", "5", ":", },
+ { "Y", "y", "6", "‰", "Ы", "ы", "6", ";", },
+ { "U", "u", "7", "'", "У", "у", "7", "~", },
+ { "I", "i", "8", "`", "И", "и", "8", "(", },
+ { "O", "o", "9", ":", "О", "о", "9", ")", },
+ { "P", "p", "0", ";", "П", "п", "0", "=", },
+ },
+ -- second raw
+ {
+ { "A", "a", "+", "…", "А", "а", "Ш", "ш", },
+ { "S", "s", "-", "_", "С", "с", "Ѕ", "ѕ", },
+ { "D", "d", "*", "=", "Д", "д", "Э", "э", },
+ { "F", "f", "/", "\\", "Ф", "ф", "Ю", "ю", },
+ { "G", "g", "%", "„", "Г", "г", "Ґ", "ґ", },
+ { "H", "h", "^", "“", "Ч", "ч", "Ј", "ј", },
+ { "J", "j", "<", "”", "Й", "й", "І", "і", },
+ { "K", "k", "=", "\"", "К", "к", "Ќ", "ќ", },
+ { "L", "l", ">", "~", "Л", "л", "Љ", "љ", },
+ },
+ -- third raw
+ {
+ { label = "Shift",
+ icon = "resources/icons/appbar.arrow.shift.png",
+ width = 1.5
+ },
+ { "Z", "z", "(", "$", "З", "з", "Щ", "щ", },
+ { "X", "x", ")", "€", "Х", "х", "№", "@", },
+ { "C", "c", "&", "¥", "Ц", "ц", "Џ", "џ", },
+ { "V", "v", ":", "£", "В", "в", "Ў", "ў", },
+ { "B", "b", "π", "‚", "Б", "б", "Ћ", "ћ", },
+ { "N", "n", "е", "‘", "Н", "н", "Њ", "њ", },
+ { "M", "m", "~", "’", "М", "м", "Ї", "ї", },
+ { label = "Backspace",
+ icon = "resources/icons/appbar.clear.reflect.horizontal.png",
+ width = 1.5
+ },
+ },
+ -- fourth raw
+ {
+ { "Sym", "Sym", "ABC", "ABC", "Sym", "Sym", "ABC", "ABC",
+ width = 1.5},
+ { label = "IM",
+ icon = "resources/icons/appbar.globe.wire.png",
+ },
+ { label = "space",
+ " ", " ", " ", " ", " ", " ", " ", " ",
+ width = 5.0},
+ { ",", ".", ".", ",", ",", ".", "Є", "є", },
+ { label = "Enter",
+ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n",
+ icon = "resources/icons/appbar.arrow.enter.png",
+ width = 1.5,
+ },
+
+ }
+ }
+ self:initLayout(self.layout)
+end
+
+function VirtualKeyboard:initLayout(layout)
+ local function VKLayout(b1, b2, b3)
+ local function boolnum(bool)
+ return bool and 1 or 0
+ end
+ return 2 - boolnum(b1) + 2 * boolnum(b2) + 4 * boolnum(b3)
+ end
+
+ if layout then
+ -- to be sure layout is selected properly
+ layout = math.max(layout, self.min_layout)
+ layout = math.min(layout, self.max_layout)
+ self.layout = layout
+ -- fill the layout modes
+ layout = layout % 4
+ self.shiftmode = (layout == 1 or layout == 3)
+ self.symbolmode = (layout == 3 or layout == 4)
+ self.utf8mode = (self.layout > 5)
+ else -- or, without input parameter, restore layout from current layout modes
+ self.layout = VKLayout(self.shiftmode, self.symbolmode, self.utf8mode)
+ end
+ self:addKeys()
+end
+
+function VirtualKeyboard:addKeys()
+ local base_key_width = math.floor((self.width - 11*self.key_padding - 2*self.padding)/10)
+ local base_key_height = math.floor((self.height - 5*self.key_padding - 2*self.padding)/4)
+ local h_key_padding = HorizontalSpan:new{width = self.key_padding}
+ local v_key_padding = VerticalSpan:new{width = self.key_padding}
+ local vertical_group = VerticalGroup:new{}
+ for i = 1, #self.KEYS do
+ local horizontal_group = HorizontalGroup:new{}
+ for j = 1, #self.KEYS[i] do
+ local width_factor = self.KEYS[i][j].width or 1.0
+ local key_width = math.floor((base_key_width + self.key_padding) * width_factor)
+ - self.key_padding
+ local key_height = base_key_height
+ local label = self.KEYS[i][j].label or self.KEYS[i][j][self.layout]
+ local key = VirtualKey:new{
+ key = self.KEYS[i][j][self.layout],
+ icon = self.KEYS[i][j].icon,
+ label = label,
+ keyboard = self,
+ width = key_width,
+ height = key_height,
+ }
+ table.insert(horizontal_group, key)
+ if j ~= #self.KEYS[i] then
+ table.insert(horizontal_group, h_key_padding)
+ end
+ end
+ table.insert(vertical_group, horizontal_group)
+ if i ~= #self.KEYS then
+ table.insert(vertical_group, v_key_padding)
+ end
+ end
+
+ local size = vertical_group:getSize()
+ local keyboard_frame = FrameContainer:new{
+ margin = 0,
+ bordersize = self.bordersize,
+ background = 0,
+ radius = 0,
+ padding = self.padding,
+ CenterContainer:new{
+ dimen = Geom:new{
+ w = self.width - 2*self.bordersize -2*self.padding,
+ h = self.height - 2*self.bordersize -2*self.padding,
+ },
+ vertical_group,
+ }
+ }
+ self[1] = BottomContainer:new{
+ dimen = Screen:getSize(),
+ keyboard_frame,
+ }
+ self.dimen = keyboard_frame:getSize()
+end
+
+function VirtualKeyboard:setLayout(key)
+ if key == "Shift" then
+ self.shiftmode = not self.shiftmode
+ elseif key == "Sym" or key == "ABC" then
+ self.symbolmode = not self.symbolmode
+ elseif key == "IM" then
+ self.utf8mode = not self.utf8mode
+ end
+ self:initLayout()
+ UIManager:setDirty(self, "partial")
+end
+
+function VirtualKeyboard:addChar(key)
+ DEBUG("add char", key)
+ self.inputbox:addChar(key)
+ UIManager:setDirty(self, "partial")
+ UIManager:setDirty(self.inputbox, "partial")
+end
+
+function VirtualKeyboard:delChar()
+ DEBUG("delete char")
+ self.inputbox:delChar()
+ UIManager:setDirty(self, "partial")
+ UIManager:setDirty(self.inputbox, "partial")
+end
diff --git a/resources/icons/appbar.arrow.enter.png b/resources/icons/appbar.arrow.enter.png
new file mode 100644
index 000000000..ce3fd48d9
Binary files /dev/null and b/resources/icons/appbar.arrow.enter.png differ
diff --git a/resources/icons/appbar.arrow.shift.png b/resources/icons/appbar.arrow.shift.png
new file mode 100644
index 000000000..840f4f7db
Binary files /dev/null and b/resources/icons/appbar.arrow.shift.png differ
diff --git a/resources/icons/appbar.clear.reflect.horizontal.png b/resources/icons/appbar.clear.reflect.horizontal.png
new file mode 100644
index 000000000..b800bf14c
Binary files /dev/null and b/resources/icons/appbar.clear.reflect.horizontal.png differ
diff --git a/resources/icons/appbar.globe.wire.png b/resources/icons/appbar.globe.wire.png
new file mode 100644
index 000000000..13c25f6e1
Binary files /dev/null and b/resources/icons/appbar.globe.wire.png differ
diff --git a/resources/icons/src/appbar.arrow.enter.svg b/resources/icons/src/appbar.arrow.enter.svg
new file mode 100644
index 000000000..73fe8cefe
--- /dev/null
+++ b/resources/icons/src/appbar.arrow.enter.svg
@@ -0,0 +1,54 @@
+
+
\ No newline at end of file
diff --git a/resources/icons/src/appbar.arrow.left.svg b/resources/icons/src/appbar.arrow.left.svg
new file mode 100644
index 000000000..c436355dd
--- /dev/null
+++ b/resources/icons/src/appbar.arrow.left.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/resources/icons/src/appbar.arrow.shift.svg b/resources/icons/src/appbar.arrow.shift.svg
new file mode 100644
index 000000000..9376d4f82
--- /dev/null
+++ b/resources/icons/src/appbar.arrow.shift.svg
@@ -0,0 +1,57 @@
+
+
\ No newline at end of file
diff --git a/resources/icons/src/appbar.globe.wire.xaml b/resources/icons/src/appbar.globe.wire.xaml
new file mode 100644
index 000000000..4443828ab
--- /dev/null
+++ b/resources/icons/src/appbar.globe.wire.xaml
@@ -0,0 +1,4 @@
+
+