diff --git a/ROADMAP.md b/ROADMAP.md index 481128f..e94671f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -15,21 +15,29 @@ Update v1.0.3. Unreleased: -- ... +- [Improvement] Major performance improvement when applying theme preferences +- [Improvement] Move the Font configuration to it's own menu option under View > Font Preferences... +- [Improvement] Move the "Reset to Default" option to the main Theme Preferences dialog window +- [Improvement] Change user screen & UI scaling settings to correctly display vector fonts +- [Improvement] Add "Save As" button for the Theme configuration +- [Feature] Add "Outline" color +- [Feature] Add "Title Bar" Window color -Update v2.0.0: +Update v1.0.7: -- [Test-Feature] Tint Mode where you can only edit the Tab color + optional underglow -- [Improvement] A new color (set) - Window Title Bar -- [Improvement] Read font names from TTF files +- [Improvement] Rework the dialogs and menu options +- [Improvement] Update template with new elements for 1.3 and check for compatibility with 1.2 -Future: +Update v1.0.8: +- [Fix] Make the title bar change it's height with font size + +Update v2.0.0: + +- [Improvement] Read font names from TTF files - [Feature] Add a Dark theme template - [Feature] [Blocked] Separate "field_background" when Aseprite fixes the menu shadow - [Feature] [Blocked] Add separate color for Tooltip Text (Tooltip Section?) - currently it doesn't work due to a bug -- [Feature] Generating a theme from the current color palette -- [Refactor] Add a Default model next to Template - Template is a template, default is default ## FX diff --git a/Theme Preferences/Base64Encoder.lua b/Theme Preferences/Base64Encoder.lua index 292c9d3..7b48d0e 100644 --- a/Theme Preferences/Base64Encoder.lua +++ b/Theme Preferences/Base64Encoder.lua @@ -1,4 +1,6 @@ -local CODE_VERSION = 1 +local Template = dofile("./Template.lua")() + +local CODE_VERSION = 2 local START_CHARACTER = "<" local LAST_CHARACTER = ">" local SPLIT_CHARACTER = ":" @@ -41,7 +43,14 @@ local Base64ThemeEncoder = { "editor_cursor", -- "editor_cursor_shadow", -- "editor_cursor_outline", -- - "editor_icons" -- + "editor_icons", -- + -- Outline + "outline", -- + -- Window Title Bar + "window_title_bar_corner_highlight", -- + "window_title_bar_highlight", -- + "window_title_bar_background", -- + "window_title_bar_shadow" } } @@ -149,7 +158,9 @@ function Base64ThemeEncoder:EncodeColors(colors) local result = "" for _, id in ipairs(self.colorIds) do - result = result .. self:EncodeColor(colors[id]) + result = result .. + self:EncodeColor( + colors[id] or self:GetDefaultColor(id, colors)) end return result @@ -228,7 +239,9 @@ function Base64ThemeEncoder:DecodeColors(version, encodedColors) end local colors = {} - for i, id in ipairs(self.colorIds) do colors[id] = decodedColors[i] end + for i, id in ipairs(self.colorIds) do + colors[id] = decodedColors[i] or self:GetDefaultColor(id, colors) + end return colors end @@ -251,4 +264,20 @@ function Base64ThemeEncoder:DecodeName(code) return name end +function Base64ThemeEncoder:GetDefaultColor(id, colors) + if id == "outline" then return Template.colors[id] end + + if id == "window_title_bar_corner_highlight" then + return colors["tab_corner_highlight"] + end + + if id == "window_title_bar_highlight" then return colors["tab_highlight"] end + + if id == "window_title_bar_background" then + return colors["tab_background"] + end + + if id == "window_title_bar_shadow" then return colors["tab_shadow"] end +end + return Base64ThemeEncoder diff --git a/Theme Preferences/DefaultFont.lua b/Theme Preferences/DefaultFont.lua new file mode 100644 index 0000000..b4528a0 --- /dev/null +++ b/Theme Preferences/DefaultFont.lua @@ -0,0 +1,6 @@ +return function() + return { + default = {name = "Aseprite", size = "9"}, + mini = {name = "Aseprite Mini", size = "7"} + } +end diff --git a/Theme Preferences/DialogBounds.lua b/Theme Preferences/DialogBounds.lua new file mode 100644 index 0000000..95d3724 --- /dev/null +++ b/Theme Preferences/DialogBounds.lua @@ -0,0 +1,27 @@ +function GetWindowSize() + if app.apiVersion >= 25 then return app.window end + + local dialog = Dialog() + dialog:show{wait = false} + dialog:close() + + return Size(dialog.bounds.x * 2 + dialog.bounds.width, + dialog.bounds.y * 2 + dialog.bounds.height) +end + +return function(size, position) + local window = GetWindowSize() + + local uiScale = app.preferences.general["ui_scale"] + size = Size(size.width * uiScale, size.height * uiScale) + + local x = (window.width - size.width) / 2 + local y = (window.height - size.height) / 2 + + if position then + x = position.x + y = position.y + end + + return Rectangle(x, y, size.width, size.height) +end diff --git a/Theme Preferences/ExportConfigurationDialog.lua b/Theme Preferences/ExportConfigurationDialog.lua new file mode 100644 index 0000000..6b8bb4b --- /dev/null +++ b/Theme Preferences/ExportConfigurationDialog.lua @@ -0,0 +1,10 @@ +return function(name, code, onClose) + local dialog = Dialog {title = "Export " .. name, onclose = onClose} + + dialog -- + :entry{label = "Code", text = code} -- + :separator() -- + :button{text = "Close"} -- + + return dialog +end diff --git a/Theme Preferences/FileProvider.lua b/Theme Preferences/FileProvider.lua new file mode 100644 index 0000000..6b37b72 --- /dev/null +++ b/Theme Preferences/FileProvider.lua @@ -0,0 +1,20 @@ +local FileProvider = {} + +function FileProvider:ReadAll(filePath) + local file = assert(io.open(filePath, "rb")) + local content = file:read("*all") + file:close() + + return content +end + +function FileProvider:Write(filePath, content) + local file = io.open(filePath, "w") + + if file then + file:write(content) + file:close() + end +end + +return FileProvider diff --git a/Theme Preferences/FontPreferences.lua b/Theme Preferences/FontPreferences.lua new file mode 100644 index 0000000..58b6589 --- /dev/null +++ b/Theme Preferences/FontPreferences.lua @@ -0,0 +1,222 @@ +local FontPreferencesDialog = dofile("./FontPreferencesDialog.lua") +local DefaultFont = dofile("./DefaultFont.lua") +local FileProvider = dofile("./FileProvider.lua") + +local FontPreferences = {preferences = nil, availableFonts = {}} + +function FontPreferences:Init(preferences) + self.preferences = preferences + self.preferences.font = self.preferences.font or DefaultFont() + + self:_RefreshAvailableFonts() +end + +function FontPreferences:GetCurrentFont() return self.preferences.font end + +function FontPreferences:SetCurrentFont(font) + self.preferences.font = font or DefaultFont() +end + +-- FUTURE: Revisit this, currently can cause issues and completely break the window layout rendering Aseprite unusable +function FontPreferences:VerifyScaling(font) + if font.default.type == "spritesheet" and font.mini.type == "spritesheet" then + return + end + + local screenScale = app.preferences.general["screen_scale"] + local uiScale = app.preferences.general["ui_scale"] + + if screenScale < uiScale then return end + + app.preferences.general["screen_scale"] = uiScale + app.preferences.general["ui_scale"] = screenScale + + app.alert { + title = "Warning", + text = "If the fonts appear blurry, please restart Aseprite for all changes to be applied." + } +end + +function FontPreferences:_FindAll(content, patternStart, patternEnd) + local results = {} + local start = 0 + + while start ~= -1 do + local matchStart = string.find(content, patternStart, start) + + if not matchStart then break end + local matchEnd = string.find(content, patternEnd, + matchStart + #patternStart) + + local name = string.sub(content, matchStart + #patternStart, + matchEnd - #patternEnd) + + table.insert(results, name) + + start = matchEnd + #patternEnd + end + + return results +end + +function FontPreferences:_ParseFont(fontDescription) + local result = {} + + local name = "" + local value = "" + local hasName = false + + for i = 1, #fontDescription do + local char = string.sub(fontDescription, i, i) + + if char == " " and not hasName then + -- If name and value are already found, save and reset + if #name > 0 and #value > 0 then + result[name] = value + + name = "" + value = "" + hasName = false + end + elseif char == "=" or char == "/" then + -- Ignore + elseif char == "\"" then + if not hasName then + hasName = true + else + result[name] = value + + name = "" + value = "" + hasName = false + end + elseif hasName then + value = value .. char + else + name = name .. char + end + end + + return result +end + +function FontPreferences:_ExtractFonts(filePath) + local fileContent = FileProvider:ReadAll(filePath) + fileContent = fileContent:gsub("[\n\r\t]+", " ") + + local fontDeclarations = self:_FindAll(fileContent, "") + + local result = {} + + for _, fontDeclaration in ipairs(fontDeclarations) do + local font = self:_ParseFont(fontDeclaration) + table.insert(result, font) + end + + return result +end + +function FontPreferences:GetFontsFromDirectory(path, fonts) + -- Validate the path + if not app.fs.isDirectory(path) then return end + + local files = app.fs.listFiles(path) + fonts = fonts or {} + + for _, file in ipairs(files) do + local filePath = app.fs.joinPath(path, file) + + if app.fs.isDirectory(filePath) then + self:GetFontsFromDirectory(filePath, fonts) + elseif file == "fonts.xml" or file == "theme.xml" then + local extractedFonts = self:_ExtractFonts(filePath) + + for _, font in ipairs(extractedFonts) do + -- If the font has an ID it's a reference, not a declaration + if not font.id then fonts[font.name] = font end + end + elseif app.fs.fileExtension(filePath) == "ttf" then + local name = app.fs.fileTitle(filePath) + + fonts[name] = { + name = name, + type = "truetype", + file = app.fs.fileName(filePath) + } + end + end + + return fonts +end + +function FontPreferences:_RefreshAvailableFonts() + self.availableFonts = {} + + local systemFonts = self:_GetSystemFonts() + for name, font in pairs(systemFonts) do self.availableFonts[name] = font end + + -- Aseprite Fonts + local asepriteDataDirectory = app.fs.filePath(app.fs.appPath) + local asepriteFonts = self:GetFontsFromDirectory(asepriteDataDirectory) + for name, font in pairs(asepriteFonts) do + self.availableFonts[name] = font + end + + -- Declared Fonts + local extensionsDirectory = app.fs.joinPath(app.fs.userConfigPath, + "extensions") + local declaredFonts = self:GetFontsFromDirectory(extensionsDirectory) + for name, font in pairs(declaredFonts) do + self.availableFonts[name] = font + end +end + +function FontPreferences:_GetSystemFonts() + -- Windows + local roamingPath = os.getenv("APPDATA") + local appDataPath = roamingPath and app.fs.filePath(roamingPath) + local windowsUserFontsPath = app.fs.joinPath(appDataPath, + "Local\\Microsoft\\Windows\\Fonts") or + "" + + -- Mac + local homePath = os.getenv("HOME") + local macUserFontsPath = app.fs.joinPath(homePath, "Library/Fonts") or "" + + local fontsDirectories = { + "C:/Windows/Fonts", windowsUserFontsPath, -- Windows + "/Library/Fonts/", "/System/Library/Fonts/", macUserFontsPath, -- Mac + "~/.fonts", "/usr/local/share/fonts", "/usr/share/fonts" -- Linux + } + + local systemFonts = {} + + for _, fontsDirectory in ipairs(fontsDirectories) do + if app.fs.isDirectory(fontsDirectory) then + local fonts = self:GetFontsFromDirectory(fontsDirectory) + + for fontName, font in pairs(fonts) do + systemFonts[fontName] = font + end + end + end + + return systemFonts +end + +function FontPreferences:OpenDialog(onClose, onSuccess) + local currentFont = FontPreferences:GetCurrentFont() + + local onConfirm = function(newFont) + FontPreferences:SetCurrentFont(newFont) + onSuccess(newFont) + self:VerifyScaling(newFont) + end + + local dialog = FontPreferencesDialog(currentFont, self.availableFonts, + onClose, onConfirm) + + dialog:show{wait = false} +end + +return FontPreferences diff --git a/Theme Preferences/FontPreferencesDialog.lua b/Theme Preferences/FontPreferencesDialog.lua new file mode 100644 index 0000000..d8ce20b --- /dev/null +++ b/Theme Preferences/FontPreferencesDialog.lua @@ -0,0 +1,114 @@ +local DefaultFont = dofile("./DefaultFont.lua") + +local FontSizes = {"6", "7", "8", "9", "10", "11", "12"} + +function HasSize(font) return font.type ~= "spritesheet" end + +function GetFontNames(fonts) + local fontNames = {} + + for name, _ in pairs(fonts) do table.insert(fontNames, name) end + table.sort(fontNames) + + return fontNames +end + +return function(font, availableFonts, onclose, onconfirm) + local fontNames = GetFontNames(availableFonts) + + local dialog = Dialog {title = "Font Preferences", onclose = onclose} + + local updateFonts = function() + local fontName = dialog.data["default-font"] + local defaultFont = availableFonts[fontName] + + local miniFontName = dialog.data["mini-font"] + local miniFont = availableFonts[miniFontName] + + local newFont = { + default = { + name = defaultFont.name, + type = defaultFont.type, + file = defaultFont.file, + size = dialog.data["default-font-size"] + }, + mini = { + name = miniFont.name, + type = miniFont.type, + file = miniFont.file, + size = dialog.data["mini-font-size"] + } + } + + onconfirm(newFont) + end + + dialog -- + :separator{text = "Default"} -- + :combobox{ + id = "default-font", + label = "Name", + option = font.default.name, + options = fontNames, + onchange = function() + local fontName = dialog.data["default-font"] + dialog:modify{ + id = "default-font-size", + enabled = HasSize(availableFonts[fontName]) + } + end + } -- + :combobox{ + id = "default-font-size", + options = FontSizes, + option = font.default.size, + enabled = HasSize(font.default) + } -- + :separator{text = "Mini"} -- + :combobox{ + id = "mini-font", + label = "Name", + option = font.mini.name, + options = fontNames, + onchange = function() + local miniFontName = dialog.data["mini-font"] + dialog:modify{ + id = "mini-font-size", + enabled = HasSize(availableFonts[miniFontName]) + } + end + } -- + :combobox{ + id = "mini-font-size", + options = FontSizes, + option = font.mini.size, + enabled = HasSize(font.mini) + } -- + :separator() -- + :button{ + text = "Reset to Default", + onclick = function() + local default = DefaultFont() + + dialog -- + :modify{id = "default-font-size", option = default.default.size} -- + :modify{id = "mini-font-size", option = default.mini.size} -- + :modify{id = "default-font", option = default.default.name} -- + :modify{id = "mini-font", option = default.mini.name} -- + + updateFonts() + end + } -- + :separator() -- + :button{ + text = "OK", + onclick = function() + dialog:close() + updateFonts() + end + } -- + :button{text = "Apply", onclick = updateFonts} -- + :button{text = "Cancel"} + + return dialog +end diff --git a/Theme Preferences/FontsProvider.lua b/Theme Preferences/FontsProvider.lua deleted file mode 100644 index 54a1b11..0000000 --- a/Theme Preferences/FontsProvider.lua +++ /dev/null @@ -1,379 +0,0 @@ -local DefaultFont = { - default = {name = "Aseprite", size = "9"}, - mini = {name = "Aseprite Mini", size = "7"} -} -local FontSizes = {"6", "7", "8", "9", "10", "11", "12"} - -local FontsProvider = {storage = nil, availableFonts = {}} - -function FontsProvider:Init(options) - self.storage = options.storage - self.storage.font = self.storage.font or DefaultFont - - self:_RefreshAvailableFonts() -end - -function FontsProvider:GetCurrentFont() return self.storage.font end - -function FontsProvider:SetDefaultFont(fontName) - if fontName == nil or #fontName == 0 then return end - - local newFont = self.availableFonts[fontName] - if newFont == nil then return end - - self.storage.font.default.name = newFont.name - self.storage.font.default.type = newFont.type - self.storage.font.default.file = newFont.file -end - -function FontsProvider:SetMiniFont(fontName) - - if fontName == nil or #fontName == 0 then return end - - local newFont = self.availableFonts[fontName] - if newFont == nil then return end - - self.storage.font.mini.name = newFont.name - self.storage.font.mini.type = newFont.type - self.storage.font.mini.file = newFont.file -end - -function FontsProvider:SetDefaultFontSize(fontSize) - self.storage.font.default.size = fontSize -end - -function FontsProvider:SetMiniFontSize(fontSize) - self.storage.font.mini.size = fontSize -end - -function FontsProvider:GetFontDeclaration(font) - if not font.type or not font.file then return "" end - - return string.format("", - font.name, font.type, font.file) -end - --- FUTURE: Revisit this, currently can cause issues and completely break the window layout rendering Aseprite unusable -function FontsProvider:VerifyScaling() - local currentFont = self:GetCurrentFont() - - local isDefaultFontVector = currentFont.default.type == nil or - currentFont.default.type ~= "spritesheet" - local isMiniFontVector = currentFont.mini.type == nil or - currentFont.mini.type == "spritesheet" - - if not isDefaultFontVector and not isMiniFontVector then return end - - local screenScale = app.preferences.general["screen_scale"] - local uiScale = app.preferences.general["ui_scale"] - - if screenScale < uiScale then return end - - local userChoice = app.alert { - title = "Warning", - text = { - "One of the selected fonts may appear blurry, switching UI and Screen Scaling may help.", - "", - "Current: Screen " .. tostring(screenScale * 100) .. "%, " .. "UI " .. - tostring(uiScale * 100) .. "%", - "Suggested: Screen " .. tostring(uiScale * 100) .. "%, " .. "UI " .. - tostring(screenScale * 100) .. "%", "", - "Would you like to switch?" - }, - buttons = {"Yes", "No"} - } - - if userChoice == 1 then -- Yes = 1 - app.preferences.general["screen_scale"] = uiScale - app.preferences.general["ui_scale"] = screenScale - - app.alert { - title = "Aseprite Restart Necessary", - text = "Please restart Aseprite for the changes to be applied." - } - end -end - -function FontsProvider:_ReadAll(filePath) - local file = assert(io.open(filePath, "rb")) - local content = file:read("*all") - file:close() - return content -end - -function FontsProvider:_FindAll(content, patternStart, patternEnd) - local results = {} - local start = 0 - - while start ~= -1 do - local matchStart = string.find(content, patternStart, start) - - if not matchStart then break end - local matchEnd = string.find(content, patternEnd, - matchStart + #patternStart) - - local name = string.sub(content, matchStart + #patternStart, - matchEnd - #patternEnd) - - table.insert(results, name) - - start = matchEnd + #patternEnd - end - - return results -end - -function FontsProvider:_ParseFont(fontDescription) - local result = {} - - local name = "" - local value = "" - local hasName = false - - for i = 1, #fontDescription do - local char = string.sub(fontDescription, i, i) - - if char == " " and not hasName then - -- If name and value are already found, save and reset - if #name > 0 and #value > 0 then - result[name] = value - - name = "" - value = "" - hasName = false - end - elseif char == "=" or char == "/" then - -- Ignore - elseif char == "\"" then - if not hasName then - hasName = true - else - result[name] = value - - name = "" - value = "" - hasName = false - end - elseif hasName then - value = value .. char - else - name = name .. char - end - end - - return result -end - -function FontsProvider:_ExtractFonts(filePath) - local fileContent = self:_ReadAll(filePath) - fileContent = fileContent:gsub("[\n\r\t]+", " ") - - local fontDeclarations = self:_FindAll(fileContent, "") - - local result = {} - - for _, fontDeclaration in ipairs(fontDeclarations) do - local font = self:_ParseFont(fontDeclaration) - table.insert(result, font) - end - - return result -end - -function FontsProvider:GetFontsFromDirectory(path, fonts) - -- Validate the path - if not app.fs.isDirectory(path) then return end - - local files = app.fs.listFiles(path) - fonts = fonts or {} - - for _, file in ipairs(files) do - local filePath = app.fs.joinPath(path, file) - - if app.fs.isDirectory(filePath) then - self:GetFontsFromDirectory(filePath, fonts) - elseif file == "fonts.xml" or file == "theme.xml" then - local extractedFonts = self:_ExtractFonts(filePath) - - for _, font in ipairs(extractedFonts) do - -- If the font has an ID it's a reference, not a declaration - if not font.id then fonts[font.name] = font end - end - elseif app.fs.fileExtension(filePath) == "ttf" then - local name = app.fs.fileTitle(filePath) - - fonts[name] = { - name = name, - type = "truetype", - file = app.fs.fileName(filePath) - } - end - end - - return fonts -end - -function FontsProvider:GetAvailableFontNames() - if not self.availableFonts then self:_RefreshAvailableFonts() end - - local fontNames = {} - - for name, _ in pairs(self.availableFonts) do - table.insert(fontNames, name) - end - - table.sort(fontNames) - - return fontNames -end - -function FontsProvider:_RefreshAvailableFonts() - self.availableFonts = {} - - local systemFonts = self:_GetSystemFonts() - for name, font in pairs(systemFonts) do self.availableFonts[name] = font end - - -- Aseprite Fonts - local asepriteDataDirectory = app.fs.filePath(app.fs.appPath) - local asepriteFonts = self:GetFontsFromDirectory(asepriteDataDirectory) - for name, font in pairs(asepriteFonts) do - self.availableFonts[name] = font - end - - -- Declared Fonts - local extensionsDirectory = app.fs.joinPath(app.fs.userConfigPath, - "extensions") - local declaredFonts = self:GetFontsFromDirectory(extensionsDirectory) - for name, font in pairs(declaredFonts) do - self.availableFonts[name] = font - end -end - -function FontsProvider:_GetSystemFonts() - -- Windows - local roamingPath = os.getenv("APPDATA") - local appDataPath = roamingPath and app.fs.filePath(roamingPath) - local windowsUserFontsPath = app.fs.joinPath(appDataPath, - "Local\\Microsoft\\Windows\\Fonts") or - "" - - -- Mac - local homePath = os.getenv("HOME") - local macUserFontsPath = app.fs.joinPath(homePath, "Library/Fonts") or "" - - local fontsDirectories = { - "C:/Windows/Fonts", windowsUserFontsPath, -- Windows - "/Library/Fonts/", "/System/Library/Fonts/", macUserFontsPath, -- Mac - "~/.fonts", "/usr/local/share/fonts", "/usr/share/fonts" -- Linux - } - - local systemFonts = {} - - for _, fontsDirectory in ipairs(fontsDirectories) do - if app.fs.isDirectory(fontsDirectory) then - local fonts = self:GetFontsFromDirectory(fontsDirectory) - - for fontName, font in pairs(fonts) do - systemFonts[fontName] = font - end - end - end - - return systemFonts -end - -function FontsProvider:_HasSize(font) return font.type ~= "spritesheet" end - -function FontsProvider:OpenDialog(onconfirm) - local dialog = Dialog("Font Configuration") - - local fontNames = self:GetAvailableFontNames() - local currentFont = self:GetCurrentFont() - - local updateFonts = function() - self:SetDefaultFontSize(dialog.data["default-font-size"]) - self:SetMiniFontSize(dialog.data["mini-font-size"]) - - self:SetDefaultFont(dialog.data["default-font"]) - self:SetMiniFont(dialog.data["mini-font"]) - - onconfirm() - - -- self:VerifyScaling() - end - - dialog -- - :separator{text = "Default"} -- - :combobox{ - id = "default-font", - label = "Name", - option = currentFont.default.name, - options = fontNames, - onchange = function() - local newFont = self.availableFonts[dialog.data["default-font"]] - dialog:modify{ - id = "default-font-size", - enabled = self:_HasSize(newFont) - } - end - } -- - :combobox{ - id = "default-font-size", - options = FontSizes, - option = currentFont.default.size or DefaultFont.default.size, - enabled = self:_HasSize(currentFont.default), - onchange = function() - self:SetDefaultFontSize(dialog.data["default-font-size"]) - end - } -- - :separator{text = "Mini"} -- - :combobox{ - id = "mini-font", - label = "Name", - option = currentFont.mini.name, - options = fontNames, - onchange = function() - local newFont = self.availableFonts[dialog.data["mini-font"]] - dialog:modify{ - id = "mini-font-size", - enabled = self:_HasSize(newFont) - } - end - } -- - :combobox{ - id = "mini-font-size", - options = FontSizes, - option = currentFont.mini.size or DefaultFont.mini.size, - enabled = self:_HasSize(currentFont.mini), - onchange = function() - self:SetMiniFontSize(dialog.data["mini-font-size"]) - end - } -- - :separator() -- - :button{ - text = "Reset to Default", - onclick = function() - dialog -- - :modify{id = "default-font-size", option = DefaultFont.default.size} -- - :modify{id = "mini-font-size", option = DefaultFont.mini.size} -- - :modify{id = "default-font", option = DefaultFont.default.name} -- - :modify{id = "mini-font", option = DefaultFont.mini.name} -- - - updateFonts() - end - } -- - :separator() -- - :button{ - text = "OK", - onclick = function() - updateFonts() - dialog:close() - end - } -- - :button{text = "Apply", onclick = updateFonts} -- - :button{text = "Cancel"} - - dialog:show() -end - -return FontsProvider diff --git a/Theme Preferences/ImportConfigurationDialog.lua b/Theme Preferences/ImportConfigurationDialog.lua new file mode 100644 index 0000000..9329b78 --- /dev/null +++ b/Theme Preferences/ImportConfigurationDialog.lua @@ -0,0 +1,35 @@ +local IMPORT_DIALOG_WIDTH = 540 + +return function(decode, onConfirm) + local dialog = Dialog("Import") + + dialog -- + :entry{id = "code", label = "Code"} -- + :separator{id = "separator"} -- + :button{ + text = "Import", + onclick = function() + local theme = decode(dialog.data.code) + + if not theme then + dialog:modify{id = "separator", text = "Incorrect code"} + return + end + + dialog:close() + onConfirm(theme) + end + } -- + :button{text = "Cancel"} -- + + -- Open and close to initialize bounds + dialog:show{wait = false} + dialog:close() + + local bounds = dialog.bounds + bounds.x = bounds.x - (IMPORT_DIALOG_WIDTH - bounds.width) / 2 + bounds.width = IMPORT_DIALOG_WIDTH + dialog.bounds = bounds + + return dialog +end diff --git a/Theme Preferences/LoadConfigurationDialog.lua b/Theme Preferences/LoadConfigurationDialog.lua new file mode 100644 index 0000000..b290426 --- /dev/null +++ b/Theme Preferences/LoadConfigurationDialog.lua @@ -0,0 +1,96 @@ +local ExportConfigurationDialog = dofile("./ExportConfigurationDialog.lua") +local DialogBounds = dofile("./DialogBounds.lua") + +local ExportDialogSize = Size(540, 67) + +local ConfigurationsPerPage = 10 +local CurrentPage = 1 + +return function(themes, onload, ondelete, onimport) + local pages = math.ceil(#themes / ConfigurationsPerPage) + + local dialog = Dialog("Load Configuration") + + -- TODO: Hide tabs in older version of Aseprite, also hide them when there's only one page + + for page = 1, pages do + dialog:tab{ + id = "tab-" .. page, + text = " " .. page .. " ", + onclick = function() CurrentPage = page end + } + + for i = 1, ConfigurationsPerPage do + local index = i + (page - 1) * ConfigurationsPerPage + if index > #themes then break end + + local theme = themes[index] + + dialog -- + :button{ + label = theme.name, -- TODO: Limit the max number of characters displayed here to not make different pages have buttons of different sizes + text = "Load", + onclick = function() + local confirmation = app.alert { + title = "Loading theme " .. theme.name, + text = "Unsaved changes will be lost, do you want to continue?", + buttons = {"Yes", "No"} + } + + if confirmation == 1 then + dialog:close() + onload(theme) + end + end + } -- + :button{ + text = "Export", + onclick = function() + dialog:close() + local onExportDialogClose = function() + dialog:show() + end + + local exportDialog = + ExportConfigurationDialog(theme.name, theme.code, + onExportDialogClose) + exportDialog:show{bounds = DialogBounds(ExportDialogSize)} + end + } -- + :button{ + text = "Delete", + onclick = function() + local confirmation = app.alert { + title = "Delete " .. theme.name, + text = "Are you sure?", + buttons = {"Yes", "No"} + } + + if confirmation == 1 then + dialog:close() + ondelete(index - 1) + end + end + } + end + end + + local selectedPage = math.min(pages, CurrentPage) + + dialog:endtabs{id = "tabs", selected = "tab-" .. selectedPage} + + dialog -- + :button{ + text = "Import", + onclick = function() + dialog:close() + onimport() + end + } -- + + dialog -- + :separator() -- + :button{text = "Close"} -- + + return dialog +end diff --git a/Theme Preferences/RefreshTheme.lua b/Theme Preferences/RefreshTheme.lua new file mode 100644 index 0000000..bee9138 --- /dev/null +++ b/Theme Preferences/RefreshTheme.lua @@ -0,0 +1,99 @@ +local Template = dofile("./Template.lua") +local FileProvider = dofile("./FileProvider.lua") + +local THEME_ID = "custom" + +local ExtensionsDirectory = app.fs.joinPath(app.fs.userConfigPath, "extensions") +local BaseDirectory = app.fs.joinPath(ExtensionsDirectory, "theme-preferences") + +local SheetTemplatePath = app.fs.joinPath(BaseDirectory, "sheet-template.png") +local SheetPath = app.fs.joinPath(BaseDirectory, "sheet.png") +local XmlTemplatePath = app.fs.joinPath(BaseDirectory, "theme-template.xml") +local XmlPath = app.fs.joinPath(BaseDirectory, "theme.xml") + +-- Preload the theme sheet template file +local TemplateSheetImage = nil + +function ColorToHex(color) + return string.format("#%02x%02x%02x", color.red, color.green, color.blue) +end + +function UpdateThemeSheet(template, theme) + -- Prepare color lookup + local map = {} + + for id, templateColor in pairs(template.colors) do + map[templateColor.rgbaPixel] = theme.colors[id] + end + + -- Prepare sheet.png + if TemplateSheetImage == nil then + TemplateSheetImage = Image {fromFile = SheetTemplatePath} + end + + local image = Image(TemplateSheetImage) + + -- Save references to function to improve performance + local getPixel, drawPixel = image.getPixel, image.drawPixel + local value, themeColor + + local pc = app.pixelColor + local rgba, r, g, b, rgbaA = pc.rgba, pc.rgbaR, pc.rgbaG, pc.rgbaB, pc.rgbaA + + for x = 0, image.width - 1 do + for y = 0, image.height - 1 do + value = getPixel(image, x, y) + themeColor = map[rgba(r(value), g(value), b(value))] + + if themeColor then + drawPixel(image, x, y, rgba(themeColor.red, themeColor.green, + themeColor.blue, rgbaA(value))) + end + end + end + + image:saveAs(SheetPath) +end + +function FormatFontDeclaration(font) + if not font.type or not font.file then return "" end + + return string.format("", + font.name, font.type, font.file) +end + +function UpdateThemeXml(template, theme, font) + -- Prepare theme.xml + local xmlContent = FileProvider:ReadAll(XmlTemplatePath) + + for id, _ in pairs(template.colors) do + xmlContent = xmlContent:gsub("<" .. id .. ">", + ColorToHex(theme.colors[id])) + end + + -- Setting fonts for these just in case it's a system font + xmlContent = xmlContent:gsub("", + FormatFontDeclaration(font.default)) + xmlContent = xmlContent:gsub("", font.default.name) + xmlContent = xmlContent:gsub("", font.default.size) + + xmlContent = xmlContent:gsub("", + FormatFontDeclaration(font.mini)) + xmlContent = xmlContent:gsub("", font.mini.name) + xmlContent = xmlContent:gsub("", font.mini.size) + + FileProvider:Write(XmlPath, xmlContent) +end + +return function(theme, font) + local template = Template() + + UpdateThemeSheet(template, theme) + UpdateThemeXml(template, theme, font) + + -- Switch Aseprite to the custom theme + app.preferences.theme.selected = THEME_ID + + -- Force refresh of the Aseprite UI to reload the theme + app.command.Refresh() +end diff --git a/Theme Preferences/SaveConfigurationDialog.lua b/Theme Preferences/SaveConfigurationDialog.lua new file mode 100644 index 0000000..c899dfa --- /dev/null +++ b/Theme Preferences/SaveConfigurationDialog.lua @@ -0,0 +1,42 @@ +return function(theme, isImport, onConfirmation) + local title = "Save Configuration" + local okButtonText = "&OK" + + if isImport then + title = "Import Configuration" + okButtonText = "Save" + end + + local dialog = Dialog(title) + + dialog -- + :entry{ + id = "name", + label = "Name", + text = theme.name, + onchange = function() + dialog:modify{id = "ok", enabled = #dialog.data.name > 0} -- + end + } -- + :separator() -- + :button{ + id = "ok", + text = okButtonText, + enabled = #theme.name > 0, + onclick = function() onConfirmation(dialog.data.name) end + } -- + + if isImport then + dialog:button{ + text = "Save and Apply", + enabled = #theme.name > 0, + onclick = function() + onConfirmation(dialog.data.name, true) + end + } + end + + dialog:button{text = "Cancel"} + + return dialog +end diff --git a/Theme Preferences/Template.lua b/Theme Preferences/Template.lua index ed23c95..35c5785 100644 --- a/Theme Preferences/Template.lua +++ b/Theme Preferences/Template.lua @@ -1,142 +1,62 @@ -return { - name = "Default", - colors = { - -- Button - ["button_highlight"] = Color {gray = 255, alpha = 255}, - ["button_background"] = Color {gray = 198, alpha = 255}, - ["button_shadow"] = Color {gray = 124, alpha = 255}, - ["button_selected"] = Color { - red = 120, - green = 96, - blue = 80, - alpha = 255 - }, +return function() + return { + name = "Default", + colors = { + -- Button + ["button_highlight"] = Color {gray = 255}, + ["button_background"] = Color {gray = 198}, + ["button_shadow"] = Color {gray = 124}, + ["button_selected"] = Color {r = 120, g = 96, b = 80}, - -- Tab - ["tab_corner_highlight"] = Color { - red = 255, - green = 255, - blue = 254, - alpha = 255 - }, - ["tab_highlight"] = Color { - red = 173, - green = 202, - blue = 222, - alpha = 255 - }, - ["tab_background"] = Color { - red = 125, - green = 146, - blue = 158, - alpha = 255 - }, - ["tab_shadow"] = Color {red = 100, green = 84, blue = 96, alpha = 255}, + -- Tab + ["tab_corner_highlight"] = Color {r = 255, g = 255, b = 254}, + ["tab_highlight"] = Color {r = 173, g = 202, b = 222}, + ["tab_background"] = Color {r = 125, g = 146, b = 158}, + ["tab_shadow"] = Color {r = 100, g = 84, b = 96}, - -- Window - ["window_hover"] = Color { - red = 255, - green = 235, - blue = 182, - alpha = 255 - }, - ["window_highlight"] = Color { - red = 255, - green = 254, - blue = 255, - alpha = 255 - }, - ["window_background"] = Color { - red = 210, - green = 202, - blue = 189, - alpha = 255 - }, - ["window_shadow"] = Color { - red = 149, - green = 129, - blue = 116, - alpha = 255 - }, - ["window_corner_shadow"] = Color { - red = 100, - green = 85, - blue = 96, - alpha = 255 - }, + -- Window + ["window_hover"] = Color {r = 255, g = 235, b = 182}, + ["window_highlight"] = Color {r = 255, g = 254, b = 255}, + ["window_background"] = Color {r = 210, g = 202, b = 189}, + ["window_shadow"] = Color {r = 149, g = 129, b = 116}, + ["window_corner_shadow"] = Color {r = 100, g = 85, b = 96}, - -- Text - ["text_regular"] = Color {gray = 2, alpha = 255}, - ["text_active"] = Color {gray = 253, alpha = 255}, - ["text_link"] = Color {red = 44, green = 76, blue = 145, alpha = 255}, - ["text_separator"] = Color { - red = 44, - green = 76, - blue = 145, - alpha = 255 - }, + -- Text + ["text_regular"] = Color {gray = 2}, + ["text_active"] = Color {gray = 253}, + ["text_link"] = Color {r = 44, g = 76, b = 145}, + ["text_separator"] = Color {r = 44, g = 76, b = 145}, - -- Field - ["field_highlight"] = Color { - red = 255, - green = 87, - blue = 87, - alpha = 255 - }, - ["field_background"] = Color {gray = 254, alpha = 255}, - ["field_shadow"] = Color {gray = 197, alpha = 255}, - ["field_corner_shadow"] = Color {gray = 123, alpha = 255}, + -- Field + ["field_highlight"] = Color {r = 255, g = 87, b = 87}, + ["field_background"] = Color {gray = 254}, + ["field_shadow"] = Color {gray = 197}, + ["field_corner_shadow"] = Color {gray = 123}, - -- Editor - ["editor_background"] = Color { - red = 101, - green = 85, - blue = 97, - alpha = 255 - }, - ["editor_background_shadow"] = Color { - red = 65, - green = 65, - blue = 44, - alpha = 255 - }, - ["editor_tooltip"] = Color { - red = 255, - green = 255, - blue = 125, - alpha = 255 - }, - ["editor_tooltip_shadow"] = Color { - red = 125, - green = 146, - blue = 157, - alpha = 255 - }, - ["editor_tooltip_corner_shadow"] = Color { - red = 100, - green = 84, - blue = 95, - alpha = 255 - }, - ["editor_cursor"] = Color { - red = 254, - green = 255, - blue = 255, - alpha = 255 - }, - ["editor_cursor_shadow"] = Color { - red = 123, - green = 124, - blue = 124, - alpha = 255 - }, - ["editor_cursor_outline"] = Color { - red = 1, - green = 0, - blue = 0, - alpha = 255 - }, - ["editor_icons"] = Color {gray = 1, alpha = 255} - }, - parameters = {isAdvanced = false} -} + -- Editor + ["editor_background"] = Color {r = 101, g = 85, b = 97}, + ["editor_background_shadow"] = Color {r = 65, g = 65, b = 44}, + ["editor_tooltip"] = Color {r = 255, g = 255, b = 125}, + ["editor_tooltip_shadow"] = Color {r = 125, g = 146, b = 157}, + ["editor_tooltip_corner_shadow"] = Color {r = 100, g = 84, b = 95}, + ["editor_cursor"] = Color {r = 254, g = 255, b = 255}, + ["editor_cursor_shadow"] = Color {r = 123, g = 124, b = 124}, + ["editor_cursor_outline"] = Color {r = 1, g = 0, b = 0}, + ["editor_icons"] = Color {gray = 1}, + + -- Outline + ["outline"] = Color {gray = 0}, + + -- Window Title Bar + ["window_title_bar_corner_highlight"] = Color { + r = 255, + g = 255, + b = 253 + }, + ["window_title_bar_highlight"] = Color {r = 173, g = 202, b = 221}, + ["window_title_bar_background"] = Color {r = 125, g = 146, b = 156}, + ["window_title_bar_shadow"] = Color {r = 100, g = 85, b = 95} + }, + parameters = {isAdvanced = false} + } +end diff --git a/Theme Preferences/ThemeManager.lua b/Theme Preferences/ThemeManager.lua deleted file mode 100644 index d907653..0000000 --- a/Theme Preferences/ThemeManager.lua +++ /dev/null @@ -1,350 +0,0 @@ -local EXPORT_DIALOG_WIDTH = 540 - -local ThemeEncoder = dofile("./Base64Encoder.lua") - -local ThemeManager = {storage = nil} - -function ThemeManager:Init(options) - self.storage = options.storage - - self.storage.savedThemes = self.storage.savedThemes or { - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "" - } - - self.storage.savedThemes = self.storage.savedThemes or - "" -end - -function ThemeManager:SetCurrentTheme(theme) - local code = ThemeEncoder:EncodeSigned(theme.name, theme.parameters, - theme.colors) - - if code then self.storage.currentTheme = code end -end - -function ThemeManager:GetCurrentTheme() - if self.storage.currentTheme then - return ThemeEncoder:DecodeSigned(self.storage.currentTheme) - end -end - -function ThemeManager:Find(name) - for i, savedthemeCode in ipairs(self.storage.savedThemes) do - if ThemeEncoder:DecodeName(savedthemeCode) == name then return i end - end -end - -function ThemeManager:Save(theme, onsave, isImport) - local title = "Save Configuration" - local okButtonText = "OK" - - if isImport then - title = "Import Configuration" - okButtonText = "Save" - end - - local saveDialog = Dialog(title) - - local save = function(options) - local applyImmediately = options and options.apply - local isNameUsed = self:Find(saveDialog.data.name) - - if isNameUsed then - local overwriteConfirmation = app.alert { - title = "Configuration overwrite", - text = "Configuration with a name " .. saveDialog.data.name .. - " already exists, do you want to overwrite it?", - buttons = {"Yes", "No"} - } - - if overwriteConfirmation ~= 1 then return end - end - - theme.name = saveDialog.data.name - - if not isImport or (isImport and applyImmediately) then - onsave(theme) - end - - local code = ThemeEncoder:EncodeSigned(theme.name, theme.parameters, - theme.colors) - - if isNameUsed then - self.storage.savedThemes[isNameUsed] = code - else - table.insert(self.storage.savedThemes, code) - end - - saveDialog:close() - end - - saveDialog -- - :entry{ - id = "name", - label = "Name", - text = theme.name, - onchange = function() - saveDialog:modify{id = "ok", enabled = #saveDialog.data.name > 0} -- - end - } -- - :separator() -- - :button{ - id = "ok", - text = okButtonText, - enabled = #theme.name > 0, - onclick = function() save() end - } -- - - if isImport then - saveDialog:button{ - text = "Save and Apply", - enabled = #theme.name > 0, - onclick = function() save {apply = true} end - } - end - - saveDialog -- - :button{text = "Cancel"} -- - :show() - -end - -function ThemeManager:ShowExportDialog(name, code, onclose) - local isFirstOpen = true - - local exportDialog = Dialog { - title = "Export " .. name, - onclose = function() if not isFirstOpen then onclose() end end - } - - exportDialog -- - :entry{label = "Code", text = code} -- - :separator() -- - :button{text = "Close"} -- - - -- Open and close to initialize bounds - exportDialog:show{wait = false} - exportDialog:close() - - isFirstOpen = false - - local bounds = exportDialog.bounds - bounds.x = bounds.x - (EXPORT_DIALOG_WIDTH - bounds.width) / 2 - bounds.width = EXPORT_DIALOG_WIDTH - exportDialog.bounds = bounds - - exportDialog:show() -end - -local CurrentPage = 1 -local ConfigurationsPerPage = 10 -local LoadButtonIdPrefix = "saved-theme-load-" -local ExportButtonIdPrefix = "saved-theme-export-" -local DeleteButtonIdPrefix = "saved-theme-delete-" - -function ThemeManager:Load(onload, onreset) - local pages = math.ceil(#self.storage.savedThemes / ConfigurationsPerPage) - - CurrentPage = math.min(CurrentPage, pages) - - local skip = (CurrentPage - 1) * ConfigurationsPerPage - - local browseDialog = Dialog("Load Configuration") - - local updateBrowseDialog = function() - browseDialog -- - :modify{id = "button-previous", enabled = CurrentPage > 1} -- - :modify{id = "button-next", enabled = CurrentPage < pages} - - skip = (CurrentPage - 1) * ConfigurationsPerPage - - for index = 1, ConfigurationsPerPage do - local savedthemeCode = self.storage.savedThemes[skip + index] - local loadButtonId = LoadButtonIdPrefix .. tostring(index) - local exportButtonId = ExportButtonIdPrefix .. tostring(index) - local deleteButtonId = DeleteButtonIdPrefix .. tostring(index) - - if savedthemeCode then - local theme = ThemeEncoder:DecodeSigned(savedthemeCode) - - browseDialog -- - :modify{id = loadButtonId, visible = true, label = theme.name} -- - :modify{id = exportButtonId, visible = true} -- - :modify{id = deleteButtonId, visible = true} - else - browseDialog -- - :modify{id = loadButtonId, visible = false} -- - :modify{id = exportButtonId, visible = false} -- - :modify{id = deleteButtonId, visible = false} - end - end - end - - browseDialog -- - :button{ - id = "button-previous", - text = "Previous", - enabled = false, - onclick = function() - CurrentPage = CurrentPage - 1 - updateBrowseDialog() - end - } -- - :button{text = "", enabled = false} -- - :button{ - id = "button-next", - text = "Next", - enabled = pages > 1, - onclick = function() - CurrentPage = CurrentPage + 1 - updateBrowseDialog() - end - } -- - :separator() - - for index = 1, ConfigurationsPerPage do - browseDialog -- - :button{ - id = LoadButtonIdPrefix .. tostring(index), - label = "", -- Set empty label, without it it's impossible to update it later - text = "Load", - onclick = function() - local savedthemeCode = self.storage.savedThemes[skip + index] - local theme = ThemeEncoder:DecodeSigned(savedthemeCode) - - local confirmation = app.alert { - title = "Loading theme " .. theme.name, - text = "Unsaved changes will be lost, do you want to continue?", - buttons = {"Yes", "No"} - } - - if confirmation == 1 then - browseDialog:close() - onload(theme) - end - end - } -- - :button{ - id = ExportButtonIdPrefix .. tostring(index), - text = "Export", - onclick = function() - local savedthemeCode = self.storage.savedThemes[skip + index] - local theme = ThemeEncoder:DecodeSigned(savedthemeCode) - - browseDialog:close() - local onExportDialogClose = function() - browseDialog:show() - end - - self:ShowExportDialog(theme.name, savedthemeCode, - onExportDialogClose) - end - } -- - :button{ - id = DeleteButtonIdPrefix .. tostring(index), - text = "Delete", - onclick = function() - local savedthemeCode = self.storage.savedThemes[skip + index] - local theme = ThemeEncoder:DecodeSigned(savedthemeCode) - - local confirmation = app.alert { - title = "Delete " .. theme.name, - text = "Are you sure?", - buttons = {"Yes", "No"} - } - - if confirmation == 1 then - table.remove(self.storage.savedThemes, skip + index) - - browseDialog:close() - self:Load(onload, onreset) - end - end - } - end - - if #self.storage.savedThemes > 0 then - browseDialog:separator{id = "separator"} - end - - -- Initialize - updateBrowseDialog() - - browseDialog -- - :button{ - text = "Import", - onclick = function() - browseDialog:close() - local importDialog = Dialog("Import") - - importDialog -- - :entry{id = "code", label = "Code"} -- - :separator{id = "separator"} -- - :button{ - text = "Import", - onclick = function() - local code = importDialog.data.code - local theme = ThemeEncoder:DecodeSigned(code) - - if not theme then - importDialog:modify{ - id = "separator", - text = "Incorrect code" - } - return - end - - importDialog:close() - - self:Save(theme, onload, true) - end - } -- - :button{text = "Cancel"} -- - - -- Open and close to initialize bounds - importDialog:show{wait = false} - importDialog:close() - - local bounds = importDialog.bounds - bounds.x = bounds.x - (EXPORT_DIALOG_WIDTH - bounds.width) / 2 - bounds.width = EXPORT_DIALOG_WIDTH - importDialog.bounds = bounds - - importDialog:show() - end - } -- - :button{ - text = "Reset to Default", - onclick = function() - local confirmation = app.alert { - title = "Resetting theme", - text = "Unsaved changes will be lost, do you want to continue?", - buttons = {"Yes", "No"} - } - - if confirmation == 1 then - browseDialog:close() - onreset() - end - end - } - - browseDialog -- - :separator() -- - :button{text = "Close"} -- - :show() -end - -return ThemeManager diff --git a/Theme Preferences/ThemePreferences.lua b/Theme Preferences/ThemePreferences.lua new file mode 100644 index 0000000..1fcaa29 --- /dev/null +++ b/Theme Preferences/ThemePreferences.lua @@ -0,0 +1,144 @@ +local ThemeEncoder = dofile("./Base64Encoder.lua") +local LoadConfigurationDialog = dofile("./LoadConfigurationDialog.lua") +local ImportConfigurationDialog = dofile("./ImportConfigurationDialog.lua") +local SaveConfigurationDialog = dofile("./SaveConfigurationDialog.lua") +local Template = dofile("./Template.lua") + +local ThemePreferences = {preferences = nil} + +function ThemePreferences:Init(preferences) + self.preferences = preferences + self.preferences.savedThemes = self.preferences.savedThemes or { + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + } + + self.preferences.savedThemes = self.preferences.savedThemes or + "" +end + +function ThemePreferences:SetCurrentTheme(theme) + local code = ThemeEncoder:EncodeSigned(theme.name, theme.parameters, + theme.colors) + + if code then self.preferences.currentTheme = code end +end + +function ThemePreferences:GetCurrentTheme() + local currentTheme = self.preferences.currentTheme + if currentTheme then return ThemeEncoder:DecodeSigned(currentTheme) end + + return Template() +end + +function ThemePreferences:GetThemeIndex(name) + for i, encodedTheme in ipairs(self.preferences.savedThemes) do + if ThemeEncoder:DecodeName(encodedTheme) == name then return i end + end +end + +function ThemePreferences:Save(theme) + local themeIndex = self:GetThemeIndex(theme.name) + + local code = ThemeEncoder:EncodeSigned(theme.name, theme.parameters, + theme.colors) + + self.preferences.savedThemes[themeIndex] = code +end + +function ThemePreferences:SaveAs(theme, onsave, isImport) + local dialog = nil + + local onConfirm = function(name, applyImmediately) + local themeIndex = self:GetThemeIndex(name) + + if themeIndex then + local overwriteConfirmation = app.alert { + title = "Configuration overwrite", + text = "Configuration with a name " .. name .. + " already exists, do you want to overwrite it?", + buttons = {"Yes", "No"} + } + + if overwriteConfirmation ~= 1 then return end + end + + theme.name = name + + if not isImport or (isImport and applyImmediately) then + onsave(theme) + end + + local code = ThemeEncoder:EncodeSigned(theme.name, theme.parameters, + theme.colors) + + if themeIndex then + self.preferences.savedThemes[themeIndex] = code + else + table.insert(self.preferences.savedThemes, code) + end + + dialog:close() + end + + dialog = SaveConfigurationDialog(theme, isImport, onConfirm) + dialog:show() +end + +function ThemePreferences:GetDecodedThemes() + local themes = {} + + for _, encodedTheme in ipairs(self.preferences.savedThemes) do + local theme = ThemeEncoder:DecodeSigned(encodedTheme) + theme.code = encodedTheme + + table.insert(themes, theme) + end + + return themes +end + +function ThemePreferences:Load(onload) + local themes = self:GetDecodedThemes() + + local dialog = nil + local onDelete = nil + local onImport = nil + + local CreateDialog = function() + dialog = LoadConfigurationDialog(themes, onload, onDelete, onImport) + dialog:show() + end + + onDelete = function(index) + table.remove(self.preferences.savedThemes, index) + themes = self:GetDecodedThemes() + CreateDialog() + end + + onImport = function() + local decode = function(code) + return ThemeEncoder:DecodeSigned(code) + end + + local onConfirm = function(theme) self:Save(theme, onload, true) end + + local importDialog = ImportConfigurationDialog(decode, onConfirm) + importDialog:show() + end + + CreateDialog() +end + +return ThemePreferences diff --git a/Theme Preferences/ThemePreferencesDialog.lua b/Theme Preferences/ThemePreferencesDialog.lua new file mode 100644 index 0000000..decaaeb --- /dev/null +++ b/Theme Preferences/ThemePreferencesDialog.lua @@ -0,0 +1,499 @@ +local Template = dofile("./Template.lua")() + +function ShiftRGB(value, modifier) + return math.max(math.min(value + modifier, 255), 0) +end + +function ShiftColor(color, redModifier, greenModifer, blueModifier) + return Color { + red = ShiftRGB(color.red, redModifier), + green = ShiftRGB(color.green, greenModifer), + blue = ShiftRGB(color.blue, blueModifier), + alpha = color.alpha + } +end + +return function(options) + local title = "Theme Preferences" + local titleModified = title .. " (modified)" + + function UpdateTitle(name) + title = "Theme Preferences: " .. name + titleModified = title .. " (modified)" + end + + UpdateTitle(options.name) + + local isModified = options.isModified + local colors = options.colors + local parameters = options.parameters + + local dialog = Dialog { + title = isModified and titleModified or title, + onclose = options.onclose + } + + function GetParameters() + return { + isAdvanced = dialog.data["mode-advanced"], + isModified = isModified + } + end + + function MarkAsModified(value) + if isModified == value then return end + + isModified = value + + dialog -- + :modify{id = "save-configuration", enabled = value} -- + :modify{id = "save-as-configuration", enabled = value} -- + :modify{title = title .. (value and " (modified)" or "")} + end + + function ThemeColor(widgetOptions) + dialog:color{ + id = widgetOptions.id, + label = widgetOptions.label, + color = colors[widgetOptions.id] or + Template.colors[widgetOptions.id], + visible = widgetOptions.visible, + onchange = function() + if widgetOptions.onchange then + local color = dialog.data[widgetOptions.id] + widgetOptions.onchange(color) + end + + MarkAsModified(true) + end + } + end + + function ChangeCursorColors() + local color = dialog.data["editor_cursor"] + local outlinecolor = dialog.data["editor_cursor_outline"] + + dialog:modify{ + id = "editor_cursor_shadow", + color = Color { + red = (color.red + outlinecolor.red) / 2, + green = (color.green + outlinecolor.green) / 2, + blue = (color.blue + outlinecolor.blue) / 2, + alpha = color.alpha + } + } + + MarkAsModified(true) + end + + function ChangeMode(options) + -- Set default options + options = options or {} + options.force = options.force ~= nil and options.force or false + + local isSimple = dialog.data["mode-simple"] + + if isSimple then + if not options.force then + local confirmation = app.alert { + title = "Warning", + text = "Switching to Simple Mode will modify your theme, do you want to continue?", + buttons = {"Yes", "No"} + } + + if confirmation == 2 then + dialog:modify{id = "mode-simple", selected = false} + dialog:modify{id = "mode-advanced", selected = true} + return + end + end + + -- Set new simple values when switching to Simple Mode + dialog -- + :modify{id = "simple-link", color = dialog.data["text_link"]} -- + :modify{ + id = "simple-button", + color = dialog.data["button_background"] + } -- + :modify{id = "simple-tab", color = dialog.data["tab_background"]} -- + :modify{ + id = "simple-window-title-bar", + color = dialog.data["window_title_bar_background"] + } -- + :modify{ + id = "simple-window", + color = dialog.data["window_background"] + } -- + :modify{id = "editor_icons", color = dialog.data["text_regular"]} + end + + dialog -- + :modify{id = "simple-link", visible = isSimple} -- + :modify{id = "simple-button", visible = isSimple} -- + :modify{id = "simple-tab", visible = isSimple} -- + :modify{id = "simple-window-title-bar", visible = isSimple} -- + :modify{id = "simple-window", visible = isSimple} -- + + local advancedWidgetIds = { + "button_highlight", "button_background", "button_shadow", + "tab_corner_highlight", "tab_highlight", "tab_background", + "tab_shadow", "window_highlight", "window_background", + "window_shadow", "text_link", "text_separator", "editor_icons", + "window_title_bar_corner_highlight", "window_title_bar_highlight", + "window_title_bar_background", "window_title_bar_shadow" + } + + for _, id in ipairs(advancedWidgetIds) do + dialog:modify{id = id, visible = dialog.data["mode-advanced"]} + end + + if not options.force then MarkAsModified(true) end + end + + dialog -- + :radio{ + id = "mode-simple", + label = "Mode", + text = "Simple", + selected = not parameters.isAdvanced, + onclick = function() ChangeMode() end + } -- + :radio{ + id = "mode-advanced", + text = "Advanced", + selected = parameters.isAdvanced, + onclick = function() ChangeMode() end + } + + dialog:separator{text = "Text"} + + ThemeColor {label = "Active/Regular", id = "text_active", visible = true} + ThemeColor { + id = "text_regular", + visible = true, + onchange = function(color) + if dialog.data["mode-simple"] then + dialog:modify{id = "editor_icons", color = color} + end + end + } + ThemeColor {label = "Link/Separator", id = "text_link", visible = false} + ThemeColor {id = "text_separator", visible = false} + + dialog:color{ + id = "simple-link", + label = "Link/Separator", + color = colors["text_link"], + onchange = function() + local color = dialog.data["simple-link"] + + dialog:modify{id = "text_link", color = color} + dialog:modify{id = "text_separator", color = color} + + MarkAsModified(true) + end + } + + dialog:separator{text = "Input Fields"} + + ThemeColor {label = "Highlight", id = "field_highlight", visible = true} + + -- FUTURE: Allow for separate chaning of the "field_background" + -- dialog:color{ + -- id = "simple-field", + -- label = "Background", + -- color = colors["field_background"], + -- onchange = function() + -- local color = dialog.data["simple-field"] + + -- local shadowColor = Color { + -- red = ShiftRGB(color.red, -57), + -- green = ShiftRGB(color.green, -57), + -- blue = ShiftRGB(color.blue, -57), + -- alpha = color.alpha + -- } + + -- local cornerShadowColor = Color { + -- red = ShiftRGB(color.red, -74), + -- green = ShiftRGB(color.green, -74), + -- blue = ShiftRGB(color.blue, -74), + -- alpha = color.alpha + -- } + + -- colors["field_background"] = color + -- colors["field_shadow"] = shadowColor + -- colors["field_corner_shadow"] = cornerShadowColor + -- end + -- } + + dialog:separator{text = "Editor"} + + ThemeColor { + label = "Background", + id = "editor_background", + onchange = function(color) + dialog:modify{ + id = "editor_background_shadow", + color = ShiftColor(color, -36, -20, -53) + } + end + } + + ThemeColor {id = "editor_tooltip_shadow", visible = false} + ThemeColor {id = "editor_tooltip_corner_shadow", visible = false} + ThemeColor {id = "editor_background_shadow", visible = false} + + ThemeColor {label = "Icons", id = "editor_icons", visible = false} + + ThemeColor { + label = "Tooltip", + id = "editor_tooltip", + onchange = function(color) + dialog:modify{ + id = "editor_tooltip_shadow", + color = ShiftColor(color, -100, -90, -32) + } + dialog:modify{ + id = "editor_tooltip_corner_shadow", + color = ShiftColor(color, -125, -152, -94) + } + end + } + + dialog -- + :color{ + id = "editor_cursor", + label = "Cursor", + color = colors["editor_cursor"], + onchange = function() ChangeCursorColors() end + } -- + :color{ + id = "editor_cursor_outline", + color = colors["editor_cursor_outline"], + onchange = function() ChangeCursorColors() end + } + + ThemeColor {id = "editor_cursor_shadow", visible = false} + + dialog:separator{text = "Button"} + + ThemeColor {id = "button_highlight", visible = false} + ThemeColor {id = "button_background", visible = false} + ThemeColor {id = "button_shadow", visible = false} + + dialog:color{ + id = "simple-button", + color = colors["button_background"], + onchange = function() + local color = dialog.data["simple-button"] + + dialog:modify{ + id = "button_highlight", + color = ShiftColor(color, 57, 57, 57) + } + dialog:modify{id = "button_background", color = color} + dialog:modify{ + id = "button_shadow", + color = ShiftColor(color, -74, -74, -74) + } + + MarkAsModified(true) + end + } + + ThemeColor {label = "Selected", id = "button_selected", visible = true} + + dialog:separator{text = "Tab"} + + ThemeColor {id = "tab_corner_highlight", visible = false} + ThemeColor {id = "tab_highlight", visible = false} + ThemeColor {id = "tab_background", visible = false} + ThemeColor {id = "tab_shadow", visible = false} + + dialog:color{ + id = "simple-tab", + color = colors["tab_background"], + onchange = function() + local color = dialog.data["simple-tab"] + + dialog:modify{ + id = "tab_corner_highlight", + color = ShiftColor(color, 131, 110, 98) + } + dialog:modify{ + id = "tab_highlight", + color = ShiftColor(color, 49, 57, 65) + } + dialog:modify{id = "tab_background", color = color} + dialog:modify{ + id = "tab_shadow", + color = ShiftColor(color, -24, -61, -61) + } + + MarkAsModified(true) + end + } + + dialog:separator{text = "Window"} + + dialog:color{ + label = "Title Bar", + id = "simple-window-title-bar", + color = colors["window_title_bar_background"], + onchange = function() + local color = dialog.data["simple-window-title-bar"] + + dialog:modify{ + id = "window_title_bar_corner_highlight", + color = ShiftColor(color, 131, 110, 98) + } + dialog:modify{ + id = "window_title_bar_highlight", + color = ShiftColor(color, 49, 57, 65) + } + dialog:modify{id = "window_title_bar_background", color = color} + dialog:modify{ + id = "window_title_bar_shadow", + color = ShiftColor(color, -24, -61, -61) + } + + MarkAsModified(true) + end + } -- + + ThemeColor { + label = "Title Bar", + id = "window_title_bar_corner_highlight", + visible = false + } + ThemeColor {id = "window_title_bar_highlight", visible = false} + ThemeColor {id = "window_title_bar_background", visible = false} + ThemeColor {id = "window_title_bar_shadow", visible = false} + + ThemeColor { + label = "Body", + id = "window_highlight", + visible = false, + onchange = function(color) + -- FUTURE: Remove this when setting a separate value for the "field_background" is possible + + local fieldShadowColor = ShiftColor(color, -57, -57, -57) + local filedCornerShadowColor = ShiftColor(color, -74, -74, -74) + + dialog:modify{id = "field_background", color = color} + dialog:modify{id = "field_shadow", color = fieldShadowColor} + dialog:modify{ + id = "field_corner_shadow", + color = filedCornerShadowColor + } + end + } + + ThemeColor {id = "window_background", visible = false} + ThemeColor {id = "window_corner_shadow", visible = false} + + ThemeColor { + id = "window_shadow", + visible = false, + onchange = function(color) + dialog:modify{ + id = "window_corner_shadow", + color = ShiftColor(color, -49, -44, -20) + } + end + } + + ThemeColor {id = "field_background", visible = false} + ThemeColor {id = "field_shadow", visible = false} + ThemeColor {id = "field_corner_shadow", visible = false} + + dialog:color{ + label = "Body", + id = "simple-window", + color = colors["window_background"], + onchange = function() + local color = dialog.data["simple-window"] + local highlightColor = ShiftColor(color, 45, 54, 66) + + dialog:modify{id = "window_highlight", color = highlightColor} + dialog:modify{id = "window_background", color = color} + dialog:modify{ + id = "window_shadow", + color = ShiftColor(color, -61, -73, -73) + } + dialog:modify{ + id = "window_corner_shadow", + color = ShiftColor(color, -110, -117, -93) + } + + -- FUTURE: Remove this when setting a separate value for the "field_background" is possible + + local fieldShadowColor = ShiftColor(highlightColor, -57, -57, -57) + local filedCornerShadowColor = + ShiftColor(highlightColor, -74, -74, -74) + + dialog:modify{id = "field_background", color = highlightColor} + dialog:modify{id = "field_shadow", color = fieldShadowColor} + dialog:modify{ + id = "field_corner_shadow", + color = filedCornerShadowColor + } + + MarkAsModified(true) + end + } -- + + ThemeColor {label = "Hover", id = "window_hover", visible = true} + ThemeColor {label = "Outline", id = "outline", visible = true} + + dialog -- + :separator() -- + :button{ + id = "save-configuration", + label = "Configuration", + text = "Save", + enabled = isModified, -- Only allows saving of a modified theme + onclick = function() + options.onsave(dialog.data, GetParameters()) + MarkAsModified(false) + end + } -- + :button{ + id = "save-as-configuration", + text = "Save As", + enabled = isModified, -- Only allows saving of a modified theme + onclick = function() + local refreshTitle = function(name) + UpdateTitle(name) + MarkAsModified(false) + end + + options.onsaveas(dialog.data, GetParameters(), refreshTitle) + end + } -- + :button{text = "Load", onclick = function() options.onload() end} -- + :separator() -- + :button{ + text = "Reset to Default", + onclick = function() options.onreset() end + } -- + :separator() -- + :button{ + text = "OK", + onclick = function() + options.onok(dialog.data, GetParameters()) + dialog:close() + end + } -- + :button{ + text = "Apply", + onclick = function() options.onok(dialog.data, GetParameters()) end + } -- + :button{text = "Cancel", onclick = function() dialog:close() end} -- + + if parameters.isAdvanced then ChangeMode {force = true} end + + return dialog +end + +-- TODO: Reset the theme on cancel, reverting all unsaved changes? diff --git a/Theme Preferences/extension.lua b/Theme Preferences/extension.lua index 9dfa915..b1def9c 100644 --- a/Theme Preferences/extension.lua +++ b/Theme Preferences/extension.lua @@ -1,731 +1,175 @@ local Template = dofile("./Template.lua") -local ThemeManager = dofile("./ThemeManager.lua") -local FontsProvider = dofile("./FontsProvider.lua") - -local THEME_ID = "custom" -local DIALOG_WIDTH = 240 -local DIALOG_TITLE = "Theme Preferences" - -local ExtensionsDirectory = app.fs.joinPath(app.fs.userConfigPath, "extensions") -local ThemePreferencesDirectory = app.fs.joinPath(ExtensionsDirectory, - "theme-preferences") -local SheetTemplatePath = app.fs.joinPath(ThemePreferencesDirectory, - "sheet-template.png") -local SheetPath = app.fs.joinPath(ThemePreferencesDirectory, "sheet.png") -local ThemeXmlTemplatePath = app.fs.joinPath(ThemePreferencesDirectory, - "theme-template.xml") -local ThemeXmlPath = app.fs.joinPath(ThemePreferencesDirectory, "theme.xml") - -function ReadAll(filePath) - local file = assert(io.open(filePath, "rb")) - local content = file:read("*all") - file:close() - return content -end - -function WriteAll(filePath, content) - local file = io.open(filePath, "w") - if file then - file:write(content) - file:close() - end -end - -function ColorToHex(color) - return string.format("#%02x%02x%02x", color.red, color.green, color.blue) -end - -function RgbaPixelToColor(rgbaPixel) - return Color { - red = app.pixelColor.rgbaR(rgbaPixel), - green = app.pixelColor.rgbaG(rgbaPixel), - blue = app.pixelColor.rgbaB(rgbaPixel), - alpha = app.pixelColor.rgbaA(rgbaPixel) - } -end - -function CopyColor(originalColor) - return Color { - red = originalColor.red, - green = originalColor.green, - blue = originalColor.blue, - alpha = originalColor.alpha - } -end - --- Color Definitions -local Theme = {name = "", colors = {}, parameters = {}} - --- Copy template to theme -Theme.name = Template.name - -for id, color in pairs(Template.colors) do Theme.colors[id] = CopyColor(color) end - -for id, parameter in pairs(Template.parameters) do - Theme.parameters[id] = parameter -end - --- Dialog -local ThemePreferencesDialog = { - isModified = false, - lastRefreshState = false, - isDialogOpen = false, - onClose = nil, - dialog = nil -} - -ThemePreferencesDialog.dialog = Dialog { - title = DIALOG_TITLE, - onclose = function() ThemePreferencesDialog:onClose() end -} - -function ThemePreferencesDialog:SetInitialWidth() - self.dialog:show{wait = false} - self.dialog:close() - - local uiScale = app.preferences.general["ui_scale"] - - local bounds = self.dialog.bounds - bounds.x = bounds.x - (DIALOG_WIDTH - bounds.width) / 2 - bounds.width = DIALOG_WIDTH * uiScale +local ThemePreferences = dofile("./ThemePreferences.lua") +local FontPreferences = dofile("./FontPreferences.lua") +local ThemePreferencesDialog = dofile("./ThemePreferencesDialog.lua") +local DialogBounds = dofile("./DialogBounds.lua") +local RefreshTheme = dofile("./RefreshTheme.lua") - self.dialog.bounds = bounds -end - -function ThemePreferencesDialog:RefreshTheme(template, theme) - -- Prepare color lookup - local Map = {} - - for id, templateColor in pairs(template.colors) do - -- Map the template color to the theme color - Map[ColorToHex(templateColor)] = theme.colors[id] - end - - -- Prepare sheet.png - local image = Image {fromFile = SheetTemplatePath} - local pixelValue, newColor, pixelData, pixelColor, pixelValueKey, - resultColor - - -- Save references to function to improve performance - local getPixel, drawPixel = image.getPixel, image.drawPixel - - local cache = {} - - for x = 0, image.width - 1 do - for y = 0, image.height - 1 do - pixelValue = getPixel(image, x, y) - - if pixelValue > 0 then - pixelValueKey = tostring(pixelValue) - pixelData = cache[pixelValueKey] - - if not pixelData then - pixelColor = RgbaPixelToColor(pixelValue) - - cache[pixelValueKey] = { - id = ColorToHex(pixelColor), - color = pixelColor - } +local SimpleDialogSize = Size(240, 422) +local AdvancedDialogSize = Size(240, 440) - pixelData = cache[pixelValueKey] - end - - resultColor = Map[pixelData.id] +local IsDialogOpen = false +local IsFontsDialogOpen = false +local IsModified = false - if resultColor ~= nil then - newColor = CopyColor(resultColor) - newColor.alpha = pixelData.color.alpha -- Restore the original alpha value +function init(plugin) + -- Do nothing when UI is not available + if not app.isUIAvailable then return end - drawPixel(image, x, y, newColor) - end - end + -- Copy plugin theme preferences data for backwards compatibility + if plugin.preferences.themePreferences then + for key, value in pairs(plugin.preferences.themePreferences) do + plugin.preferences[key] = value end - end - - image:saveAs(SheetPath) - - -- Update the XML theme file - ThemePreferencesDialog:UpdateThemeXml(theme) - - app.command.Refresh() -end - -function ThemePreferencesDialog:UpdateThemeXml(theme) - -- Prepare theme.xml - local xmlContent = ReadAll(ThemeXmlTemplatePath) - - for id, color in pairs(theme.colors) do - xmlContent = xmlContent:gsub("<" .. id .. ">", ColorToHex(color)) - end - - local font = FontsProvider:GetCurrentFont() - - -- Setting fonts for these just in case it's a system font - xmlContent = xmlContent:gsub("", - FontsProvider:GetFontDeclaration(font.default)) - xmlContent = xmlContent:gsub("", font.default.name) - xmlContent = xmlContent:gsub("", font.default.size) - - xmlContent = xmlContent:gsub("", - FontsProvider:GetFontDeclaration(font.mini)) - xmlContent = xmlContent:gsub("", font.mini.name) - xmlContent = xmlContent:gsub("", font.mini.size) - - -- TODO: If using system fonts - ask user if they want to switch default scaling percentages - - WriteAll(ThemeXmlPath, xmlContent) -end - -function ThemePreferencesDialog:Refresh() - self.lastRefreshState = self.isModified - self:RefreshTheme(Template, Theme) - ThemeManager:SetCurrentTheme(Theme) - - -- Switch Aseprite to the custom theme - if app.preferences.theme.selected ~= THEME_ID then - app.preferences.theme.selected = THEME_ID + plugin.preferences.themePreferences = nil end -end -function ShiftRGB(value, modifier) - return math.max(math.min(value + modifier, 255), 0) -end + local preferences = plugin.preferences -function ShiftColor(color, redModifier, greenModifer, blueModifier) - return Color { - red = ShiftRGB(color.red, redModifier), - green = ShiftRGB(color.green, greenModifer), - blue = ShiftRGB(color.blue, blueModifier), - alpha = color.alpha - } -end + -- Initialize data from plugin preferences + ThemePreferences:Init(preferences) + FontPreferences:Init(preferences) + IsModified = preferences.isThemeModified -function ThemePreferencesDialog:MarkThemeAsModified() - self.isModified = true + plugin:newCommand{ + id = "ThemePreferencesNew", + title = "Theme Preferences...", + group = "view_screen", + onenabled = function() return not IsDialogOpen end, + onclick = function() + local currentTheme = ThemePreferences:GetCurrentTheme() - self.dialog -- - :modify{id = "save-configuration", enabled = true} -- - :modify{title = DIALOG_TITLE .. ": " .. Theme.name .. " (modified)"} -end + local dialog = nil + local CreateDialog = function() end -function ThemePreferencesDialog:SetThemeColor(id, color) - Theme.colors[id] = color - if self.dialog.data[id] then self.dialog:modify{id = id, color = color} end -end + local onSave = function(colors, parameters) + currentTheme.colors = colors + currentTheme.parameters = parameters + IsModified = false -function ThemePreferencesDialog:ChangeMode(options) - -- Set default options - options = options or {} - options.force = options.force ~= nil and options.force or false - - local isSimple = self.dialog.data["mode-simple"] - - if isSimple then - if not options.force then - local confirmation = app.alert { - title = "Warning", - text = "Switching to Simple Mode will modify your theme, do you want to continue?", - buttons = {"Yes", "No"} - } - - if confirmation == 2 then - self.dialog:modify{id = "mode-simple", selected = false} - self.dialog:modify{id = "mode-advanced", selected = true} - return + ThemePreferences:Save(currentTheme) end - end - - -- Set new simple values when switching to Simple Mode - self.dialog -- - :modify{id = "simple-link", color = Theme.colors["text_link"]} -- - :modify{id = "simple-button", color = Theme.colors["button_background"]} -- - :modify{id = "simple-tab", color = Theme.colors["tab_background"]} -- - :modify{id = "simple-window", color = Theme.colors["window_background"]} -- - :modify{id = "editor_icons", color = Theme.colors["text_regular"]} - end - - self.dialog -- - :modify{id = "simple-link", visible = isSimple} -- - :modify{id = "simple-button", visible = isSimple} -- - :modify{id = "simple-tab", visible = isSimple} -- - :modify{id = "simple-window", visible = isSimple} - - local advancedWidgetIds = { - "button_highlight", "button_background", "button_shadow", - "tab_corner_highlight", "tab_highlight", "tab_background", "tab_shadow", - "window_highlight", "window_background", "window_shadow", "text_link", - "text_separator", "editor_icons" - } - - for _, id in ipairs(advancedWidgetIds) do - self.dialog:modify{id = id, visible = self.dialog.data["mode-advanced"]} - end - - Theme.parameters.isAdvanced = self.dialog.data["mode-advanced"] - self:MarkThemeAsModified() -end - -function ThemePreferencesDialog:LoadTheme(theme) - -- Copy theme to the current theme - Theme.name = theme.name - Theme.parameters = theme.parameters - - -- Chanage mode - self.dialog -- - :modify{id = "mode-simple", selected = not theme.parameters.isAdvanced} -- - :modify{id = "mode-advanced", selected = theme.parameters.isAdvanced} - - self:ChangeMode{force = true} - - -- Load simple versions first to then overwrite advanced colors - local simpleButtons = { - ["simple-link"] = theme.colors["text_link"], - ["simple-button"] = theme.colors["button_background"], - -- ["simple-field"] = Theme.colors["field_background"], - ["simple-tab"] = theme.colors["tab_background"], - ["simple-window"] = theme.colors["window_background"] - } - for id, color in pairs(simpleButtons) do - self.dialog:modify{id = id, color = color} - end - - -- Finally, copy colors - for id, color in pairs(theme.colors) do - -- Copy color just in case - self:SetThemeColor(id, CopyColor(color)) - end - - self.dialog:modify{title = DIALOG_TITLE .. ": " .. theme.name} -- - self.dialog:modify{id = "save-configuration", enabled = false} - - self.isModified = false -end + local onSaveAs = function(colors, parameters, refreshTitle) + local onsuccess = function(theme) + refreshTitle(theme.name) -function ThemePreferencesDialog:ThemeColor(options) - self.dialog:color{ - id = options.id, - label = options.label, - color = Theme.colors[options.id], - visible = options.visible, - onchange = function() - local color = self.dialog.data[options.id] - Theme.colors[options.id] = color - - if options.onchange then options.onchange(color) end - - self:MarkThemeAsModified() - end - } -end + currentTheme.colors = colors + currentTheme.parameters = parameters + IsModified = false -function ThemePreferencesDialog:ChangeCursorColors() - local color = self.dialog.data["editor_cursor"] - local outlinecolor = self.dialog.data["editor_cursor_outline"] - - local shadowColor = Color { - red = (color.red + outlinecolor.red) / 2, - green = (color.green + outlinecolor.green) / 2, - blue = (color.blue + outlinecolor.blue) / 2, - alpha = color.alpha - } - - Theme.colors["editor_cursor"] = color - Theme.colors["editor_cursor_shadow"] = shadowColor - Theme.colors["editor_cursor_outline"] = outlinecolor - - self:MarkThemeAsModified() -end - -function ThemePreferencesDialog:LoadCurrentTheme() - local currentTheme = ThemeManager:GetCurrentTheme() - if currentTheme then self:LoadTheme(currentTheme) end -end - -function ThemePreferencesDialog:Init() - -- Colors = Tint, Highlight, Tooltip (label as Hover) - - -- Link/Separator = Tint Color - -- Simple Tab Color = 50/50 Tint Color/Window Background Color - -- Highlight = Highlight - -- Tooltip = Tooltip - -- Hover = 50/50 Tooltip/Window Background Color - - self.dialog -- - -- :radio{ - -- id = "mode-tint", - -- label = "Mode", - -- text = "Tint", - -- selected = true, - -- onclick = ChangeMode - -- } -- - :radio{ - id = "mode-simple", - label = "Mode", - text = "Simple", - selected = true, - onclick = function() self:ChangeMode() end - } -- - :radio{ - id = "mode-advanced", - text = "Advanced", - selected = false, - onclick = function() self:ChangeMode() end - } - - self.dialog:separator{text = "Text"} + ThemePreferences:SetCurrentTheme(theme) + end - self:ThemeColor{ - label = "Active/Regular", - id = "text_active", - visible = true - } - self:ThemeColor{ - id = "text_regular", - visible = true, - onchange = function(color) - if self.dialog.data["mode-simple"] then - self:SetThemeColor("editor_icons", color) + ThemePreferences:SaveAs(currentTheme, onsuccess) end - end - } - self:ThemeColor{label = "Link/Separator", id = "text_link", visible = false} - self:ThemeColor{id = "text_separator", visible = false} - - self.dialog:color{ - id = "simple-link", - label = "Link/Separator", - color = Theme.colors["text_link"], - onchange = function() - local color = self.dialog.data["simple-link"] - - self:SetThemeColor("text_link", color) - self:SetThemeColor("text_separator", color) - - self:MarkThemeAsModified() - end - } - - self.dialog:separator{text = "Input Fields"} - - self:ThemeColor{label = "Highlight", id = "field_highlight", visible = true} - - -- FUTURE: Allow for separate chaning of the "field_background" - -- dialog:color{ - -- id = "simple-field", - -- label = "Background", - -- color = Theme.colors["field_background"], - -- onchange = function() - -- local color = dialog.data["simple-field"] - - -- local shadowColor = Color { - -- red = ShiftRGB(color.red, -57), - -- green = ShiftRGB(color.green, -57), - -- blue = ShiftRGB(color.blue, -57), - -- alpha = color.alpha - -- } - - -- local cornerShadowColor = Color { - -- red = ShiftRGB(color.red, -74), - -- green = ShiftRGB(color.green, -74), - -- blue = ShiftRGB(color.blue, -74), - -- alpha = color.alpha - -- } - - -- Theme.colors["field_background"] = color - -- Theme.colors["field_shadow"] = shadowColor - -- Theme.colors["field_corner_shadow"] = cornerShadowColor - -- end - -- } - - self.dialog:separator{text = "Editor"} - - self:ThemeColor{ - label = "Background", - id = "editor_background", - onchange = function(color) - local shadowColor = ShiftColor(color, -36, -20, -53) - Theme.colors["editor_background_shadow"] = shadowColor - end - } - self:ThemeColor{label = "Icons", id = "editor_icons", visible = false} + local onLoad = function() + -- Hide the Theme Preferences dialog + dialog:close() - self:ThemeColor{ - label = "Tooltip", - id = "editor_tooltip", - onchange = function(color) - local shadowColor = ShiftColor(color, -100, -90, -32) - local cornerShadowColor = ShiftColor(color, -125, -152, -94) + local onConfirm = function(theme) + currentTheme = theme or Template() - Theme.colors["editor_tooltip_shadow"] = shadowColor - Theme.colors["editor_tooltip_corner_shadow"] = cornerShadowColor - end - } - - self.dialog -- - :color{ - id = "editor_cursor", - label = "Cursor", - color = Theme.colors["editor_cursor"], - onchange = function() self:ChangeCursorColors() end - } -- - :color{ - id = "editor_cursor_outline", - color = Theme.colors["editor_cursor_outline"], - onchange = function() self:ChangeCursorColors() end - } - - self.dialog:separator{text = "Button"} - - self:ThemeColor{id = "button_highlight", visible = false} - self:ThemeColor{id = "button_background", visible = false} - self:ThemeColor{id = "button_shadow", visible = false} - - self.dialog:color{ - id = "simple-button", - color = Theme.colors["button_background"], - onchange = function() - local color = self.dialog.data["simple-button"] - local highlightColor = ShiftColor(color, 57, 57, 57) - local shadowColor = ShiftColor(color, -74, -74, -74) - - self:SetThemeColor("button_highlight", highlightColor) - self:SetThemeColor("button_background", color) - self:SetThemeColor("button_shadow", shadowColor) - - self:MarkThemeAsModified() - end - } + ThemePreferences:SetCurrentTheme(currentTheme) + IsModified = false - self:ThemeColor{label = "Selected", id = "button_selected", visible = true} + local currentFont = FontPreferences:GetCurrentFont() - self.dialog:separator{text = "Tab"} - - self:ThemeColor{id = "tab_corner_highlight", visible = false} - self:ThemeColor{id = "tab_highlight", visible = false} - self:ThemeColor{id = "tab_background", visible = false} - self:ThemeColor{id = "tab_shadow", visible = false} - - self.dialog:color{ - id = "simple-tab", - color = Theme.colors["tab_background"], - onchange = function() - local color = self.dialog.data["simple-tab"] - local cornerHighlightColor = ShiftColor(color, 131, 110, 98) - local highlightColor = ShiftColor(color, 49, 57, 65) - local shadowColor = ShiftColor(color, -24, -61, -61) - - self:SetThemeColor("tab_corner_highlight", cornerHighlightColor) - self:SetThemeColor("tab_highlight", highlightColor) - self:SetThemeColor("tab_background", color) - self:SetThemeColor("tab_shadow", shadowColor) - - self:MarkThemeAsModified() - end - } - - self.dialog:separator{text = "Window"} - - self:ThemeColor{ - id = "window_highlight", - visible = false, - onchange = function(color) - Theme.colors["window_highlight"] = color - - -- FUTURE: Remove this when setting a separate value for the "field_background" is possible + RefreshTheme(currentTheme, currentFont) + end - local fieldShadowColor = ShiftColor(color, -57, -57, -57) - local filedCornerShadowColor = ShiftColor(color, -74, -74, -74) + ThemePreferences:Load(onConfirm) - Theme.colors["field_background"] = color - Theme.colors["field_shadow"] = fieldShadowColor - Theme.colors["field_corner_shadow"] = filedCornerShadowColor - end - } + -- Reopen the dialog + dialog = CreateDialog() + end - self:ThemeColor{id = "window_background", visible = false} + local onReset = function() + -- Hide the Theme Preferences dialog + dialog:close() - self:ThemeColor{ - id = "window_shadow", - visible = false, - onchange = function(color) - local cornerShadowColor = ShiftColor(color, -49, -44, -20) - self:SetThemeColor("window_corner_shadow", cornerShadowColor) - end - } + currentTheme = Template() - self.dialog:color{ - id = "simple-window", - color = Theme.colors["window_background"], - onchange = function() - local color = self.dialog.data["simple-window"] - local highlightColor = ShiftColor(color, 45, 54, 66) - local shadowColor = ShiftColor(color, -61, -73, -73) - local cornerShadowColor = ShiftColor(color, -110, -117, -93) + ThemePreferences:SetCurrentTheme(currentTheme) + IsModified = false - self:SetThemeColor("window_highlight", highlightColor) - self:SetThemeColor("window_background", color) - self:SetThemeColor("window_shadow", shadowColor) - self:SetThemeColor("window_corner_shadow", cornerShadowColor) + local currentFont = FontPreferences:GetCurrentFont() - -- FUTURE: Remove this when setting a separate value for the "field_background" is possible + RefreshTheme(currentTheme, currentFont) - local fieldShadowColor = ShiftColor(highlightColor, -57, -57, -57) - local filedCornerShadowColor = - ShiftColor(highlightColor, -74, -74, -74) + -- Reopen the dialog + dialog = CreateDialog() + end - Theme.colors["field_background"] = highlightColor - Theme.colors["field_shadow"] = fieldShadowColor - Theme.colors["field_corner_shadow"] = filedCornerShadowColor + local onConfirm = function(colors, parameters) + currentTheme.colors = colors + currentTheme.parameters = parameters - self:MarkThemeAsModified() - end - } -- + IsModified = parameters.isModified - self:ThemeColor{label = "Hover", id = "window_hover", visible = true} + ThemePreferences:SetCurrentTheme(currentTheme) - self.dialog -- - :separator() -- - :button{ - id = "save-configuration", - label = "Configuration", - text = "Save", - enabled = false, - onclick = function() - local onsave = function(theme) - self.dialog:modify{title = DIALOG_TITLE .. ": " .. theme.name} - self.dialog:modify{id = "save-configuration", enabled = false} + local currentFont = FontPreferences:GetCurrentFont() - self.isModified = false - self.lastRefreshState = false + RefreshTheme(currentTheme, currentFont) end - ThemeManager:Save(Theme, onsave) - end - } -- - :button{ - text = "Load", - onclick = function() - local onload = function(theme) - self:LoadTheme(theme) - self:Refresh() - end + CreateDialog = function() + local newDialog = ThemePreferencesDialog { + name = currentTheme.name, + colors = currentTheme.colors, + parameters = currentTheme.parameters, + isModified = IsModified, + onclose = function() IsDialogOpen = false end, + onsave = onSave, + onsaveas = onSaveAs, + onload = onLoad, + onreset = onReset, + onok = onConfirm + } + + local bounds = currentTheme.parameters.isAdvanced and + AdvancedDialogSize or SimpleDialogSize + + local position = nil + + if dialog then + position = Point(dialog.bounds.x, dialog.bounds.y) + end - local onreset = function() - self:LoadTheme(Template) - self:Refresh() + newDialog:show{ + wait = false, + bounds = DialogBounds(bounds, position), + autoscrollbars = true + } + return newDialog end - -- Hide the Theme Preferences dialog - ThemePreferencesDialog.dialog:close() - - ThemeManager:Load(onload, onreset) - - -- Reopen the dialog - ThemePreferencesDialog.dialog:show{wait = false} - end - } -- - :button{ - text = "Font", - onclick = function() - local onconfirm = function() self:Refresh() end - - -- Hide the Theme Preferences dialog - ThemePreferencesDialog.dialog:close() - - FontsProvider:OpenDialog(onconfirm) - - -- Reopen the dialog - ThemePreferencesDialog.dialog:show{wait = false} + dialog = CreateDialog() + IsDialogOpen = true end } - self.dialog -- - :separator() -- - :button{ - text = "OK", - onclick = function() - self:Refresh() - self.dialog:close() - end - } -- - :button{text = "Apply", onclick = function() self:Refresh() end} -- - :button{text = "Cancel", onclick = function() self.dialog:close() end} -- -end - -function init(plugin) - -- Do nothing when UI is not available - if not app.isUIAvailable then return end - - -- Initialize plugin preferences data for backwards compatibility - plugin.preferences.themePreferences = - plugin.preferences.themePreferences or {} - local storage = plugin.preferences.themePreferences - - ThemeManager:Init{storage = storage} - FontsProvider:Init{storage = storage} - - -- Initialize the diaog - ThemePreferencesDialog:Init() - - -- Initialize data from plugin preferences - ThemePreferencesDialog:LoadCurrentTheme() - ThemePreferencesDialog.isModified = plugin.preferences.themePreferences - .isThemeModified - if ThemePreferencesDialog.isModified then - ThemePreferencesDialog:MarkThemeAsModified() - end - - -- Treat the "Modified" state as the last known refresh state - ThemePreferencesDialog.lastRefreshState = ThemePreferencesDialog.isModified - - -- Setup function to be called on close - ThemePreferencesDialog.onClose = function() - ThemePreferencesDialog:LoadCurrentTheme() - - ThemePreferencesDialog.isModified = - ThemePreferencesDialog.lastRefreshState - if ThemePreferencesDialog.isModified then - ThemePreferencesDialog:MarkThemeAsModified() - end - - ThemePreferencesDialog.isDialogOpen = false - end - - -- Set the initial width of the dialog - ThemePreferencesDialog:SetInitialWidth() - plugin:newCommand{ - id = "ThemePreferences", - title = DIALOG_TITLE .. "...", + id = "FontPreferences", + title = "Font Preferences...", group = "view_screen", - onenabled = function() - return not ThemePreferencesDialog.isDialogOpen - end, + onenabled = function() return not IsFontsDialogOpen end, onclick = function() - -- Refreshing the UI on open to fix the issue where the dialog would keep parts of the old theme - app.command.Refresh() - - -- Show Theme Preferences dialog - ThemePreferencesDialog.dialog:show{wait = false} - - -- Treat the "Modified" state as the last known refresh state - ThemePreferencesDialog.lastRefreshState = - ThemePreferencesDialog.isModified + local onClose = function() IsFontsDialogOpen = false end - -- Update the dialog if the theme is modified - if ThemePreferencesDialog.isModified then - ThemePreferencesDialog:MarkThemeAsModified() + local onConfirm = function(font) + local currentTheme = ThemePreferences:GetCurrentTheme() + RefreshTheme(currentTheme, font) end - ThemePreferencesDialog.isDialogOpen = true + FontPreferences:OpenDialog(onClose, onConfirm) + + IsFontsDialogOpen = true end } end -function exit(plugin) - plugin.preferences.themePreferences.isThemeModified = - ThemePreferencesDialog.isModified -end +function exit(plugin) plugin.preferences.isThemeModified = IsModified end diff --git a/Theme Preferences/sheet-template.png b/Theme Preferences/sheet-template.png index 2840d01..f21119d 100644 Binary files a/Theme Preferences/sheet-template.png and b/Theme Preferences/sheet-template.png differ diff --git a/Theme Preferences/theme-template.xml b/Theme Preferences/theme-template.xml index 04b551e..a178007 100644 --- a/Theme Preferences/theme-template.xml +++ b/Theme Preferences/theme-template.xml @@ -68,9 +68,9 @@ - + - +