From 90d275f2cd6fe82fa53d0ef62a034e8799e9d5f9 Mon Sep 17 00:00:00 2001 From: Jarcode Date: Sat, 7 Sep 2019 02:32:21 -0700 Subject: [PATCH] glava-config widget generation (incomplete) --- glava-config/config.lua | 307 +++++++++++++++++++++++ glava-config/main.lua | 52 +++- glava-config/mappings.lua | 40 +++ glava-config/utils.lua | 68 +++++ glava-config/window.lua | 514 ++++++++++++++++++++++++++++++++------ glava/xwin.c | 2 +- resources/transparent.png | Bin 0 -> 690 bytes shaders/glava/rc.glsl | 2 +- 8 files changed, 901 insertions(+), 84 deletions(-) create mode 100644 glava-config/config.lua create mode 100644 glava-config/mappings.lua create mode 100644 glava-config/utils.lua create mode 100644 resources/transparent.png diff --git a/glava-config/config.lua b/glava-config/config.lua new file mode 100644 index 0000000..dcaa396 --- /dev/null +++ b/glava-config/config.lua @@ -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 diff --git a/glava-config/main.lua b/glava-config/main.lua index 822a14b..266a399 100644 --- a/glava-config/main.lua +++ b/glava-config/main.lua @@ -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 diff --git a/glava-config/mappings.lua b/glava-config/mappings.lua new file mode 100644 index 0000000..0adf2ba --- /dev/null +++ b/glava-config/mappings.lua @@ -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" + } + } +} diff --git a/glava-config/utils.lua b/glava-config/utils.lua new file mode 100644 index 0000000..8b24d3c --- /dev/null +++ b/glava-config/utils.lua @@ -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 diff --git a/glava-config/window.lua b/glava-config/window.lua index d0ca1ee..e14a88d 100644 --- a/glava-config/window.lua +++ b/glava-config/window.lua @@ -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", diff --git a/glava/xwin.c b/glava/xwin.c index c56fbdb..faeba16 100644 --- a/glava/xwin.c +++ b/glava/xwin.c @@ -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; diff --git a/resources/transparent.png b/resources/transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..29c4977bdf058f7defd649e0ad2b243febae1a52 GIT binary patch literal 690 zcmeAS@N?(olHy`uVBq!ia0y~yVBi2@4kiW$2Jc+WI}8krt(nfw0iMpz3I#>^X_+~x z3=A3*YbV-z9Cna78Xvv2E=1;*;}J#Y*C~!6M*>#|v~Vp9%eZ>*$tC`Up3ajVJ<`}A zCi5p^jnxi|*RLLEaWu&*=&keX(hGX@f09$T1b^S}kMHh1ekc22{q&hxWj&0m(j;G| zIGuPF*788R>O!mC5rJtlR-W%l;+psPwVd6)dq*p$ecx&CzJHRrt<6GYWa+`Dn$W ztSRr$?^;uE@6noRtJhTe|2q^A7ajM=>TIIQ#s=380fog~yi*bt{Z_kmb(*|YxB27T z7q2qiXC1FW(V4|eqLUu8|8Ks3cUEg>p}FQ12CMj--^y$(<&AD$yZvX%rY`e%C4v4K39~NSLgqqm-+2Gvq?))UtmE}ECT}rXMsm#F$061G6*wP zEVVCVU|_iH>Eal|A