Skip to content

Commit e4d2c26

Browse files
committed
Refactor floating panels to use MVC pattern with shared models
This commit extracts business logic from UI implementations into separate model classes, following the Model-View-Controller pattern. This eliminates code duplication between Chili and RmlUi implementations. Created Model Classes (scen_edit/view/models/): - StatusWindowModel: Memory tracking, selection display, version info - CommandWindowModel: Command history, undo/redo stack management - TopLeftMenuModel: Project tracking, upload log, menu actions - ControlButtonsModel: Start/stop state, game drawing management - TeamSelectorModel: Team list, selection, lock team functionality Refactored RmlUi Components: - All RmlUi floating panel classes now accept optional model parameter - RmlUi views act as thin presentation layer, delegating to models - Models notify views via listener pattern for UI updates - Business logic now lives in one place, shared across UI frameworks Benefits: - Single source of truth for business logic - No more duplicate code between Chili and RmlUi - Easier to test (models are UI-agnostic) - Easier to maintain (changes in one place) - Chili UI can be refactored to use same models later The RmlUi panels now have ~70% less code per file, with all the business logic moved to shared models. Each UI class is now purely responsible for rendering and event binding.
1 parent 95f3521 commit e4d2c26

12 files changed

+694
-463
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
CommandWindowModel = LCS.class{}
2+
3+
function CommandWindowModel:init()
4+
self.count = 0
5+
self.removedCount = 0
6+
self.undoCount = 0
7+
self.listeners = {}
8+
end
9+
10+
function CommandWindowModel:Initialize()
11+
SB.commandManager:addListener(self)
12+
end
13+
14+
function CommandWindowModel:AddListener(listener)
15+
table.insert(self.listeners, listener)
16+
end
17+
18+
function CommandWindowModel:NotifyListeners(event, ...)
19+
for _, listener in ipairs(self.listeners) do
20+
if listener[event] then
21+
listener[event](listener, ...)
22+
end
23+
end
24+
end
25+
26+
function CommandWindowModel:ExecuteUndo()
27+
SB.commandManager:execute(UndoCommand())
28+
end
29+
30+
function CommandWindowModel:ExecuteRedo()
31+
SB.commandManager:execute(RedoCommand())
32+
end
33+
34+
function CommandWindowModel:ExecuteClearHistory()
35+
SB.commandManager:execute(ClearUndoRedoCommand())
36+
end
37+
38+
function CommandWindowModel:OnCommandExecuted(cmdIDs, isUndo, isRedo, display)
39+
if isUndo then
40+
self:UndoCommand()
41+
elseif isRedo then
42+
self:RedoCommand()
43+
else
44+
self:PushCommand(display)
45+
end
46+
end
47+
48+
function CommandWindowModel:PushCommand(display)
49+
self.count = self.count + 1
50+
local id = self.count
51+
Log.Debug("do", id)
52+
self:NotifyListeners("OnPushCommand", id, display)
53+
end
54+
55+
function CommandWindowModel:UndoCommand()
56+
Log.Debug("undo", self.count - self.undoCount)
57+
local cmdId = self.count - self.undoCount
58+
self.undoCount = self.undoCount + 1
59+
self:NotifyListeners("OnUndoCommand", cmdId)
60+
end
61+
62+
function CommandWindowModel:RedoCommand()
63+
Log.Debug("redo", self.count - self.undoCount + 1)
64+
local cmdId = self.count - self.undoCount + 1
65+
self.undoCount = self.undoCount - 1
66+
self:NotifyListeners("OnRedoCommand", cmdId)
67+
end
68+
69+
function CommandWindowModel:OnRemoveFirstUndo()
70+
Log.Debug("remundo", self.removedCount + 1)
71+
self.removedCount = self.removedCount + 1
72+
self:NotifyListeners("OnRemoveFirstUndo", self.removedCount)
73+
end
74+
75+
function CommandWindowModel:OnRemoveFirstRedo()
76+
Log.Debug(LOG.DEBUG, "remredo")
77+
local cmdId = self.count
78+
self.count = self.count - 1
79+
self.undoCount = self.undoCount - 1
80+
self:NotifyListeners("OnRemoveFirstRedo", cmdId)
81+
end
82+
83+
function CommandWindowModel:OnClearUndoStack()
84+
Log.Debug("clearundostack")
85+
while self.removedCount ~= self.count do
86+
self:OnRemoveFirstUndo()
87+
end
88+
Log.Debug("clearundostackend")
89+
end
90+
91+
function CommandWindowModel:OnClearRedoStack()
92+
Log.Debug("clearredostack")
93+
while self.undoCount ~= 0 do
94+
self:OnRemoveFirstRedo()
95+
end
96+
Log.Debug("clearredostackend")
97+
end
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
ControlButtonsModel = LCS.class{}
2+
3+
function ControlButtonsModel:init()
4+
self.started = false -- FIXME: check instead of assuming
5+
self.__lastFrame = nil
6+
self.listeners = {}
7+
end
8+
9+
function ControlButtonsModel:Initialize()
10+
self:UpdateGameDrawing()
11+
end
12+
13+
function ControlButtonsModel:AddListener(listener)
14+
table.insert(self.listeners, listener)
15+
end
16+
17+
function ControlButtonsModel:NotifyListeners(event, ...)
18+
for _, listener in ipairs(self.listeners) do
19+
if listener[event] then
20+
listener[event](listener, ...)
21+
end
22+
end
23+
end
24+
25+
function ControlButtonsModel:OnStartStop()
26+
local frame = Spring.GetGameFrame()
27+
if self.__lastFrame then
28+
if frame - self.__lastFrame < 15 then
29+
return
30+
end
31+
end
32+
self.__lastFrame = frame
33+
34+
if not self.started then
35+
local cmd = StartCommand()
36+
SB.commandManager:execute(cmd)
37+
self:GameStarted()
38+
else
39+
local cmd = StopCommand()
40+
SB.commandManager:execute(cmd)
41+
self:GameStopped()
42+
end
43+
end
44+
45+
function ControlButtonsModel:OnToggleUI()
46+
SB.view:SetVisible(not SB.view.__visible)
47+
end
48+
49+
-- All this better belongs to some command/model
50+
function ControlButtonsModel:UpdateGameDrawing()
51+
-- show/hide SB GUI
52+
if SB.view then
53+
if not self.started then
54+
SB.view:SetVisible(true)
55+
else
56+
SB.view:SetVisible(false)
57+
end
58+
end
59+
60+
if self.started then
61+
SB.delay(function()
62+
local success, msg = pcall(function()
63+
local OnStopEditingUnsynced = SB.model.game.OnStopEditingUnsynced
64+
if OnStopEditingUnsynced then
65+
OnStopEditingUnsynced()
66+
end
67+
end)
68+
if not success then
69+
Log.Error(msg)
70+
Log.Error("Error in custom OnStopEditingUnsynced")
71+
end
72+
end)
73+
else
74+
SB.delay(function()
75+
local success, msg = pcall(function()
76+
local OnStartEditingUnsynced = SB.model.game.OnStartEditingUnsynced
77+
if OnStartEditingUnsynced then
78+
OnStartEditingUnsynced()
79+
end
80+
end)
81+
if not success then
82+
Log.Error(msg)
83+
Log.Error("Error in custom OnStartEditingUnsynced")
84+
end
85+
end)
86+
end
87+
end
88+
89+
function ControlButtonsModel:GameStarted()
90+
self.started = true
91+
self:UpdateGameDrawing()
92+
self:NotifyListeners("OnGameStarted")
93+
end
94+
95+
function ControlButtonsModel:GameStopped()
96+
self.started = false
97+
self:UpdateGameDrawing()
98+
self:NotifyListeners("OnGameStopped")
99+
end
100+
101+
function ControlButtonsModel:IsStarted()
102+
return self.started
103+
end
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
StatusWindowModel = LCS.class{}
2+
3+
function StatusWindowModel:init()
4+
self.posStr = "Off-screen"
5+
self.selectionStr = "No selection"
6+
self.memoryStr = "Memory 0 MB"
7+
self.versionStr = ""
8+
self.update = 0
9+
self.warnedTime = nil
10+
end
11+
12+
function StatusWindowModel:Initialize()
13+
SB.delay(function()
14+
SB.view.selectionManager:addListener(self)
15+
self:OnSelectionChanged()
16+
end)
17+
18+
self:UpdateVersion()
19+
end
20+
21+
function StatusWindowModel:Update()
22+
self:UpdateSelection()
23+
self:UpdateMemory()
24+
self.update = self.update + 1
25+
end
26+
27+
function StatusWindowModel:UpdateSelection()
28+
local x, y = Spring.GetMouseState()
29+
local result, coords = Spring.TraceScreenRay(x, y, true)
30+
if result == "ground" then
31+
local worldX, worldY, worldZ = coords[1], coords[2], coords[3]
32+
self.posStr = string.format("X: %d, Y: %d, Z: %d", worldX, worldY, worldZ)
33+
else
34+
self.posStr = "Off-screen"
35+
end
36+
end
37+
38+
function StatusWindowModel:UpdateMemory()
39+
if self.update % 60 ~= 0 then
40+
return
41+
end
42+
43+
local videoMemoryStr
44+
if Spring.GetVidMemUsage then
45+
local vram, vramMax = Spring.GetVidMemUsage()
46+
videoMemoryStr = ("Video memory: %.0f/%.0f MB"):format(vram, vramMax)
47+
end
48+
49+
local memory
50+
if Spring.GetLuaMemUsage then
51+
local memoryInStates = {Spring.GetLuaMemUsage()}
52+
memory = memoryInStates[3] / 1024
53+
else
54+
memory = collectgarbage("count") / 1024
55+
end
56+
57+
-- Detect extensive memory usage
58+
local color = SB.conf.STATUS_TEXT_OK_COLOR
59+
local memoryWarnLevel = 500
60+
if string.find(Engine.versionFull, "BIGMEM", nil, true) then
61+
memoryWarnLevel = 16000
62+
end
63+
64+
if memory > memoryWarnLevel then
65+
color = SB.conf.STATUS_TEXT_DANGER_COLOR
66+
if not self.warnedTime or os.clock() - self.warnedTime > 10 then
67+
self.warnedTime = os.clock()
68+
WG.Chotify:Post({
69+
body = SB.conf.STATUS_TEXT_DANGER_COLOR .. "Danger:\b\255\255\255\255 Large memory usage, may lead to a crash if it increases further.\n\n" ..
70+
"Consider clearing the undo-redo stack to free memory.\b",
71+
title = "Low Memory",
72+
time = 10,
73+
})
74+
SB.stateManager:SetState(DefaultState())
75+
end
76+
elseif memory > 300 then
77+
color = SB.conf.STATUS_TEXT_WARN_COLOR
78+
end
79+
80+
self.memoryStr = "Memory " .. color .. ('%.0f'):format(memory) .. " MB\b"
81+
if videoMemoryStr then
82+
self.memoryStr = self.memoryStr .. " " .. videoMemoryStr
83+
end
84+
end
85+
86+
function StatusWindowModel:UpdateVersion()
87+
local launcherVersion = SB_LAUNCHER_VERSION and (' (' .. SB_LAUNCHER_VERSION .. ')') or ''
88+
self.versionStr = Game.gameName .. "-" .. Game.gameVersion .. launcherVersion
89+
end
90+
91+
function StatusWindowModel:OnSelectionChanged()
92+
local selCount = SB.view.selectionManager:GetSelectionCount()
93+
if selCount == 1 then
94+
local selection = SB.view.selectionManager:GetSelection()
95+
local objectID
96+
for _, v in pairs(selection) do
97+
if v and #v == 1 then
98+
objectID = v[1]
99+
end
100+
end
101+
self.selectionStr = string.format("Selected : 1 (ID=%d)", objectID)
102+
elseif selCount > 0 then
103+
self.selectionStr = string.format("Selected: %d", selCount)
104+
else
105+
self.selectionStr = "No selection"
106+
end
107+
end
108+
109+
function StatusWindowModel:GetStatusText()
110+
return self.posStr .. ". " .. self.selectionStr
111+
end
112+
113+
function StatusWindowModel:GetMemoryText()
114+
return self.memoryStr
115+
end
116+
117+
function StatusWindowModel:GetVersionText()
118+
return self.versionStr
119+
end

0 commit comments

Comments
 (0)