glava-config widget generation (incomplete)
This commit is contained in:
307
glava-config/config.lua
Normal file
307
glava-config/config.lua
Normal file
@@ -0,0 +1,307 @@
|
||||
local lfs = require "lfs"
|
||||
local mappings = require "glava-config.mappings"
|
||||
|
||||
local config = {
|
||||
Profile = { mt = {} }
|
||||
}
|
||||
|
||||
config.Profile.__index = config.Profile
|
||||
setmetatable(config.Profile, config.Profile.mt)
|
||||
|
||||
-- Split path into entries, such that `table.concat` can be used to
|
||||
-- reconstruct the path. Prepends the result with an empty string so
|
||||
-- root (absolute) paths are preserved
|
||||
local function path_split(str, sep)
|
||||
local sep, fields = sep or ":", (str:sub(1, sep:len()) == sep and {""} or {})
|
||||
local pattern = string.format("([^%s]+)", sep)
|
||||
str:gsub(pattern, function(c) fields[#fields + 1] = c end)
|
||||
return fields
|
||||
end
|
||||
|
||||
-- Concatenates paths such that duplicate path separators are removed.
|
||||
-- Can be used on non-split arguments
|
||||
local function path_concat(...)
|
||||
local ret = {}
|
||||
for _, v in ipairs({...}) do
|
||||
for _, e in ipairs(path_split(v, "/")) do
|
||||
if e ~= "" or #ret == 0 then
|
||||
ret[#ret + 1] = e
|
||||
end
|
||||
end
|
||||
end
|
||||
return table.concat(ret, "/")
|
||||
end
|
||||
|
||||
-- To parse data from GLSL configs we use some complex pattern matching.
|
||||
--
|
||||
-- Because Lua's patterns operate on a per-character basis and do not offer
|
||||
-- any read-ahead functionality, we use a pattern 'replacement' functionality
|
||||
-- such that the match of an input pattern is passed to a function to produce
|
||||
-- an output pattern.
|
||||
--
|
||||
-- This effectively means we have some fairly powerful parsing which allows us
|
||||
-- to handle things like quoted strings with escaped characters.
|
||||
local function none(...) return ... end
|
||||
local MATCH_ENTRY_PATTERN = "^%s*%#(%a+)%s+(%a+)"
|
||||
local MATCH_DATA_PREFIX = "^%s*%#%a+%s+%a+"
|
||||
local MATCH_TYPES = {
|
||||
["float"] = {
|
||||
pattern = "(%d+.?%d*)",
|
||||
cast = tonumber,
|
||||
serialize = tostring
|
||||
},
|
||||
["int"] = {
|
||||
pattern = "(%d+)",
|
||||
cast = tonumber,
|
||||
serialize = function(x) tostring(math.floor(x)) end
|
||||
},
|
||||
["string"] = {
|
||||
pattern = "(.+)",
|
||||
-- Strip away string quotation and escape syntax
|
||||
cast = function(match)
|
||||
local ret = {}
|
||||
local escaped = false
|
||||
for c in match:gmatch(".") do
|
||||
if c == "\"" then
|
||||
if escaped then ret[#ret + 1] = c end
|
||||
elseif c ~= "\\" then ret[#ret + 1] = c end
|
||||
if c == "\\" then
|
||||
if escaped then ret[#ret + 1] = c end
|
||||
escaped = not escaped
|
||||
else escaped = false end
|
||||
end
|
||||
return table.concat(ret, "")
|
||||
end,
|
||||
-- Read-ahead function to generate a fixed-width pattern
|
||||
-- to match the next (possibly quoted) string
|
||||
transform = function(match)
|
||||
local quoted = false
|
||||
local start = true
|
||||
local escaped = false
|
||||
local count = 0
|
||||
local skip = 0
|
||||
for c in match:gmatch(".") do
|
||||
count = count + 1
|
||||
if c == "\"" then
|
||||
if start then
|
||||
start = false
|
||||
quoted = true
|
||||
elseif not escaped then
|
||||
if quoted then
|
||||
-- End-quote; end of string
|
||||
break
|
||||
else
|
||||
-- Formatting error: non-escaped quote after string start: `foo"bar`
|
||||
-- We attempt to resolve this by halting parsing and skipping the
|
||||
-- out-of-context quotation
|
||||
count = count - 1
|
||||
skip = skip + 1
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif c == " " then
|
||||
if not start and not quoted then
|
||||
-- Un-escaped space; end of string
|
||||
-- skip the space itself
|
||||
count = count - 1
|
||||
break
|
||||
end
|
||||
else start = false end
|
||||
if c == "\\" then
|
||||
escaped = not escaped
|
||||
else escaped = false end
|
||||
end
|
||||
-- Strings without an ending quote will simply take up the remainder of
|
||||
-- the request, causing the following arguments to be overwritten. This
|
||||
-- is intended to ensure we can save valid options after stripping out
|
||||
-- the errornous quotes and using defaults for the subsequent arguments.
|
||||
local ret = { "(" }
|
||||
for t = 1, count do
|
||||
ret[1 + t] = "."
|
||||
end
|
||||
ret[2 + count] = ")"
|
||||
for t = 1, skip do
|
||||
ret[2 + count + t] = "."
|
||||
end
|
||||
return table.concat(ret, "")
|
||||
end,
|
||||
serialize = function(x)
|
||||
return string.format("\"%s\"", x)
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
config.path_concat = path_concat
|
||||
config.path_split = path_split
|
||||
|
||||
local function create_p(parts, mode, silent)
|
||||
local function errfmt(err)
|
||||
return string.format("Failed to create '%s' in '%s': %s",
|
||||
path_concat(parts, "/"), path_concat(arr, "/"), err)
|
||||
end
|
||||
for i, v in ipairs(arr) do
|
||||
parts[#parts + 1] = v
|
||||
local failret = false
|
||||
if silent then failret = #parts == #arr end
|
||||
local path = path_concat(parts, "/")
|
||||
local m = (i == #arr and mode or "directory")
|
||||
local attr, err = lfs.attributes(path, "mode")
|
||||
if attr == nil then
|
||||
({
|
||||
file = function()
|
||||
local ret, err = lfs.touch(path)
|
||||
if ret ~= true then return false, errfmt(err) end
|
||||
end,
|
||||
directory = function()
|
||||
local ret, err = lfs.mkdir(path)
|
||||
if ret ~= true then return false, errfmt(err) end
|
||||
end,
|
||||
})[m]()
|
||||
elseif attr ~= m then
|
||||
if not (silent and #parts == #arr) then
|
||||
return false, string.format("'%s' is not a %s", path, m)
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function create_p(path, ...) create_pf(path_split(path, "/"), ...) end
|
||||
local function unwrap(ret, err)
|
||||
if ret == nil or ret == false then
|
||||
glava.fail(err)
|
||||
else return ret end
|
||||
end
|
||||
|
||||
function config.Profile:__call(args)
|
||||
local self = { name = args.name }
|
||||
self:rebuild()
|
||||
return setmetatable(self, config.Profile)
|
||||
end
|
||||
|
||||
function config.Profile:rename(new)
|
||||
|
||||
end
|
||||
|
||||
function config.Profile:rebuild()
|
||||
self.store = {}
|
||||
self.path = path_concat(glava.config_path, "profiles", self.name)
|
||||
unwrap(create_p(self.path, "directory", true))
|
||||
local unbuilt = {}
|
||||
for k, _ in pairs(mappings) do
|
||||
unbuilt[k] = true
|
||||
end
|
||||
for file in lfs.dir(self.path) do
|
||||
if file ~= "." and file ~= ".." and mappings[file] ~= nil then
|
||||
self:rebuild_file(file, path_concat(path, file))
|
||||
unbuilt[file] = nil
|
||||
end
|
||||
end
|
||||
for file, _ in pairs(unbuilt) do
|
||||
self:rebuild_file(file, path_concat(path, file), true)
|
||||
end
|
||||
end
|
||||
|
||||
function config.Profile:rebuild_file(file, path, phony)
|
||||
local fstore = {}
|
||||
local fmap = mappings[file]
|
||||
self.store[file] = fstore
|
||||
|
||||
for k, _ in pairs(fmap) do
|
||||
if type(k) == "string" and k ~= "name" then
|
||||
unbuilt[k] = true
|
||||
end
|
||||
end
|
||||
|
||||
function parse_line(line, idx, key, default)
|
||||
local map = fmap[key]
|
||||
if map == nil then return end
|
||||
local tt = type(map.field_type) == "table" and map.field_type or { map.field_type }
|
||||
local _,e = string.find(line, MATCH_DATA_PREFIX)
|
||||
local at = string.sub(line, 1, e)
|
||||
if default == nil or fstore[key] == nil then
|
||||
fstore[key] = {}
|
||||
end
|
||||
if default == nil then fstore[key].line = idx end
|
||||
for t, v in ipairs(tt) do
|
||||
local r, i, match = string.find(at, "%s*" .. MATCH_TYPES[v].pattern)
|
||||
if r ~= nil then
|
||||
-- Handle read-ahead pattern transforms
|
||||
if MATCH_TYPES[v].transform ~= nil then
|
||||
_, i, match = string.find(at, "%s*" .. MATCH_TYPES[v].transform(match))
|
||||
end
|
||||
if default == nil or fstore[key][t] == nil then
|
||||
fstore[key][t] = MATCH_TYPES[v].cast(match)
|
||||
end
|
||||
at = string.sub(at, 1, i)
|
||||
else break end
|
||||
end
|
||||
end
|
||||
|
||||
local idx = 1
|
||||
if phony ~= true then
|
||||
for line in io.lines(path) do
|
||||
local mtype, arg = string.match(line, MATCH_ENTRY_PATTERN)
|
||||
if mtype ~= nil then
|
||||
parse_line(line, idx, string.format("%s:%s", mtype, arg))
|
||||
end
|
||||
idx = idx + 1
|
||||
end
|
||||
end
|
||||
|
||||
idx = 1
|
||||
for line in io.lines(path_concat(glava.system_shader_path, file)) do
|
||||
local mtype, arg = string.match(line, MATCH_ENTRY_PATTERN)
|
||||
if mtype ~= nil then
|
||||
parse_line(line, idx, string.format("%s:%s", mtype, arg), true)
|
||||
end
|
||||
idx = idx + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Sync all
|
||||
function config.Profile:sync()
|
||||
for k, v in pairs(self.store) do self:sync_file(k) end
|
||||
end
|
||||
|
||||
-- Sync filename relative to profile root
|
||||
function config.Profile:sync_file(fname)
|
||||
local fstore = self.store[fname]
|
||||
local fmap = mappings[file]
|
||||
local fpath = path_concat(self.path, fname)
|
||||
local buf = {}
|
||||
local extra = {}
|
||||
local idx = 1
|
||||
for k, v in fstore do
|
||||
local parts = { string.format("#%s", string.gsub(k, ":", " ")) }
|
||||
local field = fmap[k].field_type
|
||||
for i, e in ipairs(type(field) == "table" and field or { field }) do
|
||||
parts[#parts + 1] = MATCH_TYPES[e].serialize(v[i])
|
||||
end
|
||||
local serialized = table.concat(parts, " ")
|
||||
if v.line then buf[line] = serialized
|
||||
else extra[#extra + 1] = serialized end
|
||||
end
|
||||
if lfs.attributes(fpath, "mode") == "file" then
|
||||
for line in io.lines(path) do
|
||||
if not buf[idx] then
|
||||
buf[idx] = line
|
||||
end
|
||||
idx = idx + 1
|
||||
end
|
||||
for _, v in ipairs(extra) do
|
||||
buf[#buf + 1] = v
|
||||
end
|
||||
end
|
||||
local handle, err = io.open(fpath, "w+")
|
||||
if handle then
|
||||
handle:write(table.concat(buf, "\n"))
|
||||
handle:close()
|
||||
else
|
||||
glava.fail(string.format("Could not open file handle to \"%s\": %s", handle, err))
|
||||
end
|
||||
end
|
||||
|
||||
return config
|
||||
@@ -1,5 +1,3 @@
|
||||
local window = require("glava-config.window")
|
||||
|
||||
local function dependency(name)
|
||||
if package.loaded[name] then
|
||||
return
|
||||
@@ -17,14 +15,64 @@ local function dependency(name)
|
||||
end
|
||||
end
|
||||
|
||||
function glava.fail(message)
|
||||
print(string.format("!!FATAL!!: %s", message))
|
||||
os.exit(1)
|
||||
end
|
||||
|
||||
local main = {}
|
||||
|
||||
-- Format string, but silently return nil if varargs contains any nil entries
|
||||
local function format_silent(fmt, ...)
|
||||
for _, v in ipairs({...}) do
|
||||
if v == nil then return nil end
|
||||
end
|
||||
return string.format(fmt, ...)
|
||||
end
|
||||
|
||||
function main.entry(prog, ...)
|
||||
dependency("lgi")
|
||||
dependency("lfs")
|
||||
|
||||
if glava.resource_path:sub(glava.resource_path:len()) ~= "/" then
|
||||
glava.resource_path = glava.resource_path .. "/"
|
||||
end
|
||||
glava.config_path = format_silent("%s/glava", os.getenv("XDG_CONFIG_HOME"))
|
||||
or format_silent("%s/.config/glava", os.getenv("HOME"))
|
||||
or "/home/.config/glava"
|
||||
|
||||
local lfs = require "lfs"
|
||||
local window = require "glava-config.window"
|
||||
|
||||
glava.module_list = {}
|
||||
for m in lfs.dir(glava.system_shader_path) do
|
||||
if m ~= "." and m ~= ".."
|
||||
and lfs.attributes(glava.system_shader_path .. "/" .. m, "mode") == "directory"
|
||||
and m ~= "util" then
|
||||
glava.module_list[#glava.module_list + 1] = m
|
||||
end
|
||||
end
|
||||
|
||||
local mappings = require "glava-config.mappings"
|
||||
-- Associate `map_name = tbl` from mapping list for future lookups
|
||||
for k, v in pairs(mappings) do
|
||||
local i = 1
|
||||
local adv = false
|
||||
while v[i] ~= nil do
|
||||
if type(v[i]) == "table" then
|
||||
v[v[i][1]] = v[i]
|
||||
v[i].advanced = adv
|
||||
i = i + 1
|
||||
elseif type(v[i]) == "string" and v[i] == "advanced" then
|
||||
adv = true
|
||||
table.remove(v, i)
|
||||
else
|
||||
glava.fail(string.format("Unknown mappings entry type for file: \"%s\"", type(v)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Enter into Gtk window
|
||||
window()
|
||||
end
|
||||
|
||||
|
||||
40
glava-config/mappings.lua
Normal file
40
glava-config/mappings.lua
Normal file
@@ -0,0 +1,40 @@
|
||||
return {
|
||||
["rc.glsl"] = {
|
||||
name = "Global Options",
|
||||
{
|
||||
"request:mod",
|
||||
field_type = "string",
|
||||
field_attrs = {
|
||||
entries = glava.module_list
|
||||
},
|
||||
description = "Visualizer module"
|
||||
},
|
||||
{
|
||||
"request:fakeident",
|
||||
field_type = "ident",
|
||||
description = "Some identifier"
|
||||
},
|
||||
{
|
||||
"request:fakefloat",
|
||||
field_type = "float",
|
||||
description = "Some Float"
|
||||
},
|
||||
{
|
||||
"request:setbg",
|
||||
field_type = "color",
|
||||
field_attrs = { alpha = true },
|
||||
description = "Window background color"
|
||||
},
|
||||
"advanced",
|
||||
{
|
||||
"request:setversion",
|
||||
field_type = { "int", "int" },
|
||||
field_attrs = {
|
||||
frame_label = "Version",
|
||||
{ label = "Major:", lower = 0, upper = 10, width = 2 },
|
||||
{ label = "Minor:", lower = 0, upper = 10, width = 2 }
|
||||
},
|
||||
description = "OpenGL context version request"
|
||||
}
|
||||
}
|
||||
}
|
||||
68
glava-config/utils.lua
Normal file
68
glava-config/utils.lua
Normal file
@@ -0,0 +1,68 @@
|
||||
local lgi = require "lgi"
|
||||
local Gdk = lgi.Gdk
|
||||
|
||||
local utils = {}
|
||||
|
||||
function utils.infer_color_bits(x)
|
||||
if x:sub(1, 1) ~= "#" then
|
||||
x = "#" .. x
|
||||
end
|
||||
for i = 1, 9 - x:len() do
|
||||
x = x .. (x:len() >= 7 and "F" or "0")
|
||||
end
|
||||
return x
|
||||
end
|
||||
|
||||
function utils.sanitize_color(x)
|
||||
return utils.infer_color_bits(x):sub(1, 9):gsub("[^#0-9a-fA-F]", "0")
|
||||
end
|
||||
|
||||
function utils.parse_color_rgba(x)
|
||||
local x = utils.infer_color_bits(x)
|
||||
return Gdk.RGBA.parse(
|
||||
string.format(
|
||||
"rgba(%d,%d,%d,%f)",
|
||||
tonumber(x:sub(2, 3), 16),
|
||||
tonumber(x:sub(4, 5), 16),
|
||||
tonumber(x:sub(6, 7), 16),
|
||||
tonumber(x:sub(8, 9), 16) / 255
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
function utils.rgba_to_gdk_color(x)
|
||||
return Gdk.Color(
|
||||
math.floor(x.red * 255 + 0.5),
|
||||
math.floor(x.green * 255 + 0.5),
|
||||
math.floor(x.blue * 255 + 0.5)
|
||||
)
|
||||
end
|
||||
|
||||
function utils.rgba_to_integral(x)
|
||||
return {
|
||||
red = math.floor(x.red * 255 + 0.5),
|
||||
green = math.floor(x.green * 255 + 0.5),
|
||||
blue = math.floor(x.blue * 255 + 0.5)
|
||||
}
|
||||
end
|
||||
|
||||
function utils.format_color_rgba(x)
|
||||
return string.format(
|
||||
"#%02X%02X%02X%02X",
|
||||
math.floor(x.red * 255 + 0.5),
|
||||
math.floor(x.green * 255 + 0.5),
|
||||
math.floor(x.blue * 255 + 0.5),
|
||||
math.floor(x.alpha * 255 + 0.5)
|
||||
)
|
||||
end
|
||||
|
||||
function utils.format_color_rgb(x)
|
||||
return string.format(
|
||||
"#%02X%02X%02X",
|
||||
math.floor(x.red * 255 + 0.5),
|
||||
math.floor(x.green * 255 + 0.5),
|
||||
math.floor(x.blue * 255 + 0.5)
|
||||
)
|
||||
end
|
||||
|
||||
return utils
|
||||
@@ -1,10 +1,52 @@
|
||||
--[[
|
||||
MAINTAINER NOTICE:
|
||||
|
||||
This application aims to be both Gtk+ 3 and 4 compatible for future-proofing. This means
|
||||
avoiding *every* deprecated widget in Gtk+ 3, and watching out for some old functionality:
|
||||
|
||||
* Gdk.Color usage, use Gdk.RGBA instead
|
||||
* Pango styles and style overrides
|
||||
* Check convenience wrappers for deprecation, ie. GtkColorButton
|
||||
* Avoid seldom used containers, as they may have been removed in 4.x (ie. GtkButtonBox)
|
||||
|
||||
In some cases we use deprecated widgets or 3.x restricted functionality, but only when we
|
||||
query that the types are available from LGI (and otherwise use 4.x compatible code).
|
||||
]]
|
||||
|
||||
return function()
|
||||
local lgi = require 'lgi'
|
||||
local GObject = lgi.GObject
|
||||
local Gtk = lgi.Gtk
|
||||
local Pango = lgi.Pango
|
||||
local Gdk = lgi.Gdk
|
||||
local lgi = require 'lgi'
|
||||
local utils = require 'glava-config.utils'
|
||||
local mappings = require 'glava-config.mappings'
|
||||
local GObject = lgi.GObject
|
||||
local Gtk = lgi.Gtk
|
||||
local Pango = lgi.Pango
|
||||
local Gdk = lgi.Gdk
|
||||
local GdkPixbuf = lgi.GdkPixbuf
|
||||
local cairo = lgi.cairo
|
||||
|
||||
-- Both `GtkColorChooserDialog` and `GtkColorSelectionDialog` are
|
||||
-- supported by this tool, but the latter is deprecated and does
|
||||
-- not exist in 4.x releases.
|
||||
--
|
||||
-- The old chooser, however, is objectively better so let's try
|
||||
-- to use it if it exists.
|
||||
local use_old_chooser = true
|
||||
if Gtk.ColorSelectionDialog == nil then
|
||||
use_old_chooser = false
|
||||
end
|
||||
|
||||
local window
|
||||
|
||||
local repeat_pattern = cairo.SurfacePattern(
|
||||
cairo.ImageSurface.create_from_png(glava.resource_path .. "transparent.png")
|
||||
)
|
||||
repeat_pattern:set_extend("REPEAT")
|
||||
|
||||
-- We need to define a CSS class to use an alternative font for
|
||||
-- color and identity entries; used to indicate to the user that
|
||||
-- the field has formatting requirements
|
||||
local cssp = Gtk.CssProvider {}
|
||||
cssp:load_from_data(".fixed-width-font-entry { font-family: \"Monospace\"; }")
|
||||
|
||||
local ItemColumn = {
|
||||
PROFILE = 1,
|
||||
@@ -31,58 +73,340 @@ return function()
|
||||
[ItemColumn.WEIGHT] = 600
|
||||
}
|
||||
|
||||
-- Apply `t[k] = v` to all table argument at array indexes,
|
||||
-- and return the unpacked list of tables. Used for nesting
|
||||
-- widget construction.
|
||||
local function apply(tbl)
|
||||
local ret = {}
|
||||
for k, v in ipairs(tbl) do
|
||||
ret[k] = v
|
||||
tbl[k] = nil
|
||||
end
|
||||
for k, v in pairs(tbl) do
|
||||
for _, r in ipairs(ret) do
|
||||
r[k] = v
|
||||
end
|
||||
end
|
||||
return unpack(ret)
|
||||
end
|
||||
|
||||
local function ComboBoxFixed(tbl)
|
||||
local inst = Gtk.ComboBoxText { id = tbl.id }
|
||||
for _, v in pairs(tbl) do
|
||||
if type(v) == "table" then
|
||||
inst:append_text(v[1])
|
||||
end
|
||||
inst:append_text(v)
|
||||
end
|
||||
inst:set_active(tbl.default or 0)
|
||||
return inst
|
||||
end
|
||||
|
||||
local SpoilerView = function(tbl)
|
||||
local stack = Gtk.Stack {
|
||||
expand = true,
|
||||
transition_type = Gtk.StackTransitionType.CROSSFADE
|
||||
}
|
||||
local btn = Gtk.CheckButton {
|
||||
active = tbl.active or false
|
||||
}
|
||||
if tbl.active ~= true then
|
||||
stack:add_named(Gtk.Box {}, "none")
|
||||
end
|
||||
stack:add_named(tbl[1], "view")
|
||||
if tbl.active == true then
|
||||
stack:add_named(Gtk.Box {}, "none")
|
||||
end
|
||||
function btn:on_toggled(path)
|
||||
stack:set_visible_child_name(btn.active and "view" or "none")
|
||||
end
|
||||
return Gtk.Box {
|
||||
expand = false,
|
||||
orientation = "VERTICAL",
|
||||
spacing = 4,
|
||||
Gtk.Box {
|
||||
orientation = "HORIZONTAL",
|
||||
spacing = 6,
|
||||
btn,
|
||||
Gtk.Label { label = tbl.label or "Spoiler" }
|
||||
},
|
||||
Gtk.Separator(),
|
||||
stack
|
||||
}
|
||||
end
|
||||
|
||||
local ConfigView = function(tbl)
|
||||
local grid = {
|
||||
row_spacing = 5,
|
||||
column_spacing = 12,
|
||||
column_homogeneous = false,
|
||||
row_homogeneous = true
|
||||
}
|
||||
local list = {}
|
||||
local idx = 0
|
||||
for _, entry in pairs(tbl) do
|
||||
local function cbuild(list, entry, idx)
|
||||
list[#list + 1] = {
|
||||
Gtk.Label { label = entry[1], xalign = 0 },
|
||||
Gtk.Label { label = entry[1], halign = "START", valign = "START" },
|
||||
left_attach = 0, top_attach = idx
|
||||
}
|
||||
list[#list + 1] = {
|
||||
Gtk.Alignment { xscale = 0, yscale = 0, xalign = 1, entry[2] },
|
||||
Gtk.Box { hexpand = true },
|
||||
left_attach = 1, top_attach = idx
|
||||
}
|
||||
list[#list + 1] = {
|
||||
apply { halign = "FILL", hexpand = false, entry[2] },
|
||||
left_attach = 2, top_attach = idx
|
||||
}
|
||||
end
|
||||
for _, entry in ipairs(tbl) do
|
||||
cbuild(list, entry, idx)
|
||||
idx = idx + 1
|
||||
end
|
||||
local adv = {}
|
||||
if tbl.advanced then
|
||||
idx = 0
|
||||
for _, entry in ipairs(tbl.advanced) do
|
||||
cbuild(adv, entry, idx)
|
||||
idx = idx + 1
|
||||
end
|
||||
end
|
||||
for k, v in pairs(grid) do
|
||||
list[k] = v
|
||||
adv[k] = v
|
||||
end
|
||||
return Gtk.ScrolledWindow {
|
||||
shadow_type = "IN",
|
||||
expand = true,
|
||||
Gtk.Alignment {
|
||||
top_padding = 12,
|
||||
left_padding = 20,
|
||||
right_padding = 20,
|
||||
xscale = 1,
|
||||
yscale = 1,
|
||||
xalign = 0,
|
||||
Gtk.Grid {
|
||||
row_spacing = 5,
|
||||
column_spacing = 12,
|
||||
column_homogeneous = true,
|
||||
unpack(list)
|
||||
}
|
||||
Gtk.Box {
|
||||
margin_top = 12,
|
||||
margin_left = 16,
|
||||
margin_right = 16,
|
||||
hexpand = true,
|
||||
vexpand = true,
|
||||
halign = "FILL",
|
||||
orientation = "VERTICAL",
|
||||
spacing = 6,
|
||||
Gtk.Grid(list),
|
||||
#adv > 0 and
|
||||
SpoilerView {
|
||||
label = "Show Advanced",
|
||||
Gtk.Grid(adv)
|
||||
} or Gtk.Box {}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
local function wrap_label(widget, label)
|
||||
if label then
|
||||
widget = Gtk.Box {
|
||||
orientation = "HORIZONTAL",
|
||||
spacing = 6,
|
||||
Gtk.Label {
|
||||
label = label
|
||||
}, widget
|
||||
}
|
||||
end
|
||||
return widget
|
||||
end
|
||||
local widget_generators
|
||||
widget_generators = {
|
||||
["boolean"] = function(attrs)
|
||||
local widget = Gtk.Switch { hexpand = false }
|
||||
return {
|
||||
widget = Gtk.Box { Gtk.Box { hexpand = true }, wrap_label(widget, attrs.label) },
|
||||
set_data = function(x)
|
||||
widget.active = x
|
||||
end
|
||||
}
|
||||
end,
|
||||
["string"] = function(attrs)
|
||||
local widget = attrs.entries ~= nil
|
||||
and apply { hexpand = true, ComboBoxFixed(attrs.entries) }
|
||||
or Gtk.Entry { width_chars = 16 }
|
||||
return {
|
||||
widget = wrap_label(widget, attrs.label),
|
||||
internal = widget,
|
||||
set_data = function(x)
|
||||
if not attrs.entries then
|
||||
widget:set_text(x)
|
||||
else
|
||||
for k, v in ipairs(attrs.entries) do
|
||||
if v == x then
|
||||
widget:set_active(v - 1)
|
||||
return
|
||||
end
|
||||
end
|
||||
local fmt = "WARNING: Invalid string entry for Gtk.ComboBox mapping: \"%s\""
|
||||
print(string.format(fmt, x))
|
||||
end
|
||||
end
|
||||
}
|
||||
end,
|
||||
["ident"] = function(attrs)
|
||||
local s = widget_generators.string(attrs)
|
||||
s.internal:get_style_context():add_provider(cssp, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||
s.internal:get_style_context():add_class("fixed-width-font-entry")
|
||||
if not attrs.entries then
|
||||
-- Handle idenifier formatting for entries without a preset list
|
||||
function s.internal:on_changed()
|
||||
local i = s.internal.text
|
||||
if i:match("[^%w]") ~= nil or i:sub(1, 1):match("[^%a]") ~= nil then
|
||||
s.internal.text = i:gsub("[^%w]", ""):gsub("^[^%a]+", "")
|
||||
end
|
||||
-- todo: handle changed (signal override?)
|
||||
end
|
||||
end
|
||||
return s
|
||||
end,
|
||||
["float"] = function(attrs)
|
||||
local widget = Gtk.SpinButton {
|
||||
hexpand = true,
|
||||
adjustment = Gtk.Adjustment {
|
||||
lower = attrs.lower or 0,
|
||||
upper = attrs.upper or 100,
|
||||
page_increment = attrs.increment or 1,
|
||||
page_size = attrs.increment or 1,
|
||||
step_increment = attrs.increment or 1
|
||||
},
|
||||
width_chars = attrs.width or 6,
|
||||
numeric = true,
|
||||
digits = attrs.digits or 2,
|
||||
climb_rate = attrs.increment or 1
|
||||
}
|
||||
return {
|
||||
widget = wrap_label(widget, attrs.label),
|
||||
set_data = function(x) widget:set_value(x) end
|
||||
}
|
||||
end,
|
||||
["int"] = function(attrs)
|
||||
local widget = Gtk.SpinButton {
|
||||
hexpand = true,
|
||||
adjustment = Gtk.Adjustment {
|
||||
lower = attrs.lower or 0,
|
||||
upper = attrs.upper or 100,
|
||||
page_increment = attrs.increment or 1,
|
||||
page_size = attrs.increment or 1,
|
||||
step_increment = attrs.increment or 1
|
||||
},
|
||||
width_chars = attrs.width or 6,
|
||||
numeric = true,
|
||||
digits = 0,
|
||||
climb_rate = attrs.increment or 1
|
||||
}
|
||||
return {
|
||||
widget = wrap_label(apply { vexpand = false, widget }, attrs.label),
|
||||
set_data = function(x) widget:set_value(x) end
|
||||
}
|
||||
end,
|
||||
-- The color type is the hardest to implement; as Gtk deprecated
|
||||
-- the old color chooser button, so we have to implement our own.
|
||||
-- The benefits of doing this mean we get to use the "nice" Gtk3
|
||||
-- chooser, and the button rendering itself is much better.
|
||||
["color"] = function(attrs)
|
||||
local c = Gdk.RGBA {
|
||||
red = 1.0, green = 1.0, blue = 1.0, alpha = 1.0
|
||||
}
|
||||
local area = Gtk.DrawingArea()
|
||||
area:set_size_request(16, 16)
|
||||
local draw = function(widget, cr)
|
||||
local context = widget:get_style_context()
|
||||
local width = widget:get_allocated_width()
|
||||
local height = widget:get_allocated_height()
|
||||
local aargc = { width / 2, height / 2, math.min(width, height) / 2, 0, 2 * math.pi }
|
||||
Gtk.render_background(context, cr, 0, 0, width, height)
|
||||
cr:set_source(repeat_pattern)
|
||||
cr:arc(unpack(aargc))
|
||||
cr:fill()
|
||||
cr:set_source_rgba(c.red, c.green, c.blue, c.alpha)
|
||||
cr:arc(unpack(aargc))
|
||||
cr:fill()
|
||||
end
|
||||
-- Gtk 3/4 compat
|
||||
if area.set_draw_func then
|
||||
area:set_draw_func(draw)
|
||||
else
|
||||
area.on_draw = draw
|
||||
end
|
||||
local btn = Gtk.Button {
|
||||
apply {
|
||||
margin_top = 1,
|
||||
margin_bottom = 1,
|
||||
area
|
||||
}
|
||||
}
|
||||
local entry = Gtk.Entry {
|
||||
hexpand = true,
|
||||
width_chars = 9,
|
||||
max_length = 9,
|
||||
text = attrs.alpha and "#FFFFFFFF" or "#FFFFFF"
|
||||
}
|
||||
entry:get_style_context():add_provider(cssp, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||
entry:get_style_context():add_class("fixed-width-font-entry")
|
||||
local widget = Gtk.Box {
|
||||
orientation = "HORIZONTAL",
|
||||
spacing = 0,
|
||||
entry, btn
|
||||
}
|
||||
widget:get_style_context():add_class("linked")
|
||||
widget = wrap_label(widget, attrs.label)
|
||||
function btn:on_clicked()
|
||||
local dialog = (use_old_chooser and Gtk.ColorSelectionDialog or Gtk.ColorChooserDialog) {
|
||||
title = "Select Color",
|
||||
transient_for = window,
|
||||
modal = true,
|
||||
destroy_with_parent = true
|
||||
}
|
||||
if use_old_chooser then
|
||||
dialog.cancel_button:set_visible(false)
|
||||
dialog.ok_button.label = "Close"
|
||||
dialog.color_selection.current_rgba = c
|
||||
if attrs.alpha then
|
||||
dialog.color_selection.has_opacity_control = true
|
||||
end
|
||||
function dialog.color_selection:on_color_changed()
|
||||
c = dialog.color_selection.current_rgba
|
||||
entry:set_text(attrs.alpha and utils.format_color_rgba(c) or utils.format_color_rgb(c))
|
||||
area:queue_draw()
|
||||
end
|
||||
else
|
||||
dialog.rgba = c
|
||||
if attrs.alpha then
|
||||
dialog.use_alpha = true
|
||||
end
|
||||
end
|
||||
|
||||
local ret = dialog:run()
|
||||
dialog:set_visible(false)
|
||||
|
||||
if not use_old_chooser and ret == Gtk.ResponseType.OK then
|
||||
c = dialog.rgba
|
||||
entry:set_text(attrs.alpha and utils.format_color_rgba(c) or utils.format_color_rgb(c))
|
||||
area:queue_draw()
|
||||
end
|
||||
end
|
||||
function entry:on_changed()
|
||||
local s = utils.sanitize_color(entry.text)
|
||||
c = utils.parse_color_rgba(s)
|
||||
area:queue_draw()
|
||||
end
|
||||
return {
|
||||
widget = widget,
|
||||
set_data = function(x)
|
||||
local s = utils.sanitize_color(x)
|
||||
c = utils.parse_color_rgba(s)
|
||||
area:queue_draw()
|
||||
entry:set_text(s)
|
||||
end
|
||||
}
|
||||
end
|
||||
}
|
||||
|
||||
local ServiceView = function(self)
|
||||
local switch = Gtk.Switch { id = "autostart_enabled", sensitive = false }
|
||||
local switch = Gtk.Switch {
|
||||
id = "autostart_enabled",
|
||||
sensitive = false,
|
||||
hexpand = false
|
||||
}
|
||||
local method = ComboBoxFixed {
|
||||
{ "None" },
|
||||
{ "SystemD User Service" },
|
||||
{ "InitD Entry" },
|
||||
{ "Desktop Entry" }
|
||||
"None",
|
||||
"SystemD User Service",
|
||||
"InitD Entry",
|
||||
"Desktop Entry"
|
||||
}
|
||||
method.on_changed = function(box)
|
||||
local opt = box:get_active_text()
|
||||
@@ -107,36 +431,62 @@ return function()
|
||||
end
|
||||
-- TODO handle enable here
|
||||
end
|
||||
return ConfigView { { "Enabled", switch }, { "Autostart Method", method } }
|
||||
return ConfigView {
|
||||
{ "Enabled", Gtk.Box { Gtk.Box { hexpand = true }, switch } },
|
||||
{ "Autostart Method", method }
|
||||
}
|
||||
end
|
||||
|
||||
local ProfileView = function(name)
|
||||
local self = { name = name }
|
||||
local notebook = Gtk.Notebook {
|
||||
expand = true,
|
||||
{ tab_label = "Global Options",
|
||||
Gtk.ScrolledWindow {
|
||||
shadow_type = "IN",
|
||||
Gtk.Box {}
|
||||
local args = {}
|
||||
for k, v in pairs(mappings) do
|
||||
local layout = {}
|
||||
for _, e in ipairs(v) do
|
||||
if type(e) == "table" then
|
||||
local fields = {}
|
||||
local ftypes = type(e.field_type) == "table" and e.field_type or { e.field_type }
|
||||
local fattrs = type(e.field_type) == "table" and e.field_attrs or { e.field_attrs }
|
||||
if not fattrs then fattrs = {} end
|
||||
for i, f in ipairs(ftypes) do
|
||||
local entry = widget_generators[f](fattrs[i] or {})
|
||||
fields[#fields + 1] = entry.widget
|
||||
end
|
||||
fields.orientation = "VERTICAL"
|
||||
fields.spacing = 2
|
||||
local fwidget = {
|
||||
e.description,
|
||||
#fields > 1 and
|
||||
Gtk.Frame {
|
||||
label = fattrs.frame_label,
|
||||
apply {
|
||||
margin_left = 4,
|
||||
margin_right = 4,
|
||||
margin_top = 4,
|
||||
margin_bottom = 4,
|
||||
Gtk.Box(fields)
|
||||
}
|
||||
} or Gtk.Box(fields)
|
||||
}
|
||||
if not e.advanced then
|
||||
layout[#layout + 1] = fwidget
|
||||
else
|
||||
if not layout.advanced then layout.advanced = {} end
|
||||
layout.advanced[#layout.advanced + 1] = fwidget
|
||||
end
|
||||
end
|
||||
end
|
||||
args[#args + 1] = { tab_label = v.name, ConfigView(layout) }
|
||||
end
|
||||
args[#args + 1] = {
|
||||
tab_label = "Autostart",
|
||||
name ~= "Default" and ServiceView(self) or
|
||||
Gtk.Label {
|
||||
label = "Autostart options are not available for the default user profile."
|
||||
}
|
||||
},
|
||||
{ tab_label = "Smoothing Options",
|
||||
Gtk.ScrolledWindow {
|
||||
shadow_type = "IN",
|
||||
Gtk.Box {}
|
||||
}
|
||||
},
|
||||
{ tab_label = "Module Options",
|
||||
Gtk.ScrolledWindow {
|
||||
shadow_type = "IN",
|
||||
Gtk.Box {}
|
||||
}
|
||||
},
|
||||
{ tab_label = "Autostart",
|
||||
name ~= "Default" and ServiceView(self) or Gtk.Label {
|
||||
label = "Autostart options are not available for the default user profile." }
|
||||
}
|
||||
}
|
||||
args.expand = true
|
||||
notebook = Gtk.Notebook(args)
|
||||
notebook:show_all()
|
||||
self.widget = notebook
|
||||
function self:rename(new)
|
||||
@@ -152,7 +502,7 @@ return function()
|
||||
view_registry[default_entry[ItemColumn.PROFILE]] = ProfileView(default_entry[ItemColumn.PROFILE])
|
||||
item_store:append(default_entry)
|
||||
|
||||
local window = Gtk.Window {
|
||||
window = Gtk.Window {
|
||||
title = "GLava Config",
|
||||
default_width = 320,
|
||||
default_height = 200,
|
||||
@@ -206,27 +556,31 @@ return function()
|
||||
Gtk.Box {
|
||||
orientation = "HORIZONTAL",
|
||||
spacing = 4,
|
||||
Gtk.Alignment {
|
||||
xscale = 0,
|
||||
Gtk.Box {
|
||||
homogeneous = true,
|
||||
Gtk.Button {
|
||||
id = "reload",
|
||||
label = "Reload",
|
||||
image = Gtk.Image { stock = Gtk.STOCK_REFRESH }
|
||||
},
|
||||
Gtk.Button {
|
||||
id = "add",
|
||||
label = "New",
|
||||
image = Gtk.Image { stock = Gtk.STOCK_NEW },
|
||||
},
|
||||
Gtk.Button {
|
||||
id = "remove",
|
||||
label = "Delete",
|
||||
sensitive = false,
|
||||
image = Gtk.Image { stock = Gtk.STOCK_DELETE },
|
||||
}
|
||||
}
|
||||
apply {
|
||||
hexpand = false,
|
||||
homogeneous = true,
|
||||
(function()
|
||||
local box = Gtk.Box {
|
||||
Gtk.Button {
|
||||
id = "reload",
|
||||
label = "Reload",
|
||||
image = Gtk.Image { stock = Gtk.STOCK_REFRESH }
|
||||
},
|
||||
Gtk.Button {
|
||||
id = "add",
|
||||
label = "New",
|
||||
image = Gtk.Image { stock = Gtk.STOCK_NEW },
|
||||
},
|
||||
Gtk.Button {
|
||||
id = "remove",
|
||||
label = "Delete",
|
||||
sensitive = false,
|
||||
image = Gtk.Image { stock = Gtk.STOCK_DELETE },
|
||||
}
|
||||
}
|
||||
box:get_style_context():add_class("linked")
|
||||
return box
|
||||
end)()
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -260,7 +614,7 @@ return function()
|
||||
end
|
||||
return profile_name
|
||||
end
|
||||
|
||||
|
||||
function window.child.view:on_row_activated(path, column)
|
||||
local name = item_store[path][ItemColumn.PROFILE]
|
||||
window.child.stack_view:set_visible_child_name(name)
|
||||
@@ -293,7 +647,7 @@ return function()
|
||||
item_store[path][ItemColumn.ENABLED] =
|
||||
view_registry[item_store[path][ItemColumn.PROFILE]].widget.child.autostart_enabled.active
|
||||
end
|
||||
|
||||
|
||||
function window.child.add:on_clicked()
|
||||
local profile_name = unique_profile("New Profile")
|
||||
local entry = {
|
||||
@@ -307,7 +661,7 @@ return function()
|
||||
view_registry[profile_name] = view
|
||||
window.child.stack_view:add_named(view.widget, profile_name);
|
||||
end
|
||||
|
||||
|
||||
function window.child.remove:on_clicked()
|
||||
local dialog = Gtk.Dialog {
|
||||
title = "Confirmation",
|
||||
|
||||
@@ -74,7 +74,7 @@ void xwin_assign_icon_bmp(struct gl_wcb* wcb, void* impl, const char* path) {
|
||||
|
||||
/* Obtain image data pointer from offset */
|
||||
const char* data = (const char*) (((const uint8_t*) header) + header->offset);
|
||||
|
||||
|
||||
/* Assign icon using the older WMHints. Most window managers don't actually use this. */
|
||||
XWMHints hints = {};
|
||||
hints.flags = IconPixmapHint;
|
||||
|
||||
BIN
resources/transparent.png
Normal file
BIN
resources/transparent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 690 B |
@@ -51,7 +51,7 @@
|
||||
/* Window geometry (x, y, width, height) */
|
||||
#request setgeometry 0 0 800 600
|
||||
|
||||
/* Window background color (RGB format).
|
||||
/* Window background color (RGBA format).
|
||||
Does not work with `setopacity "xroot"` */
|
||||
#request setbg 00000000
|
||||
|
||||
|
||||
Reference in New Issue
Block a user