From 980bbab3253cdd87a9d5353dc8a1210687bb1222 Mon Sep 17 00:00:00 2001 From: Moust4ki Date: Thu, 15 Oct 2026 16:13:27 +0200 Subject: [PATCH 1/7] Report a problem: a window that writes a precise report for GitHub The guide window's flag (beside the NPC directory's pin, reached up from the pin in focus mode), the DING_REPORT binding and its chord, and /ding bug open a Ding window that shows in plain words what the report will contain: the guide, the step as the window numbers it and its index in the guide's file, its summary and keys, the position, the character, the step's quests and the log's quests the next 3 steps name with their counters, the arrow's target, the route settings, the theme, TomTom, the versions and the date (UTC). Never the character's name, the realm or the account. Four ready reasons for a gamepad, the player's words with a keyboard (the box never takes the keyboard from the chat), then "Create the text": GitHub Markdown with English labels, selected for Ctrl+C, with the issue form's link. The last 20 reports stay in DingDB.reports ("Restore all defaults" keeps them) for a player who can only send the file. Tests: report_test, the report window in dialog_layout_test and single_action_test (every language, gamepad and mouse), a smoke scenario with the gamepad alone. Co-Authored-By: Claude Opus 5.5 --- Ding/Bindings.xml | 3 + Ding/Core/Commands.lua | 4 + Ding/Core/Data.lua | 8 + Ding/Core/Ding.lua | 3 + Ding/Core/GuideText.lua | 5 +- Ding/Core/Import.lua | 36 +- Ding/Core/Locales.lua | 1 + Ding/Core/Report.lua | 371 +++++++++++++++ Ding/Ding.toc | 2 + Ding/Locales/deDE.lua | 41 +- Ding/Locales/enUS.lua | 41 +- Ding/Locales/esES.lua | 41 +- Ding/Locales/frFR.lua | 41 +- Ding/Locales/koKR.lua | 41 +- Ding/Locales/ptBR.lua | 41 +- Ding/Locales/zhCN.lua | 41 +- Ding/Locales/zhTW.lua | 41 +- Ding/UI/Config.lua | 1 + Ding/UI/Gamepad.lua | 3 + Ding/UI/Panel.lua | 85 +++- Ding/UI/Report.lua | 702 +++++++++++++++++++++++++++++ tools/smoke_test.lua | 40 ++ tools/tests/dialog_layout_test.lua | 48 +- tools/tests/report_test.lua | 408 +++++++++++++++++ tools/tests/single_action_test.lua | 67 ++- tools/wow_mock.lua | 5 +- 26 files changed, 2071 insertions(+), 49 deletions(-) create mode 100644 Ding/Core/Report.lua create mode 100644 Ding/UI/Report.lua create mode 100644 tools/tests/report_test.lua diff --git a/Ding/Bindings.xml b/Ding/Bindings.xml index e7687df..fadfbaa 100644 --- a/Ding/Bindings.xml +++ b/Ding/Bindings.xml @@ -29,6 +29,9 @@ Ding.Panel:CycleTarget() + + Ding.ReportWindow:Toggle() + diff --git a/Ding/Core/Commands.lua b/Ding/Core/Commands.lua index 53f4370..c65d731 100644 --- a/Ding/Core/Commands.lua +++ b/Ding/Core/Commands.lua @@ -51,6 +51,10 @@ SlashCmdList.DING = function(input) else Ding:Print(L.TRIP_NONE) end + elseif cmd == "bug" or cmd == "report" then + -- Report a problem: what the guide shows and where the character stands, as a text for + -- GitHub (UI/Report.lua). + Ding.ReportWindow:Open() elseif cmd == "find" then -- The NPC directory's search: a service, a weapon, a profession or a name. Ding.Finder:Find(rest) diff --git a/Ding/Core/Data.lua b/Ding/Core/Data.lua index b2af3cc..8de7bdc 100644 --- a/Ding/Core/Data.lua +++ b/Ding/Core/Data.lua @@ -165,6 +165,14 @@ function D:QuestName(questID) return get("Quest", "name", questID) or string.format(L.UNKNOWN_QUEST, questID) end +-- Its name in English whatever the client's language (the bug reports, read by the guide's +-- authors: Core/Report.lua). +function D:QuestEnglishName(questID) + local name = raw("Quest", "name", questID) + if type(name) == "string" and name ~= "" then return name end + return self:QuestName(questID) +end + function D:QuestRequiredLevel(questID) return get("Quest", "requiredLevel", questID) or 0 end diff --git a/Ding/Core/Ding.lua b/Ding/Core/Ding.lua index 000bacd..deff7e4 100644 --- a/Ding/Core/Ding.lua +++ b/Ding/Core/Ding.lua @@ -58,6 +58,9 @@ local DEFAULTS = { -- [action] = "PADLSHOULDER+PADDUP": gamepad chords Ding watches itself (UI/Gamepad.lua); -- kept by "Restore all defaults", like the game's key bindings. chords = {}, + -- The last reports of "Report a problem" (Core/Report.lua), oldest first: { at, title, text }. + -- Not a setting: "Restore all defaults" keeps them. + reports = {}, } Ding.DEFAULTS = DEFAULTS local SETTINGS = { "panel", "arrow", "item", "minimap", "ui" } diff --git a/Ding/Core/GuideText.lua b/Ding/Core/GuideText.lua index c2010cf..4de5989 100644 --- a/Ding/Core/GuideText.lua +++ b/Ding/Core/GuideText.lua @@ -285,7 +285,8 @@ end -- values under that key (the steps' keys); opts.compact: one line; opts.layout: [key] = -- "lines" (a list, one value per line, each on one line: the steps) or "blocks" (a list, one -- value per line, each laid out like the whole: the guides of a pack). The whole is one --- entry per line; any other table is on one line. +-- entry per line (opts.inline: on one line, spaced like a step); any other table is on one +-- line. local function ranks(list) local rank = {} for i, k in ipairs(list or {}) do rank[k] = i end @@ -332,5 +333,5 @@ function T.Write(value, opts) return "{\n" .. inner .. table.concat(parts, ",\n" .. inner) .. ",\n" .. indent .. "}" end - return write(value, "", "block", rootRank) + return write(value, "", not opts.inline and "block" or nil, rootRank) end diff --git a/Ding/Core/Import.lua b/Ding/Core/Import.lua index e11dea2..ed1081c 100644 --- a/Ding/Core/Import.lua +++ b/Ding/Core/Import.lua @@ -632,6 +632,21 @@ end --------------------------------------------------------------------------- local ID_KINDS = { quests = true, items = true, npcs = true, objects = true } +-- A step as written in a guide; `without`: keys left out. +local function exportStep(step, without) + local s = {} + for key, v in pairs(step) do + local kind = not (without and without[key]) and STEP_KEYS[key] or nil + -- One id alone, as guides are written (the registry made it a list). + if ID_KINDS[kind] and type(v) == "table" and #v == 1 and type(v[1]) == "number" then + s[key] = v[1] + elseif kind then + s[key] = copy(v) + end + end + return s +end + local function exportGuide(guide, pack, inPack) local out = {} for _, key in ipairs({ "id", "title", "faction", "races", "classes", "minLevel", "maxLevel", "next" }) do @@ -639,22 +654,17 @@ local function exportGuide(guide, pack, inPack) end if not inPack and pack and pack.category and pack.category ~= "leveling" then out.category = pack.category end out.steps = {} - for i, step in ipairs(guide.steps) do - local s = {} - for key, v in pairs(step) do - local kind = STEP_KEYS[key] - -- One id alone, as guides are written (the registry made it a list). - if ID_KINDS[kind] and type(v) == "table" and #v == 1 and type(v[1]) == "number" then - s[key] = v[1] - elseif kind then - s[key] = copy(v) - end - end - out.steps[i] = s - end + for i, step in ipairs(guide.steps) do out.steps[i] = exportStep(step) end return out end +-- A step's keys on one line, as a guide writes them, without its texts (text, note, why): +-- "{ turnin = 783, accept = 7, go = { 1429, 48.2, 41.6 } }" (the bug reports: Core/Report.lua). +local PROSE = { text = true, note = true, why = true } +function I:StepText(step) + return Text.Write(exportStep(step, PROSE), { inline = true, order = STEP_ORDER }) +end + -- A pack (by its id) or a guide (by its id; the active one by default) as text, in full or -- on one line (compact). A pack of one guide of its own id (a guide imported alone) is -- written as that guide. nil when there is no such guide. diff --git a/Ding/Core/Locales.lua b/Ding/Core/Locales.lua index 75a31a3..aa80a0c 100644 --- a/Ding/Core/Locales.lua +++ b/Ding/Core/Locales.lua @@ -29,5 +29,6 @@ BINDING_NAME_DING_SKIP = L.BIND_SKIP BINDING_NAME_DING_UNSKIP = L.BIND_UNSKIP BINDING_NAME_DING_FINDER = L.BIND_FINDER BINDING_NAME_DING_CYCLE = L.BIND_CYCLE +BINDING_NAME_DING_REPORT = L.BIND_REPORT _G["BINDING_NAME_CLICK DingItemButton:LeftButton"] = L.BIND_ITEM _G["BINDING_NAME_CLICK DingItemButton:Target"] = L.BIND_TARGET diff --git a/Ding/Core/Report.lua b/Ding/Core/Report.lua new file mode 100644 index 0000000..a509057 --- /dev/null +++ b/Ding/Core/Report.lua @@ -0,0 +1,371 @@ +-- Report a problem (the window: UI/Report.lua): what Ding writes down for the people who make +-- the guide, as GitHub Markdown with English labels (the player's words stay as typed). The +-- guide pack, its section and the step (its number as the guide window shows it, its index in +-- the guide's file, its summary and its keys); where the character stands (map, position, +-- level, race, class, side); the step's quests and the quests of the log the next steps name +-- (state, objectives with the client's counters); what the arrow points at; the settings that +-- change the route, the theme, TomTom; Ding's version, the client's language and build, the +-- date (UTC). Never the character's name, the realm nor the account. The last KEEP reports stay +-- in DingDB.reports, outside the settings ("Restore all defaults" keeps them): a player who +-- cannot copy a text (a gamepad alone) can send the file. +local _, Ding = ... +local Compat = Ding.Compat + +local R = {} +Ding.Report = R + +R.KEEP = 20 +R.NEXT_STEPS = 3 -- the next steps whose quests of the log are listed too +R.ISSUE_URL = "https://github.com/moust4ki/DingGuideWoWForever/issues/new?template=ding-report.yml" +R.FILE = "WTF\\Account\\\\SavedVariables\\Ding.lua" + +-- The ready reasons (with a gamepad, typing is hard): the window's words, the text's. +R.REASONS = { + { key = "npc", label = "REPORT_REASON_NPC", text = "The NPC is not there" }, + { key = "arrow", label = "REPORT_REASON_ARROW", text = "The arrow points to the wrong place" }, + { key = "quest", label = "REPORT_REASON_QUEST", text = "The quest is not offered" }, + { key = "other", label = "REPORT_REASON_OTHER", text = "Something else" }, +} + +-- The settings that change the route (Ding.db.ui). +R.OPTIONS = { "elites", "dungeons", "hearth", "deathSkip", "skipGrey", "serviceSteps", "trainerSteps", "flightSteps" } + +local STATES = { log = "in the log", ready = "ready to turn in", turnedin = "turned in", none = "not in the log" } + +--------------------------------------------------------------------------- +-- Helpers +--------------------------------------------------------------------------- +-- A text of the game or of a guide on one line, without the client's escapes (colors, +-- textures): a "|" left alone would start one in the window's box. +local function clean(s) + if type(s) ~= "string" then return nil end + s = s:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""):gsub("|T.-|t", ""):gsub("|", "/") + s = s:gsub("%s*\n%s*", " "):gsub("^%s+", ""):gsub("%s+$", "") + return s +end +R.Clean = clean + +local function one(n) return ("%.1f"):format(n) end + +-- fn's result, nil when it fails: a part that cannot be read must not stop the report. +local function try(fn, ...) + local ok, a, b, c, d = pcall(fn, ...) + if ok then return a, b, c, d end + return nil +end + +local function number(v) + v = Compat.Plain(v) + return type(v) == "number" and v or nil +end + +--------------------------------------------------------------------------- +-- What the report holds +--------------------------------------------------------------------------- +local function questState(q) + if Compat.IsInLog(q) then return Compat.IsQuestReady(q) and "ready" or "log" end + if Ding.Engine:IsTurnedIn(q) then return "turnedin" end + return "none" +end + +-- A quest: id, its English name (and the client's when it differs), level, state, where the +-- guide names it, and its objectives as the client gives them (text, have / need, done). +local function questInfo(q, where) + local D = Ding.Data + local info = { id = q, where = where, state = questState(q), objectives = {} } + info.name = clean(try(D.QuestEnglishName, D, q)) + local shown = clean(try(D.QuestName, D, q)) + if shown ~= info.name then info.shownName = shown end + info.level = try(D.QuestLevel, D, q) + if info.state == "log" or info.state == "ready" then + for _, o in ipairs(Compat.GetObjectives(q)) do + info.objectives[#info.objectives + 1] = { text = clean(o.text), have = tonumber(o.numFulfilled), + need = tonumber(o.numRequired), finished = o.finished } + end + end + return info +end + +local function stepQuests(step, out) + out = out or {} + for _, key in ipairs({ "accept", "complete", "turnin" }) do + local list = step and step[key] + if type(list) == "number" then list = { list } end + for _, q in ipairs(type(list) == "table" and list or {}) do out[#out + 1] = q end + end + return out +end + +-- The guide: the step on screen (the one held a moment after it is done, as the window shows +-- it), its number among the character's steps and its index in the guide's file. +local function guideInfo() + local E, P = Ding.Engine, Ding.Panel + local guide, index = E.guide, E.index + if not guide then return nil end + local held = P and P.holding + if held and held.guide then guide, index = held.guide, held.index end + local step = guide.steps[index] + local pack = guide.pack and Ding:GetGuidePack(guide.pack) + local info = { + id = guide.id, title = clean(guide.title), packId = guide.pack, pack = clean(pack and pack.title), + index = index, count = #guide.steps, finished = E.finished and not held, step = step, + } + info.number, info.total = E:StepNumber(info.finished and #guide.steps or index, guide) + if step then + info.summary = clean(try(E.Summary, E, step)) + info.keys = try(Ding.Import.StepText, Ding.Import, step) + end + return info +end + +-- The step's quests whatever their state, then the quests of the log that the next steps name. +local function questsInfo(guide) + local list, seen = {}, {} + if not guide then return list end + for _, q in ipairs(stepQuests(guide.step)) do + if not seen[q] then + seen[q] = true + list[#list + 1] = questInfo(q, "this step") + end + end + local E = Ding.Engine + local where = {} + for i, s in ipairs(E.guide and E.guide.steps or {}) do where[s] = i end + for _, s in ipairs(try(E.NextSteps, E, R.NEXT_STEPS) or {}) do + local index = where[s] + for _, q in ipairs(stepQuests(s)) do + if not seen[q] and Compat.IsInLog(q) then + seen[q] = true + local n = index and E:StepNumber(index) or nil + list[#list + 1] = questInfo(q, n and ("step " .. n) or "a next step") + end + end + end + return list +end + +local function playerInfo() + local p = {} + p.map, p.x, p.y = Compat.GetPlayerMapPosition() + p.zone = p.map and clean(Compat.MapName(p.map)) or nil + p.place = clean(Compat.ZoneText()) + p.level = number(try(UnitLevel, "player")) + p.race, p.raceName = Compat.PlayerRace(), clean((try(UnitRace, "player"))) + p.class, p.className = Compat.PlayerClass(), clean((try(UnitClass, "player"))) + local faction, factionName = try(UnitFactionGroup, "player") + p.faction, p.factionName = clean(faction), clean(factionName) + return p +end + +-- Yards between the player and a point { map, x, y }; nil on another continent or unknown. +local function distanceTo(point) + local continent, px, py = Compat.GetPlayerWorld() + if not continent or not point.map then return nil end + local c, wx, wy = Compat.MapToWorld(point.map, point.x, point.y) + if c ~= continent or not wx then return nil end + return math.sqrt((wx - px) ^ 2 + (wy - py) ^ 2), true +end + +-- What the arrow points at now: the point it shows (a farming loop's stop, a flight master on +-- the way), the travel hint, the directory's trip, the step's number of candidate points. +local function arrowInfo() + local A, E = Ding.Arrow, Ding.Engine + local info = { candidates = #(E.candidates or {}) } + local point = A and (A.shownPoint or A.point) + if point then + info.label = clean(point.label) + info.map, info.x, info.y = point.map, point.x, point.y + info.zone = point.map and clean(Compat.MapName(point.map)) or nil + info.npc, info.object = point.npc, point.object + info.distance = try(distanceTo, point) + end + info.hint = A and clean(A.hint) + local trip = Ding.Finder and Ding.Finder.trip + if trip then info.trip = clean(trip.label .. ": " .. (trip.name or "?")) end + info.shown = Ding.db.arrow.shown and true or false + return info +end + +local function settingsInfo() + local ui = Ding.db.ui + local options = {} + for _, key in ipairs(R.OPTIONS) do options[#options + 1] = { key = key, on = ui[key] and true or false } end + return { + options = options, theme = ui.theme, + tomtom = Ding.db.arrow.tomtom and true or false, tomtomLoaded = Compat.IsAddOnLoaded("TomTom"), + } +end + +local function clientInfo() + local c = { version = Ding.version, locale = try(GetLocale) } + local version, build = try(GetBuildInfo) + c.build = version and (version .. (build and (" (" .. build .. ")") or "")) or nil + c.at = try(date, "!%Y-%m-%d %H:%M UTC") + return c +end + +-- Everything the report holds, read now (the window shows it, the text writes it). +function R:Collect() + local guide = guideInfo() + return { + guide = guide, + player = playerInfo(), + quests = questsInfo(guide), + arrow = arrowInfo(), + settings = settingsInfo(), + client = clientInfo(), + } +end + +--------------------------------------------------------------------------- +-- The text (GitHub Markdown) +--------------------------------------------------------------------------- +function R:Reason(key) + for _, reason in ipairs(self.REASONS) do + if reason.key == key then return reason end + end + return nil +end + +-- The player's first line, cut after a word near 60 characters ("..."). +local function firstLine(s) + s = (s or ""):match("^%s*([^\n]*)") or "" + s = s:gsub("%s+$", "") + if #s <= 60 then return s end + local cut = s:sub(1, 57) + local words = cut:match("^(.*%S)%s+%S*$") + if words and #words >= 30 then cut = words end + -- Never half a character (UTF-8). + cut = cut:gsub("[\192-\255][\128-\191]*$", function(c) + local lead = c:byte(1) + local size = lead >= 240 and 4 or lead >= 224 and 3 or 2 + return #c == size and c or "" + end) + return cut .. "..." +end + +-- The short line a player can use as the issue's title: the step, then what went wrong (the +-- player's first words when no ready reason says it). +function R:Title(info, reasonKey, comment) + local g = info.guide + local where = g and ("Step %d/%d of %s"):format(g.number, g.total, g.title or g.id) or "No guide" + local reason = self:Reason(reasonKey) + local what = reason and reason.key ~= "other" and reason.text or nil + if not what and firstLine(comment) ~= "" then what = firstLine(comment) end + return "Ding! report: " .. where .. (what and (": " .. what) or "") +end + +local function questLine(q) + local name = q.name or "?" + if q.shownName then name = name .. " / " .. q.shownName end + local level = q.level and q.level > 0 and (", level " .. q.level) or "" + return ("- [%d] %s%s: %s (%s)"):format(q.id, name, level, STATES[q.state] or q.state, q.where) +end + +local function objectiveLine(o) + local count = (o.have and o.need) and (" (%d/%d)"):format(o.have, o.need) or "" + return (" - %s%s%s"):format(o.text or "?", count, o.finished and ", done" or "") +end + +function R:Text(info, reasonKey, comment) + local out = {} + local function add(line) out[#out + 1] = line end + add(self:Title(info, reasonKey, comment)) + add("") + local reason = self:Reason(reasonKey) + add("**What went wrong:** " .. (reason and reason.text or "not chosen")) + comment = (comment or ""):gsub("^%s+", ""):gsub("%s+$", "") + if comment ~= "" then + add("") + add("**In the player's words:**") + for line in (comment .. "\n"):gmatch("(.-)\r?\n") do add("> " .. line) end + end + + local g = info.guide + add("") + add("### Guide") + if g then + add(("- Pack: %s (`%s`)"):format(g.pack or "?", tostring(g.packId))) + add(("- Section: %s (`%s`)"):format(g.title or "?", g.id)) + add(("- Step: %d / %d, index %d of %d in the guide's file%s"):format(g.number, g.total, g.index, g.count, + g.finished and ", section finished" or "")) + if g.summary and g.summary ~= "" then add("- Summary: " .. g.summary) end + if g.keys then add("- Keys: `" .. g.keys .. "`") end + else + add("- No guide") + end + + local p = info.player + add("") + add("### Player") + if p.map then + add(("- Map: %s, UiMap %d%s"):format(p.zone or "?", p.map, p.place and (", " .. p.place) or "")) + add(("- Position: %s, %s"):format(one(p.x), one(p.y))) + else + add("- Position: unknown here" .. (p.place and (" (" .. p.place .. ")") or "")) + end + add(("- Level %s %s %s, %s"):format(p.level or "?", p.race or "?", p.class or "?", p.faction or "?")) + + add("") + add("### Quests") + if #info.quests == 0 then add("- None for this step") end + for _, q in ipairs(info.quests) do + add(questLine(q)) + for _, o in ipairs(q.objectives) do add(objectiveLine(o)) end + end + + local a = info.arrow + add("") + add("### Arrow") + if a.label or a.map then + local where = a.map and (" at %s, %s, UiMap %d%s"):format(one(a.x or 0), one(a.y or 0), a.map, + a.zone and (" (" .. a.zone .. ")") or "") or "" + local ids = (a.npc and (", NPC " .. a.npc) or "") .. (a.object and (", object " .. a.object) or "") + local far = a.distance and (", %d yards away"):format(math.floor(a.distance + 0.5)) or ", distance unknown" + add(("- Points to: %s%s%s%s"):format(a.label or "?", ids, where, far)) + else + add("- No target") + end + if a.hint then add("- Travel: " .. a.hint) end + if a.trip then add("- Directory trip: " .. a.trip) end + add(("- Candidate points for this step: %d"):format(a.candidates)) + if not a.shown then add("- The arrow is hidden") end + + local s = info.settings + add("") + add("### Settings") + local options = {} + for _, o in ipairs(s.options) do options[#options + 1] = o.key .. " " .. (o.on and "on" or "off") end + add("- Route: " .. table.concat(options, ", ")) + add(("- Theme: %s; TomTom: %s%s"):format(tostring(s.theme), s.tomtom and "on" or "off", + s.tomtomLoaded and " (installed)" or "")) + + local c = info.client + add("") + add("### Client") + add(("- Ding! %s, locale %s, game %s"):format(tostring(c.version), tostring(c.locale), c.build or "unknown")) + add("- " .. (c.at or "date unknown")) + return table.concat(out, "\n") +end + +--------------------------------------------------------------------------- +-- The reports kept (DingDB.reports: { at, title, text }, oldest first) +--------------------------------------------------------------------------- +function R:Saved() + local db = Ding.db + if type(db.reports) ~= "table" then db.reports = {} end + return db.reports +end + +function R:Save(info, text, title) + local list = self:Saved() + list[#list + 1] = { at = info.client.at, title = title, text = text } + while #list > self.KEEP do table.remove(list, 1) end +end + +-- The report of `info` with a reason and the player's words: its text, kept with the others. +function R:Make(info, reasonKey, comment) + local text = self:Text(info, reasonKey, comment) + self:Save(info, text, self:Title(info, reasonKey, comment)) + return text +end diff --git a/Ding/Ding.toc b/Ding/Ding.toc index f788721..ed89107 100644 --- a/Ding/Ding.toc +++ b/Ding/Ding.toc @@ -61,6 +61,7 @@ Core\Rewards.lua Core\GuideText.lua Core\Import.lua Core\Why.lua +Core\Report.lua UI\Themes.lua UI\Widgets.lua UI\Gamepad.lua @@ -78,6 +79,7 @@ UI\MinimapPins.lua UI\QuestMobs.lua UI\Config.lua UI\Setup.lua +UI\Report.lua Core\Commands.lua Guides\Leveling\Alliance\01.lua diff --git a/Ding/Locales/deDE.lua b/Ding/Locales/deDE.lua index 56b4441..5ab2bc2 100644 --- a/Ding/Locales/deDE.lua +++ b/Ding/Locales/deDE.lua @@ -84,7 +84,7 @@ Ding.Locales.deDE = { ARROW_MODE_SET = "Pfeilausrichtung: Einstellung %d von 4", DEBUG_ON = "Debugmodus an.", DEBUG_OFF = "Debugmodus aus.", - HELP = "Befehle: /ding (zeigen/verbergen), /ding config, /ding intro, /ding list, /ding guide , /ding where, /ding passages [n], /ding import, /ding export [id], /ding find , /ding cancel, /ding next, /ding prev, /ding skip, /ding back, /ding reset, /ding scale <0.5-1.5>, /ding arrow, /ding arrowflip, /ding minimap, /ding debug", + HELP = "Befehle: /ding (zeigen/verbergen), /ding config, /ding intro, /ding list, /ding guide , /ding where, /ding passages [n], /ding import, /ding export [id], /ding find , /ding cancel, /ding bug, /ding next, /ding prev, /ding skip, /ding back, /ding reset, /ding scale <0.5-1.5>, /ding arrow, /ding arrowflip, /ding minimap, /ding debug", -- Guide window QUOTE = "„%s“", @@ -378,6 +378,7 @@ Ding.Locales.deDE = { CITY_RATCHET = "Ratschet", CITY_EVERLOOK = "Ewige Warte", BIND_FINDER = "NSC-Verzeichnis", + BIND_REPORT = "Problem melden", TRAINING_DUE = "Neue Zauber bei deinem Klassenlehrer (%s)", TRAINING_DUE_GUESS = "Neue Zauber warten vielleicht bei deinem Klassenlehrer (Stufe %d)", TRAINING_NEAR = "Dein Klassenlehrer %s ist gleich hier: neue Zauber warten.", @@ -508,6 +509,44 @@ Ding.Locales.deDE = { EXPORT_ONE_LINE = "In einer Zeile", EXPORT_FULL = "Vollständig", EXPORT_NONE = "Kein Leitfaden zum Exportieren: %s", + + -- Report a problem (UI/Report.lua; the report itself is written in English, for GitHub) + REPORT_INTRO = "Ding! schreibt auf, was der Guide zeigt und wo du bist, für die Leute, die den Guide machen. Nichts wird von selbst gesendet: Du veröffentlichst den Text selbst.", + REPORT_CONTAINS = "Was der Bericht enthält", + REPORT_PART_GUIDE = "Der Guide: %s, %s", + REPORT_PART_STEP = "Schritt %d / %d (Nummer %d in der Datei des Guides): %s", + REPORT_NO_GUIDE = "Der Guide: keiner gewählt", + REPORT_PART_PLACE = "Wo du bist: %s, bei %s, %s", + REPORT_PLACE_UNKNOWN = "Wo du bist: Das Spiel gibt deine Position hier nicht an", + REPORT_PART_CHARACTER = "Dein Charakter: Stufe %s, %s, %s, %s", + REPORT_PART_QUESTS = "Quests: %s", + REPORT_QUESTS_NONE = "Quests: keine für diesen Schritt", + REPORT_QUEST = "%s: %s", + REPORT_STATE_LOG = "in deinem Questlog", + REPORT_STATE_READY = "bereit zum Abgeben", + REPORT_STATE_DONE = "abgegeben", + REPORT_STATE_NONE = "nicht in deinem Questlog", + REPORT_PART_ARROW = "Der Pfeil: %s, %s", + REPORT_ARROW_NONE = "Der Pfeil: gerade kein Ziel", + REPORT_PART_SETTINGS = "Deine Einstellungen, die die Route ändern, das Design und ob TomTom an ist", + REPORT_PART_CLIENT = "Die Version von Ding! (%s), Version und Sprache des Spiels, Datum und Uhrzeit", + REPORT_PART_PRIVATE = "Nie der Name deines Charakters, dein Realm oder dein Account.", + REPORT_WHAT = "Was stimmt nicht?", + REPORT_REASON_NPC = "Der NSC ist nicht da", + REPORT_REASON_ARROW = "Der Pfeil zeigt an die falsche Stelle", + REPORT_REASON_QUEST = "Die Quest wird nicht angeboten", + REPORT_REASON_OTHER = "Etwas anderes", + REPORT_COMMENT = "In deinen Worten (freiwillig)", + REPORT_COMMENT_HINT = "Was du gesehen hast, was du erwartet hast: ein paar Worte helfen sehr.", + REPORT_CREATE = "Text erstellen", + REPORT_MOUSE = "Klick: wählen · Esc: schließen", + REPORT_COPY = "Der Text ist markiert: Strg+C zum Kopieren.", + REPORT_CLIPBOARD = "Ein Addon kann keinen Text in die Zwischenablage legen: Ding! markiert ihn, du kopierst ihn.", + REPORT_PASTE = "Füge ihn dann auf GitHub in einen neuen Bericht ein, unter dieser Adresse:", + REPORT_SELECT_TEXT = "Text markieren", + REPORT_SELECT_LINK = "Link markieren", + REPORT_SAVED = "Keine Tastatur zur Hand? Ding! behält deine letzten %d Berichte in %s, gespeichert beim Ausloggen oder Neuladen: Du kannst diese Datei schicken.", + IMPORT_LINE = "Zeile %d: %s", IMPORT_STEP = "Schritt %d: %s", IMPORT_GUIDE = "Leitfaden %s: %s", diff --git a/Ding/Locales/enUS.lua b/Ding/Locales/enUS.lua index b36aede..2653da5 100644 --- a/Ding/Locales/enUS.lua +++ b/Ding/Locales/enUS.lua @@ -87,7 +87,7 @@ Ding.Locales.enUS = { ARROW_MODE_SET = "Arrow orientation: setting %d of 4", DEBUG_ON = "Debug mode on.", DEBUG_OFF = "Debug mode off.", - HELP = "Commands: /ding (show/hide), /ding config, /ding intro, /ding list, /ding guide , /ding where, /ding passages [n], /ding import, /ding export [id], /ding find , /ding cancel, /ding next, /ding prev, /ding skip, /ding back, /ding reset, /ding scale <0.5-1.5>, /ding arrow, /ding arrowflip, /ding minimap, /ding debug", + HELP = "Commands: /ding (show/hide), /ding config, /ding intro, /ding list, /ding guide , /ding where, /ding passages [n], /ding import, /ding export [id], /ding find , /ding cancel, /ding bug, /ding next, /ding prev, /ding skip, /ding back, /ding reset, /ding scale <0.5-1.5>, /ding arrow, /ding arrowflip, /ding minimap, /ding debug", -- Guide window QUOTE = "“%s”", @@ -381,6 +381,7 @@ Ding.Locales.enUS = { CITY_RATCHET = "Ratchet", CITY_EVERLOOK = "Everlook", BIND_FINDER = "NPC directory", + BIND_REPORT = "Report a problem", TRAINING_DUE = "New spells at your class trainer (%s)", TRAINING_DUE_GUESS = "New spells may wait at your class trainer (level %d)", TRAINING_NEAR = "Your class trainer %s is right here: new spells are waiting.", @@ -511,6 +512,44 @@ Ding.Locales.enUS = { EXPORT_ONE_LINE = "On one line", EXPORT_FULL = "In full", EXPORT_NONE = "No guide to export: %s", + + -- Report a problem (UI/Report.lua; the report itself is written in English, for GitHub) + REPORT_INTRO = "Ding! writes down what the guide shows and where you are, for the people who make the guide. Nothing is sent by itself: you post the text yourself.", + REPORT_CONTAINS = "What the report will contain", + REPORT_PART_GUIDE = "The guide: %s, %s", + REPORT_PART_STEP = "Step %d / %d (number %d in the guide's file): %s", + REPORT_NO_GUIDE = "The guide: none chosen", + REPORT_PART_PLACE = "Where you are: %s, at %s, %s", + REPORT_PLACE_UNKNOWN = "Where you are: the game does not give your position here", + REPORT_PART_CHARACTER = "Your character: level %s, %s, %s, %s", + REPORT_PART_QUESTS = "Quests: %s", + REPORT_QUESTS_NONE = "Quests: none for this step", + REPORT_QUEST = "%s: %s", + REPORT_STATE_LOG = "in your log", + REPORT_STATE_READY = "ready to turn in", + REPORT_STATE_DONE = "turned in", + REPORT_STATE_NONE = "not in your log", + REPORT_PART_ARROW = "The arrow: %s, %s", + REPORT_ARROW_NONE = "The arrow: no target right now", + REPORT_PART_SETTINGS = "Your settings that change the route, the theme, and whether TomTom is on", + REPORT_PART_CLIENT = "The version of Ding! (%s), the game's version and language, the date and time", + REPORT_PART_PRIVATE = "Never your character's name, your realm or your account.", + REPORT_WHAT = "What went wrong?", + REPORT_REASON_NPC = "The NPC is not there", + REPORT_REASON_ARROW = "The arrow points to the wrong place", + REPORT_REASON_QUEST = "The quest is not offered", + REPORT_REASON_OTHER = "Something else", + REPORT_COMMENT = "In your words (optional)", + REPORT_COMMENT_HINT = "What you saw, what you expected: a few words help a lot.", + REPORT_CREATE = "Create the text", + REPORT_MOUSE = "Click to choose · Esc: close", + REPORT_COPY = "The text is selected: Ctrl+C to copy.", + REPORT_CLIPBOARD = "An addon cannot put text on the clipboard: Ding! selects it, you copy it.", + REPORT_PASTE = "Then paste it into a new report on GitHub, at this address:", + REPORT_SELECT_TEXT = "Select the text", + REPORT_SELECT_LINK = "Select the link", + REPORT_SAVED = "No keyboard at hand? Ding! keeps your last %d reports in %s, saved when you log out or reload: you can send that file.", + IMPORT_LINE = "Line %d: %s", IMPORT_STEP = "Step %d: %s", IMPORT_GUIDE = "Guide %s: %s", diff --git a/Ding/Locales/esES.lua b/Ding/Locales/esES.lua index ee67461..b0f29f0 100644 --- a/Ding/Locales/esES.lua +++ b/Ding/Locales/esES.lua @@ -87,7 +87,7 @@ Ding.Locales.esES = { ARROW_MODE_SET = "Orientación de la flecha: ajuste %d de 4", DEBUG_ON = "Depuración activada.", DEBUG_OFF = "Depuración desactivada.", - HELP = "Comandos: /ding (mostrar/ocultar), /ding config, /ding intro, /ding list, /ding guide , /ding where, /ding passages [n], /ding import, /ding export [id], /ding find , /ding cancel, /ding next, /ding prev, /ding skip, /ding back, /ding reset, /ding scale <0.5-1.5>, /ding arrow, /ding arrowflip, /ding minimap, /ding debug", + HELP = "Comandos: /ding (mostrar/ocultar), /ding config, /ding intro, /ding list, /ding guide , /ding where, /ding passages [n], /ding import, /ding export [id], /ding find , /ding cancel, /ding bug, /ding next, /ding prev, /ding skip, /ding back, /ding reset, /ding scale <0.5-1.5>, /ding arrow, /ding arrowflip, /ding minimap, /ding debug", -- Guide window QUOTE = "«%s»", @@ -381,6 +381,7 @@ Ding.Locales.esES = { CITY_RATCHET = "Trinquete", CITY_EVERLOOK = "Vista Eterna", BIND_FINDER = "Directorio de PNJ", + BIND_REPORT = "Informar de un problema", TRAINING_DUE = "Hechizos nuevos con tu instructor de clase (%s)", TRAINING_DUE_GUESS = "Puede que haya hechizos nuevos con tu instructor de clase (nivel %d)", TRAINING_NEAR = "Tu instructor de clase %s está aquí mismo: te esperan hechizos nuevos.", @@ -511,6 +512,44 @@ Ding.Locales.esES = { EXPORT_ONE_LINE = "En una línea", EXPORT_FULL = "Completo", EXPORT_NONE = "No hay guía que exportar: %s", + + -- Report a problem (UI/Report.lua; the report itself is written in English, for GitHub) + REPORT_INTRO = "Ding! anota lo que muestra la guía y dónde estás, para quienes hacen la guía. No se envía nada solo: tú publicas el texto.", + REPORT_CONTAINS = "Lo que contendrá el informe", + REPORT_PART_GUIDE = "La guía: %s, %s", + REPORT_PART_STEP = "Paso %d / %d (número %d en el archivo de la guía): %s", + REPORT_NO_GUIDE = "La guía: ninguna elegida", + REPORT_PART_PLACE = "Dónde estás: %s, en %s, %s", + REPORT_PLACE_UNKNOWN = "Dónde estás: el juego no da tu posición aquí", + REPORT_PART_CHARACTER = "Tu personaje: nivel %s, %s, %s, %s", + REPORT_PART_QUESTS = "Misiones: %s", + REPORT_QUESTS_NONE = "Misiones: ninguna para este paso", + REPORT_QUEST = "%s: %s", + REPORT_STATE_LOG = "en tu registro", + REPORT_STATE_READY = "lista para entregar", + REPORT_STATE_DONE = "entregada", + REPORT_STATE_NONE = "no está en tu registro", + REPORT_PART_ARROW = "La flecha: %s, %s", + REPORT_ARROW_NONE = "La flecha: sin objetivo por ahora", + REPORT_PART_SETTINGS = "Tus ajustes que cambian la ruta, el tema y si TomTom está activado", + REPORT_PART_CLIENT = "La versión de Ding! (%s), la versión y el idioma del juego, la fecha y la hora", + REPORT_PART_PRIVATE = "Nunca el nombre de tu personaje, tu reino ni tu cuenta.", + REPORT_WHAT = "¿Qué ha fallado?", + REPORT_REASON_NPC = "El PNJ no está", + REPORT_REASON_ARROW = "La flecha señala el lugar equivocado", + REPORT_REASON_QUEST = "No ofrecen la misión", + REPORT_REASON_OTHER = "Otra cosa", + REPORT_COMMENT = "Con tus palabras (opcional)", + REPORT_COMMENT_HINT = "Lo que viste, lo que esperabas: unas palabras ayudan mucho.", + REPORT_CREATE = "Crear el texto", + REPORT_MOUSE = "Clic: elegir · Esc: cerrar", + REPORT_COPY = "El texto está seleccionado: Ctrl+C para copiarlo.", + REPORT_CLIPBOARD = "Un addon no puede poner texto en el portapapeles: Ding! lo selecciona y tú lo copias.", + REPORT_PASTE = "Después pégalo en un informe nuevo en GitHub, en esta dirección:", + REPORT_SELECT_TEXT = "Seleccionar el texto", + REPORT_SELECT_LINK = "Seleccionar el enlace", + REPORT_SAVED = "¿Sin teclado a mano? Ding! guarda tus últimos %d informes en %s, escrito al cerrar sesión o recargar: puedes enviar ese archivo.", + IMPORT_LINE = "Línea %d: %s", IMPORT_STEP = "Paso %d: %s", IMPORT_GUIDE = "Guía %s: %s", diff --git a/Ding/Locales/frFR.lua b/Ding/Locales/frFR.lua index 09af26c..f85888c 100644 --- a/Ding/Locales/frFR.lua +++ b/Ding/Locales/frFR.lua @@ -84,7 +84,7 @@ Ding.Locales.frFR = { ARROW_MODE_SET = "Orientation de la flèche : réglage %d sur 4", DEBUG_ON = "Mode débogage activé.", DEBUG_OFF = "Mode débogage désactivé.", - HELP = "Commandes : /ding (afficher/masquer), /ding config, /ding intro, /ding list, /ding guide , /ding where, /ding passages [n], /ding import, /ding export [id], /ding find , /ding cancel, /ding next, /ding prev, /ding skip, /ding back, /ding reset, /ding scale <0.5-1.5>, /ding arrow, /ding arrowflip, /ding minimap, /ding debug", + HELP = "Commandes : /ding (afficher/masquer), /ding config, /ding intro, /ding list, /ding guide , /ding where, /ding passages [n], /ding import, /ding export [id], /ding find , /ding cancel, /ding bug, /ding next, /ding prev, /ding skip, /ding back, /ding reset, /ding scale <0.5-1.5>, /ding arrow, /ding arrowflip, /ding minimap, /ding debug", QUOTE = "« %s »", STEP_LABEL = "Étape %d", @@ -377,6 +377,7 @@ Ding.Locales.frFR = { CITY_RATCHET = "Cabestan", CITY_EVERLOOK = "Long-guet", BIND_FINDER = "Annuaire des PNJ", + BIND_REPORT = "Signaler un problème", TRAINING_DUE = "Nouveaux sorts chez ton maître de classe (%s)", TRAINING_DUE_GUESS = "De nouveaux sorts t'attendent peut-être chez ton maître de classe (niveau %d)", TRAINING_NEAR = "Ton maître de classe %s est juste là : de nouveaux sorts t'attendent.", @@ -506,6 +507,44 @@ Ding.Locales.frFR = { EXPORT_ONE_LINE = "Sur une ligne", EXPORT_FULL = "En entier", EXPORT_NONE = "Aucun guide à exporter : %s", + + -- Report a problem (UI/Report.lua; the report itself is written in English, for GitHub) + REPORT_INTRO = "Ding! note ce que montre le guide et où tu te trouves, pour ceux qui font le guide. Rien n'est envoyé tout seul : c'est toi qui publies le texte.", + REPORT_CONTAINS = "Ce que contiendra le rapport", + REPORT_PART_GUIDE = "Le guide : %s, %s", + REPORT_PART_STEP = "Étape %d / %d (numéro %d dans le fichier du guide) : %s", + REPORT_NO_GUIDE = "Le guide : aucun choisi", + REPORT_PART_PLACE = "Où tu es : %s, en %s, %s", + REPORT_PLACE_UNKNOWN = "Où tu es : le jeu ne donne pas ta position ici", + REPORT_PART_CHARACTER = "Ton personnage : niveau %s, %s, %s, %s", + REPORT_PART_QUESTS = "Quêtes : %s", + REPORT_QUESTS_NONE = "Quêtes : aucune pour cette étape", + REPORT_QUEST = "%s : %s", + REPORT_STATE_LOG = "dans ton journal", + REPORT_STATE_READY = "prête à rendre", + REPORT_STATE_DONE = "rendue", + REPORT_STATE_NONE = "pas dans ton journal", + REPORT_PART_ARROW = "La flèche : %s, %s", + REPORT_ARROW_NONE = "La flèche : aucune cible pour l'instant", + REPORT_PART_SETTINGS = "Tes réglages qui changent le parcours, le thème, et si TomTom est activé", + REPORT_PART_CLIENT = "La version de Ding! (%s), la version et la langue du jeu, la date et l'heure", + REPORT_PART_PRIVATE = "Jamais le nom de ton personnage, ton royaume ni ton compte.", + REPORT_WHAT = "Qu'est-ce qui ne va pas ?", + REPORT_REASON_NPC = "Le PNJ n'est pas là", + REPORT_REASON_ARROW = "La flèche indique le mauvais endroit", + REPORT_REASON_QUEST = "La quête n'est pas proposée", + REPORT_REASON_OTHER = "Autre chose", + REPORT_COMMENT = "Avec tes mots (facultatif)", + REPORT_COMMENT_HINT = "Ce que tu as vu, ce que tu attendais : quelques mots aident beaucoup.", + REPORT_CREATE = "Créer le texte", + REPORT_MOUSE = "Clic : choisir · Échap : fermer", + REPORT_COPY = "Le texte est sélectionné : Ctrl+C pour le copier.", + REPORT_CLIPBOARD = "Un addon ne peut pas mettre de texte dans le presse-papiers : Ding! le sélectionne, tu le copies.", + REPORT_PASTE = "Colle-le ensuite dans un nouveau rapport sur GitHub, à cette adresse :", + REPORT_SELECT_TEXT = "Sélectionner le texte", + REPORT_SELECT_LINK = "Sélectionner le lien", + REPORT_SAVED = "Pas de clavier sous la main ? Ding! garde tes %d derniers rapports dans %s, enregistré quand tu te déconnectes ou recharges l'interface : tu peux envoyer ce fichier.", + IMPORT_LINE = "Ligne %d : %s", IMPORT_STEP = "Étape %d : %s", IMPORT_GUIDE = "Guide %s : %s", diff --git a/Ding/Locales/koKR.lua b/Ding/Locales/koKR.lua index 0fa8d7f..8095a33 100644 --- a/Ding/Locales/koKR.lua +++ b/Ding/Locales/koKR.lua @@ -85,7 +85,7 @@ Ding.Locales.koKR = { ARROW_MODE_SET = "화살표 방향: 설정 %d/4", DEBUG_ON = "디버그 모드 켜짐.", DEBUG_OFF = "디버그 모드 꺼짐.", - HELP = "명령어: /ding (표시/숨기기), /ding config, /ding intro, /ding list, /ding guide , /ding where, /ding passages [n], /ding import, /ding export [id], /ding find <텍스트>, /ding cancel, /ding next, /ding prev, /ding skip, /ding back, /ding reset, /ding scale <0.5-1.5>, /ding arrow, /ding arrowflip, /ding minimap, /ding debug", + HELP = "명령어: /ding (표시/숨기기), /ding config, /ding intro, /ding list, /ding guide , /ding where, /ding passages [n], /ding import, /ding export [id], /ding find <텍스트>, /ding cancel, /ding bug, /ding next, /ding prev, /ding skip, /ding back, /ding reset, /ding scale <0.5-1.5>, /ding arrow, /ding arrowflip, /ding minimap, /ding debug", -- Guide window QUOTE = "“%s”", @@ -379,6 +379,7 @@ Ding.Locales.koKR = { CITY_RATCHET = "톱니항", CITY_EVERLOOK = "눈망루 마을", BIND_FINDER = "NPC 안내소", + BIND_REPORT = "문제 신고", TRAINING_DUE = "직업 전문가에게 새 주문 (%s)", TRAINING_DUE_GUESS = "직업 전문가에게 새 주문이 있을 수 있습니다 (%d 레벨)", TRAINING_NEAR = "직업 전문가 %s: 바로 여기 있습니다. 새 주문을 배울 수 있습니다.", @@ -509,6 +510,44 @@ Ding.Locales.koKR = { EXPORT_ONE_LINE = "한 줄로", EXPORT_FULL = "전체", EXPORT_NONE = "내보낼 가이드 없음: %s", + + -- Report a problem (UI/Report.lua; the report itself is written in English, for GitHub) + REPORT_INTRO = "Ding!은 가이드가 보여 주는 내용과 현재 위치를 가이드 제작자를 위해 기록합니다. 저절로 전송되는 것은 없으며, 텍스트는 직접 올립니다.", + REPORT_CONTAINS = "신고에 들어갈 내용", + REPORT_PART_GUIDE = "가이드: %s, %s", + REPORT_PART_STEP = "단계 %d / %d (가이드 파일의 %d번): %s", + REPORT_NO_GUIDE = "가이드: 선택하지 않음", + REPORT_PART_PLACE = "현재 위치: %s, %s, %s", + REPORT_PLACE_UNKNOWN = "현재 위치: 이곳에서는 게임이 위치를 알려 주지 않습니다", + REPORT_PART_CHARACTER = "캐릭터: 레벨 %s, %s, %s, %s", + REPORT_PART_QUESTS = "퀘스트: %s", + REPORT_QUESTS_NONE = "퀘스트: 이 단계에는 없음", + REPORT_QUEST = "%s: %s", + REPORT_STATE_LOG = "퀘스트 목록에 있음", + REPORT_STATE_READY = "완료 보고 가능", + REPORT_STATE_DONE = "완료함", + REPORT_STATE_NONE = "퀘스트 목록에 없음", + REPORT_PART_ARROW = "화살표: %s, %s", + REPORT_ARROW_NONE = "화살표: 지금은 목표 없음", + REPORT_PART_SETTINGS = "경로를 바꾸는 설정, 테마, TomTom 사용 여부", + REPORT_PART_CLIENT = "Ding! 버전(%s), 게임 버전과 언어, 날짜와 시간", + REPORT_PART_PRIVATE = "캐릭터 이름, 서버, 계정은 절대 들어가지 않습니다.", + REPORT_WHAT = "무엇이 잘못되었나요?", + REPORT_REASON_NPC = "NPC가 그 자리에 없습니다", + REPORT_REASON_ARROW = "화살표가 엉뚱한 곳을 가리킵니다", + REPORT_REASON_QUEST = "퀘스트를 받을 수 없습니다", + REPORT_REASON_OTHER = "기타", + REPORT_COMMENT = "직접 쓰기 (선택)", + REPORT_COMMENT_HINT = "본 것과 기대한 것: 몇 마디만 있어도 큰 도움이 됩니다.", + REPORT_CREATE = "텍스트 만들기", + REPORT_MOUSE = "클릭: 선택 · Esc: 닫기", + REPORT_COPY = "텍스트가 선택되었습니다: Ctrl+C로 복사하세요.", + REPORT_CLIPBOARD = "애드온은 클립보드에 텍스트를 넣을 수 없습니다. Ding!이 선택해 두면 직접 복사하세요.", + REPORT_PASTE = "그런 다음 이 주소의 GitHub 새 신고에 붙여 넣으세요:", + REPORT_SELECT_TEXT = "텍스트 선택", + REPORT_SELECT_LINK = "링크 선택", + REPORT_SAVED = "키보드가 없나요? Ding!은 마지막 신고 %d개를 %s에 보관합니다. 로그아웃하거나 UI를 다시 불러올 때 저장되며, 이 파일을 보내면 됩니다.", + IMPORT_LINE = "%d번째 줄: %s", IMPORT_STEP = "%d단계: %s", IMPORT_GUIDE = "가이드 %s: %s", diff --git a/Ding/Locales/ptBR.lua b/Ding/Locales/ptBR.lua index b18ac23..6d56a61 100644 --- a/Ding/Locales/ptBR.lua +++ b/Ding/Locales/ptBR.lua @@ -85,7 +85,7 @@ Ding.Locales.ptBR = { ARROW_MODE_SET = "Orientação da seta: ajuste %d de 4", DEBUG_ON = "Depuração ativada.", DEBUG_OFF = "Depuração desativada.", - HELP = "Comandos: /ding (mostrar/ocultar), /ding config, /ding intro, /ding list, /ding guide , /ding where, /ding passages [n], /ding import, /ding export [id], /ding find , /ding cancel, /ding next, /ding prev, /ding skip, /ding back, /ding reset, /ding scale <0.5-1.5>, /ding arrow, /ding arrowflip, /ding minimap, /ding debug", + HELP = "Comandos: /ding (mostrar/ocultar), /ding config, /ding intro, /ding list, /ding guide , /ding where, /ding passages [n], /ding import, /ding export [id], /ding find , /ding cancel, /ding bug, /ding next, /ding prev, /ding skip, /ding back, /ding reset, /ding scale <0.5-1.5>, /ding arrow, /ding arrowflip, /ding minimap, /ding debug", -- Guide window QUOTE = "“%s”", @@ -379,6 +379,7 @@ Ding.Locales.ptBR = { CITY_RATCHET = "Vila Catraca", CITY_EVERLOOK = "Visteterna", BIND_FINDER = "Diretório de PNJs", + BIND_REPORT = "Relatar um problema", TRAINING_DUE = "Novos feitiços com seu instrutor de classe (%s)", TRAINING_DUE_GUESS = "Talvez haja feitiços novos com o instrutor da sua classe (nível %d)", TRAINING_NEAR = "O instrutor da sua classe, %s, está bem aqui: feitiços novos esperam por você.", @@ -509,6 +510,44 @@ Ding.Locales.ptBR = { EXPORT_ONE_LINE = "Em uma linha", EXPORT_FULL = "Completo", EXPORT_NONE = "Nenhum guia para exportar: %s", + + -- Report a problem (UI/Report.lua; the report itself is written in English, for GitHub) + REPORT_INTRO = "O Ding! anota o que o guia mostra e onde você está, para quem faz o guia. Nada é enviado sozinho: você mesmo publica o texto.", + REPORT_CONTAINS = "O que o relatório vai conter", + REPORT_PART_GUIDE = "O guia: %s, %s", + REPORT_PART_STEP = "Passo %d / %d (número %d no arquivo do guia): %s", + REPORT_NO_GUIDE = "O guia: nenhum escolhido", + REPORT_PART_PLACE = "Onde você está: %s, em %s, %s", + REPORT_PLACE_UNKNOWN = "Onde você está: o jogo não informa sua posição aqui", + REPORT_PART_CHARACTER = "Seu personagem: nível %s, %s, %s, %s", + REPORT_PART_QUESTS = "Missões: %s", + REPORT_QUESTS_NONE = "Missões: nenhuma para este passo", + REPORT_QUEST = "%s: %s", + REPORT_STATE_LOG = "no seu registro", + REPORT_STATE_READY = "pronta para entregar", + REPORT_STATE_DONE = "entregue", + REPORT_STATE_NONE = "fora do seu registro", + REPORT_PART_ARROW = "A seta: %s, %s", + REPORT_ARROW_NONE = "A seta: sem alvo agora", + REPORT_PART_SETTINGS = "Suas configurações que mudam a rota, o tema e se o TomTom está ligado", + REPORT_PART_CLIENT = "A versão do Ding! (%s), a versão e o idioma do jogo, a data e a hora", + REPORT_PART_PRIVATE = "Nunca o nome do seu personagem, seu reino nem sua conta.", + REPORT_WHAT = "O que deu errado?", + REPORT_REASON_NPC = "O PNJ não está lá", + REPORT_REASON_ARROW = "A seta aponta para o lugar errado", + REPORT_REASON_QUEST = "A missão não é oferecida", + REPORT_REASON_OTHER = "Outra coisa", + REPORT_COMMENT = "Com suas palavras (opcional)", + REPORT_COMMENT_HINT = "O que você viu, o que esperava: algumas palavras ajudam muito.", + REPORT_CREATE = "Criar o texto", + REPORT_MOUSE = "Clique: escolher · Esc: fechar", + REPORT_COPY = "O texto está selecionado: Ctrl+C para copiar.", + REPORT_CLIPBOARD = "Um addon não pode colocar texto na área de transferência: o Ding! seleciona, você copia.", + REPORT_PASTE = "Depois cole em um novo relatório no GitHub, neste endereço:", + REPORT_SELECT_TEXT = "Selecionar o texto", + REPORT_SELECT_LINK = "Selecionar o link", + REPORT_SAVED = "Sem teclado à mão? O Ding! guarda seus últimos %d relatórios em %s, gravado ao sair ou recarregar: você pode enviar esse arquivo.", + IMPORT_LINE = "Linha %d: %s", IMPORT_STEP = "Etapa %d: %s", IMPORT_GUIDE = "Guia %s: %s", diff --git a/Ding/Locales/zhCN.lua b/Ding/Locales/zhCN.lua index 7095bc5..c805f80 100644 --- a/Ding/Locales/zhCN.lua +++ b/Ding/Locales/zhCN.lua @@ -85,7 +85,7 @@ Ding.Locales.zhCN = { ARROW_MODE_SET = "箭头朝向:方案%d(共4种)", DEBUG_ON = "调试模式已开启。", DEBUG_OFF = "调试模式已关闭。", - HELP = "命令:/ding(显示/隐藏),/ding config,/ding intro,/ding list,/ding guide ,/ding where,/ding passages [n],/ding import,/ding export [id],/ding find <文字>,/ding cancel,/ding next,/ding prev,/ding skip,/ding back,/ding reset,/ding scale <0.5-1.5>,/ding arrow,/ding arrowflip,/ding minimap,/ding debug", + HELP = "命令:/ding(显示/隐藏),/ding config,/ding intro,/ding list,/ding guide ,/ding where,/ding passages [n],/ding import,/ding export [id],/ding find <文字>,/ding cancel,/ding bug,/ding next,/ding prev,/ding skip,/ding back,/ding reset,/ding scale <0.5-1.5>,/ding arrow,/ding arrowflip,/ding minimap,/ding debug", -- Guide window QUOTE = "“%s”", @@ -379,6 +379,7 @@ Ding.Locales.zhCN = { CITY_RATCHET = "棘齿城", CITY_EVERLOOK = "永望镇", BIND_FINDER = "NPC名录", + BIND_REPORT = "报告问题", TRAINING_DUE = "职业训练师处有新技能(%s)", TRAINING_DUE_GUESS = "职业训练师那里可能有新法术(%d级)", TRAINING_NEAR = "你的职业训练师%s就在这里:有新法术可以学习。", @@ -509,6 +510,44 @@ Ding.Locales.zhCN = { EXPORT_ONE_LINE = "单行", EXPORT_FULL = "完整", EXPORT_NONE = "没有可导出的指南:%s", + + -- Report a problem (UI/Report.lua; the report itself is written in English, for GitHub) + REPORT_INTRO = "Ding! 会为制作指南的人记下指南显示的内容和你所在的位置。不会自动发送任何东西:文本由你自己发布。", + REPORT_CONTAINS = "报告将包含的内容", + REPORT_PART_GUIDE = "指南:%s,%s", + REPORT_PART_STEP = "第 %d / %d 步(指南文件中的第 %d 条):%s", + REPORT_NO_GUIDE = "指南:未选择", + REPORT_PART_PLACE = "你的位置:%s,坐标 %s, %s", + REPORT_PLACE_UNKNOWN = "你的位置:游戏在这里不提供你的位置", + REPORT_PART_CHARACTER = "你的角色:等级 %s,%s,%s,%s", + REPORT_PART_QUESTS = "任务:%s", + REPORT_QUESTS_NONE = "任务:此步骤没有任务", + REPORT_QUEST = "%s:%s", + REPORT_STATE_LOG = "在任务日志中", + REPORT_STATE_READY = "可以交任务", + REPORT_STATE_DONE = "已交", + REPORT_STATE_NONE = "不在任务日志中", + REPORT_PART_ARROW = "箭头:%s,%s", + REPORT_ARROW_NONE = "箭头:目前没有目标", + REPORT_PART_SETTINGS = "会改变路线的设置、主题,以及 TomTom 是否开启", + REPORT_PART_CLIENT = "Ding! 的版本(%s)、游戏的版本和语言、日期和时间", + REPORT_PART_PRIVATE = "绝不包含你的角色名、服务器或账号。", + REPORT_WHAT = "出了什么问题?", + REPORT_REASON_NPC = "NPC 不在那里", + REPORT_REASON_ARROW = "箭头指向了错误的地方", + REPORT_REASON_QUEST = "接不到这个任务", + REPORT_REASON_OTHER = "其他问题", + REPORT_COMMENT = "用你自己的话说(可选)", + REPORT_COMMENT_HINT = "你看到了什么,你原本期待什么:几句话就很有帮助。", + REPORT_CREATE = "生成文本", + REPORT_MOUSE = "点击:选择 · Esc:关闭", + REPORT_COPY = "文本已选中:按 Ctrl+C 复制。", + REPORT_CLIPBOARD = "插件无法把文本放进剪贴板:Ding! 会选中文本,由你来复制。", + REPORT_PASTE = "然后粘贴到 GitHub 上的新报告中,地址如下:", + REPORT_SELECT_TEXT = "选中文本", + REPORT_SELECT_LINK = "选中链接", + REPORT_SAVED = "手边没有键盘?Ding! 会把你最近的 %d 份报告保存在 %s,在你登出或重载界面时写入:你可以发送这个文件。", + IMPORT_LINE = "第 %d 行:%s", IMPORT_STEP = "第 %d 步:%s", IMPORT_GUIDE = "指南 %s:%s", diff --git a/Ding/Locales/zhTW.lua b/Ding/Locales/zhTW.lua index feccc51..a53a1d0 100644 --- a/Ding/Locales/zhTW.lua +++ b/Ding/Locales/zhTW.lua @@ -85,7 +85,7 @@ Ding.Locales.zhTW = { ARROW_MODE_SET = "箭頭方向:設定%d(共4種)", DEBUG_ON = "除錯模式已開啟。", DEBUG_OFF = "除錯模式已關閉。", - HELP = "指令:/ding(顯示/隱藏)、/ding config、/ding intro、/ding list、/ding guide 、/ding where、/ding passages [n]、/ding import、/ding export [id]、/ding find <文字>、/ding cancel、/ding next、/ding prev、/ding skip、/ding back、/ding reset、/ding scale <0.5-1.5>、/ding arrow、/ding arrowflip、/ding minimap、/ding debug", + HELP = "指令:/ding(顯示/隱藏)、/ding config、/ding intro、/ding list、/ding guide 、/ding where、/ding passages [n]、/ding import、/ding export [id]、/ding find <文字>、/ding cancel、/ding bug、/ding next、/ding prev、/ding skip、/ding back、/ding reset、/ding scale <0.5-1.5>、/ding arrow、/ding arrowflip、/ding minimap、/ding debug", -- Guide window QUOTE = "「%s」", @@ -379,6 +379,7 @@ Ding.Locales.zhTW = { CITY_RATCHET = "棘齒城", CITY_EVERLOOK = "永望鎮", BIND_FINDER = "NPC名錄", + BIND_REPORT = "回報問題", TRAINING_DUE = "職業訓練師處有新技能(%s)", TRAINING_DUE_GUESS = "職業訓練師那裡可能有新法術(%d級)", TRAINING_NEAR = "你的職業訓練師%s就在這裡:有新法術可以學習。", @@ -509,6 +510,44 @@ Ding.Locales.zhTW = { EXPORT_ONE_LINE = "單行", EXPORT_FULL = "完整", EXPORT_NONE = "沒有可匯出的指南:%s", + + -- Report a problem (UI/Report.lua; the report itself is written in English, for GitHub) + REPORT_INTRO = "Ding! 會為製作指南的人記下指南顯示的內容和你所在的位置。不會自動傳送任何東西:文字由你自己發布。", + REPORT_CONTAINS = "回報將包含的內容", + REPORT_PART_GUIDE = "指南:%s,%s", + REPORT_PART_STEP = "第 %d / %d 步(指南檔案中的第 %d 條):%s", + REPORT_NO_GUIDE = "指南:未選擇", + REPORT_PART_PLACE = "你的位置:%s,座標 %s, %s", + REPORT_PLACE_UNKNOWN = "你的位置:遊戲在這裡不提供你的位置", + REPORT_PART_CHARACTER = "你的角色:等級 %s,%s,%s,%s", + REPORT_PART_QUESTS = "任務:%s", + REPORT_QUESTS_NONE = "任務:此步驟沒有任務", + REPORT_QUEST = "%s:%s", + REPORT_STATE_LOG = "在任務日誌中", + REPORT_STATE_READY = "可以交任務", + REPORT_STATE_DONE = "已交", + REPORT_STATE_NONE = "不在任務日誌中", + REPORT_PART_ARROW = "箭頭:%s,%s", + REPORT_ARROW_NONE = "箭頭:目前沒有目標", + REPORT_PART_SETTINGS = "會改變路線的設定、主題,以及 TomTom 是否開啟", + REPORT_PART_CLIENT = "Ding! 的版本(%s)、遊戲的版本和語言、日期和時間", + REPORT_PART_PRIVATE = "絕不包含你的角色名稱、伺服器或帳號。", + REPORT_WHAT = "出了什麼問題?", + REPORT_REASON_NPC = "NPC 不在那裡", + REPORT_REASON_ARROW = "箭頭指向了錯誤的地方", + REPORT_REASON_QUEST = "接不到這個任務", + REPORT_REASON_OTHER = "其他問題", + REPORT_COMMENT = "用你自己的話說(選填)", + REPORT_COMMENT_HINT = "你看到了什麼,你原本期待什麼:幾句話就很有幫助。", + REPORT_CREATE = "產生文字", + REPORT_MOUSE = "點擊:選擇 · Esc:關閉", + REPORT_COPY = "文字已選取:按 Ctrl+C 複製。", + REPORT_CLIPBOARD = "插件無法把文字放進剪貼簿:Ding! 會選取文字,由你來複製。", + REPORT_PASTE = "然後貼到 GitHub 上的新回報中,網址如下:", + REPORT_SELECT_TEXT = "選取文字", + REPORT_SELECT_LINK = "選取連結", + REPORT_SAVED = "手邊沒有鍵盤?Ding! 會把你最近的 %d 份回報保存在 %s,在你登出或重新載入介面時寫入:你可以傳送這個檔案。", + IMPORT_LINE = "第 %d 行:%s", IMPORT_STEP = "第 %d 步:%s", IMPORT_GUIDE = "指南 %s:%s", diff --git a/Ding/UI/Config.lua b/Ding/UI/Config.lua index f5fcc56..48d3782 100644 --- a/Ding/UI/Config.lua +++ b/Ding/UI/Config.lua @@ -965,6 +965,7 @@ local BINDINGS = { { action = "CLICK DingItemButton:LeftButton", label = L.BIND_ITEM }, { action = "CLICK DingItemButton:Target", label = L.BIND_TARGET }, { action = "DING_FINDER", label = L.BIND_FINDER }, + { action = "DING_REPORT", label = L.BIND_REPORT }, { action = "DING_CYCLE", label = L.BIND_CYCLE }, { action = "DING_FOCUS", label = L.BIND_FOCUS }, { action = "DING_TOGGLE", label = L.BIND_TOGGLE }, diff --git a/Ding/UI/Gamepad.lua b/Ding/UI/Gamepad.lua index 729543c..d84fed5 100644 --- a/Ding/UI/Gamepad.lua +++ b/Ding/UI/Gamepad.lua @@ -734,6 +734,7 @@ local CHORD_RUN = { DING_UNSKIP = function() Ding.Panel:BackToSkipped() end, DING_FINDER = function() Ding.Finder:Toggle() end, DING_CYCLE = function() Ding.Panel:CycleTarget() end, + DING_REPORT = function() Ding.ReportWindow:Toggle() end, } G.CHORD_RUN = CHORD_RUN @@ -1300,6 +1301,8 @@ local function gamepadChanged() if F and F.frame and F.frame:IsShown() then Ding:Safe(F.Layout, F) end local S = Ding.Setup if S and S:IsShown() then Ding:Safe(S.Layout, S) end + local Report = Ding.ReportWindow + if Report and Report:IsShown() then Ding:Safe(Report.Refresh, Report) end if G.moving then Ding:Safe(plateHelp, G.plate) end if Ding.Panel and Ding.Panel.Update then Ding:Safe(Ding.Panel.Update, Ding.Panel) end end diff --git a/Ding/UI/Panel.lua b/Ding/UI/Panel.lua index ebc43c3..58d81f7 100644 --- a/Ding/UI/Panel.lua +++ b/Ding/UI/Panel.lua @@ -1180,9 +1180,28 @@ function P:Create() f.finder:SetScript("OnClick", function() Ding.Finder:Toggle() end) f.finder.focus = UI.Focus(f.finder, true) f.finder.key = UI.Glyph(f) - f.finder.key:SetPoint("RIGHT", f.finder, "LEFT", -6, 0) f.finder.key:Hide() + -- Report a problem (UI/Report.lua): a flag, left of the directory's pin. In focus mode, + -- above the pin (D-pad up again), or left of it. The A chip of either sits left of both. + f.report = UI.IconButton(f, 20) + f.report:SetPoint("TOPRIGHT", f, "TOPRIGHT", -76, -2) + local pole = f.report:CreateTexture(nil, "ARTWORK") + UI.Fill(pole, C.secondary) + pole:SetSize(1.5, 12) + pole:SetPoint("CENTER", f.report, "CENTER", -3.5, 0) + local flag = f.report:CreateTexture(nil, "ARTWORK") + UI.Fill(flag, C.secondary) + flag:SetSize(7, 5) + flag:SetPoint("TOPLEFT", pole, "TOPRIGHT", 0, 0) + f.report.tooltip = L.BIND_REPORT + f.report:SetScript("OnClick", function() Ding.ReportWindow:Toggle() end) + f.report.focus = UI.Focus(f.report, true) + f.report.key = UI.Glyph(f) + f.report.key:SetPoint("RIGHT", f.report, "LEFT", -6, 0) + f.report.key:Hide() + f.finder.key:SetPoint("RIGHT", f.report, "LEFT", -6, 0) + -- The title in the theme's title font (the game's own in Halo and Ardoise), one line. f.title = text(f, "semibold", 12, C.text) UI.SetTitleFont(f.title, "window") @@ -1190,7 +1209,7 @@ function P:Create() -- The title area drags the window; its tooltip gives the full guide name. f.titleArea = CreateFrame("Button", nil, f) f.titleArea:SetPoint("TOPLEFT", f, "TOPLEFT", 28, 0) - f.titleArea:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", -76, -TITLE_HEIGHT) + f.titleArea:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", -100, -TITLE_HEIGHT) f.titleArea:RegisterForDrag("LeftButton") f.titleArea:SetScript("OnDragStart", startDrag) f.titleArea:SetScript("OnDragStop", stopDrag) @@ -1934,21 +1953,32 @@ local function layoutRow(f, i, data, y, width) return height end +-- The title bar's buttons focus mode reaches: [0] the directory's, above the first row; [-1] +-- the report's, above it. +local function headerButtons(f) return { [0] = f.finder, [-1] = f.report } end + function P:LayoutFull() local f, db, E = self.frame, Ding.db.panel, Ding.Engine local width, guide, held = db.width, E.guide, self.holding f.mini:Hide() - for _, region in ipairs({ f.menu, f.close, f.minimize, f.finder, f.title, f.titleArea, f.titleDivider, + for _, region in ipairs({ f.menu, f.close, f.minimize, f.finder, f.report, f.title, f.titleArea, f.titleDivider, f.stepDivider, f.prev, f.next, f.stepText, f.stepTotal, f.zone }) do region:Show() end - -- In focus mode, the directory's button can take the focus (above the first row): A on it. - local onFinder = self.focusing and self.focusIndex == 0 - f.finder.focus:SetShown(onFinder) - local finderKey = onFinder and Ding.Gamepad:Active() - if finderKey then f.finder.key:SetKey("PAD1") end - f.finder.key:SetShown(finderKey and true or false) - f.title:SetWidth(width - 28 - 76 - (finderKey and (f.finder.key:GetWidth() + 6) or 0)) + -- In focus mode, the title bar's buttons can take the focus (the directory's above the first + -- row, the report's above it): A on the focused one, the title shortens for the chip. + local chipWidth = 0 + for index, b in pairs(headerButtons(f)) do + local on = self.focusing and self.focusIndex == index + b.focus:SetShown(on) + local key = on and Ding.Gamepad:Active() + if key then + b.key:SetKey("PAD1") + chipWidth = b.key:GetWidth() + 6 + end + b.key:SetShown(key and true or false) + end + f.title:SetWidth(width - 28 - 100 - chipWidth) f.title:SetText(UI.Colored("accent", Ding.TITLE) .. UI.Colored("disabled", " · ") .. (guide and guide.title or L.GUIDE_NONE)) -- Step bar @@ -1992,7 +2022,7 @@ function P:LayoutFull() self.rowsShown = rows if self.focusing then -- The step may have changed under the focus: keep it on a row, and A on its target. - self.focusIndex = clamp(self.focusIndex or 1, self.focusIndex == 0 and 0 or 1, math.max(1, #rows)) + self.focusIndex = clamp(self.focusIndex or 1, math.min(1, self.focusIndex or 1), math.max(1, #rows)) syncItemKey() end y = -LIST_TOP - LIST_PAD @@ -2031,8 +2061,9 @@ function P:LayoutCompact() local f, E = self.frame, Ding.Engine local width, m = Ding.db.panel.width, f.mini f.finder.key:Hide() + f.report.key:Hide() f.focusHints:Hide() - for _, region in ipairs({ f.menu, f.close, f.minimize, f.finder, f.title, f.titleArea, f.titleDivider, + for _, region in ipairs({ f.menu, f.close, f.minimize, f.finder, f.report, f.title, f.titleArea, f.titleDivider, f.stepDivider, f.prev, f.next, f.stepText, f.stepTotal, f.zone, f.lb, f.rb, f.trackHolder, f.fillHolder }) do region:Hide() end @@ -2151,7 +2182,7 @@ end -- The focus mode's hint bar: A (what it does on that row), X (its card), B (leave). function P:FocusHints() local list = {} - local row = self.focusIndex ~= 0 and self.rowsShown and self.rowsShown[self.focusIndex] or nil + local row = self.focusIndex > 0 and self.rowsShown and self.rowsShown[self.focusIndex] or nil local a = self:FocusAction(row) if a then list[#list + 1] = UI.Hint("PAD1", a) end if self:HasCard(row) then list[#list + 1] = UI.Hint("PAD3", L.FOCUS_DETAIL) end @@ -2159,15 +2190,17 @@ function P:FocusHints() return list end --- The directory's button focused (above the first row): its name, as the mouse's hover says it. +-- A title bar button focused (the directory's above the first row, the report's above it): +-- its name, as the mouse's hover says it. function P:ShowFinderTip() - local b = self.frame.finder - if not (self.focusing and self.focusIndex == 0) then - if GameTooltip:GetOwner() == b then GameTooltip:Hide() end - return + local buttons = headerButtons(self.frame) + for index, b in pairs(buttons) do + if not (self.focusing and self.focusIndex == index) and GameTooltip:GetOwner() == b then GameTooltip:Hide() end end + local b = self.focusing and buttons[self.focusIndex] + if not b then return end GameTooltip:SetOwner(b, "ANCHOR_TOP") - GameTooltip:SetText(L.BIND_FINDER) + GameTooltip:SetText(b.tooltip) GameTooltip:Show() end @@ -2194,6 +2227,7 @@ function P:StopFocus() self.focusing = false for _, row in ipairs(self.frame.rows) do hideQuest(row) end hideQuest(self.frame.finder) + hideQuest(self.frame.report) Ding.Gamepad:Release(self.frame) self:Update() end @@ -2207,8 +2241,8 @@ function P:OnFocusKey(key) if key == "PADDUP" or key == "PADDDOWN" then if count > 0 then for _, row in ipairs(self.frame.rows) do hideQuest(row) end - -- Above the first row: the directory's button. - self.focusIndex = clamp((self.focusIndex or 1) + (key == "PADDUP" and -1 or 1), 0, count) + -- Above the first row: the directory's button, then the report's. + self.focusIndex = clamp((self.focusIndex or 1) + (key == "PADDUP" and -1 or 1), -1, count) syncItemKey() self:Update() self:ShowFinderTip() @@ -2218,6 +2252,10 @@ function P:OnFocusKey(key) self:StopFocus() return Ding.Finder:Open() end + if self.focusIndex == -1 then + self:StopFocus() + return Ding.ReportWindow:Open() + end self:Aim(self.rowsShown and self.rowsShown[self.focusIndex]) elseif key == "PAD3" then local row = self.frame.rows[self.focusIndex] @@ -2231,6 +2269,11 @@ function P:OnFocusKey(key) self:SetCompact(true) elseif key == "PAD2" or key == "PADFORWARD" then self:StopFocus() + elseif (key == "PADDLEFT" or key == "PADDRIGHT") and (self.focusIndex or 1) <= 0 then + -- In the title bar: the report's button left of the directory's. + self.focusIndex = key == "PADDLEFT" and -1 or 0 + self:Update() + self:ShowFinderTip() elseif key == "PADDLEFT" then -- On a gear upgrade: not wanted. On a weapon to learn: never again. On a jump offer: -- never mind. diff --git a/Ding/UI/Report.lua b/Ding/UI/Report.lua new file mode 100644 index 0000000..40049f7 --- /dev/null +++ b/Ding/UI/Report.lua @@ -0,0 +1,702 @@ +-- "Report a problem" (binding DING_REPORT, the guide window's flag button beside the +-- directory's pin, /ding bug): Ding's own window, in two pages. First what the report will +-- contain, in plain words (the facts are read when the window opens: Core/Report.lua), what +-- went wrong (four ready reasons: a gamepad types little) and the player's words (optional; +-- the box takes the keyboard only once no other box has it: the game's chat above all). Then +-- "Create the text": the report in a read-only box, selected for Ctrl+C (an addon cannot write +-- the clipboard: the window says so), the link of the GitHub form to paste it into, and where +-- Ding keeps the last reports for a player without a keyboard (DingDB.reports). +-- Gamepad (temporary bindings, out of combat only): the D-pad up / down between the reasons, +-- the box and the buttons, left / right along the buttons, A picks or presses, Y selects the +-- text, B closes. Each action shows once: A on the focused row or button, Y on "Select the +-- text", B by the close cross; the footer is the mouse help without a gamepad. Mouse: hover and +-- click, right click or Esc closes. +local _, Ding = ... +local L = Ding.L +local UI = Ding.UI +local C = UI.C + +local W = {} +Ding.ReportWindow = W + +local SIZE = UI.SIZE +local WIDTH, TITLE, PAD, INSET, BUTTON = 520, SIZE.title, 12, SIZE.pad, SIZE.button +local EDGE, LINE = 8, 30 -- the footer's margins; a hint line above the buttons +local FOOTER = EDGE + BUTTON + EDGE +local HEADER, GAP, ROW_GAP = 20, 4, SIZE.gap +local ROW_PAD, ROW_MIN = 6, 28 -- a reason's margin above and below; its height +local ROW_WIDTH = WIDTH - 2 * INSET +local MARK, CHIP = 8 + 16 + 10, 24 -- the radio mark before a reason; room for the A chip +local COMMENT_HEIGHT, TEXT_HEIGHT, LINK_HEIGHT = 52, 196, 26 +local QUESTS_SHOWN = 3 +local KEYS = { "PADDUP", "PADDDOWN", "PADDLEFT", "PADDRIGHT", "PAD1", "PAD2", "PAD4", "PADFORWARD" } + +local function G() return Ding.Gamepad end +local function R() return Ding.Report end + +--------------------------------------------------------------------------- +-- Keyboard focus (as the import window: UI/ImportWindow.lua) +--------------------------------------------------------------------------- +-- Ding never takes the keyboard from a box that is not one of its own (the chat's still has it +-- while /ding bug runs): taken away in Ding's code, the chat's focus-lost handler runs in Ding's +-- name and the game blocks it with its gamepad interface on. The box waits until it is free. +local function ownBox(focus) + local f = W.frame + return f and (focus == f.comment.edit or focus == f.text.edit or focus == f.link.edit) +end + +local function keyboardTaken() + if type(GetCurrentKeyBoardFocus) ~= "function" then return false end + local ok, focus = pcall(GetCurrentKeyBoardFocus) + if not ok or focus == nil then return false end + return not ownBox(focus) +end + +-- The status line of the page on screen: what the window waits for. +local function setStatus(text) + local f = W.frame + local status = W.page == "done" and f.doneStatus or f.formStatus + status:SetText(text or "") +end + +-- The box takes the keyboard (its text selected with `select`), now or once it is free. +function W:Focus(box, select) + if keyboardTaken() then + self.pendingFocus = { box = box, select = select } + setStatus(L.IMPORT_WAIT_KEYBOARD) + self:Layout() + return false + end + local waited = self.pendingFocus ~= nil + self.pendingFocus = nil + if waited then + setStatus(nil) + self:Layout() + end + box.edit:SetFocus() + if select then box.edit:HighlightText() end + box:UpdatePlaceholder() + return true +end + +function W:OnUpdate() + local pending = self.pendingFocus + if pending and not keyboardTaken() then self:Focus(pending.box, pending.select) end +end + +--------------------------------------------------------------------------- +-- Pieces +--------------------------------------------------------------------------- +-- A box of text: a multi-line edit box that scrolls, or one line (`single`). A read-only box +-- puts its text back when it is typed into (selecting and Ctrl+C still work). `placeholder`: +-- a line shown while the box is empty and has not the keyboard. +local function textBox(parent, height, opts) + local box = CreateFrame("Frame", nil, parent) + box:SetHeight(height) + box.bg = UI.Shape(box, "row", C.control, "BACKGROUND"):Place(box) + local edit + if opts.single then + edit = CreateFrame("EditBox", nil, box) + edit:SetPoint("TOPLEFT", box, "TOPLEFT", 8, -4) + edit:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -8, 4) + else + local scroll = CreateFrame("ScrollFrame", nil, box) + scroll:SetPoint("TOPLEFT", box, "TOPLEFT", 8, -6) + scroll:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -8, 6) + scroll:EnableMouseWheel(true) + edit = CreateFrame("EditBox", nil, scroll) + edit:SetMultiLine(true) + edit:SetWidth(ROW_WIDTH - 16) + edit:SetHeight(height - 12) + scroll:SetScrollChild(edit) + scroll:SetScript("OnMouseWheel", function(self, delta) + local range = self:GetVerticalScrollRange() or 0 + self:SetVerticalScroll(math.max(0, math.min(range, self:GetVerticalScroll() - delta * 28))) + end) + -- The cursor stays in view while typing or moving through the text. + edit:SetScript("OnCursorChanged", function(_, _, y, _, h) + local top, view = scroll:GetVerticalScroll(), scroll:GetHeight() + y = -(y or 0) + if y < top then + scroll:SetVerticalScroll(y) + elseif y + (h or 0) > top + view then + scroll:SetVerticalScroll(y + (h or 0) - view) + end + end) + box.scroll = scroll + end + edit:SetAutoFocus(false) + edit:SetMaxLetters(opts.readOnly and 0 or 500) + edit:SetFontObject(UI.Font("regular", 11)) + UI.Color(edit, opts.readOnly and C.secondary or C.text) + if opts.placeholder then + box.placeholder = UI.Text(box, "regular", 11, C.muted) + box.placeholder:SetPoint("TOPLEFT", box, "TOPLEFT", 8, -6) + box.placeholder:SetWidth(ROW_WIDTH - 16) + box.placeholder:SetJustifyH("LEFT") + end + function box:UpdatePlaceholder() + if not self.placeholder then return end + self.placeholder:SetText(opts.placeholder()) + self.placeholder:SetShown((edit:GetText() or "") == "" and not edit:HasFocus()) + end + -- Esc: out of the player's box (the words stay), or the window closes. + edit:SetScript("OnEscapePressed", function(self) + self:ClearFocus() + if opts.readOnly then W:Close() end + end) + edit:SetScript("OnEditFocusLost", function() box:UpdatePlaceholder() end) + edit:SetScript("OnTextChanged", function(self, typed) + if typed and self.fixed then + self:SetText(self.fixed) + self:HighlightText() + end + box:UpdatePlaceholder() + end) + box:EnableMouse(true) + box:SetScript("OnMouseDown", function() W:Focus(box) end) + function box:SetValue(text) + edit.fixed = opts.readOnly and text or nil + edit:SetText(text) + if self.scroll then self.scroll:SetVerticalScroll(0) end + self:UpdatePlaceholder() + end + function box:GetValue() return edit:GetText() or "" end + -- Everything selected, ready for Ctrl+C. + function box:SelectAll() W:Focus(box, true) end + box.edit = edit + return box +end + +-- A section's title ("What went wrong?"). +local function header(parent) + local t = UI.Text(parent, "semibold", 11, C.muted) + t:SetJustifyH("LEFT") + return t +end + +-- A text that wraps, as wide as the rows. +local function paragraph(parent, weight, size, color) + local t = UI.Text(parent, weight, size, color) + UI.Wrap(t, size) + t:SetJustifyH("LEFT") + return t +end + +-- A reason: a radio ring (its dot when chosen), its words, the A chip on the right (gamepad). +local function reasonRow(parent, i) + local r = CreateFrame("Button", nil, parent) + r:SetWidth(ROW_WIDTH) + r:RegisterForClicks("LeftButtonUp", "RightButtonUp") + r.focus = UI.Focus(r) + r.ring = UI.Icon(r, "ring", 16, C.muted, "ARTWORK") + r.ring:SetPoint("TOPLEFT", r, "TOPLEFT", 8, -ROW_PAD) + r.dot = UI.Icon(r, "circle", 8, C.accent, "OVERLAY") + r.dot:SetPoint("CENTER", r.ring, "CENTER", 0, 0) + r.label = paragraph(r, "regular", 12, C.text) + r.label:SetPoint("TOPLEFT", r, "TOPLEFT", MARK, -ROW_PAD) + r.key = UI.Glyph(r) + r.key:SetKey("PAD1") + r.key:SetPoint("RIGHT", r, "RIGHT", -8, 0) + r.key:Hide() + r:SetScript("OnEnter", function() W:SetFocus(i) end) + r:SetScript("OnClick", function(_, button) + if button == "RightButton" then return W:Close() end + W:SetFocus(i) + W:Choose(R().REASONS[i].key) + end) + return r +end + +local function newButton(parent, index) + local b = CreateFrame("Button", nil, parent) + b:SetHeight(BUTTON) + b:RegisterForClicks("LeftButtonUp") + b.bg = UI.Shape(b, "row", C.control, "BACKGROUND"):Place(b) + b.hover = UI.Shape(b, "row", C.hover, "BACKGROUND", 1):Place(b) + b.hover:Hide() + b.focus = UI.Focus(b, true) + b.text = UI.Text(b, "medium", 12, C.text) + b.text:SetPoint("CENTER", b, "CENTER", 0, 0) + b:SetScript("OnClick", function() W:Press(index) end) + b:SetScript("OnEnter", function() + b.hover:Show() + W:FocusButton(index) + end) + b:SetScript("OnLeave", function() b.hover:Hide() end) + return b +end + +--------------------------------------------------------------------------- +-- Window +--------------------------------------------------------------------------- +function W:Create() + if self.frame then return end + local f = CreateFrame("Frame", "DingReport", UIParent) + f:SetFrameStrata("DIALOG") + f:SetClampedToScreen(true) + f:EnableMouse(true) + f:SetSize(WIDTH, 400) + UI.Dialog(f, 0, 40) + f.look = UI.Window(f, "dialog") + UI.DialogBars(f, TITLE) + + -- Title bar: "Ding! · Report a problem", B, close cross. + f.title = UI.Text(f, "semibold", 13, C.text) + UI.SetTitleFont(f.title, "settings") + f.title:SetPoint("LEFT", f, "TOPLEFT", 12, -TITLE / 2) + f.close = UI.IconButton(f, 24) + f.close:SetPoint("RIGHT", f, "TOPRIGHT", -6, -TITLE / 2) + f.close.label = UI.Text(f.close, "regular", 16, C.secondary) + f.close.label:SetPoint("CENTER", f.close, "CENTER", 0, 1) + f.close.label:SetText("×") + f.close:SetScript("OnClick", function() W:Close() end) + f.back = UI.Glyph(f) + f.back:SetKey("PAD2") + f.back:SetPoint("RIGHT", f.close, "LEFT", -8, 0) + UI.Separator(f, -TITLE) + + -- First page: what the report holds, what went wrong, the player's words. + local form = CreateFrame("Frame", nil, f) + form:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -(TITLE + 1)) + form:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -(TITLE + 1)) + f.form = form + f.intro = paragraph(form, "regular", 12, C.text) + f.containsTitle = header(form) + f.parts = {} + f.whatTitle = header(form) + f.reasons = {} + for i = 1, #R().REASONS do f.reasons[i] = reasonRow(form, i) end + f.commentTitle = header(form) + f.commentKey = UI.Glyph(form) + f.commentKey:SetKey("PAD1") + f.commentKey:Hide() + f.comment = textBox(form, COMMENT_HEIGHT, { placeholder = function() return L.REPORT_COMMENT_HINT end }) + f.comment.focus = UI.Focus(f.comment, true) + f.comment:SetScript("OnEnter", function() W:SetFocus(#f.reasons + 1) end) + f.formStatus = paragraph(form, "regular", 11, C.secondary) + + -- Second page: the text, the link, where the reports are kept. + local done = CreateFrame("Frame", nil, f) + done:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -(TITLE + 1)) + done:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -(TITLE + 1)) + f.done = done + f.copy = paragraph(done, "medium", 12, C.text) + f.clipboard = paragraph(done, "regular", 11, C.secondary) + f.text = textBox(done, TEXT_HEIGHT, { readOnly = true }) + f.paste = paragraph(done, "regular", 12, C.text) + f.link = textBox(done, LINK_HEIGHT, { readOnly = true, single = true }) + f.saved = paragraph(done, "regular", 11, C.secondary) + f.doneStatus = paragraph(done, "regular", 11, C.secondary) + + f.divider = UI.Divider(f, -100) + f.buttons = {} + f.hints = G():NewHintBar(f, WIDTH / 2, 22) + f:SetScript("OnMouseUp", function(_, button) if button == "RightButton" then W:Close() end end) + f:SetScript("OnHide", function() W:OnHide() end) + f:SetScript("OnUpdate", function() if W.pendingFocus then Ding:Safe(W.OnUpdate, W) end end) + f:Hide() + if type(UISpecialFrames) == "table" then table.insert(UISpecialFrames, "DingReport") end + self.frame = f +end + +-- A theme change: the open window again (the texts' fonts may change their heights). +UI.Theme.OnApply(function() + if W.frame and W.frame:IsShown() then Ding:Safe(W.Layout, W) end +end) + +--------------------------------------------------------------------------- +-- What the report will contain, in plain words (the text itself is in English) +--------------------------------------------------------------------------- +local STATE_WORDS = { log = "REPORT_STATE_LOG", ready = "REPORT_STATE_READY", turnedin = "REPORT_STATE_DONE", + none = "REPORT_STATE_NONE" } + +local function questWords(q) + local words = L.REPORT_QUEST:format(L.QUOTE:format(q.shownName or q.name or "?"), L[STATE_WORDS[q.state]]) + local counts = {} + for _, o in ipairs(q.objectives) do + if o.have and o.need then counts[#counts + 1] = ("%d/%d"):format(o.have, o.need) end + end + if #counts > 0 then words = words .. ", " .. table.concat(counts, ", ") end + return words +end + +local function coordinate(n) return ("%.1f"):format(n or 0) end + +-- The parts of the report, one sentence each. +function W:Parts(info) + local parts = {} + local g = info.guide + if g then + parts[#parts + 1] = L.REPORT_PART_GUIDE:format(g.pack or g.packId or "?", g.title or g.id) + parts[#parts + 1] = L.REPORT_PART_STEP:format(g.number, g.total, g.index, g.summary or "") + else + parts[#parts + 1] = L.REPORT_NO_GUIDE + end + local p = info.player + if p.map then + parts[#parts + 1] = L.REPORT_PART_PLACE:format(p.zone or p.place or "?", coordinate(p.x), coordinate(p.y)) + else + parts[#parts + 1] = L.REPORT_PLACE_UNKNOWN + end + parts[#parts + 1] = L.REPORT_PART_CHARACTER:format(p.level or "?", p.raceName or p.race or "?", + p.className or p.class or "?", p.factionName or p.faction or "?") + if #info.quests == 0 then + parts[#parts + 1] = L.REPORT_QUESTS_NONE + else + local list = {} + for i = 1, math.min(QUESTS_SHOWN, #info.quests) do list[i] = questWords(info.quests[i]) end + local words = table.concat(list, "; ") + if #info.quests > QUESTS_SHOWN then words = words .. "; " .. UI.More(#info.quests - QUESTS_SHOWN, true) end + parts[#parts + 1] = L.REPORT_PART_QUESTS:format(words) + end + local a = info.arrow + if a.label or a.map then + local far = a.distance and L.YARDS:format(Ding.Compat.Number(a.distance)) + or (a.map and L.DISTANCE_UNKNOWN) or L.ANOTHER_CONTINENT + parts[#parts + 1] = L.REPORT_PART_ARROW:format(a.label or "?", far) + else + parts[#parts + 1] = L.REPORT_ARROW_NONE + end + parts[#parts + 1] = L.REPORT_PART_SETTINGS + parts[#parts + 1] = L.REPORT_PART_CLIENT:format(tostring(info.client.version)) + parts[#parts + 1] = L.REPORT_PART_PRIVATE + return parts +end + +--------------------------------------------------------------------------- +-- Layout +--------------------------------------------------------------------------- +-- Places a wrapped text at `y` (from the page's top); returns the y under it. +local function place(fs, page, y, text, x, width) + fs:SetText(text or "") + fs:SetShown((text or "") ~= "") + if (text or "") == "" then return y end + fs:ClearAllPoints() + fs:SetPoint("TOPLEFT", page, "TOPLEFT", x or PAD, -math.ceil(y)) + fs:SetWidth(width or (WIDTH - 2 * PAD)) + return y + fs:GetStringHeight() +end + +-- The page's status line (what the window waits for), 6 under the rest; hidden when empty. +local function status(fs, page, y) + local text = fs:GetText() or "" + if text == "" then + fs:Hide() + return y + end + return place(fs, page, y + 6, text) +end + +local function placeBox(box, page, y) + box:ClearAllPoints() + box:SetPoint("TOPLEFT", page, "TOPLEFT", INSET, -math.ceil(y)) + box:SetPoint("TOPRIGHT", page, "TOPRIGHT", -INSET, -math.ceil(y)) + return y + box:GetHeight() +end + +function W:LayoutForm() + local f, pad = self.frame, G():Active() + local page = f.form + local y = PAD + y = place(f.intro, page, y, L.REPORT_INTRO) + 8 + y = place(f.containsTitle, page, y, L.REPORT_CONTAINS) + GAP + local parts = self:Parts(self.info) + for i, text in ipairs(parts) do + f.parts[i] = f.parts[i] or paragraph(page, "regular", 11, C.secondary) + y = place(f.parts[i], page, y, text, PAD + 8, WIDTH - 2 * PAD - 8) + 2 + end + for i = #parts + 1, #f.parts do f.parts[i]:Hide() end + y = y + 8 + y = place(f.whatTitle, page, y, L.REPORT_WHAT) + GAP + for i, reason in ipairs(R().REASONS) do + local r = f.reasons[i] + r.label:SetWidth(ROW_WIDTH - MARK - 8 - CHIP) + r.label:SetText(L[reason.label]) + local chosen = self.reason == reason.key + r.dot:SetShown(chosen) + UI.Tint(r.ring, chosen and C.accent or C.muted) + r:SetHeight(math.max(ROW_MIN, math.ceil(ROW_PAD * 2 + r.label:GetStringHeight()))) + r:ClearAllPoints() + r:SetPoint("TOPLEFT", page, "TOPLEFT", INSET, -math.ceil(y)) + r:Show() + y = y + r:GetHeight() + ROW_GAP + end + y = y + 8 + -- The words' title, the A chip at its right when the box is focused (gamepad). + local chip = pad and UI.KeysWidth({ "PAD1" }) or 0 + y = place(f.commentTitle, page, y, L.REPORT_COMMENT, PAD, WIDTH - 2 * PAD - chip) + f.commentKey:ClearAllPoints() + f.commentKey:SetPoint("RIGHT", page, "TOPRIGHT", -INSET - 2, -math.ceil(y - 7)) + y = placeBox(f.comment, page, y + GAP) + f.comment:UpdatePlaceholder() + return status(f.formStatus, page, y) + PAD +end + +function W:LayoutDone() + local f = self.frame + local page = f.done + local y = PAD + y = place(f.copy, page, y, L.REPORT_COPY) + 2 + y = place(f.clipboard, page, y, L.REPORT_CLIPBOARD) + 8 + y = placeBox(f.text, page, y) + 10 + y = place(f.paste, page, y, L.REPORT_PASTE) + 6 + y = placeBox(f.link, page, y) + 10 + y = place(f.saved, page, y, L.REPORT_SAVED:format(R().KEEP, R().FILE)) + return status(f.doneStatus, page, y) + PAD +end + +-- The page on screen, its buttons and the footer; the window as high as its content. +function W:Layout() + local f = self.frame + if not (f and self.page) then return end + local pad = G():Active() + UI.Title(f.title, L.BIND_REPORT) + f.back:SetShown(pad) + local form = self.page == "form" + f.form:SetShown(form) + f.done:SetShown(not form) + local height = form and self:LayoutForm() or self:LayoutDone() + local page = form and f.form or f.done + page:SetHeight(height) + local top = TITLE + 1 + height + f.divider:ClearAllPoints() + f.divider:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -top) + f.divider:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -top) + self:LayoutButtons() + local below = self:UpdateHelp() + f:SetHeight(top + 1 + FOOTER + (below and LINE or 0)) + self:ShowFocus() +end + +-- The buttons of the page: { label, run, primary, key }, laid out from the right; `key`: the +-- gamepad button that presses it wherever the focus is. With a gamepad every button keeps +-- room for its chips, so that it does not grow when the focus comes to it. +function W:LayoutButtons() + local f, list = self.frame, self.actions or {} + local pad = G():Active() + local x = -INSET + for i = #list, 1, -1 do + local b = f.buttons[i] or newButton(f, i) + f.buttons[i] = b + local action = list[i] + b.text:SetText(action.label) + b.bg:SetColor(action.primary and C.accent or C.control) + UI.Color(b.text, action.primary and C.onAccent or C.text) + local room = pad and UI.KeysWidth({ "PAD1", action.key }) or 0 + b:SetWidth(math.max(96, UI.Measure(b.text) + 28 + room)) + b:ClearAllPoints() + b:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", x, EDGE) + x = x - b:GetWidth() - 8 + b:Show() + end + for i = #list + 1, #f.buttons do f.buttons[i]:Hide() end + self.buttonsWidth = -x - INSET - 8 +end + +-- The footer's help: nothing with a gamepad (every action has its chip), the mouse help +-- without; left of the buttons, or on a line of its own above them when it does not fit. +-- Returns true when it takes that line. +function W:UpdateHelp() + local f = self.frame + local hints = {} + if not G():Active() then + hints[1] = UI.Hint(nil, self.page == "form" and L.REPORT_MOUSE or L.EXPORT_MOUSE) + end + local full = WIDTH - 2 * INSET + f.hints:SetRoom(math.max(0, full - (self.buttonsWidth or 0) - 16)) + f.hints:SetHints(hints) + local below = f.hints.shown < #hints + if below then + f.hints:SetRoom(full) + f.hints:SetHints(hints) + end + f.hints:ClearAllPoints() + f.hints:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", INSET, EDGE + 1 + (below and LINE or 0)) + return below +end + +--------------------------------------------------------------------------- +-- Focus: the D-pad's stops on the page +--------------------------------------------------------------------------- +-- The form: the reasons, the box, then the buttons; the text page: its buttons. +function W:Stops() + local stops = {} + if self.page == "form" then + for i = 1, #R().REASONS do stops[#stops + 1] = { kind = "reason", i = i } end + stops[#stops + 1] = { kind = "comment" } + end + for i = 1, #(self.actions or {}) do stops[#stops + 1] = { kind = "button", i = i } end + return stops +end + +function W:Focused() + return self:Stops()[self.focus or 1] +end + +function W:SetFocus(i) + local count = #self:Stops() + self.focus = math.max(1, math.min(count, i or 1)) + self:ShowFocus() +end + +function W:FocusButton(index) + for i, stop in ipairs(self:Stops()) do + if stop.kind == "button" and stop.i == index then return self:SetFocus(i) end + end +end + +-- The focus ring and the A chip where the focus is (A only with a gamepad), the buttons' own +-- chips (Y on "Select the text"). +function W:ShowFocus() + local f = self.frame + local pad = G():Active() + local stop = self:Focused() or {} + for i, r in ipairs(f.reasons) do + local on = stop.kind == "reason" and stop.i == i + r.focus:SetShown(on) + r.key:SetShown(on and pad) + end + local onComment = stop.kind == "comment" + f.comment.focus:SetShown(onComment and self.page == "form") + f.commentKey:SetShown(onComment and pad and self.page == "form") + for i, b in ipairs(f.buttons) do + local action = self.actions and self.actions[i] + local focused = stop.kind == "button" and stop.i == i and b:IsShown() + b.focus:SetShown(pad and focused) + local keys = {} + if pad and focused then keys[#keys + 1] = "PAD1" end + if pad and action and action.key then keys[#keys + 1] = action.key end + UI.ButtonKeys(b, keys, action and action.primary) + end +end + +--------------------------------------------------------------------------- +-- Actions +--------------------------------------------------------------------------- +function W:Choose(key) + self.reason = key + self:Layout() +end + +function W:Press(index) + local action = self.actions and self.actions[index] + if action then action.run() end +end + +-- A on the focused stop: a reason is chosen (and the focus goes on to "Create the text": a +-- gamepad types little), the box takes the keyboard, a button is pressed. +function W:Activate() + local stop = self:Focused() + if not stop then return end + if stop.kind == "reason" then + self:Choose(R().REASONS[stop.i].key) + self:FocusButton(1) + elseif stop.kind == "comment" then + self:Focus(self.frame.comment) + else + self:Press(stop.i) + end +end + +-- "Create the text": the report written and kept, the text selected for Ctrl+C. +function W:Make() + local f = self.frame + local comment = f.comment:GetValue() + f.comment.edit:ClearFocus() + self.pendingFocus = nil + self.made = R():Make(self.info, self.reason, comment) + self.page = "done" + f.formStatus:SetText("") + f.doneStatus:SetText("") + f.text:SetValue(self.made) + f.link:SetValue(R().ISSUE_URL) + self.actions = { + { label = L.REPORT_SELECT_TEXT, primary = true, key = "PAD4", run = function() W.frame.text:SelectAll() end }, + { label = L.REPORT_SELECT_LINK, run = function() W.frame.link:SelectAll() end }, + } + self.focus = 1 + self:Layout() + f.text:SelectAll() +end + +--------------------------------------------------------------------------- +-- Opening +--------------------------------------------------------------------------- +function W:Open() + if InCombatLockdown() then + Ding:Print(L.CONFIG_COMBAT) + return false + end + -- What the report holds is read before anything changes on screen. + local info = R():Collect() + self:Create() + local f = self.frame + if Ding.Config.frame and Ding.Config.frame:IsShown() then Ding.Config:Close() end + if Ding.Finder.frame and Ding.Finder.frame:IsShown() then Ding.Finder:Close() end + if Ding.ImportWindow.frame and Ding.ImportWindow.frame:IsShown() then Ding.ImportWindow:Close() end + if Ding.Setup:IsShown() then Ding.Setup:Close(true) end + if Ding.Panel.focusing then Ding.Panel:StopFocus() end + self.info, self.reason, self.made, self.pendingFocus = info, nil, nil, nil + self.page = "form" + f.formStatus:SetText("") + f.comment:SetValue("") + self.actions = { { label = L.REPORT_CREATE, primary = true, run = function() W:Make() end } } + self.focus = 1 + f:Show() + self:Layout() + G():Take(f, function(key) W:OnKey(key) end, 0.4, 0.12, KEYS) + -- With a keyboard the words come first; with a gamepad, the ready reasons. + if not G():Active() then self:Focus(f.comment) end + return true +end + +function W:Close() + if self.frame then self.frame:Hide() end +end + +function W:Toggle() + if self.frame and self.frame:IsShown() then self:Close() else self:Open() end +end + +function W:IsShown() + return self.frame ~= nil and self.frame:IsShown() +end + +function W:OnHide() + local f = self.frame + if not f then return end + G():Release(f) + self.pendingFocus = nil + f.comment.edit:ClearFocus() + f.text.edit:ClearFocus() + f.link.edit:ClearFocus() +end + +-- A gamepad plugged in or out (Ding.Gamepad): chips and help again. +function W:Refresh() + if self:IsShown() then self:Layout() end +end + +function W:OnKey(key) + local stops = self:Stops() + local count = #stops + local stop = stops[self.focus or 1] or {} + if key == "PADDUP" or key == "PADDDOWN" then + if count > 0 then self:SetFocus(((self.focus or 1) - 1 + (key == "PADDUP" and -1 or 1)) % count + 1) end + elseif key == "PADDLEFT" or key == "PADDRIGHT" then + -- Along the buttons. + local buttons = #(self.actions or {}) + if stop.kind == "button" and buttons > 1 then + self:FocusButton((stop.i - 1 + (key == "PADDLEFT" and -1 or 1)) % buttons + 1) + end + elseif key == "PAD1" then + self:Activate() + elseif key == "PAD4" then + if self.page == "done" then self.frame.text:SelectAll() end + elseif key == "PAD2" or key == "PADFORWARD" then + self:Close() + end +end + +Ding:On("PLAYER_REGEN_DISABLED", function() W:Close() end) diff --git a/tools/smoke_test.lua b/tools/smoke_test.lua index bb91717..c36a317 100644 --- a/tools/smoke_test.lua +++ b/tools/smoke_test.lua @@ -1808,5 +1808,45 @@ ok, err = pcall(function() end) if not ok then problem("why: " .. tostring(err)) end +-- Report a problem with the gamepad alone: the guide window's flag (focus mode, up twice, A), +-- a reason (A), "Create the text" (A); the text holds the step, the position and the +-- objective's counter, never the character's name; it is kept; B closes, nothing left bound. +ok, err = pcall(function() + local RW = Ding.ReportWindow + Ding.RegisterGuide({ id = "smoke-report", title = "Report", races = { "NightElf" }, steps = { + { accept = 456 }, { complete = 456 }, { turnin = 456 }, + } }) + scenarioCharacter() + H.resetQuests() + E.turnedIn, Ding.char.manual = {}, {} + player.level, player.map, player.x, player.y = 5, 1438, 58.7, 44 + E:SetGuide("smoke-report", 1, true) + tick(1) + H.accept(456); flush(); tick(1) + H.setObjective(456, 1, 2, 7) + fire("QUEST_LOG_UPDATE"); tick(1) + expect(E.index == 2, "report: not on the step that completes the quest") + local before = #db.reports + P:StartFocus() + P.focusIndex = 1; P:Update() + press("PADDUP"); press("PADDUP") + expect(P.focusIndex == -1, "report: the guide window's flag is not reached") + checkPanel("report: the flag focused") + press("PAD1") + expect(RW:IsShown(), "report: A on the flag does not open the window") + press("PAD1"); press("PAD1") + expect(RW.page == "done", "report: A on a reason, then on Create the text, does not make it") + local text = RW.frame.text:GetValue() + expect(text:find("index 2 of 3", 1, true), "report: no step in the text") + expect(text:find("Position: 58.7, 44.0", 1, true), "report: no position in the text") + expect(text:find("2/7", 1, true), "report: no objective counter in the text") + expect(not text:find(UnitName("player"), 1, true), "report: the character's name is in the text") + expect(#db.reports == math.min(before + 1, Ding.Report.KEEP), "report: not kept in DingDB.reports") + press("PAD2") + expect(not RW:IsShown(), "report: B does not close the window") + expect(next(overrides) == nil, "report: temporary bindings still active after closing") +end) +if not ok then problem("report: " .. tostring(err)) end + print(("Result: %d problem(s)"):format(H.problems())) os.exit(H.problems() > 0 and 1 or 0) diff --git a/tools/tests/dialog_layout_test.lua b/tools/tests/dialog_layout_test.lua index 9714b17..0327dd2 100644 --- a/tools/tests/dialog_layout_test.lua +++ b/tools/tests/dialog_layout_test.lua @@ -1,5 +1,5 @@ -- Ding's dialog windows (the NPC directory, the settings, the setup assistant, import / --- export): on every row, in every language, gamepad on and off, nothing overlaps (the A chip, a +-- export, report a problem): on every row, in every language, gamepad on and off, nothing overlaps (the A chip, a -- time, a caret, a value, a text) and nothing leaves its row (tools/layout.lua places the -- mocked widgets); the sizes the windows share (UI.SIZE, a little denser than the design's -- first handoff at the user's request); their size setting (Appearance tab, 60 % to 120 %, @@ -10,7 +10,7 @@ local T = dofile(tools .. "/testlib.lua")(arg[1], { locale = "enUS" }) local Layout = dofile(tools .. "/layout.lua") local Ding, H = T.Ding, T.H local UI, L = Ding.UI, Ding.L -local F, Cfg, S, W = Ding.Finder, Ding.Config, Ding.Setup, Ding.ImportWindow +local F, Cfg, S, W, Rep = Ding.Finder, Ding.Config, Ding.Setup, Ding.ImportWindow, Ding.ReportWindow Ding.RegisterGuide({ id = "t-layout", title = "Layout", steps = { { accept = 456 }, @@ -25,6 +25,7 @@ end local function closeAll() if W.frame then W:Close() end + if Rep.frame then Rep:Close() end if F.frame then F:Close() end if S:IsShown() then S:Close(true) end Cfg:Close() @@ -226,6 +227,48 @@ T.test("setup assistant, every language, gamepad and mouse: nothing overlaps on none(problems, "overlaps in the setup assistant") end) +--------------------------------------------------------------------------- +-- Report a problem +--------------------------------------------------------------------------- +-- Both pages: every text and box inside the window, none over another (the texts wrap whole, +-- the window grows with them), each reason's ring, words and A chip on its row, the buttons +-- and the footer's help apart; with each stop focused in turn. +local function checkReport(where, problems) + Rep:Open(); H.flush() + local f = Rep.frame + for page = 1, 2 do + for i = 1, #Rep:Stops() do + Rep:SetFocus(i) + local parts = { title = f.title, back = f.back, close = f.close, hints = f.hints } + for n, b in ipairs(f.buttons) do parts["button " .. n] = b end + if page == 1 then + parts.intro, parts.contains, parts.what = f.intro, f.containsTitle, f.whatTitle + parts.commentTitle, parts.commentKey, parts.comment = f.commentTitle, f.commentKey, f.comment + parts.status = f.formStatus + for n, fs in ipairs(f.parts) do parts["part " .. n] = fs end + for n, r in ipairs(f.reasons) do + parts["reason " .. n] = r + report(problems, where, ("reason %d %q (focus on %d)"):format(n, r.label:GetText() or "", i), + Layout.check(r, { ring = r.ring, label = r.label, A = r.key })) + end + else + parts.copy, parts.clipboard, parts.text = f.copy, f.clipboard, f.text + parts.paste, parts.link, parts.saved = f.paste, f.link, f.saved + end + report(problems, where, ("page %d, focus on %d"):format(page, i), Layout.check(f, parts)) + end + if page == 1 then Rep:Make(); H.flush() end + end + Rep:Close(); H.flush() +end + +T.test("report a problem, every language, gamepad and mouse: nothing overlaps, nothing leaves the window", function() + start() + local problems = {} + everyLanguage(function(where) checkReport(where, problems) end) + none(problems, "overlaps in the report window") +end) + --------------------------------------------------------------------------- -- Shared sizes --------------------------------------------------------------------------- @@ -292,6 +335,7 @@ local function dialogs() { "directory", function() F:Open(1) end, function() return F.frame end, 80 }, { "setup assistant", function() S:Open() end, function() return S.frame end, 40 }, { "import", function() W:Open("import") end, function() return W.frame end, 40 }, + { "report a problem", function() Rep:Open() end, function() return Rep.frame end, 40 }, } end diff --git a/tools/tests/report_test.lua b/tools/tests/report_test.lua new file mode 100644 index 0000000..a51c79a --- /dev/null +++ b/tools/tests/report_test.lua @@ -0,0 +1,408 @@ +-- Report a problem (Core/Report.lua, UI/Report.lua): the guide window's flag beside the +-- directory's pin, reached by the gamepad's focus mode; the binding, its chord, /ding bug; the +-- window's parts in plain words, the ready reasons with a gamepad, the player's words with a +-- keyboard (never taken from the chat); the text (GitHub Markdown: the step, the position, the +-- quests and their counters, the arrow, the settings, the client; never the character's name +-- nor the realm); the GitHub link; the last 20 reports kept, "Restore all defaults" included. +local tools = (arg[0]:match("^(.*)[/\\]") or ".") .. "/.." +local T = dofile(tools .. "/testlib.lua")(arg[1], { locale = "enUS" }) +local Layout = dofile(tools .. "/layout.lua") +local Ding, H = T.Ding, T.H +local E, P, L, R, W, G = Ding.Engine, Ding.Panel, Ding.L, Ding.Report, Ding.ReportWindow, Ding.Gamepad + +Ding.RegisterGuide({ id = "t-report", title = "Report test", steps = { + { accept = 456 }, + { complete = 456 }, + { turnin = 456, accept = 457 }, + { complete = 916 }, + { complete = 457 }, +} }) + +local function pad(on) + H.setGamepadConnected(on) + H.fire(on and "GAME_PAD_CONNECTED" or "GAME_PAD_DISCONNECTED") + H.flush() +end + +local function click(frame, button) + frame.scripts.OnClick(frame, button or "LeftButton") + H.flush() +end + +-- A night elf in Shadowglen on "The Balance of Nature" (456): 3 of 7 nightsabers slain; +-- "Webwood Venom" (916, named by a next step) and "A Good Friend" (4495, named by none) in the log. +local function start(gamepad) + W:Close() + if P.focusing then P:StopFocus() end + H.closeDialog() + T.reset({ level = 3, class = "HUNTER", race = "NightElf" }) + H.player.name = "Thalindra" + H.player.map, H.player.x, H.player.y = 1438, 58.71, 44.27 + pad(gamepad ~= false) + E:SetGuide("t-report", 1, true); H.flush() + H.accept(456); H.flush() + H.setObjective(456, 1, 3, 7) + H.setQuest(916, "log") + H.setQuest(4495, "log") + H.fire("QUEST_LOG_UPDATE") + H.tick(1) + T.eq(E.index, 2, "not on the step that completes 456") +end + +local function chipsOn(b) + local keys = {} + for _, g in ipairs(b.keyGlyphs or {}) do + if g:IsShown() then keys[g.padKey] = true end + end + return keys +end + +local function anyKey(keys) return next(keys) ~= nil end + +local function shownParts() + local out = {} + for _, fs in ipairs(W.frame.parts) do + if fs:IsShown() then out[#out + 1] = fs:GetText() end + end + return out +end + +local function partWith(text) + for _, part in ipairs(shownParts()) do + if part:find(text, 1, true) then return part end + end + return nil +end + +--------------------------------------------------------------------------- +-- How to open it +--------------------------------------------------------------------------- +T.test("the guide window: a flag right beside the directory's pin, its tooltip in words; a click opens and closes", function() + start() + local f = P.frame + T.ok(f.report:IsShown() and f.finder:IsShown(), "no report button") + local flag, pin = Layout.rect(f.report), Layout.rect(f.finder) + T.ok(math.abs(flag.top - pin.top) < 0.01, "not on the pin's line") + T.ok(flag.right <= pin.left and pin.left - flag.right <= 6, ("not beside the pin (%.0f apart)"):format(pin.left - flag.right)) + -- The title stops before the flag (its drag area too). + T.ok(Layout.rect(f.title).right <= flag.left, "the title runs under the flag") + T.ok(Layout.rect(f.titleArea).right <= flag.left, "the title's drag area covers the flag") + f.report.scripts.OnEnter(f.report) + T.ok(GameTooltip:IsShown() and GameTooltip:GetOwner() == f.report, "no tooltip") + T.eq(GameTooltip.text, L.BIND_REPORT) + f.report.scripts.OnLeave(f.report) + click(f.report) + T.ok(W:IsShown(), "the flag does not open the window") + click(f.report) + T.ok(not W:IsShown(), "the flag does not close it") + -- Minimized: hidden with the other title bar buttons. + P:SetCompact(true); H.flush() + T.ok(not f.report:IsShown(), "the flag stays on the minimized window") + P:SetCompact(false); H.flush() +end) + +T.test("focus mode: up from the first row, the pin then the flag; left and right between them; A opens", function() + start() + local f = P.frame + P:StartFocus(); H.flush() + P.focusIndex = 1; P:Update() + H.press("PADDUP") + T.eq(P.focusIndex, 0, "the pin is not reached") + H.press("PADDUP") + T.eq(P.focusIndex, -1, "the flag is not reached") + T.ok(f.report.focus.ring:IsShown() and f.report.key:IsShown(), "the flag shows neither focus nor A") + T.ok(not f.finder.focus.ring:IsShown() and not f.finder.key:IsShown(), "the pin keeps the focus") + T.ok(GameTooltip:IsShown() and GameTooltip:GetOwner() == f.report and GameTooltip.text == L.BIND_REPORT, + "the focused flag does not say what it is") + -- The A chip sits left of both buttons, the title shortens for it. + T.ok(Layout.rect(f.report.key).right <= Layout.rect(f.report).left, "the A chip covers the flag") + T.ok(Layout.rect(f.title).right <= Layout.rect(f.report.key).left, "the title runs under the A chip") + H.press("PADDRIGHT") + T.eq(P.focusIndex, 0, "right from the flag: not the pin") + T.ok(Layout.rect(f.finder.key).right <= Layout.rect(f.report).left, "the pin's A chip covers the flag") + H.press("PADDLEFT") + T.eq(P.focusIndex, -1, "left from the pin: not the flag") + H.press("PADDUP") + T.eq(P.focusIndex, -1, "above the flag: nothing") + H.press("PADDDOWN"); H.press("PADDDOWN") + T.eq(P.focusIndex, 1, "back down to the first row") + H.press("PADDUP"); H.press("PADDUP"); H.press("PAD1") + T.ok(W:IsShown() and not P.focusing, "A on the flag does not open the window") + W:Close(); H.flush() +end) + +T.test("a key binding, its chord, a row in the Keybinds tab, /ding bug and the /ding help", function() + start() + T.eq(_G.BINDING_NAME_DING_REPORT, L.BIND_REPORT) + T.ok(G:ChordAllowed("DING_REPORT"), "no chord for it") + local xml = assert(io.open(tools .. "/../Ding/Bindings.xml")):read("*a") + T.ok(xml:find('%s*Ding%.ReportWindow:Toggle%(%)'), "not in Bindings.xml") + Ding.Config:Open("bindings"); H.flush() + local found = false + for _, row in ipairs(Ding.Config.TABS[Ding.Config.TAB.bindings].rows) do + if row.opts and row.opts.action == "DING_REPORT" then found = true end + end + T.ok(found, "no row in the Keybinds tab") + Ding.Config:Close(); H.flush() + G.CHORD_RUN.DING_REPORT(); H.flush() + T.ok(W:IsShown(), "the chord's action does not open it") + G.CHORD_RUN.DING_REPORT(); H.flush() + T.ok(not W:IsShown(), "the chord's action does not close it") + SlashCmdList.DING("bug"); H.flush() + T.ok(W:IsShown(), "/ding bug does not open it") + W:Close(); H.flush() + T.ok(L.HELP:find("/ding bug", 1, true), "/ding help does not list /ding bug") + for _, name in ipairs({ "frFR", "deDE", "esES", "ptBR", "koKR", "zhCN", "zhTW" }) do + local source = assert(io.open(tools .. "/../Ding/Locales/" .. name .. ".lua")):read("*a") + T.ok(source:match('\n HELP = "[^\n]*/ding bug'), name .. ": /ding help does not list /ding bug") + end + -- In combat: not opened, said in words. + H.setCombat(true) + T.ok(not W:Open(), "opened in combat") + H.setCombat(false) +end) + +--------------------------------------------------------------------------- +-- The window +--------------------------------------------------------------------------- +T.test("the window says in plain words what the report will contain, before anything is made", function() + start() + W:Open(); H.flush() + local f = W.frame + T.ok(f.title:GetText():find(L.BIND_REPORT, 1, true), "no title") + T.ok(f.form:IsShown() and not f.done:IsShown(), "not on the first page") + T.eq(#R:Saved(), 0, "a report made on opening") + local number, total = E:StepNumber(2) + T.ok(partWith(L.REPORT_PART_STEP:format(number, total, 2, "")), "no step part:\n" .. table.concat(shownParts(), "\n")) + T.ok(partWith(L.REPORT_PART_GUIDE:format("Ding!", "Report test")) or partWith("Report test"), "no guide part") + T.ok(partWith(L.REPORT_PART_PLACE:format("Teldrassil", "58.7", "44.3")) or partWith("58.7, 44.3"), + "no position part:\n" .. table.concat(shownParts(), "\n")) + local quests = partWith(L.REPORT_PART_QUESTS:format("")) + T.ok(quests, "no quests part") + T.ok(quests:find(Ding.Data:QuestName(456), 1, true) and quests:find("3/7", 1, true), + "the step's quest and its counter missing: " .. quests) + T.ok(quests:find(Ding.Data:QuestName(916), 1, true), "the next step's quest of the log missing: " .. quests) + T.ok(not quests:find(Ding.Data:QuestName(4495), 1, true), "a quest no step names is listed: " .. quests) + local arrow = partWith(L.REPORT_PART_ARROW:match("^(.-)%%s")) + local shown = Ding.Arrow.shownPoint + T.ok(shown and arrow and arrow:find(shown.label, 1, true), "the arrow's target missing: " .. tostring(arrow)) + T.ok(partWith(L.REPORT_PART_SETTINGS), "no settings part") + T.ok(partWith(L.REPORT_PART_CLIENT:format(Ding.version)), "no client part") + T.ok(partWith(L.REPORT_PART_PRIVATE), "the window does not say what is never in it") + for _, part in ipairs(shownParts()) do + T.ok(not part:find("Thalindra", 1, true), "the character's name in the window") + end + -- The four ready reasons, none chosen yet; the box for the player's words. + for i, reason in ipairs(R.REASONS) do + T.eq(f.reasons[i].label:GetText(), L[reason.label]) + T.ok(not f.reasons[i].dot:IsShown(), "a reason already chosen") + end + T.ok(f.comment:IsShown(), "no box for the player's words") + W:Close(); H.flush() +end) + +T.test("the text: title line, step, position, quests with counters, arrow, settings, client, the words; no name", function() + start(false) + W:Open(); H.flush() + local f = W.frame + -- With a keyboard the box has it at once: the player types. + T.ok(f.comment.edit:HasFocus(), "the words' box does not take the keyboard") + H.type(f.comment.edit, "Ilthalaine is not at his place\nI looked everywhere") + click(f.reasons[1]) + T.ok(f.reasons[1].dot:IsShown(), "the click does not choose the reason") + click(f.buttons[1]) + T.ok(f.done:IsShown() and not f.form:IsShown(), "not on the text's page") + local text = f.text:GetValue() + local number, total = E:StepNumber(2) + local first = text:match("^[^\n]*") + T.eq(first, ("Ding! report: Step %d/%d of Report test: The NPC is not there"):format(number, total)) + for _, want in ipairs({ + "**What went wrong:** The NPC is not there", + "> Ilthalaine is not at his place\n> I looked everywhere", + "- Section: Report test (`t-report`)", + ("- Step: %d / %d, index 2 of 5 in the guide's file"):format(number, total), + "- Keys: `{ complete = 456 }`", + "- Map: ", "UiMap 1438", + "- Position: 58.7, 44.3", + "- Level 3 NightElf HUNTER, Alliance", + "- [456] The Balance of Nature", + "in the log (this step)", + "Young Nightsaber slain: 3/7 (3/7)", + "- [916] ", + ("(step %d)"):format((E:StepNumber(4))), + "- Candidate points for this step: ", + "- Route: elites off, dungeons off, hearth on, deathSkip off, skipGrey off, serviceSteps on, trainerSteps on, flightSteps on", + "- Theme: ", "TomTom: off", + "- Ding! " .. Ding.version .. ", locale enUS, game 1.60.0 (61234)", + " UTC", + }) do + T.ok(text:find(want, 1, true), ("the text lacks %q:\n%s"):format(want, text)) + end + T.ok(not text:find("[4495]", 1, true), "a quest no step names is in the text") + T.ok(not text:find("Thalindra", 1, true), "the character's name is in the text") + T.ok(not text:find("Forever Realm", 1, true), "the realm is in the text") + T.ok(not text:find("|", 1, true), "a raw | in the text (the box would read it as an escape)") + -- The arrow: what it points at, its map and distance. + local arrow = text:match("%- Points to: ([^\n]*)") + T.ok(arrow and arrow:find("UiMap 1438", 1, true) and arrow:find("yards away", 1, true), + "the arrow's target: " .. tostring(arrow)) + -- Selected for Ctrl+C, said in words; the link to paste it into, selectable too. + T.ok(f.text.edit:HasFocus() and f.text.edit.selection, "the text is not selected") + T.eq(f.copy:GetText(), L.REPORT_COPY) + T.ok(L.REPORT_COPY:find("Ctrl+C", 1, true)) + T.eq(f.clipboard:GetText(), L.REPORT_CLIPBOARD) + T.eq(f.link:GetValue(), "https://github.com/moust4ki/DingGuideWoWForever/issues/new?template=ding-report.yml") + click(f.buttons[2]) + T.ok(f.link.edit:HasFocus() and f.link.edit.selection, "Select the link does not select it") + T.ok(f.saved:GetText():find("WTF\\Account\\\\SavedVariables\\Ding.lua", 1, true), "where the file is") + -- Read-only: typing puts the text back. + H.type(f.text.edit, "changed") + T.eq(f.text:GetValue(), text, "the report can be typed over") + -- Kept with the others. + local saved = R:Saved() + T.eq(#saved, 1) + T.eq(saved[1].text, text) + T.eq(saved[1].title, first) + W:Close(); H.flush() + T.ok(not f.text.edit:HasFocus() and not f.link.edit:HasFocus(), "a box keeps the keyboard once closed") +end) + +T.test("with a gamepad: the reasons first, A picks one and goes on to Create, A creates, Y selects, B closes", function() + start() + W:Open(); H.flush() + local f = W.frame + T.ok(not f.comment.edit:HasFocus(), "the words' box takes the keyboard with a gamepad") + T.ok(f.reasons[1].focus.ring:IsShown() and f.reasons[1].key:IsShown(), "the first reason is not focused with A") + H.press("PADDDOWN") + T.ok(f.reasons[2].focus.ring:IsShown() and not f.reasons[1].key:IsShown(), "D-pad down: not the second reason") + H.press("PAD1") + T.ok(f.reasons[2].dot:IsShown() and W.reason == "arrow", "A does not choose the reason") + T.ok(chipsOn(f.buttons[1]).PAD1, "A not on Create the text after a reason") + T.ok(not f.reasons[2].key:IsShown(), "A left on the reason") + -- The words' box, optional, is a stop between the reasons and the button. + H.press("PADDUP") + T.ok(f.comment.focus.ring:IsShown() and f.commentKey:IsShown(), "the box is not a stop with its A") + H.press("PAD1") + T.ok(f.comment.edit:HasFocus(), "A on the box does not give it the keyboard") + H.type(f.comment.edit, "") + H.press("PADDDOWN") + H.press("PAD1") + T.ok(f.done:IsShown(), "A on Create the text does not make it") + local text = f.text:GetValue() + T.ok(text:find("**What went wrong:** The arrow points to the wrong place", 1, true), text) + T.ok(not text:find("In the player's words", 1, true), "an empty comment is written") + T.ok(text:match("^[^\n]*"):find(": The arrow points to the wrong place$"), "the title line lacks the reason") + -- Each action once: A on the focused button, Y on Select the text, B by the cross. + T.ok(chipsOn(f.buttons[1]).PAD1 and chipsOn(f.buttons[1]).PAD4, "A and Y not on Select the text") + T.ok(not anyKey(chipsOn(f.buttons[2])), "a chip on Select the link") + T.ok(f.back:IsShown(), "no B by the close cross") + T.eq(#(f.hints.list or {}), 0, "a hint repeats a button with a gamepad") + H.press("PADDRIGHT") + T.ok(chipsOn(f.buttons[2]).PAD1 and not chipsOn(f.buttons[1]).PAD1, "D-pad right: A not on Select the link") + H.press("PAD1") + T.ok(f.link.edit:HasFocus(), "A on Select the link") + H.press("PAD4") + T.ok(f.text.edit:HasFocus() and f.text.edit.selection, "Y does not select the text") + H.press("PAD2") + T.ok(not W:IsShown(), "B does not close the window") + T.eq(G.owner, nil, "the gamepad is not given back") +end) + +T.test("Something else with words: the title line takes the player's first words", function() + start() + local info = R:Collect() + local title = R:Title(info, "other", "The boars run through the wall near the well, and more words after that") + T.ok(title:find(": The boars run through the wall near the well, and more...", 1, true), title) + -- Never half a character. + local french = R:Title(info, "other", ("\195\169"):rep(40)) + T.ok(french:find(("\195\169"):rep(28) .. "...", 1, true) and not french:find(("\195\169"):rep(29), 1, true), french) + T.ok(#title < 120, "the title is too long") + T.ok(R:Title(info, "other", ""):find("Report test$"), "Something else without words: " .. R:Title(info, "other", "")) + T.ok(R:Text(info, nil, ""):find("**What went wrong:** not chosen", 1, true), "no reason chosen") +end) + +T.test("keyboard: from the chat (/ding bug) the words' box waits for the chat to close; nothing blocked", function() + start(false) + local problems = H.problems() + H.chat("/ding bug") + local f = W.frame + T.ok(W:IsShown(), "/ding bug from the chat does not open it") + T.ok(H.chatHasKeyboard(), "Ding took the keyboard from the chat") + T.ok(not f.comment.edit:HasFocus(), "the box has the keyboard while the chat does") + T.eq(f.formStatus:GetText(), L.IMPORT_WAIT_KEYBOARD) + T.ok(f.formStatus:IsShown(), "the wait is not said") + H.closeChat() + H.tick(0.2) + T.ok(f.comment.edit:HasFocus(), "the box does not take the keyboard once the chat is closed") + T.ok(not f.formStatus:IsShown(), "the wait is still said") + T.eq(H.problems(), problems, "a blocked action (see above)") + W:Close(); H.flush() +end) + +T.test("mouse: no chip, the mouse help; plugged in while open, the chips come", function() + start(false) + W:Open(); H.flush() + local f = W.frame + T.ok(not f.back:IsShown(), "B by the cross without a gamepad") + for _, r in ipairs(f.reasons) do T.ok(not r.key:IsShown(), "an A chip on a reason") end + T.ok(not anyKey(chipsOn(f.buttons[1])), "a chip on Create the text") + T.eq(#f.hints.list, 1) + T.eq(f.hints.list[1].label, L.REPORT_MOUSE) + T.eq(f.hints.list[1].keys, nil, "a gamepad hint without a gamepad") + -- Right click closes. + f.scripts.OnMouseUp(f, "RightButton") + T.ok(not W:IsShown(), "a right click does not close it") + W:Open(); H.flush() + pad(true) + T.ok(f.back:IsShown() and f.reasons[1].key:IsShown(), "plugged in: no chips") + T.eq(#(f.hints.list or {}), 0, "plugged in: the mouse help stays") + W:Close(); H.flush() + pad(false) + W:Open(); H.flush() + click(f.buttons[1]) + T.eq(f.hints.list[1].label, L.EXPORT_MOUSE, "the text's page: not the copy help") + W:Close(); H.flush() +end) + +--------------------------------------------------------------------------- +-- The reports kept +--------------------------------------------------------------------------- +T.test("the last 20 reports are kept, oldest out first; \"Restore all defaults\" keeps them", function() + start() + local info = R:Collect() + for i = 1, 25 do R:Make(info, "other", "report " .. i) end + local saved = R:Saved() + T.eq(#saved, 20) + T.ok(saved[1].text:find("> report 6", 1, true), "the oldest is not the one dropped") + T.ok(saved[20].text:find("> report 25", 1, true), "the newest is not last") + T.ok(saved[20].at and saved[20].at:find("UTC$"), "no date") + Ding:ResetSettings() + T.eq(#Ding.db.reports, 20, "Restore all defaults drops the reports") + T.eq(Ding.DEFAULTS.reports ~= nil, true) +end) + +T.test("no guide, no position: the report still says what it can", function() + start() + E.guide = nil + H.player.map = nil + W:Open(); H.flush() + T.ok(partWith(L.REPORT_NO_GUIDE), "no guide: not said") + T.ok(partWith(L.REPORT_PLACE_UNKNOWN), "no position: not said") + click(W.frame.buttons[1]) + local text = W.frame.text:GetValue() + T.ok(text:find("^Ding! report: No guide"), text) + T.ok(text:find("- No guide", 1, true) and text:find("- Position: unknown here", 1, true), text) + W:Close(); H.flush() +end) + +T.test("combat closes it and gives the gamepad back", function() + start() + W:Open(); H.flush() + H.setCombat(true) + H.fire("PLAYER_REGEN_DISABLED"); H.flush() + T.ok(not W:IsShown(), "still open in combat") + T.eq(G.owner, nil, "the gamepad is not given back") + H.setCombat(false) +end) + +T.run() diff --git a/tools/tests/single_action_test.lua b/tools/tests/single_action_test.lua index d19e62d..9270062 100644 --- a/tools/tests/single_action_test.lua +++ b/tools/tests/single_action_test.lua @@ -3,12 +3,13 @@ -- button). With a gamepad, a button carries its chips (A on the focused one, B on the one -- that closes, Y where a button selects) and the hint bar keeps only what has no button; -- without one, the buttons alone and the mouse help. Checked in every window with a hint bar --- (import / export, settings, move mode, the NPC directory), gamepad plugged in and not, while --- it is plugged in or out, and in every language (buttons and hints fit). +-- (import / export, settings, move mode, the NPC directory, report a problem), gamepad plugged +-- in and not, while it is plugged in or out, and in every language (buttons and hints fit). local tools = (arg[0]:match("^(.*)[/\\]") or ".") .. "/.." local T = dofile(tools .. "/testlib.lua")(arg[1]) local Ding, H = T.Ding, T.H local Cfg, G, W, F, L = Ding.Config, Ding.Gamepad, Ding.ImportWindow, Ding.Finder, Ding.L +local Rep = Ding.ReportWindow local root = tools .. "/.." local GUIDE = [[{ id = "t-once", title = "Once", minLevel = 1, maxLevel = 10, @@ -24,6 +25,7 @@ local function closeAll() if G:IsMoving() then G:CancelMove() end if W.frame then W:Close() end if F.frame then F:Close() end + Rep:Close() Cfg:Close() H.flush() end @@ -367,6 +369,62 @@ T.test("NPC directory: A on the focused row, B on the cross or the list's title, closeAll() end) +--------------------------------------------------------------------------- +-- Report a problem +--------------------------------------------------------------------------- +-- What carries a chip: the reasons, the box's title, the buttons, B by the close cross. +local function reportButtons() + local f, out = Rep.frame, {} + for i, b in ipairs(f.buttons) do + if b:IsShown() then out[#out + 1] = { label = Rep.actions[i].label, keys = chipsOn(b), button = b } end + end + for _, r in ipairs(f.reasons) do + out[#out + 1] = { label = r.label:GetText(), keys = r.key:IsShown() and { [r.key.key or "PAD1"] = true } or {} } + end + out[#out + 1] = { keys = f.commentKey:IsShown() and { PAD1 = true } or {} } + out[#out + 1] = { keys = f.back:IsShown() and { PAD2 = true } or {} } + return out +end + +-- With a gamepad each key shows once (A where the focus is, B by the cross, Y on Select the +-- text), no hint; without one, no chip and the mouse help. The buttons fit beside the help. +local function checkReport(where) + local f = Rep.frame + local buttons = reportButtons() + if G:Active() then + local count = {} + for _, b in ipairs(buttons) do + for key in pairs(b.keys) do count[key] = (count[key] or 0) + 1 end + end + T.eq(count.PAD1, 1, where .. ": A") + T.eq(count.PAD2, 1, where .. ": B") + T.eq(count.PAD4, Rep.page == "done" and 1 or nil, where .. ": Y") + T.eq(#hintsOf(f.hints), 0, where .. ": a hint with a gamepad") + else + mouseOnly(where, f.hints, buttons) + end + T.eq(f.hints.shown, #f.hints.list, where .. ": a hint left out") +end + +T.test("report a problem: A where the focus is, Y on Select the text, B by the cross; the mouse help without", function() + start() + Rep:Open(); H.flush() + checkReport("report") + for _ = 1, #Rep:Stops() do + H.press("PADDDOWN") + checkReport("report, focus on " .. Rep.focus) + end + -- A on a reason goes on to Create the text, A there makes it. + H.press("PAD1"); H.press("PAD1"); H.flush() + T.eq(Rep.page, "done", "A on Create the text") + checkReport("report's text") + H.press("PADDRIGHT") + checkReport("report's text, the link's button focused") + pad(false) + checkReport("report's text, unplugged") + closeAll() +end) + --------------------------------------------------------------------------- -- Every language: the buttons, their chips and the hints fit --------------------------------------------------------------------------- @@ -425,6 +483,11 @@ T.test("every language: import, export, settings buttons and move mode fit, game startMove() checkPlate(where .. ", move") closeAll() + Rep:Open(); H.flush() + checkReport(where .. ", report") + Rep:Make(); H.flush() + checkReport(where .. ", report's text") + closeAll() end end useStrings(own) diff --git a/tools/wow_mock.lua b/tools/wow_mock.lua index 3bf4df2..d53d320 100644 --- a/tools/wow_mock.lua +++ b/tools/wow_mock.lua @@ -509,6 +509,9 @@ return function(opts) _G.UnitRace = function() return player.race, player.race end _G.UnitClass = function() return player.class, player.class end _G.UnitFactionGroup = function() return player.faction end + -- The client's version, build, build date, interface; the realm (never in a report of Ding's). + _G.GetBuildInfo = function() return "1.60.0", "61234", "Oct 1 2026", 16001 end + _G.GetRealmName = function() return "Forever Realm" end -- Font objects; NumberFontNormalLarge is left out on purpose to exercise the fallback. local function fontObject(size) return { GetFont = function() return "Fonts\\FRIZQT__.TTF", size, "" end } end for name, size in pairs({ GameFontNormal = 12, GameFontHighlight = 12, GameFontHighlightSmall = 10, @@ -925,7 +928,7 @@ return function(opts) if plateUnits[unit] then return plateUnits[unit].name end if unit == "target" then return targetName end if unit == "npc" then return npcName end - if unit == "player" then return "Player" end + if unit == "player" then return player.name or "Player" end return nil end _G.IsInGroup = function() return inGroup end From 9f1bb2102d1ad3dc12a93ccd2b4b4d858f0f603c Mon Sep 17 00:00:00 2001 From: Moust4ki Date: Thu, 15 Oct 2026 16:13:31 +0200 Subject: [PATCH 2/7] Report a problem: the GitHub issue form, README, changelog v1.8.0, docs .github/ISSUE_TEMPLATE/ding-report.yml: "Paste the text Ding made here" and "What happened?". README: the feature, /ding bug and how to report a problem. CHANGELOG: the v1.8.0 entry. SPEC, architecture and UI handoff: the window, the text, DingDB.reports, the flag in focus mode. Co-Authored-By: Claude Opus 5.5 --- .github/ISSUE_TEMPLATE/ding-report.yml | 23 ++++++++++++ CHANGELOG.md | 18 +++++++++ README.md | 30 ++++++++++++++- docs/SPEC.md | 52 ++++++++++++++++++++++++-- docs/UI-handoff.md | 20 ++++++++++ docs/architecture.md | 14 ++++++- 6 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/ding-report.yml diff --git a/.github/ISSUE_TEMPLATE/ding-report.yml b/.github/ISSUE_TEMPLATE/ding-report.yml new file mode 100644 index 0000000..d87b3e5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ding-report.yml @@ -0,0 +1,23 @@ +name: Report a problem (made in game by Ding!) +description: Paste the text Ding! wrote for you in game, with the step, your position and your quests. +title: "Ding! report: " +body: + - type: markdown + attributes: + value: | + In game, open **Report a problem** in Ding!: the flag in the guide window's title bar, its key (Settings > Keybinds), or `/ding bug`. Pick what went wrong, add a few words if you can, then **Create the text** and copy it with Ctrl+C. + The text's first line can be the title above. Without a keyboard, Ding! keeps your last 20 reports in `WTF\Account\\SavedVariables\Ding.lua` (written when you log out or reload): you can attach that file instead. + - type: textarea + id: report + attributes: + label: Paste the text Ding made here + description: The whole text, from "Ding! report:" to the date. It never holds your character's name, realm or account. + validations: + required: true + - type: textarea + id: what + attributes: + label: What happened? + description: Anything the text does not say (optional). + validations: + required: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 852e38d..a4194c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## v1.8.0 + +Nothing else to install: QuestieDB is no longer needed, and TomTom stays optional. + +- **Report a problem**: a flag in the guide window's title bar, right beside the NPC + directory's pin (with a gamepad: up from the first line in focus mode), its own key binding + (Settings > Keybinds, a button or two like the others) and `/ding bug`. A window shows in plain + words what the report will contain: the guide, the step and its place in the guide's file, + where you stand, your character's level, race, class and side, the step's quests and the ones + of your log the next steps need, with their counters, what the arrow points at, the settings + that change your route, Ding's and the game's versions. Never your character's name, your + realm or your account. +- Pick what went wrong among four ready answers (handy with a gamepad) and add your own words if + you can type, then **Create the text**: it is selected for Ctrl+C (an addon cannot copy for + you), with the link of the GitHub page to paste it into. Ding keeps your last 20 reports in its + saved variables (`WTF\Account\\SavedVariables\Ding.lua`), so a player without a + keyboard can send that file instead; "Restore all defaults" keeps them. + ## v1.7.1 Nothing else to install: QuestieDB is no longer needed, and TomTom stays optional. diff --git a/README.md b/README.md index a0f0c9c..8f7f389 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,11 @@ instead from the *Arrow* tab of the settings (off by default). NPC. When you carry (or a quest offers) a weapon your class can use but you have not learned, the guide window says so, why, and which weapon master teaches it; right-click it (Left in focus mode) never to see that weapon suggested again. +- **Report a problem.** An NPC missing, the arrow pointing to the wrong place, a quest not + offered? The flag in the guide window's title bar (beside the NPC directory's pin), its key + binding or `/ding bug` writes a precise report for you: the step, where you stand, your + quests and their counters, what the arrow points at, your route settings, never your + character's name. See [Report a problem](#report-a-problem). - **Minimap button.** Left-click for the settings, right-click to show or hide the guide. - **Community guides.** Ding runs other guides too: install a guide pack and pick it in the settings (*Guide* tab, first list), then its section in the second list. See @@ -117,7 +122,7 @@ The guide window never takes a button away from the game. To browse it with the bind "Browse the guide with the gamepad" (focus mode): the D-pad picks a line, A sends the arrow there (or uses the item), X shows its quest, LB / RB change steps, Y minimizes the window, B gives the gamepad back to the game. Up from the first line, the focus reaches the -NPC directory's button. +NPC directory's button, then the "Report a problem" flag beside it. The NPC directory (bind "NPC directory", or use the guide window's pin button): LB / RB change tabs, the D-pad picks, A opens a list or sends the arrow, B goes back or closes. To cancel a trip: @@ -150,10 +155,31 @@ lifts, gates and tunnels near you, for testers; the arrow points at the n-th) · text) · `/ding export [id]` (a guide as text) · `/ding find ` (the NPC directory: a service, a weapon, a profession or a name, in your game's language or in English, such as `/ding find gun`, `/ding find bank`, `/ding find Buliwyf`) · `/ding cancel` (end the directory's -trip: the arrow follows the guide again) · `/ding next` · `/ding prev` · `/ding skip` · `/ding back` · `/ding reset` (take the guide up +trip: the arrow follows the guide again) · `/ding bug` (report a problem) · `/ding next` · `/ding prev` · `/ding skip` · `/ding back` · `/ding reset` (take the guide up where you are) · `/ding scale <0.5-1.5>` · `/ding arrow` · `/ding arrowflip` · `/ding minimap` · `/ding debug` +## Report a problem + +Something wrong with a step? Ding writes the report for you, in game: + +1. Open **Report a problem**: the flag in the guide window's title bar (beside the NPC + directory's pin; with a gamepad, up from the first line in focus mode), its key binding + (*Keybinds* tab: a button or two, like the others) or `/ding bug`. +2. The window shows what the report will contain: the guide, the step ("Step 19 / 161") and its + place in the guide's file, where you stand, your character, the step's quests and their + counters, what the arrow points at, the settings that change your route, Ding's and the + game's versions. Never your character's name, your realm or your account. +3. Pick what went wrong ("The NPC is not there", "The arrow points to the wrong place", "The + quest is not offered", "Something else") and, if you can type, add a few words. +4. **Create the text**: it is selected, copy it with Ctrl+C (an addon cannot copy for you), then + paste it into a [new report on GitHub](https://github.com/moust4ki/DingGuideWoWForever/issues/new?template=ding-report.yml) + (the window shows that link too). Its first line can be the title. + +No keyboard at hand? Ding keeps your last 20 reports in +`WTF\Account\\SavedVariables\Ding.lua` (written when you log out or reload): send that +file instead. + ## Community guide packs Anyone can write a guide for Ding and share it as a **guide pack**: a small addon of Lua diff --git a/docs/SPEC.md b/docs/SPEC.md index 5d2893b..616c913 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -1311,6 +1311,48 @@ textures and font flags, never a size nor a position. - `/ding cancel` ("Trip cancelled: the arrow follows the guide again.", or "No trip in progress."). A weapon master's trainer window is not taken for the class trainer's (`Training:Record`). +- **Report a problem** (binding "Report a problem", `DING_REPORT`, a chord like the others; the + guide window's flag, right beside the directory's pin; `/ding bug`; `Core/Report.lua` writes, + `UI/Report.lua` shows): players send precise reports the authors read on GitHub issues. + - What it holds, read when the window opens: the guide pack, the section (title and id), the + step on screen (the held one while it shows) as the window numbers it ("Step 19 / 161") and + its index in the guide's file, its summary (`Engine:Summary`) and its keys on one line + (`Import:StepText`: the step as a guide writes it, without text, note and why); the player's + UiMap (id and name), sub-zone, x and y (one decimal), level, race, class, side; the step's + quests (accept, complete, turnin) whatever their state, then the quests of the log that the + next 3 steps name (`Engine:NextSteps`), each with its id, English name (`Data: + QuestEnglishName`, and the client's when it differs), level, state (in the log, ready to + turn in, turned in, not in the log), where the guide names it, and its objectives as the + client gives them (text, have / need, done); what the arrow shows (the point: label, NPC or + object id, map, x, y, yards away or unknown; the travel hint, the directory's trip, hidden), + the step's number of candidate points; the route options (elites, dungeons, hearth, + deathSkip, skipGrey, serviceSteps, trainerSteps, flightSteps), the theme, TomTom on or off + (and installed); Ding's version, the client's locale, its build (`GetBuildInfo`), the date + and time in UTC. **Never** the character's name, the realm nor anything of the account. + - First page ("Ding! · Report a problem", a dialog like the directory): one sentence on what + it is for and that nothing is sent by itself; "What the report will contain", one plain + sentence per part (the step, where you are, your character, the quests with their counters + and "and 2 more", the arrow, the settings, the versions, never your name); "What went + wrong?", four ready reasons with a radio ring (a dot when chosen: never color alone): "The + NPC is not there", "The arrow points to the wrong place", "The quest is not offered", + "Something else"; "In your words (optional)", a box with a line in it while empty; "Create + the text" (accent). With a gamepad the reasons come first: A picks one and the focus goes + on to "Create the text" (a gamepad types little); the box is a stop too, A gives it the + keyboard. Without one, the box takes the keyboard at once. A box never takes the keyboard + from a box that is not Ding's (the chat's, after `/ding bug`): it waits, and a line says so. + - "Create the text": the report in a read-only box, selected, "The text is selected: Ctrl+C + to copy.", a line saying an addon cannot put text on the clipboard, the link of the GitHub + form (`.github/ISSUE_TEMPLATE/ding-report.yml`) in a one-line read-only box, and where the + reports are kept for a player without a keyboard (`WTF\Account\\SavedVariables\ + Ding.lua`, written when the player logs out or reloads). Buttons "Select the text" (Y) and + "Select the link". + - The text: GitHub Markdown, English labels, the player's words as typed (a quote). Its first + line is a title ("Ding! report: Step 19/161 of
: ", the player's first words + cut after a word near 60 characters when the reason is "Something else"), then "What went + wrong", the words, and sections Guide, Player, Quests, Arrow, Settings, Client. No raw "|" + (the box would read an escape). + - Kept: each text made goes to `DingDB.reports` (`{ at, title, text }`), the last 20, the + oldest out first; not a setting: "Restore all defaults" keeps them. - **Weapon to learn** (guide window, option "Remind me of weapons to learn" in the Guide tab, on by default): a row "Weapon to learn: Guns — Buliwyf Stonehand, Ironforge" (the weapon master the fewest seconds away) only when the player has such a weapon in hand: in the bags, @@ -1516,8 +1558,9 @@ gamepad buttons). Every gamepad action therefore has an equivalent: - Game bindings: next / previous step, show the guide, minimize the guide, open the settings, use the quest item, target the arrow's NPC or monster, browse the guide (focus - mode), skip the step, go back to a skipped step, the NPC directory, the arrow on the step's next target. They work in combat, - except opening the settings, focus mode and the NPC directory. + mode), skip the step, go back to a skipped step, the NPC directory, the arrow on the step's next target, + report a problem. They work in combat, except opening the settings, focus mode, the NPC + directory and the report window. - No window captures a button permanently: the guide window stays displayed without taking anything away from the game. - Focus mode of the guide window: the D-pad moves through the lines, A points the arrow at @@ -1526,11 +1569,12 @@ gamepad buttons). Every gamepad action therefore has an equivalent: On a talent suggestion, A twice learns the talent. Right, on a row with a quest, finds that quest in the guide; Left drops the offer. Up from the first line, the focus goes to the NPC directory's button of the title bar (outer focus ring, A beside it): A opens the - directory. + directory. Up again (or Left), the "Report a problem" flag beside it (Right goes back to the + pin): A opens the report window. The A chip of either sits left of the flag. Only these buttons are taken; the mode stops in combat. - Settings window: temporary bindings (`SetOverrideBindingClick`) set when it opens and removed when it closes. Every Ding window that takes the gamepad (settings, setup - assistant, NPC directory, import / export, focus mode, move mode) also reads its buttons with + assistant, NPC directory, import / export, report a problem, focus mode, move mode) also reads its buttons with a frame of its own first (`DingPadInput`, `OnGamePadButtonDown`), before any binding: neither the game's gamepad interface nor another addon's override bindings go before them (reported: in focus mode, Easy Controller on, the D-pad cast the gamepad bar's spells); only the diff --git a/docs/UI-handoff.md b/docs/UI-handoff.md index 6252390..9455889 100644 --- a/docs/UI-handoff.md +++ b/docs/UI-handoff.md @@ -546,6 +546,26 @@ Objective verbs: `Accept` · `Turn in` · `Kill` · `Collect` · `Use … on … title bar, left of minimize (a map pin: an 8-unit `text.secondary` circle with a 3-unit `bg` dot, over a 7-unit downward caret); tooltip "NPC directory". In focus mode, up from the first row: the outer focus ring around it and the A chip on its left (the title shortens). +- **The guide window's "Report a problem" button** (not in the mockup): a 20 × 20 icon button + right beside the directory's pin, on its left (4 between them; the title and its drag area + stop 4 before it): a flag, a 1.5 × 12 `text.secondary` pole with a 7 × 5 `text.secondary` + cloth at its top right; tooltip "Report a problem". In focus mode, up from the pin (or Left + from it): its outer focus ring; the A chip of the pin or the flag sits 6 left of the flag. +- **Report a problem window** (not in the mockup): a dialog of Ding's, 520 wide, the shared + sizes (UI.SIZE), the title bar "Ding! · Report a problem" with the close cross and B beside it + (gamepad). First page, 12 from the edges: the intro (12 Regular `text`, wrapped), the block + titles "What the report will contain", "What went wrong?", "In your words (optional)" (11 + SemiBold `text.muted`), the parts (11 Regular `text.secondary`, wrapped, 8 further in), the + four reasons as rows of the setup assistant's look (radio ring 16 `text.muted`, its 8-unit + `accent` dot and an `accent` ring when chosen, 12 Regular label, inset focus, A on the right), + the box (52 high, `bg.control`, outer focus ring when focused, its A chip at the right of its + title, a `text.muted` line in it while empty), then a status line when the window waits for the + keyboard. Second page: "The text is selected: Ctrl+C to copy." (12 Medium), the clipboard line + (11 `text.secondary`), the read-only text box (196 high, 11 Regular `text.secondary`), "Then + paste it…" (12 Regular), the link's one-line box (26), where the reports are kept (11 + `text.secondary`). Footer 40 under a divider: buttons from the right (24 high, "Create the + text" or "Select the text" in the accent), the mouse help on the left without a gamepad. The + window grows with its texts: none is ever cut. - **Information apart from the step** (Yendi's first rule for the window): after the step's own rows, a 1-unit `divider` line across the list, then a section title "Good to know" (11 SemiBold `text.muted`, no icon, padding 8 above, none below; "Next steps" has the same line and title), diff --git a/docs/architecture.md b/docs/architecture.md index db57884..a4f8bc3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -12,7 +12,8 @@ Ding/ Ding.toc load order; no hard dependency; optional: TomTom Bindings.xml bindings DING_NEXT / DING_PREV / DING_TOGGLE / DING_CONFIG / DING_COMPACT, DING_FOCUS (focus mode), DING_SKIP / DING_UNSKIP, DING_FINDER (NPC directory), - DING_CYCLE (the arrow on the step's next target), + DING_CYCLE (the arrow on the step's next target), DING_REPORT (report a + problem), CLICK DingItemButton:LeftButton (quest item), CLICK DingItemButton:Target (target the arrow's NPC or monster: the same secure button, virtual button) Core/ @@ -58,6 +59,11 @@ Ding/ Why.lua Ding.Why: why the guide makes its choices, in plain words (a step's `why`, a section's `leftOut`): a short line under a surprising step, the whole reason on demand (row tooltips), why a quest is left out (Why:Quest) + Report.lua Ding.Report: "Report a problem": the facts (Collect: the guide, the step on + screen, the position, the step's quests and the log's quests the next 3 + steps name, the arrow's target, the route settings, the client; never the + character's name nor the realm), the text (GitHub Markdown, English + labels), the last 20 kept in DingDB.reports (not a setting) Commands.lua /ding UI/ Themes.lua Ding.UI.Themes: the four themes (halo by default, ardoise, bandes, @@ -108,6 +114,9 @@ Ding/ Setup.lua Ding.Setup: the setup assistant, four screens of choices (Setup.SCREENS), once per account at the first launch (Setup:FirstLaunch), again from the Guide tab or /ding intro + Report.lua Ding.ReportWindow: "Report a problem" (DING_REPORT, /ding bug, the guide + window's flag): what the report holds in plain words, four ready reasons, + the player's words; then the text selected for Ctrl+C and the GitHub link Locales/ enUS.lua interface texts in English (reference, default language) .lua interface texts of one language (frFR, deDE, esES, ptBR, koKR, zhCN, zhTW) @@ -354,7 +363,8 @@ Contracts between modules: `timings`, `flightTimes` (`[":"] = { seconds, count }`, flights measured), `imported` (`[pack id] = { pack, guides, at }`: guides imported as text, kept by "restore all defaults"), `chords` (`[action] = "PADLSHOULDER+PADDUP"`: Ding's gamepad chords, kept by - "restore all defaults"), + "restore all defaults"), `reports` (the last 20 of "Report a problem", oldest first: + `{ at, title, text }`, kept by "restore all defaults": Core/Report.lua), `debug`, `layout` (settings version: `Ding.Migrate`; 3: themes, Halo for everyone). Defaults: `Ding.DEFAULTS`. From 6f994d3730bdf0887ecec3c40beacfd91b99f504 Mon Sep 17 00:00:00 2001 From: Moust4ki Date: Thu, 15 Oct 2026 16:18:36 +0200 Subject: [PATCH 3/7] Problem with this step?: a warm flag that invites a press, plain wording Yendi: players must notice the button and feel invited to press it, and it must read as a problem with the guide, not a technical bug report. The flag now sits on a warm fill of its own in every theme (tokens report and onReport: Halo and Ardoise #FF6A3D, Bandes #FF7A50, Ding dark #FF7043; the flag at 6.5:1 or more on it, the fill at 6.5:1 or more on the window, unlike the accent), a lighter veil under the mouse, its tooltip in words: "Problem with this step?" and a line under it. The window's title and the binding: "Report a problem with the guide". No player-facing text says "bug" (the /ding bug command apart). Icon only: a word on it would cut the guide's title at the window's usual widths. Tests: every theme's fill, ink, contrast and focus ring; the tooltip; no "bug" in the report texts of the 8 languages. Co-Authored-By: Claude Opus 5.5 --- .github/ISSUE_TEMPLATE/ding-report.yml | 2 +- CHANGELOG.md | 4 +- Ding/Locales/deDE.lua | 4 +- Ding/Locales/enUS.lua | 4 +- Ding/Locales/esES.lua | 4 +- Ding/Locales/frFR.lua | 4 +- Ding/Locales/koKR.lua | 4 +- Ding/Locales/ptBR.lua | 4 +- Ding/Locales/zhCN.lua | 4 +- Ding/Locales/zhTW.lua | 4 +- Ding/UI/Panel.lua | 35 ++++++++++++---- Ding/UI/Themes.lua | 6 +++ README.md | 9 ++-- docs/SPEC.md | 14 +++++-- docs/UI-handoff.md | 11 +++-- docs/UI-themes.md | 8 +++- tools/tests/report_test.lua | 57 +++++++++++++++++++++++++- 17 files changed, 145 insertions(+), 33 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/ding-report.yml b/.github/ISSUE_TEMPLATE/ding-report.yml index d87b3e5..e08ffda 100644 --- a/.github/ISSUE_TEMPLATE/ding-report.yml +++ b/.github/ISSUE_TEMPLATE/ding-report.yml @@ -5,7 +5,7 @@ body: - type: markdown attributes: value: | - In game, open **Report a problem** in Ding!: the flag in the guide window's title bar, its key (Settings > Keybinds), or `/ding bug`. Pick what went wrong, add a few words if you can, then **Create the text** and copy it with Ctrl+C. + In game, open **Report a problem with the guide** in Ding!: the orange flag in the guide window's title bar ("Problem with this step?"), its key (Settings > Keybinds), or `/ding bug`. Pick what went wrong, add a few words if you can, then **Create the text** and copy it with Ctrl+C. The text's first line can be the title above. Without a keyboard, Ding! keeps your last 20 reports in `WTF\Account\\SavedVariables\Ding.lua` (written when you log out or reload): you can attach that file instead. - type: textarea id: report diff --git a/CHANGELOG.md b/CHANGELOG.md index a4194c1..de53270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ Nothing else to install: QuestieDB is no longer needed, and TomTom stays optional. -- **Report a problem**: a flag in the guide window's title bar, right beside the NPC - directory's pin (with a gamepad: up from the first line in focus mode), its own key binding +- **Problem with this step?** An orange flag in the guide window's title bar, right beside the + NPC directory's pin (with a gamepad: up from the first line in focus mode), its own key binding (Settings > Keybinds, a button or two like the others) and `/ding bug`. A window shows in plain words what the report will contain: the guide, the step and its place in the guide's file, where you stand, your character's level, race, class and side, the step's quests and the ones diff --git a/Ding/Locales/deDE.lua b/Ding/Locales/deDE.lua index 5ab2bc2..a20f175 100644 --- a/Ding/Locales/deDE.lua +++ b/Ding/Locales/deDE.lua @@ -378,7 +378,9 @@ Ding.Locales.deDE = { CITY_RATCHET = "Ratschet", CITY_EVERLOOK = "Ewige Warte", BIND_FINDER = "NSC-Verzeichnis", - BIND_REPORT = "Problem melden", + BIND_REPORT = "Ein Problem mit dem Guide melden", + REPORT_BUTTON = "Ein Problem mit diesem Schritt?", + REPORT_BUTTON_TIP = "Sag es den Leuten, die den Guide machen: Ding! schreibt den Bericht für dich.", TRAINING_DUE = "Neue Zauber bei deinem Klassenlehrer (%s)", TRAINING_DUE_GUESS = "Neue Zauber warten vielleicht bei deinem Klassenlehrer (Stufe %d)", TRAINING_NEAR = "Dein Klassenlehrer %s ist gleich hier: neue Zauber warten.", diff --git a/Ding/Locales/enUS.lua b/Ding/Locales/enUS.lua index 2653da5..efdca6d 100644 --- a/Ding/Locales/enUS.lua +++ b/Ding/Locales/enUS.lua @@ -381,7 +381,9 @@ Ding.Locales.enUS = { CITY_RATCHET = "Ratchet", CITY_EVERLOOK = "Everlook", BIND_FINDER = "NPC directory", - BIND_REPORT = "Report a problem", + BIND_REPORT = "Report a problem with the guide", + REPORT_BUTTON = "Problem with this step?", + REPORT_BUTTON_TIP = "Tell the people who make the guide: Ding! writes the report for you.", TRAINING_DUE = "New spells at your class trainer (%s)", TRAINING_DUE_GUESS = "New spells may wait at your class trainer (level %d)", TRAINING_NEAR = "Your class trainer %s is right here: new spells are waiting.", diff --git a/Ding/Locales/esES.lua b/Ding/Locales/esES.lua index b0f29f0..2fd5507 100644 --- a/Ding/Locales/esES.lua +++ b/Ding/Locales/esES.lua @@ -381,7 +381,9 @@ Ding.Locales.esES = { CITY_RATCHET = "Trinquete", CITY_EVERLOOK = "Vista Eterna", BIND_FINDER = "Directorio de PNJ", - BIND_REPORT = "Informar de un problema", + BIND_REPORT = "Informar de un problema con la guía", + REPORT_BUTTON = "¿Algún problema con este paso?", + REPORT_BUTTON_TIP = "Cuéntaselo a quienes hacen la guía: Ding! escribe el informe por ti.", TRAINING_DUE = "Hechizos nuevos con tu instructor de clase (%s)", TRAINING_DUE_GUESS = "Puede que haya hechizos nuevos con tu instructor de clase (nivel %d)", TRAINING_NEAR = "Tu instructor de clase %s está aquí mismo: te esperan hechizos nuevos.", diff --git a/Ding/Locales/frFR.lua b/Ding/Locales/frFR.lua index f85888c..cbdaaf5 100644 --- a/Ding/Locales/frFR.lua +++ b/Ding/Locales/frFR.lua @@ -377,7 +377,9 @@ Ding.Locales.frFR = { CITY_RATCHET = "Cabestan", CITY_EVERLOOK = "Long-guet", BIND_FINDER = "Annuaire des PNJ", - BIND_REPORT = "Signaler un problème", + BIND_REPORT = "Signaler un souci avec le guide", + REPORT_BUTTON = "Un souci avec cette étape ?", + REPORT_BUTTON_TIP = "Dis-le à ceux qui font le guide : Ding! écrit le rapport pour toi.", TRAINING_DUE = "Nouveaux sorts chez ton maître de classe (%s)", TRAINING_DUE_GUESS = "De nouveaux sorts t'attendent peut-être chez ton maître de classe (niveau %d)", TRAINING_NEAR = "Ton maître de classe %s est juste là : de nouveaux sorts t'attendent.", diff --git a/Ding/Locales/koKR.lua b/Ding/Locales/koKR.lua index 8095a33..30d8545 100644 --- a/Ding/Locales/koKR.lua +++ b/Ding/Locales/koKR.lua @@ -379,7 +379,9 @@ Ding.Locales.koKR = { CITY_RATCHET = "톱니항", CITY_EVERLOOK = "눈망루 마을", BIND_FINDER = "NPC 안내소", - BIND_REPORT = "문제 신고", + BIND_REPORT = "가이드 문제 신고", + REPORT_BUTTON = "이 단계에 문제가 있나요?", + REPORT_BUTTON_TIP = "가이드 제작자에게 알려 주세요. 신고 내용은 Ding!이 대신 작성합니다.", TRAINING_DUE = "직업 전문가에게 새 주문 (%s)", TRAINING_DUE_GUESS = "직업 전문가에게 새 주문이 있을 수 있습니다 (%d 레벨)", TRAINING_NEAR = "직업 전문가 %s: 바로 여기 있습니다. 새 주문을 배울 수 있습니다.", diff --git a/Ding/Locales/ptBR.lua b/Ding/Locales/ptBR.lua index 6d56a61..3b96d04 100644 --- a/Ding/Locales/ptBR.lua +++ b/Ding/Locales/ptBR.lua @@ -379,7 +379,9 @@ Ding.Locales.ptBR = { CITY_RATCHET = "Vila Catraca", CITY_EVERLOOK = "Visteterna", BIND_FINDER = "Diretório de PNJs", - BIND_REPORT = "Relatar um problema", + BIND_REPORT = "Relatar um problema com o guia", + REPORT_BUTTON = "Algum problema com este passo?", + REPORT_BUTTON_TIP = "Conte para quem faz o guia: o Ding! escreve o relatório para você.", TRAINING_DUE = "Novos feitiços com seu instrutor de classe (%s)", TRAINING_DUE_GUESS = "Talvez haja feitiços novos com o instrutor da sua classe (nível %d)", TRAINING_NEAR = "O instrutor da sua classe, %s, está bem aqui: feitiços novos esperam por você.", diff --git a/Ding/Locales/zhCN.lua b/Ding/Locales/zhCN.lua index c805f80..b186d0f 100644 --- a/Ding/Locales/zhCN.lua +++ b/Ding/Locales/zhCN.lua @@ -379,7 +379,9 @@ Ding.Locales.zhCN = { CITY_RATCHET = "棘齿城", CITY_EVERLOOK = "永望镇", BIND_FINDER = "NPC名录", - BIND_REPORT = "报告问题", + BIND_REPORT = "报告指南的问题", + REPORT_BUTTON = "这一步有问题吗?", + REPORT_BUTTON_TIP = "告诉制作指南的人:Ding! 会替你写好报告。", TRAINING_DUE = "职业训练师处有新技能(%s)", TRAINING_DUE_GUESS = "职业训练师那里可能有新法术(%d级)", TRAINING_NEAR = "你的职业训练师%s就在这里:有新法术可以学习。", diff --git a/Ding/Locales/zhTW.lua b/Ding/Locales/zhTW.lua index a53a1d0..aaf9b00 100644 --- a/Ding/Locales/zhTW.lua +++ b/Ding/Locales/zhTW.lua @@ -379,7 +379,9 @@ Ding.Locales.zhTW = { CITY_RATCHET = "棘齒城", CITY_EVERLOOK = "永望鎮", BIND_FINDER = "NPC名錄", - BIND_REPORT = "回報問題", + BIND_REPORT = "回報指南的問題", + REPORT_BUTTON = "這一步有問題嗎?", + REPORT_BUTTON_TIP = "告訴製作指南的人:Ding! 會替你寫好回報。", TRAINING_DUE = "職業訓練師處有新技能(%s)", TRAINING_DUE_GUESS = "職業訓練師那裡可能有新法術(%d級)", TRAINING_NEAR = "你的職業訓練師%s就在這裡:有新法術可以學習。", diff --git a/Ding/UI/Panel.lua b/Ding/UI/Panel.lua index 58d81f7..52e9e0e 100644 --- a/Ding/UI/Panel.lua +++ b/Ding/UI/Panel.lua @@ -1182,19 +1182,32 @@ function P:Create() f.finder.key = UI.Glyph(f) f.finder.key:Hide() - -- Report a problem (UI/Report.lua): a flag, left of the directory's pin. In focus mode, - -- above the pin (D-pad up again), or left of it. The A chip of either sits left of both. - f.report = UI.IconButton(f, 20) + -- "Problem with this step?" (UI/Report.lua), left of the directory's pin: a flag on a warm + -- fill that stands out in every theme (Yendi: players must feel invited to press it), its + -- tooltip in words (the color never says it alone). A lighter veil under the mouse. In focus + -- mode, above the pin (D-pad up again), or left of it. The A chip of either sits left of both. + f.report = UI.IconButton(f, 20, C.report) f.report:SetPoint("TOPRIGHT", f, "TOPRIGHT", -76, -2) + f.report.hover = UI.Shape(f.report, "row", C.hover, "BACKGROUND", 1):Place(f.report) + f.report.hover:Hide() local pole = f.report:CreateTexture(nil, "ARTWORK") - UI.Fill(pole, C.secondary) + UI.Fill(pole, C.onReport) pole:SetSize(1.5, 12) pole:SetPoint("CENTER", f.report, "CENTER", -3.5, 0) local flag = f.report:CreateTexture(nil, "ARTWORK") - UI.Fill(flag, C.secondary) + UI.Fill(flag, C.onReport) flag:SetSize(7, 5) flag:SetPoint("TOPLEFT", pole, "TOPRIGHT", 0, 0) - f.report.tooltip = L.BIND_REPORT + f.report.pole, f.report.flag = pole, flag + f.report.tooltip, f.report.tooltipLine = L.REPORT_BUTTON, L.REPORT_BUTTON_TIP + f.report:SetScript("OnEnter", function(self) + self.hover:Show() + P:ShowButtonTip(self) + end) + f.report:SetScript("OnLeave", function(self) + self.hover:Hide() + if GameTooltip:GetOwner() == self then GameTooltip:Hide() end + end) f.report:SetScript("OnClick", function() Ding.ReportWindow:Toggle() end) f.report.focus = UI.Focus(f.report, true) f.report.key = UI.Glyph(f) @@ -2198,9 +2211,17 @@ function P:ShowFinderTip() if not (self.focusing and self.focusIndex == index) and GameTooltip:GetOwner() == b then GameTooltip:Hide() end end local b = self.focusing and buttons[self.focusIndex] - if not b then return end + if b then self:ShowButtonTip(b) end +end + +-- A title bar button's name, and the line under it when it has one. +function P:ShowButtonTip(b) GameTooltip:SetOwner(b, "ANCHOR_TOP") GameTooltip:SetText(b.tooltip) + if b.tooltipLine then + local c = C.secondary + GameTooltip:AddLine(b.tooltipLine, c[1], c[2], c[3], true) + end GameTooltip:Show() end diff --git a/Ding/UI/Themes.lua b/Ding/UI/Themes.lua index 2bbf893..3c51be3 100644 --- a/Ding/UI/Themes.lua +++ b/Ding/UI/Themes.lua @@ -65,6 +65,9 @@ local DD = { todoBorder = { 0.898, 0.404, 0.420, 0.5 }, -- A quest above the player's level (quest.above): shown, never held back. above = hex("#FF9A3C"), + -- "Report a problem" (the guide window's button): a warm fill that stands out in every theme, + -- the flag on it in a dark ink; its tooltip says it in words. + report = hex("#FF7043"), onReport = hex("#140A06"), -- A step the player left by hand: marked done (violet) or skipped (amber). manual = { 0.655, 0.545, 0.980, 1 }, manualIconBg = { 0.655, 0.545, 0.980, 0.2 }, @@ -123,6 +126,7 @@ local HALO = { done = hex("#5FD068"), doneIconBg = NONE, doneRowBg = NONE, todo = hex("#FFD100"), todoIconBg = NONE, todoRowBg = NONE, todoBorder = hex("#FFD10080"), above = hex("#FF9A3C"), + report = hex("#FF6A3D"), onReport = hex("#17130E"), -- Not in the handoff (later visual pass): violet and teal, apart from gold and green. manual = hex("#B49CFF"), manualIconBg = NONE, manualRowBg = NONE, skipped = hex("#5CC8D6"), skippedIconBg = NONE, skippedRowBg = NONE, @@ -160,6 +164,7 @@ local ARDOISE = { -- Amber, not red. todo = hex("#FFB040"), todoIconBg = hex("#FFB0403D"), todoRowBg = hex("#FFB04033"), todoBorder = hex("#FFB04080"), above = hex("#FF9A3C"), + report = hex("#FF6A3D"), onReport = hex("#17130E"), manual = hex("#B49CFF"), manualIconBg = hex("#B49CFF38"), manualRowBg = hex("#B49CFF21"), skipped = hex("#5CC8D6"), skippedIconBg = hex("#5CC8D638"), skippedRowBg = hex("#5CC8D621"), impossibleIconBg = hex("#FFFFFF0D"), otherIconBg = hex("#FFFFFF12"), previewIconBg = hex("#FFFFFF0D"), @@ -201,6 +206,7 @@ local BANDES = { todo = hex("#F5F7FA"), todoIconBg = NONE, todoRowBg = grad(hex("#12283EEB"), hex("#12283E8C")), todoBorder = hex("#F5F7FA80"), above = hex("#FF9A3C"), + report = hex("#FF7A50"), onReport = hex("#0A1118"), manual = hex("#B49CFF"), manualIconBg = NONE, manualRowBg = grad(hex("#2C2148D1"), hex("#0406095C")), skipped = hex("#E8C547"), skippedIconBg = NONE, skippedRowBg = grad(hex("#3A3010D1"), hex("#0406095C")), impossibleIconBg = NONE, otherIconBg = NONE, previewIconBg = NONE, diff --git a/README.md b/README.md index 8f7f389..9ee0c5c 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,8 @@ instead from the *Arrow* tab of the settings (off by default). learned, the guide window says so, why, and which weapon master teaches it; right-click it (Left in focus mode) never to see that weapon suggested again. - **Report a problem.** An NPC missing, the arrow pointing to the wrong place, a quest not - offered? The flag in the guide window's title bar (beside the NPC directory's pin), its key - binding or `/ding bug` writes a precise report for you: the step, where you stand, your + offered? The orange flag in the guide window's title bar ("Problem with this step?", beside the + NPC directory's pin), its key binding or `/ding bug` writes a precise report for you: the step, where you stand, your quests and their counters, what the arrow points at, your route settings, never your character's name. See [Report a problem](#report-a-problem). - **Minimap button.** Left-click for the settings, right-click to show or hide the guide. @@ -163,8 +163,9 @@ where you are) · `/ding scale <0.5-1.5>` · `/ding arrow` · `/ding arrowflip` Something wrong with a step? Ding writes the report for you, in game: -1. Open **Report a problem**: the flag in the guide window's title bar (beside the NPC - directory's pin; with a gamepad, up from the first line in focus mode), its key binding +1. Open **Report a problem with the guide**: the orange flag in the guide window's title bar + ("Problem with this step?", beside the NPC directory's pin; with a gamepad, up from the first + line in focus mode), its key binding (*Keybinds* tab: a button or two, like the others) or `/ding bug`. 2. The window shows what the report will contain: the guide, the step ("Step 19 / 161") and its place in the guide's file, where you stand, your character, the step's quests and their diff --git a/docs/SPEC.md b/docs/SPEC.md index 616c913..3c032e5 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -1311,9 +1311,15 @@ textures and font flags, never a size nor a position. - `/ding cancel` ("Trip cancelled: the arrow follows the guide again.", or "No trip in progress."). A weapon master's trainer window is not taken for the class trainer's (`Training:Record`). -- **Report a problem** (binding "Report a problem", `DING_REPORT`, a chord like the others; the - guide window's flag, right beside the directory's pin; `/ding bug`; `Core/Report.lua` writes, - `UI/Report.lua` shows): players send precise reports the authors read on GitHub issues. +- **Report a problem with the guide** (binding of that name, `DING_REPORT`, a chord like the + others; the guide window's flag, right beside the directory's pin; `/ding bug`; + `Core/Report.lua` writes, `UI/Report.lua` shows): players send precise reports the authors + read on GitHub issues. The flag must invite a press (Yendi): a warm fill of its own in every + theme (token `report`, the flag in `onReport`, both at least 4.5:1, unlike the accent), its + tooltip in words, "Problem with this step?" and a line under it ("Tell the people who make the + guide: Ding! writes the report for you."); never the word "bug" in what the player reads (the + command apart). Icon only: a word on it would cut the guide's title at the window's usual + widths. - What it holds, read when the window opens: the guide pack, the section (title and id), the step on screen (the held one while it shows) as the window numbers it ("Step 19 / 161") and its index in the guide's file, its summary (`Engine:Summary`) and its keys on one line @@ -1329,7 +1335,7 @@ textures and font flags, never a size nor a position. deathSkip, skipGrey, serviceSteps, trainerSteps, flightSteps), the theme, TomTom on or off (and installed); Ding's version, the client's locale, its build (`GetBuildInfo`), the date and time in UTC. **Never** the character's name, the realm nor anything of the account. - - First page ("Ding! · Report a problem", a dialog like the directory): one sentence on what + - First page ("Ding! · Report a problem with the guide", a dialog like the directory): one sentence on what it is for and that nothing is sent by itself; "What the report will contain", one plain sentence per part (the step, where you are, your character, the quests with their counters and "and 2 more", the arrow, the settings, the versions, never your name); "What went diff --git a/docs/UI-handoff.md b/docs/UI-handoff.md index 9455889..d221bdf 100644 --- a/docs/UI-handoff.md +++ b/docs/UI-handoff.md @@ -548,11 +548,14 @@ Objective verbs: `Accept` · `Turn in` · `Kill` · `Collect` · `Use … on … first row: the outer focus ring around it and the A chip on its left (the title shortens). - **The guide window's "Report a problem" button** (not in the mockup): a 20 × 20 icon button right beside the directory's pin, on its left (4 between them; the title and its drag area - stop 4 before it): a flag, a 1.5 × 12 `text.secondary` pole with a 7 × 5 `text.secondary` - cloth at its top right; tooltip "Report a problem". In focus mode, up from the pin (or Left - from it): its outer focus ring; the A chip of the pin or the flag sits 6 left of the flag. + stop 4 before it), always filled with the warm `report` token (#FF6A3D in Halo and Ardoise, + #FF7A50 in Bandes, #FF7043 in Ding dark: it must invite a press), a `hover` veil over it under + the mouse: a flag, a 1.5 × 12 `onReport` pole with a 7 × 5 `onReport` cloth at its top right; + tooltip "Problem with this step?" and, in `text.secondary` under it, "Tell the people who make + the guide: Ding! writes the report for you." In focus mode, up from the pin (or Left from it): + its outer focus ring; the A chip of the pin or the flag sits 6 left of the flag. - **Report a problem window** (not in the mockup): a dialog of Ding's, 520 wide, the shared - sizes (UI.SIZE), the title bar "Ding! · Report a problem" with the close cross and B beside it + sizes (UI.SIZE), the title bar "Ding! · Report a problem with the guide" with the close cross and B beside it (gamepad). First page, 12 from the edges: the intro (12 Regular `text`, wrapped), the block titles "What the report will contain", "What went wrong?", "In your words (optional)" (11 SemiBold `text.muted`), the parts (11 Regular `text.secondary`, wrapped, 8 further in), the diff --git a/docs/UI-themes.md b/docs/UI-themes.md index 9b5df7d..6dec74a 100644 --- a/docs/UI-themes.md +++ b/docs/UI-themes.md @@ -162,6 +162,8 @@ Keys are the previous handoff's, plus `quest`, `quest.above`, `link`, `card.bg`, | `on.accent` | `#17130E` | 0.090, 0.075, 0.055 | check on checked box | | `quest` | `#FFD100` | 1, 0.820, 0 | quest names in headers | | `quest.above` | `#FF9A3C` | 1, 0.604, 0.235 | quest above the player's level | +| `report` | `#FF6A3D` | 1, 0.416, 0.239 | the guide window's "Problem with this step?" flag: its fill | +| `on.report` | `#17130E` | 0.090, 0.075, 0.055 | the flag on that fill | | `link` | `#8CCBFF` | 0.549, 0.796, 1 | item / NPC / target names | | `done` | `#5FD068` | 0.373, 0.816, 0.408 | check glyph, counter, arrived | | `todo` | `#FFD100` | = accent | dot / diamond glyph, counter | @@ -199,6 +201,8 @@ Arrow: `arrow_halo_gold.tga` (real colors, **not tinted**), behind `arrow_halo_b | `on.accent` | `#17130E` | 0.090, 0.075, 0.055 | | | `quest` | `#F5C842` | = accent | | | `quest.above` | `#FF9A3C` | 1, 0.604, 0.235 | | +| `report` | `#FF6A3D` | 1, 0.416, 0.239 | | +| `on.report` | `#17130E` | 0.090, 0.075, 0.055 | | | `link` | `#8CCBFF` | 0.549, 0.796, 1 | | | `done` | `#6FD36F` | 0.435, 0.827, 0.435 | | | `done.row.bg` | `#6FD36F21` | 0.435, 0.827, 0.435, 0.13 | | @@ -242,6 +246,8 @@ Arrow: `arrow_slate.tga` tinted `#F2EDE4`, behind tinted `todo`; arrived `ring.t | `on.accent` | `#0A1118` | 0.039, 0.067, 0.094 | | | `quest` | `#FFD100` | 1, 0.820, 0 | the game's quest gold | | `quest.above` | `#FF9A3C` | | | +| `report` | `#FF7A50` | 1, 0.478, 0.314 | | +| `on.report` | `#0A1118` | 0.039, 0.067, 0.094 | | | `link` | `#63C2FF` | = accent | | | `done` | `#5ED38A` | 0.369, 0.827, 0.541 | | | `todo` | `#F5F7FA` | = text.primary | to-do glyph and counter | @@ -253,7 +259,7 @@ Flags: `windowStyle="bands"`, `titleFont="noto"`, `fontOutline="OUTLINE"`, `text Arrow: `arrow_band.tga` tinted `accent`, behind tinted `arrow.back`; arrived `ring_outlined.tga` + `icon_check_large_outlined.tga` tinted `done` (the black outline stays black when tinted). ### `dingdark` -The token table of `docs/UI-handoff.md`, unchanged, with flags `windowStyle="panel"`, `titleFont="noto"`, `stateRowBg=true`, everything else off, plus the new keys: `quest = accent`, `quest.above = #FF9A3C`, `link = accent`, `card.bg = bg.window` opaque, `card.text = #CCCCCC`, `notice.bg = #FFFFFF0A`, `arrow.sub = text.glyph`, `arrow.back = todo`, `chip.bg = surface.glyph`, `chip.text = text.glyph`, `line = divider`. +The token table of `docs/UI-handoff.md`, unchanged, with flags `windowStyle="panel"`, `titleFont="noto"`, `stateRowBg=true`, everything else off, plus the new keys: `quest = accent`, `quest.above = #FF9A3C`, `link = accent`, `card.bg = bg.window` opaque, `card.text = #CCCCCC`, `notice.bg = #FFFFFF0A`, `arrow.sub = text.glyph`, `arrow.back = todo`, `chip.bg = surface.glyph`, `chip.text = text.glyph`, `line = divider`, `report = #FF7043`, `on.report = #140A06`. --- diff --git a/tools/tests/report_test.lua b/tools/tests/report_test.lua index a51c79a..c6c0b6b 100644 --- a/tools/tests/report_test.lua +++ b/tools/tests/report_test.lua @@ -9,6 +9,7 @@ local T = dofile(tools .. "/testlib.lua")(arg[1], { locale = "enUS" }) local Layout = dofile(tools .. "/layout.lua") local Ding, H = T.Ding, T.H local E, P, L, R, W, G = Ding.Engine, Ding.Panel, Ding.L, Ding.Report, Ding.ReportWindow, Ding.Gamepad +local UI = Ding.UI Ding.RegisterGuide({ id = "t-report", title = "Report test", steps = { { accept = 456 }, @@ -87,10 +88,16 @@ T.test("the guide window: a flag right beside the directory's pin, its tooltip i -- The title stops before the flag (its drag area too). T.ok(Layout.rect(f.title).right <= flag.left, "the title runs under the flag") T.ok(Layout.rect(f.titleArea).right <= flag.left, "the title's drag area covers the flag") + -- Its tooltip says it in words: a problem with this step, not a technical "bug". f.report.scripts.OnEnter(f.report) T.ok(GameTooltip:IsShown() and GameTooltip:GetOwner() == f.report, "no tooltip") - T.eq(GameTooltip.text, L.BIND_REPORT) + T.eq(GameTooltip.text, L.REPORT_BUTTON) + T.eq(L.REPORT_BUTTON, "Problem with this step?") + T.ok(table.concat(GameTooltip.lines or {}, "\n"):find(L.REPORT_BUTTON_TIP, 1, true), "no line under the tooltip's title") + T.ok(f.report.hover:IsShown() and f.report.bg:IsShown() and f.report.bg.token == UI.C.report, + "the warm fill does not stay under the mouse") f.report.scripts.OnLeave(f.report) + T.ok(not f.report.hover:IsShown() and not GameTooltip:IsShown(), "the hover stays") click(f.report) T.ok(W:IsShown(), "the flag does not open the window") click(f.report) @@ -112,7 +119,7 @@ T.test("focus mode: up from the first row, the pin then the flag; left and right T.eq(P.focusIndex, -1, "the flag is not reached") T.ok(f.report.focus.ring:IsShown() and f.report.key:IsShown(), "the flag shows neither focus nor A") T.ok(not f.finder.focus.ring:IsShown() and not f.finder.key:IsShown(), "the pin keeps the focus") - T.ok(GameTooltip:IsShown() and GameTooltip:GetOwner() == f.report and GameTooltip.text == L.BIND_REPORT, + T.ok(GameTooltip:IsShown() and GameTooltip:GetOwner() == f.report and GameTooltip.text == L.REPORT_BUTTON, "the focused flag does not say what it is") -- The A chip sits left of both buttons, the title shortens for it. T.ok(Layout.rect(f.report.key).right <= Layout.rect(f.report).left, "the A chip covers the flag") @@ -131,6 +138,52 @@ T.test("focus mode: up from the first row, the pin then the flag; left and right W:Close(); H.flush() end) +-- WCAG contrast of two colors (0-1 values). +local function luminance(c) + local function channel(v) return v <= 0.03928 and v / 12.92 or ((v + 0.055) / 1.055) ^ 2.4 end + return 0.2126 * channel(c[1]) + 0.7152 * channel(c[2]) + 0.0722 * channel(c[3]) +end +local function contrast(a, b) + local x, y = luminance(a), luminance(b) + if x < y then x, y = y, x end + return (x + 0.05) / (y + 0.05) +end + +T.test("the flag stands out in every theme: a warm fill unlike the accent, a dark flag on it, the focus ring", function() + start() + local f = P.frame + for _, id in ipairs(UI.ThemeOrder) do + UI.Theme.Apply(id); H.flush() + local C = UI.C + T.ok(f.report.bg:IsShown() and f.report.bg.token == C.report, id .. ": no fill") + T.eq(UI.HexOf(f.report.bg.color), UI.HexOf(C.report), id .. ": the fill is not the theme's") + T.ok(C.report[1] >= 0.9 and C.report[1] > C.report[2] and C.report[2] > C.report[3], id .. ": not warm") + T.ok(math.abs(C.report[2] - C.accent[2]) > 0.2 or math.abs(C.report[3] - C.accent[3]) > 0.2, + id .. ": too close to the accent") + T.ok(contrast(C.report, C.onReport) >= 4.5, ("%s: the flag on its fill, %.1f:1"):format(id, contrast(C.report, C.onReport))) + T.ok(contrast(C.report, C.bg) >= 4.5, ("%s: the fill on the window, %.1f:1"):format(id, contrast(C.report, C.bg))) + T.ok(f.report.pole.dingPaint[2] == C.onReport and f.report.flag.dingPaint[2] == C.onReport, id .. ": the flag's ink") + P:StartFocus(); H.flush() + P.focusIndex = -1; P:Update() + T.ok(f.report.focus.ring:IsShown() and f.report.key:IsShown(), id .. ": no focus ring or A on the flag") + T.ok(f.report.bg:IsShown() and f.report.bg.token == C.report, id .. ": the fill goes with the focus") + P:StopFocus(); H.flush() + end + UI.Theme.Apply("halo"); H.flush() +end) + +T.test("in words, a problem with the guide: never \"bug\" in what the player reads (the command apart)", function() + for _, name in ipairs({ "enUS", "frFR", "deDE", "esES", "ptBR", "koKR", "zhCN", "zhTW" }) do + local source = assert(io.open(tools .. "/../Ding/Locales/" .. name .. ".lua")):read("*a") + for key, value in source:gmatch('\n (%u[%u_]*) = "([^\n]*)",') do + if key == "BIND_REPORT" or key:find("^REPORT_") then + T.ok(not value:lower():find("bug", 1, true), ("%s %s says bug: %s"):format(name, key, value)) + end + end + end + T.eq(L.BIND_REPORT, "Report a problem with the guide") +end) + T.test("a key binding, its chord, a row in the Keybinds tab, /ding bug and the /ding help", function() start() T.eq(_G.BINDING_NAME_DING_REPORT, L.BIND_REPORT) From d9d1f8441f486b702236feadeb6a4bbc950905be Mon Sep 17 00:00:00 2001 From: Moust4ki Date: Thu, 15 Oct 2026 16:26:42 +0200 Subject: [PATCH 4/7] Report's text page in the player's order; the GitHub address shown whole Yendi, in game: the address first ("1. Open this address on GitHub:"), selected first with its button on the left, then the text ("2. Copy this text and paste it into the report:"), then how to copy and where the reports are kept. The address was cut in its box ("...ding-repor"): 475 units in Noto Sans at 11 for a 488-unit box. It is now in 10 (432) and the window is 560 wide like the import window (a 528-unit box). Tests: the order, the buttons' order and chips, the address's width read from the shipped font's own metrics. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +- Ding/Locales/deDE.lua | 6 +- Ding/Locales/enUS.lua | 6 +- Ding/Locales/esES.lua | 6 +- Ding/Locales/frFR.lua | 6 +- Ding/Locales/koKR.lua | 6 +- Ding/Locales/ptBR.lua | 6 +- Ding/Locales/zhCN.lua | 6 +- Ding/Locales/zhTW.lua | 6 +- Ding/UI/Report.lua | 42 ++++++++------ README.md | 7 ++- docs/SPEC.md | 14 +++-- docs/UI-handoff.md | 13 +++-- tools/tests/dialog_layout_test.lua | 4 +- tools/tests/report_test.lua | 89 +++++++++++++++++++++++++++--- 15 files changed, 152 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de53270..6962776 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,9 @@ Nothing else to install: QuestieDB is no longer needed, and TomTom stays optiona that change your route, Ding's and the game's versions. Never your character's name, your realm or your account. - Pick what went wrong among four ready answers (handy with a gamepad) and add your own words if - you can type, then **Create the text**: it is selected for Ctrl+C (an addon cannot copy for - you), with the link of the GitHub page to paste it into. Ding keeps your last 20 reports in its + you can type, then **Create the text**: 1. the address of the GitHub page to open, 2. the text + to paste into it, each selected by its button for Ctrl+C (an addon cannot copy for you). Ding + keeps your last 20 reports in its saved variables (`WTF\Account\\SavedVariables\Ding.lua`), so a player without a keyboard can send that file instead; "Restore all defaults" keeps them. diff --git a/Ding/Locales/deDE.lua b/Ding/Locales/deDE.lua index a20f175..11290e3 100644 --- a/Ding/Locales/deDE.lua +++ b/Ding/Locales/deDE.lua @@ -542,9 +542,9 @@ Ding.Locales.deDE = { REPORT_COMMENT_HINT = "Was du gesehen hast, was du erwartet hast: ein paar Worte helfen sehr.", REPORT_CREATE = "Text erstellen", REPORT_MOUSE = "Klick: wählen · Esc: schließen", - REPORT_COPY = "Der Text ist markiert: Strg+C zum Kopieren.", - REPORT_CLIPBOARD = "Ein Addon kann keinen Text in die Zwischenablage legen: Ding! markiert ihn, du kopierst ihn.", - REPORT_PASTE = "Füge ihn dann auf GitHub in einen neuen Bericht ein, unter dieser Adresse:", + REPORT_CLIPBOARD = "Jeder Knopf markiert sein Feld, dann kopiert Strg+C es: Ein Addon kann keinen Text in die Zwischenablage legen.", + REPORT_STEP_LINK = "1. Öffne diese Adresse auf GitHub:", + REPORT_STEP_TEXT = "2. Kopiere diesen Text und füge ihn in den Bericht ein:", REPORT_SELECT_TEXT = "Text markieren", REPORT_SELECT_LINK = "Link markieren", REPORT_SAVED = "Keine Tastatur zur Hand? Ding! behält deine letzten %d Berichte in %s, gespeichert beim Ausloggen oder Neuladen: Du kannst diese Datei schicken.", diff --git a/Ding/Locales/enUS.lua b/Ding/Locales/enUS.lua index efdca6d..19f0ef2 100644 --- a/Ding/Locales/enUS.lua +++ b/Ding/Locales/enUS.lua @@ -545,9 +545,9 @@ Ding.Locales.enUS = { REPORT_COMMENT_HINT = "What you saw, what you expected: a few words help a lot.", REPORT_CREATE = "Create the text", REPORT_MOUSE = "Click to choose · Esc: close", - REPORT_COPY = "The text is selected: Ctrl+C to copy.", - REPORT_CLIPBOARD = "An addon cannot put text on the clipboard: Ding! selects it, you copy it.", - REPORT_PASTE = "Then paste it into a new report on GitHub, at this address:", + REPORT_CLIPBOARD = "Each button selects its box, then Ctrl+C copies it: an addon cannot put text on the clipboard.", + REPORT_STEP_LINK = "1. Open this address on GitHub:", + REPORT_STEP_TEXT = "2. Copy this text and paste it into the report:", REPORT_SELECT_TEXT = "Select the text", REPORT_SELECT_LINK = "Select the link", REPORT_SAVED = "No keyboard at hand? Ding! keeps your last %d reports in %s, saved when you log out or reload: you can send that file.", diff --git a/Ding/Locales/esES.lua b/Ding/Locales/esES.lua index 2fd5507..c844956 100644 --- a/Ding/Locales/esES.lua +++ b/Ding/Locales/esES.lua @@ -545,9 +545,9 @@ Ding.Locales.esES = { REPORT_COMMENT_HINT = "Lo que viste, lo que esperabas: unas palabras ayudan mucho.", REPORT_CREATE = "Crear el texto", REPORT_MOUSE = "Clic: elegir · Esc: cerrar", - REPORT_COPY = "El texto está seleccionado: Ctrl+C para copiarlo.", - REPORT_CLIPBOARD = "Un addon no puede poner texto en el portapapeles: Ding! lo selecciona y tú lo copias.", - REPORT_PASTE = "Después pégalo en un informe nuevo en GitHub, en esta dirección:", + REPORT_CLIPBOARD = "Cada botón selecciona su cuadro y Ctrl+C lo copia: un addon no puede poner texto en el portapapeles.", + REPORT_STEP_LINK = "1. Abre esta dirección en GitHub:", + REPORT_STEP_TEXT = "2. Copia este texto y pégalo en el informe:", REPORT_SELECT_TEXT = "Seleccionar el texto", REPORT_SELECT_LINK = "Seleccionar el enlace", REPORT_SAVED = "¿Sin teclado a mano? Ding! guarda tus últimos %d informes en %s, escrito al cerrar sesión o recargar: puedes enviar ese archivo.", diff --git a/Ding/Locales/frFR.lua b/Ding/Locales/frFR.lua index cbdaaf5..8571986 100644 --- a/Ding/Locales/frFR.lua +++ b/Ding/Locales/frFR.lua @@ -540,9 +540,9 @@ Ding.Locales.frFR = { REPORT_COMMENT_HINT = "Ce que tu as vu, ce que tu attendais : quelques mots aident beaucoup.", REPORT_CREATE = "Créer le texte", REPORT_MOUSE = "Clic : choisir · Échap : fermer", - REPORT_COPY = "Le texte est sélectionné : Ctrl+C pour le copier.", - REPORT_CLIPBOARD = "Un addon ne peut pas mettre de texte dans le presse-papiers : Ding! le sélectionne, tu le copies.", - REPORT_PASTE = "Colle-le ensuite dans un nouveau rapport sur GitHub, à cette adresse :", + REPORT_CLIPBOARD = "Chaque bouton sélectionne sa zone, puis Ctrl+C la copie : un addon ne peut pas mettre de texte dans le presse-papiers.", + REPORT_STEP_LINK = "1. Ouvre cette adresse sur GitHub :", + REPORT_STEP_TEXT = "2. Copie ce texte et colle-le dans le rapport :", REPORT_SELECT_TEXT = "Sélectionner le texte", REPORT_SELECT_LINK = "Sélectionner le lien", REPORT_SAVED = "Pas de clavier sous la main ? Ding! garde tes %d derniers rapports dans %s, enregistré quand tu te déconnectes ou recharges l'interface : tu peux envoyer ce fichier.", diff --git a/Ding/Locales/koKR.lua b/Ding/Locales/koKR.lua index 30d8545..9d43f48 100644 --- a/Ding/Locales/koKR.lua +++ b/Ding/Locales/koKR.lua @@ -543,9 +543,9 @@ Ding.Locales.koKR = { REPORT_COMMENT_HINT = "본 것과 기대한 것: 몇 마디만 있어도 큰 도움이 됩니다.", REPORT_CREATE = "텍스트 만들기", REPORT_MOUSE = "클릭: 선택 · Esc: 닫기", - REPORT_COPY = "텍스트가 선택되었습니다: Ctrl+C로 복사하세요.", - REPORT_CLIPBOARD = "애드온은 클립보드에 텍스트를 넣을 수 없습니다. Ding!이 선택해 두면 직접 복사하세요.", - REPORT_PASTE = "그런 다음 이 주소의 GitHub 새 신고에 붙여 넣으세요:", + REPORT_CLIPBOARD = "버튼으로 상자를 선택한 뒤 Ctrl+C로 복사하세요. 애드온은 클립보드에 텍스트를 넣을 수 없습니다.", + REPORT_STEP_LINK = "1. GitHub에서 이 주소를 여세요:", + REPORT_STEP_TEXT = "2. 이 텍스트를 복사해 신고에 붙여 넣으세요:", REPORT_SELECT_TEXT = "텍스트 선택", REPORT_SELECT_LINK = "링크 선택", REPORT_SAVED = "키보드가 없나요? Ding!은 마지막 신고 %d개를 %s에 보관합니다. 로그아웃하거나 UI를 다시 불러올 때 저장되며, 이 파일을 보내면 됩니다.", diff --git a/Ding/Locales/ptBR.lua b/Ding/Locales/ptBR.lua index 3b96d04..ba13782 100644 --- a/Ding/Locales/ptBR.lua +++ b/Ding/Locales/ptBR.lua @@ -543,9 +543,9 @@ Ding.Locales.ptBR = { REPORT_COMMENT_HINT = "O que você viu, o que esperava: algumas palavras ajudam muito.", REPORT_CREATE = "Criar o texto", REPORT_MOUSE = "Clique: escolher · Esc: fechar", - REPORT_COPY = "O texto está selecionado: Ctrl+C para copiar.", - REPORT_CLIPBOARD = "Um addon não pode colocar texto na área de transferência: o Ding! seleciona, você copia.", - REPORT_PASTE = "Depois cole em um novo relatório no GitHub, neste endereço:", + REPORT_CLIPBOARD = "Cada botão seleciona sua caixa e Ctrl+C a copia: um addon não pode colocar texto na área de transferência.", + REPORT_STEP_LINK = "1. Abra este endereço no GitHub:", + REPORT_STEP_TEXT = "2. Copie este texto e cole no relatório:", REPORT_SELECT_TEXT = "Selecionar o texto", REPORT_SELECT_LINK = "Selecionar o link", REPORT_SAVED = "Sem teclado à mão? O Ding! guarda seus últimos %d relatórios em %s, gravado ao sair ou recarregar: você pode enviar esse arquivo.", diff --git a/Ding/Locales/zhCN.lua b/Ding/Locales/zhCN.lua index b186d0f..511a4dc 100644 --- a/Ding/Locales/zhCN.lua +++ b/Ding/Locales/zhCN.lua @@ -543,9 +543,9 @@ Ding.Locales.zhCN = { REPORT_COMMENT_HINT = "你看到了什么,你原本期待什么:几句话就很有帮助。", REPORT_CREATE = "生成文本", REPORT_MOUSE = "点击:选择 · Esc:关闭", - REPORT_COPY = "文本已选中:按 Ctrl+C 复制。", - REPORT_CLIPBOARD = "插件无法把文本放进剪贴板:Ding! 会选中文本,由你来复制。", - REPORT_PASTE = "然后粘贴到 GitHub 上的新报告中,地址如下:", + REPORT_CLIPBOARD = "每个按钮会选中对应的框,再按 Ctrl+C 复制:插件无法把文本放进剪贴板。", + REPORT_STEP_LINK = "1. 在 GitHub 上打开这个地址:", + REPORT_STEP_TEXT = "2. 复制这段文本,粘贴到报告中:", REPORT_SELECT_TEXT = "选中文本", REPORT_SELECT_LINK = "选中链接", REPORT_SAVED = "手边没有键盘?Ding! 会把你最近的 %d 份报告保存在 %s,在你登出或重载界面时写入:你可以发送这个文件。", diff --git a/Ding/Locales/zhTW.lua b/Ding/Locales/zhTW.lua index aaf9b00..c986b22 100644 --- a/Ding/Locales/zhTW.lua +++ b/Ding/Locales/zhTW.lua @@ -543,9 +543,9 @@ Ding.Locales.zhTW = { REPORT_COMMENT_HINT = "你看到了什麼,你原本期待什麼:幾句話就很有幫助。", REPORT_CREATE = "產生文字", REPORT_MOUSE = "點擊:選擇 · Esc:關閉", - REPORT_COPY = "文字已選取:按 Ctrl+C 複製。", - REPORT_CLIPBOARD = "插件無法把文字放進剪貼簿:Ding! 會選取文字,由你來複製。", - REPORT_PASTE = "然後貼到 GitHub 上的新回報中,網址如下:", + REPORT_CLIPBOARD = "每個按鈕會選取對應的框,再按 Ctrl+C 複製:插件無法把文字放進剪貼簿。", + REPORT_STEP_LINK = "1. 在 GitHub 上開啟這個網址:", + REPORT_STEP_TEXT = "2. 複製這段文字,貼到回報中:", REPORT_SELECT_TEXT = "選取文字", REPORT_SELECT_LINK = "選取連結", REPORT_SAVED = "手邊沒有鍵盤?Ding! 會把你最近的 %d 份回報保存在 %s,在你登出或重新載入介面時寫入:你可以傳送這個檔案。", diff --git a/Ding/UI/Report.lua b/Ding/UI/Report.lua index 40049f7..525a56c 100644 --- a/Ding/UI/Report.lua +++ b/Ding/UI/Report.lua @@ -3,9 +3,10 @@ -- contain, in plain words (the facts are read when the window opens: Core/Report.lua), what -- went wrong (four ready reasons: a gamepad types little) and the player's words (optional; -- the box takes the keyboard only once no other box has it: the game's chat above all). Then --- "Create the text": the report in a read-only box, selected for Ctrl+C (an addon cannot write --- the clipboard: the window says so), the link of the GitHub form to paste it into, and where --- Ding keeps the last reports for a player without a keyboard (DingDB.reports). +-- "Create the text": in the order the player goes (Yendi, in game), 1. the address of the +-- GitHub form, whole on its line and selected first, 2. the report in a read-only box; each +-- button selects its box for Ctrl+C (an addon cannot write the clipboard: the window says so); +-- then where Ding keeps the last reports for a player without a keyboard (DingDB.reports). -- Gamepad (temporary bindings, out of combat only): the D-pad up / down between the reasons, -- the box and the buttons, left / right along the buttons, A picks or presses, Y selects the -- text, B closes. Each action shows once: A on the focused row or button, Y on "Select the @@ -20,7 +21,8 @@ local W = {} Ding.ReportWindow = W local SIZE = UI.SIZE -local WIDTH, TITLE, PAD, INSET, BUTTON = 520, SIZE.title, 12, SIZE.pad, SIZE.button +-- 560 wide, as the import window: the GitHub address fits its box whole (LINK_FONT). +local WIDTH, TITLE, PAD, INSET, BUTTON = 560, SIZE.title, 12, SIZE.pad, SIZE.button local EDGE, LINE = 8, 30 -- the footer's margins; a hint line above the buttons local FOOTER = EDGE + BUTTON + EDGE local HEADER, GAP, ROW_GAP = 20, 4, SIZE.gap @@ -28,6 +30,9 @@ local ROW_PAD, ROW_MIN = 6, 28 -- a reason's margin above and local ROW_WIDTH = WIDTH - 2 * INSET local MARK, CHIP = 8 + 16 + 10, 24 -- the radio mark before a reason; room for the A chip local COMMENT_HEIGHT, TEXT_HEIGHT, LINK_HEIGHT = 52, 196, 26 +-- The address in 10: 432 units in Noto Sans (475 in 11, which the game cut in a 520 window). +local LINK_FONT = 10 +W.WIDTH, W.LINK_FONT = WIDTH, LINK_FONT local QUESTS_SHOWN = 3 local KEYS = { "PADDUP", "PADDDOWN", "PADDLEFT", "PADDRIGHT", "PAD1", "PAD2", "PAD4", "PADFORWARD" } @@ -127,7 +132,7 @@ local function textBox(parent, height, opts) end edit:SetAutoFocus(false) edit:SetMaxLetters(opts.readOnly and 0 or 500) - edit:SetFontObject(UI.Font("regular", 11)) + edit:SetFontObject(UI.Font("regular", opts.size or 11)) UI.Color(edit, opts.readOnly and C.secondary or C.text) if opts.placeholder then box.placeholder = UI.Text(box, "regular", 11, C.muted) @@ -281,11 +286,11 @@ function W:Create() done:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -(TITLE + 1)) done:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -(TITLE + 1)) f.done = done - f.copy = paragraph(done, "medium", 12, C.text) - f.clipboard = paragraph(done, "regular", 11, C.secondary) + f.linkStep = paragraph(done, "medium", 12, C.text) + f.link = textBox(done, LINK_HEIGHT, { readOnly = true, single = true, size = LINK_FONT }) + f.textStep = paragraph(done, "medium", 12, C.text) f.text = textBox(done, TEXT_HEIGHT, { readOnly = true }) - f.paste = paragraph(done, "regular", 12, C.text) - f.link = textBox(done, LINK_HEIGHT, { readOnly = true, single = true }) + f.clipboard = paragraph(done, "regular", 11, C.secondary) f.saved = paragraph(done, "regular", 11, C.secondary) f.doneStatus = paragraph(done, "regular", 11, C.secondary) @@ -437,11 +442,11 @@ function W:LayoutDone() local f = self.frame local page = f.done local y = PAD - y = place(f.copy, page, y, L.REPORT_COPY) + 2 - y = place(f.clipboard, page, y, L.REPORT_CLIPBOARD) + 8 - y = placeBox(f.text, page, y) + 10 - y = place(f.paste, page, y, L.REPORT_PASTE) + 6 - y = placeBox(f.link, page, y) + 10 + y = place(f.linkStep, page, y, L.REPORT_STEP_LINK) + 6 + y = placeBox(f.link, page, y) + 12 + y = place(f.textStep, page, y, L.REPORT_STEP_TEXT) + 6 + y = placeBox(f.text, page, y) + 8 + y = place(f.clipboard, page, y, L.REPORT_CLIPBOARD) + 6 y = place(f.saved, page, y, L.REPORT_SAVED:format(R().KEEP, R().FILE)) return status(f.doneStatus, page, y) + PAD end @@ -599,7 +604,8 @@ function W:Activate() end end --- "Create the text": the report written and kept, the text selected for Ctrl+C. +-- "Create the text": the report written and kept; the address selected for Ctrl+C first (the +-- page's first step), its button focused, then the text's. function W:Make() local f = self.frame local comment = f.comment:GetValue() @@ -612,12 +618,12 @@ function W:Make() f.text:SetValue(self.made) f.link:SetValue(R().ISSUE_URL) self.actions = { - { label = L.REPORT_SELECT_TEXT, primary = true, key = "PAD4", run = function() W.frame.text:SelectAll() end }, - { label = L.REPORT_SELECT_LINK, run = function() W.frame.link:SelectAll() end }, + { label = L.REPORT_SELECT_LINK, primary = true, run = function() W.frame.link:SelectAll() end }, + { label = L.REPORT_SELECT_TEXT, key = "PAD4", run = function() W.frame.text:SelectAll() end }, } self.focus = 1 self:Layout() - f.text:SelectAll() + f.link:SelectAll() end --------------------------------------------------------------------------- diff --git a/README.md b/README.md index 9ee0c5c..08c4a90 100644 --- a/README.md +++ b/README.md @@ -173,9 +173,10 @@ Something wrong with a step? Ding writes the report for you, in game: game's versions. Never your character's name, your realm or your account. 3. Pick what went wrong ("The NPC is not there", "The arrow points to the wrong place", "The quest is not offered", "Something else") and, if you can type, add a few words. -4. **Create the text**: it is selected, copy it with Ctrl+C (an addon cannot copy for you), then - paste it into a [new report on GitHub](https://github.com/moust4ki/DingGuideWoWForever/issues/new?template=ding-report.yml) - (the window shows that link too). Its first line can be the title. +4. **Create the text**: the window shows, in order, the address of a + [new report on GitHub](https://github.com/moust4ki/DingGuideWoWForever/issues/new?template=ding-report.yml) + to open, then the text to paste into it. Each has a button that selects it; copy with + Ctrl+C (an addon cannot copy for you). The text's first line can be the title. No keyboard at hand? Ding keeps your last 20 reports in `WTF\Account\\SavedVariables\Ding.lua` (written when you log out or reload): send that diff --git a/docs/SPEC.md b/docs/SPEC.md index 3c032e5..3772db5 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -1346,12 +1346,14 @@ textures and font flags, never a size nor a position. on to "Create the text" (a gamepad types little); the box is a stop too, A gives it the keyboard. Without one, the box takes the keyboard at once. A box never takes the keyboard from a box that is not Ding's (the chat's, after `/ding bug`): it waits, and a line says so. - - "Create the text": the report in a read-only box, selected, "The text is selected: Ctrl+C - to copy.", a line saying an addon cannot put text on the clipboard, the link of the GitHub - form (`.github/ISSUE_TEMPLATE/ding-report.yml`) in a one-line read-only box, and where the - reports are kept for a player without a keyboard (`WTF\Account\\SavedVariables\ - Ding.lua`, written when the player logs out or reloads). Buttons "Select the text" (Y) and - "Select the link". + - "Create the text", in the order the player goes (Yendi, in game): "1. Open this address on + GitHub:" and the GitHub form's address (`.github/ISSUE_TEMPLATE/ding-report.yml`) whole in a + one-line read-only box (10, the window 560 wide: in 11 the game cut it), selected first; + "2. Copy this text and paste it into the report:" and the report in a read-only box; a line + saying each button selects its box, then Ctrl+C copies it (an addon cannot put text on the + clipboard); where the reports are kept for a player without a keyboard + (`WTF\Account\\SavedVariables\Ding.lua`, written when the player logs out or + reloads). Buttons in the same order: "Select the link" (focused), "Select the text" (Y). - The text: GitHub Markdown, English labels, the player's words as typed (a quote). Its first line is a title ("Ding! report: Step 19/161 of
: ", the player's first words cut after a word near 60 characters when the reason is "Something else"), then "What went diff --git a/docs/UI-handoff.md b/docs/UI-handoff.md index d221bdf..ff3c2bf 100644 --- a/docs/UI-handoff.md +++ b/docs/UI-handoff.md @@ -554,7 +554,7 @@ Objective verbs: `Accept` · `Turn in` · `Kill` · `Collect` · `Use … on … tooltip "Problem with this step?" and, in `text.secondary` under it, "Tell the people who make the guide: Ding! writes the report for you." In focus mode, up from the pin (or Left from it): its outer focus ring; the A chip of the pin or the flag sits 6 left of the flag. -- **Report a problem window** (not in the mockup): a dialog of Ding's, 520 wide, the shared +- **Report a problem window** (not in the mockup): a dialog of Ding's, 560 wide, the shared sizes (UI.SIZE), the title bar "Ding! · Report a problem with the guide" with the close cross and B beside it (gamepad). First page, 12 from the edges: the intro (12 Regular `text`, wrapped), the block titles "What the report will contain", "What went wrong?", "In your words (optional)" (11 @@ -563,12 +563,13 @@ Objective verbs: `Accept` · `Turn in` · `Kill` · `Collect` · `Use … on … `accent` dot and an `accent` ring when chosen, 12 Regular label, inset focus, A on the right), the box (52 high, `bg.control`, outer focus ring when focused, its A chip at the right of its title, a `text.muted` line in it while empty), then a status line when the window waits for the - keyboard. Second page: "The text is selected: Ctrl+C to copy." (12 Medium), the clipboard line - (11 `text.secondary`), the read-only text box (196 high, 11 Regular `text.secondary`), "Then - paste it…" (12 Regular), the link's one-line box (26), where the reports are kept (11 + keyboard. Second page, in the player's order: "1. Open this address on GitHub:" (12 Medium), + the link's one-line box (26 high, the address in 10 Regular: whole, with room), 12, "2. Copy + this text and paste it into the report:" (12 Medium), the read-only text box (196 high, 11 + Regular `text.secondary`), the clipboard line and where the reports are kept (11 `text.secondary`). Footer 40 under a divider: buttons from the right (24 high, "Create the - text" or "Select the text" in the accent), the mouse help on the left without a gamepad. The - window grows with its texts: none is ever cut. + text" or, first, "Select the link" in the accent, then "Select the text"), the mouse help on + the left without a gamepad. The window grows with its texts: none is ever cut. - **Information apart from the step** (Yendi's first rule for the window): after the step's own rows, a 1-unit `divider` line across the list, then a section title "Good to know" (11 SemiBold `text.muted`, no icon, padding 8 above, none below; "Next steps" has the same line and title), diff --git a/tools/tests/dialog_layout_test.lua b/tools/tests/dialog_layout_test.lua index 0327dd2..368a48e 100644 --- a/tools/tests/dialog_layout_test.lua +++ b/tools/tests/dialog_layout_test.lua @@ -252,8 +252,8 @@ local function checkReport(where, problems) Layout.check(r, { ring = r.ring, label = r.label, A = r.key })) end else - parts.copy, parts.clipboard, parts.text = f.copy, f.clipboard, f.text - parts.paste, parts.link, parts.saved = f.paste, f.link, f.saved + parts.linkStep, parts.link, parts.textStep = f.linkStep, f.link, f.textStep + parts.text, parts.clipboard, parts.saved = f.text, f.clipboard, f.saved end report(problems, where, ("page %d, focus on %d"):format(page, i), Layout.check(f, parts)) end diff --git a/tools/tests/report_test.lua b/tools/tests/report_test.lua index c6c0b6b..5fd7be9 100644 --- a/tools/tests/report_test.lua +++ b/tools/tests/report_test.lua @@ -299,13 +299,22 @@ T.test("the text: title line, step, position, quests with counters, arrow, setti local arrow = text:match("%- Points to: ([^\n]*)") T.ok(arrow and arrow:find("UiMap 1438", 1, true) and arrow:find("yards away", 1, true), "the arrow's target: " .. tostring(arrow)) - -- Selected for Ctrl+C, said in words; the link to paste it into, selectable too. - T.ok(f.text.edit:HasFocus() and f.text.edit.selection, "the text is not selected") - T.eq(f.copy:GetText(), L.REPORT_COPY) - T.ok(L.REPORT_COPY:find("Ctrl+C", 1, true)) - T.eq(f.clipboard:GetText(), L.REPORT_CLIPBOARD) + -- In the player's order (Yendi): 1. the address, selected first; 2. the text; then how to + -- copy (said in words) and where the reports are kept. The buttons in the same order. + local function top(region) return Layout.rect(region).top end + T.eq(f.linkStep:GetText(), L.REPORT_STEP_LINK) + T.eq(f.textStep:GetText(), L.REPORT_STEP_TEXT) + T.ok(top(f.linkStep) > top(f.link) and top(f.link) > top(f.textStep) and top(f.textStep) > top(f.text) + and top(f.text) > top(f.clipboard) and top(f.clipboard) > top(f.saved), "not in the order: link, text, the rest") T.eq(f.link:GetValue(), "https://github.com/moust4ki/DingGuideWoWForever/issues/new?template=ding-report.yml") + T.ok(f.link.edit:HasFocus() and f.link.edit.selection, "the address is not selected first") + T.ok(L.REPORT_CLIPBOARD:find("Ctrl+C", 1, true), "how to copy is not said") + T.eq(f.clipboard:GetText(), L.REPORT_CLIPBOARD) + T.eq(W.actions[1].label, L.REPORT_SELECT_LINK, "the link's button is not first") + T.ok(Layout.rect(f.buttons[1]).right <= Layout.rect(f.buttons[2]).left, "the link's button is not on the left") click(f.buttons[2]) + T.ok(f.text.edit:HasFocus() and f.text.edit.selection, "Select the text does not select it") + click(f.buttons[1]) T.ok(f.link.edit:HasFocus() and f.link.edit.selection, "Select the link does not select it") T.ok(f.saved:GetText():find("WTF\\Account\\\\SavedVariables\\Ding.lua", 1, true), "where the file is") -- Read-only: typing puts the text back. @@ -345,14 +354,19 @@ T.test("with a gamepad: the reasons first, A picks one and goes on to Create, A T.ok(text:find("**What went wrong:** The arrow points to the wrong place", 1, true), text) T.ok(not text:find("In the player's words", 1, true), "an empty comment is written") T.ok(text:match("^[^\n]*"):find(": The arrow points to the wrong place$"), "the title line lacks the reason") - -- Each action once: A on the focused button, Y on Select the text, B by the cross. - T.ok(chipsOn(f.buttons[1]).PAD1 and chipsOn(f.buttons[1]).PAD4, "A and Y not on Select the text") - T.ok(not anyKey(chipsOn(f.buttons[2])), "a chip on Select the link") + -- Each action once: A on the focused button (the link's, first), Y on Select the text, B by + -- the cross. + T.ok(chipsOn(f.buttons[1]).PAD1 and not chipsOn(f.buttons[1]).PAD4, "A not (alone) on Select the link") + T.ok(chipsOn(f.buttons[2]).PAD4 and not chipsOn(f.buttons[2]).PAD1, "Y not (alone) on Select the text") + T.ok(f.link.edit:HasFocus(), "the address is not selected first") T.ok(f.back:IsShown(), "no B by the close cross") T.eq(#(f.hints.list or {}), 0, "a hint repeats a button with a gamepad") H.press("PADDRIGHT") - T.ok(chipsOn(f.buttons[2]).PAD1 and not chipsOn(f.buttons[1]).PAD1, "D-pad right: A not on Select the link") + T.ok(chipsOn(f.buttons[2]).PAD1 and chipsOn(f.buttons[2]).PAD4 and not chipsOn(f.buttons[1]).PAD1, + "D-pad right: A not on Select the text") H.press("PAD1") + T.ok(f.text.edit:HasFocus(), "A on Select the text") + H.press("PADDLEFT"); H.press("PAD1") T.ok(f.link.edit:HasFocus(), "A on Select the link") H.press("PAD4") T.ok(f.text.edit:HasFocus() and f.text.edit.selection, "Y does not select the text") @@ -361,6 +375,63 @@ T.test("with a gamepad: the reasons first, A picks one and goes on to Create, A T.eq(G.owner, nil, "the gamepad is not given back") end) +-- The width of an ASCII text in a TrueType font at `size`: its glyphs' advances (tables cmap, +-- format 4, and hmtx). +local function fontWidth(path, text, size) + local data = assert(io.open(path, "rb")):read("*a") + local function u16(o) local a, b = data:byte(o + 1, o + 2); return a * 256 + b end + local function s16(o) local v = u16(o); return v >= 32768 and v - 65536 or v end + local function u32(o) return u16(o) * 65536 + u16(o + 2) end + local tables = {} + for i = 0, u16(4) - 1 do + local record = 12 + 16 * i + tables[data:sub(record + 1, record + 4)] = u32(record + 8) + end + local units, metrics, cmap = u16(tables.head + 18), u16(tables.hhea + 34), tables.cmap + local sub + for i = 0, u16(cmap + 2) - 1 do + local o = cmap + u32(cmap + 8 + 8 * i) + if not sub and u16(o) == 4 then sub = o end + end + local segments = u16(sub + 6) / 2 + local function glyph(c) + for s = 0, segments - 1 do + if c <= u16(sub + 14 + 2 * s) then + local start = u16(sub + 16 + 2 * segments + 2 * s) + if c < start then return 0 end + local delta = s16(sub + 16 + 4 * segments + 2 * s) + local at = sub + 16 + 6 * segments + 2 * s + if u16(at) == 0 then return (c + delta) % 65536 end + local g = u16(at + u16(at) + 2 * (c - start)) + return g == 0 and 0 or (g + delta) % 65536 + end + end + return 0 + end + local total = 0 + for i = 1, #text do total = total + u16(tables.hmtx + 4 * math.min(glyph(text:byte(i)), metrics - 1)) end + return total * size / units +end + +T.test("the GitHub address shows whole in its box (the game cut it): Noto Sans' widths, room to spare", function() + start() + W:Open(); H.flush() + W:Make(); H.flush() + local f = W.frame + local box = Layout.rect(f.link.edit) + local room = box.right - box.left + local font = tools .. "/../Ding/Media/Fonts/NotoSans-Regular.ttf" + local width = fontWidth(font, R.ISSUE_URL, W.LINK_FONT) + T.ok(width * 1.15 <= room, ("the address takes %.0f of %.0f units"):format(width, room)) + -- What the game cut: the address in 11 in a 520 window's box (488). + T.ok(fontWidth(font, R.ISSUE_URL, 11) > 488 - 16, "the measure does not explain what the game cut") + -- The mock's measure (6 a character, wider than the font) fits too. + local fs = UI.Text(UIParent, "regular", W.LINK_FONT, UI.C.text) + fs:SetText(R.ISSUE_URL) + T.ok(fs:GetStringWidth() <= room, "the address does not fit the mock's measure") + W:Close(); H.flush() +end) + T.test("Something else with words: the title line takes the player's first words", function() start() local info = R:Collect() From f7f59a710316d3de7f54c8f1d502d7b854407a8a Mon Sep 17 00:00:00 2001 From: Moust4ki Date: Thu, 15 Oct 2026 16:28:59 +0200 Subject: [PATCH 5/7] Report window: one glyph per button, A while focused, Y once the focus leaves Yendi, in game: the focused "Select the text" showed "A Y Select the text". A focused button now carries A alone; its own key (Y) comes back once the focus leaves, and still works meanwhile. Each button keeps room for the wider of the two chips, so nothing moves with the focus. single_action_test: no control of the report window shows two glyphs, at every stop, under the mouse, in every language; report_test: the chips and the width of "Select the text" focused and not. Co-Authored-By: Claude Opus 5.5 --- Ding/UI/Report.lua | 21 ++++++++++++++------- tools/tests/report_test.lua | 8 ++++++-- tools/tests/single_action_test.lua | 26 +++++++++++++++++++++++--- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/Ding/UI/Report.lua b/Ding/UI/Report.lua index 525a56c..39b9149 100644 --- a/Ding/UI/Report.lua +++ b/Ding/UI/Report.lua @@ -9,9 +9,10 @@ -- then where Ding keeps the last reports for a player without a keyboard (DingDB.reports). -- Gamepad (temporary bindings, out of combat only): the D-pad up / down between the reasons, -- the box and the buttons, left / right along the buttons, A picks or presses, Y selects the --- text, B closes. Each action shows once: A on the focused row or button, Y on "Select the --- text", B by the close cross; the footer is the mouse help without a gamepad. Mouse: hover and --- click, right click or Esc closes. +-- text, B closes. Each action shows once, one glyph per control: A on the focused row or +-- button, Y on "Select the text" while the focus is elsewhere, B by the close cross; the +-- footer is the mouse help without a gamepad. Mouse: hover and click, right click or Esc +-- closes. local _, Ding = ... local L = Ding.L local UI = Ding.UI @@ -488,7 +489,8 @@ function W:LayoutButtons() b.text:SetText(action.label) b.bg:SetColor(action.primary and C.accent or C.control) UI.Color(b.text, action.primary and C.onAccent or C.text) - local room = pad and UI.KeysWidth({ "PAD1", action.key }) or 0 + -- One chip at a time (W:ShowFocus): room for the wider of A and its own key. + local room = pad and math.max(UI.KeysWidth({ "PAD1" }), UI.KeysWidth(action.key and { action.key })) or 0 b:SetWidth(math.max(96, UI.Measure(b.text) + 28 + room)) b:ClearAllPoints() b:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", x, EDGE) @@ -552,7 +554,7 @@ function W:FocusButton(index) end -- The focus ring and the A chip where the focus is (A only with a gamepad), the buttons' own --- chips (Y on "Select the text"). +-- chips elsewhere (Y on "Select the text" while it has not the focus). function W:ShowFocus() local f = self.frame local pad = G():Active() @@ -569,9 +571,14 @@ function W:ShowFocus() local action = self.actions and self.actions[i] local focused = stop.kind == "button" and stop.i == i and b:IsShown() b.focus:SetShown(pad and focused) + -- One glyph per control (Yendi, in game: "A Y Select the text"): A while it has the + -- focus, its own key (Y) once the focus leaves; that key still works meanwhile. local keys = {} - if pad and focused then keys[#keys + 1] = "PAD1" end - if pad and action and action.key then keys[#keys + 1] = action.key end + if pad and focused then + keys[1] = "PAD1" + elseif pad and action and action.key then + keys[1] = action.key + end UI.ButtonKeys(b, keys, action and action.primary) end end diff --git a/tools/tests/report_test.lua b/tools/tests/report_test.lua index 5fd7be9..530c522 100644 --- a/tools/tests/report_test.lua +++ b/tools/tests/report_test.lua @@ -361,9 +361,13 @@ T.test("with a gamepad: the reasons first, A picks one and goes on to Create, A T.ok(f.link.edit:HasFocus(), "the address is not selected first") T.ok(f.back:IsShown(), "no B by the close cross") T.eq(#(f.hints.list or {}), 0, "a hint repeats a button with a gamepad") + -- Focused, Select the text shows A alone (in game: "A Y Select the text"); Y comes back once + -- the focus leaves, and still selects the text meanwhile. Its width does not move. + local width = f.buttons[2]:GetWidth() H.press("PADDRIGHT") - T.ok(chipsOn(f.buttons[2]).PAD1 and chipsOn(f.buttons[2]).PAD4 and not chipsOn(f.buttons[1]).PAD1, - "D-pad right: A not on Select the text") + T.ok(chipsOn(f.buttons[2]).PAD1 and not chipsOn(f.buttons[2]).PAD4, "focused: not A alone on Select the text") + T.ok(not anyKey(chipsOn(f.buttons[1])), "a chip left on Select the link") + T.eq(f.buttons[2]:GetWidth(), width, "Select the text changes size with the focus") H.press("PAD1") T.ok(f.text.edit:HasFocus(), "A on Select the text") H.press("PADDLEFT"); H.press("PAD1") diff --git a/tools/tests/single_action_test.lua b/tools/tests/single_action_test.lua index 9270062..58b6785 100644 --- a/tools/tests/single_action_test.lua +++ b/tools/tests/single_action_test.lua @@ -398,7 +398,16 @@ local function checkReport(where) end T.eq(count.PAD1, 1, where .. ": A") T.eq(count.PAD2, 1, where .. ": B") - T.eq(count.PAD4, Rep.page == "done" and 1 or nil, where .. ": Y") + -- Y on Select the text, unless A is on it: one glyph per control (in game, focused: "A Y + -- Select the text"). + local textButton = Rep.page == "done" and buttons[2] or nil + local yShown = textButton and not textButton.keys.PAD1 + T.eq(count.PAD4, yShown and 1 or nil, where .. ": Y") + for _, b in ipairs(buttons) do + local n = 0 + for _ in pairs(b.keys) do n = n + 1 end + T.ok(n <= 1, ("%s: %d glyphs on %s"):format(where, n, b.label or "a control")) + end T.eq(#hintsOf(f.hints), 0, where .. ": a hint with a gamepad") else mouseOnly(where, f.hints, buttons) @@ -417,9 +426,18 @@ T.test("report a problem: A where the focus is, Y on Select the text, B by the c -- A on a reason goes on to Create the text, A there makes it. H.press("PAD1"); H.press("PAD1"); H.flush() T.eq(Rep.page, "done", "A on Create the text") - checkReport("report's text") - H.press("PADDRIGHT") checkReport("report's text, the link's button focused") + H.press("PADDRIGHT") + checkReport("report's text, the text's button focused") + local b = Rep.frame.buttons[2] + T.ok(chipsOn(b).PAD1 and not chipsOn(b).PAD4, "focused: Select the text shows more than A") + -- The mouse over it: the same, A alone there, Y nowhere. + Rep.frame.buttons[1].scripts.OnEnter(Rep.frame.buttons[1]) + b.scripts.OnEnter(b) + checkReport("report's text, the mouse over Select the text") + T.ok(chipsOn(b).PAD1 and not chipsOn(b).PAD4, "hovered: Select the text shows more than A") + H.press("PADDLEFT") + T.ok(chipsOn(b).PAD4 and not chipsOn(b).PAD1, "the focus gone: Y does not come back on Select the text") pad(false) checkReport("report's text, unplugged") closeAll() @@ -487,6 +505,8 @@ T.test("every language: import, export, settings buttons and move mode fit, game checkReport(where .. ", report") Rep:Make(); H.flush() checkReport(where .. ", report's text") + Rep:FocusButton(2) + checkReport(where .. ", report's text, Select the text focused") closeAll() end end From 6b7a18ae654d6f5059b3c06da1672a4bc6a5ae73 Mon Sep 17 00:00:00 2001 From: Moust4ki Date: Thu, 15 Oct 2026 16:58:14 +0200 Subject: [PATCH 6/7] Smoke: the report's name check uses a name of its own (the mock's "Player" is a heading) Co-Authored-By: Claude Opus 5.5 --- tools/smoke_test.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/smoke_test.lua b/tools/smoke_test.lua index c36a317..65a8c78 100644 --- a/tools/smoke_test.lua +++ b/tools/smoke_test.lua @@ -1820,6 +1820,7 @@ ok, err = pcall(function() H.resetQuests() E.turnedIn, Ding.char.manual = {}, {} player.level, player.map, player.x, player.y = 5, 1438, 58.7, 44 + player.name = "Thalindra" E:SetGuide("smoke-report", 1, true) tick(1) H.accept(456); flush(); tick(1) @@ -1840,7 +1841,9 @@ ok, err = pcall(function() expect(text:find("index 2 of 3", 1, true), "report: no step in the text") expect(text:find("Position: 58.7, 44.0", 1, true), "report: no position in the text") expect(text:find("2/7", 1, true), "report: no objective counter in the text") - expect(not text:find(UnitName("player"), 1, true), "report: the character's name is in the text") + expect(UnitName("player") == "Thalindra" and not text:find("Thalindra", 1, true), + "report: the character's name is in the text") + player.name = nil expect(#db.reports == math.min(before + 1, Ding.Report.KEEP), "report: not kept in DingDB.reports") press("PAD2") expect(not RW:IsShown(), "report: B does not close the window") From e382aa57cdd35caca6daa5090ed07baa8afc2300 Mon Sep 17 00:00:00 2001 From: Moust4ki Date: Thu, 15 Oct 2026 16:58:35 +0200 Subject: [PATCH 7/7] Changelog v1.8.0: line breaks Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b54805f..959d28f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,9 +15,9 @@ Nothing else to install: QuestieDB is no longer needed, and TomTom stays optiona - Pick what went wrong among four ready answers (handy with a gamepad) and add your own words if you can type, then **Create the text**: 1. the address of the GitHub page to open, 2. the text to paste into it, each selected by its button for Ctrl+C (an addon cannot copy for you). Ding - keeps your last 20 reports in its - saved variables (`WTF\Account\\SavedVariables\Ding.lua`), so a player without a - keyboard can send that file instead; "Restore all defaults" keeps them. + keeps your last 20 reports in its saved variables + (`WTF\Account\\SavedVariables\Ding.lua`), so a player without a keyboard can send + that file instead; "Restore all defaults" keeps them. ## v1.7.1