glava-config widget generation (incomplete)

This commit is contained in:
Jarcode
2019-09-07 02:32:21 -07:00
parent 4bfbc859f8
commit 90d275f2cd
8 changed files with 901 additions and 84 deletions

View File

@@ -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",