glava-config widget generation (incomplete)
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user