|
DefineClass.XControl = { |
|
__parents = { "XWindow", "FXObject" }, |
|
|
|
properties = { |
|
{ category = "Interaction", id = "Enabled", editor = "bool", default = true, }, |
|
{ category = "Interaction", id = "Target", editor = "text", default = "", }, |
|
{ category = "FX", id = "FXMouseIn", editor = "text", default = "", }, |
|
{ category = "FX", id = "FXPress", editor = "text", default = "", }, |
|
{ category = "FX", id = "FXPressDisabled", editor = "text", default = "", }, |
|
{ category = "Visual", id = "FocusedBorderColor", name = "Focused border color", editor = "color", default = RGB(0, 0, 0), }, |
|
{ category = "Visual", id = "FocusedBackground", name = "Focused background", editor = "color", default = RGBA(0, 0, 0, 0), }, |
|
{ category = "Visual", id = "DisabledBorderColor", name = "Disabled border color", editor = "color", default = RGB(0, 0, 0), }, |
|
{ category = "Visual", id = "DisabledBackground", name = "Disabled background", editor = "color", default = RGBA(0, 0, 0, 0), }, |
|
|
|
{ category = "FX", id = "Particles", read_only = true, editor = "string_list", default = false, } |
|
}, |
|
enabled = true, |
|
IdNode = true, |
|
HandleMouse = true, |
|
particles = false, |
|
} |
|
|
|
|
|
|
|
DefineClass.UIParticleInstance = { |
|
__parents = {"PropertyObject"}, |
|
|
|
id = false, |
|
parsys_name = false, |
|
foreground = true, |
|
lifetime = -1, |
|
transfer_to_parent = false, |
|
stop_on_transfer = true, |
|
offset = point(0, 0), |
|
owner = false, |
|
delete_owner = false, |
|
halign = "middle", |
|
valign = "middle", |
|
keep_alive = false, |
|
polyline = false, |
|
params = false, |
|
dynamic_params = false, |
|
} |
|
|
|
local function align_position(alignment, rstart, rend) |
|
if alignment == "begin" then |
|
return rstart |
|
elseif alignment == "end" then |
|
return rend |
|
elseif alignment == "middle" then |
|
return (rstart + rend) / 2 |
|
else |
|
assert("Invalid alignment") |
|
return rstart |
|
end |
|
end |
|
|
|
local function calc_particle_origin(control, particle) |
|
local box = control.content_box |
|
local posx = align_position(particle.halign, box:minx(), box:maxx()) |
|
local posy = align_position(particle.valign, box:miny(), box:maxy()) |
|
return posx, posy |
|
end |
|
|
|
function UIParticleInstance:ApplyDynamicParams() |
|
local proto = self.parsys_name |
|
local dynamic_params = ParGetDynamicParams(proto) |
|
if not next(dynamic_params) then |
|
self.dynamic_params = nil |
|
return |
|
end |
|
self.dynamic_params = dynamic_params |
|
local set_value = self.SetParamDef |
|
for k, v in pairs(dynamic_params) do |
|
set_value(self, v, v.default_value) |
|
end |
|
end |
|
|
|
local UIParticleSetDynamicDataString = UIL.UIParticleSetDynamicDataString |
|
function UIParticleInstance:SetPointsAsPolyline(pts) |
|
self.polyline = pstr("") |
|
for _, pt in ipairs(pts or empty_table) do |
|
self.polyline:AppendVertex(pt) |
|
end |
|
UIParticleSetDynamicDataString(self.id, 0, self.polyline) |
|
end |
|
|
|
|
|
function UIParticleInstance:SetParam(param, value) |
|
local dynamic_params = self.dynamic_params |
|
local def = dynamic_params and rawget(dynamic_params, param) |
|
if def then |
|
self:SetParamDef(def, value) |
|
end |
|
end |
|
|
|
function UIParticleInstance:SetParamDef(def, value) |
|
local ptype = def.type |
|
if ptype == "number" then |
|
UIParticleSetDynamicDataString(self.id, def.index, value) |
|
elseif ptype == "color" then |
|
UIParticleSetDynamicDataString(self.id, def.index, value) |
|
elseif ptype == "point" then |
|
local x, y, z = value:xyz() |
|
local idx = def.index |
|
UIParticleSetDynamicDataString(self.id, idx, x) |
|
UIParticleSetDynamicDataString(self.id, idx + 1, y) |
|
UIParticleSetDynamicDataString(self.id, idx + 2, z or 0) |
|
elseif ptype == "bool" then |
|
UIParticleSetDynamicDataString(self.id, def.index, value and 1 or 0) |
|
end |
|
end |
|
|
|
function UIParticleInstance:UpdateBordersPolyline() |
|
local bbox = self.owner.box |
|
local pts = { |
|
point(bbox:minx(), bbox:miny()), |
|
point(bbox:maxx(), bbox:miny()), |
|
point(bbox:maxx(), bbox:maxy()), |
|
point(bbox:minx(), bbox:maxy()), |
|
} |
|
|
|
local x, y = calc_particle_origin(self.owner, self) |
|
local origin = point(x, y) |
|
for idx, _ in ipairs(pts) do |
|
local diff = (pts[idx] - origin) |
|
pts[idx] = point(diff:x() * guim, diff:y() * -guim) |
|
end |
|
|
|
self:SetPointsAsPolyline(pts) |
|
|
|
self:SetParam("width", bbox:sizex() * 1000) |
|
self:SetParam("height", bbox:sizey() * 1000) |
|
end |
|
|
|
local HasUIParticles = UIL.HasUIParticles |
|
local StopUIParticlesEmitter = UIL.StopUIParticlesEmitter |
|
local function ParticleLifetimeFunc(particle, lifetime) |
|
if lifetime >= 0 then |
|
Sleep(lifetime) |
|
StopUIParticlesEmitter(particle.id) |
|
end |
|
|
|
local last_tick_had_particles = true |
|
Sleep(1000) |
|
while true do |
|
local has_particles = HasUIParticles(particle.id) or particle.keep_alive |
|
if not has_particles and not last_tick_had_particles then |
|
break |
|
end |
|
last_tick_had_particles = has_particles |
|
Sleep(1000) |
|
end |
|
|
|
assert(particle.owner) |
|
particle.owner:KillParSystem(particle.id, "leave_lifetimethread") |
|
end |
|
|
|
function XControl:OnBoxChanged() |
|
for _, particle in ipairs(self.particles) do |
|
particle:UpdateBordersPolyline() |
|
end |
|
end |
|
|
|
function XControl:AddParSystem(id, name, instance) |
|
self.particles = self.particles or {} |
|
instance = instance or UIParticleInstance:new({}) |
|
|
|
assert(name) |
|
if not id then |
|
id = UIL.PlaceUIParticles(name) |
|
end |
|
assert(id) |
|
assert(instance.owner == false) |
|
instance.id = id |
|
instance.parsys_name = name |
|
instance.owner = self |
|
instance:ApplyDynamicParams() |
|
|
|
instance.lifetime_thread = CreateRealTimeThread(ParticleLifetimeFunc, instance, instance.lifetime) |
|
table.insert(self.particles, instance) |
|
|
|
self:Invalidate() |
|
instance:UpdateBordersPolyline() |
|
return id |
|
end |
|
|
|
function XControl:StopParticle(particle, force) |
|
if type(particle) ~= "table" then |
|
particle = table.find_value(self.particles, "id", particle) |
|
if not particle then return end |
|
end |
|
particle.keep_alive = false |
|
if force then |
|
self:KillParSystem(particle.id) |
|
else |
|
DeleteThread(particle.lifetime_thread) |
|
particle.lifetime_thread = CreateRealTimeThread(ParticleLifetimeFunc, particle, 0) |
|
end |
|
end |
|
|
|
function XControl:KillParticlesWithName(name) |
|
if not self.particles then return end |
|
for _, particle in ipairs(self.particles) do |
|
if particle.parsys_name == name then |
|
self:KillParSystem(particle.id) |
|
end |
|
end |
|
end |
|
|
|
function XControl:GetParticleName(id) |
|
if not self.particles then return end |
|
local particle = table.find_value(self.particles, "id", id) |
|
if not particle then return end |
|
return particle.parsys_name |
|
end |
|
|
|
function XControl:TransferParticleUp(particle) |
|
assert(table.find(self.particles, particle)) |
|
local parent = self.parent |
|
local top_level_end_of_life_window = self |
|
while parent and (parent.window_state ~= "open" or IsKindOf(parent, "XContentTemplate")) do |
|
top_level_end_of_life_window = parent |
|
parent = parent.parent |
|
end |
|
|
|
if not parent then return end |
|
|
|
|
|
|
|
|
|
|
|
|
|
local particle_holder = XControl:new({}, parent) |
|
|
|
|
|
|
|
particle_holder.particles = {} |
|
table.insert(particle_holder.particles, particle) |
|
particle.offset = particle.owner.content_box:min() - point(calc_particle_origin(particle_holder, particle)) + particle.offset |
|
particle.owner = particle_holder |
|
particle.delete_owner = true |
|
particle.foreground = true |
|
table.remove_value(self.particles, particle) |
|
if #self.particles == 0 then |
|
self.particles = false |
|
end |
|
end |
|
|
|
function XControl:KillParSystem(id, leave_lifetimethread) |
|
if not self.particles then return end |
|
|
|
local idx = table.find(self.particles, "id", id) |
|
assert(idx) |
|
local particle = self.particles[idx] |
|
assert(particle.owner == self) |
|
if not leave_lifetimethread then |
|
DeleteThread(particle.lifetime_thread) |
|
end |
|
UIL.DeleteUIParticles(particle.id) |
|
table.remove(self.particles, idx) |
|
if #self.particles == 0 then |
|
self.particles = false |
|
end |
|
if particle.delete_owner then |
|
self:delete() |
|
end |
|
particle.keep_alive = false |
|
self:Invalidate() |
|
end |
|
|
|
function XControl:HasParticle(id) |
|
if not self.particles then return false end |
|
if not table.find(self.particles, "id", id) then return false end |
|
return true |
|
end |
|
|
|
if Platform.developer then |
|
function XControl:DbgPlayFX(...) |
|
local index = self.particles and #(self.particles) or 1 |
|
self:PlayFX(...) |
|
if not self.particles then |
|
return |
|
end |
|
for i = index, #self.particles do |
|
local particle = self.particles[i] |
|
if particle.lifetime == -1 then |
|
particle.keep_alive = true |
|
end |
|
end |
|
end |
|
end |
|
|
|
function XControl:ParticlesOnDone() |
|
local particles = self.particles |
|
if particles then |
|
for i = #particles, 1, -1 do |
|
local particle = particles[i] |
|
if particle.transfer_to_parent and UIL.ShouldWaitForHasUIParticles(particle.id) then |
|
if particle.stop_on_transfer then |
|
self:StopParticle(particle) |
|
end |
|
self:TransferParticleUp(particle) |
|
else |
|
self:KillParSystem(particle.id) |
|
end |
|
end |
|
end |
|
end |
|
|
|
function XControl:Done() |
|
self:ParticlesOnDone() |
|
end |
|
|
|
function GetUIParticleAlignmentItems(horizontal) |
|
return { |
|
{ value = "begin", text = horizontal and "left" or "top" }, |
|
{ value = "middle", text = "center" }, |
|
{ value = "end", text = horizontal and "right" or "bottom" }, |
|
} |
|
end |
|
|
|
function XControl:DrawParticles(foreground) |
|
for key, particle in ipairs(self.particles) do |
|
if particle.foreground == foreground then |
|
local scale = self.scale:x() |
|
UIL.DrawParticles(particle.id, point(calc_particle_origin(self, particle)) + particle.offset, scale, scale, 0) |
|
end |
|
end |
|
end |
|
|
|
function XControl:DrawBackground() |
|
XWindow.DrawBackground(self) |
|
self:DrawParticles(false) |
|
end |
|
|
|
function XControl:DrawChildren(clip_box) |
|
XWindow.DrawChildren(self, clip_box) |
|
self:DrawParticles(true) |
|
end |
|
|
|
function XControl:GetParticles() |
|
return self.particles and table.map(self.particles, "parsys_name") |
|
end |
|
|
|
|
|
|
|
function XControl:SetEnabled(enabled, force) |
|
local old = self.enabled |
|
self.enabled = enabled and true or false |
|
if self.enabled == old and not force then return end |
|
for _, win in ipairs(self) do |
|
if win:IsKindOf("XControl") then |
|
win:SetEnabled(enabled) |
|
end |
|
end |
|
self:Invalidate() |
|
end |
|
|
|
function XControl:GetEnabled() |
|
return self.enabled |
|
end |
|
|
|
function XControl:PlayFX(fx, moment, pos) |
|
if fx and fx ~= "" then |
|
PlayFX(fx, moment or "start", self, self.Id, pos) |
|
end |
|
end |
|
|
|
function XControl:OnSetFocus(focus) |
|
self:Invalidate() |
|
XWindow.OnSetFocus(self, focus) |
|
end |
|
|
|
function XControl:OnKillFocus() |
|
self:Invalidate() |
|
XWindow.OnKillFocus(self) |
|
end |
|
|
|
function XControl:CalcBackground() |
|
if not self.enabled then return self.DisabledBackground end |
|
local FocusedBackground, Background = self.FocusedBackground, self.Background |
|
if FocusedBackground == Background then return Background end |
|
return self:IsFocused() and FocusedBackground or Background |
|
end |
|
|
|
function XControl:CalcBorderColor() |
|
if not self.enabled then return self.DisabledBorderColor end |
|
local FocusedBorderColor, BorderColor = self.FocusedBorderColor, self.BorderColor |
|
if FocusedBorderColor == BorderColor then return BorderColor end |
|
return self:IsFocused() and FocusedBorderColor or BorderColor |
|
end |
|
|
|
function XControl:OnSetRollover(rollover) |
|
XWindow.OnSetRollover(self, rollover) |
|
self:PlayHoverFX(rollover) |
|
end |
|
|
|
if FirstLoad then |
|
LastUIFXPos = false |
|
end |
|
|
|
function XControl:TryMarkUIFX(event) |
|
|
|
if event and event ~= "" then |
|
local pt = terminal.GetMousePos() |
|
if self:MouseInWindow(pt) and pt == LastUIFXPos then |
|
return |
|
end |
|
LastUIFXPos = pt |
|
end |
|
return true |
|
end |
|
|
|
function XControl:PlayActionFX(forced) |
|
local event = (self.enabled or forced) and self.FXPress or self.FXPressDisabled |
|
self:TryMarkUIFX(event) |
|
self:PlayFX(event) |
|
return true |
|
end |
|
|
|
function XControl:PlayHoverFX(rollover) |
|
if not self.enabled or rollover and not self:TryMarkUIFX(self.FXMouseIn) then |
|
return false |
|
end |
|
self:PlayFX(self.FXMouseIn, rollover and "start" or "end") |
|
return true |
|
end |
|
|
|
function XControl:OnMouseButtonDown(pos, button) |
|
if button == "L" then |
|
self:PlayActionFX() |
|
end |
|
end |
|
|
|
|
|
|
|
|
|
DefineClass.XContextControl = { |
|
__parents = { "XContextWindow", "XControl", }, |
|
ContextUpdateOnOpen = true, |
|
} |
|
|
|
|
|
|
|
|
|
DefineClass.XFontControl = { |
|
__parents = { "XControl" }, |
|
|
|
properties = { |
|
category = "Visual", |
|
{ id = "TextStyle", editor = "preset_id", default = "GedDefault", invalidate = "measure", preset_class = "TextStyle", editor_preview = true, }, |
|
{ id = "TextFont", editor = "text", default = "", invalidate = "measure", no_edit = true, }, |
|
{ id = "TextColor", editor = "color", default = RGB(32, 32, 32), invalidate = "measure", no_edit = true, }, |
|
{ id = "RolloverTextColor", editor = "color", default = RGB(0, 0, 0), invalidate = "measure", no_edit = true, }, |
|
{ id = "DisabledTextColor", editor = "color", default = RGBA(32, 32, 32, 128), invalidate = "measure", no_edit = true, }, |
|
{ id = "DisabledRolloverTextColor", editor = "color", default = RGBA(40, 40, 40, 128), invalidate = "measure", no_edit = true, }, |
|
{ id = "ShadowType", editor = "choice", default = "shadow", items = {"shadow", "extrude", "outline"}, invalidate = "measure", no_edit = true, }, |
|
{ id = "ShadowSize", editor = "number", default = 0, invalidate = "measure", no_edit = true, }, |
|
{ id = "ShadowColor", editor = "color", default = RGBA(0, 0, 0, 48), invalidate = "measure", no_edit = true, }, |
|
{ id = "ShadowDir", editor = "point", default = point(1,1), invalidate = "measure", no_edit = true, }, |
|
|
|
{ id = "DisabledShadowColor", editor = "color", default = RGBA(0, 0, 0, 48), invalidate = "measure", no_edit = true, }, |
|
}, |
|
|
|
font_id = false, |
|
font_height = 10, |
|
font_linespace = 0, |
|
font_baseline = 8, |
|
} |
|
|
|
function XFontControl:Init() |
|
self:SetTextStyle(self.TextStyle) |
|
end |
|
|
|
function XFontControl:SetTextStyle(style, force) |
|
self.TextStyle = style ~= "" and style or nil |
|
local text_style = TextStyles[style] |
|
if style == "" or not text_style then return end |
|
self:SetTextFont(style, force) |
|
self:SetTextColor(text_style.TextColor) |
|
self:SetRolloverTextColor(text_style.RolloverTextColor) |
|
self:SetDisabledTextColor(text_style.DisabledTextColor) |
|
self:SetShadowType(text_style.ShadowType) |
|
self:SetShadowSize(text_style.ShadowSize) |
|
self:SetShadowColor(text_style.ShadowColor) |
|
self:SetShadowDir(text_style.ShadowDir) |
|
self:SetDisabledShadowColor(text_style.DisabledShadowColor) |
|
self:SetDisabledRolloverTextColor(text_style.DisabledRolloverTextColor) |
|
end |
|
|
|
function XFontControl:SetTextFont(font, force) |
|
if self.TextFont == font and not force then return end |
|
self.TextFont = font |
|
self.font_id = false |
|
self:InvalidateMeasure() |
|
self:Invalidate() |
|
end |
|
|
|
function XFontControl:OnScaleChanged(scale) |
|
self.font_id = false |
|
end |
|
|
|
function XFontControl:CalcTextColor() |
|
return self.enabled and |
|
(self.rollover and self.RolloverTextColor or self.TextColor) or |
|
(self.rollover and self.DisabledRolloverTextColor or self.DisabledTextColor) |
|
end |
|
|
|
function XFontControl:OnSetRollover(rollover) |
|
local invalidate |
|
if self.enabled then |
|
invalidate = self.RolloverTextColor ~= self.TextColor |
|
else |
|
invalidate = self.DisabledRolloverTextColor ~= self.DisabledTextColor |
|
end |
|
if invalidate then |
|
self:Invalidate() |
|
end |
|
|
|
XControl.OnSetRollover(self, rollover) |
|
end |
|
|
|
function XFontControl:GetFontId() |
|
local font_id = self.font_id |
|
if not font_id then |
|
local text_style = TextStyles[self:GetTextStyle()] |
|
if not text_style then |
|
assert(false, string.format("Invalid text style '%s'", self:GetTextStyle())) |
|
return |
|
end |
|
font_id, self.font_height, self.font_baseline = text_style:GetFontIdHeightBaseline(self.scale:y()) |
|
self.font_id = font_id |
|
end |
|
return font_id |
|
end |
|
|
|
function XFontControl:GetFontHeight() |
|
self:GetFontId() |
|
return self.font_height |
|
end |
|
|
|
function XFontControl:SetFontProps(font_control) |
|
local style = font_control:GetTextStyle() |
|
if style ~= "" and TextStyles[style] then |
|
self:SetTextStyle(style) |
|
return |
|
end |
|
self:SetTextFont(font_control:GetTextFont()) |
|
self:SetTextColor(font_control:GetTextColor()) |
|
self:SetRolloverTextColor(font_control:GetRolloverTextColor()) |
|
self:SetDisabledTextColor(font_control:GetDisabledTextColor()) |
|
self:SetShadowType(font_control:GetShadowType()) |
|
self:SetShadowSize(font_control:GetShadowSize()) |
|
self:SetShadowColor(font_control:GetShadowColor()) |
|
self:SetShadowDir(font_control:GetShadowDir()) |
|
self:SetDisabledShadowColor(font_control:GetDisabledShadowColor()) |
|
self:SetDisabledRolloverTextColor(font_control:GetDisabledRolloverTextColor()) |
|
end |
|
|
|
|
|
|
|
|
|
DefineClass.XTranslateText = { |
|
__parents = { "XFontControl", "XContextControl" }, |
|
|
|
properties = { |
|
{ category = "General", id = "Translate", editor = "bool", default = false, }, |
|
{ category = "General", id = "Text", editor = "text", default = "", translate = function (obj) return obj:GetProperty("Translate") end, }, |
|
{ category = "General", id = "UpdateTimeLimit", name = "Update limit", editor = "number", default = 0, }, |
|
}, |
|
ContextUpdateOnOpen = false, |
|
text = "", |
|
last_update_time = 0, |
|
} |
|
|
|
function XTranslateText:OnTextChanged(text) |
|
end |
|
|
|
function XTranslateText:SetText(text) |
|
if type(text) == "number" then text = tostring(text) end |
|
self.Text = text or nil |
|
text = text or "" |
|
assert(self.Translate or type(text) == "string") |
|
assert(not self.Translate or IsT(text)) |
|
if text ~= "" and (self.Translate or IsT(text)) then |
|
text = _InternalTranslate(text, self.context) |
|
end |
|
if self.text ~= text then |
|
self:OnTextChanged(text) |
|
self.text = text |
|
self.last_update_time = RealTime() |
|
self:InvalidateMeasure() |
|
self:Invalidate() |
|
end |
|
end |
|
|
|
function XTranslateText:OnContextUpdate(context) |
|
local limit = self.UpdateTimeLimit |
|
if limit == 0 or (RealTime() - self.last_update_time) >= limit then |
|
self:SetText(self.Text) |
|
elseif not self:GetThread("ContextUpdate") then |
|
self:CreateThread("ContextUpdate", function(self) |
|
Sleep(self.last_update_time + self.UpdateTimeLimit - RealTime()) |
|
self:OnContextUpdate() |
|
end, self) |
|
end |
|
end |
|
|
|
function XTranslateText:OnXTemplateSetProperty(prop_id, old_value) |
|
|
|
if prop_id == "Translate" then |
|
self:UpdateLocalizedProperty("Text", self.Translate) |
|
ObjModified(self) |
|
end |
|
end |
|
|
|
function RecursiveUpdateTTexts(root) |
|
if IsKindOf(root, "XTranslateText") and root.Translate and IsT(root:GetText()) then |
|
root:SetText(root:GetText()) |
|
root:SetTextStyle(root:GetTextStyle(), "force") |
|
end |
|
for i = 1, #root do |
|
RecursiveUpdateTTexts(root[i]) |
|
end |
|
end |
|
|
|
function OnMsg.TranslationChanged() |
|
ClearTextStyleCache() |
|
RecursiveUpdateTTexts(terminal.desktop) |
|
end |
|
|
|
|
|
|
|
DefineClass.XEditableText = { |
|
__parents = { "XFontControl", "XContextControl" }, |
|
|
|
properties = { |
|
{ category = "General", id = "Translate", name = "Translated text", editor = "bool", default = false, |
|
help = "Enabled for texts that the developers enter and that need to go into the translation tables.\n\nGetText will return a T value with a localization ID.", |
|
}, |
|
{ category = "General", id = "UserText", name = "User text", editor = "bool", default = false, |
|
help = "Enable for user-entered texts that need to be filtered for profanity.\n\nGetText will return a special T value with extra data such as source user ID, language, etc.", |
|
}, |
|
{ category = "General", id = "UserTextType", editor = "choice", default = "unknown", items = {"name", "chat", "game_content", "unknown"}, no_edit = function(obj) return not obj.UserText end, |
|
help = "The user text is filtered in a different way, depending on this value; supported by Steam only.", |
|
}, |
|
{ category = "General", id = "Text", editor = "text", translate = function(self) return self.Translate end, default = "", }, |
|
{ category = "General", id = "OnTextChanged", editor = "func", params = "self"}, |
|
}, |
|
|
|
text = "", |
|
text_translation_id = false, |
|
} |
|
|
|
function XEditableText:SetText(text) |
|
if self.Translate then |
|
assert(IsT(text)) |
|
self.text_translation_id = TGetID(text) or nil |
|
text = type(text) == "string" and text or TDevModeGetEnglishText(text, "deep", "no_assert") |
|
elseif self.UserText then |
|
assert(IsUserText(text)) |
|
text = type(text) == "string" and text or TDevModeGetEnglishText(text, "deep", "no_assert") |
|
end |
|
self:SetTranslatedText(text) |
|
end |
|
|
|
function XEditableText:SetTranslatedText(text, notify) |
|
if self.text ~= text then |
|
assert(type(text) == "string") |
|
self.text = IsT(text) and TDevModeGetEnglishText(text) or text |
|
if notify ~= false then |
|
self:OnTextChanged() |
|
end |
|
self:InvalidateMeasure() |
|
self:Invalidate() |
|
end |
|
end |
|
|
|
function XEditableText:GetText() |
|
local text = self.text |
|
if text == "" or (not self.Translate and not self.UserText) then |
|
return text |
|
elseif self.UserText then |
|
return CreateUserText(self.text, self.UserTextType) |
|
end |
|
local id = self.text_translation_id or RandomLocId() |
|
self.text_translation_id = id |
|
text = text:gsub("\r?\n", "\n") |
|
return T{id, text} |
|
end |
|
|
|
function XEditableText:GetTranslatedText() |
|
return self.text |
|
end |
|
|
|
function XEditableText:OnTextChanged() |
|
end |
|
|
|
|
|
|
|
|
|
xpopup_anchor_types = {"none", "custom", "drop", "drop-right", "smart", "left", "right", "top", "bottom", "center-top", "center-bottom", "bottom-right", "bottom-left", "top-left", "top-right", "right-center", "left-center" , "mouse", "live-mouse"} |
|
DefineClass.XPopup = { |
|
__parents = { "XControl" }, |
|
properties = { |
|
{ category = "General", id = "Anchor", editor = "rect", default = box(0, 0, 0, 0), }, |
|
{ category = "General", id = "AnchorType", editor = "choice", default = "none", items = xpopup_anchor_types }, |
|
}, |
|
LayoutMethod = "VList", |
|
Dock = "ignore", |
|
Background = RGB(240, 240, 240), |
|
FocusedBackground = RGB(240, 240, 240), |
|
BorderWidth = 1, |
|
BorderColor = RGB(128, 128, 128), |
|
FocusedBorderColor = RGB(128, 128, 128), |
|
|
|
popup_parent = false, |
|
} |
|
|
|
function XPopup:GetSafeAreaBox() |
|
return GetSafeAreaBox() |
|
end |
|
|
|
function XPopup:GetCustomAnchor(x, y, width, height, anchor) |
|
return anchor:minx(), anchor:miny(), width, height |
|
end |
|
|
|
function XPopup:UpdateLayout() |
|
local margins_x1, margins_y1, margins_x2, margins_y2 = ScaleXY(self.scale, self.Margins:xyxy()) |
|
local anchor = self:GetAnchor() |
|
local safe_area_x1, safe_area_y1, safe_area_x2, safe_area_y2 = self:GetSafeAreaBox() |
|
local x, y = self.box:minxyz() |
|
local width, height = self.measure_width - margins_x1 - margins_x2, self.measure_height - margins_y1 - margins_y2 |
|
local a_type = self.AnchorType |
|
if a_type == "smart" then |
|
local space = anchor:minx() - safe_area_x1 - width - margins_x2 |
|
a_type = "left" |
|
if space < safe_area_x2 - anchor:maxx() - width - margins_x1 then |
|
space = safe_area_x2 - anchor:maxx() - width - margins_x1 |
|
a_type = "right" |
|
end |
|
if space < anchor:miny() - safe_area_y1 - height - margins_y2 then |
|
space = anchor:miny() - safe_area_y1 - height - margins_y2 |
|
a_type = "top" |
|
end |
|
if space < safe_area_y2 - anchor:maxy() - height - margins_y1 then |
|
space = safe_area_y2 - anchor:maxy() - height - margins_y1 |
|
a_type = "bottom" |
|
end |
|
end |
|
if a_type == "live-mouse" then |
|
local pos = terminal.GetMousePos() |
|
anchor = sizebox(pos, UIL.MeasureImage(GetMouseCursor())) |
|
a_type = "bottom" |
|
end |
|
if a_type == "mouse" then |
|
x, y = anchor:x(), anchor:y() |
|
elseif a_type == "left" then |
|
x = anchor:minx() - width - margins_x2 |
|
y = anchor:miny() - margins_y1 |
|
elseif a_type == "right" then |
|
x = anchor:maxx() + margins_x1 |
|
y = anchor:miny() - margins_y1 |
|
elseif a_type == "top" then |
|
x = anchor:minx() - margins_x1 |
|
y = anchor:miny() - height - margins_y2 |
|
elseif a_type == "bottom" then |
|
x = anchor:minx() - margins_x1 |
|
y = anchor:maxy() + margins_y2 |
|
end |
|
if a_type == "center-top" then |
|
x = anchor:minx() + ((anchor:maxx() - anchor:minx()) - width)/2 |
|
y = anchor:miny() - height - margins_y2 |
|
end |
|
if a_type == "center-bottom" then |
|
x = anchor:minx() + ((anchor:maxx() - anchor:minx()) - width)/2 |
|
y = anchor:maxy() + margins_y2 |
|
end |
|
if a_type == "bottom-right" then |
|
x = anchor:maxx() + margins_x1 |
|
y = anchor:maxy() - height - margins_y2 |
|
end |
|
if a_type == "bottom-left" then |
|
x = anchor:minx() - width - margins_x2 |
|
y = anchor:maxy() - height - margins_y2 |
|
end |
|
if a_type == "right-center" then |
|
x = anchor:maxx() + margins_x1 |
|
y = anchor:miny() + ((anchor:maxy() - anchor:miny()) - height)/2 |
|
end |
|
if a_type == "left-center" then |
|
x = anchor:minx() - width - margins_x2 |
|
y = anchor:miny() + ((anchor:maxy() - anchor:miny()) - height)/2 |
|
end |
|
if a_type == "top-right" then |
|
x = anchor:maxx() + margins_x1 |
|
y = anchor:miny()- margins_y1 |
|
end |
|
if a_type == "top-left" then |
|
x = anchor:minx() - width - margins_x2 |
|
y = anchor:miny()- margins_y1 |
|
end |
|
if a_type == "drop" then |
|
x, y = anchor:minx(), anchor:maxy() |
|
width = Max(anchor:sizex(), width) |
|
end |
|
if a_type == "drop-right" then |
|
x, y = anchor:minx() + anchor:sizex() - width, anchor:maxy() |
|
width = Max(anchor:sizex(), width) |
|
end |
|
if a_type == "custom" then |
|
x, y, width, height = self:GetCustomAnchor(x, y, width, height, anchor) |
|
end |
|
|
|
if x + width + margins_x2 > safe_area_x2 then |
|
x = safe_area_x2 - width - margins_x2 |
|
elseif x < safe_area_x1 then |
|
x = safe_area_x1 |
|
end |
|
if y + height + margins_y2 > safe_area_y2 then |
|
y = safe_area_y2 - height - margins_y2 |
|
elseif y < safe_area_y1 then |
|
y = safe_area_y1 |
|
end |
|
|
|
self:SetBox(x, y, width, height) |
|
return XControl.UpdateLayout(self) |
|
end |
|
|
|
function XPopup:OnKillFocus(new_focus) |
|
if self.window_state ~= "open" then |
|
XWindow.OnKillFocus(self) |
|
return |
|
end |
|
|
|
local popup = self |
|
while IsKindOf(popup, "XPopup") and not (new_focus and popup:IsWithinPopupChain(new_focus)) do |
|
popup:Close() |
|
popup = popup.popup_parent |
|
end |
|
XWindow.OnKillFocus(self) |
|
end |
|
|
|
function XPopup:IsWithinPopupChain(child) |
|
local popup = child:IsKindOf("XPopup") and child or GetParentOfKind(child, "XPopup") |
|
while popup do |
|
if popup == self then return true end |
|
popup = GetParentOfKind(popup.popup_parent, "XPopup") |
|
end |
|
end |
|
|
|
function XPopup:OnMouseButtonDown(pt, button) |
|
if button == "L" then |
|
self:SetFocus() |
|
return "break" |
|
end |
|
end |
|
|
|
|
|
|
|
|
|
DefineClass.XPopupList = { |
|
__parents = { "XPopup" }, |
|
properties = { |
|
{ category = "General", id = "MinItems", editor = "number", default = 5, }, |
|
{ category = "General", id = "MaxItems", editor = "number", default = 25, }, |
|
{ category = "General", id = "AutoFocus", editor = "bool", default = true, }, |
|
}, |
|
IdNode = true, |
|
} |
|
|
|
function XPopupList:Init() |
|
XSleekScroll:new({ |
|
Id = "idScroll", |
|
Target = "idContainer", |
|
Dock = "right", |
|
Margins = box(1, 1, 1, 1), |
|
AutoHide = true, |
|
MinThumbSize = 30, |
|
}, self) |
|
XScrollArea:new({ |
|
Id = "idContainer", |
|
Dock = "box", |
|
LayoutMethod = "VList", |
|
VScroll = "idScroll", |
|
}, self) |
|
self.idContainer.EnumFocusChildren = function(this, f) |
|
for _, win in ipairs(this) do |
|
local order = win:GetFocusOrder() |
|
if order then |
|
f(win, order:xy()) |
|
else |
|
win:EnumFocusChildren(f) |
|
end |
|
end |
|
end |
|
end |
|
|
|
function XPopupList:Open(...) |
|
if self.AutoFocus then |
|
self.idContainer:SetFocus() |
|
end |
|
XPopup.Open(self, ...) |
|
end |
|
|
|
function XPopupList:UpdateLayout() |
|
local a_type = self.AnchorType |
|
if a_type ~= "drop" and a_type ~= "drop-right" then |
|
return XPopup.UpdateLayout(self) |
|
end |
|
|
|
local margins_x1, margins_y1, margins_x2, margins_y2 = ScaleXY(self.scale, self.Margins:xyxy()) |
|
local anchor = self.Anchor |
|
local safe_area_x1, safe_area_y1, safe_area_x2, safe_area_y2 = GetSafeAreaBox() |
|
local width, height = Max(anchor:sizex(),self.measure_width - margins_x1 - margins_x2), self.measure_height - margins_y1 - margins_y2 |
|
|
|
local x, y = anchor:minx(), anchor:maxy() |
|
|
|
if a_type == "drop-right" then |
|
x = anchor:minx() + anchor:sizex() - width |
|
end |
|
|
|
|
|
if x + width + margins_x2 > safe_area_x2 then |
|
x = safe_area_x2 - width - margins_x2 |
|
elseif x < safe_area_x1 then |
|
x = safe_area_x1 |
|
end |
|
|
|
local items = self.idContainer |
|
local popup_max_y = y + height + margins_y2 |
|
local space_y = safe_area_y2 - y |
|
local fail = false |
|
if (safe_area_y2 - popup_max_y)<0 then |
|
|
|
local vspace = self.idContainer.LayoutVSpacing |
|
y = anchor:maxy() |
|
local size = margins_y1 + margins_y2 - vspace |
|
for i = 1, Min(#items, self.MaxItems) do |
|
local newsize = size + vspace + items[i].measure_height |
|
if newsize > space_y then |
|
fail = i<=self.MinItems |
|
break |
|
end |
|
size = newsize |
|
end |
|
if not fail then |
|
height = size |
|
end |
|
|
|
|
|
if fail then |
|
y = anchor:miny() |
|
local popup_min_y = y - height - margins_y1 |
|
local space_y = y - safe_area_y1 |
|
if (popup_min_y - safe_area_y1)<0 then |
|
|
|
fail = false |
|
size = margins_y1 + margins_y2 + items[1].measure_height |
|
for i = 2, Min(#items, self.MaxItems) do |
|
local newsize = size + vspace + items[i].measure_height |
|
if newsize > space_y then |
|
fail = i<=self.MinItems |
|
break |
|
end |
|
size = newsize |
|
end |
|
height = size |
|
end |
|
y = y - height |
|
end |
|
end |
|
|
|
if fail then |
|
if y + height + margins_y2 > safe_area_y2 then |
|
y = safe_area_y2 - height - margins_y2 |
|
elseif y < safe_area_y1 then |
|
y = safe_area_y1 |
|
end |
|
end |
|
self:SetBox(x, y, width, height) |
|
return XControl.UpdateLayout(self) |
|
end |
|
|
|
function XPopupList:Measure(preferred_width, preferred_height) |
|
local width, height = XPopup.Measure(self, preferred_width, preferred_height) |
|
local items = self.idContainer |
|
if #items > self.MaxItems then |
|
local item_height = (self.MaxItems - 1) * self.idContainer.LayoutVSpacing |
|
for i = 1, self.MaxItems do |
|
item_height = item_height + items[i].measure_height |
|
end |
|
self.idContainer.MouseWheelStep = items[1].measure_height * 2 |
|
return width, Min(height, item_height) |
|
end |
|
return width, height |
|
end |
|
|
|
function XPopupList:OnShortcut(shortcut, source, ...) |
|
if shortcut == "Escape" or shortcut == "ButtonB" then |
|
self:Close() |
|
return "break" |
|
end |
|
local relation = XShortcutToRelation[shortcut] |
|
if shortcut == "Down" or shortcut == "Up" or relation == "down" or relation == "up" then |
|
local focus = self.desktop.keyboard_focus |
|
local order = focus and focus:GetFocusOrder() |
|
if shortcut == "Down" or relation == "down" then |
|
focus = self.idContainer:GetRelativeFocus(order or point(0, 0), "next") |
|
else |
|
focus = self.idContainer:GetRelativeFocus(order or point(1000000000, 1000000000), "prev") |
|
end |
|
if focus then |
|
self.idContainer:ScrollIntoView(focus) |
|
focus:SetFocus() |
|
end |
|
return "break" |
|
end |
|
end |
|
|
|
|
|
|
|
|
|
DefineClass.XPropControl = { |
|
__parents = { "XContextControl" }, |
|
properties = { |
|
{ category = "Scroll", id = "BindTo", name = "Bind to property", editor = "text", default = "", }, |
|
|
|
}, |
|
prop_meta = false, |
|
value = false, |
|
} |
|
|
|
function XPropControl:Init(parent, context) |
|
self.prop_meta = ResolveValue(context, "prop_meta") |
|
end |
|
|
|
function XPropControl:SetBindTo(prop_id, prop_meta) |
|
self.BindTo = prop_id |
|
if not prop_meta then |
|
ForEachObjInContext(self.context, function(obj, self, prop_id) |
|
prop_meta = prop_meta or IsKindOf(obj, "PropertyObject") and obj:GetPropertyMetadata(prop_id) |
|
end, self, prop_id) |
|
end |
|
self.prop_meta = prop_meta |
|
end |
|
|
|
function XPropControl:OnPropUpdate(context, prop_meta, value) |
|
end |
|
|
|
function XPropControl:GetPropName() |
|
local prop_meta = self.prop_meta |
|
return prop_meta and prop_meta.name or "" |
|
end |
|
|
|
function XPropControl:UpdatePropertyNames(prop_meta) |
|
local name = self:ResolveId("idName") |
|
if name then |
|
name:SetText(prop_meta.name or prop_meta.id) |
|
end |
|
if prop_meta.help and editor ~= "help" then |
|
self:SetRolloverText(prop_meta.help) |
|
end |
|
end |
|
|
|
function XPropControl:OnContextUpdate(context) |
|
local prop_id = self.BindTo |
|
local prop_meta = self.prop_meta |
|
if context and (prop_id ~= "" or prop_meta) then |
|
if prop_meta then |
|
prop_id = prop_meta.id |
|
self:UpdatePropertyNames(prop_meta) |
|
end |
|
local value = ResolveValue(context, prop_id) |
|
if value ~= rawget(self, "value") then |
|
self.value = value |
|
self:OnPropUpdate(context, prop_meta, value) |
|
end |
|
end |
|
XContextControl.OnContextUpdate(self, context) |
|
end |
|
|
|
|
|
|
|
|
|
DefineClass.XProgress = { |
|
__parents = { "XPropControl" }, |
|
|
|
properties = { |
|
{ category = "Progress", id = "Horizontal", name = "Horizontal", editor = "bool", default = true }, |
|
{ category = "Progress", id = "Progress", name = "Progress", editor = "number", default = 0 }, |
|
{ category = "Progress", id = "MaxProgress", name = "Max progress", editor = "number", default = 100, invalidate = "measure", }, |
|
{ category = "Progress", id = "MinProgressSize", name = "Size at progress 0", editor = "number", default = 0 }, |
|
{ category = "Progress", id = "ProgressClip", name = "Clip window", editor = "bool", default = false, invalidate = true, }, |
|
}, |
|
} |
|
|
|
function XProgress:OnPropUpdate(context, prop_meta, value) |
|
assert(type(value) == "number") |
|
if type(value) == "number" then |
|
if prop_meta then |
|
local scale = prop_meta.scale |
|
scale = type(scale) == "string" and const.Scale[scale] or scale or 1 |
|
local min = prop_eval(prop_meta.min, context, prop_meta) or 0 |
|
local max = prop_eval(prop_meta.max, context, prop_meta) |
|
self:SetMaxProgress(max and (max - min) / scale or self.MaxProgress) |
|
self:SetProgress((value - min) / scale) |
|
else |
|
self:SetProgress(value) |
|
end |
|
end |
|
end |
|
|
|
function XProgress:SetProgress(value) |
|
if self.Progress == value then return end |
|
self.Progress = value |
|
if self.ProgressClip then |
|
self:Invalidate() |
|
else |
|
self:InvalidateMeasure() |
|
end |
|
end |
|
|
|
function XProgress:MeasureSizeAdjust(max_width, max_height) |
|
local old_width = max_width |
|
|
|
local docked_x, docked_y = 0, 0 |
|
for _, win in ipairs(self) do |
|
local dock = win.Dock |
|
if dock then |
|
win:UpdateMeasure(max_width, max_height) |
|
if dock == "left" or dock == "right" then |
|
docked_x = docked_x + win.measure_width |
|
elseif dock == "top" or dock == "bottom" then |
|
docked_y = docked_y + win.measure_height |
|
end |
|
end |
|
end |
|
|
|
local max = Max(1, self.MaxProgress) |
|
local progress = self.ProgressClip and max or Clamp(self.Progress, 0, max) |
|
if self.Horizontal then |
|
max_width = max_width - docked_x |
|
local min = ScaleXY(self.scale, self.MinProgressSize) |
|
max_width = min + (max_width - min) * progress / max |
|
max_width = max_width + docked_x |
|
else |
|
max_height = max_height - docked_y |
|
local _, min = ScaleXY(self.scale, 0, self.MinProgressSize) |
|
max_height = min + (max_height - min) * progress / max |
|
max_height = max_height + docked_y |
|
end |
|
|
|
return max_width, max_height |
|
end |
|
|
|
|
|
|
|
DefineClass.XAspectWindow = { |
|
__parents = { "XWindow" }, |
|
properties = { |
|
{ category = "General", id = "Aspect", name = "Aspect", editor = "combo", default = point(16, 9), items = { |
|
{ name = "21:9 movie (64:27)", value = point(64, 27)}, |
|
{ name = "2:1 Univisium", value = point(2, 1)}, |
|
{ name = "16:9 HD", value = point(16, 9)}, |
|
{ name = "5:3", value = point(5, 3)}, |
|
{ name = "1.618:1 golden ratio", value = point(1618, 1000)}, |
|
{ name = "3:2 35mm film", value = point(3, 2)}, |
|
{ name = "4:3 legacy TV/monitor", value = point(4, 3)}, |
|
{ name = "1:1", value = point(1, 1)}, |
|
{ name = "1:2", value = point(1, 2)}, |
|
{ name = "1:3", value = point(1, 3)}, |
|
{ name = "1:4", value = point(1, 4)}, |
|
{ name = "1:5", value = point(1, 5)}, |
|
}}, |
|
{ category = "General", id = "UseAllSpace", name = "Use available space", editor = "bool", default = true, }, |
|
{ category = "General", id = "Fit", name = "Fit", editor = "choice", default = "smallest", items = {"none", "width", "height", "smallest", "largest"}, }, |
|
} |
|
} |
|
|
|
local box0 = box(0, 0, 0, 0) |
|
function XAspectWindow:SetLayoutSpace(x, y, width, height) |
|
local fit = self.Fit |
|
if fit ~= "none" then |
|
assert(self.Margins == box0) |
|
assert(self.Padding == box0) |
|
local aspect_x, aspect_y = self.Aspect:xy() |
|
local h_align = self.HAlign |
|
if fit == "smallest" or fit == "largest" then |
|
local space_is_wider = width * aspect_y >= height * aspect_x |
|
fit = space_is_wider == (fit == "largest") and "width" or "height" |
|
end |
|
if fit == "width" then |
|
local h = width * aspect_y / aspect_x |
|
local v_align = self.VAlign |
|
if v_align == "top" then |
|
elseif v_align == "center" or v_align == "stretch" then |
|
y = y + (height - h) / 2 |
|
elseif v_align == "bottom" then |
|
y = y + (height - h) |
|
end |
|
height = h |
|
elseif fit == "height" then |
|
local w = height * aspect_x / aspect_y |
|
local h_align = self.HAlign |
|
if h_align == "left" then |
|
elseif h_align == "center" or h_align == "stretch" then |
|
x = x + (width - w) / 2 |
|
elseif h_align == "right" then |
|
x = x + (width - w) |
|
end |
|
width = w |
|
end |
|
self:SetBox(x, y, width, height) |
|
return |
|
end |
|
XWindow.SetLayoutSpace(self, x, y, width, height) |
|
end |
|
|
|
function XAspectWindow:Measure(max_width, max_height) |
|
local aspect_x, aspect_y = self.Aspect:xy() |
|
local m_width = Min(max_width, max_height * aspect_x / aspect_y) |
|
local m_height = Min(max_height, max_width * aspect_y / aspect_x) |
|
local width, height = XWindow.Measure(self, m_width, m_height) |
|
local min_width = Max(width, height * aspect_x / aspect_y) |
|
local min_height = Max(height, width * aspect_y / aspect_x) |
|
if self.UseAllSpace then |
|
return Max(min_width, m_width), Max(min_height, m_height) |
|
end |
|
return min_width, min_height |
|
end |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function NewXVirtualContent(parent, context, xtemplate, width, height, refresh_interval, min_width, min_height) |
|
local obj = { |
|
MinWidth = min_width or width or 10, |
|
MaxWidth = width or 1000000, |
|
MinHeight = min_height or height or 10, |
|
MaxHeight = height or 1000000, |
|
desktop = false, |
|
parent = false, |
|
children = false, |
|
window_state = false, |
|
box = empty_box, |
|
content_box = empty_box, |
|
scale = XWindow.scale, |
|
xtemplate = xtemplate, |
|
context = context or false, |
|
measure_update = true, |
|
layout_update = true, |
|
outside_parent = true, |
|
RefreshInterval = refresh_interval, |
|
} |
|
return XVirtualContent:new(obj, parent, context) |
|
end |
|
|
|
DefineClass.XVirtualContent = { |
|
__parents = { "XControl" }, |
|
xtemplate = false, |
|
spawned = false, |
|
selected = false, |
|
RefreshInterval = false, |
|
} |
|
|
|
local function UpdateContext(win) |
|
for _, child in ipairs(win) do |
|
if IsKindOf(child, "XContextWindow") then |
|
child:OnContextUpdate(child.context) |
|
end |
|
UpdateContext(child) |
|
end |
|
end |
|
|
|
function XVirtualContent:SpawnChildren() |
|
XTemplateSpawn(self.xtemplate, self, self.context) |
|
if self.RefreshInterval then |
|
self:CreateThread("UpdateContext", function(self) |
|
while true do |
|
Sleep(self.RefreshInterval) |
|
UpdateContext(self) |
|
end |
|
end, self) |
|
end |
|
end |
|
|
|
function XVirtualContent:UpdateMeasure(max_width, max_height) |
|
|
|
if not self.spawned and (self.measure_width ~= 0 or self.measure_height ~= 0) then |
|
self.measure_update = false |
|
return |
|
end |
|
XControl.UpdateMeasure(self, max_width, max_height) |
|
end |
|
|
|
function XVirtualContent:SetOutsideParent(outside_parent) |
|
XWindow.SetOutsideParent(self, outside_parent) |
|
self:SetSpawned(not outside_parent) |
|
end |
|
|
|
function XVirtualContent:SetSpawned(spawn) |
|
if self.spawned == spawn then return end |
|
if not spawn and self.parent.force_keep_items_spawned then return end |
|
self.spawned = spawn |
|
|
|
self.Invalidate = empty_func |
|
self:DeleteChildren() |
|
if spawn then |
|
self:SpawnChildren() |
|
for _, win in ipairs(self) do |
|
win:Open() |
|
end |
|
self:UpdateMeasure(self.parent.content_box:size():xy()) |
|
self:UpdateLayout() |
|
else |
|
self:DeleteThread("UpdateContext") |
|
end |
|
self.Invalidate = nil |
|
if spawn then |
|
local scrollarea = GetParentOfKind(self, "XScrollArea") |
|
if scrollarea then |
|
scrollarea:InvalidateMeasure() |
|
end |
|
self:SetChildSelected() |
|
Msg("XWindowRecreated", self) |
|
end |
|
if self.desktop:GetKeyboardFocus() == self then |
|
self:SetFocus() |
|
end |
|
end |
|
|
|
function XVirtualContent:SetSelected(selected) |
|
self.selected = selected |
|
self:SetChildSelected() |
|
end |
|
|
|
function XVirtualContent:SetChildSelected() |
|
local child = self[1] |
|
if child then |
|
child:ResolveRelativeFocusOrder(self.FocusOrder) |
|
if child:HasMember("SetSelected") then |
|
child:SetSelected(self.selected) |
|
end |
|
end |
|
end |
|
|
|
function XVirtualContent:SetFocus() |
|
XControl.SetFocus(self[1] or self) |
|
end |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DefineClass.XSizeConstrainedWindow = { |
|
__parents = { "XWindow" }, |
|
} |
|
|
|
local one = point(1000, 1000) |
|
function XSizeConstrainedWindow:UpdateMeasure(max_width, max_height) |
|
if not self.measure_update then return end |
|
|
|
|
|
XWindow.UpdateMeasure(self, max_width, max_height) |
|
|
|
|
|
if self.measure_width > max_width or self.measure_height > max_height then |
|
|
|
local scale_x, scale_y = self.scale:xy() |
|
local scale_ratio = MulDivRound(scale_y, 1000, scale_x) |
|
self:SetScaleModifier(one) |
|
XWindow.UpdateMeasure(self, max_width, max_height) |
|
|
|
|
|
local space_ratio = MulDivRound(max_height, 1000, max_width) |
|
local measure_ratio = MulDivRound(self.measure_height, 1000, self.measure_width) |
|
local width_contrained = measure_ratio < space_ratio |
|
|
|
|
|
local content_width, content_height = ScaleXY(self.parent.scale, self.measure_width, self.measure_height) |
|
if width_contrained then |
|
scale_x = MulDivRound(self.parent.scale:x(), max_width, content_width) |
|
scale_y = MulDivRound(scale_x, scale_ratio, 1000) |
|
else |
|
scale_y = MulDivRound(self.parent.scale:y(), max_height, content_height) |
|
scale_x = MulDivRound(scale_y, 1000, scale_ratio) |
|
end |
|
|
|
self:SetScaleModifier(point(scale_x, scale_y)) |
|
XWindow.UpdateMeasure(self, max_width, max_height) |
|
end |
|
end |
|
|
|
function CreateNumberEditor(parent, id, up_pressed, down_pressed, no_buttons) |
|
local panel = XWindow:new({ Dock = "box" }, parent) |
|
local button_panel = XWindow:new({ |
|
Id = "idNumberEditor", |
|
Dock = "right", |
|
}, panel) |
|
local function get_button_multiplier() |
|
if terminal.IsKeyPressed(const.vkControl) then |
|
return 10 |
|
elseif terminal.IsKeyPressed(const.vkShift) then |
|
return 100 |
|
else |
|
return 1 |
|
end |
|
end |
|
local button_rollover_text = "Use LMB, Ctrl+LMB, or Shift+LMB to change the value." |
|
local top_btn = not no_buttons and XTextButton:new({ |
|
Dock = "top", |
|
OnPress = function(button) up_pressed(get_button_multiplier()) end, |
|
Padding = box(1, 2, 1, 1), |
|
Icon = "CommonAssets/UI/arrowup-40.tga", |
|
IconScale = point(500, 500), |
|
IconColor = RGB(0, 0, 0), |
|
FoldWhenHidden = true, |
|
DisabledIconColor = RGBA(0, 0, 0, 128), |
|
Background = RGBA(0, 0, 0, 0), |
|
DisabledBackground = RGBA(0, 0, 0, 0), |
|
RolloverBackground = RGB(204, 232, 255), |
|
PressedBackground = RGB(121, 189, 241), |
|
RolloverTemplate = "GedPropRollover", |
|
RolloverText = button_rollover_text, |
|
RolloverAnchor = "center-top", |
|
}, button_panel) |
|
|
|
local bottom_btn = not no_buttons and XTextButton:new({ |
|
Dock = "bottom", |
|
OnPress = function(button) down_pressed(get_button_multiplier()) end, |
|
Padding = box(1, 1, 1, 2), |
|
Icon = "CommonAssets/UI/arrowdown-40.tga", |
|
IconScale = point(500, 500), |
|
IconColor = RGB(0, 0, 0), |
|
FoldWhenHidden = true, |
|
DisabledIconColor = RGBA(0, 0, 0, 128), |
|
Background = RGBA(0, 0, 0, 0), |
|
DisabledBackground = RGBA(0, 0, 0, 0), |
|
RolloverBackground = RGB(204, 232, 255), |
|
PressedBackground = RGB(121, 189, 241), |
|
RolloverTemplate = "GedPropRollover", |
|
RolloverText = button_rollover_text, |
|
RolloverAnchor = "center-bottom", |
|
}, button_panel) |
|
|
|
local edit = XNumberEdit:new({ |
|
Id = id, |
|
Dock = "box", |
|
OnShortcut = function(control, shortcut, ...) |
|
if shortcut == "Up" then |
|
up_pressed(1) |
|
elseif shortcut == "Down" then |
|
down_pressed(1) |
|
elseif shortcut == "Ctrl-Up" then |
|
up_pressed(10) |
|
elseif shortcut == "Ctrl-Down" then |
|
down_pressed(10) |
|
elseif shortcut == "Ctrl-Left" then |
|
up_pressed(100) |
|
elseif shortcut == "Ctrl-Right" then |
|
down_pressed(100) |
|
else |
|
return XNumberEdit.OnShortcut(control, shortcut, ...) |
|
end |
|
return "break" |
|
end, |
|
top_btn = top_btn or nil, |
|
bottom_btn = bottom_btn or nil, |
|
OnMouseWheelForward = function() if terminal.IsKeyPressed(const.vkControl) then up_pressed(1) return "break" end end, |
|
OnMouseWheelBack = function() if terminal.IsKeyPressed(const.vkControl) then down_pressed(1) return "break" end end, |
|
RolloverTemplate = "GedPropRollover", |
|
RolloverText = "Use arrow keys, Ctrl+arrows, or Ctrl+MouseWheel to change the value.", |
|
}, panel) |
|
|
|
return edit, top_btn, bottom_btn |
|
end |