-- mim_chat.lua --[[ The miniwindow parts of the MIM_Chat plugin, which loads this file from its own directory. Ported from Fiendish's Aardwolf chat window libraries (text_rect.lua, scrollbar.lua, aardwolf_colors.lua and mw_theme_base.lua), which borrow from the MUSHclient community (https://www.mushclient.com/forum/?id=9385). Adapted for Murderous Intentions: the colour codes are the game's own, and it needs only the libraries that come with MUSHclient. Colour codes: {r {g {y {b {m {c {w red, green, yellow, blue, magenta, cyan, white {R {G {Y {B {M {C {W {D the bright ones, and dark grey {x back to the normal colour {{ a { any colour for the text (F) or behind it (B): three hex digits, red green blue < the text "" Functions: ColoursToStyles (text, default_colour, background, multiline) ToMultilineStyles (text_or_styles, default_colour, background, multiline) StylesToColours (styles) styles back to colour coded text strip_colours (text) the text without colour codes strip_colours_from_styles (styles) TruncateStyles (styles, startcol, endcol) StylesWidth (win, font, bold_font, styles, show_bold, utf8) TextRect a scrolling text area: selecting, copying, links, a scrollback of max_lines lines ScrollBar a scroll bar to go with it Theme the window's colours, title bar, 3D boxes and resize grip --]] require "copytable" require "commas" require "wait" -- ask MUSHclient to redraw soon (several changes make one redraw) local function repaint () Redraw () end -- repaint -- a string split at a pattern; with keep, the matches are in the list too: -- ("a1b", "%d", true) gives {"a", "1", "b"} local function split (s, pat, keep) local fields = {} local start = 1 s:gsub ("()(" .. pat .. ")", function (index, match) table.insert (fields, s:sub (start, index - 1)) if keep then table.insert (fields, match) end -- if start = index + #match end) table.insert (fields, s:sub (start)) return fields end -- split ------------------------------------------------------------------------------- -- Theme: the window's look (the little of mw_theme_base the chat window uses) ------------------------------------------------------------------------------- Theme = { RESIZER_SIZE = 16, DYNAMIC_BUTTON_PADDING = 10, TITLE_PADDING = 2, PRIMARY_BODY = 0x000000, -- behind the text SECONDARY_BODY = 0x2B2B2B, -- the rest of the window BODY_TEXT = 0xC0C0C0, TITLE_BAR = 0x404040, TITLE_TEXT = 0xE0E0E0, BORDER = 0x707070, THREE_D_HIGHLIGHT = 0x9A9A9A, THREE_D_GRADIENT_FIRST = 0x555555, THREE_D_SOFTSHADOW = 0x303030, THREE_D_HARDSHADOW = 0x000000, THREE_D_SURFACE_DETAIL = 0xD0D0D0, CLICKABLE = 0x464646, -- tabs (colours are 0xBBGGRR) CLICKABLE_TEXT = 0xC8C8C8, CLICKABLE_HOVER = 0xC8C8C8, CLICKABLE_HOVER_TEXT = 0x202020, CLICKABLE_HOT = 0x0064C8, -- a tab with new lines: orange CLICKABLE_HOT_TEXT = 0xFFFFFF, SCROLL_TRACK_COLOR1 = 0x404040, SCROLL_TRACK_COLOR2 = 0x5A5A5A, VERTICAL_TRACK_BRUSH = miniwin.brush_fine_pattern, } function Theme.TextHeight (win, font) return WindowFontInfo (win, font, 1) end -- Theme.TextHeight -- a raised (or, pressed, sunken) box function Theme.Draw3DRect (win, left, top, right, bottom, depressed) WindowRectOp (win, miniwin.rect_fill, left, top, right + 1, bottom + 1, Theme.THREE_D_GRADIENT_FIRST) local light, dark = Theme.THREE_D_HIGHLIGHT, Theme.THREE_D_HARDSHADOW if depressed then light, dark = dark, light end -- if WindowLine (win, left, top, right, top, light, miniwin.pen_solid, 1) WindowLine (win, left, top, left, bottom, light, miniwin.pen_solid, 1) WindowLine (win, left, bottom, right + 1, bottom, dark, miniwin.pen_solid, 1) WindowLine (win, right, top, right, bottom, dark, miniwin.pen_solid, 1) end -- Theme.Draw3DRect -- The background, border and (if there is a title) a title bar to drag the -- window by; without a title, a thin strip along the top drags it. -- Returns the left, top, right and bottom of the space inside. function Theme.DressWindow (win, font, title) local width, height = WindowInfo (win, 3), WindowInfo (win, 4) WindowRectOp (win, miniwin.rect_fill, 0, 0, 0, 0, Theme.SECONDARY_BODY) local top = 4 if title and title ~= "" then top = Theme.TextHeight (win, font) + 2 * Theme.TITLE_PADDING + 2 WindowRectOp (win, miniwin.rect_fill, 1, 1, width - 1, top, Theme.TITLE_BAR) local tw = WindowTextWidth (win, font, title) WindowText (win, font, title, math.max (3, (width - tw) / 2), Theme.TITLE_PADDING + 1, width - 3, top, Theme.TITLE_TEXT) WindowLine (win, 1, top, width - 1, top, Theme.THREE_D_HIGHLIGHT, miniwin.pen_solid, 1) end -- if if WindowMoveHotspot (win, "zz_mw_" .. win .. "_movewindow_hotspot", 0, 0, width, top) ~= error_code.eOK then movewindow.add_drag_handler (win, 0, 0, width, top, miniwin.cursor_both_arrow) end -- if WindowRectOp (win, miniwin.rect_frame, 0, 0, 0, 0, Theme.BORDER) return 1, top + 1, width - 2, height - 2 end -- Theme.DressWindow -- A grip in the bottom right corner for resizing ("$" sorts it ahead of the -- other hotspots, so it gets the mouse where they overlap). Once made, its -- hotspot is only moved: making it again while it is being dragged would -- end the drag. function Theme.AddResizeTag (win, kind, x, y, mousedown, dragmove, dragrelease) local size = Theme.RESIZER_SIZE local width, height = WindowInfo (win, 3), WindowInfo (win, 4) x = width - size y = height - size WindowRectOp (win, miniwin.rect_fill, x, y, width - 1, height - 1, Theme.SECONDARY_BODY) for i = 0, 2 do WindowLine (win, x + size - 3 - i * 4, y + size - 2, x + size - 2, y + size - 3 - i * 4, Theme.THREE_D_HIGHLIGHT, miniwin.pen_solid, 1) end -- for if WindowMoveHotspot (win, "$resizer", x, y, x + size, y + size) ~= error_code.eOK then WindowAddHotspot (win, "$resizer", x, y, x + size, y + size, "", "", mousedown, "", "", "Drag to resize", miniwin.cursor_nw_se_arrow, 0) WindowDragHandler (win, "$resizer", dragmove, dragrelease, 0) end -- if end -- Theme.AddResizeTag ------------------------------------------------------------------------------- -- Colour codes ------------------------------------------------------------------------------- -- code letter -> { ANSI colour (1 black ... 8 white), bright } local BASIC = { r = {2, false}, g = {3, false}, y = {4, false}, b = {5, false}, m = {6, false}, c = {7, false}, w = {8, false}, R = {2, true}, G = {3, true}, Y = {4, true}, B = {5, true}, M = {6, true}, C = {7, true}, W = {8, true}, D = {1, true}, } -- the game's other names for them local SAME = { ["1"] = "r", ["2"] = "g", ["3"] = "y", ["4"] = "b", ["5"] = "m", ["6"] = "c", ["7"] = "w", ["!"] = "R", ["@"] = "G", ["#"] = "Y", ["$"] = "B", ["%"] = "M", ["^"] = "C", ["&"] = "W", ["8"] = "D", ["*"] = "D", ["0"] = "x", -- channel codes, if a line still has them: the game's default colours A = "G", E = "R", F = "M", H = "B", J = "y", K = "c", L = "W", N = "C", P = "Y", Q = "g", S = "m", T = "r", U = "c", V = "w", a = "m", e = "B", f = "R", h = "B", i = "c", j = "Y", k = "x", l = "C", } local BASIC_ORDER = { "r", "g", "y", "b", "m", "c", "w", "R", "G", "Y", "B", "M", "C", "W", "D" } local function basic_colour (letter) local b = BASIC [letter] return b [2] and GetBoldColour (b [1]) or GetNormalColour (b [1]) end -- basic_colour -- : each hex digit is a 0-255 value in steps of 17; colours are 0xBBGGRR local function tc_colour (hex) local r, g, b = tonumber (hex:sub (1, 1), 16), tonumber (hex:sub (2, 2), 16), tonumber (hex:sub (3, 3), 16) return (b * 17) * 65536 + (g * 17) * 256 + (r * 17) end -- tc_colour -- the nearest (or ) for a colour local function tc_code (kind, colour) local r, g, b = colour % 256, math.floor (colour / 256) % 256, math.floor (colour / 65536) % 256 return string.format ("<%s%X%X%X>", kind, math.floor (r / 17 + 0.5), math.floor (g / 17 + 0.5), math.floor (b / 17 + 0.5)) end -- tc_code -- a colour given as a number, "{r", "r" or "" local function to_colour (c, default) if type (c) == "number" then return c elseif type (c) == "string" then local hex = c:match ("^<[Ff](%x%x%x)>$") if hex then return tc_colour (hex) end -- if local letter = c:match ("^{?(.)$") letter = letter and (SAME [letter] or letter) if letter and BASIC [letter] then return basic_colour (letter) end -- if end -- if return default end -- to_colour -- the code for a colour (and boldness), for turning styles back into text local function code_for (colour, bold) for _, letter in ipairs (BASIC_ORDER) do if basic_colour (letter) == colour and (bold == nil or BASIC [letter] [2] == bold) then return "{" .. letter end -- if end -- for for _, letter in ipairs (BASIC_ORDER) do if basic_colour (letter) == colour then return "{" .. letter end -- if end -- for return tc_code ("F", colour) end -- code_for -- Text with colour codes to a line of styles, or (multiline) lines of them, -- split at newlines. default_colour is for text before any code and after -- {x (MUSHclient's normal white if not given); background is behind it all -- until a . function ColoursToStyles (input, default_colour, background, multiline) input = tostring (input or "") default_colour = to_colour (default_colour, GetNormalColour (8)) background = to_colour (background, nil) local styles, buf = {}, {} local fg, fgcode, bold = default_colour, nil, false local bg, bgcode = background, nil local function flush () if #buf > 0 then local text = table.concat (buf) table.insert (styles, { text = text, length = #text, textcolour = fg, backcolour = bg, bold = bold, code = fgcode, bgcode = bgcode }) buf = {} end -- if end -- flush local i, n = 1, #input while i <= n do local j = input:find ("[{<]", i) if not j then table.insert (buf, input:sub (i)) break end -- if if j > i then table.insert (buf, input:sub (i, j - 1)) end -- if i = j local c = input:sub (i, i) if c == "{" then local d = input:sub (i + 1, i + 1) if d == "{" then table.insert (buf, "{") elseif d ~= "" then flush () local letter = SAME [d] or d if BASIC [letter] then fg, bold, fgcode = basic_colour (letter), BASIC [letter] [2], "{" .. d elseif letter == "z" then -- blinking: no colour change else -- {x and anything else: back to normal fg, bold, fgcode = default_colour, false, "{" .. d bg, bgcode = background, nil end -- if end -- if (a lone { at the end is dropped) i = i + 2 elseif input:find ("^<<[FfBb]%x%x%x>", i) then table.insert (buf, input:sub (i + 1, i + 6)) i = i + 7 elseif input:find ("^<[FfBb]%x%x%x>", i) then flush () local code = input:sub (i, i + 5) if code:sub (2, 2):upper () == "F" then fg, bold, fgcode = tc_colour (code:sub (3, 5)), false, code else bg, bgcode = tc_colour (code:sub (3, 5)), code end -- if i = i + 6 else table.insert (buf, "<") i = i + 1 end -- if end -- while flush () if #styles == 0 then styles [1] = { text = "", length = 0, textcolour = default_colour, backcolour = background, bold = false } end -- if if multiline then return split_boundaries (styles, "\n") end -- if return styles end -- ColoursToStyles -- does this code ({x, and the codes that aren't colours) turn everything, -- the background too, back to normal? local function is_reset (code) if code:sub (1, 1) ~= "{" then return false end -- if local letter = code:sub (2) letter = SAME [letter] or letter return not BASIC [letter] and letter ~= "z" end -- is_reset -- Styles (a line, or lines of them) back to colour coded text. function StylesToColours (styles) local style_lines = styles if styles [1] and not styles [1] [1] then style_lines = { styles } end -- if local line_texts = {} for _, line in ipairs (style_lines) do local parts = {} local lastfg, lastbg = nil, nil for _, style in ipairs (line) do local fgc = style.code or (style.textcolour and code_for (style.textcolour, style.bold)) or "{x" local bgc = style.bgcode if not bgc and style.backcolour and style.backcolour ~= 0 then bgc = tc_code ("B", style.backcolour) end -- if if is_reset (fgc) and fgc ~= lastfg then table.insert (parts, fgc) lastfg, lastbg = fgc, nil end -- if if bgc ~= lastbg then if bgc then table.insert (parts, bgc) else table.insert (parts, "{x") lastfg = "{x" end -- if lastbg = bgc end -- if if fgc ~= lastfg then table.insert (parts, fgc) lastfg = fgc end -- if local text = style.text:gsub ("{", "{{"):gsub ("<([FfBb]%x%x%x>)", "<<%1") table.insert (parts, text) end -- for table.insert (line_texts, table.concat (parts)) end -- for return table.concat (line_texts, "\n") end -- StylesToColours -- the text without its colour codes function strip_colours (s) s = tostring (s or "") s = s:gsub ("{{", "\1"):gsub ("{.", ""):gsub ("{$", "") s = s:gsub ("<<([FfBb]%x%x%x>)", "\2%1"):gsub ("<[FfBb]%x%x%x>", "") return (s:gsub ("\1", "{"):gsub ("\2", "<")) end -- strip_colours -- the game's codes are already as short as they go function canonicalize_colours (s) return s end -- canonicalize_colours function strip_colours_from_styles (styles) local style_lines = styles if styles [1] and not styles [1] [1] then style_lines = { styles } end -- if local line_texts = {} for _, line in ipairs (style_lines) do local parts = {} for _, v in ipairs (line) do table.insert (parts, v.text) end -- for table.insert (line_texts, table.concat (parts)) end -- for return table.concat (line_texts, "\n") end -- strip_colours_from_styles -- part of a line of styles, from column startcol to endcol function TruncateStyles (styles, startcol, endcol) if (styles == nil) or (styles [1] == nil) then return styles end -- if startcol = startcol or 1 endcol = endcol or 99999 -- negative columns count back from the end if (startcol < 0) or (endcol < 0) then local total_chars = 0 for _, v in ipairs (styles) do total_chars = total_chars + v.length end -- for if startcol < 0 then startcol = total_chars + startcol + 1 end -- if if endcol < 0 then endcol = total_chars + endcol + 1 end -- if end -- if if startcol > endcol then startcol, endcol = endcol, startcol end -- if local found_first = false local col_counter = 0 local new_styles = {} local break_after = false for _, v in ipairs (styles) do local new_style = copytable.shallow (v) col_counter = col_counter + new_style.length if endcol <= col_counter then local marker = endcol - (col_counter - v.length) new_style.text = new_style.text:sub (1, marker) new_style.length = marker break_after = true end -- if if startcol <= col_counter then if not found_first then local marker = startcol - (col_counter - v.length) found_first = true new_style.text = new_style.text:sub (marker) new_style.length = new_style.length - marker + 1 end -- if table.insert (new_styles, new_style) end -- if if break_after then break end -- if end -- for return new_styles end -- TruncateStyles function StylesWidth (win, plain_font, bold_font, styles, show_bold, utf8) local width = 0 for _, v in ipairs (styles) do local font = plain_font if show_bold and v.bold and bold_font then font = bold_font end -- if width = width + WindowTextWidth (win, font, v.text, utf8) end -- for return width end -- StylesWidth -- colour coded text, a line of styles or lines of styles: always lines of styles function ToMultilineStyles (message, default_colour, background, multiline) if type (message) == "string" then message = ColoursToStyles (message, default_colour, background, multiline) end -- if assert (type (message) == "table", "ToMultilineStyles: not colour coded text or styles") if message.text then message = { { message } } elseif (type (message [1]) == "table") and message [1].text then message = { message } end -- if assert ((type (message [1]) == "table") and (type (message [1] [1]) == "table") and message [1] [1].text, "ToMultilineStyles: not colour coded text or styles") local black = GetNormalColour (1) for _, line in ipairs (message) do for _, style in ipairs (line) do if style.length == nil then style.length = #(style.text) end -- if if style.backcolour == black then style.backcolour = nil end -- if end -- for end -- for return message end -- ToMultilineStyles -- A line of styles cut at a pattern (spaces if not given): -- {{styles}, {separator styles}, {styles}, ...} function partition_boundaries (styles, separator_pattern) separator_pattern = separator_pattern or "%s+" local partitions = {} local last_text = nil local cur_partition = {} for _, style in ipairs (styles) do for _, text in ipairs (split (style.text, separator_pattern, true)) do if last_text then local last_endswith = last_text:match (separator_pattern .. "$") local this_startswith = text:match ("^" .. separator_pattern) if last_endswith ~= this_startswith then if #cur_partition == 0 then cur_partition = { { text = "", length = 0 } } end -- if table.insert (partitions, cur_partition) cur_partition = {} end -- if end -- if local length = #text if length > 0 then table.insert (cur_partition, { text = text, length = length, bold = style.bold, backcolour = style.backcolour, textcolour = style.textcolour, code = style.code, bgcode = style.bgcode }) end -- if last_text = text end -- for end -- for if #cur_partition == 0 then cur_partition = { { text = "", length = 0 } } end -- if table.insert (partitions, cur_partition) return partitions end -- partition_boundaries -- a line of styles split at a separator: {{styles}, {styles}, ...} function split_boundaries (styles, separator) local partitioned = partition_boundaries (styles, separator) local lines = {} for i = 1, #partitioned, 2 do table.insert (lines, partitioned [i]) end -- for return lines end -- split_boundaries ------------------------------------------------------------------------------- -- TextRect: a scrolling area of text ------------------------------------------------------------------------------- -- the colour to show selected text on: darker on light backgrounds, lighter on dark local function getHighlightColor (bg) local r, g, b = bg % 256, math.floor (bg / 256) % 256, math.floor (bg / 65536) % 256 if (math.max (r, g, b) + math.min (r, g, b)) / 2 >= 128 then return math.floor (b / 2) * 65536 + math.floor (g / 2) * 256 + math.floor (r / 2) end -- if return bg + 0x444444 end -- getHighlightColor TextRect = { hotspot_map = {} } TextRect_defaults = { raw_lines = {}, wrapped_lines = {}, max_lines = 1000, font_name = "Courier New", font_size = 10, start_line = 1, display_start_line = 1, end_line = 1, display_end_line = 1, num_raw_lines = 0, num_wrapped_lines = 0, keepscrolling = "", padding = 5, background_color = 0x000000, highlight_color = getHighlightColor (0x000000) } TextRect_mt = { __index = TextRect } function TextRect.new ( window, name, left, top, right, bottom, max_lines, scrollable, background_color, padding, font_name, font_size, external_scroll_handler, call_on_select, unselectable, uncopyable, no_url_hyperlinks, no_autowrap, menu_generator_function ) local new_tr = setmetatable (copytable.deep (TextRect_defaults), TextRect_mt) new_tr.id = "TextRect_" .. window .. "_" .. name new_tr.window = window new_tr.name = name new_tr:configure ( left, top, right, bottom, max_lines, scrollable, background_color, padding, font_name, font_size, external_scroll_handler, call_on_select, unselectable, uncopyable, no_url_hyperlinks, no_autowrap, menu_generator_function) return new_tr end -- TextRect.new function TextRect:set_bgcolor (bgcolor) if bgcolor ~= self.background_color then self.background_color = bgcolor or self.background_color self.highlight_color = getHighlightColor (self.background_color) end -- if end -- TextRect:set_bgcolor function TextRect:configure ( left, top, right, bottom, max_lines, scrollable, background_color, padding, font_name, font_size, external_scroll_handler, call_on_select, unselectable, uncopyable, no_url_hyperlinks, no_autowrap, menu_generator_function ) self:setExternalMenuFunction (menu_generator_function) self.scrollable = scrollable self.external_scroll_handler = external_scroll_handler self.call_on_select = call_on_select self.padding = padding or self.padding self.max_lines = max_lines or self.max_lines self.font_name = font_name or self.font_name self.font_size = font_size or self.font_size self.no_autowrap = no_autowrap self.unselectable = unselectable self.uncopyable = uncopyable self.no_url_hyperlinks = no_url_hyperlinks self:set_bgcolor (background_color) self:loadFont (self.font_name, self.font_size) self:setRect (left, top, right, bottom) end -- TextRect:configure function TextRect:loadFont (name, size) if (not self.font) or (name ~= self.font_name) or (size ~= self.font_size) then self.font = self.id .. "_font" self.font_bold = self.id .. "_font_bold" self.font_name = name or self.font_name self.font_size = size or self.font_size WindowFont (self.window, self.font, self.font_name, self.font_size, false, false, false, false, 0) WindowFont (self.window, self.font_bold, self.font_name, self.font_size, true, false, false, false, 0) self.line_height = WindowFontInfo (self.window, self.font, 1) end -- if if self.padded_height then self.rect_lines = math.floor (self.padded_height / self.line_height) end -- if end -- TextRect:loadFont -- the web links in some text: {{start, stop, text}, ...} function TextRect:findURLs (text) local URLs = {} local start, position = 0, 0 -- rex: MUSHclient's regular expressions (PCRE) local re = rex.new ("(?:https?://|mailto:)\\S*[\\w/=@#\\-\\?]") re:gmatch (text, function (link, _) start, position = string.find (text, link, position, true) table.insert (URLs, { start = start, stop = position, text = link }) end) return URLs end -- TextRect:findURLs function TextRect:textWidth (styles_or_color_coded_text) local width = 0 for _, styles in ipairs (ToMultilineStyles (styles_or_color_coded_text, nil, nil, true)) do width = math.max (width, self:styles_width (styles)) end -- for return width end -- TextRect:textWidth function TextRect:addText (message, hyperlinks) for _, message in ipairs (ToMultilineStyles (message, Theme.BODY_TEXT, nil, true)) do -- the links in it, to make clickable when it is drawn local links = {} if hyperlinks then links = copytable.deep (hyperlinks) end -- if if not self.no_url_hyperlinks then for _, v in ipairs (self:findURLs (strip_colours_from_styles (message))) do table.insert (links, v) end -- for end -- if -- the oldest line goes when the scrollback is full if self.num_raw_lines >= self.max_lines then table.remove (self.raw_lines, 1) self.num_raw_lines = self.num_raw_lines - 1 end -- if table.insert (self.raw_lines, { [1] = message, [2] = links }) self.num_raw_lines = self.num_raw_lines + 1 self:wrapLine (message, links, self.num_raw_lines) end -- for end -- TextRect:addText function TextRect:addColorLine (line) self:addText (line) end -- TextRect:addColorLine function TextRect:addStyles (styles) self:addText (styles) end -- TextRect:addStyles function TextRect:doUpdateCallbacks () if self.update_callbacks then for _, cb in ipairs (self.update_callbacks) do cb [2] (cb [1], self.display_start_line, self.rect_lines, self.num_wrapped_lines) end -- for end -- if end -- TextRect:doUpdateCallbacks function TextRect:cap_messages () if self.num_wrapped_lines >= self.max_lines then -- the history is full: the oldest line goes table.remove (self.wrapped_lines, 1) self.num_wrapped_lines = self.num_wrapped_lines - 1 self.start_line = math.max (1, self.start_line - 1) self.end_line = math.max (1, self.end_line - 1) self.display_start_line = math.max (1, self.display_start_line - 1) self.display_end_line = math.max (1, self.display_end_line - 1) if self.copy_start_line then self.copy_start_line = math.max (0, self.copy_start_line - 1) self.copy_end_line = math.max (self.copy_start_line, self.copy_end_line - 1) end -- if end -- if end -- TextRect:cap_messages function TextRect:lockstep () -- scroll down if viewing the bottom if self.end_line == self.num_wrapped_lines then if self.end_line >= self.rect_lines then self.start_line = self.start_line + 1 self.display_start_line = self.start_line end -- if self.end_line = self.end_line + 1 self.display_end_line = self.end_line end -- if end -- TextRect:lockstep function TextRect:wrapLine (stylerun, rawURLs, raw_index) local available = self.padded_width local line_styles = {} local beginning = true local length = 0 local styles = copytable.deep (stylerun) local urls = copytable.deep (rawURLs) local remove = table.remove local insert = table.insert local sub = string.sub local show_bold = (GetOption ("show_bold") == 1) -- keep taking styles and fitting them on the current line while #styles > 0 do local style = remove (styles, 1) local font = self.font if style.bold and show_bold then font = self.font_bold end -- if local t_width = WindowTextWidth (self.window, font, style.text) -- if it fits, the whole style goes in (and if there's no room for -- even a letter, it goes in anyway) if self.no_autowrap or (t_width <= available) or ((length == 0) and (WindowTextWidth (self.window, font, sub (style.text, 1, 1)) > available)) then if style.length > 0 then insert (line_styles, style) end -- if length = length + style.length available = available - t_width else -- split the style -- look for a space to break at local col = 2 local fit_col = nil while col < style.length do if sub (style.text, col, col) == " " then t_width = WindowTextWidth (self.window, font, sub (style.text, 1, col - 1)) if t_width > available then break else fit_col = col end -- if end -- if col = col + 1 end -- while if not fit_col then -- no space to break at if available == self.padded_width then -- at the start of the line -- as much as fits while col > 1 do t_width = WindowTextWidth (self.window, font, sub (style.text, 1, col)) if t_width <= available then fit_col = col break end -- if col = col - 1 end -- while end -- if end -- if if fit_col then -- the part that fits on this line; the rest goes back in the list local style_left = copytable.shallow (style) style.text = sub (style.text, 1, fit_col) style.length = fit_col style_left.text = sub (style_left.text, fit_col + 1) style_left.length = style_left.length - fit_col if style.length > 0 then insert (line_styles, style) end -- if insert (styles, 1, style_left) length = length + style.length else insert (styles, 1, style) -- all of it on the next line end -- if available = 0 -- now we need to wrap end -- if -- out of styles or out of room: a line of what we have if ((available <= 0) and not self.no_autowrap) or (#styles == 0) then self:cap_messages () local line_urls = {} while urls [1] and urls [1].stop <= length do insert (line_urls, remove (urls, 1)) end -- while if urls [1] and urls [1].start < length then local url = copytable.deep (urls [1]) url.stop = length + 1 urls [1].old = true insert (line_urls, url) end -- if for i, _ in ipairs (urls) do urls [i].start = urls [i].start - length urls [i].stop = urls [i].stop - length if urls [i].start <= 1 then urls [i].start = 1 end -- if end -- for self:lockstep () self.num_wrapped_lines = self.num_wrapped_lines + 1 local raw_index_delta = self.num_wrapped_lines - raw_index table.insert (self.wrapped_lines, { [1] = line_styles, [2] = beginning, [3] = line_urls, [4] = raw_index_delta }) available = self.padded_width line_styles = {} length = 0 beginning = false end -- if end -- while end -- TextRect:wrapLine function TextRect:clear (draw_after) draw_after = (draw_after == nil) or (draw_after == true) self.raw_lines = {} self.num_raw_lines = 0 self:reWrapLines () if draw_after then self:draw () end -- if end -- TextRect:clear function TextRect:reWrapLines () local raw_index = 0 -- wrapLine moves the start position, so keep our own local start_line = math.max (1, self.display_start_line or 1) if self.num_wrapped_lines ~= 0 and self.wrapped_lines [start_line] then raw_index = start_line - self.wrapped_lines [start_line] [4] end -- if self.wrapped_lines = {} self.num_wrapped_lines = 0 for i, line in ipairs (self.raw_lines) do if i == raw_index then start_line = self.num_wrapped_lines + 1 end -- if self:wrapLine (line [1], line [2], i) end -- for self.start_line = start_line self.start_line, self.end_line = self:snapToBottom () self.display_start_line = self.start_line self.display_end_line = self.end_line end -- TextRect:reWrapLines function TextRect:drawLine (line, styles, selection_start, selection_end, utf8, show_bold) local left = self.padded_left local top = self.padded_top + (line * self.line_height) + 1 -- behind the selection if (selection_start ~= nil) and (selection_end ~= nil) then WindowRectOp (self.window, miniwin.rect_fill, selection_start, top, selection_end - 1, top + self.line_height, self.highlight_color) end -- if for _, v in ipairs (styles) do local t = v.text local is_utf = utf8 and utils.utf8valid (t) local font = self.font if show_bold and v.bold then font = self.font_bold end -- if -- the text's own background, around the selection if v.backcolour and (v.backcolour ~= 0) then local bgwidth = WindowTextWidth (self.window, font, t, is_utf) local bgleft = left local bgright = left + bgwidth if (selection_start ~= nil) and (selection_end ~= nil) then if (bgleft <= selection_start) and (bgright >= selection_end) then WindowRectOp (self.window, miniwin.rect_fill, bgleft, top, math.min (self.padded_right, selection_start), math.min (self.padded_bottom, top + self.line_height), v.backcolour) bgleft = selection_end else if (bgleft >= selection_start) and (bgleft <= selection_end) then bgleft = selection_end end -- if if (bgright >= selection_start) and (bgright <= selection_end) then bgright = selection_start end -- if end -- if end -- if if bgleft < bgright then WindowRectOp (self.window, miniwin.rect_fill, bgleft, top, math.min (self.padded_right, bgright), math.min (self.padded_bottom, top + self.line_height), v.backcolour) end -- if end -- if left = left + WindowText (self.window, font, t, left, top, self.padded_right, self.padded_bottom, v.textcolour, is_utf) end -- for end -- TextRect:drawLine function TextRect:styles_width (styles, show_bold) if show_bold == nil then show_bold = (GetOption ("show_bold") == 1) end -- if return StylesWidth (self.window, self.font, self.font_bold, styles, show_bold) end -- TextRect:styles_width function TextRect:draw (cleanup_first, inside_callback) if cleanup_first ~= false then -- default true self:_deleteHyperlinks () end -- if if not self.area_hotspot then self:initArea () end -- if WindowRectOp (self.window, miniwin.rect_fill, self.left, self.top, self.right, self.bottom + 1, self.background_color) self.display_start_line, self.display_end_line = self:snapToBottom () if self.num_wrapped_lines >= 1 then local show_bold = (GetOption ("show_bold") == 1) local utf8 = (GetOption ("utf_8") == 1) for count = self.display_start_line, self.display_end_line do local ax, zx = nil, nil local line_styles = self.wrapped_lines [count] [1] if self.keepscrolling == "" then -- clickable links for _, url_part in ipairs (self.wrapped_lines [count] [3]) do local left = self.padded_left + self:styles_width (TruncateStyles (line_styles, 0, url_part.start - 1), show_bold) local right = left + self:styles_width (TruncateStyles (line_styles, url_part.start, url_part.stop), show_bold) local top = self.padded_top + ((count - self.display_start_line) * self.line_height) - 1 local bottom = top + self.line_height local link_id = self:generateHotspotID (table.concat ({ url_part.text, " ", count, " ", url_part.start, " ", url_part.stop })) if not WindowHotspotInfo (self.window, link_id, 1) then self.hyperlinks [link_id] = url_part.text WindowAddHotspot (self.window, link_id, left, top, math.min (right, self.padded_right), bottom, "TextRect.linkHover", "TextRect.cancelLinkHover", "TextRect.clickUrl", "", "TextRect.mouseUp", url_part.label or ("Click to open:\n" .. url_part.text), miniwin.cursor_hand) if self.scrollable then WindowScrollwheelHandler (self.window, link_id, "TextRect.wheelMove") elseif self.external_scroll_handler then WindowScrollwheelHandler (self.window, link_id, self.external_scroll_handler) end -- if end -- if end -- for end -- if local line_length = self.padded_left + self:styles_width (line_styles, show_bold) -- the part of this line that is selected if self.copy_start_line ~= nil and self.copy_end_line ~= nil and count >= self.copy_start_line and count <= self.copy_end_line then ax = ((count == self.copy_start_line) and self.start_copying_x and math.min (self.start_copying_x, line_length)) or self.padded_left zx = math.min (self.padded_right, ((count == self.copy_end_line) and self.end_copying_x and math.min (self.end_copying_x, line_length)) or line_length) end -- if if ax == zx then ax, zx = nil, nil end -- if self:drawLine (count - self.display_start_line, line_styles, ax, zx, utf8, show_bold) end -- for end -- if if not inside_callback then self:doUpdateCallbacks () end -- if self:underline_hyperlinks () self:draw_scrollback_marker () end -- TextRect:draw function TextRect:draw_scrollback_marker () if self.display_end_line < self.num_wrapped_lines then local text = "Scroll down for more" local width = WindowTextWidth (self.window, self.font, text) local left = self.right - width - 15 local top = self.bottom - WindowFontInfo (self.window, self.font, 1) + 1 WindowCircleOp (self.window, miniwin.circle_rectangle, left, top, self.right - 5, self.bottom + 2, Theme.THREE_D_SOFTSHADOW, miniwin.pen_solid, 1, Theme.THREE_D_GRADIENT_FIRST, miniwin.brush_solid) WindowText (self.window, self.font, text, left + 5, top, self.right - 3, self.bottom, Theme.THREE_D_SURFACE_DETAIL, false) end -- if end -- TextRect:draw_scrollback_marker function TextRect:addUpdateCallback (object, callback) self.update_callbacks = self.update_callbacks or {} table.insert (self.update_callbacks, { object, callback }) end -- TextRect:addUpdateCallback function TextRect:snapToBottom () local start_line = math.max (1, math.min (self.start_line, self.num_wrapped_lines - self.rect_lines + 1)) local end_line = math.max (1, math.min (start_line + self.rect_lines - 1, self.num_wrapped_lines)) return start_line, end_line end -- TextRect:snapToBottom function TextRect:setRect (left, top, right, bottom) self.left = left self.top = top self.right = right self.bottom = bottom + 1 self.width = right - left self.height = bottom - top + 1 self.padded_left = self.left + self.padding self.padded_top = self.top + self.padding self.padded_right = self.right - self.padding self.padded_bottom = self.bottom - self.padding self.padded_width = self.padded_right - self.padded_left self.padded_height = self.padded_bottom - self.padded_top if self.area_hotspot then WindowMoveHotspot (self.window, self.area_hotspot, self.left, self.top, self.right, self.bottom) end -- if if self.line_height then -- a third of a line extra makes resizing a bit more comfortable self.rect_lines = math.max (1, math.floor ((self.padded_height + (self.line_height / 3)) / self.line_height)) end -- if end -- TextRect:setRect function TextRect:getScroll () return self.start_line end -- TextRect:getScroll function TextRect:setScroll (new_pos, no_draw_after) self.start_line = math.max (1, math.min (new_pos, self.num_wrapped_lines - self.rect_lines + 1)) self.end_line = math.min (self.start_line + self.rect_lines - 1, self.num_wrapped_lines) self.display_start_line = self.start_line self.display_end_line = self.end_line if not no_draw_after then self:draw (true, true) end -- if end -- TextRect:setScroll -- scroll a line at a time while the mouse is dragged above or below the text function TextRect:scroll (dragging) wait.make (function () while self.keepscrolling == "up" or self.keepscrolling == "down" do if self.keepscrolling == "up" then if self.start_line > 1 then self.start_line = self.start_line - 1 self.end_line = self.end_line - 1 self.display_start_line = self.start_line self.display_end_line = self.end_line end -- if elseif self.end_line < self.num_wrapped_lines then self.start_line = self.start_line + 1 self.end_line = self.end_line + 1 self.display_start_line = self.start_line self.display_end_line = self.end_line end -- if if dragging then self:updateSelect () else self:draw (false) end -- if repaint () wait.time (0.01) end -- while end) end -- TextRect:scroll function TextRect:initArea () -- selecting, the right-click menu and scrolling self.area_hotspot = self:generateHotspotID ("textarea") if (not self.unselectable) or self.scrollable or self.external_scroll_handler then if self.unselectable then WindowAddHotspot (self.window, self.area_hotspot, self.left, self.top, self.right, self.top + self.height, "", "", "", "", "TextRect.mouseUp", "", miniwin.cursor_arrow, 0) else WindowAddHotspot (self.window, self.area_hotspot, self.left, self.top, self.right, self.top + self.height, "", "", "TextRect.mouseDown", "TextRect.cancelMouseDown", "TextRect.mouseUp", "", miniwin.cursor_ibeam, 0) WindowDragHandler (self.window, self.area_hotspot, "TextRect.dragMove", "TextRect.dragRelease", 0x10) end -- if if self.scrollable then WindowScrollwheelHandler (self.window, self.area_hotspot, "TextRect.wheelMove") elseif self.external_scroll_handler then WindowScrollwheelHandler (self.window, self.area_hotspot, self.external_scroll_handler) end -- if end -- if self.hyperlinks = {} end -- TextRect:initArea function TextRect:_deleteHyperlinks () if self.hyperlinks then for k, _ in pairs (self.hyperlinks) do WindowDeleteHotspot (self.window, k) TextRect.hotspot_map [k] = nil end -- for end -- if self.hyperlinks = {} end -- TextRect:_deleteHyperlinks function TextRect:unInit () if self.area_hotspot then TextRect.hotspot_map [self.area_hotspot] = nil WindowDeleteHotspot (self.window, self.area_hotspot) self.area_hotspot = nil end -- if self:_deleteHyperlinks () end -- TextRect:unInit -- the word (or character, or line) under target_x function TextRect:get_target_bounds (separator_pattern, line_number, target_x, partition_cache_key) target_x = math.min (math.max (target_x, self.padded_left), self.padded_right) local line = self.wrapped_lines [line_number] if not line then return end -- if local line_styles = line [1] local show_bold = (GetOption ("show_bold") == 1) if separator_pattern then if line_styles ~= self.last_partitioned_styles [partition_cache_key] then self.last_partitioned_styles [partition_cache_key] = line_styles self.last_partitioned_line_sections [partition_cache_key] = partition_boundaries (line_styles, separator_pattern) end -- if else self.last_partitioned_line_sections [partition_cache_key] = { line_styles } end -- if local section_start = self.padded_left local section_end = self.padded_left local start_pos = 0 local end_pos = 0 for _, section in ipairs (self.last_partitioned_line_sections [partition_cache_key]) do local section_size = 0 local section_length = 0 for _, style in ipairs (section) do local font = self.font if show_bold and style.bold then font = self.font_bold end -- if section_size = section_size + WindowTextWidth (self.window, font, style.text) section_length = section_length + style.length end -- for section_end = section_start + section_size end_pos = start_pos + section_length if (section_start <= target_x) and (section_end >= target_x) then break end -- if section_start = section_end start_pos = end_pos end -- for return start_pos, end_pos, section_start, section_end end -- TextRect:get_target_bounds function TextRect.mouseDown (flags, hotspot_id) if bit.band (flags, miniwin.hotspot_got_lh_mouse) == 0 then return -- just the left button end -- if local tr = TextRect.hotspot_map [hotspot_id] if not tr then return end -- if tr.temp_start_copying_x = WindowInfo (tr.window, 14) tr.copy_start_windowline = math.floor ((WindowInfo (tr.window, 15) - tr.top) / tr.line_height) tr.temp_start_line = tr.copy_start_windowline + tr.start_line local now = utils.timer () tr.num_clicks = tr.num_clicks or 1 tr.last_partitioned_styles = {} tr.last_partitioned_line_sections = {} if tr.last_click_time and ((now - tr.last_click_time) < 0.4) then -- double click: a word; triple click: the line tr.num_clicks = tr.num_clicks + 1 tr.separator_pattern = "[^%w%-]+" if tr.num_clicks ~= 2 then tr.separator_pattern = nil tr.num_clicks = 1 end -- if local start_pos, end_pos = tr:get_target_bounds (tr.separator_pattern, tr.temp_start_line, tr.temp_start_copying_x, "start") tr:set_selection (tr.temp_start_line, tr.temp_start_line, start_pos, end_pos) else tr.separator_pattern = "." tr.num_clicks = 1 tr:set_selection (nil, nil, nil, nil) end -- if tr.last_click_time = now tr:draw (false) if tr.call_on_select then tr.call_on_select (tr.copy_start_line, tr.copy_end_line, tr.start_copying_pos, tr.end_copying_pos, tr.start_copying_x, tr.end_copying_x) end -- if repaint () end -- TextRect.mouseDown function TextRect.dragMove (flags, hotspot_id) if bit.band (flags, miniwin.hotspot_got_lh_mouse) == 0 then return end -- if local tr = TextRect.hotspot_map [hotspot_id] if not tr or tr.num_wrapped_lines == 0 then return end -- if tr:updateSelect () if tr.scrollable then -- scroll if the mouse goes above or below the text if tr.end_copying_y < tr.top then if tr.keepscrolling ~= "up" then tr.keepscrolling = "up" tr:scroll (true) end -- if return elseif tr.end_copying_y > tr.bottom then if tr.keepscrolling ~= "down" then tr.keepscrolling = "down" tr:scroll (true) end -- if return else tr.keepscrolling = "" end -- if end -- if repaint () end -- TextRect.dragMove function TextRect.dragRelease (flags, hotspot_id) local tr = TextRect.hotspot_map [hotspot_id] if tr and tr.call_on_select then tr.call_on_select (tr.copy_start_line, tr.copy_end_line, tr.start_copying_pos, tr.end_copying_pos, tr.start_copying_x, tr.end_copying_x) end -- if repaint () end -- TextRect.dragRelease function TextRect.mouseUp (flags, hotspot_id) local tr = TextRect.hotspot_map [hotspot_id] if not tr then return end -- if tr.keepscrolling = "" tr:draw () if bit.band (flags, miniwin.hotspot_got_rh_mouse) ~= 0 then tr:rightClickMenu (hotspot_id) end -- if repaint () return true end -- TextRect.mouseUp function TextRect.cancelMouseDown (flags, hotspot_id) local tr = TextRect.hotspot_map [hotspot_id] if not tr then return end -- if tr.keepscrolling = "" tr:draw () repaint () end -- TextRect.cancelMouseDown function TextRect:set_selection (start_line, end_line, start_pos, end_pos) if start_line and end_line and start_pos and end_pos and ((start_line >= 1) or (end_line >= 1)) and ((start_line <= self.num_wrapped_lines) or (end_line <= self.num_wrapped_lines)) then if start_line < 1 and end_line >= 1 then start_line = 1 start_pos = 0 end -- if if end_line > self.num_wrapped_lines and start_line <= self.num_wrapped_lines then end_line = self.num_wrapped_lines end_pos = #strip_colours_from_styles (self.wrapped_lines [end_line] [1]) end -- if self.start_copying_pos = start_pos self.end_copying_pos = end_pos self.copy_end_line = end_line self.copy_start_line = start_line self.start_copying_x = self.padded_left + self:styles_width (TruncateStyles (self.wrapped_lines [start_line] [1], 0, start_pos)) self.end_copying_x = self.padded_left + self:styles_width (TruncateStyles (self.wrapped_lines [end_line] [1], 0, end_pos)) else self.start_copying_pos = nil self.end_copying_pos = nil self.copy_end_line = nil self.copy_start_line = nil self.start_copying_x = nil self.end_copying_x = nil end -- if end -- TextRect:set_selection function TextRect:updateSelect () self.copy_start_line = self.temp_start_line self.end_copying_y = WindowInfo (self.window, 18) - WindowInfo (self.window, 11) self.copy_end_windowline = math.floor ((self.end_copying_y - self.top) / self.line_height) self.copy_end_line = self.copy_end_windowline + self.start_line local cursor_x = WindowInfo (self.window, 17) - WindowInfo (self.window, 10) self.start_copying_x = self.temp_start_copying_x self.end_copying_x = math.max (self.left, math.min (self.right, cursor_x)) -- selecting backwards: swap the ends if self.copy_end_line < self.copy_start_line then self.copy_start_line, self.copy_end_line = self.copy_end_line, self.copy_start_line self.start_copying_x, self.end_copying_x = self.end_copying_x, self.start_copying_x elseif (self.copy_end_line == self.copy_start_line) and (self.end_copying_x < self.start_copying_x) then self.start_copying_x, self.end_copying_x = self.end_copying_x, self.start_copying_x end -- if -- off the top or bottom: whole lines if self.copy_end_line > self.num_wrapped_lines then self.end_copying_x = self.right end -- if if self.copy_start_line < 1 then self.start_copying_x = self.padded_left end -- if self.copy_start_line = math.max (1, math.min (self.num_wrapped_lines, self.copy_start_line)) self.copy_end_line = math.max (1, math.min (self.num_wrapped_lines, self.copy_end_line)) -- from character (or word, or line) boundaries local start_pos, _, start_x = self:get_target_bounds (self.separator_pattern, self.copy_start_line, self.start_copying_x, "start") self.start_copying_pos = start_pos self.start_copying_x = start_x local _, end_pos, _, end_x = self:get_target_bounds (self.separator_pattern, self.copy_end_line, self.end_copying_x, "end") self.end_copying_pos = end_pos self.end_copying_x = end_x if (self.copy_start_line == self.copy_end_line) and (self.start_copying_pos == self.end_copying_pos) then self:set_selection (nil, nil, nil, nil) end -- if self:draw (false) if self.call_on_select then self.call_on_select (self.copy_start_line, self.copy_end_line, self.start_copying_pos, self.end_copying_pos, self.start_copying_x, self.end_copying_x) end -- if end -- TextRect:updateSelect function TextRect.wheelMove (flags, hotspot_id) local tr = TextRect.hotspot_map [hotspot_id] if not tr then return end -- if local delta = math.ceil (bit.shr (flags, 16) / 3) local line_delta = math.max (1, math.ceil (delta / tr.line_height)) tr.wheeling = true if bit.band (flags, miniwin.wheel_scroll_back) ~= 0 then -- down if tr.start_line < tr.num_wrapped_lines - tr.rect_lines + 1 then tr.start_line = math.max (1, math.min (tr.num_wrapped_lines - tr.rect_lines + 1, tr.start_line + line_delta)) tr.end_line = math.min (tr.num_wrapped_lines, tr.start_line + tr.rect_lines - 1) tr.display_start_line = tr.start_line tr.display_end_line = tr.end_line tr:draw () repaint () end -- if elseif tr.start_line > 1 then -- up tr.start_line = math.max (1, tr.start_line - line_delta) tr.end_line = math.min (tr.num_wrapped_lines, tr.start_line + tr.rect_lines - 1) tr.display_start_line = tr.start_line tr.display_end_line = tr.end_line tr:draw () repaint () end -- if tr.wheeling = false end -- TextRect.wheelMove function TextRect:underline_hyperlinks () local hotspot_id = WindowInfo (self.window, 19) if hotspot_id then local url = self.hyperlinks [hotspot_id] if url then for _, v in ipairs (WindowHotspotList (self.window) or {}) do if string.find (v, url, 1, true) then local left = WindowHotspotInfo (self.window, v, 1) local right = WindowHotspotInfo (self.window, v, 3) local bottom = WindowHotspotInfo (self.window, v, 4) + 1 WindowLine (self.window, left, bottom, right, bottom, 0xffffff, miniwin.pen_solid, 1) end -- if end -- for end -- if end -- if end -- TextRect:underline_hyperlinks function TextRect.linkHover (flags, hotspot_id) local tr = TextRect.hotspot_map [hotspot_id] if not tr or tr.wheeling or (GetOption ("underline_hyperlinks") == 0) then return end -- if tr:underline_hyperlinks () repaint () end -- TextRect.linkHover function TextRect.cancelLinkHover (flags, hotspot_id) local tr = TextRect.hotspot_map [hotspot_id] if not tr then return end -- if local url = tr.hyperlinks [hotspot_id] if not url or not string.find (WindowInfo (tr.window, 19) or "", url, 1, true) then tr:draw (false) repaint () end -- if end -- TextRect.cancelLinkHover function TextRect:setExternalMenuFunction (menu_generator_function) self.external_menu_generator = menu_generator_function end -- TextRect:setExternalMenuFunction function TextRect:rightClickMenu (hotspot_id) local menu_text = {} local menu_functions = {} local function extend_options (external_string, external_functions) if external_string:sub (1, 1) == "!" then external_string = external_string:sub (2) end -- if if #menu_text > 0 then table.insert (menu_text, "-") end -- if table.insert (menu_text, external_string) for _, v in ipairs (external_functions) do table.insert (menu_functions, v) end -- for end -- extend_options if hotspot_id and self.hyperlinks [hotspot_id] and (#(self:findURLs (self.hyperlinks [hotspot_id])) > 0) then table.insert (menu_text, "Browse URL: " .. self.hyperlinks [hotspot_id]) table.insert (menu_text, "Copy URL to Clipboard") table.insert (menu_text, "-") table.insert (menu_functions, TextRect.browseUrl) table.insert (menu_functions, TextRect.copyUrl) end -- if if not self.uncopyable then if (self.copy_start_line ~= nil) and (self.copy_end_line ~= nil) then table.insert (menu_text, "Copy Selected") table.insert (menu_text, "Copy Selected Without Colors") table.insert (menu_functions, TextRect.copy) table.insert (menu_functions, TextRect.copyPlain) end -- if table.insert (menu_text, "Copy All") table.insert (menu_functions, TextRect.copyFull) table.insert (menu_text, "Copy All Without Colors") table.insert (menu_functions, TextRect.copyFullPlain) end -- if if self.external_menu_generator then local selected = (self.copy_start_line ~= nil) and (self.copy_end_line ~= nil) and self:selected_text (false) or nil local external_string, external_functions = self.external_menu_generator (selected) extend_options (external_string, external_functions) end -- if local result = tonumber (WindowMenu (self.window, WindowInfo (self.window, 14), -- x WindowInfo (self.window, 15), -- y "!" .. table.concat (menu_text, "|"))) if result and menu_functions [result] then menu_functions [result] (self, hotspot_id, result) end -- if end -- TextRect:rightClickMenu function TextRect:browseUrl (hotspot_id) local url = self.hyperlinks [hotspot_id] if url and #(self:findURLs (url)) > 0 then OpenBrowser (url) end -- if end -- TextRect:browseUrl function TextRect.clickUrl (flags, hotspot_id) if bit.band (flags, miniwin.hotspot_got_rh_mouse) ~= 0 then -- just the left button return end -- if local tr = TextRect.hotspot_map [hotspot_id] if tr then tr:browseUrl (hotspot_id) end -- if end -- TextRect.clickUrl function TextRect:serializeContents () require "serialize" return serialize.save_simple ({ raw_lines = self.raw_lines, start_line = self.start_line }) end -- TextRect:serializeContents function TextRect:deserializeContents (contents, no_draw_after) if (type (contents) == "string") and (contents ~= "") then local f = loadstring ("return " .. contents) local ok, t = pcall (f or function () end) if ok and (type (t) == "table") and t.raw_lines and t.start_line then self.raw_lines = t.raw_lines self.num_raw_lines = #self.raw_lines self:reWrapLines () self:setScroll (t.start_line, no_draw_after) end -- if end -- if end -- TextRect:deserializeContents function TextRect:copyAndNotify (text) ColourNote ("cyan", "", "--------------------Copied to clipboard--------------------") ColourNote ("yellow", "", text) ColourNote ("cyan", "", "-----------------------------------------------------------") SetClipboard (text) end -- TextRect:copyAndNotify function TextRect:copyUrl (hotspot_id) local url = self.hyperlinks [hotspot_id] if url then self:copyAndNotify (url) end -- if end -- TextRect:copyUrl function TextRect:copyPlain () self:copyAndNotify (self:selected_text (false)) end -- TextRect:copyPlain function TextRect:copy () self:copyAndNotify (self:selected_text (true)) end -- TextRect:copy -- the selected text, with or without colour codes; wrapped lines are put -- back together function TextRect:selected_text (with_colors) local s_text = {} local current_message = {} local function store_message () if with_colors then table.insert (s_text, StylesToColours (current_message)) else table.insert (s_text, strip_colours_from_styles (current_message)) end -- if current_message = {} end -- store_message for copy_line = self.copy_start_line, self.copy_end_line do if self.wrapped_lines [copy_line] ~= nil then local startpos = 0 local endpos = 99999 if copy_line == self.copy_start_line then startpos = self.start_copying_pos end -- if if copy_line == self.copy_end_line then endpos = self.end_copying_pos end -- if if (endpos ~= startpos) or (self.copy_start_line ~= self.copy_end_line) then -- a new message: keep the one before if (copy_line ~= self.copy_start_line) and self.wrapped_lines [copy_line] [2] then store_message () end -- if local line_styles = TruncateStyles (self.wrapped_lines [copy_line] [1], startpos + 1, endpos) if line_styles then for _, s in ipairs (line_styles) do table.insert (current_message, s) end -- for else table.insert (s_text, "") end -- if end -- if end -- if end -- for store_message () return table.concat (s_text, "\n") end -- TextRect:selected_text function TextRect:getStyles () local t = {} for _, line in ipairs (self.raw_lines) do table.insert (t, line [1]) end -- for return t end -- TextRect:getStyles function TextRect:getText () local t = {} for _, line in ipairs (self.raw_lines) do table.insert (t, StylesToColours (line [1])) end -- for return table.concat (t, "\n") end -- TextRect:getText function TextRect:copyFull () SetClipboard (self:getText ()) ColourNote ("yellow", "", "All text copied to clipboard ", "limegreen", "", "with", "yellow", "", " colors.") end -- TextRect:copyFull function TextRect:copyFullPlain () SetClipboard (strip_colours (self:getText ())) ColourNote ("yellow", "", "All text copied to clipboard ", "red", "", "without", "yellow", "", " colors.") end -- TextRect:copyFullPlain function TextRect:generateHotspotID (id) local hotspot_id = self.id .. "_hotspot_" .. id TextRect.hotspot_map [hotspot_id] = self return hotspot_id end -- TextRect:generateHotspotID ------------------------------------------------------------------------------- -- ScrollBar ------------------------------------------------------------------------------- ScrollBar = { hotspot_map = {} } ScrollBar_defaults = { total_steps = 1, visible_steps = 1, step = 1 } ScrollBar_mt = { __index = ScrollBar } function ScrollBar.new (window, name, left, top, right, bottom, min_thumb_size) local new_sb = setmetatable (copytable.deep (ScrollBar_defaults), ScrollBar_mt) new_sb.id = "ScrollBar_" .. window .. "_" .. name new_sb.name = name new_sb.window = window new_sb.left = left new_sb.top = top new_sb.right = right new_sb.bottom = bottom new_sb.width = right - left new_sb.height = bottom - top new_sb.min_thumb_size = min_thumb_size or 10 return new_sb end -- ScrollBar.new function ScrollBar:initButtons () WindowAddHotspot (self.window, self:generateHotspotID ("up"), self.left, self.top, self.right, self.top + self.width, "", "", "ScrollBar.mouseDownUpArrow", "ScrollBar.cancelMouseDown", "ScrollBar.mouseUp", "", miniwin.cursor_hand, 0) WindowAddHotspot (self.window, self:generateHotspotID ("down"), self.left, self.bottom - self.width, self.right, self.bottom, "", "", "ScrollBar.mouseDownDownArrow", "ScrollBar.cancelMouseDown", "ScrollBar.mouseUp", "", miniwin.cursor_hand, 0) end -- ScrollBar:initButtons function ScrollBar:_delHotspot (key) local id = self:generateHotspotID (key) ScrollBar.hotspot_map [id] = nil WindowDeleteHotspot (self.window, id) end -- ScrollBar:_delHotspot function ScrollBar:unInit () self.has_hotspots = false self:_delHotspot ("up") self:_delHotspot ("down") self:_delHotspot ("scroller") end -- ScrollBar:unInit function ScrollBar:setRect (left, top, right, bottom) if (self.left ~= left) or (self.top ~= top) or (self.right ~= right) or (self.bottom ~= bottom) then self.left = left self.top = top self.right = right self.bottom = bottom self.width = right - left self.height = bottom - top WindowMoveHotspot (self.window, self:generateHotspotID ("up"), self.left, self.top, self.right, self.top + self.width) WindowMoveHotspot (self.window, self:generateHotspotID ("down"), self.left, self.bottom - self.width, self.right, self.bottom) end -- if end -- ScrollBar:setRect function ScrollBar:doUpdateCallbacks () if self.update_callbacks then for _, cb in ipairs (self.update_callbacks) do if cb [1] then cb [2] (cb [1], self.step) else cb [2] (self.step) end -- if end -- for end -- if end -- ScrollBar:doUpdateCallbacks function ScrollBar:setScroll (step, visible_steps, total_steps, no_draw_after) self.step = step or self.step self.visible_steps = visible_steps or self.visible_steps self.total_steps = total_steps or self.total_steps if not no_draw_after then self:draw (true) end -- if end -- ScrollBar:setScroll -- the points of an arrow on a button local function arrow (left, top, width, up, pressed) local mid_x = (width - 2) / 2 local shift = pressed and 3 or 1 local tip, base if up then tip = top + math.ceil (width / 4 + 0.5) + shift base = top + round_banker (width / 2) + shift else tip = top + width - math.ceil (width / 4 + 0.5) - 2 + shift base = top + width - round_banker (width / 2) - 2 + shift end -- if return string.format ("%i,%i,%i,%i,%i,%i,%i,%i", left + math.floor (mid_x) + shift, tip, left + math.floor (mid_x) - math.floor (mid_x / 2) + shift, base, left + math.ceil (mid_x) + math.floor (mid_x / 2) + shift, base, left + math.ceil (mid_x) + shift, tip) end -- arrow function ScrollBar:draw (inside_callback) if not self.has_hotspots then self:initButtons () end -- if -- the track WindowCircleOp (self.window, miniwin.circle_rectangle, self.left, self.top + self.width + 1, self.right + 1, self.bottom - self.width, Theme.SCROLL_TRACK_COLOR1, miniwin.pen_solid, 1, Theme.SCROLL_TRACK_COLOR2, Theme.VERTICAL_TRACK_BRUSH) -- the up button (its arrow greyed out at the top) local pressed = (self.keepscrolling == "up") Theme.Draw3DRect (self.window, self.left, self.top, self.right, self.top + self.width, pressed) local color = (self.step == 1) and Theme.THREE_D_SOFTSHADOW or Theme.THREE_D_SURFACE_DETAIL WindowPolygon (self.window, arrow (self.left, self.top, self.width, true, pressed), color, miniwin.pen_solid + miniwin.pen_join_miter, 1, color, miniwin.brush_solid, true, false) -- the down button pressed = (self.keepscrolling == "down") Theme.Draw3DRect (self.window, self.left, self.bottom - self.width, self.right, self.bottom, pressed) local slots = math.max (0, self.total_steps - self.visible_steps) color = (self.step > slots) and Theme.THREE_D_SOFTSHADOW or Theme.THREE_D_SURFACE_DETAIL WindowPolygon (self.window, arrow (self.left, self.bottom - self.width, self.width, false, pressed), color, miniwin.pen_solid + miniwin.pen_join_miter, 1, color, miniwin.brush_solid, true, false) -- the thumb local position local scroll_height = self.height - (2 * self.width) - 2 if slots ~= 0 then self.size = math.min (scroll_height, math.max (self.min_thumb_size, scroll_height - slots)) local available_space = scroll_height - self.size local space_per_step = available_space / slots position = self.top + self.width + math.ceil (space_per_step * self.step) if position > self.bottom - self.width - self.size then position = self.bottom - self.width - self.size end -- if else position = self.top + self.width + 1 self.size = scroll_height end -- if if not self.has_hotspots then WindowAddHotspot (self.window, self:generateHotspotID ("scroller"), self.left, position, self.right, position + self.size, "", "", "ScrollBar.mouseDown", "", "ScrollBar.mouseUp", "", miniwin.cursor_hand, 0) WindowDragHandler (self.window, self:generateHotspotID ("scroller"), "ScrollBar.dragMove", "ScrollBar.dragRelease", 0) else WindowMoveHotspot (self.window, self:generateHotspotID ("scroller"), self.left, position, self.right, position + self.size) end -- if Theme.Draw3DRect (self.window, self.left, position, self.right, position + self.size, false) self.has_hotspots = true if not inside_callback then self:doUpdateCallbacks () end -- if end -- ScrollBar:draw function ScrollBar.mouseDown (flags, hotspot_id) local sb = ScrollBar.hotspot_map [hotspot_id] if not sb then return end -- if sb.start_pos = WindowHotspotInfo (sb.window, hotspot_id, 2) - WindowInfo (sb.window, 15) sb.dragging_scrollbar = true end -- ScrollBar.mouseDown function ScrollBar.mouseDownDownArrow (flags, hotspot_id) local sb = ScrollBar.hotspot_map [hotspot_id] if sb then sb.keepscrolling = "down" sb:scroll () end -- if end -- ScrollBar.mouseDownDownArrow function ScrollBar.mouseDownUpArrow (flags, hotspot_id) local sb = ScrollBar.hotspot_map [hotspot_id] if sb then sb.keepscrolling = "up" sb:scroll () end -- if end -- ScrollBar.mouseDownUpArrow function ScrollBar:addUpdateCallback (object, callback) self.update_callbacks = self.update_callbacks or {} table.insert (self.update_callbacks, { object, callback }) end -- ScrollBar:addUpdateCallback function ScrollBar.dragMove (flags, hotspot_id) local sb = ScrollBar.hotspot_map [hotspot_id] if not sb or not sb.start_pos then return end -- if local mouse_y = WindowInfo (sb.window, 18) - WindowInfo (sb.window, 11) local top_coord = mouse_y + sb.start_pos local available_begin = sb.top + sb.width local available_end = sb.bottom - sb.width - sb.size local position = math.min (math.max (top_coord, available_begin), available_end) - available_begin sb.dragging_scrollbar = true local available_space = sb.height - (2 * sb.width) - sb.size if available_space > 0 and sb.total_steps > sb.visible_steps then local space_per_step = available_space / (sb.total_steps - sb.visible_steps) sb.step = math.floor (position / space_per_step) + 1 end -- if sb:draw () repaint () end -- ScrollBar.dragMove function ScrollBar.dragRelease (flags, hotspot_id) local sb = ScrollBar.hotspot_map [hotspot_id] if sb then sb.dragging_scrollbar = false sb:draw () repaint () end -- if end -- ScrollBar.dragRelease function ScrollBar.cancelMouseDown (flags, hotspot_id) local sb = ScrollBar.hotspot_map [hotspot_id] if sb then sb.keepscrolling = "" sb:draw () repaint () end -- if end -- ScrollBar.cancelMouseDown function ScrollBar.mouseUp (flags, hotspot_id) local sb = ScrollBar.hotspot_map [hotspot_id] if sb then sb.keepscrolling = "" sb:draw () repaint () end -- if return true end -- ScrollBar.mouseUp function ScrollBar:generateHotspotID (id) local hotspot_id = self.id .. "_hotspot_" .. id ScrollBar.hotspot_map [hotspot_id] = self return hotspot_id end -- ScrollBar:generateHotspotID -- a step at a time while an arrow button is held down function ScrollBar:scroll () wait.make (function () while self.keepscrolling == "up" or self.keepscrolling == "down" do if self.keepscrolling == "up" then if self.step > 1 then self.step = self.step - 1 end -- if elseif (self.step + self.visible_steps) <= self.total_steps then self.step = self.step + 1 end -- if self:draw () repaint () wait.time (0.01) end -- while end) end -- ScrollBar:scroll