<NME> toggleswitch.lua
<BEF> --[[--
Displays a button that toggles between states. Used in bottom configuration panel.

@usage
    local ToggleSwitch = require("ui/widget/toggleswitch")
]]

local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
local CenterContainer = require("ui/widget/container/centercontainer")
local Device = require("device")
local FocusManager = require("ui/widget/focusmanager")
local Font = require("ui/font")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local HorizontalGroup = require("ui/widget/horizontalgroup")
local FrameContainer = require("ui/widget/container/framecontainer")
local Notification = require("ui/widget/notification")
local Size = require("ui/size")
local TextWidget = require("ui/widget/textwidget")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local _ = require("gettext")
local Screen = Device.screen

local ToggleLabel = TextWidget:extend{
    bold = true,
    bgcolor = Blitbuffer.COLOR_WHITE,
    fgcolor = Blitbuffer.COLOR_BLACK,
}

local ToggleSwitch = FocusManager:extend{
    width = Screen:scaleBySize(216),
    height = Size.item.height_default,
    bgcolor = Blitbuffer.COLOR_WHITE, -- unfocused item color
    fgcolor = Blitbuffer.COLOR_DARK_GRAY, -- focused item color
    font_face = "cfont",
    font_size = 16,
    enabled = true,
    row_count = 1,
}

function ToggleSwitch:init()
    self.layout = {{}}
    -- Item count per row
    self.n_pos = math.ceil(#self.toggle / self.row_count)
    self.position = nil

    self.toggle_frame = FrameContainer:new{
        background = Blitbuffer.COLOR_WHITE,
        color = Blitbuffer.COLOR_DARK_GRAY,
        radius = Size.radius.window,
        bordersize = Size.border.thin,
        padding = Size.padding.small,
        dim = not self.enabled,
    }

    self.toggle_content = VerticalGroup:new{}
    for i = 1, self.row_count do
        table.insert(self.toggle_content, HorizontalGroup:new{})
    end

    local item_padding = Size.padding.default -- only used to check if text truncate needed
    local item_border_size = Size.border.thin
    local frame_inner_width = self.width - 2*self.toggle_frame.padding - 2* self.toggle_frame.bordersize
    -- We'll need to adjust items width and distribute the accumulated fractional part to some
    -- of them for proper visual alignment
    local item_width_real = frame_inner_width / self.n_pos - 2*item_border_size
    local item_width = math.floor(item_width_real)
    local item_width_adjust = item_width_real - item_width
    -- Note: the height provided by ConfigDialog might be smaller than needed,
    -- it gets too thin if we account for padding & border
    local item_height = self.height / self.row_count
    local item_width_to_add = 0
    for i = 1, #self.toggle do
        local real_item_width = item_width
        item_width_to_add = item_width_to_add + item_width_adjust
        if item_width_to_add >= 1 then
            -- One pixel wider to better align the entire widget
            real_item_width = item_width + math.floor(item_width_to_add)
            item_width_to_add = item_width_to_add - math.floor(item_width_to_add)
        end
        local text = self.toggle[i]
        local face = Font:getFace(self.font_face, self.font_size)
        local label = ToggleLabel:new{
            text = text,
            face = face,
            max_width = real_item_width - item_padding,
        }
        local content = CenterContainer:new{
            dimen = Geom:new{
                w = real_item_width,
                h = item_height,
            },
            label,
        }
        local button = FrameContainer:new{
            background = Blitbuffer.COLOR_WHITE,
            color = Blitbuffer.COLOR_DARK_GRAY,
            margin = 0,
            radius = Size.radius.window,
            bordersize = item_border_size,
            padding = 0,
            focusable = true,
            focus_border_size = item_border_size,
            focus_border_color = Blitbuffer.COLOR_BLACK,
            content,
        }
        table.insert(self.toggle_content[math.ceil(i / self.n_pos)], button)
        table.insert(self.layout[1], button)
    end
    self.toggle_frame[1] = self.toggle_content
    self[1] = self.toggle_frame
    self.dimen = Geom:new(self.toggle_frame:getSize())
    self.ges_events = {
        TapSelect = {
            GestureRange:new{
                ges = "tap",
                range = self.dimen,
            },
        },
        HoldSelect = {
            GestureRange:new{
                ges = "hold",
                range = self.dimen,
            },
        },
    }
end

function ToggleSwitch:update()
    local pos = self.position
    for i = 1, #self.toggle_content do
        local row = self.toggle_content[i]
        for j = 1, #row do
            local cell = row[j]
            if pos == (i - 1) * self.n_pos + j then
                cell.color = self.fgcolor
                cell.background = self.fgcolor
                cell[1][1].fgcolor = Blitbuffer.COLOR_WHITE
            else
                cell.color = self.bgcolor
                cell.background = self.bgcolor
                cell[1][1].fgcolor = Blitbuffer.COLOR_BLACK
            end
        end
    end
end

function ToggleSwitch:setPosition(position)
    self.position = position
    self:update()
end

function ToggleSwitch:togglePosition(position, update)
    if self.n_pos == 2 and self.alternate ~= false then
        self.position = (self.position+1)%self.n_pos
        self.position = self.position == 0 and self.n_pos or self.position
    elseif self.n_pos == 1 then
        self.position = self.position == 1 and 0 or 1
    else
        self.position = position
    end
    if update then
        self:update()
    end
end

function ToggleSwitch:circlePosition()
    if self.position then
        self.position = (self.position+1)%self.n_pos
        self.position = self.position == 0 and self.n_pos or self.position
        self:update()
    end
end

function ToggleSwitch:calculatePosition(gev)
    local x = (gev.pos.x - self.dimen.x) / self.dimen.w * self.n_pos
    if BD.mirroredUILayout() then
        x = self.n_pos - x
    end
    local y = (gev.pos.y - self.dimen.y) / self.dimen.h * self.row_count
    return math.max(1, math.ceil(x)) + math.min(self.row_count-1, math.floor(y)) * self.n_pos
end

function ToggleSwitch:onTapSelect(arg, gev)
    if not self.enabled then
        if self.readonly ~= true then
            return true
        else
            return
        end
    end
    if gev then
        local position = self:calculatePosition(gev)
        if self.toggle[position] ~= "⋮" then
            self:togglePosition(position, true)
        else
            self:togglePosition(position, false)
        end
    else
        self:circlePosition()
    end

    --[[
    if self.values then
        self.values = self.values or {}
        self.config:onConfigChoice(self.name, self.values[self.position])
    end
    if self.event then
        self.args = self.args or {}
        self.config:onConfigEvent(self.event, self.args[self.position])
    end
    --]]
    if self.callback then
        self.callback(self.position)
        self.config:onConfigEvents(self.events, self.position)
    end
    --]]
    self.callback(self.position)
    if self.toggle[self.position] ~= "⋮" then
        self.config:onConfigChoose(self.values, self.name,
            self.event, self.args, self.events, self.position, self.delay_repaint)

        UIManager:setDirty(self.config, function()
            return "ui", self.dimen
        end)

        UIManager:tickAfterNext(function()
            Notification:setNotifySource(Notification.SOURCE_OTHER) -- only allow events, if they are activated
        end)
    end
    return true
end

function ToggleSwitch:onHoldSelect(arg, gev)
    local position = self:calculatePosition(gev)
    if self.toggle[position] == "⋮" then
        return true
    end
    if self.name == "font_fine_tune" then
        --- @note Ugly hack for the only widget that uses a dual toggle for fine-tuning (others prefer a buttonprogress)
        self.config:onMakeFineTuneDefault("font_size", _("Font Size"),
                        self.values or self.args, self.toggle, position == 1 and "-" or "+")
    else
        self.config:onMakeDefault(self.name, self.name_text,
                        self.values or self.args, self.toggle, position)
    end
    return true
end

return ToggleSwitch

<MSG> Fix crash on book status change (#5453)


<DFF> @@ -217,7 +217,9 @@ function ToggleSwitch:onTapSelect(arg, gev)
         self.config:onConfigEvents(self.events, self.position)
     end
     --]]
-    self.callback(self.position)
+    if self.callback then
+        self.callback(self.position)
+    end
     if self.toggle[self.position] ~= "⋮" then
         self.config:onConfigChoose(self.values, self.name,
             self.event, self.args, self.events, self.position, self.delay_repaint)

{'commit': 'diff --git a/frontend/ui/widget/toggleswitch.lua b/frontend/ui/widget/toggleswitch.lua\nindex 6ef241b7..e61b2e41 100644\n--- a/frontend/ui/widget/toggleswitch.lua\n+++ b/frontend/ui/widget/toggleswitch.lua\n@@ -217,7 +217,9 @@ function ToggleSwitch:onTapSelect(arg, gev)\n         self.config:onConfigEvents(self.events, self.position)\n     end\n     --]]\n-    self.callback(self.position)\n+    if self.callback then\n+        self.callback(self.position)\n+    end\n     if self.toggle[self.position] ~= "⋮" then\n         self.config:onConfigChoose(self.values, self.name,\n             self.event, self.args, self.events, self.position, self.delay_repaint)\n', 'commit_subject': 'Fix crash on book status change (#5453)', 'repo_name': 'koreader/koreader', 'committer': {'name': 'poire-z', 'email': '1c8e309c3b0fdde8adf6a8ab3f59f8be10f6825c@users.noreply.github.com', 'time_sec': '1570223214', 'tz_offset': '120', 'date': {'seconds': '1570223214'}}, 'commit_tree': 'a88de4b74d8e5aa915de4611c00c57fe1342f308', 'commit_message': 'Fix crash on book status change (#5453)\n\n', 'language_name': 'Shell', 'license': 'agpl-3.0', 'hunks': '@@ -217,7 +217,9 @@ function ToggleSwitch:onTapSelect(arg, gev)\n         self.config:onConfigEvents(self.events, self.position)\n     end\n     --]]\n-    self.callback(self.position)\n+    if self.callback then\n+        self.callback(self.position)\n+    end\n     if self.toggle[self.position] ~= "⋮" then\n         self.config:onConfigChoose(self.values, self.name,\n             self.event, self.args, self.events, self.position, self.delay_repaint)\n', 'addition_count': 3, 'deletion_count': 1, 'src_file': 'a/frontend/ui/widget/toggleswitch.lua', 'tgt_file': 'b/frontend/ui/widget/toggleswitch.lua', 'file_extension': '.lua', 'before_file': '--[[--\nDisplays a button that toggles between states. Used in bottom configuration panel.\n\n@usage\n    local ToggleSwitch = require("ui/widget/toggleswitch")\n]]\n\nlocal BD = require("ui/bidi")\nlocal Blitbuffer = require("ffi/blitbuffer")\nlocal CenterContainer = require("ui/widget/container/centercontainer")\nlocal Device = require("device")\nlocal FocusManager = require("ui/widget/focusmanager")\nlocal Font = require("ui/font")\nlocal Geom = require("ui/geometry")\nlocal GestureRange = require("ui/gesturerange")\nlocal HorizontalGroup = require("ui/widget/horizontalgroup")\nlocal FrameContainer = require("ui/widget/container/framecontainer")\nlocal Notification = require("ui/widget/notification")\nlocal Size = require("ui/size")\nlocal TextWidget = require("ui/widget/textwidget")\nlocal UIManager = require("ui/uimanager")\nlocal VerticalGroup = require("ui/widget/verticalgroup")\nlocal _ = require("gettext")\nlocal Screen = Device.screen\n\nlocal ToggleLabel = TextWidget:extend{\n    bold = true,\n    bgcolor = Blitbuffer.COLOR_WHITE,\n    fgcolor = Blitbuffer.COLOR_BLACK,\n}\n\nlocal ToggleSwitch = FocusManager:extend{\n    width = Screen:scaleBySize(216),\n    height = Size.item.height_default,\n    bgcolor = Blitbuffer.COLOR_WHITE, -- unfocused item color\n    fgcolor = Blitbuffer.COLOR_DARK_GRAY, -- focused item color\n    font_face = "cfont",\n    font_size = 16,\n    enabled = true,\n    row_count = 1,\n}\n\nfunction ToggleSwitch:init()\n    self.layout = {{}}\n    -- Item count per row\n    self.n_pos = math.ceil(#self.toggle / self.row_count)\n    self.position = nil\n\n    self.toggle_frame = FrameContainer:new{\n        background = Blitbuffer.COLOR_WHITE,\n        color = Blitbuffer.COLOR_DARK_GRAY,\n        radius = Size.radius.window,\n        bordersize = Size.border.thin,\n        padding = Size.padding.small,\n        dim = not self.enabled,\n    }\n\n    self.toggle_content = VerticalGroup:new{}\n    for i = 1, self.row_count do\n        table.insert(self.toggle_content, HorizontalGroup:new{})\n    end\n\n    local item_padding = Size.padding.default -- only used to check if text truncate needed\n    local item_border_size = Size.border.thin\n    local frame_inner_width = self.width - 2*self.toggle_frame.padding - 2* self.toggle_frame.bordersize\n    -- We\'ll need to adjust items width and distribute the accumulated fractional part to some\n    -- of them for proper visual alignment\n    local item_width_real = frame_inner_width / self.n_pos - 2*item_border_size\n    local item_width = math.floor(item_width_real)\n    local item_width_adjust = item_width_real - item_width\n    -- Note: the height provided by ConfigDialog might be smaller than needed,\n    -- it gets too thin if we account for padding & border\n    local item_height = self.height / self.row_count\n    local item_width_to_add = 0\n    for i = 1, #self.toggle do\n        local real_item_width = item_width\n        item_width_to_add = item_width_to_add + item_width_adjust\n        if item_width_to_add >= 1 then\n            -- One pixel wider to better align the entire widget\n            real_item_width = item_width + math.floor(item_width_to_add)\n            item_width_to_add = item_width_to_add - math.floor(item_width_to_add)\n        end\n        local text = self.toggle[i]\n        local face = Font:getFace(self.font_face, self.font_size)\n        local label = ToggleLabel:new{\n            text = text,\n            face = face,\n            max_width = real_item_width - item_padding,\n        }\n        local content = CenterContainer:new{\n            dimen = Geom:new{\n                w = real_item_width,\n                h = item_height,\n            },\n            label,\n        }\n        local button = FrameContainer:new{\n            background = Blitbuffer.COLOR_WHITE,\n            color = Blitbuffer.COLOR_DARK_GRAY,\n            margin = 0,\n            radius = Size.radius.window,\n            bordersize = item_border_size,\n            padding = 0,\n            focusable = true,\n            focus_border_size = item_border_size,\n            focus_border_color = Blitbuffer.COLOR_BLACK,\n            content,\n        }\n        table.insert(self.toggle_content[math.ceil(i / self.n_pos)], button)\n        table.insert(self.layout[1], button)\n    end\n    self.toggle_frame[1] = self.toggle_content\n    self[1] = self.toggle_frame\n    self.dimen = Geom:new(self.toggle_frame:getSize())\n    self.ges_events = {\n        TapSelect = {\n            GestureRange:new{\n                ges = "tap",\n                range = self.dimen,\n            },\n        },\n        HoldSelect = {\n            GestureRange:new{\n                ges = "hold",\n                range = self.dimen,\n            },\n        },\n    }\nend\n\nfunction ToggleSwitch:update()\n    local pos = self.position\n    for i = 1, #self.toggle_content do\n        local row = self.toggle_content[i]\n        for j = 1, #row do\n            local cell = row[j]\n            if pos == (i - 1) * self.n_pos + j then\n                cell.color = self.fgcolor\n                cell.background = self.fgcolor\n                cell[1][1].fgcolor = Blitbuffer.COLOR_WHITE\n            else\n                cell.color = self.bgcolor\n                cell.background = self.bgcolor\n                cell[1][1].fgcolor = Blitbuffer.COLOR_BLACK\n            end\n        end\n    end\nend\n\nfunction ToggleSwitch:setPosition(position)\n    self.position = position\n    self:update()\nend\n\nfunction ToggleSwitch:togglePosition(position, update)\n    if self.n_pos == 2 and self.alternate ~= false then\n        self.position = (self.position+1)%self.n_pos\n        self.position = self.position == 0 and self.n_pos or self.position\n    elseif self.n_pos == 1 then\n        self.position = self.position == 1 and 0 or 1\n    else\n        self.position = position\n    end\n    if update then\n        self:update()\n    end\nend\n\nfunction ToggleSwitch:circlePosition()\n    if self.position then\n        self.position = (self.position+1)%self.n_pos\n        self.position = self.position == 0 and self.n_pos or self.position\n        self:update()\n    end\nend\n\nfunction ToggleSwitch:calculatePosition(gev)\n    local x = (gev.pos.x - self.dimen.x) / self.dimen.w * self.n_pos\n    if BD.mirroredUILayout() then\n        x = self.n_pos - x\n    end\n    local y = (gev.pos.y - self.dimen.y) / self.dimen.h * self.row_count\n    return math.max(1, math.ceil(x)) + math.min(self.row_count-1, math.floor(y)) * self.n_pos\nend\n\nfunction ToggleSwitch:onTapSelect(arg, gev)\n    if not self.enabled then\n        if self.readonly ~= true then\n            return true\n        else\n            return\n        end\n    end\n    if gev then\n        local position = self:calculatePosition(gev)\n        if self.toggle[position] ~= "⋮" then\n            self:togglePosition(position, true)\n        else\n            self:togglePosition(position, false)\n        end\n    else\n        self:circlePosition()\n    end\n\n    --[[\n    if self.values then\n        self.values = self.values or {}\n        self.config:onConfigChoice(self.name, self.values[self.position])\n    end\n    if self.event then\n        self.args = self.args or {}\n        self.config:onConfigEvent(self.event, self.args[self.position])\n    end\n    --]]\n    if self.callback then\n        self.callback(self.position)\n        self.config:onConfigEvents(self.events, self.position)\n    end\n    --]]\n    self.callback(self.position)\n    if self.toggle[self.position] ~= "⋮" then\n        self.config:onConfigChoose(self.values, self.name,\n            self.event, self.args, self.events, self.position, self.delay_repaint)\n\n        UIManager:setDirty(self.config, function()\n            return "ui", self.dimen\n        end)\n\n        UIManager:tickAfterNext(function()\n            Notification:setNotifySource(Notification.SOURCE_OTHER) -- only allow events, if they are activated\n        end)\n    end\n    return true\nend\n\nfunction ToggleSwitch:onHoldSelect(arg, gev)\n    local position = self:calculatePosition(gev)\n    if self.toggle[position] == "⋮" then\n        return true\n    end\n    if self.name == "font_fine_tune" then\n        --- @note Ugly hack for the only widget that uses a dual toggle for fine-tuning (others prefer a buttonprogress)\n        self.config:onMakeFineTuneDefault("font_size", _("Font Size"),\n                        self.values or self.args, self.toggle, position == 1 and "-" or "+")\n    else\n        self.config:onMakeDefault(self.name, self.name_text,\n                        self.values or self.args, self.toggle, position)\n    end\n    return true\nend\n\nreturn ToggleSwitch\n', 'message': 'Fix crash on book status change (#5453)\n\n', 'author_name': 'poire-z', 'lang': 'lua'}