From 90fc1e6bc05be1638e89ddf14713fce413c7f937 Mon Sep 17 00:00:00 2001 From: Dmitriy Morozov Date: Sat, 30 May 2026 18:14:40 -0400 Subject: [PATCH 1/8] Add visual stylesheet editor --- src/ipe/lua/actions.lua | 232 +++++++++++++++++++++++++++++++++++++ src/ipe/lua/main.lua | 9 +- src/ipelua/ipeluastyle.cpp | 74 ++++++++++++ 3 files changed, 312 insertions(+), 3 deletions(-) diff --git a/src/ipe/lua/actions.lua b/src/ipe/lua/actions.lua index 2e057e0..2a8aaac 100644 --- a/src/ipe/lua/actions.lua +++ b/src/ipe/lua/actions.lua @@ -2659,6 +2659,236 @@ local function sheets_add(d, dd) dd.modified = true end +local visual_style_categories = { + { label="Colors", kind="color", color=true, default="#000000", + help="Color values use the color picker or three numbers between 0 and 1." }, + { label="Pen widths", kind="pen", default="1", + help="Pen widths are numbers in Ipe points." }, + { label="Dash styles", kind="dashstyle", default="[4] 0", + help="Dash styles use Ipe syntax, for example [4 2] 0." }, + { label="Text sizes", kind="textsize", default="\\large", + help="Text sizes are numbers or LaTeX size commands such as \\large." }, + { label="Symbol sizes", kind="symbolsize", default="3", + help="Symbol sizes are numbers in Ipe points." }, + { label="Arrow sizes", kind="arrowsize", default="7", + help="Arrow sizes are numbers in Ipe points." }, + { label="Opacity", kind="opacity", default="1", + help="Opacity values are numbers between 0 and 1." }, + { label="Grid sizes", kind="gridsize", default="8", + help="Grid sizes are numbers in Ipe points." }, + { label="Angle sizes", kind="anglesize", default="45", + help="Angle sizes are numbers in degrees." }, +} + +local function visual_category_labels() + local r = {} + for i,c in ipairs(visual_style_categories) do r[i] = c.label end + return r +end + +local function visual_entry_names(entries) + local r = {} + for i,e in ipairs(entries) do r[i] = e.name end + return r +end + +local function visual_unique_name(entries, base) + local used = {} + for _,e in ipairs(entries) do used[e.name] = true end + if not used[base] then return base end + local n = 2 + while used[base .. " " .. n] do n = n + 1 end + return base .. " " .. n +end + +local function visual_rgb_to_hex(value) + local rgb = {} + for s in value:gmatch("[^%s]+") do rgb[#rgb + 1] = tonumber(s) end + if #rgb == 1 then rgb[2], rgb[3] = rgb[1], rgb[1] end + if #rgb ~= 3 or not rgb[1] or not rgb[2] or not rgb[3] then return value end + for i = 1,3 do + rgb[i] = math.max(0, math.min(255, math.floor(255 * rgb[i] + 0.5))) + end + return string.format("#%02x%02x%02x", rgb[1], rgb[2], rgb[3]) +end + +local function visual_hex_to_rgb(value) + if #value == 7 and value:sub(1, 1) == "#" then + local r = tonumber(value:sub(2, 3), 16) + local g = tonumber(value:sub(4, 5), 16) + local b = tonumber(value:sub(6, 7), 16) + if r and g and b then + return string.format("%.6g %.6g %.6g", r / 255, g / 255, b / 255) + end + end + return value +end + +local function visual_load_sheet(sheet) + local data = {} + for ci,c in ipairs(visual_style_categories) do + data[ci] = {} + for _,name in ipairs(sheet:allNames(c.kind)) do + data[ci][#data[ci] + 1] = { name=name, value=sheet:find(c.kind, name) } + end + end + return data +end + +local function visual_set_fields(d, st) + local c = visual_style_categories[st.cat] + local entries = st.data[st.cat] + local names = visual_entry_names(entries) + st.updating = true + d:set("items", names) + if #entries == 0 then + st.current = nil + d:set("name", "") + d:set("value", "") + d:set("color", c.default or "#000000") + else + st.current = math.max(1, math.min(st.current or 1, #entries)) + d:set("items", st.current) + d:set("name", entries[st.current].name) + if c.color then + d:set("value", entries[st.current].value) + d:set("color", visual_rgb_to_hex(entries[st.current].value)) + else + d:set("value", entries[st.current].value) + d:set("color", "") + end + end + d:set("value_label", c.color and "Color" or "Value") + d:set("help", c.help) + d:setEnabled("value", not c.color) + d:setEnabled("color", c.color) + st.updating = false +end + +local function visual_apply_current(d, dd, st) + local c = visual_style_categories[st.cat] + local entries = st.data[st.cat] + local name = d:get("name") + if name == "" and not st.current and #entries == 0 then return true end + if name == "" then + dd.model:warning("Cannot update stylesheet", "The symbolic name cannot be empty") + return false + end + local value = c.color and visual_hex_to_rgb(d:get("color")) or d:get("value") + if value == "" then + dd.model:warning("Cannot update stylesheet", "The value cannot be empty") + return false + end + local current = st.current or (#entries + 1) + for i = #entries,1,-1 do + if entries[i].name == name and i ~= current then + table.remove(entries, i) + if i < current then current = current - 1 end + end + end + entries[current] = { name=name, value=value } + st.current = current + st.updating = true + d:set("items", visual_entry_names(entries)) + d:set("items", st.current) + st.updating = false + return true +end + +local function visual_apply_to_sheet(d, dd, st, sheet) + if not visual_apply_current(d, dd, st) then return nil end + local nsheet = sheet:clone() + for ci,c in ipairs(visual_style_categories) do + for _,name in ipairs(nsheet:allNames(c.kind)) do + nsheet:remove(c.kind, name) + end + for _,entry in ipairs(st.data[ci]) do + local ok, msg = pcall(function () + nsheet:setAttribute(c.kind, entry.name, entry.value) + end) + if not ok then + dd.model:warning("Cannot update stylesheet", + string.format("%s '%s': %s", c.label, entry.name, msg)) + return nil + end + end + end + local parsed, msg = ipe.Sheet(nil, nsheet:xml(true)) + if not parsed then + dd.model:warning("Cannot update stylesheet", msg) + return nil + end + return nsheet +end + +local function sheets_visual_edit(d0, dd) + local i = d0:get("list") + if not i or dd.list[i]:isStandard() then return end + + local cats = visual_category_labels() + local st = { cat=1, current=1, data=visual_load_sheet(dd.list[i]) } + cats.action = function (d) + if st.updating then return end + if not visual_apply_current(d, dd, st) then return end + st.cat = d:get("category") + st.current = 1 + visual_set_fields(d, st) + end + local first_names = visual_entry_names(st.data[1]) + first_names.action = function (d) + if st.updating then return end + st.current = d:get("items") + visual_set_fields(d, st) + end + + local d = ipeui.Dialog(dd.model.ui:win(), "Visual stylesheet editor") + d:add("category_label", "label", { label="Category" }, 1, 1) + d:add("category", "combo", cats, 1, 2, 1, 3) + d:add("items", "list", first_names, 2, 1, 7, 2) + d:add("name_label", "label", { label="Name" }, 2, 3) + d:add("name", "input", { select_all=true }, 2, 4) + d:add("value_label", "label", { label="Value" }, 3, 3) + d:add("value", "input", {}, 3, 4) + d:add("color", "input", { color_picker=true }, 4, 4) + d:add("help", "label", { label="" }, 5, 3, 1, 2) + d:add("apply", "button", { label="Apply", + action=function (d) visual_apply_current(d, dd, st) end }, 6, 3) + d:add("add", "button", { label="Add", + action=function (d) + local c = visual_style_categories[st.cat] + local entries = st.data[st.cat] + entries[#entries + 1] = { + name=visual_unique_name(entries, "new"), + value=c.color and visual_hex_to_rgb(c.default) or c.default, + } + st.current = #entries + visual_set_fields(d, st) + end }, 6, 4) + d:add("delete", "button", { label="Delete", + action=function (d) + local entries = st.data[st.cat] + if st.current and entries[st.current] then + table.remove(entries, st.current) + st.current = math.min(st.current, #entries) + visual_set_fields(d, st) + end + end }, 7, 3) + d:addButton("ok", "&Ok", "accept") + d:addButton("cancel", "&Cancel", "reject") + d:setStretch("row", 2, 1) + d:setStretch("column", 2, 1) + d:setStretch("column", 4, 2) + visual_set_fields(d, st) + + if not d:execute({ 640, 420 }) then return end + local nsheet = visual_apply_to_sheet(d, dd, st, dd.list[i]) + if not nsheet then return end + dd.list[i] = nsheet + d0:set("list", sheets_namelist(dd.list)) + d0:set("list", i) + dd.modified = true +end + local function sheets_edit(d, dd) if not prefs.external_editor then dd.model:warning("Cannot edit stylesheet", @@ -2784,6 +3014,8 @@ function MODEL:action_style_sheets() d:add("save", "button", { label="&Save", action=function (d) sheets_save(d, dd) end }, 7, 4) end + d:add("visual", "button", + { label="Visual Edit", action=function (d) sheets_visual_edit(d, dd) end }, 8, 4) d:addButton("ok", "&Ok", "accept") d:addButton("cancel", "&Cancel", "reject") d:setStretch("column", 2, 1) diff --git a/src/ipe/lua/main.lua b/src/ipe/lua/main.lua index 2b3c673..2c3c02b 100644 --- a/src/ipe/lua/main.lua +++ b/src/ipe/lua/main.lua @@ -373,7 +373,8 @@ local home = os.getenv("HOME") local ipeletpath = os.getenv("IPELETPATH") if ipeletpath then config.ipeletDirs = {} - for w in string.gmatch(ipeletpath, prefs.fname_pattern) do + for dir in string.gmatch(ipeletpath, prefs.fname_pattern) do + local w = dir if w == "_" then w = ipe.folder("ipelets") end if w:sub(1,4) == "ipe:" then w = config.ipedrive .. w:sub(5) @@ -389,7 +390,8 @@ end local ipestyles = os.getenv("IPESTYLES") if ipestyles then config.styleDirs = {} - for w in string.gmatch(ipestyles, prefs.fname_pattern) do + for dir in string.gmatch(ipestyles, prefs.fname_pattern) do + local w = dir if w == "_" then w = ipe.folder("styles") end if w:sub(1,4) == "ipe:" then w = config.ipedrive .. w:sub(5) @@ -512,7 +514,8 @@ if config.toolkit == "cocoa" then first_file = nil end if #style_sheets > 0 then prefs.styles = style_sheets end config.styleList = {} -for _,w in ipairs(prefs.styles) do +for _,style in ipairs(prefs.styles) do + local w = style if w:sub(-4) ~= ".isy" then w = w .. ".isy" end if not w:find(prefs.fsep) then w = findStyle(w) end config.styleList[#config.styleList + 1] = w diff --git a/src/ipelua/ipeluastyle.cpp b/src/ipelua/ipeluastyle.cpp index c3129e4..0d293c0 100644 --- a/src/ipelua/ipeluastyle.cpp +++ b/src/ipelua/ipeluastyle.cpp @@ -219,6 +219,77 @@ static int sheet_remove(lua_State * L) { return 0; } +static Attribute check_absolute_string_attribute(Kind kind, lua_State * L, int i) { + size_t len; + const char * data = luaL_checklstring(L, i, &len); + String str(data, len); + Attribute value; + switch (kind) { + case EPen: + case ESymbolSize: + case EArrowSize: + case ETextStretch: + case EGridSize: + case EAngleSize: + case EOpacity: + value = Attribute::makeScalar(str, Attribute::NORMAL()); + luaL_argcheck(L, !value.isSymbolic(), i, "value is not absolute"); + break; + case EColor: + value = Attribute::makeColor(str, Attribute::NORMAL()); + luaL_argcheck(L, value.isColor(), i, "value is not an absolute color"); + break; + case EDashStyle: + value = Attribute::makeDashStyle(str); + luaL_argcheck(L, !value.isSymbolic(), i, "dashstyle is not absolute"); + break; + case ETextSize: + value = Attribute::makeTextSize(str); + luaL_argcheck(L, !value.isSymbolic(), i, "textsize is not absolute"); + break; + default: luaL_argerror(L, 2, "cannot set string value of this kind"); break; + } + return value; +} + +static int sheet_allNames(lua_State * L) { + StyleSheet * s = check_sheet(L, 1)->sheet; + Kind kind = Kind(luaL_checkoption(L, 2, nullptr, kind_names)); + AttributeSeq seq; + s->allNames(kind, seq); + lua_createtable(L, seq.size(), 0); + for (int i = 0; i < size(seq); ++i) { + push_string(L, seq[i].string()); + lua_rawseti(L, -2, i + 1); + } + return 1; +} + +static int sheet_find(lua_State * L) { + StyleSheet * s = check_sheet(L, 1)->sheet; + Kind kind = Kind(luaL_checkoption(L, 2, nullptr, kind_names)); + if (kind == ESymbol || kind == EGradient || kind == ETiling || kind == EEffect) + luaL_argerror(L, 2, "this kind has no simple attribute value"); + const char * name = luaL_checklstring(L, 3, nullptr); + Attribute sym(true, name); + if (!s->has(kind, sym)) { + lua_pushnil(L); + return 1; + } + push_string(L, s->find(kind, sym).string()); + return 1; +} + +static int sheet_setAttribute(lua_State * L) { + StyleSheet * s = check_sheet(L, 1)->sheet; + Kind kind = Kind(luaL_checkoption(L, 2, nullptr, kind_names)); + const char * name = luaL_checklstring(L, 3, nullptr); + Attribute sym(true, name); + Attribute value = check_absolute_string_attribute(kind, L, 4); + s->add(kind, sym, value); + return 0; +} + static int sheet_isStandard(lua_State * L) { SSheet * p = check_sheet(L, 1); lua_pushboolean(L, p->sheet->isStandard()); @@ -282,6 +353,9 @@ static const struct luaL_Reg sheet_methods[] = {{"__gc", sheet_destructor}, {"add", sheet_add}, {"addFrom", sheet_addfrom}, {"remove", sheet_remove}, + {"allNames", sheet_allNames}, + {"find", sheet_find}, + {"setAttribute", sheet_setAttribute}, {"set", sheet_set}, {"isStandard", sheet_isStandard}, {"name", sheet_name}, From 75b72e2e5e03e2bfa4e434d48440951d88f752e1 Mon Sep 17 00:00:00 2001 From: Dmitriy Morozov Date: Sat, 30 May 2026 18:33:49 -0400 Subject: [PATCH 2/8] Add graphical stylesheet previews --- src/ipe-web/src/dialogs.ts | 172 +++++++++++++++++++++++++ src/ipe/lua/actions.lua | 23 +++- src/ipeui/ipeui_cocoa.cpp | 251 ++++++++++++++++++++++++++++++++++++- src/ipeui/ipeui_common.cpp | 21 +++- src/ipeui/ipeui_common.h | 12 +- src/ipeui/ipeui_gtk.cpp | 199 +++++++++++++++++++++++++++++ src/ipeui/ipeui_js.cpp | 3 + src/ipeui/ipeui_qt.cpp | 174 +++++++++++++++++++++++++ src/ipeui/ipeui_win.cpp | 195 ++++++++++++++++++++++++++++ 9 files changed, 1040 insertions(+), 10 deletions(-) diff --git a/src/ipe-web/src/dialogs.ts b/src/ipe-web/src/dialogs.ts index 7621e69..c2e37e2 100644 --- a/src/ipe-web/src/dialogs.ts +++ b/src/ipe-web/src/dialogs.ts @@ -21,6 +21,7 @@ type DialogElement = | "textedit" | "list" | "checkbox" + | "image" | "button"; export interface ElementOptions { @@ -35,6 +36,8 @@ export interface ElementOptions { col: number; rowspan: number; colspan: number; + width: number; + height: number; } interface ButtonOptions { @@ -111,6 +114,9 @@ export function setElement(w: ElementOptions): void { const el = document.getElementById(`dialog-element-${w.name}`); if (el == null) return; switch (w.type) { + case "image": + drawImagePreview(el as HTMLCanvasElement, w.text); + break; case "checkbox": (el as HTMLInputElement).checked = w.value !== 0; break; @@ -132,6 +138,162 @@ export function setElement(w: ElementOptions): void { } } +function previewNumber(value: string, fallback: number): number { + const n = Number.parseFloat(value); + return Number.isFinite(n) ? n : fallback; +} + +function previewColor(value: string): string { + if (value.startsWith("#")) return value; + const rgb = value.trim().split(/\s+/).map(Number.parseFloat); + if (rgb.length === 1) rgb[1] = rgb[2] = rgb[0]; + const [r = 0, g = 0, b = 0] = rgb; + return `rgb(${Math.max(0, Math.min(255, Math.round(255 * r)))}, ${Math.max(0, Math.min(255, Math.round(255 * g)))}, ${Math.max(0, Math.min(255, Math.round(255 * b)))})`; +} + +function previewNamedSize(value: string, fallback: number): number { + const sizes: Record = { + "\\tiny": 8, + "\\scriptsize": 9, + "\\footnotesize": 10, + "\\small": 12, + "\\normalsize": 14, + "\\large": 18, + "\\Large": 22, + "\\LARGE": 26, + "\\huge": 30, + "\\Huge": 36, + }; + return sizes[value] ?? previewNumber(value, fallback); +} + +function previewDashPattern(value: string): number[] { + const m = value.match(/\[([^\]]+)\]/); + if (!m) return []; + return m[1] + .trim() + .split(/\s+/) + .map(Number.parseFloat) + .filter(Number.isFinite); +} + +function drawImagePreview(canvas: HTMLCanvasElement, spec: string): void { + const [kind, value = ""] = spec.split("|", 2); + const ctx = canvas.getContext("2d"); + if (ctx == null) return; + const w = canvas.width; + const h = canvas.height; + ctx.clearRect(0, 0, w, h); + ctx.fillStyle = "rgb(255,255,220)"; + ctx.fillRect(0, 0, w, h); + ctx.strokeStyle = "rgb(160,160,130)"; + ctx.strokeRect(0.5, 0.5, w - 1, h - 1); + const left = 18; + const top = 16; + const right = w - 18; + const bottom = h - 16; + const cx = (left + right) / 2; + const cy = (top + bottom) / 2; + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + if (kind === "color") { + ctx.fillStyle = previewColor(value); + ctx.fillRect(left + 8, top + 8, right - left - 16, bottom - top - 36); + ctx.strokeStyle = "black"; + ctx.strokeRect(left + 8, top + 8, right - left - 16, bottom - top - 36); + ctx.fillStyle = "black"; + ctx.font = "12px sans-serif"; + ctx.textAlign = "center"; + ctx.fillText(value, cx, bottom - 4); + } else if (kind === "pen" || kind === "dashstyle") { + ctx.strokeStyle = "rgb(20,40,160)"; + ctx.lineWidth = + kind === "pen" ? Math.max(0.5, Math.min(24, previewNumber(value, 1))) : 4; + if (kind === "dashstyle") ctx.setLineDash(previewDashPattern(value)); + else ctx.setLineDash([]); + ctx.beginPath(); + ctx.moveTo(left, cy); + ctx.lineTo(right, cy); + ctx.stroke(); + ctx.setLineDash([]); + } else if (kind === "textsize") { + ctx.fillStyle = "rgb(30,30,30)"; + ctx.font = `${Math.max(8, Math.min(48, previewNamedSize(value, 18)))}px sans-serif`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText("Sample", cx, cy); + } else if (kind === "symbolsize") { + const s = Math.max(6, Math.min(42, previewNumber(value, 3) * 3)); + ctx.fillStyle = "rgb(230,80,70)"; + ctx.strokeStyle = "rgb(20,40,160)"; + ctx.lineWidth = 2; + for (const x of [ + left + (right - left) * 0.25, + cx, + left + (right - left) * 0.75, + ]) { + ctx.beginPath(); + ctx.arc(x, cy, s / 2, 0, 2 * Math.PI); + ctx.fill(); + ctx.stroke(); + } + } else if (kind === "arrowsize") { + const s = Math.max(8, Math.min(50, previewNumber(value, 7) * 2)); + ctx.strokeStyle = "rgb(20,40,160)"; + ctx.fillStyle = "rgb(20,40,160)"; + ctx.lineWidth = 4; + ctx.beginPath(); + ctx.moveTo(left, cy); + ctx.lineTo(right - s, cy); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(right, cy); + ctx.lineTo(right - s, cy - 0.45 * s); + ctx.lineTo(right - s, cy + 0.45 * s); + ctx.closePath(); + ctx.fill(); + } else if (kind === "opacity") { + const op = Math.max(0, Math.min(1, previewNumber(value, 1))); + ctx.fillStyle = "rgb(80,120,230)"; + ctx.fillRect(left + 12, top + 10, (right - left) * 0.45, bottom - top - 20); + ctx.fillStyle = `rgba(230,70,50,${op})`; + ctx.fillRect(cx - 12, top + 10, (right - left) * 0.45, bottom - top - 20); + } else if (kind === "gridsize") { + const step = Math.max(6, Math.min(24, previewNumber(value, 8) / 2)); + ctx.strokeStyle = "rgb(170,170,170)"; + ctx.lineWidth = 1; + for (let x = left; x <= right; x += step) { + ctx.beginPath(); + ctx.moveTo(x, top); + ctx.lineTo(x, bottom); + ctx.stroke(); + } + for (let y = top; y <= bottom; y += step) { + ctx.beginPath(); + ctx.moveTo(left, y); + ctx.lineTo(right, y); + ctx.stroke(); + } + } else if (kind === "anglesize") { + const radians = (previewNumber(value, 45) * Math.PI) / 180; + const ox = left + 0.25 * (right - left); + const oy = bottom - 12; + const len = Math.min((right - left) * 0.65, (bottom - top) * 0.9); + ctx.strokeStyle = "rgb(70,70,70)"; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(ox, oy); + ctx.lineTo(right, oy); + ctx.stroke(); + ctx.strokeStyle = "rgb(20,40,160)"; + ctx.lineWidth = 4; + ctx.beginPath(); + ctx.moveTo(ox, oy); + ctx.lineTo(ox + len * Math.cos(radians), oy - len * Math.sin(radians)); + ctx.stroke(); + } +} + export function setupElements( ipe: Ipe, body: HTMLDivElement, @@ -198,6 +360,15 @@ export function setupElements( el = el1; break; } + case "image": { + const el1 = document.createElement("canvas"); + el1.id = `dialog-element-${w.name}`; + el1.width = w.width; + el1.height = w.height; + drawImagePreview(el1, w.text); + el = el1; + break; + } case "list": { const el1 = document.createElement("div"); el1.id = `dialog-element-${w.name}`; @@ -233,6 +404,7 @@ export function setupElements( case "input": case "button": case "combo": + case "image": el.style.justifySelf = "stretch"; el.style.alignSelf = "center"; break; diff --git a/src/ipe/lua/actions.lua b/src/ipe/lua/actions.lua index 2a8aaac..f1f4f07 100644 --- a/src/ipe/lua/actions.lua +++ b/src/ipe/lua/actions.lua @@ -2724,6 +2724,14 @@ local function visual_hex_to_rgb(value) return value end +local function visual_preview_spec(c, value) + return c.kind .. "|" .. (value or "") +end + +local function visual_set_preview(d, c, value) + d:set("preview", visual_preview_spec(c, value)) +end + local function visual_load_sheet(sheet) local data = {} for ci,c in ipairs(visual_style_categories) do @@ -2746,6 +2754,7 @@ local function visual_set_fields(d, st) d:set("name", "") d:set("value", "") d:set("color", c.default or "#000000") + visual_set_preview(d, c, c.color and visual_hex_to_rgb(c.default or "#000000") or c.default) else st.current = math.max(1, math.min(st.current or 1, #entries)) d:set("items", st.current) @@ -2757,6 +2766,7 @@ local function visual_set_fields(d, st) d:set("value", entries[st.current].value) d:set("color", "") end + visual_set_preview(d, c, entries[st.current].value) end d:set("value_label", c.color and "Color" or "Value") d:set("help", c.help) @@ -2791,6 +2801,7 @@ local function visual_apply_current(d, dd, st) st.updating = true d:set("items", visual_entry_names(entries)) d:set("items", st.current) + visual_set_preview(d, c, value) st.updating = false return true end @@ -2851,8 +2862,10 @@ local function sheets_visual_edit(d0, dd) d:add("value", "input", {}, 3, 4) d:add("color", "input", { color_picker=true }, 4, 4) d:add("help", "label", { label="" }, 5, 3, 1, 2) - d:add("apply", "button", { label="Apply", - action=function (d) visual_apply_current(d, dd, st) end }, 6, 3) + d:add("preview_label", "label", { label="Preview" }, 6, 3) + d:add("preview", "image", { width=300, height=130 }, 7, 3, 2, 2) + d:add("apply", "button", { label="Apply / Preview", + action=function (d) visual_apply_current(d, dd, st) end }, 9, 3) d:add("add", "button", { label="Add", action=function (d) local c = visual_style_categories[st.cat] @@ -2863,7 +2876,7 @@ local function sheets_visual_edit(d0, dd) } st.current = #entries visual_set_fields(d, st) - end }, 6, 4) + end }, 9, 4) d:add("delete", "button", { label="Delete", action=function (d) local entries = st.data[st.cat] @@ -2872,7 +2885,7 @@ local function sheets_visual_edit(d0, dd) st.current = math.min(st.current, #entries) visual_set_fields(d, st) end - end }, 7, 3) + end }, 10, 3) d:addButton("ok", "&Ok", "accept") d:addButton("cancel", "&Cancel", "reject") d:setStretch("row", 2, 1) @@ -2880,7 +2893,7 @@ local function sheets_visual_edit(d0, dd) d:setStretch("column", 4, 2) visual_set_fields(d, st) - if not d:execute({ 640, 420 }) then return end + if not d:execute({ 680, 520 }) then return end local nsheet = visual_apply_to_sheet(d, dd, st, dd.list[i]) if not nsheet then return end dd.list[i] = nsheet diff --git a/src/ipeui/ipeui_cocoa.cpp b/src/ipeui/ipeui_cocoa.cpp index ca2ec6c..fead3d1 100644 --- a/src/ipeui/ipeui_cocoa.cpp +++ b/src/ipeui/ipeui_cocoa.cpp @@ -35,6 +35,11 @@ #include "ipeuilayout_cocoa.h" +#include +#include +#include +#include + #define COLORICONSIZE 12 inline const char * N2C(NSString * aStr) { return aStr.UTF8String; } @@ -218,6 +223,239 @@ class PDialog : public Dialog { // -------------------------------------------------------------------- +static double previewNumber(const std::string & value, double fallback) { + std::istringstream stream(value); + double v; + if (stream >> v) return v; + return fallback; +} + +static double previewNamedSize(const std::string & value, double fallback) { + if (value == "\\tiny") return 8.0; + if (value == "\\scriptsize") return 9.0; + if (value == "\\footnotesize") return 10.0; + if (value == "\\small") return 12.0; + if (value == "\\normalsize") return 14.0; + if (value == "\\large") return 18.0; + if (value == "\\Large") return 22.0; + if (value == "\\LARGE") return 26.0; + if (value == "\\huge") return 30.0; + if (value == "\\Huge") return 36.0; + return previewNumber(value, fallback); +} + +static NSColor * previewColor(const std::string & value, double alpha = 1.0) { + if (value.size() == 7 && value[0] == '#') { + unsigned int r = 0, g = 0, b = 0; + sscanf(value.c_str() + 1, "%2x%2x%2x", &r, &g, &b); + return [NSColor colorWithCalibratedRed:r / 255.0 + green:g / 255.0 + blue:b / 255.0 + alpha:alpha]; + } + std::istringstream stream(value); + double r = 0.0, g = 0.0, b = 0.0; + if (stream >> r) { + if (!(stream >> g)) g = r; + if (!(stream >> b)) b = r; + } + return [NSColor colorWithCalibratedRed:std::clamp(r, 0.0, 1.0) + green:std::clamp(g, 0.0, 1.0) + blue:std::clamp(b, 0.0, 1.0) + alpha:alpha]; +} + +static std::vector previewDashPattern(const std::string & value) { + std::vector dashes; + size_t left = value.find('['); + size_t right = value.find(']', left + 1); + if (left == std::string::npos || right == std::string::npos) return dashes; + std::istringstream stream(value.substr(left + 1, right - left - 1)); + double v; + while (stream >> v) dashes.push_back(std::max(0.5, v)); + return dashes; +} + +@interface IpeDialogImage : NSView + +- (instancetype)initWithWidth:(int)width height:(int)height spec:(const std::string &)spec; +- (void)setSpec:(const std::string &)spec; + +@end + +@implementation IpeDialogImage { + std::string iSpec; + int iWidth; + int iHeight; +} + +- (instancetype)initWithWidth:(int)width height:(int)height spec:(const std::string &)spec { + self = [super initWithFrame:NSMakeRect(0., 0., width, height)]; + if (self) { + iWidth = width; + iHeight = height; + iSpec = spec; + } + return self; +} + +- (BOOL)isFlipped { return YES; } + +- (NSSize)intrinsicContentSize { return NSMakeSize(iWidth, iHeight); } + +- (void)setSpec:(const std::string &)spec { + iSpec = spec; + [self setNeedsDisplay:YES]; +} + +- (void)drawRect:(NSRect)dirtyRect { + (void)dirtyRect; + NSRect bounds = [self bounds]; + [[NSColor colorWithCalibratedRed:1.0 green:1.0 blue:0.86 alpha:1.0] setFill]; + NSRectFill(bounds); + [[NSColor colorWithCalibratedRed:0.62 green:0.62 blue:0.50 alpha:1.0] setStroke]; + NSFrameRect(bounds); + + size_t sep = iSpec.find('|'); + std::string kind = sep == std::string::npos ? iSpec : iSpec.substr(0, sep); + std::string value = sep == std::string::npos ? std::string() : iSpec.substr(sep + 1); + NSRect body = NSInsetRect(bounds, 18., 16.); + + if (kind == "color") { + NSRect swatch = NSInsetRect(body, 8., 8.); + swatch.size.height -= 28.; + [previewColor(value) setFill]; + NSRectFill(swatch); + [[NSColor blackColor] setStroke]; + NSFrameRect(swatch); + NSDictionary * attrs = @{NSFontAttributeName : [NSFont systemFontOfSize:12.]}; + [S2N(value) drawInRect:NSMakeRect(body.origin.x, NSMaxY(body) - 20., + body.size.width, 18.) + withAttributes:attrs]; + } else if (kind == "pen" || kind == "dashstyle") { + NSBezierPath * path = [NSBezierPath bezierPath]; + [path moveToPoint:NSMakePoint(NSMinX(body), NSMidY(body))]; + [path lineToPoint:NSMakePoint(NSMaxX(body), NSMidY(body))]; + [path setLineWidth:(kind == "pen") + ? std::clamp(previewNumber(value, 1.0), 0.5, 24.0) + : 4.0]; + if (kind == "dashstyle") { + std::vector dashes = previewDashPattern(value); + if (!dashes.empty()) + [path setLineDash:dashes.data() count:(NSInteger)dashes.size() phase:0.0]; + } + [[NSColor colorWithCalibratedRed:0.08 green:0.16 blue:0.63 alpha:1.0] setStroke]; + [path stroke]; + } else if (kind == "textsize") { + double size = std::clamp(previewNamedSize(value, 18.0), 8.0, 48.0); + NSDictionary * attrs = @{ + NSFontAttributeName : [NSFont systemFontOfSize:size], + NSForegroundColorAttributeName : [NSColor textColor] + }; + [@"Sample" drawInRect:body withAttributes:attrs]; + } else if (kind == "symbolsize") { + double s = std::clamp(previewNumber(value, 3.0) * 3.0, 6.0, 42.0); + NSArray * centers = @[ + [NSValue valueWithPoint:NSMakePoint(NSMinX(body) + body.size.width * 0.25, + NSMidY(body))], + [NSValue valueWithPoint:NSMakePoint(NSMidX(body), NSMidY(body))], + [NSValue valueWithPoint:NSMakePoint(NSMinX(body) + body.size.width * 0.75, + NSMidY(body))] + ]; + [[NSColor colorWithCalibratedRed:0.90 green:0.31 blue:0.27 alpha:1.0] setFill]; + [[NSColor colorWithCalibratedRed:0.08 green:0.16 blue:0.63 alpha:1.0] setStroke]; + NSPoint c = [(NSValue *)[centers objectAtIndex:0] pointValue]; + [[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(c.x - s / 2., c.y - s / 2., s, s)] fill]; + [[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(c.x - s / 2., c.y - s / 2., s, s)] stroke]; + c = [(NSValue *)[centers objectAtIndex:1] pointValue]; + [[NSBezierPath bezierPathWithRect:NSMakeRect(c.x - s / 2., c.y - s / 2., s, s)] fill]; + [[NSBezierPath bezierPathWithRect:NSMakeRect(c.x - s / 2., c.y - s / 2., s, s)] stroke]; + c = [(NSValue *)[centers objectAtIndex:2] pointValue]; + NSBezierPath * diamond = [NSBezierPath bezierPath]; + [diamond moveToPoint:NSMakePoint(c.x, c.y - s / 2.)]; + [diamond lineToPoint:NSMakePoint(c.x + s / 2., c.y)]; + [diamond lineToPoint:NSMakePoint(c.x, c.y + s / 2.)]; + [diamond lineToPoint:NSMakePoint(c.x - s / 2., c.y)]; + [diamond closePath]; + [diamond fill]; + [diamond stroke]; + } else if (kind == "arrowsize") { + double s = std::clamp(previewNumber(value, 7.0) * 2.0, 8.0, 50.0); + NSPoint a = NSMakePoint(NSMinX(body), NSMidY(body)); + NSPoint b = NSMakePoint(NSMaxX(body) - s, NSMidY(body)); + NSBezierPath * line = [NSBezierPath bezierPath]; + [line moveToPoint:a]; + [line lineToPoint:b]; + [line setLineWidth:4.0]; + [[NSColor colorWithCalibratedRed:0.08 green:0.16 blue:0.63 alpha:1.0] setStroke]; + [line stroke]; + NSBezierPath * arrow = [NSBezierPath bezierPath]; + [arrow moveToPoint:NSMakePoint(b.x + s, b.y)]; + [arrow lineToPoint:NSMakePoint(b.x, b.y - 0.45 * s)]; + [arrow lineToPoint:NSMakePoint(b.x, b.y + 0.45 * s)]; + [arrow closePath]; + [[NSColor colorWithCalibratedRed:0.08 green:0.16 blue:0.63 alpha:1.0] setFill]; + [arrow fill]; + } else if (kind == "opacity") { + double op = std::clamp(previewNumber(value, 1.0), 0.0, 1.0); + NSRect left = NSMakeRect(NSMinX(body) + 12., NSMinY(body) + 10., + body.size.width * 0.45, body.size.height - 20.); + NSRect right = NSMakeRect(NSMidX(body) - 12., NSMinY(body) + 10., + body.size.width * 0.45, body.size.height - 20.); + [[NSColor colorWithCalibratedRed:0.31 green:0.47 blue:0.90 alpha:1.0] setFill]; + NSRectFill(left); + [[NSColor colorWithCalibratedRed:0.90 green:0.27 blue:0.20 alpha:op] setFill]; + NSRectFillUsingOperation(right, NSCompositingOperationSourceOver); + [[NSColor blackColor] setStroke]; + NSFrameRect(left); + NSFrameRect(right); + } else if (kind == "gridsize") { + double n = std::clamp(previewNumber(value, 8.0), 2.0, 64.0); + double step = std::clamp(n / 2.0, 6.0, 24.0); + [[NSColor colorWithCalibratedWhite:0.65 alpha:1.0] setStroke]; + for (double x = NSMinX(body); x <= NSMaxX(body); x += step) { + NSBezierPath * p = [NSBezierPath bezierPath]; + [p moveToPoint:NSMakePoint(x, NSMinY(body))]; + [p lineToPoint:NSMakePoint(x, NSMaxY(body))]; + [p stroke]; + } + for (double y = NSMinY(body); y <= NSMaxY(body); y += step) { + NSBezierPath * p = [NSBezierPath bezierPath]; + [p moveToPoint:NSMakePoint(NSMinX(body), y)]; + [p lineToPoint:NSMakePoint(NSMaxX(body), y)]; + [p stroke]; + } + NSBezierPath * diag = [NSBezierPath bezierPath]; + [diag moveToPoint:NSMakePoint(NSMinX(body), NSMaxY(body))]; + [diag lineToPoint:NSMakePoint(NSMaxX(body), NSMinY(body))]; + [diag setLineWidth:3.0]; + [[NSColor colorWithCalibratedRed:0.08 green:0.16 blue:0.63 alpha:1.0] setStroke]; + [diag stroke]; + } else if (kind == "anglesize") { + double degrees = previewNumber(value, 45.0); + double radians = degrees * 3.14159265358979323846 / 180.0; + NSPoint o = NSMakePoint(NSMinX(body) + 0.25 * body.size.width, NSMaxY(body) - 12.); + double len = std::min(body.size.width * 0.65, body.size.height * 0.9); + NSPoint p = NSMakePoint(o.x + len * std::cos(radians), o.y - len * std::sin(radians)); + NSBezierPath * base = [NSBezierPath bezierPath]; + [base moveToPoint:o]; + [base lineToPoint:NSMakePoint(NSMaxX(body), o.y)]; + [base setLineWidth:2.0]; + [[NSColor darkGrayColor] setStroke]; + [base stroke]; + NSBezierPath * ray = [NSBezierPath bezierPath]; + [ray moveToPoint:o]; + [ray lineToPoint:p]; + [ray setLineWidth:4.0]; + [[NSColor colorWithCalibratedRed:0.08 green:0.16 blue:0.63 alpha:1.0] setStroke]; + [ray stroke]; + } +} + +@end + +// -------------------------------------------------------------------- + PDialog::PDialog(lua_State * L0, WINID parent, const char * caption, const char * language) : Dialog(L0, parent, caption, language) { @@ -257,6 +495,7 @@ void PDialog::setMapped(lua_State * L, int idx) { case ELabel: case EInput: [((NSTextField *)ctrl) setStringValue:S2N(m.text)]; break; case ETextEdit: setTextView((NSTextView *)ctrl, m.text); break; + case EImage: [(IpeDialogImage *)ctrl setSpec:m.text]; break; case ECheckBox: [((NSButton *)ctrl) setState:m.value]; break; case EList: // listbox gets items directly from items array @@ -289,7 +528,8 @@ void PDialog::retrieveValues() { } void PDialog::enableItem(int idx, bool value) { - if (iElements[idx].type != ETextEdit) [((NSControl *)iViews[idx]) setEnabled:value]; + if (iElements[idx].type != ETextEdit && iElements[idx].type != EImage) + [((NSControl *)iViews[idx]) setEnabled:value]; } void PDialog::fillComboBox(NSPopUpButton * cb, int idx) { @@ -386,6 +626,10 @@ void PDialog::layoutControls() { layout(w, cols[m.col], "l=l"); layout(w, cols[m.col + m.colspan - 1], "r=r"); if (m.type == EInput || m.type == ETextEdit) layout(w, nil, "w>0", 100); + if (m.type == EImage) { + layout(w, nil, "w>0", m.minWidth); + layout(w, nil, "h>0", m.minHeight); + } // does it have stretch? BOOL rowStretch = NO; for (int r = m.row; r < m.row + m.rowspan; ++r) @@ -480,6 +724,11 @@ Dialog::Result PDialog::buildAndRun(int w, int h) { if (m.flags & ESelectAll) [t selectText:content]; ctrl = t; } break; + case EImage: { + view = [[IpeDialogImage alloc] initWithWidth:m.minWidth + height:m.minHeight + spec:m.text]; + } break; case ETextEdit: { scroll = [[NSScrollView alloc] initWithFrame:NSZeroRect]; NSTextView * tv = [[NSTextView alloc] initWithFrame:NSZeroRect]; diff --git a/src/ipeui/ipeui_common.cpp b/src/ipeui/ipeui_common.cpp index c9aa10c..d7ecfce 100644 --- a/src/ipeui/ipeui_common.cpp +++ b/src/ipeui/ipeui_common.cpp @@ -117,8 +117,9 @@ int Dialog::addButton(lua_State * L) { } int Dialog::add(lua_State * L) { - static const char * const typenames[] = {"button", "text", "list", "label", - "combo", "checkbox", "input", nullptr}; + static const char * const typenames[] = {"button", "text", "list", + "label", "combo", "checkbox", + "input", "image", nullptr}; SElement m; m.name = checkstring(L, 2); @@ -144,6 +145,7 @@ int Dialog::add(lua_State * L) { case ECombo: addCombo(L, m); break; case ECheckBox: addCheckbox(L, m); break; case EInput: addInput(L, m); break; + case EImage: addImage(L, m); break; default: break; } iElements.push_back(m); @@ -223,6 +225,18 @@ void Dialog::addInput(lua_State * L, SElement & m) { lua_pop(L, 3); } +void Dialog::addImage(lua_State * L, SElement & m) { + m.minWidth = 180; + m.minHeight = 80; + lua_getfield(L, 4, "value"); + if (lua_isstring(L, -1)) m.text = tostring(L, -1); + lua_getfield(L, 4, "width"); + if (lua_isnumber(L, -1)) m.minWidth = luaL_checkinteger(L, -1); + lua_getfield(L, 4, "height"); + if (lua_isnumber(L, -1)) m.minHeight = luaL_checkinteger(L, -1); + lua_pop(L, 3); // height, width, value +} + void Dialog::addTextEdit(lua_State * L, SElement & m) { lua_getfield(L, 4, "read_only"); if (lua_toboolean(L, -1)) m.flags |= EReadOnly; @@ -313,7 +327,8 @@ void Dialog::setUnmapped(lua_State * L, int idx) { switch (m.type) { case ELabel: case ETextEdit: - case EInput: m.text = checkstring(L, 3); break; + case EInput: + case EImage: m.text = checkstring(L, 3); break; case EList: case ECombo: if (lua_isnumber(L, 3)) { diff --git a/src/ipeui/ipeui_common.h b/src/ipeui/ipeui_common.h index 76b78f6..e5624e0 100644 --- a/src/ipeui/ipeui_common.h +++ b/src/ipeui/ipeui_common.h @@ -127,7 +127,16 @@ class Dialog { ESpellCheck = 0x200, EColorPicker = 0x400, }; - enum TType { EButton = 0, ETextEdit, EList, ELabel, ECombo, ECheckBox, EInput }; + enum TType { + EButton = 0, + ETextEdit, + EList, + ELabel, + ECombo, + ECheckBox, + EInput, + EImage + }; struct SElement { std::string name; @@ -158,6 +167,7 @@ class Dialog { void addCombo(lua_State * L, SElement & m); void addCheckbox(lua_State * L, SElement & m); void addInput(lua_State * L, SElement & m); + void addImage(lua_State * L, SElement & m); void setListItems(lua_State * L, int index, SElement & m); diff --git a/src/ipeui/ipeui_gtk.cpp b/src/ipeui/ipeui_gtk.cpp index 1c6d5bd..6a2bddb 100644 --- a/src/ipeui/ipeui_gtk.cpp +++ b/src/ipeui/ipeui_gtk.cpp @@ -29,6 +29,11 @@ */ #include "ipeui_common.h" + +#include +#include +#include + using String = std::string; // -------------------------------------------------------------------- @@ -64,6 +69,186 @@ PDialog::~PDialog() { // } +static double previewNumber(const std::string & value, double fallback) { + std::istringstream stream(value); + double v; + if (stream >> v) return v; + return fallback; +} + +static double previewNamedSize(const std::string & value, double fallback) { + if (value == "\\tiny") return 8.0; + if (value == "\\scriptsize") return 9.0; + if (value == "\\footnotesize") return 10.0; + if (value == "\\small") return 12.0; + if (value == "\\normalsize") return 14.0; + if (value == "\\large") return 18.0; + if (value == "\\Large") return 22.0; + if (value == "\\LARGE") return 26.0; + if (value == "\\huge") return 30.0; + if (value == "\\Huge") return 36.0; + return previewNumber(value, fallback); +} + +static void previewColor(const std::string & value, double & r, double & g, double & b) { + if (value.size() == 7 && value[0] == '#') { + unsigned int rr = 0, gg = 0, bb = 0; + sscanf(value.c_str() + 1, "%2x%2x%2x", &rr, &gg, &bb); + r = rr / 255.0; + g = gg / 255.0; + b = bb / 255.0; + return; + } + std::istringstream stream(value); + r = g = b = 0.0; + if (stream >> r) { + if (!(stream >> g)) g = r; + if (!(stream >> b)) b = r; + } + r = std::clamp(r, 0.0, 1.0); + g = std::clamp(g, 0.0, 1.0); + b = std::clamp(b, 0.0, 1.0); +} + +static std::vector previewDashPattern(const std::string & value) { + std::vector dashes; + size_t left = value.find('['); + size_t right = value.find(']', left + 1); + if (left == std::string::npos || right == std::string::npos) return dashes; + std::istringstream stream(value.substr(left + 1, right - left - 1)); + double v; + while (stream >> v) dashes.push_back(std::max(0.5, v)); + return dashes; +} + +static void drawImagePreview(cairo_t * cr, int width, int height, const std::string & spec) { + size_t sep = spec.find('|'); + std::string kind = sep == std::string::npos ? spec : spec.substr(0, sep); + std::string value = sep == std::string::npos ? std::string() : spec.substr(sep + 1); + + cairo_set_source_rgb(cr, 1.0, 1.0, 0.86); + cairo_rectangle(cr, 0.5, 0.5, width - 1.0, height - 1.0); + cairo_fill_preserve(cr); + cairo_set_source_rgb(cr, 0.62, 0.62, 0.50); + cairo_set_line_width(cr, 1.0); + cairo_stroke(cr); + + double left = 18.0, top = 16.0, right = width - 18.0, bottom = height - 16.0; + double cx = 0.5 * (left + right); + double cy = 0.5 * (top + bottom); + cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); + cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND); + + if (kind == "color") { + double r, g, b; + previewColor(value, r, g, b); + cairo_set_source_rgb(cr, r, g, b); + cairo_rectangle(cr, left + 8.0, top + 8.0, right - left - 16.0, bottom - top - 36.0); + cairo_fill_preserve(cr); + cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); + cairo_stroke(cr); + cairo_select_font_face(cr, "sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); + cairo_set_font_size(cr, 12.0); + cairo_text_extents_t ext; + cairo_text_extents(cr, value.c_str(), &ext); + cairo_move_to(cr, cx - ext.width / 2.0 - ext.x_bearing, bottom - 5.0); + cairo_show_text(cr, value.c_str()); + } else if (kind == "pen" || kind == "dashstyle") { + cairo_set_source_rgb(cr, 0.08, 0.16, 0.63); + cairo_set_line_width(cr, kind == "pen" ? std::clamp(previewNumber(value, 1.0), 0.5, 24.0) : 4.0); + std::vector dashes = previewDashPattern(value); + if (kind == "dashstyle" && !dashes.empty()) cairo_set_dash(cr, dashes.data(), dashes.size(), 0.0); + cairo_move_to(cr, left, cy); + cairo_line_to(cr, right, cy); + cairo_stroke(cr); + cairo_set_dash(cr, nullptr, 0, 0.0); + } else if (kind == "textsize") { + double size = std::clamp(previewNamedSize(value, 18.0), 8.0, 48.0); + cairo_select_font_face(cr, "sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); + cairo_set_font_size(cr, size); + cairo_set_source_rgb(cr, 0.12, 0.12, 0.12); + cairo_text_extents_t ext; + cairo_text_extents(cr, "Sample", &ext); + cairo_move_to(cr, cx - ext.width / 2.0 - ext.x_bearing, cy - ext.height / 2.0 - ext.y_bearing); + cairo_show_text(cr, "Sample"); + } else if (kind == "symbolsize") { + double s = std::clamp(previewNumber(value, 3.0) * 3.0, 6.0, 42.0); + double xs[] = {left + (right - left) * 0.25, cx, left + (right - left) * 0.75}; + cairo_set_line_width(cr, 2.0); + cairo_set_source_rgb(cr, 0.90, 0.31, 0.27); + for (double x : xs) { + cairo_arc(cr, x, cy, s / 2.0, 0.0, 2.0 * 3.14159265358979323846); + cairo_fill_preserve(cr); + cairo_set_source_rgb(cr, 0.08, 0.16, 0.63); + cairo_stroke(cr); + cairo_set_source_rgb(cr, 0.90, 0.31, 0.27); + } + } else if (kind == "arrowsize") { + double s = std::clamp(previewNumber(value, 7.0) * 2.0, 8.0, 50.0); + cairo_set_source_rgb(cr, 0.08, 0.16, 0.63); + cairo_set_line_width(cr, 4.0); + cairo_move_to(cr, left, cy); + cairo_line_to(cr, right - s, cy); + cairo_stroke(cr); + cairo_move_to(cr, right, cy); + cairo_line_to(cr, right - s, cy - 0.45 * s); + cairo_line_to(cr, right - s, cy + 0.45 * s); + cairo_close_path(cr); + cairo_fill(cr); + } else if (kind == "opacity") { + double op = std::clamp(previewNumber(value, 1.0), 0.0, 1.0); + cairo_set_source_rgb(cr, 0.31, 0.47, 0.90); + cairo_rectangle(cr, left + 12.0, top + 10.0, (right - left) * 0.45, bottom - top - 20.0); + cairo_fill(cr); + cairo_set_source_rgba(cr, 0.90, 0.27, 0.20, op); + cairo_rectangle(cr, cx - 12.0, top + 10.0, (right - left) * 0.45, bottom - top - 20.0); + cairo_fill(cr); + } else if (kind == "gridsize") { + double step = std::clamp(previewNumber(value, 8.0) / 2.0, 6.0, 24.0); + cairo_set_source_rgb(cr, 0.67, 0.67, 0.67); + cairo_set_line_width(cr, 1.0); + for (double x = left; x <= right; x += step) { + cairo_move_to(cr, x, top); + cairo_line_to(cr, x, bottom); + } + for (double y = top; y <= bottom; y += step) { + cairo_move_to(cr, left, y); + cairo_line_to(cr, right, y); + } + cairo_stroke(cr); + cairo_set_source_rgb(cr, 0.08, 0.16, 0.63); + cairo_set_line_width(cr, 3.0); + cairo_move_to(cr, left, bottom); + cairo_line_to(cr, right, top); + cairo_stroke(cr); + } else if (kind == "anglesize") { + double radians = previewNumber(value, 45.0) * 3.14159265358979323846 / 180.0; + double ox = left + 0.25 * (right - left); + double oy = bottom - 12.0; + double len = std::min((right - left) * 0.65, (bottom - top) * 0.9); + cairo_set_source_rgb(cr, 0.27, 0.27, 0.27); + cairo_set_line_width(cr, 2.0); + cairo_move_to(cr, ox, oy); + cairo_line_to(cr, right, oy); + cairo_stroke(cr); + cairo_set_source_rgb(cr, 0.08, 0.16, 0.63); + cairo_set_line_width(cr, 4.0); + cairo_move_to(cr, ox, oy); + cairo_line_to(cr, ox + len * std::cos(radians), oy - len * std::sin(radians)); + cairo_stroke(cr); + } +} + +static gboolean imageExpose(GtkWidget * widget, GdkEventExpose *, gpointer) { + GtkAllocation allocation; + gtk_widget_get_allocation(widget, &allocation); + const char * spec = (const char *)g_object_get_data(G_OBJECT(widget), "ipe-dialog-image-spec"); + cairo_t * cr = gdk_cairo_create(gtk_widget_get_window(widget)); + drawImagePreview(cr, allocation.width, allocation.height, spec ? spec : ""); + cairo_destroy(cr); + return FALSE; +} + void PDialog::acceptDialog(lua_State * L) { int accept = lua_toboolean(L, 2); (void)accept; // TODO @@ -90,6 +275,11 @@ void PDialog::setMapped(lua_State * L, int idx) { m.text.c_str(), -1); break; case EInput: gtk_entry_set_text(GTK_ENTRY(w), m.text.c_str()); break; + case EImage: + g_object_set_data_full(G_OBJECT(w), "ipe-dialog-image-spec", + g_strdup(m.text.c_str()), g_free); + gtk_widget_queue_draw(w); + break; case EList: if (lua_istable(L, 3)) { GtkTreeModel * mod = gtk_tree_view_get_model(GTK_TREE_VIEW(w)); @@ -272,6 +462,15 @@ Dialog::Result PDialog::buildAndRun(int w, int h) { gtk_entry_set_activates_default(GTK_ENTRY(w), TRUE); xOptions |= GTK_FILL; break; + case EImage: + w = gtk_drawing_area_new(); + gtk_widget_set_size_request(w, m.minWidth, m.minHeight); + g_object_set_data_full(G_OBJECT(w), "ipe-dialog-image-spec", + g_strdup(m.text.c_str()), g_free); + g_signal_connect(w, "expose-event", G_CALLBACK(imageExpose), nullptr); + xOptions |= GTK_FILL; + yOptions |= GTK_FILL; + break; case ETextEdit: w = gtk_text_view_new(); gtk_text_view_set_editable(GTK_TEXT_VIEW(w), !(m.flags & EReadOnly)); diff --git a/src/ipeui/ipeui_js.cpp b/src/ipeui/ipeui_js.cpp index 67a7436..52efccc 100644 --- a/src/ipeui/ipeui_js.cpp +++ b/src/ipeui/ipeui_js.cpp @@ -64,6 +64,7 @@ class PDialog : public Dialog { static const char * typenames[] = { "button", "textedit", "list", "label", "combo", "checkbox", "input", + "image", }; val PDialog::element(const SElement & m) { @@ -84,6 +85,8 @@ val PDialog::element(const SElement & m) { w.set("col", m.col); w.set("rowspan", m.rowspan); w.set("colspan", m.colspan); + w.set("width", m.minWidth); + w.set("height", m.minHeight); return w; } diff --git a/src/ipeui/ipeui_qt.cpp b/src/ipeui/ipeui_qt.cpp index bfb9332..e06be94 100644 --- a/src/ipeui/ipeui_qt.cpp +++ b/src/ipeui/ipeui_qt.cpp @@ -45,6 +45,8 @@ #include #include #include +#include +#include #include #include #include @@ -55,6 +57,9 @@ #include #include #include +#include +#include +#include #ifdef IPE_SPELLCHECK #pragma GCC diagnostic push @@ -135,6 +140,167 @@ class LatexHighlighter : public QSyntaxHighlighter { virtual void highlightBlock(const QString & text); }; +// -------------------------------------------------------------------- + +static double previewNumber(const QString & value, double fallback) { + std::string s = value.toStdString(); + std::istringstream stream(s); + double v; + if (stream >> v) return v; + return fallback; +} + +static double previewNamedSize(const QString & value, double fallback) { + if (value == QLatin1String("\\tiny")) return 3.0; + if (value == QLatin1String("\\scriptsize")) return 4.0; + if (value == QLatin1String("\\footnotesize")) return 5.0; + if (value == QLatin1String("\\small")) return 6.0; + if (value == QLatin1String("\\normalsize")) return 7.0; + if (value == QLatin1String("\\large")) return 9.0; + if (value == QLatin1String("\\Large")) return 11.0; + if (value == QLatin1String("\\LARGE")) return 13.0; + if (value == QLatin1String("\\huge")) return 15.0; + if (value == QLatin1String("\\Huge")) return 18.0; + return previewNumber(value, fallback); +} + +static QColor previewColor(const QString & value) { + if (value.startsWith(QLatin1Char('#'))) return QColor(value); + std::istringstream stream(value.toStdString()); + double r = 0.0, g = 0.0, b = 0.0; + if (stream >> r) { + if (!(stream >> g)) g = r; + if (!(stream >> b)) b = r; + } + return QColor::fromRgbF(std::clamp(r, 0.0, 1.0), std::clamp(g, 0.0, 1.0), + std::clamp(b, 0.0, 1.0)); +} + +static QVector previewDashPattern(const QString & value) { + QVector dashes; + int left = value.indexOf(QLatin1Char('[')); + int right = value.indexOf(QLatin1Char(']'), left + 1); + if (left < 0 || right < 0) return dashes; + std::istringstream stream(value.mid(left + 1, right - left - 1).toStdString()); + double v; + while (stream >> v) dashes.push_back(std::max(0.1, v / 4.0)); + return dashes; +} + +class DialogImage : public QWidget { +public: + DialogImage(int width, int height, QWidget * parent = nullptr) + : QWidget(parent) { + setMinimumSize(width, height); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + } + + void setSpec(const std::string & spec) { + iSpec = QString::fromUtf8(spec.c_str()); + update(); + } + +protected: + void paintEvent(QPaintEvent *) override; + +private: + QString iSpec; +}; + +void DialogImage::paintEvent(QPaintEvent *) { + QString kind = iSpec.section(QLatin1Char('|'), 0, 0); + QString value = iSpec.section(QLatin1Char('|'), 1); + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + QRectF r = rect().adjusted(0.5, 0.5, -0.5, -0.5); + painter.fillRect(r, QColor(255, 255, 220)); + painter.setPen(QPen(QColor(160, 160, 130), 1)); + painter.drawRect(r); + + QRectF body = r.adjusted(18, 16, -18, -16); + if (kind == QLatin1String("color")) { + QColor c = previewColor(value); + painter.fillRect(body.adjusted(8, 8, -8, -28), c); + painter.setPen(Qt::black); + painter.drawRect(body.adjusted(8, 8, -8, -28)); + painter.drawText(body.adjusted(8, body.height() - 18, -8, 0), + Qt::AlignCenter, value); + } else if (kind == QLatin1String("pen") || kind == QLatin1String("dashstyle")) { + QPen pen(QColor(20, 40, 160), + std::clamp(previewNumber(value, 1.0), 0.5, 24.0), Qt::SolidLine, + Qt::RoundCap, Qt::RoundJoin); + if (kind == QLatin1String("dashstyle")) { + pen.setWidthF(4.0); + QVector dashes = previewDashPattern(value); + if (!dashes.isEmpty()) pen.setDashPattern(dashes); + } + painter.setPen(pen); + double y = body.center().y(); + painter.drawLine(QPointF(body.left(), y), QPointF(body.right(), y)); + } else if (kind == QLatin1String("textsize")) { + QFont font = painter.font(); + font.setPointSizeF(std::clamp(previewNamedSize(value, 10.0), 4.0, 36.0)); + painter.setFont(font); + painter.setPen(QColor(30, 30, 30)); + painter.drawText(body, Qt::AlignCenter, QStringLiteral("Sample")); + } else if (kind == QLatin1String("symbolsize")) { + double s = std::clamp(previewNumber(value, 3.0) * 3.0, 6.0, 42.0); + painter.setPen(QPen(QColor(20, 40, 160), 2)); + painter.setBrush(QColor(230, 80, 70)); + QPointF c1(body.left() + body.width() * 0.25, body.center().y()); + QPointF c2(body.center().x(), body.center().y()); + QPointF c3(body.left() + body.width() * 0.75, body.center().y()); + painter.drawEllipse(c1, s / 2, s / 2); + painter.drawRect(QRectF(c2.x() - s / 2, c2.y() - s / 2, s, s)); + QPolygonF diamond; + diamond << QPointF(c3.x(), c3.y() - s / 2) << QPointF(c3.x() + s / 2, c3.y()) + << QPointF(c3.x(), c3.y() + s / 2) << QPointF(c3.x() - s / 2, c3.y()); + painter.drawPolygon(diamond); + } else if (kind == QLatin1String("arrowsize")) { + double s = std::clamp(previewNumber(value, 7.0) * 2.0, 8.0, 50.0); + QPointF a(body.left(), body.center().y()); + QPointF b(body.right() - s, body.center().y()); + painter.setPen(QPen(QColor(20, 40, 160), 4, Qt::SolidLine, Qt::RoundCap)); + painter.drawLine(a, b); + QPolygonF arrow; + arrow << QPointF(b.x() + s, b.y()) << QPointF(b.x(), b.y() - 0.45 * s) + << QPointF(b.x(), b.y() + 0.45 * s); + painter.setBrush(QColor(20, 40, 160)); + painter.drawPolygon(arrow); + } else if (kind == QLatin1String("opacity")) { + double op = std::clamp(previewNumber(value, 1.0), 0.0, 1.0); + QRectF left(body.left() + 12, body.top() + 10, body.width() * 0.45, body.height() - 20); + QRectF right(body.center().x() - 12, body.top() + 10, body.width() * 0.45, + body.height() - 20); + painter.fillRect(left, QColor(80, 120, 230)); + painter.fillRect(right, QColor(230, 70, 50, int(255 * op + 0.5))); + painter.setPen(Qt::black); + painter.drawRect(left); + painter.drawRect(right); + } else if (kind == QLatin1String("gridsize")) { + double n = std::clamp(previewNumber(value, 8.0), 2.0, 64.0); + double step = std::clamp(n / 2.0, 6.0, 24.0); + painter.setPen(QPen(QColor(170, 170, 170), 1)); + for (double x = body.left(); x <= body.right(); x += step) + painter.drawLine(QPointF(x, body.top()), QPointF(x, body.bottom())); + for (double y = body.top(); y <= body.bottom(); y += step) + painter.drawLine(QPointF(body.left(), y), QPointF(body.right(), y)); + painter.setPen(QPen(QColor(20, 40, 160), 3)); + painter.drawLine(body.bottomLeft(), body.topRight()); + } else if (kind == QLatin1String("anglesize")) { + double degrees = previewNumber(value, 45.0); + double radians = degrees * 3.14159265358979323846 / 180.0; + QPointF o(body.left() + 0.25 * body.width(), body.bottom() - 12); + double len = std::min(body.width() * 0.65, body.height() * 0.9); + QPointF p(o.x() + len * std::cos(radians), o.y() - len * std::sin(radians)); + painter.setPen(QPen(QColor(70, 70, 70), 2)); + painter.drawLine(o, QPointF(body.right(), o.y())); + painter.setPen(QPen(QColor(20, 40, 160), 4, Qt::SolidLine, Qt::RoundCap)); + painter.drawLine(o, p); + } +} + void LatexHighlighter::applyFormat(const QString & text, QRegularExpression & exp, const QTextCharFormat & format) { QRegularExpressionMatch match; @@ -282,6 +448,9 @@ void PDialog::setMapped(lua_State * L, int idx) { case EInput: (qobject_cast(w))->setText(QString::fromUtf8(m.text.c_str())); break; + case EImage: + (qobject_cast(w))->setSpec(m.text); + break; case EList: { QListWidget * l = qobject_cast(w); if (!lua_isnumber(L, 3)) { @@ -376,6 +545,11 @@ Dialog::Result PDialog::buildAndRun(int w, int h) { if (m.flags & ESelectAll) t->selectAll(); w = t; } break; + case EImage: { + DialogImage * image = new DialogImage(m.minWidth, m.minHeight, qDialog); + image->setSpec(m.text); + w = image; + } break; case ECombo: { QComboBox * b = new QComboBox(qDialog); for (int k = 0; k < int(m.items.size()); ++k) diff --git a/src/ipeui/ipeui_win.cpp b/src/ipeui/ipeui_win.cpp index f4f235d..54f6801 100644 --- a/src/ipeui/ipeui_win.cpp +++ b/src/ipeui/ipeui_win.cpp @@ -33,6 +33,10 @@ #include +#include +#include +#include + // -------------------------------------------------------------------- #define IDBASE 9000 @@ -76,6 +80,181 @@ static std::string wideToUtf8(const wchar_t * wbuf) { return std::string(multi.data()); } +static double previewNumber(const std::string & value, double fallback) { + std::istringstream stream(value); + double v; + if (stream >> v) return v; + return fallback; +} + +static double previewNamedSize(const std::string & value, double fallback) { + if (value == "\\tiny") return 8.0; + if (value == "\\scriptsize") return 9.0; + if (value == "\\footnotesize") return 10.0; + if (value == "\\small") return 12.0; + if (value == "\\normalsize") return 14.0; + if (value == "\\large") return 18.0; + if (value == "\\Large") return 22.0; + if (value == "\\LARGE") return 26.0; + if (value == "\\huge") return 30.0; + if (value == "\\Huge") return 36.0; + return previewNumber(value, fallback); +} + +static COLORREF previewColor(const std::string & value) { + if (value.size() == 7 && value[0] == '#') { + unsigned int r = 0, g = 0, b = 0; + sscanf(value.c_str() + 1, "%2x%2x%2x", &r, &g, &b); + return RGB(r, g, b); + } + std::istringstream stream(value); + double r = 0.0, g = 0.0, b = 0.0; + if (stream >> r) { + if (!(stream >> g)) g = r; + if (!(stream >> b)) b = r; + } + return RGB(int(255.0 * std::clamp(r, 0.0, 1.0) + 0.5), + int(255.0 * std::clamp(g, 0.0, 1.0) + 0.5), + int(255.0 * std::clamp(b, 0.0, 1.0) + 0.5)); +} + +static COLORREF blendColor(COLORREF a, COLORREF b, double t) { + t = std::clamp(t, 0.0, 1.0); + return RGB(int(GetRValue(a) * (1.0 - t) + GetRValue(b) * t + 0.5), + int(GetGValue(a) * (1.0 - t) + GetGValue(b) * t + 0.5), + int(GetBValue(a) * (1.0 - t) + GetBValue(b) * t + 0.5)); +} + +static void fillRect(HDC dc, const RECT & r, COLORREF color) { + HBRUSH b = CreateSolidBrush(color); + FillRect(dc, &r, b); + DeleteObject(b); +} + +static void drawImagePreview(HDC dc, RECT rc, const std::string & spec) { + size_t sep = spec.find('|'); + std::string kind = sep == std::string::npos ? spec : spec.substr(0, sep); + std::string value = sep == std::string::npos ? std::string() : spec.substr(sep + 1); + + fillRect(dc, rc, RGB(255, 255, 220)); + HBRUSH frame = CreateSolidBrush(RGB(160, 160, 130)); + FrameRect(dc, &rc, frame); + DeleteObject(frame); + + RECT body = rc; + InflateRect(&body, -18, -16); + int cx = (body.left + body.right) / 2; + int cy = (body.top + body.bottom) / 2; + COLORREF blue = RGB(20, 40, 160); + COLORREF red = RGB(230, 80, 70); + if (kind == "color") { + RECT swatch = body; + InflateRect(&swatch, -8, -8); + swatch.bottom -= 28; + fillRect(dc, swatch, previewColor(value)); + FrameRect(dc, &swatch, (HBRUSH)GetStockObject(BLACK_BRUSH)); + SetBkMode(dc, TRANSPARENT); + DrawTextA(dc, value.c_str(), -1, &body, DT_CENTER | DT_BOTTOM | DT_SINGLELINE); + } else if (kind == "pen" || kind == "dashstyle") { + int width = kind == "pen" ? int(std::clamp(previewNumber(value, 1.0), 1.0, 24.0)) : 4; + HPEN pen = CreatePen(kind == "dashstyle" ? PS_DASH : PS_SOLID, width, blue); + HGDIOBJ oldPen = SelectObject(dc, pen); + MoveToEx(dc, body.left, cy, nullptr); + LineTo(dc, body.right, cy); + SelectObject(dc, oldPen); + DeleteObject(pen); + } else if (kind == "textsize") { + int size = int(std::clamp(previewNamedSize(value, 18.0), 8.0, 48.0)); + HFONT font = CreateFontA(-size, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, + DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, + DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, "Segoe UI"); + HGDIOBJ oldFont = SelectObject(dc, font); + SetBkMode(dc, TRANSPARENT); + DrawTextA(dc, "Sample", -1, &body, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + SelectObject(dc, oldFont); + DeleteObject(font); + } else if (kind == "symbolsize") { + int s = int(std::clamp(previewNumber(value, 3.0) * 3.0, 6.0, 42.0)); + HPEN pen = CreatePen(PS_SOLID, 2, blue); + HBRUSH brush = CreateSolidBrush(red); + HGDIOBJ oldPen = SelectObject(dc, pen); + HGDIOBJ oldBrush = SelectObject(dc, brush); + int xs[] = {body.left + (body.right - body.left) / 4, cx, + body.left + 3 * (body.right - body.left) / 4}; + Ellipse(dc, xs[0] - s / 2, cy - s / 2, xs[0] + s / 2, cy + s / 2); + Rectangle(dc, xs[1] - s / 2, cy - s / 2, xs[1] + s / 2, cy + s / 2); + POINT diamond[] = {{xs[2], cy - s / 2}, {xs[2] + s / 2, cy}, + {xs[2], cy + s / 2}, {xs[2] - s / 2, cy}}; + Polygon(dc, diamond, 4); + SelectObject(dc, oldBrush); + SelectObject(dc, oldPen); + DeleteObject(brush); + DeleteObject(pen); + } else if (kind == "arrowsize") { + int s = int(std::clamp(previewNumber(value, 7.0) * 2.0, 8.0, 50.0)); + HPEN pen = CreatePen(PS_SOLID, 4, blue); + HBRUSH brush = CreateSolidBrush(blue); + HGDIOBJ oldPen = SelectObject(dc, pen); + HGDIOBJ oldBrush = SelectObject(dc, brush); + MoveToEx(dc, body.left, cy, nullptr); + LineTo(dc, body.right - s, cy); + POINT arrow[] = {{body.right, cy}, {body.right - s, int(cy - 0.45 * s)}, + {body.right - s, int(cy + 0.45 * s)}}; + Polygon(dc, arrow, 3); + SelectObject(dc, oldBrush); + SelectObject(dc, oldPen); + DeleteObject(brush); + DeleteObject(pen); + } else if (kind == "opacity") { + double op = std::clamp(previewNumber(value, 1.0), 0.0, 1.0); + RECT left = {body.left + 12, body.top + 10, + body.left + 12 + int((body.right - body.left) * 0.45), body.bottom - 10}; + RECT right = {cx - 12, body.top + 10, + cx - 12 + int((body.right - body.left) * 0.45), body.bottom - 10}; + fillRect(dc, left, RGB(80, 120, 230)); + fillRect(dc, right, blendColor(RGB(255, 255, 220), red, op)); + FrameRect(dc, &left, (HBRUSH)GetStockObject(BLACK_BRUSH)); + FrameRect(dc, &right, (HBRUSH)GetStockObject(BLACK_BRUSH)); + } else if (kind == "gridsize") { + int step = int(std::clamp(previewNumber(value, 8.0) / 2.0, 6.0, 24.0)); + HPEN grid = CreatePen(PS_SOLID, 1, RGB(170, 170, 170)); + HGDIOBJ oldPen = SelectObject(dc, grid); + for (int x = body.left; x <= body.right; x += step) { + MoveToEx(dc, x, body.top, nullptr); + LineTo(dc, x, body.bottom); + } + for (int y = body.top; y <= body.bottom; y += step) { + MoveToEx(dc, body.left, y, nullptr); + LineTo(dc, body.right, y); + } + SelectObject(dc, oldPen); + DeleteObject(grid); + HPEN pen = CreatePen(PS_SOLID, 3, blue); + oldPen = SelectObject(dc, pen); + MoveToEx(dc, body.left, body.bottom, nullptr); + LineTo(dc, body.right, body.top); + SelectObject(dc, oldPen); + DeleteObject(pen); + } else if (kind == "anglesize") { + double radians = previewNumber(value, 45.0) * 3.14159265358979323846 / 180.0; + int ox = body.left + (body.right - body.left) / 4; + int oy = body.bottom - 12; + double len = std::min((body.right - body.left) * 0.65, (body.bottom - body.top) * 0.9); + HPEN base = CreatePen(PS_SOLID, 2, RGB(70, 70, 70)); + HGDIOBJ oldPen = SelectObject(dc, base); + MoveToEx(dc, ox, oy, nullptr); + LineTo(dc, body.right, oy); + SelectObject(dc, oldPen); + DeleteObject(base); + HPEN pen = CreatePen(PS_SOLID, 4, blue); + oldPen = SelectObject(dc, pen); + MoveToEx(dc, ox, oy, nullptr); + LineTo(dc, int(ox + len * std::cos(radians)), int(oy - len * std::sin(radians))); + SelectObject(dc, oldPen); + DeleteObject(pen); + } +} + void buildFlags(std::vector & t, DWORD flags) { union { DWORD dw; @@ -157,6 +336,7 @@ void PDialog::setMapped(lua_State * L, int idx) { case ETextEdit: case EInput: case ELabel: setWindowText(h, m.text.c_str()); break; + case EImage: InvalidateRect(h, nullptr, TRUE); break; case EList: if (!lua_isnumber(L, 3)) { ListBox_ResetContent(h); @@ -383,6 +563,16 @@ BOOL CALLBACK PDialog::dialogProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM case WM_SIZE: if (d) return d->handleResize(); return FALSE; + case WM_DRAWITEM: + if (d && wParam >= IDBASE && wParam < IDBASE + d->iElements.size()) { + SElement & m = d->iElements[wParam - IDBASE]; + if (m.type == EImage) { + DRAWITEMSTRUCT * dis = (DRAWITEMSTRUCT *)lParam; + drawImagePreview(dis->hDC, dis->rcItem, m.text); + return TRUE; + } + } + return FALSE; case WM_DESTROY: // Remove the subclasses from text edits for (int i = 0; i < int(d->iElements.size()); ++i) { @@ -422,6 +612,11 @@ void PDialog::buildElements(std::vector & t) { buildDimensions(t, m, id); buildControl(t, 0x0082, m.text.c_str()); // static text break; + case EImage: + buildFlags(t, flags | SS_OWNERDRAW); + buildDimensions(t, m, id); + buildControl(t, 0x0082, nullptr); // static frame + break; case EInput: buildFlags(t, flags | ES_LEFT | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL); buildDimensions(t, m, id); From 88520a6c98883066b4f2f081ddff30d13678197e Mon Sep 17 00:00:00 2001 From: Dmitriy Morozov Date: Sat, 30 May 2026 23:52:47 -0400 Subject: [PATCH 3/8] Fix grid size stylesheet previews --- src/ipe-web/src/dialogs.ts | 2 +- src/ipe/lua/actions.lua | 13 +++++++++++-- src/ipeui/ipeui_cocoa.cpp | 3 +-- src/ipeui/ipeui_gtk.cpp | 2 +- src/ipeui/ipeui_qt.cpp | 3 +-- src/ipeui/ipeui_win.cpp | 2 +- 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/ipe-web/src/dialogs.ts b/src/ipe-web/src/dialogs.ts index c2e37e2..145f85b 100644 --- a/src/ipe-web/src/dialogs.ts +++ b/src/ipe-web/src/dialogs.ts @@ -259,7 +259,7 @@ function drawImagePreview(canvas: HTMLCanvasElement, spec: string): void { ctx.fillStyle = `rgba(230,70,50,${op})`; ctx.fillRect(cx - 12, top + 10, (right - left) * 0.45, bottom - top - 20); } else if (kind === "gridsize") { - const step = Math.max(6, Math.min(24, previewNumber(value, 8) / 2)); + const step = Math.max(1, Math.min(64, previewNumber(value, 8))); ctx.strokeStyle = "rgb(170,170,170)"; ctx.lineWidth = 1; for (let x = left; x <= right; x += step) { diff --git a/src/ipe/lua/actions.lua b/src/ipe/lua/actions.lua index f1f4f07..32281e7 100644 --- a/src/ipe/lua/actions.lua +++ b/src/ipe/lua/actions.lua @@ -2870,9 +2870,18 @@ local function sheets_visual_edit(d0, dd) action=function (d) local c = visual_style_categories[st.cat] local entries = st.data[st.cat] + local name = d:get("name") + if st.current and entries[st.current] and name == entries[st.current].name then + name = "new" + end + if name == "" then name = "new" end + local value = c.color and visual_hex_to_rgb(d:get("color")) or d:get("value") + if value == "" then + value = c.color and visual_hex_to_rgb(c.default) or c.default + end entries[#entries + 1] = { - name=visual_unique_name(entries, "new"), - value=c.color and visual_hex_to_rgb(c.default) or c.default, + name=visual_unique_name(entries, name), + value=value, } st.current = #entries visual_set_fields(d, st) diff --git a/src/ipeui/ipeui_cocoa.cpp b/src/ipeui/ipeui_cocoa.cpp index fead3d1..c9af163 100644 --- a/src/ipeui/ipeui_cocoa.cpp +++ b/src/ipeui/ipeui_cocoa.cpp @@ -410,8 +410,7 @@ static std::vector previewDashPattern(const std::string & value) { NSFrameRect(left); NSFrameRect(right); } else if (kind == "gridsize") { - double n = std::clamp(previewNumber(value, 8.0), 2.0, 64.0); - double step = std::clamp(n / 2.0, 6.0, 24.0); + double step = std::clamp(previewNumber(value, 8.0), 1.0, 64.0); [[NSColor colorWithCalibratedWhite:0.65 alpha:1.0] setStroke]; for (double x = NSMinX(body); x <= NSMaxX(body); x += step) { NSBezierPath * p = [NSBezierPath bezierPath]; diff --git a/src/ipeui/ipeui_gtk.cpp b/src/ipeui/ipeui_gtk.cpp index 6a2bddb..9688856 100644 --- a/src/ipeui/ipeui_gtk.cpp +++ b/src/ipeui/ipeui_gtk.cpp @@ -204,7 +204,7 @@ static void drawImagePreview(cairo_t * cr, int width, int height, const std::str cairo_rectangle(cr, cx - 12.0, top + 10.0, (right - left) * 0.45, bottom - top - 20.0); cairo_fill(cr); } else if (kind == "gridsize") { - double step = std::clamp(previewNumber(value, 8.0) / 2.0, 6.0, 24.0); + double step = std::clamp(previewNumber(value, 8.0), 1.0, 64.0); cairo_set_source_rgb(cr, 0.67, 0.67, 0.67); cairo_set_line_width(cr, 1.0); for (double x = left; x <= right; x += step) { diff --git a/src/ipeui/ipeui_qt.cpp b/src/ipeui/ipeui_qt.cpp index e06be94..41b86af 100644 --- a/src/ipeui/ipeui_qt.cpp +++ b/src/ipeui/ipeui_qt.cpp @@ -279,8 +279,7 @@ void DialogImage::paintEvent(QPaintEvent *) { painter.drawRect(left); painter.drawRect(right); } else if (kind == QLatin1String("gridsize")) { - double n = std::clamp(previewNumber(value, 8.0), 2.0, 64.0); - double step = std::clamp(n / 2.0, 6.0, 24.0); + double step = std::clamp(previewNumber(value, 8.0), 1.0, 64.0); painter.setPen(QPen(QColor(170, 170, 170), 1)); for (double x = body.left(); x <= body.right(); x += step) painter.drawLine(QPointF(x, body.top()), QPointF(x, body.bottom())); diff --git a/src/ipeui/ipeui_win.cpp b/src/ipeui/ipeui_win.cpp index 54f6801..e020bc3 100644 --- a/src/ipeui/ipeui_win.cpp +++ b/src/ipeui/ipeui_win.cpp @@ -216,7 +216,7 @@ static void drawImagePreview(HDC dc, RECT rc, const std::string & spec) { FrameRect(dc, &left, (HBRUSH)GetStockObject(BLACK_BRUSH)); FrameRect(dc, &right, (HBRUSH)GetStockObject(BLACK_BRUSH)); } else if (kind == "gridsize") { - int step = int(std::clamp(previewNumber(value, 8.0) / 2.0, 6.0, 24.0)); + int step = int(std::clamp(previewNumber(value, 8.0), 1.0, 64.0)); HPEN grid = CreatePen(PS_SOLID, 1, RGB(170, 170, 170)); HGDIOBJ oldPen = SelectObject(dc, grid); for (int x = body.left; x <= body.right; x += step) { From d18e2cd408f61a588d82ddaf0c9994c271aedd79 Mon Sep 17 00:00:00 2001 From: Dmitriy Morozov Date: Sun, 31 May 2026 08:42:18 -0400 Subject: [PATCH 4/8] Match stylesheet previews to canvas zoom --- src/ipe-web/src/dialogs.ts | 20 ++++++++++++-------- src/ipe/lua/actions.lua | 26 +++++++++++++------------- src/ipeui/ipeui_cocoa.cpp | 21 ++++++++++++++------- src/ipeui/ipeui_gtk.cpp | 21 ++++++++++++++------- src/ipeui/ipeui_qt.cpp | 19 +++++++++++-------- src/ipeui/ipeui_win.cpp | 20 +++++++++++++------- 6 files changed, 77 insertions(+), 50 deletions(-) diff --git a/src/ipe-web/src/dialogs.ts b/src/ipe-web/src/dialogs.ts index 145f85b..360b770 100644 --- a/src/ipe-web/src/dialogs.ts +++ b/src/ipe-web/src/dialogs.ts @@ -178,7 +178,8 @@ function previewDashPattern(value: string): number[] { } function drawImagePreview(canvas: HTMLCanvasElement, spec: string): void { - const [kind, value = ""] = spec.split("|", 2); + const [kind, value = "", zoomText = "1"] = spec.split("|"); + const zoom = Math.max(0.1, Math.min(100, previewNumber(zoomText, 1))); const ctx = canvas.getContext("2d"); if (ctx == null) return; const w = canvas.width; @@ -208,8 +209,11 @@ function drawImagePreview(canvas: HTMLCanvasElement, spec: string): void { } else if (kind === "pen" || kind === "dashstyle") { ctx.strokeStyle = "rgb(20,40,160)"; ctx.lineWidth = - kind === "pen" ? Math.max(0.5, Math.min(24, previewNumber(value, 1))) : 4; - if (kind === "dashstyle") ctx.setLineDash(previewDashPattern(value)); + kind === "pen" + ? Math.max(0.1, previewNumber(value, 1) * zoom) + : Math.max(0.1, 4 * zoom); + if (kind === "dashstyle") + ctx.setLineDash(previewDashPattern(value).map((x) => x * zoom)); else ctx.setLineDash([]); ctx.beginPath(); ctx.moveTo(left, cy); @@ -218,12 +222,12 @@ function drawImagePreview(canvas: HTMLCanvasElement, spec: string): void { ctx.setLineDash([]); } else if (kind === "textsize") { ctx.fillStyle = "rgb(30,30,30)"; - ctx.font = `${Math.max(8, Math.min(48, previewNamedSize(value, 18)))}px sans-serif`; + ctx.font = `${Math.max(1, previewNamedSize(value, 18) * zoom)}px sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("Sample", cx, cy); } else if (kind === "symbolsize") { - const s = Math.max(6, Math.min(42, previewNumber(value, 3) * 3)); + const s = Math.max(1, previewNumber(value, 3) * 3 * zoom); ctx.fillStyle = "rgb(230,80,70)"; ctx.strokeStyle = "rgb(20,40,160)"; ctx.lineWidth = 2; @@ -238,10 +242,10 @@ function drawImagePreview(canvas: HTMLCanvasElement, spec: string): void { ctx.stroke(); } } else if (kind === "arrowsize") { - const s = Math.max(8, Math.min(50, previewNumber(value, 7) * 2)); + const s = Math.max(1, previewNumber(value, 7) * 2 * zoom); ctx.strokeStyle = "rgb(20,40,160)"; ctx.fillStyle = "rgb(20,40,160)"; - ctx.lineWidth = 4; + ctx.lineWidth = Math.max(0.1, 4 * zoom); ctx.beginPath(); ctx.moveTo(left, cy); ctx.lineTo(right - s, cy); @@ -259,7 +263,7 @@ function drawImagePreview(canvas: HTMLCanvasElement, spec: string): void { ctx.fillStyle = `rgba(230,70,50,${op})`; ctx.fillRect(cx - 12, top + 10, (right - left) * 0.45, bottom - top - 20); } else if (kind === "gridsize") { - const step = Math.max(1, Math.min(64, previewNumber(value, 8))); + const step = Math.max(1, previewNumber(value, 8) * zoom); ctx.strokeStyle = "rgb(170,170,170)"; ctx.lineWidth = 1; for (let x = left; x <= right; x += step) { diff --git a/src/ipe/lua/actions.lua b/src/ipe/lua/actions.lua index 32281e7..3bfdd3e 100644 --- a/src/ipe/lua/actions.lua +++ b/src/ipe/lua/actions.lua @@ -2724,12 +2724,12 @@ local function visual_hex_to_rgb(value) return value end -local function visual_preview_spec(c, value) - return c.kind .. "|" .. (value or "") +local function visual_preview_spec(dd, c, value) + return c.kind .. "|" .. (value or "") .. "|" .. dd.model.ui:zoom() end -local function visual_set_preview(d, c, value) - d:set("preview", visual_preview_spec(c, value)) +local function visual_set_preview(d, dd, c, value) + d:set("preview", visual_preview_spec(dd, c, value)) end local function visual_load_sheet(sheet) @@ -2743,7 +2743,7 @@ local function visual_load_sheet(sheet) return data end -local function visual_set_fields(d, st) +local function visual_set_fields(d, dd, st) local c = visual_style_categories[st.cat] local entries = st.data[st.cat] local names = visual_entry_names(entries) @@ -2754,7 +2754,7 @@ local function visual_set_fields(d, st) d:set("name", "") d:set("value", "") d:set("color", c.default or "#000000") - visual_set_preview(d, c, c.color and visual_hex_to_rgb(c.default or "#000000") or c.default) + visual_set_preview(d, dd, c, c.color and visual_hex_to_rgb(c.default or "#000000") or c.default) else st.current = math.max(1, math.min(st.current or 1, #entries)) d:set("items", st.current) @@ -2766,7 +2766,7 @@ local function visual_set_fields(d, st) d:set("value", entries[st.current].value) d:set("color", "") end - visual_set_preview(d, c, entries[st.current].value) + visual_set_preview(d, dd, c, entries[st.current].value) end d:set("value_label", c.color and "Color" or "Value") d:set("help", c.help) @@ -2801,7 +2801,7 @@ local function visual_apply_current(d, dd, st) st.updating = true d:set("items", visual_entry_names(entries)) d:set("items", st.current) - visual_set_preview(d, c, value) + visual_set_preview(d, dd, c, value) st.updating = false return true end @@ -2843,13 +2843,13 @@ local function sheets_visual_edit(d0, dd) if not visual_apply_current(d, dd, st) then return end st.cat = d:get("category") st.current = 1 - visual_set_fields(d, st) + visual_set_fields(d, dd, st) end local first_names = visual_entry_names(st.data[1]) first_names.action = function (d) if st.updating then return end st.current = d:get("items") - visual_set_fields(d, st) + visual_set_fields(d, dd, st) end local d = ipeui.Dialog(dd.model.ui:win(), "Visual stylesheet editor") @@ -2884,7 +2884,7 @@ local function sheets_visual_edit(d0, dd) value=value, } st.current = #entries - visual_set_fields(d, st) + visual_set_fields(d, dd, st) end }, 9, 4) d:add("delete", "button", { label="Delete", action=function (d) @@ -2892,7 +2892,7 @@ local function sheets_visual_edit(d0, dd) if st.current and entries[st.current] then table.remove(entries, st.current) st.current = math.min(st.current, #entries) - visual_set_fields(d, st) + visual_set_fields(d, dd, st) end end }, 10, 3) d:addButton("ok", "&Ok", "accept") @@ -2900,7 +2900,7 @@ local function sheets_visual_edit(d0, dd) d:setStretch("row", 2, 1) d:setStretch("column", 2, 1) d:setStretch("column", 4, 2) - visual_set_fields(d, st) + visual_set_fields(d, dd, st) if not d:execute({ 680, 520 }) then return end local nsheet = visual_apply_to_sheet(d, dd, st, dd.list[i]) diff --git a/src/ipeui/ipeui_cocoa.cpp b/src/ipeui/ipeui_cocoa.cpp index c9af163..9e442de 100644 --- a/src/ipeui/ipeui_cocoa.cpp +++ b/src/ipeui/ipeui_cocoa.cpp @@ -317,8 +317,14 @@ static std::vector previewDashPattern(const std::string & value) { NSFrameRect(bounds); size_t sep = iSpec.find('|'); + size_t sep2 = sep == std::string::npos ? std::string::npos : iSpec.find('|', sep + 1); std::string kind = sep == std::string::npos ? iSpec : iSpec.substr(0, sep); - std::string value = sep == std::string::npos ? std::string() : iSpec.substr(sep + 1); + std::string value = sep == std::string::npos + ? std::string() + : iSpec.substr(sep + 1, sep2 - sep - 1); + double zoom = sep2 == std::string::npos + ? 1.0 + : std::clamp(previewNumber(iSpec.substr(sep2 + 1), 1.0), 0.1, 100.0); NSRect body = NSInsetRect(bounds, 18., 16.); if (kind == "color") { @@ -337,24 +343,25 @@ static std::vector previewDashPattern(const std::string & value) { [path moveToPoint:NSMakePoint(NSMinX(body), NSMidY(body))]; [path lineToPoint:NSMakePoint(NSMaxX(body), NSMidY(body))]; [path setLineWidth:(kind == "pen") - ? std::clamp(previewNumber(value, 1.0), 0.5, 24.0) - : 4.0]; + ? std::max(0.1, previewNumber(value, 1.0) * zoom) + : std::max(0.1, 4.0 * zoom)]; if (kind == "dashstyle") { std::vector dashes = previewDashPattern(value); + for (CGFloat & dash : dashes) dash *= zoom; if (!dashes.empty()) [path setLineDash:dashes.data() count:(NSInteger)dashes.size() phase:0.0]; } [[NSColor colorWithCalibratedRed:0.08 green:0.16 blue:0.63 alpha:1.0] setStroke]; [path stroke]; } else if (kind == "textsize") { - double size = std::clamp(previewNamedSize(value, 18.0), 8.0, 48.0); + double size = std::max(1.0, previewNamedSize(value, 18.0) * zoom); NSDictionary * attrs = @{ NSFontAttributeName : [NSFont systemFontOfSize:size], NSForegroundColorAttributeName : [NSColor textColor] }; [@"Sample" drawInRect:body withAttributes:attrs]; } else if (kind == "symbolsize") { - double s = std::clamp(previewNumber(value, 3.0) * 3.0, 6.0, 42.0); + double s = std::max(1.0, previewNumber(value, 3.0) * 3.0 * zoom); NSArray * centers = @[ [NSValue valueWithPoint:NSMakePoint(NSMinX(body) + body.size.width * 0.25, NSMidY(body))], @@ -380,7 +387,7 @@ static std::vector previewDashPattern(const std::string & value) { [diamond fill]; [diamond stroke]; } else if (kind == "arrowsize") { - double s = std::clamp(previewNumber(value, 7.0) * 2.0, 8.0, 50.0); + double s = std::max(1.0, previewNumber(value, 7.0) * 2.0 * zoom); NSPoint a = NSMakePoint(NSMinX(body), NSMidY(body)); NSPoint b = NSMakePoint(NSMaxX(body) - s, NSMidY(body)); NSBezierPath * line = [NSBezierPath bezierPath]; @@ -410,7 +417,7 @@ static std::vector previewDashPattern(const std::string & value) { NSFrameRect(left); NSFrameRect(right); } else if (kind == "gridsize") { - double step = std::clamp(previewNumber(value, 8.0), 1.0, 64.0); + double step = std::max(1.0, previewNumber(value, 8.0) * zoom); [[NSColor colorWithCalibratedWhite:0.65 alpha:1.0] setStroke]; for (double x = NSMinX(body); x <= NSMaxX(body); x += step) { NSBezierPath * p = [NSBezierPath bezierPath]; diff --git a/src/ipeui/ipeui_gtk.cpp b/src/ipeui/ipeui_gtk.cpp index 9688856..9459d8a 100644 --- a/src/ipeui/ipeui_gtk.cpp +++ b/src/ipeui/ipeui_gtk.cpp @@ -123,8 +123,14 @@ static std::vector previewDashPattern(const std::string & value) { static void drawImagePreview(cairo_t * cr, int width, int height, const std::string & spec) { size_t sep = spec.find('|'); + size_t sep2 = sep == std::string::npos ? std::string::npos : spec.find('|', sep + 1); std::string kind = sep == std::string::npos ? spec : spec.substr(0, sep); - std::string value = sep == std::string::npos ? std::string() : spec.substr(sep + 1); + std::string value = sep == std::string::npos + ? std::string() + : spec.substr(sep + 1, sep2 - sep - 1); + double zoom = sep2 == std::string::npos + ? 1.0 + : std::clamp(previewNumber(spec.substr(sep2 + 1), 1.0), 0.1, 100.0); cairo_set_source_rgb(cr, 1.0, 1.0, 0.86); cairo_rectangle(cr, 0.5, 0.5, width - 1.0, height - 1.0); @@ -155,15 +161,16 @@ static void drawImagePreview(cairo_t * cr, int width, int height, const std::str cairo_show_text(cr, value.c_str()); } else if (kind == "pen" || kind == "dashstyle") { cairo_set_source_rgb(cr, 0.08, 0.16, 0.63); - cairo_set_line_width(cr, kind == "pen" ? std::clamp(previewNumber(value, 1.0), 0.5, 24.0) : 4.0); + cairo_set_line_width(cr, kind == "pen" ? std::max(0.1, previewNumber(value, 1.0) * zoom) : std::max(0.1, 4.0 * zoom)); std::vector dashes = previewDashPattern(value); + for (double & dash : dashes) dash *= zoom; if (kind == "dashstyle" && !dashes.empty()) cairo_set_dash(cr, dashes.data(), dashes.size(), 0.0); cairo_move_to(cr, left, cy); cairo_line_to(cr, right, cy); cairo_stroke(cr); cairo_set_dash(cr, nullptr, 0, 0.0); } else if (kind == "textsize") { - double size = std::clamp(previewNamedSize(value, 18.0), 8.0, 48.0); + double size = std::max(1.0, previewNamedSize(value, 18.0) * zoom); cairo_select_font_face(cr, "sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); cairo_set_font_size(cr, size); cairo_set_source_rgb(cr, 0.12, 0.12, 0.12); @@ -172,7 +179,7 @@ static void drawImagePreview(cairo_t * cr, int width, int height, const std::str cairo_move_to(cr, cx - ext.width / 2.0 - ext.x_bearing, cy - ext.height / 2.0 - ext.y_bearing); cairo_show_text(cr, "Sample"); } else if (kind == "symbolsize") { - double s = std::clamp(previewNumber(value, 3.0) * 3.0, 6.0, 42.0); + double s = std::max(1.0, previewNumber(value, 3.0) * 3.0 * zoom); double xs[] = {left + (right - left) * 0.25, cx, left + (right - left) * 0.75}; cairo_set_line_width(cr, 2.0); cairo_set_source_rgb(cr, 0.90, 0.31, 0.27); @@ -184,9 +191,9 @@ static void drawImagePreview(cairo_t * cr, int width, int height, const std::str cairo_set_source_rgb(cr, 0.90, 0.31, 0.27); } } else if (kind == "arrowsize") { - double s = std::clamp(previewNumber(value, 7.0) * 2.0, 8.0, 50.0); + double s = std::max(1.0, previewNumber(value, 7.0) * 2.0 * zoom); cairo_set_source_rgb(cr, 0.08, 0.16, 0.63); - cairo_set_line_width(cr, 4.0); + cairo_set_line_width(cr, std::max(0.1, 4.0 * zoom)); cairo_move_to(cr, left, cy); cairo_line_to(cr, right - s, cy); cairo_stroke(cr); @@ -204,7 +211,7 @@ static void drawImagePreview(cairo_t * cr, int width, int height, const std::str cairo_rectangle(cr, cx - 12.0, top + 10.0, (right - left) * 0.45, bottom - top - 20.0); cairo_fill(cr); } else if (kind == "gridsize") { - double step = std::clamp(previewNumber(value, 8.0), 1.0, 64.0); + double step = std::max(1.0, previewNumber(value, 8.0) * zoom); cairo_set_source_rgb(cr, 0.67, 0.67, 0.67); cairo_set_line_width(cr, 1.0); for (double x = left; x <= right; x += step) { diff --git a/src/ipeui/ipeui_qt.cpp b/src/ipeui/ipeui_qt.cpp index 41b86af..f7cbd53 100644 --- a/src/ipeui/ipeui_qt.cpp +++ b/src/ipeui/ipeui_qt.cpp @@ -209,7 +209,9 @@ class DialogImage : public QWidget { void DialogImage::paintEvent(QPaintEvent *) { QString kind = iSpec.section(QLatin1Char('|'), 0, 0); - QString value = iSpec.section(QLatin1Char('|'), 1); + QString value = iSpec.section(QLatin1Char('|'), 1, 1); + double zoom = std::clamp(previewNumber(iSpec.section(QLatin1Char('|'), 2, 2), 1.0), + 0.1, 100.0); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); @@ -228,10 +230,10 @@ void DialogImage::paintEvent(QPaintEvent *) { Qt::AlignCenter, value); } else if (kind == QLatin1String("pen") || kind == QLatin1String("dashstyle")) { QPen pen(QColor(20, 40, 160), - std::clamp(previewNumber(value, 1.0), 0.5, 24.0), Qt::SolidLine, + std::max(0.1, previewNumber(value, 1.0) * zoom), Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin); if (kind == QLatin1String("dashstyle")) { - pen.setWidthF(4.0); + pen.setWidthF(std::max(0.1, 4.0 * zoom)); QVector dashes = previewDashPattern(value); if (!dashes.isEmpty()) pen.setDashPattern(dashes); } @@ -240,12 +242,12 @@ void DialogImage::paintEvent(QPaintEvent *) { painter.drawLine(QPointF(body.left(), y), QPointF(body.right(), y)); } else if (kind == QLatin1String("textsize")) { QFont font = painter.font(); - font.setPointSizeF(std::clamp(previewNamedSize(value, 10.0), 4.0, 36.0)); + font.setPointSizeF(std::max(1.0, previewNamedSize(value, 10.0) * zoom)); painter.setFont(font); painter.setPen(QColor(30, 30, 30)); painter.drawText(body, Qt::AlignCenter, QStringLiteral("Sample")); } else if (kind == QLatin1String("symbolsize")) { - double s = std::clamp(previewNumber(value, 3.0) * 3.0, 6.0, 42.0); + double s = std::max(1.0, previewNumber(value, 3.0) * 3.0 * zoom); painter.setPen(QPen(QColor(20, 40, 160), 2)); painter.setBrush(QColor(230, 80, 70)); QPointF c1(body.left() + body.width() * 0.25, body.center().y()); @@ -258,10 +260,11 @@ void DialogImage::paintEvent(QPaintEvent *) { << QPointF(c3.x(), c3.y() + s / 2) << QPointF(c3.x() - s / 2, c3.y()); painter.drawPolygon(diamond); } else if (kind == QLatin1String("arrowsize")) { - double s = std::clamp(previewNumber(value, 7.0) * 2.0, 8.0, 50.0); + double s = std::max(1.0, previewNumber(value, 7.0) * 2.0 * zoom); QPointF a(body.left(), body.center().y()); QPointF b(body.right() - s, body.center().y()); - painter.setPen(QPen(QColor(20, 40, 160), 4, Qt::SolidLine, Qt::RoundCap)); + painter.setPen(QPen(QColor(20, 40, 160), std::max(0.1, 4.0 * zoom), + Qt::SolidLine, Qt::RoundCap)); painter.drawLine(a, b); QPolygonF arrow; arrow << QPointF(b.x() + s, b.y()) << QPointF(b.x(), b.y() - 0.45 * s) @@ -279,7 +282,7 @@ void DialogImage::paintEvent(QPaintEvent *) { painter.drawRect(left); painter.drawRect(right); } else if (kind == QLatin1String("gridsize")) { - double step = std::clamp(previewNumber(value, 8.0), 1.0, 64.0); + double step = std::max(1.0, previewNumber(value, 8.0) * zoom); painter.setPen(QPen(QColor(170, 170, 170), 1)); for (double x = body.left(); x <= body.right(); x += step) painter.drawLine(QPointF(x, body.top()), QPointF(x, body.bottom())); diff --git a/src/ipeui/ipeui_win.cpp b/src/ipeui/ipeui_win.cpp index e020bc3..0cec635 100644 --- a/src/ipeui/ipeui_win.cpp +++ b/src/ipeui/ipeui_win.cpp @@ -133,8 +133,14 @@ static void fillRect(HDC dc, const RECT & r, COLORREF color) { static void drawImagePreview(HDC dc, RECT rc, const std::string & spec) { size_t sep = spec.find('|'); + size_t sep2 = sep == std::string::npos ? std::string::npos : spec.find('|', sep + 1); std::string kind = sep == std::string::npos ? spec : spec.substr(0, sep); - std::string value = sep == std::string::npos ? std::string() : spec.substr(sep + 1); + std::string value = sep == std::string::npos + ? std::string() + : spec.substr(sep + 1, sep2 - sep - 1); + double zoom = sep2 == std::string::npos + ? 1.0 + : std::clamp(previewNumber(spec.substr(sep2 + 1), 1.0), 0.1, 100.0); fillRect(dc, rc, RGB(255, 255, 220)); HBRUSH frame = CreateSolidBrush(RGB(160, 160, 130)); @@ -156,7 +162,7 @@ static void drawImagePreview(HDC dc, RECT rc, const std::string & spec) { SetBkMode(dc, TRANSPARENT); DrawTextA(dc, value.c_str(), -1, &body, DT_CENTER | DT_BOTTOM | DT_SINGLELINE); } else if (kind == "pen" || kind == "dashstyle") { - int width = kind == "pen" ? int(std::clamp(previewNumber(value, 1.0), 1.0, 24.0)) : 4; + int width = std::max(1, int((kind == "pen" ? previewNumber(value, 1.0) : 4.0) * zoom + 0.5)); HPEN pen = CreatePen(kind == "dashstyle" ? PS_DASH : PS_SOLID, width, blue); HGDIOBJ oldPen = SelectObject(dc, pen); MoveToEx(dc, body.left, cy, nullptr); @@ -164,7 +170,7 @@ static void drawImagePreview(HDC dc, RECT rc, const std::string & spec) { SelectObject(dc, oldPen); DeleteObject(pen); } else if (kind == "textsize") { - int size = int(std::clamp(previewNamedSize(value, 18.0), 8.0, 48.0)); + int size = std::max(1, int(previewNamedSize(value, 18.0) * zoom + 0.5)); HFONT font = CreateFontA(-size, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, "Segoe UI"); @@ -174,7 +180,7 @@ static void drawImagePreview(HDC dc, RECT rc, const std::string & spec) { SelectObject(dc, oldFont); DeleteObject(font); } else if (kind == "symbolsize") { - int s = int(std::clamp(previewNumber(value, 3.0) * 3.0, 6.0, 42.0)); + int s = std::max(1, int(previewNumber(value, 3.0) * 3.0 * zoom + 0.5)); HPEN pen = CreatePen(PS_SOLID, 2, blue); HBRUSH brush = CreateSolidBrush(red); HGDIOBJ oldPen = SelectObject(dc, pen); @@ -191,8 +197,8 @@ static void drawImagePreview(HDC dc, RECT rc, const std::string & spec) { DeleteObject(brush); DeleteObject(pen); } else if (kind == "arrowsize") { - int s = int(std::clamp(previewNumber(value, 7.0) * 2.0, 8.0, 50.0)); - HPEN pen = CreatePen(PS_SOLID, 4, blue); + int s = std::max(1, int(previewNumber(value, 7.0) * 2.0 * zoom + 0.5)); + HPEN pen = CreatePen(PS_SOLID, std::max(1, int(4.0 * zoom + 0.5)), blue); HBRUSH brush = CreateSolidBrush(blue); HGDIOBJ oldPen = SelectObject(dc, pen); HGDIOBJ oldBrush = SelectObject(dc, brush); @@ -216,7 +222,7 @@ static void drawImagePreview(HDC dc, RECT rc, const std::string & spec) { FrameRect(dc, &left, (HBRUSH)GetStockObject(BLACK_BRUSH)); FrameRect(dc, &right, (HBRUSH)GetStockObject(BLACK_BRUSH)); } else if (kind == "gridsize") { - int step = int(std::clamp(previewNumber(value, 8.0), 1.0, 64.0)); + int step = std::max(1, int(previewNumber(value, 8.0) * zoom + 0.5)); HPEN grid = CreatePen(PS_SOLID, 1, RGB(170, 170, 170)); HGDIOBJ oldPen = SelectObject(dc, grid); for (int x = body.left; x <= body.right; x += step) { From faad20e97273ecfd9081cca37ecdebfd7af0b672 Mon Sep 17 00:00:00 2001 From: Dmitriy Morozov Date: Sun, 31 May 2026 08:51:19 -0400 Subject: [PATCH 5/8] Add new stylesheet dialog action --- src/ipe/lua/actions.lua | 47 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/ipe/lua/actions.lua b/src/ipe/lua/actions.lua index 3bfdd3e..0f97970 100644 --- a/src/ipe/lua/actions.lua +++ b/src/ipe/lua/actions.lua @@ -2642,6 +2642,43 @@ local function sheets_namelist(list) return r end +local function sheets_unique_name(list, base) + local used = {} + for _,s in ipairs(list) do + local name = s:name() + if name then used[name] = true end + end + if not used[base] then return base end + local n = 2 + while used[base .. " " .. n] do n = n + 1 end + return base .. " " .. n +end + +local function sheets_new(d, dd) + local i = d:get("list") + if not i then i = 1 end + local name = sheets_unique_name(dd.list, "new") + while true do + name = dd.model:getString("Name of new stylesheet", "New stylesheet", name) + if not name then return end + name = name:match("^%s*(.-)%s*$") + if name == "" then + dd.model:warning("Cannot create stylesheet", "The stylesheet name cannot be empty") + elseif name ~= sheets_unique_name(dd.list, name) then + dd.model:warning("Cannot create stylesheet", + "A stylesheet with this name already exists") + else + break + end + end + local sheet = ipe.Sheet() + sheet:setName(name) + table.insert(dd.list, i, sheet) + d:set("list", sheets_namelist(dd.list)) + d:set("list", i) + dd.modified = true +end + local function sheets_add(d, dd) local i = d:get("list") if not i then i = 1 end @@ -3028,16 +3065,18 @@ function MODEL:action_style_sheets() { label="&Up", action=function (d) sheets_up(d, dd) end }, 3, 4) d:add("down", "button", { label="&Down", action=function (d) sheets_down(d, dd) end }, 4, 4) + d:add("new", "button", + { label="&New", action=function (d) sheets_new(d, dd) end }, 5, 4) if config.toolkit ~= "htmljs" then d:add("add", "button", - { label="&Add", action=function (d) sheets_add(d, dd) end }, 5, 4) + { label="&Add", action=function (d) sheets_add(d, dd) end }, 6, 4) d:add("edit", "button", - { label="Edit", action=function (d) sheets_edit(d, dd) end }, 6, 4) + { label="Edit", action=function (d) sheets_edit(d, dd) end }, 7, 4) d:add("save", "button", - { label="&Save", action=function (d) sheets_save(d, dd) end }, 7, 4) + { label="&Save", action=function (d) sheets_save(d, dd) end }, 8, 4) end d:add("visual", "button", - { label="Visual Edit", action=function (d) sheets_visual_edit(d, dd) end }, 8, 4) + { label="Visual Edit", action=function (d) sheets_visual_edit(d, dd) end }, 9, 4) d:addButton("ok", "&Ok", "accept") d:addButton("cancel", "&Cancel", "reject") d:setStretch("column", 2, 1) From ce3f55c54ff213b80b86bcaf8dee437d36ec530e Mon Sep 17 00:00:00 2001 From: Dmitriy Morozov Date: Sun, 31 May 2026 09:14:11 -0400 Subject: [PATCH 6/8] Improve rendered text style previews --- src/ipe-web/src/dialogs.ts | 37 +++++++++++++++++++------------------ src/ipe/lua/actions.lua | 36 +++++++++++++++++++++++++++++++++++- src/ipeui/ipeui_cocoa.cpp | 34 ++++++++++++++++++---------------- src/ipeui/ipeui_gtk.cpp | 36 ++++++++++++++++++++---------------- src/ipeui/ipeui_qt.cpp | 31 +++++++++++++++---------------- src/ipeui/ipeui_win.cpp | 22 ++++++---------------- 6 files changed, 113 insertions(+), 83 deletions(-) diff --git a/src/ipe-web/src/dialogs.ts b/src/ipe-web/src/dialogs.ts index 360b770..a94f7bb 100644 --- a/src/ipe-web/src/dialogs.ts +++ b/src/ipe-web/src/dialogs.ts @@ -151,22 +151,6 @@ function previewColor(value: string): string { return `rgb(${Math.max(0, Math.min(255, Math.round(255 * r)))}, ${Math.max(0, Math.min(255, Math.round(255 * g)))}, ${Math.max(0, Math.min(255, Math.round(255 * b)))})`; } -function previewNamedSize(value: string, fallback: number): number { - const sizes: Record = { - "\\tiny": 8, - "\\scriptsize": 9, - "\\footnotesize": 10, - "\\small": 12, - "\\normalsize": 14, - "\\large": 18, - "\\Large": 22, - "\\LARGE": 26, - "\\huge": 30, - "\\Huge": 36, - }; - return sizes[value] ?? previewNumber(value, fallback); -} - function previewDashPattern(value: string): number[] { const m = value.match(/\[([^\]]+)\]/); if (!m) return []; @@ -182,6 +166,7 @@ function drawImagePreview(canvas: HTMLCanvasElement, spec: string): void { const zoom = Math.max(0.1, Math.min(100, previewNumber(zoomText, 1))); const ctx = canvas.getContext("2d"); if (ctx == null) return; + canvas.dataset.previewSpec = spec; const w = canvas.width; const h = canvas.height; ctx.clearRect(0, 0, w, h); @@ -197,7 +182,23 @@ function drawImagePreview(canvas: HTMLCanvasElement, spec: string): void { const cy = (top + bottom) / 2; ctx.lineCap = "round"; ctx.lineJoin = "round"; - if (kind === "color") { + if (kind === "imagefile") { + const image = new Image(); + image.addEventListener("load", () => { + if (canvas.dataset.previewSpec !== spec) return; + const logicalWidth = image.width / zoom; + const logicalHeight = image.height / zoom; + const scale = Math.min( + 1, + (right - left) / logicalWidth, + (bottom - top) / logicalHeight, + ); + const width = logicalWidth * scale; + const height = logicalHeight * scale; + ctx.drawImage(image, cx - width / 2, cy - height / 2, width, height); + }); + image.src = value; + } else if (kind === "color") { ctx.fillStyle = previewColor(value); ctx.fillRect(left + 8, top + 8, right - left - 16, bottom - top - 36); ctx.strokeStyle = "black"; @@ -222,7 +223,7 @@ function drawImagePreview(canvas: HTMLCanvasElement, spec: string): void { ctx.setLineDash([]); } else if (kind === "textsize") { ctx.fillStyle = "rgb(30,30,30)"; - ctx.font = `${Math.max(1, previewNamedSize(value, 18) * zoom)}px sans-serif`; + ctx.font = `${Math.max(1, 9 * zoom)}px sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("Sample", cx, cy); diff --git a/src/ipe/lua/actions.lua b/src/ipe/lua/actions.lua index 0f97970..b66fcab 100644 --- a/src/ipe/lua/actions.lua +++ b/src/ipe/lua/actions.lua @@ -2761,7 +2761,40 @@ local function visual_hex_to_rgb(value) return value end +local visual_preview_width = 300 +local visual_preview_height = 130 +local visual_preview_scale = 4 +local visual_text_preview_serial = 0 + +local function visual_text_preview_spec(dd, value) + local preview_name = "__preview_textsize" + local doc = ipe.Document() + local sheets = ipe.Sheets() + local sheet = ipe.Sheet() + sheet:setName("__preview") + local ok = pcall(function () sheet:setAttribute("textsize", preview_name, value) end) + if not ok then return nil end + sheets:insert(1, sheet) + for i,s in ipairs(dd.list) do sheets:insert(i + 1, s:clone()) end + doc:replaceSheets(sheets) + local p = doc[1] + local obj = ipe.Text({ stroke="black", textsize=preview_name }, + "Sample", ipe.Vector(20, 20)) + p:insert(nil, obj, 1, "alpha") + ok = doc:runLatex(dd.model.file_name) + if not ok then return nil end + visual_text_preview_serial = visual_text_preview_serial + 1 + local png = ipe.folder("latex", string.format("style-preview-%d.png", + visual_text_preview_serial)) + dd.model.ui:renderPage(doc, 1, 1, "png", png, + dd.model.ui:zoom() * visual_preview_scale, true, false) + return string.format("imagefile|%s|%g", png, visual_preview_scale) +end + local function visual_preview_spec(dd, c, value) + if c.kind == "textsize" then + return visual_text_preview_spec(dd, value) or "textsize|" .. (value or "") .. "|" .. dd.model.ui:zoom() + end return c.kind .. "|" .. (value or "") .. "|" .. dd.model.ui:zoom() end @@ -2900,7 +2933,8 @@ local function sheets_visual_edit(d0, dd) d:add("color", "input", { color_picker=true }, 4, 4) d:add("help", "label", { label="" }, 5, 3, 1, 2) d:add("preview_label", "label", { label="Preview" }, 6, 3) - d:add("preview", "image", { width=300, height=130 }, 7, 3, 2, 2) + d:add("preview", "image", { width=visual_preview_width, + height=visual_preview_height }, 7, 3, 2, 2) d:add("apply", "button", { label="Apply / Preview", action=function (d) visual_apply_current(d, dd, st) end }, 9, 3) d:add("add", "button", { label="Add", diff --git a/src/ipeui/ipeui_cocoa.cpp b/src/ipeui/ipeui_cocoa.cpp index 9e442de..d3b072c 100644 --- a/src/ipeui/ipeui_cocoa.cpp +++ b/src/ipeui/ipeui_cocoa.cpp @@ -230,20 +230,6 @@ static double previewNumber(const std::string & value, double fallback) { return fallback; } -static double previewNamedSize(const std::string & value, double fallback) { - if (value == "\\tiny") return 8.0; - if (value == "\\scriptsize") return 9.0; - if (value == "\\footnotesize") return 10.0; - if (value == "\\small") return 12.0; - if (value == "\\normalsize") return 14.0; - if (value == "\\large") return 18.0; - if (value == "\\Large") return 22.0; - if (value == "\\LARGE") return 26.0; - if (value == "\\huge") return 30.0; - if (value == "\\Huge") return 36.0; - return previewNumber(value, fallback); -} - static NSColor * previewColor(const std::string & value, double alpha = 1.0) { if (value.size() == 7 && value[0] == '#') { unsigned int r = 0, g = 0, b = 0; @@ -327,7 +313,23 @@ static std::vector previewDashPattern(const std::string & value) { : std::clamp(previewNumber(iSpec.substr(sep2 + 1), 1.0), 0.1, 100.0); NSRect body = NSInsetRect(bounds, 18., 16.); - if (kind == "color") { + if (kind == "imagefile") { + NSImage * image = [[NSImage alloc] initWithContentsOfFile:S2N(value)]; + if (image) { + NSSize size = image.size; + size.width /= zoom; + size.height /= zoom; + double scale = std::min(body.size.width / size.width, + body.size.height / size.height); + scale = std::min(1.0, scale); + NSSize scaled = NSMakeSize(size.width * scale, size.height * scale); + NSRect target = NSMakeRect(NSMidX(body) - scaled.width / 2., + NSMidY(body) - scaled.height / 2., + scaled.width, scaled.height); + [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh]; + [image drawInRect:target]; + } + } else if (kind == "color") { NSRect swatch = NSInsetRect(body, 8., 8.); swatch.size.height -= 28.; [previewColor(value) setFill]; @@ -354,7 +356,7 @@ static std::vector previewDashPattern(const std::string & value) { [[NSColor colorWithCalibratedRed:0.08 green:0.16 blue:0.63 alpha:1.0] setStroke]; [path stroke]; } else if (kind == "textsize") { - double size = std::max(1.0, previewNamedSize(value, 18.0) * zoom); + double size = std::max(1.0, 9.0 * zoom); NSDictionary * attrs = @{ NSFontAttributeName : [NSFont systemFontOfSize:size], NSForegroundColorAttributeName : [NSColor textColor] diff --git a/src/ipeui/ipeui_gtk.cpp b/src/ipeui/ipeui_gtk.cpp index 9459d8a..da559cd 100644 --- a/src/ipeui/ipeui_gtk.cpp +++ b/src/ipeui/ipeui_gtk.cpp @@ -76,20 +76,6 @@ static double previewNumber(const std::string & value, double fallback) { return fallback; } -static double previewNamedSize(const std::string & value, double fallback) { - if (value == "\\tiny") return 8.0; - if (value == "\\scriptsize") return 9.0; - if (value == "\\footnotesize") return 10.0; - if (value == "\\small") return 12.0; - if (value == "\\normalsize") return 14.0; - if (value == "\\large") return 18.0; - if (value == "\\Large") return 22.0; - if (value == "\\LARGE") return 26.0; - if (value == "\\huge") return 30.0; - if (value == "\\Huge") return 36.0; - return previewNumber(value, fallback); -} - static void previewColor(const std::string & value, double & r, double & g, double & b) { if (value.size() == 7 && value[0] == '#') { unsigned int rr = 0, gg = 0, bb = 0; @@ -145,7 +131,25 @@ static void drawImagePreview(cairo_t * cr, int width, int height, const std::str cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND); - if (kind == "color") { + if (kind == "imagefile") { + cairo_surface_t * image = cairo_image_surface_create_from_png(value.c_str()); + if (cairo_surface_status(image) == CAIRO_STATUS_SUCCESS) { + double iw = cairo_image_surface_get_width(image); + double ih = cairo_image_surface_get_height(image); + double scale = std::min((right - left) / (iw / zoom), + (bottom - top) / (ih / zoom)); + scale = std::min(1.0, scale) / zoom; + double x = cx - 0.5 * iw * scale; + double y = cy - 0.5 * ih * scale; + cairo_save(cr); + cairo_translate(cr, x, y); + cairo_scale(cr, scale, scale); + cairo_set_source_surface(cr, image, 0.0, 0.0); + cairo_paint(cr); + cairo_restore(cr); + } + cairo_surface_destroy(image); + } else if (kind == "color") { double r, g, b; previewColor(value, r, g, b); cairo_set_source_rgb(cr, r, g, b); @@ -170,7 +174,7 @@ static void drawImagePreview(cairo_t * cr, int width, int height, const std::str cairo_stroke(cr); cairo_set_dash(cr, nullptr, 0, 0.0); } else if (kind == "textsize") { - double size = std::max(1.0, previewNamedSize(value, 18.0) * zoom); + double size = std::max(1.0, 9.0 * zoom); cairo_select_font_face(cr, "sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); cairo_set_font_size(cr, size); cairo_set_source_rgb(cr, 0.12, 0.12, 0.12); diff --git a/src/ipeui/ipeui_qt.cpp b/src/ipeui/ipeui_qt.cpp index f7cbd53..4342949 100644 --- a/src/ipeui/ipeui_qt.cpp +++ b/src/ipeui/ipeui_qt.cpp @@ -150,20 +150,6 @@ static double previewNumber(const QString & value, double fallback) { return fallback; } -static double previewNamedSize(const QString & value, double fallback) { - if (value == QLatin1String("\\tiny")) return 3.0; - if (value == QLatin1String("\\scriptsize")) return 4.0; - if (value == QLatin1String("\\footnotesize")) return 5.0; - if (value == QLatin1String("\\small")) return 6.0; - if (value == QLatin1String("\\normalsize")) return 7.0; - if (value == QLatin1String("\\large")) return 9.0; - if (value == QLatin1String("\\Large")) return 11.0; - if (value == QLatin1String("\\LARGE")) return 13.0; - if (value == QLatin1String("\\huge")) return 15.0; - if (value == QLatin1String("\\Huge")) return 18.0; - return previewNumber(value, fallback); -} - static QColor previewColor(const QString & value) { if (value.startsWith(QLatin1Char('#'))) return QColor(value); std::istringstream stream(value.toStdString()); @@ -215,13 +201,26 @@ void DialogImage::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::SmoothPixmapTransform); QRectF r = rect().adjusted(0.5, 0.5, -0.5, -0.5); painter.fillRect(r, QColor(255, 255, 220)); painter.setPen(QPen(QColor(160, 160, 130), 1)); painter.drawRect(r); QRectF body = r.adjusted(18, 16, -18, -16); - if (kind == QLatin1String("color")) { + if (kind == QLatin1String("imagefile")) { + QImage image(value); + if (!image.isNull()) { + QSizeF scaled(image.width() / zoom, image.height() / zoom); + double fit = std::min(body.width() / scaled.width(), + body.height() / scaled.height()); + if (fit < 1.0) scaled *= fit; + QRectF target(QPointF(body.center().x() - scaled.width() / 2.0, + body.center().y() - scaled.height() / 2.0), + scaled); + painter.drawImage(target, image); + } + } else if (kind == QLatin1String("color")) { QColor c = previewColor(value); painter.fillRect(body.adjusted(8, 8, -8, -28), c); painter.setPen(Qt::black); @@ -242,7 +241,7 @@ void DialogImage::paintEvent(QPaintEvent *) { painter.drawLine(QPointF(body.left(), y), QPointF(body.right(), y)); } else if (kind == QLatin1String("textsize")) { QFont font = painter.font(); - font.setPointSizeF(std::max(1.0, previewNamedSize(value, 10.0) * zoom)); + font.setPointSizeF(std::max(1.0, 9.0 * zoom)); painter.setFont(font); painter.setPen(QColor(30, 30, 30)); painter.drawText(body, Qt::AlignCenter, QStringLiteral("Sample")); diff --git a/src/ipeui/ipeui_win.cpp b/src/ipeui/ipeui_win.cpp index 0cec635..5e1cb7c 100644 --- a/src/ipeui/ipeui_win.cpp +++ b/src/ipeui/ipeui_win.cpp @@ -87,20 +87,6 @@ static double previewNumber(const std::string & value, double fallback) { return fallback; } -static double previewNamedSize(const std::string & value, double fallback) { - if (value == "\\tiny") return 8.0; - if (value == "\\scriptsize") return 9.0; - if (value == "\\footnotesize") return 10.0; - if (value == "\\small") return 12.0; - if (value == "\\normalsize") return 14.0; - if (value == "\\large") return 18.0; - if (value == "\\Large") return 22.0; - if (value == "\\LARGE") return 26.0; - if (value == "\\huge") return 30.0; - if (value == "\\Huge") return 36.0; - return previewNumber(value, fallback); -} - static COLORREF previewColor(const std::string & value) { if (value.size() == 7 && value[0] == '#') { unsigned int r = 0, g = 0, b = 0; @@ -153,7 +139,11 @@ static void drawImagePreview(HDC dc, RECT rc, const std::string & spec) { int cy = (body.top + body.bottom) / 2; COLORREF blue = RGB(20, 40, 160); COLORREF red = RGB(230, 80, 70); - if (kind == "color") { + if (kind == "imagefile") { + SetBkMode(dc, TRANSPARENT); + DrawTextA(dc, "Rendered text preview unavailable", -1, &body, + DT_CENTER | DT_VCENTER | DT_SINGLELINE); + } else if (kind == "color") { RECT swatch = body; InflateRect(&swatch, -8, -8); swatch.bottom -= 28; @@ -170,7 +160,7 @@ static void drawImagePreview(HDC dc, RECT rc, const std::string & spec) { SelectObject(dc, oldPen); DeleteObject(pen); } else if (kind == "textsize") { - int size = std::max(1, int(previewNamedSize(value, 18.0) * zoom + 0.5)); + int size = std::max(1, int(9.0 * zoom + 0.5)); HFONT font = CreateFontA(-size, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, "Segoe UI"); From c265111017bd4d37df478bd63bfd00477a3df613 Mon Sep 17 00:00:00 2001 From: Dmitriy Morozov Date: Sun, 31 May 2026 09:50:15 -0400 Subject: [PATCH 7/8] Render stylesheet previews with Ipe --- src/ipe-web/src/dialogs.ts | 130 ++++------------------------ src/ipe/lua/actions.lua | 115 +++++++++++++++++++++---- src/ipeui/ipeui_cocoa.cpp | 168 ++----------------------------------- src/ipeui/ipeui_gtk.cpp | 143 +++---------------------------- src/ipeui/ipeui_qt.cpp | 109 ++---------------------- src/ipeui/ipeui_win.cpp | 159 ++--------------------------------- 6 files changed, 142 insertions(+), 682 deletions(-) diff --git a/src/ipe-web/src/dialogs.ts b/src/ipe-web/src/dialogs.ts index a94f7bb..edc2fa0 100644 --- a/src/ipe-web/src/dialogs.ts +++ b/src/ipe-web/src/dialogs.ts @@ -143,24 +143,6 @@ function previewNumber(value: string, fallback: number): number { return Number.isFinite(n) ? n : fallback; } -function previewColor(value: string): string { - if (value.startsWith("#")) return value; - const rgb = value.trim().split(/\s+/).map(Number.parseFloat); - if (rgb.length === 1) rgb[1] = rgb[2] = rgb[0]; - const [r = 0, g = 0, b = 0] = rgb; - return `rgb(${Math.max(0, Math.min(255, Math.round(255 * r)))}, ${Math.max(0, Math.min(255, Math.round(255 * g)))}, ${Math.max(0, Math.min(255, Math.round(255 * b)))})`; -} - -function previewDashPattern(value: string): number[] { - const m = value.match(/\[([^\]]+)\]/); - if (!m) return []; - return m[1] - .trim() - .split(/\s+/) - .map(Number.parseFloat) - .filter(Number.isFinite); -} - function drawImagePreview(canvas: HTMLCanvasElement, spec: string): void { const [kind, value = "", zoomText = "1"] = spec.split("|"); const zoom = Math.max(0.1, Math.min(100, previewNumber(zoomText, 1))); @@ -197,106 +179,22 @@ function drawImagePreview(canvas: HTMLCanvasElement, spec: string): void { const height = logicalHeight * scale; ctx.drawImage(image, cx - width / 2, cy - height / 2, width, height); }); + image.addEventListener("error", () => { + if (canvas.dataset.previewSpec !== spec) return; + ctx.fillStyle = "rgb(90,90,90)"; + ctx.font = "12px sans-serif"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText("Preview unavailable", cx, cy); + }); image.src = value; - } else if (kind === "color") { - ctx.fillStyle = previewColor(value); - ctx.fillRect(left + 8, top + 8, right - left - 16, bottom - top - 36); - ctx.strokeStyle = "black"; - ctx.strokeRect(left + 8, top + 8, right - left - 16, bottom - top - 36); - ctx.fillStyle = "black"; - ctx.font = "12px sans-serif"; - ctx.textAlign = "center"; - ctx.fillText(value, cx, bottom - 4); - } else if (kind === "pen" || kind === "dashstyle") { - ctx.strokeStyle = "rgb(20,40,160)"; - ctx.lineWidth = - kind === "pen" - ? Math.max(0.1, previewNumber(value, 1) * zoom) - : Math.max(0.1, 4 * zoom); - if (kind === "dashstyle") - ctx.setLineDash(previewDashPattern(value).map((x) => x * zoom)); - else ctx.setLineDash([]); - ctx.beginPath(); - ctx.moveTo(left, cy); - ctx.lineTo(right, cy); - ctx.stroke(); - ctx.setLineDash([]); - } else if (kind === "textsize") { - ctx.fillStyle = "rgb(30,30,30)"; - ctx.font = `${Math.max(1, 9 * zoom)}px sans-serif`; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText("Sample", cx, cy); - } else if (kind === "symbolsize") { - const s = Math.max(1, previewNumber(value, 3) * 3 * zoom); - ctx.fillStyle = "rgb(230,80,70)"; - ctx.strokeStyle = "rgb(20,40,160)"; - ctx.lineWidth = 2; - for (const x of [ - left + (right - left) * 0.25, - cx, - left + (right - left) * 0.75, - ]) { - ctx.beginPath(); - ctx.arc(x, cy, s / 2, 0, 2 * Math.PI); - ctx.fill(); - ctx.stroke(); - } - } else if (kind === "arrowsize") { - const s = Math.max(1, previewNumber(value, 7) * 2 * zoom); - ctx.strokeStyle = "rgb(20,40,160)"; - ctx.fillStyle = "rgb(20,40,160)"; - ctx.lineWidth = Math.max(0.1, 4 * zoom); - ctx.beginPath(); - ctx.moveTo(left, cy); - ctx.lineTo(right - s, cy); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(right, cy); - ctx.lineTo(right - s, cy - 0.45 * s); - ctx.lineTo(right - s, cy + 0.45 * s); - ctx.closePath(); - ctx.fill(); - } else if (kind === "opacity") { - const op = Math.max(0, Math.min(1, previewNumber(value, 1))); - ctx.fillStyle = "rgb(80,120,230)"; - ctx.fillRect(left + 12, top + 10, (right - left) * 0.45, bottom - top - 20); - ctx.fillStyle = `rgba(230,70,50,${op})`; - ctx.fillRect(cx - 12, top + 10, (right - left) * 0.45, bottom - top - 20); - } else if (kind === "gridsize") { - const step = Math.max(1, previewNumber(value, 8) * zoom); - ctx.strokeStyle = "rgb(170,170,170)"; - ctx.lineWidth = 1; - for (let x = left; x <= right; x += step) { - ctx.beginPath(); - ctx.moveTo(x, top); - ctx.lineTo(x, bottom); - ctx.stroke(); - } - for (let y = top; y <= bottom; y += step) { - ctx.beginPath(); - ctx.moveTo(left, y); - ctx.lineTo(right, y); - ctx.stroke(); - } - } else if (kind === "anglesize") { - const radians = (previewNumber(value, 45) * Math.PI) / 180; - const ox = left + 0.25 * (right - left); - const oy = bottom - 12; - const len = Math.min((right - left) * 0.65, (bottom - top) * 0.9); - ctx.strokeStyle = "rgb(70,70,70)"; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(ox, oy); - ctx.lineTo(right, oy); - ctx.stroke(); - ctx.strokeStyle = "rgb(20,40,160)"; - ctx.lineWidth = 4; - ctx.beginPath(); - ctx.moveTo(ox, oy); - ctx.lineTo(ox + len * Math.cos(radians), oy - len * Math.sin(radians)); - ctx.stroke(); + return; } + ctx.fillStyle = "rgb(90,90,90)"; + ctx.font = "12px sans-serif"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(value || "Preview unavailable", cx, cy); } export function setupElements( diff --git a/src/ipe/lua/actions.lua b/src/ipe/lua/actions.lua index b66fcab..1464975 100644 --- a/src/ipe/lua/actions.lua +++ b/src/ipe/lua/actions.lua @@ -2764,42 +2764,121 @@ end local visual_preview_width = 300 local visual_preview_height = 130 local visual_preview_scale = 4 -local visual_text_preview_serial = 0 +local visual_preview_serial = 0 + +local function visual_number(value, fallback) + return tonumber(value) or fallback +end + +local function visual_segment_shape(v1, v2) + return { type="curve", closed=false; { type="segment"; v1, v2 } } +end + +local function visual_box_shape(v1, v2) + return { type="curve", closed=true; + { type="segment"; v1, ipe.Vector(v1.x, v2.y) }, + { type="segment"; ipe.Vector(v1.x, v2.y), v2 }, + { type="segment"; v2, ipe.Vector(v2.x, v1.y) } } +end + +local function visual_arc_shape(center, radius, alpha, beta) + local arc = ipe.Arc(ipe.Matrix(radius, 0, 0, radius, center.x, center.y), + alpha, beta) + return { type="curve", closed=false; + { type="arc", arc=arc; + center + radius * ipe.Direction(alpha), + center + radius * ipe.Direction(beta) } } +end + +local function visual_add_object(page, obj) + page:insert(nil, obj, 1, "alpha") +end + +local function visual_add_preview_objects(page, c, preview_name, value) + local V = ipe.Vector + if c.kind == "color" then + visual_add_object(page, ipe.Path({ stroke="black", fill=preview_name, + pathmode="strokedfilled" }, + { visual_box_shape(V(0, 0), V(130, 65)) })) + elseif c.kind == "pen" then + visual_add_object(page, ipe.Path({ stroke="black", pen=preview_name }, + { visual_segment_shape(V(0, 0), V(180, 0)) })) + elseif c.kind == "dashstyle" then + visual_add_object(page, ipe.Path({ stroke="black", pen="fat", dashstyle=preview_name }, + { visual_segment_shape(V(0, 0), V(180, 0)) })) + elseif c.kind == "textsize" then + visual_add_object(page, ipe.Text({ stroke="black", textsize=preview_name }, + "Sample", V(0, 0))) + elseif c.kind == "symbolsize" then + visual_add_object(page, ipe.Reference({ stroke="black", fill="red", + symbolsize=preview_name }, + "mark/disk(sx)", V(0, 0))) + elseif c.kind == "arrowsize" then + visual_add_object(page, ipe.Path({ stroke="black", pen="fat", farrow=true, + farrowsize=preview_name, + farrowshape="arrow/normal(spx)" }, + { visual_segment_shape(V(0, 0), V(170, 0)) }, true)) + elseif c.kind == "opacity" then + visual_add_object(page, ipe.Path({ stroke="black", fill="blue", + pathmode="strokedfilled" }, + { visual_box_shape(V(0, 0), V(80, 60)) })) + visual_add_object(page, ipe.Path({ stroke="black", fill="red", opacity=preview_name, + pathmode="strokedfilled" }, + { visual_box_shape(V(45, 10), V(125, 70)) })) + elseif c.kind == "gridsize" then + local step = math.max(1, visual_number(value, 8)) + for x = 0,160,step do + visual_add_object(page, ipe.Path({ stroke="0.7", pen="normal" }, + { visual_segment_shape(V(x, 0), V(x, 80)) })) + end + for y = 0,80,step do + visual_add_object(page, ipe.Path({ stroke="0.7", pen="normal" }, + { visual_segment_shape(V(0, y), V(160, y)) })) + end + visual_add_object(page, ipe.Path({ stroke="black", pen="fat" }, + { visual_segment_shape(V(0, 0), V(160, 80)) })) + elseif c.kind == "anglesize" then + local alpha = math.rad(visual_number(value, 45)) + local origin = V(0, 0) + local radius = 42 + visual_add_object(page, ipe.Path({ stroke="black", pen="normal" }, + { visual_segment_shape(origin, V(150, 0)) })) + visual_add_object(page, ipe.Path({ stroke="red", pen="fat" }, + { visual_segment_shape(origin, + radius * 3 * ipe.Direction(alpha)) })) + visual_add_object(page, ipe.Path({ stroke="black", pen="normal" }, + { visual_arc_shape(origin, radius, 0, alpha) })) + end +end -local function visual_text_preview_spec(dd, value) +local function visual_preview_spec(dd, c, value) local preview_name = "__preview_textsize" + if c.kind ~= "textsize" then preview_name = "__preview_" .. c.kind end local doc = ipe.Document() local sheets = ipe.Sheets() local sheet = ipe.Sheet() sheet:setName("__preview") - local ok = pcall(function () sheet:setAttribute("textsize", preview_name, value) end) + local ok = pcall(function () sheet:setAttribute(c.kind, preview_name, value) end) if not ok then return nil end sheets:insert(1, sheet) for i,s in ipairs(dd.list) do sheets:insert(i + 1, s:clone()) end doc:replaceSheets(sheets) local p = doc[1] - local obj = ipe.Text({ stroke="black", textsize=preview_name }, - "Sample", ipe.Vector(20, 20)) - p:insert(nil, obj, 1, "alpha") - ok = doc:runLatex(dd.model.file_name) - if not ok then return nil end - visual_text_preview_serial = visual_text_preview_serial + 1 + visual_add_preview_objects(p, c, preview_name, value) + if c.kind == "textsize" then + ok = doc:runLatex(dd.model.file_name) + if not ok then return nil end + end + visual_preview_serial = visual_preview_serial + 1 local png = ipe.folder("latex", string.format("style-preview-%d.png", - visual_text_preview_serial)) + visual_preview_serial)) dd.model.ui:renderPage(doc, 1, 1, "png", png, dd.model.ui:zoom() * visual_preview_scale, true, false) return string.format("imagefile|%s|%g", png, visual_preview_scale) end -local function visual_preview_spec(dd, c, value) - if c.kind == "textsize" then - return visual_text_preview_spec(dd, value) or "textsize|" .. (value or "") .. "|" .. dd.model.ui:zoom() - end - return c.kind .. "|" .. (value or "") .. "|" .. dd.model.ui:zoom() -end - local function visual_set_preview(d, dd, c, value) - d:set("preview", visual_preview_spec(dd, c, value)) + d:set("preview", visual_preview_spec(dd, c, value) or "unavailable|Preview unavailable") end local function visual_load_sheet(sheet) diff --git a/src/ipeui/ipeui_cocoa.cpp b/src/ipeui/ipeui_cocoa.cpp index d3b072c..c1f4278 100644 --- a/src/ipeui/ipeui_cocoa.cpp +++ b/src/ipeui/ipeui_cocoa.cpp @@ -36,7 +36,6 @@ #include "ipeuilayout_cocoa.h" #include -#include #include #include @@ -230,38 +229,6 @@ static double previewNumber(const std::string & value, double fallback) { return fallback; } -static NSColor * previewColor(const std::string & value, double alpha = 1.0) { - if (value.size() == 7 && value[0] == '#') { - unsigned int r = 0, g = 0, b = 0; - sscanf(value.c_str() + 1, "%2x%2x%2x", &r, &g, &b); - return [NSColor colorWithCalibratedRed:r / 255.0 - green:g / 255.0 - blue:b / 255.0 - alpha:alpha]; - } - std::istringstream stream(value); - double r = 0.0, g = 0.0, b = 0.0; - if (stream >> r) { - if (!(stream >> g)) g = r; - if (!(stream >> b)) b = r; - } - return [NSColor colorWithCalibratedRed:std::clamp(r, 0.0, 1.0) - green:std::clamp(g, 0.0, 1.0) - blue:std::clamp(b, 0.0, 1.0) - alpha:alpha]; -} - -static std::vector previewDashPattern(const std::string & value) { - std::vector dashes; - size_t left = value.find('['); - size_t right = value.find(']', left + 1); - if (left == std::string::npos || right == std::string::npos) return dashes; - std::istringstream stream(value.substr(left + 1, right - left - 1)); - double v; - while (stream >> v) dashes.push_back(std::max(0.5, v)); - return dashes; -} - @interface IpeDialogImage : NSView - (instancetype)initWithWidth:(int)width height:(int)height spec:(const std::string &)spec; @@ -328,136 +295,15 @@ static std::vector previewDashPattern(const std::string & value) { scaled.width, scaled.height); [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh]; [image drawInRect:target]; + return; } - } else if (kind == "color") { - NSRect swatch = NSInsetRect(body, 8., 8.); - swatch.size.height -= 28.; - [previewColor(value) setFill]; - NSRectFill(swatch); - [[NSColor blackColor] setStroke]; - NSFrameRect(swatch); - NSDictionary * attrs = @{NSFontAttributeName : [NSFont systemFontOfSize:12.]}; - [S2N(value) drawInRect:NSMakeRect(body.origin.x, NSMaxY(body) - 20., - body.size.width, 18.) - withAttributes:attrs]; - } else if (kind == "pen" || kind == "dashstyle") { - NSBezierPath * path = [NSBezierPath bezierPath]; - [path moveToPoint:NSMakePoint(NSMinX(body), NSMidY(body))]; - [path lineToPoint:NSMakePoint(NSMaxX(body), NSMidY(body))]; - [path setLineWidth:(kind == "pen") - ? std::max(0.1, previewNumber(value, 1.0) * zoom) - : std::max(0.1, 4.0 * zoom)]; - if (kind == "dashstyle") { - std::vector dashes = previewDashPattern(value); - for (CGFloat & dash : dashes) dash *= zoom; - if (!dashes.empty()) - [path setLineDash:dashes.data() count:(NSInteger)dashes.size() phase:0.0]; - } - [[NSColor colorWithCalibratedRed:0.08 green:0.16 blue:0.63 alpha:1.0] setStroke]; - [path stroke]; - } else if (kind == "textsize") { - double size = std::max(1.0, 9.0 * zoom); - NSDictionary * attrs = @{ - NSFontAttributeName : [NSFont systemFontOfSize:size], - NSForegroundColorAttributeName : [NSColor textColor] - }; - [@"Sample" drawInRect:body withAttributes:attrs]; - } else if (kind == "symbolsize") { - double s = std::max(1.0, previewNumber(value, 3.0) * 3.0 * zoom); - NSArray * centers = @[ - [NSValue valueWithPoint:NSMakePoint(NSMinX(body) + body.size.width * 0.25, - NSMidY(body))], - [NSValue valueWithPoint:NSMakePoint(NSMidX(body), NSMidY(body))], - [NSValue valueWithPoint:NSMakePoint(NSMinX(body) + body.size.width * 0.75, - NSMidY(body))] - ]; - [[NSColor colorWithCalibratedRed:0.90 green:0.31 blue:0.27 alpha:1.0] setFill]; - [[NSColor colorWithCalibratedRed:0.08 green:0.16 blue:0.63 alpha:1.0] setStroke]; - NSPoint c = [(NSValue *)[centers objectAtIndex:0] pointValue]; - [[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(c.x - s / 2., c.y - s / 2., s, s)] fill]; - [[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(c.x - s / 2., c.y - s / 2., s, s)] stroke]; - c = [(NSValue *)[centers objectAtIndex:1] pointValue]; - [[NSBezierPath bezierPathWithRect:NSMakeRect(c.x - s / 2., c.y - s / 2., s, s)] fill]; - [[NSBezierPath bezierPathWithRect:NSMakeRect(c.x - s / 2., c.y - s / 2., s, s)] stroke]; - c = [(NSValue *)[centers objectAtIndex:2] pointValue]; - NSBezierPath * diamond = [NSBezierPath bezierPath]; - [diamond moveToPoint:NSMakePoint(c.x, c.y - s / 2.)]; - [diamond lineToPoint:NSMakePoint(c.x + s / 2., c.y)]; - [diamond lineToPoint:NSMakePoint(c.x, c.y + s / 2.)]; - [diamond lineToPoint:NSMakePoint(c.x - s / 2., c.y)]; - [diamond closePath]; - [diamond fill]; - [diamond stroke]; - } else if (kind == "arrowsize") { - double s = std::max(1.0, previewNumber(value, 7.0) * 2.0 * zoom); - NSPoint a = NSMakePoint(NSMinX(body), NSMidY(body)); - NSPoint b = NSMakePoint(NSMaxX(body) - s, NSMidY(body)); - NSBezierPath * line = [NSBezierPath bezierPath]; - [line moveToPoint:a]; - [line lineToPoint:b]; - [line setLineWidth:4.0]; - [[NSColor colorWithCalibratedRed:0.08 green:0.16 blue:0.63 alpha:1.0] setStroke]; - [line stroke]; - NSBezierPath * arrow = [NSBezierPath bezierPath]; - [arrow moveToPoint:NSMakePoint(b.x + s, b.y)]; - [arrow lineToPoint:NSMakePoint(b.x, b.y - 0.45 * s)]; - [arrow lineToPoint:NSMakePoint(b.x, b.y + 0.45 * s)]; - [arrow closePath]; - [[NSColor colorWithCalibratedRed:0.08 green:0.16 blue:0.63 alpha:1.0] setFill]; - [arrow fill]; - } else if (kind == "opacity") { - double op = std::clamp(previewNumber(value, 1.0), 0.0, 1.0); - NSRect left = NSMakeRect(NSMinX(body) + 12., NSMinY(body) + 10., - body.size.width * 0.45, body.size.height - 20.); - NSRect right = NSMakeRect(NSMidX(body) - 12., NSMinY(body) + 10., - body.size.width * 0.45, body.size.height - 20.); - [[NSColor colorWithCalibratedRed:0.31 green:0.47 blue:0.90 alpha:1.0] setFill]; - NSRectFill(left); - [[NSColor colorWithCalibratedRed:0.90 green:0.27 blue:0.20 alpha:op] setFill]; - NSRectFillUsingOperation(right, NSCompositingOperationSourceOver); - [[NSColor blackColor] setStroke]; - NSFrameRect(left); - NSFrameRect(right); - } else if (kind == "gridsize") { - double step = std::max(1.0, previewNumber(value, 8.0) * zoom); - [[NSColor colorWithCalibratedWhite:0.65 alpha:1.0] setStroke]; - for (double x = NSMinX(body); x <= NSMaxX(body); x += step) { - NSBezierPath * p = [NSBezierPath bezierPath]; - [p moveToPoint:NSMakePoint(x, NSMinY(body))]; - [p lineToPoint:NSMakePoint(x, NSMaxY(body))]; - [p stroke]; - } - for (double y = NSMinY(body); y <= NSMaxY(body); y += step) { - NSBezierPath * p = [NSBezierPath bezierPath]; - [p moveToPoint:NSMakePoint(NSMinX(body), y)]; - [p lineToPoint:NSMakePoint(NSMaxX(body), y)]; - [p stroke]; - } - NSBezierPath * diag = [NSBezierPath bezierPath]; - [diag moveToPoint:NSMakePoint(NSMinX(body), NSMaxY(body))]; - [diag lineToPoint:NSMakePoint(NSMaxX(body), NSMinY(body))]; - [diag setLineWidth:3.0]; - [[NSColor colorWithCalibratedRed:0.08 green:0.16 blue:0.63 alpha:1.0] setStroke]; - [diag stroke]; - } else if (kind == "anglesize") { - double degrees = previewNumber(value, 45.0); - double radians = degrees * 3.14159265358979323846 / 180.0; - NSPoint o = NSMakePoint(NSMinX(body) + 0.25 * body.size.width, NSMaxY(body) - 12.); - double len = std::min(body.size.width * 0.65, body.size.height * 0.9); - NSPoint p = NSMakePoint(o.x + len * std::cos(radians), o.y - len * std::sin(radians)); - NSBezierPath * base = [NSBezierPath bezierPath]; - [base moveToPoint:o]; - [base lineToPoint:NSMakePoint(NSMaxX(body), o.y)]; - [base setLineWidth:2.0]; - [[NSColor darkGrayColor] setStroke]; - [base stroke]; - NSBezierPath * ray = [NSBezierPath bezierPath]; - [ray moveToPoint:o]; - [ray lineToPoint:p]; - [ray setLineWidth:4.0]; - [[NSColor colorWithCalibratedRed:0.08 green:0.16 blue:0.63 alpha:1.0] setStroke]; - [ray stroke]; } + NSDictionary * attrs = @{ + NSFontAttributeName : [NSFont systemFontOfSize:12.], + NSForegroundColorAttributeName : [NSColor secondaryLabelColor] + }; + NSString * message = (kind == "imagefile" || value.empty()) ? @"Preview unavailable" : S2N(value); + [message drawInRect:body withAttributes:attrs]; } @end diff --git a/src/ipeui/ipeui_gtk.cpp b/src/ipeui/ipeui_gtk.cpp index da559cd..6d9a48d 100644 --- a/src/ipeui/ipeui_gtk.cpp +++ b/src/ipeui/ipeui_gtk.cpp @@ -31,7 +31,6 @@ #include "ipeui_common.h" #include -#include #include using String = std::string; @@ -76,37 +75,6 @@ static double previewNumber(const std::string & value, double fallback) { return fallback; } -static void previewColor(const std::string & value, double & r, double & g, double & b) { - if (value.size() == 7 && value[0] == '#') { - unsigned int rr = 0, gg = 0, bb = 0; - sscanf(value.c_str() + 1, "%2x%2x%2x", &rr, &gg, &bb); - r = rr / 255.0; - g = gg / 255.0; - b = bb / 255.0; - return; - } - std::istringstream stream(value); - r = g = b = 0.0; - if (stream >> r) { - if (!(stream >> g)) g = r; - if (!(stream >> b)) b = r; - } - r = std::clamp(r, 0.0, 1.0); - g = std::clamp(g, 0.0, 1.0); - b = std::clamp(b, 0.0, 1.0); -} - -static std::vector previewDashPattern(const std::string & value) { - std::vector dashes; - size_t left = value.find('['); - size_t right = value.find(']', left + 1); - if (left == std::string::npos || right == std::string::npos) return dashes; - std::istringstream stream(value.substr(left + 1, right - left - 1)); - double v; - while (stream >> v) dashes.push_back(std::max(0.5, v)); - return dashes; -} - static void drawImagePreview(cairo_t * cr, int width, int height, const std::string & spec) { size_t sep = spec.find('|'); size_t sep2 = sep == std::string::npos ? std::string::npos : spec.find('|', sep + 1); @@ -147,107 +115,22 @@ static void drawImagePreview(cairo_t * cr, int width, int height, const std::str cairo_set_source_surface(cr, image, 0.0, 0.0); cairo_paint(cr); cairo_restore(cr); + cairo_surface_destroy(image); + return; } cairo_surface_destroy(image); - } else if (kind == "color") { - double r, g, b; - previewColor(value, r, g, b); - cairo_set_source_rgb(cr, r, g, b); - cairo_rectangle(cr, left + 8.0, top + 8.0, right - left - 16.0, bottom - top - 36.0); - cairo_fill_preserve(cr); - cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); - cairo_stroke(cr); - cairo_select_font_face(cr, "sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); - cairo_set_font_size(cr, 12.0); - cairo_text_extents_t ext; - cairo_text_extents(cr, value.c_str(), &ext); - cairo_move_to(cr, cx - ext.width / 2.0 - ext.x_bearing, bottom - 5.0); - cairo_show_text(cr, value.c_str()); - } else if (kind == "pen" || kind == "dashstyle") { - cairo_set_source_rgb(cr, 0.08, 0.16, 0.63); - cairo_set_line_width(cr, kind == "pen" ? std::max(0.1, previewNumber(value, 1.0) * zoom) : std::max(0.1, 4.0 * zoom)); - std::vector dashes = previewDashPattern(value); - for (double & dash : dashes) dash *= zoom; - if (kind == "dashstyle" && !dashes.empty()) cairo_set_dash(cr, dashes.data(), dashes.size(), 0.0); - cairo_move_to(cr, left, cy); - cairo_line_to(cr, right, cy); - cairo_stroke(cr); - cairo_set_dash(cr, nullptr, 0, 0.0); - } else if (kind == "textsize") { - double size = std::max(1.0, 9.0 * zoom); - cairo_select_font_face(cr, "sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); - cairo_set_font_size(cr, size); - cairo_set_source_rgb(cr, 0.12, 0.12, 0.12); - cairo_text_extents_t ext; - cairo_text_extents(cr, "Sample", &ext); - cairo_move_to(cr, cx - ext.width / 2.0 - ext.x_bearing, cy - ext.height / 2.0 - ext.y_bearing); - cairo_show_text(cr, "Sample"); - } else if (kind == "symbolsize") { - double s = std::max(1.0, previewNumber(value, 3.0) * 3.0 * zoom); - double xs[] = {left + (right - left) * 0.25, cx, left + (right - left) * 0.75}; - cairo_set_line_width(cr, 2.0); - cairo_set_source_rgb(cr, 0.90, 0.31, 0.27); - for (double x : xs) { - cairo_arc(cr, x, cy, s / 2.0, 0.0, 2.0 * 3.14159265358979323846); - cairo_fill_preserve(cr); - cairo_set_source_rgb(cr, 0.08, 0.16, 0.63); - cairo_stroke(cr); - cairo_set_source_rgb(cr, 0.90, 0.31, 0.27); - } - } else if (kind == "arrowsize") { - double s = std::max(1.0, previewNumber(value, 7.0) * 2.0 * zoom); - cairo_set_source_rgb(cr, 0.08, 0.16, 0.63); - cairo_set_line_width(cr, std::max(0.1, 4.0 * zoom)); - cairo_move_to(cr, left, cy); - cairo_line_to(cr, right - s, cy); - cairo_stroke(cr); - cairo_move_to(cr, right, cy); - cairo_line_to(cr, right - s, cy - 0.45 * s); - cairo_line_to(cr, right - s, cy + 0.45 * s); - cairo_close_path(cr); - cairo_fill(cr); - } else if (kind == "opacity") { - double op = std::clamp(previewNumber(value, 1.0), 0.0, 1.0); - cairo_set_source_rgb(cr, 0.31, 0.47, 0.90); - cairo_rectangle(cr, left + 12.0, top + 10.0, (right - left) * 0.45, bottom - top - 20.0); - cairo_fill(cr); - cairo_set_source_rgba(cr, 0.90, 0.27, 0.20, op); - cairo_rectangle(cr, cx - 12.0, top + 10.0, (right - left) * 0.45, bottom - top - 20.0); - cairo_fill(cr); - } else if (kind == "gridsize") { - double step = std::max(1.0, previewNumber(value, 8.0) * zoom); - cairo_set_source_rgb(cr, 0.67, 0.67, 0.67); - cairo_set_line_width(cr, 1.0); - for (double x = left; x <= right; x += step) { - cairo_move_to(cr, x, top); - cairo_line_to(cr, x, bottom); - } - for (double y = top; y <= bottom; y += step) { - cairo_move_to(cr, left, y); - cairo_line_to(cr, right, y); - } - cairo_stroke(cr); - cairo_set_source_rgb(cr, 0.08, 0.16, 0.63); - cairo_set_line_width(cr, 3.0); - cairo_move_to(cr, left, bottom); - cairo_line_to(cr, right, top); - cairo_stroke(cr); - } else if (kind == "anglesize") { - double radians = previewNumber(value, 45.0) * 3.14159265358979323846 / 180.0; - double ox = left + 0.25 * (right - left); - double oy = bottom - 12.0; - double len = std::min((right - left) * 0.65, (bottom - top) * 0.9); - cairo_set_source_rgb(cr, 0.27, 0.27, 0.27); - cairo_set_line_width(cr, 2.0); - cairo_move_to(cr, ox, oy); - cairo_line_to(cr, right, oy); - cairo_stroke(cr); - cairo_set_source_rgb(cr, 0.08, 0.16, 0.63); - cairo_set_line_width(cr, 4.0); - cairo_move_to(cr, ox, oy); - cairo_line_to(cr, ox + len * std::cos(radians), oy - len * std::sin(radians)); - cairo_stroke(cr); } + const char * message = (kind == "imagefile" || value.empty()) + ? "Preview unavailable" + : value.c_str(); + cairo_select_font_face(cr, "sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); + cairo_set_font_size(cr, 12.0); + cairo_set_source_rgb(cr, 0.35, 0.35, 0.35); + cairo_text_extents_t ext; + cairo_text_extents(cr, message, &ext); + cairo_move_to(cr, cx - ext.width / 2.0 - ext.x_bearing, + cy - ext.height / 2.0 - ext.y_bearing); + cairo_show_text(cr, message); } static gboolean imageExpose(GtkWidget * widget, GdkEventExpose *, gpointer) { diff --git a/src/ipeui/ipeui_qt.cpp b/src/ipeui/ipeui_qt.cpp index 4342949..106107d 100644 --- a/src/ipeui/ipeui_qt.cpp +++ b/src/ipeui/ipeui_qt.cpp @@ -58,7 +58,6 @@ #include #include #include -#include #include #ifdef IPE_SPELLCHECK @@ -150,29 +149,6 @@ static double previewNumber(const QString & value, double fallback) { return fallback; } -static QColor previewColor(const QString & value) { - if (value.startsWith(QLatin1Char('#'))) return QColor(value); - std::istringstream stream(value.toStdString()); - double r = 0.0, g = 0.0, b = 0.0; - if (stream >> r) { - if (!(stream >> g)) g = r; - if (!(stream >> b)) b = r; - } - return QColor::fromRgbF(std::clamp(r, 0.0, 1.0), std::clamp(g, 0.0, 1.0), - std::clamp(b, 0.0, 1.0)); -} - -static QVector previewDashPattern(const QString & value) { - QVector dashes; - int left = value.indexOf(QLatin1Char('[')); - int right = value.indexOf(QLatin1Char(']'), left + 1); - if (left < 0 || right < 0) return dashes; - std::istringstream stream(value.mid(left + 1, right - left - 1).toStdString()); - double v; - while (stream >> v) dashes.push_back(std::max(0.1, v / 4.0)); - return dashes; -} - class DialogImage : public QWidget { public: DialogImage(int width, int height, QWidget * parent = nullptr) @@ -219,87 +195,14 @@ void DialogImage::paintEvent(QPaintEvent *) { body.center().y() - scaled.height() / 2.0), scaled); painter.drawImage(target, image); + return; } - } else if (kind == QLatin1String("color")) { - QColor c = previewColor(value); - painter.fillRect(body.adjusted(8, 8, -8, -28), c); - painter.setPen(Qt::black); - painter.drawRect(body.adjusted(8, 8, -8, -28)); - painter.drawText(body.adjusted(8, body.height() - 18, -8, 0), - Qt::AlignCenter, value); - } else if (kind == QLatin1String("pen") || kind == QLatin1String("dashstyle")) { - QPen pen(QColor(20, 40, 160), - std::max(0.1, previewNumber(value, 1.0) * zoom), Qt::SolidLine, - Qt::RoundCap, Qt::RoundJoin); - if (kind == QLatin1String("dashstyle")) { - pen.setWidthF(std::max(0.1, 4.0 * zoom)); - QVector dashes = previewDashPattern(value); - if (!dashes.isEmpty()) pen.setDashPattern(dashes); - } - painter.setPen(pen); - double y = body.center().y(); - painter.drawLine(QPointF(body.left(), y), QPointF(body.right(), y)); - } else if (kind == QLatin1String("textsize")) { - QFont font = painter.font(); - font.setPointSizeF(std::max(1.0, 9.0 * zoom)); - painter.setFont(font); - painter.setPen(QColor(30, 30, 30)); - painter.drawText(body, Qt::AlignCenter, QStringLiteral("Sample")); - } else if (kind == QLatin1String("symbolsize")) { - double s = std::max(1.0, previewNumber(value, 3.0) * 3.0 * zoom); - painter.setPen(QPen(QColor(20, 40, 160), 2)); - painter.setBrush(QColor(230, 80, 70)); - QPointF c1(body.left() + body.width() * 0.25, body.center().y()); - QPointF c2(body.center().x(), body.center().y()); - QPointF c3(body.left() + body.width() * 0.75, body.center().y()); - painter.drawEllipse(c1, s / 2, s / 2); - painter.drawRect(QRectF(c2.x() - s / 2, c2.y() - s / 2, s, s)); - QPolygonF diamond; - diamond << QPointF(c3.x(), c3.y() - s / 2) << QPointF(c3.x() + s / 2, c3.y()) - << QPointF(c3.x(), c3.y() + s / 2) << QPointF(c3.x() - s / 2, c3.y()); - painter.drawPolygon(diamond); - } else if (kind == QLatin1String("arrowsize")) { - double s = std::max(1.0, previewNumber(value, 7.0) * 2.0 * zoom); - QPointF a(body.left(), body.center().y()); - QPointF b(body.right() - s, body.center().y()); - painter.setPen(QPen(QColor(20, 40, 160), std::max(0.1, 4.0 * zoom), - Qt::SolidLine, Qt::RoundCap)); - painter.drawLine(a, b); - QPolygonF arrow; - arrow << QPointF(b.x() + s, b.y()) << QPointF(b.x(), b.y() - 0.45 * s) - << QPointF(b.x(), b.y() + 0.45 * s); - painter.setBrush(QColor(20, 40, 160)); - painter.drawPolygon(arrow); - } else if (kind == QLatin1String("opacity")) { - double op = std::clamp(previewNumber(value, 1.0), 0.0, 1.0); - QRectF left(body.left() + 12, body.top() + 10, body.width() * 0.45, body.height() - 20); - QRectF right(body.center().x() - 12, body.top() + 10, body.width() * 0.45, - body.height() - 20); - painter.fillRect(left, QColor(80, 120, 230)); - painter.fillRect(right, QColor(230, 70, 50, int(255 * op + 0.5))); - painter.setPen(Qt::black); - painter.drawRect(left); - painter.drawRect(right); - } else if (kind == QLatin1String("gridsize")) { - double step = std::max(1.0, previewNumber(value, 8.0) * zoom); - painter.setPen(QPen(QColor(170, 170, 170), 1)); - for (double x = body.left(); x <= body.right(); x += step) - painter.drawLine(QPointF(x, body.top()), QPointF(x, body.bottom())); - for (double y = body.top(); y <= body.bottom(); y += step) - painter.drawLine(QPointF(body.left(), y), QPointF(body.right(), y)); - painter.setPen(QPen(QColor(20, 40, 160), 3)); - painter.drawLine(body.bottomLeft(), body.topRight()); - } else if (kind == QLatin1String("anglesize")) { - double degrees = previewNumber(value, 45.0); - double radians = degrees * 3.14159265358979323846 / 180.0; - QPointF o(body.left() + 0.25 * body.width(), body.bottom() - 12); - double len = std::min(body.width() * 0.65, body.height() * 0.9); - QPointF p(o.x() + len * std::cos(radians), o.y() - len * std::sin(radians)); - painter.setPen(QPen(QColor(70, 70, 70), 2)); - painter.drawLine(o, QPointF(body.right(), o.y())); - painter.setPen(QPen(QColor(20, 40, 160), 4, Qt::SolidLine, Qt::RoundCap)); - painter.drawLine(o, p); } + painter.setPen(QColor(60, 60, 60)); + painter.drawText(body, Qt::AlignCenter, + (kind == QLatin1String("imagefile") || value.isEmpty()) + ? QStringLiteral("Preview unavailable") + : value); } void LatexHighlighter::applyFormat(const QString & text, QRegularExpression & exp, diff --git a/src/ipeui/ipeui_win.cpp b/src/ipeui/ipeui_win.cpp index 5e1cb7c..ef1daac 100644 --- a/src/ipeui/ipeui_win.cpp +++ b/src/ipeui/ipeui_win.cpp @@ -33,10 +33,6 @@ #include -#include -#include -#include - // -------------------------------------------------------------------- #define IDBASE 9000 @@ -80,37 +76,6 @@ static std::string wideToUtf8(const wchar_t * wbuf) { return std::string(multi.data()); } -static double previewNumber(const std::string & value, double fallback) { - std::istringstream stream(value); - double v; - if (stream >> v) return v; - return fallback; -} - -static COLORREF previewColor(const std::string & value) { - if (value.size() == 7 && value[0] == '#') { - unsigned int r = 0, g = 0, b = 0; - sscanf(value.c_str() + 1, "%2x%2x%2x", &r, &g, &b); - return RGB(r, g, b); - } - std::istringstream stream(value); - double r = 0.0, g = 0.0, b = 0.0; - if (stream >> r) { - if (!(stream >> g)) g = r; - if (!(stream >> b)) b = r; - } - return RGB(int(255.0 * std::clamp(r, 0.0, 1.0) + 0.5), - int(255.0 * std::clamp(g, 0.0, 1.0) + 0.5), - int(255.0 * std::clamp(b, 0.0, 1.0) + 0.5)); -} - -static COLORREF blendColor(COLORREF a, COLORREF b, double t) { - t = std::clamp(t, 0.0, 1.0); - return RGB(int(GetRValue(a) * (1.0 - t) + GetRValue(b) * t + 0.5), - int(GetGValue(a) * (1.0 - t) + GetGValue(b) * t + 0.5), - int(GetBValue(a) * (1.0 - t) + GetBValue(b) * t + 0.5)); -} - static void fillRect(HDC dc, const RECT & r, COLORREF color) { HBRUSH b = CreateSolidBrush(color); FillRect(dc, &r, b); @@ -119,14 +84,10 @@ static void fillRect(HDC dc, const RECT & r, COLORREF color) { static void drawImagePreview(HDC dc, RECT rc, const std::string & spec) { size_t sep = spec.find('|'); - size_t sep2 = sep == std::string::npos ? std::string::npos : spec.find('|', sep + 1); std::string kind = sep == std::string::npos ? spec : spec.substr(0, sep); std::string value = sep == std::string::npos ? std::string() - : spec.substr(sep + 1, sep2 - sep - 1); - double zoom = sep2 == std::string::npos - ? 1.0 - : std::clamp(previewNumber(spec.substr(sep2 + 1), 1.0), 0.1, 100.0); + : spec.substr(sep + 1, spec.find('|', sep + 1) - sep - 1); fillRect(dc, rc, RGB(255, 255, 220)); HBRUSH frame = CreateSolidBrush(RGB(160, 160, 130)); @@ -135,120 +96,10 @@ static void drawImagePreview(HDC dc, RECT rc, const std::string & spec) { RECT body = rc; InflateRect(&body, -18, -16); - int cx = (body.left + body.right) / 2; - int cy = (body.top + body.bottom) / 2; - COLORREF blue = RGB(20, 40, 160); - COLORREF red = RGB(230, 80, 70); - if (kind == "imagefile") { - SetBkMode(dc, TRANSPARENT); - DrawTextA(dc, "Rendered text preview unavailable", -1, &body, - DT_CENTER | DT_VCENTER | DT_SINGLELINE); - } else if (kind == "color") { - RECT swatch = body; - InflateRect(&swatch, -8, -8); - swatch.bottom -= 28; - fillRect(dc, swatch, previewColor(value)); - FrameRect(dc, &swatch, (HBRUSH)GetStockObject(BLACK_BRUSH)); - SetBkMode(dc, TRANSPARENT); - DrawTextA(dc, value.c_str(), -1, &body, DT_CENTER | DT_BOTTOM | DT_SINGLELINE); - } else if (kind == "pen" || kind == "dashstyle") { - int width = std::max(1, int((kind == "pen" ? previewNumber(value, 1.0) : 4.0) * zoom + 0.5)); - HPEN pen = CreatePen(kind == "dashstyle" ? PS_DASH : PS_SOLID, width, blue); - HGDIOBJ oldPen = SelectObject(dc, pen); - MoveToEx(dc, body.left, cy, nullptr); - LineTo(dc, body.right, cy); - SelectObject(dc, oldPen); - DeleteObject(pen); - } else if (kind == "textsize") { - int size = std::max(1, int(9.0 * zoom + 0.5)); - HFONT font = CreateFontA(-size, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, - DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, - DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, "Segoe UI"); - HGDIOBJ oldFont = SelectObject(dc, font); - SetBkMode(dc, TRANSPARENT); - DrawTextA(dc, "Sample", -1, &body, DT_CENTER | DT_VCENTER | DT_SINGLELINE); - SelectObject(dc, oldFont); - DeleteObject(font); - } else if (kind == "symbolsize") { - int s = std::max(1, int(previewNumber(value, 3.0) * 3.0 * zoom + 0.5)); - HPEN pen = CreatePen(PS_SOLID, 2, blue); - HBRUSH brush = CreateSolidBrush(red); - HGDIOBJ oldPen = SelectObject(dc, pen); - HGDIOBJ oldBrush = SelectObject(dc, brush); - int xs[] = {body.left + (body.right - body.left) / 4, cx, - body.left + 3 * (body.right - body.left) / 4}; - Ellipse(dc, xs[0] - s / 2, cy - s / 2, xs[0] + s / 2, cy + s / 2); - Rectangle(dc, xs[1] - s / 2, cy - s / 2, xs[1] + s / 2, cy + s / 2); - POINT diamond[] = {{xs[2], cy - s / 2}, {xs[2] + s / 2, cy}, - {xs[2], cy + s / 2}, {xs[2] - s / 2, cy}}; - Polygon(dc, diamond, 4); - SelectObject(dc, oldBrush); - SelectObject(dc, oldPen); - DeleteObject(brush); - DeleteObject(pen); - } else if (kind == "arrowsize") { - int s = std::max(1, int(previewNumber(value, 7.0) * 2.0 * zoom + 0.5)); - HPEN pen = CreatePen(PS_SOLID, std::max(1, int(4.0 * zoom + 0.5)), blue); - HBRUSH brush = CreateSolidBrush(blue); - HGDIOBJ oldPen = SelectObject(dc, pen); - HGDIOBJ oldBrush = SelectObject(dc, brush); - MoveToEx(dc, body.left, cy, nullptr); - LineTo(dc, body.right - s, cy); - POINT arrow[] = {{body.right, cy}, {body.right - s, int(cy - 0.45 * s)}, - {body.right - s, int(cy + 0.45 * s)}}; - Polygon(dc, arrow, 3); - SelectObject(dc, oldBrush); - SelectObject(dc, oldPen); - DeleteObject(brush); - DeleteObject(pen); - } else if (kind == "opacity") { - double op = std::clamp(previewNumber(value, 1.0), 0.0, 1.0); - RECT left = {body.left + 12, body.top + 10, - body.left + 12 + int((body.right - body.left) * 0.45), body.bottom - 10}; - RECT right = {cx - 12, body.top + 10, - cx - 12 + int((body.right - body.left) * 0.45), body.bottom - 10}; - fillRect(dc, left, RGB(80, 120, 230)); - fillRect(dc, right, blendColor(RGB(255, 255, 220), red, op)); - FrameRect(dc, &left, (HBRUSH)GetStockObject(BLACK_BRUSH)); - FrameRect(dc, &right, (HBRUSH)GetStockObject(BLACK_BRUSH)); - } else if (kind == "gridsize") { - int step = std::max(1, int(previewNumber(value, 8.0) * zoom + 0.5)); - HPEN grid = CreatePen(PS_SOLID, 1, RGB(170, 170, 170)); - HGDIOBJ oldPen = SelectObject(dc, grid); - for (int x = body.left; x <= body.right; x += step) { - MoveToEx(dc, x, body.top, nullptr); - LineTo(dc, x, body.bottom); - } - for (int y = body.top; y <= body.bottom; y += step) { - MoveToEx(dc, body.left, y, nullptr); - LineTo(dc, body.right, y); - } - SelectObject(dc, oldPen); - DeleteObject(grid); - HPEN pen = CreatePen(PS_SOLID, 3, blue); - oldPen = SelectObject(dc, pen); - MoveToEx(dc, body.left, body.bottom, nullptr); - LineTo(dc, body.right, body.top); - SelectObject(dc, oldPen); - DeleteObject(pen); - } else if (kind == "anglesize") { - double radians = previewNumber(value, 45.0) * 3.14159265358979323846 / 180.0; - int ox = body.left + (body.right - body.left) / 4; - int oy = body.bottom - 12; - double len = std::min((body.right - body.left) * 0.65, (body.bottom - body.top) * 0.9); - HPEN base = CreatePen(PS_SOLID, 2, RGB(70, 70, 70)); - HGDIOBJ oldPen = SelectObject(dc, base); - MoveToEx(dc, ox, oy, nullptr); - LineTo(dc, body.right, oy); - SelectObject(dc, oldPen); - DeleteObject(base); - HPEN pen = CreatePen(PS_SOLID, 4, blue); - oldPen = SelectObject(dc, pen); - MoveToEx(dc, ox, oy, nullptr); - LineTo(dc, int(ox + len * std::cos(radians)), int(oy - len * std::sin(radians))); - SelectObject(dc, oldPen); - DeleteObject(pen); - } + (void)kind; + SetBkMode(dc, TRANSPARENT); + DrawTextA(dc, kind == "imagefile" || value.empty() ? "Preview unavailable" : value.c_str(), + -1, &body, DT_CENTER | DT_VCENTER | DT_SINGLELINE); } void buildFlags(std::vector & t, DWORD flags) { From c5579caa1f91665409a19d5ee3570182c989cc49 Mon Sep 17 00:00:00 2001 From: Dmitriy Morozov Date: Mon, 1 Jun 2026 10:12:39 -0400 Subject: [PATCH 8/8] Simplify visual color editing --- src/ipe/lua/actions.lua | 52 +++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/src/ipe/lua/actions.lua b/src/ipe/lua/actions.lua index 1464975..60d44b9 100644 --- a/src/ipe/lua/actions.lua +++ b/src/ipe/lua/actions.lua @@ -2698,7 +2698,7 @@ end local visual_style_categories = { { label="Colors", kind="color", color=true, default="#000000", - help="Color values use the color picker or three numbers between 0 and 1." }, + help="Color values are #rrggbb or three numbers between 0 and 1." }, { label="Pen widths", kind="pen", default="1", help="Pen widths are numbers in Ipe points." }, { label="Dash styles", kind="dashstyle", default="[4] 0", @@ -2750,13 +2750,33 @@ local function visual_rgb_to_hex(value) end local function visual_hex_to_rgb(value) - if #value == 7 and value:sub(1, 1) == "#" then - local r = tonumber(value:sub(2, 3), 16) - local g = tonumber(value:sub(4, 5), 16) - local b = tonumber(value:sub(6, 7), 16) - if r and g and b then - return string.format("%.6g %.6g %.6g", r / 255, g / 255, b / 255) - end + value = value:match("^%s*(.-)%s*$") + local function hex_digit(c) + local b = string.byte(c) + if string.byte("0") <= b and b <= string.byte("9") then + return b - string.byte("0") + elseif string.byte("a") <= b and b <= string.byte("f") then + return b - string.byte("a") + 10 + elseif string.byte("A") <= b and b <= string.byte("F") then + return b - string.byte("A") + 10 + end + end + local function hex_byte(s) + return 16 * hex_digit(s:sub(1, 1)) + hex_digit(s:sub(2, 2)) + end + local hex = value:match("^#(%x%x%x%x%x%x)$") + if hex then + local r = hex_byte(hex:sub(1, 2)) + local g = hex_byte(hex:sub(3, 4)) + local b = hex_byte(hex:sub(5, 6)) + return string.format("%.6g %.6g %.6g", r / 255, g / 255, b / 255) + end + local r1, g1, b1 = value:match("^#(%x)(%x)(%x)$") + if r1 then + local r = hex_byte(r1 .. r1) + local g = hex_byte(g1 .. g1) + local b = hex_byte(b1 .. b1) + return string.format("%.6g %.6g %.6g", r / 255, g / 255, b / 255) end return value end @@ -2901,26 +2921,22 @@ local function visual_set_fields(d, dd, st) if #entries == 0 then st.current = nil d:set("name", "") - d:set("value", "") - d:set("color", c.default or "#000000") + d:set("value", c.color and (c.default or "#000000") or "") visual_set_preview(d, dd, c, c.color and visual_hex_to_rgb(c.default or "#000000") or c.default) else st.current = math.max(1, math.min(st.current or 1, #entries)) d:set("items", st.current) d:set("name", entries[st.current].name) if c.color then - d:set("value", entries[st.current].value) - d:set("color", visual_rgb_to_hex(entries[st.current].value)) + d:set("value", visual_rgb_to_hex(entries[st.current].value)) else d:set("value", entries[st.current].value) - d:set("color", "") end visual_set_preview(d, dd, c, entries[st.current].value) end d:set("value_label", c.color and "Color" or "Value") d:set("help", c.help) - d:setEnabled("value", not c.color) - d:setEnabled("color", c.color) + d:setEnabled("value", true) st.updating = false end @@ -2933,7 +2949,7 @@ local function visual_apply_current(d, dd, st) dd.model:warning("Cannot update stylesheet", "The symbolic name cannot be empty") return false end - local value = c.color and visual_hex_to_rgb(d:get("color")) or d:get("value") + local value = c.color and visual_hex_to_rgb(d:get("value")) or d:get("value") if value == "" then dd.model:warning("Cannot update stylesheet", "The value cannot be empty") return false @@ -2950,6 +2966,7 @@ local function visual_apply_current(d, dd, st) st.updating = true d:set("items", visual_entry_names(entries)) d:set("items", st.current) + if c.color then d:set("value", visual_rgb_to_hex(value)) end visual_set_preview(d, dd, c, value) st.updating = false return true @@ -3009,7 +3026,6 @@ local function sheets_visual_edit(d0, dd) d:add("name", "input", { select_all=true }, 2, 4) d:add("value_label", "label", { label="Value" }, 3, 3) d:add("value", "input", {}, 3, 4) - d:add("color", "input", { color_picker=true }, 4, 4) d:add("help", "label", { label="" }, 5, 3, 1, 2) d:add("preview_label", "label", { label="Preview" }, 6, 3) d:add("preview", "image", { width=visual_preview_width, @@ -3025,7 +3041,7 @@ local function sheets_visual_edit(d0, dd) name = "new" end if name == "" then name = "new" end - local value = c.color and visual_hex_to_rgb(d:get("color")) or d:get("value") + local value = c.color and visual_hex_to_rgb(d:get("value")) or d:get("value") if value == "" then value = c.color and visual_hex_to_rgb(c.default) or c.default end