diff --git a/.github/ISSUE_TEMPLATE/ding-report.yml b/.github/ISSUE_TEMPLATE/ding-report.yml new file mode 100644 index 0000000..e08ffda --- /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 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 + 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 7037d67..959d28f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## v1.8.0 + +Nothing else to install: QuestieDB is no longer needed, and TomTom stays optional. + +- **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 + 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**: 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. + ## v1.7.1 Nothing else to install: QuestieDB is no longer needed, and TomTom stays optional. 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..11290e3 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,9 @@ Ding.Locales.deDE = { CITY_RATCHET = "Ratschet", CITY_EVERLOOK = "Ewige Warte", BIND_FINDER = "NSC-Verzeichnis", + 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.", @@ -508,6 +511,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_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.", + 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..19f0ef2 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,9 @@ Ding.Locales.enUS = { CITY_RATCHET = "Ratchet", CITY_EVERLOOK = "Everlook", BIND_FINDER = "NPC directory", + 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.", @@ -511,6 +514,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_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.", + 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..c844956 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,9 @@ Ding.Locales.esES = { CITY_RATCHET = "Trinquete", CITY_EVERLOOK = "Vista Eterna", BIND_FINDER = "Directorio de PNJ", + 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.", @@ -511,6 +514,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_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.", + 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..8571986 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,9 @@ Ding.Locales.frFR = { CITY_RATCHET = "Cabestan", CITY_EVERLOOK = "Long-guet", BIND_FINDER = "Annuaire des PNJ", + 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.", @@ -506,6 +509,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_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.", + 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..9d43f48 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,9 @@ Ding.Locales.koKR = { CITY_RATCHET = "톱니항", CITY_EVERLOOK = "눈망루 마을", BIND_FINDER = "NPC 안내소", + BIND_REPORT = "가이드 문제 신고", + REPORT_BUTTON = "이 단계에 문제가 있나요?", + REPORT_BUTTON_TIP = "가이드 제작자에게 알려 주세요. 신고 내용은 Ding!이 대신 작성합니다.", TRAINING_DUE = "직업 전문가에게 새 주문 (%s)", TRAINING_DUE_GUESS = "직업 전문가에게 새 주문이 있을 수 있습니다 (%d 레벨)", TRAINING_NEAR = "직업 전문가 %s: 바로 여기 있습니다. 새 주문을 배울 수 있습니다.", @@ -509,6 +512,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_CLIPBOARD = "버튼으로 상자를 선택한 뒤 Ctrl+C로 복사하세요. 애드온은 클립보드에 텍스트를 넣을 수 없습니다.", + REPORT_STEP_LINK = "1. GitHub에서 이 주소를 여세요:", + REPORT_STEP_TEXT = "2. 이 텍스트를 복사해 신고에 붙여 넣으세요:", + 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..ba13782 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,9 @@ Ding.Locales.ptBR = { CITY_RATCHET = "Vila Catraca", CITY_EVERLOOK = "Visteterna", BIND_FINDER = "Diretório de PNJs", + 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ê.", @@ -509,6 +512,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_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.", + 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..511a4dc 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,9 @@ Ding.Locales.zhCN = { CITY_RATCHET = "棘齿城", CITY_EVERLOOK = "永望镇", BIND_FINDER = "NPC名录", + BIND_REPORT = "报告指南的问题", + REPORT_BUTTON = "这一步有问题吗?", + REPORT_BUTTON_TIP = "告诉制作指南的人:Ding! 会替你写好报告。", TRAINING_DUE = "职业训练师处有新技能(%s)", TRAINING_DUE_GUESS = "职业训练师那里可能有新法术(%d级)", TRAINING_NEAR = "你的职业训练师%s就在这里:有新法术可以学习。", @@ -509,6 +512,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_CLIPBOARD = "每个按钮会选中对应的框,再按 Ctrl+C 复制:插件无法把文本放进剪贴板。", + REPORT_STEP_LINK = "1. 在 GitHub 上打开这个地址:", + REPORT_STEP_TEXT = "2. 复制这段文本,粘贴到报告中:", + 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..c986b22 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,9 @@ Ding.Locales.zhTW = { CITY_RATCHET = "棘齒城", CITY_EVERLOOK = "永望鎮", BIND_FINDER = "NPC名錄", + BIND_REPORT = "回報指南的問題", + REPORT_BUTTON = "這一步有問題嗎?", + REPORT_BUTTON_TIP = "告訴製作指南的人:Ding! 會替你寫好回報。", TRAINING_DUE = "職業訓練師處有新技能(%s)", TRAINING_DUE_GUESS = "職業訓練師那裡可能有新法術(%d級)", TRAINING_NEAR = "你的職業訓練師%s就在這裡:有新法術可以學習。", @@ -509,6 +512,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_CLIPBOARD = "每個按鈕會選取對應的框,再按 Ctrl+C 複製:插件無法把文字放進剪貼簿。", + REPORT_STEP_LINK = "1. 在 GitHub 上開啟這個網址:", + REPORT_STEP_TEXT = "2. 複製這段文字,貼到回報中:", + 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..52e9e0e 100644 --- a/Ding/UI/Panel.lua +++ b/Ding/UI/Panel.lua @@ -1180,9 +1180,41 @@ 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() + -- "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.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.onReport) + flag:SetSize(7, 5) + flag:SetPoint("TOPLEFT", pole, "TOPRIGHT", 0, 0) + 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) + 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 +1222,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 +1966,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 +2035,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 +2074,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 +2195,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 +2203,25 @@ 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 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(L.BIND_FINDER) + 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 @@ -2194,6 +2248,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 +2262,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 +2273,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 +2290,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..39b9149 --- /dev/null +++ b/Ding/UI/Report.lua @@ -0,0 +1,715 @@ +-- "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": 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, 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 +local C = UI.C + +local W = {} +Ding.ReportWindow = W + +local SIZE = UI.SIZE +-- 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 +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 +-- 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" } + +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", 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) + 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.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.clipboard = paragraph(done, "regular", 11, C.secondary) + 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.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 + +-- 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) + -- 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) + 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 elsewhere (Y on "Select the text" while it has not the focus). +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) + -- 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[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 + +--------------------------------------------------------------------------- +-- 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 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() + 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_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.link: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/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 a0f0c9c..08c4a90 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 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. - **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,33 @@ 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 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 + 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**: 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 +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..3772db5 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -1311,6 +1311,56 @@ 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 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 + (`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 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 + 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", 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 + 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 +1566,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 +1577,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..ff3c2bf 100644 --- a/docs/UI-handoff.md +++ b/docs/UI-handoff.md @@ -546,6 +546,30 @@ 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), 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, 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 + 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, 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, 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/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/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`. diff --git a/tools/smoke_test.lua b/tools/smoke_test.lua index bb91717..65a8c78 100644 --- a/tools/smoke_test.lua +++ b/tools/smoke_test.lua @@ -1808,5 +1808,48 @@ 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 + player.name = "Thalindra" + 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(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") + 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..368a48e 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.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 + 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..530c522 --- /dev/null +++ b/tools/tests/report_test.lua @@ -0,0 +1,536 @@ +-- 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 +local UI = Ding.UI + +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") + -- 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.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) + 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.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") + 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) + +-- 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) + 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)) + -- 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. + 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 (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") + -- 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 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") + 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) + +-- 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() + 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..58b6785 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,80 @@ 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") + -- 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) + 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, 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() +end) + --------------------------------------------------------------------------- -- Every language: the buttons, their chips and the hints fit --------------------------------------------------------------------------- @@ -425,6 +501,13 @@ 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") + Rep:FocusButton(2) + checkReport(where .. ", report's text, Select the text focused") + 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