-- Smart Pickup — ESC-fane -- Viser og redigerer runs og starter/stopper dem. Al logik ligger i ADRunsController; -- her oversættes kun mellem GUI-elementer og run-felter. Alt kører i pcall så en GUI-fejl -- aldrig tager spillet med sig. SmartPickupFrame = {} SmartPickupFrame._mt = Class(SmartPickupFrame, TabbedMenuFrameElement) SmartPickupFrame.REFRESH_INTERVAL_MS = 1000 SmartPickupFrame.STATUS_OVERRIDE_TICKS = 6 SmartPickupFrame.MAX_STATUS_REASONS = 3 SmartPickupFrame.LOG_PREFIX = "ADSmartPickup: " SmartPickupFrame.STATE_OFF, SmartPickupFrame.STATE_ON = 1, 2 SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_FLOWS, SmartPickupFrame.TAB_FIELDS, SmartPickupFrame.TAB_MONTH, SmartPickupFrame.TAB_FIELDSILOS = 1, 2, 3, 4, 5 -- Måned-fanen genopbygges hvert N. sekund (ingen klikbare rækker, så fokus går ikke tabt) SmartPickupFrame.MONTH_REBUILD_TICKS = 5 -- Marksilo-fanen har klikbare valg: genopbygges sjældnere, så fokus ikke rykker midt i et valg SmartPickupFrame.FIELDSILO_REBUILD_TICKS = 30 -- Lagerfaner: én pr. bygningstype (indeks -> kind i ADSources.getFarmInventory) SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_month", "spu_tab_fieldsilos", "spu_tab_factories", "spu_tab_silos", "spu_tab_barns", "spu_tab_sheds"} SmartPickupFrame.TAB_KINDS = {[6] = "factory", [7] = "silo", [8] = "husbandry", [9] = "objectStorage"} function SmartPickupFrame.getTabKind(tab) return SmartPickupFrame.TAB_KINDS[tab] end SmartPickupFrame.POLICIES = {"off", "store", "sell", "sellElseStore"} -- Afvisningsgrunde der handler om VOGNEN (ikke run'ets opsætning): traktoren kan ikke vælges. SmartPickupFrame.VEHICLE_BLOCKING_REASONS = {noTrailer = true, cannotCarry = true, needsAutoload = true, autoloadOn = true} function SmartPickupFrame.new(i18n, getAdEnv) local self = SmartPickupFrame:superClass().new(nil, SmartPickupFrame._mt) self.name = "SmartPickupFrame" self.i18n = i18n self.getAdEnv = getAdEnv self.selectedRunId = nil self.runIds, self.targetIds, self.poolGroups = {}, {}, {} self.rowData = {} -- element -> {kind = "ingredient"|"vehicle", ...} self.clonedRows = {} self.refreshTimer = 0 self.statusOverride = nil self.statusRow = nil -- Text-elementet i den klonede statusrække self.needsRows = {} self.pendingRebuild = false self.frameOpen = false self.focusLost = false self.currentTab = SmartPickupFrame.TAB_OVERVIEW self.subCategoryTabs = {} self.backButtonInfo = {inputAction = InputAction.MENU_BACK} self.startButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_start"), callback = function() self:onClickStartStop() end} self.newButtonInfo = {inputAction = InputAction.MENU_EXTRA_1, text = i18n:getText("spu_button_new"), callback = function() self:onClickNew() end} self.renameButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_rename"), callback = function() self:onClickRename() end} self.deleteButtonInfo = {inputAction = InputAction.MENU_CANCEL, text = i18n:getText("spu_button_delete"), callback = function() self:onClickDelete() end} -- Oversigt: Start/Stop alle i stedet for knapperne der virker på ét valgt flow. self.startAllButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_startAll"), callback = function() self:onClickStartAll() end} self.stopAllButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_stopAll"), callback = function() self:onClickStopAll() end} self.monthButtons = {self.backButtonInfo} self.flowButtons = {self.backButtonInfo, self.startButtonInfo, self.newButtonInfo, self.renameButtonInfo, self.deleteButtonInfo} self.overviewButtons = {self.backButtonInfo, self.startAllButtonInfo, self.stopAllButtonInfo, self.newButtonInfo} -- Marker: Start nu / Stop for den valgte mark -- Marker: [Start nu/Stop], [Opsæt mark], [Markflåde] (gui/SmartPickupFieldGuide.lua); guidens knapper self:initFieldButtons(i18n) self:initGuideButtons(i18n) -- Flows: [Start/Stop] [Opsæt flow] [Nyt flow] [Slet] (gui/SmartPickupFlowGuide.lua) self:initFlowButtons(i18n) -- Egen reference til listen: fallback'en i refreshButtons skal kunne sende den igen. self.menuButtonInfo = self.overviewButtons self:setMenuButtonInfo(self.menuButtonInfo) return self end function SmartPickupFrame:delete() SmartPickupFrame:superClass().delete(self) end function SmartPickupFrame:copyAttributes(src) SmartPickupFrame:superClass().copyAttributes(self, src) self.i18n, self.getAdEnv = src.i18n, src.getAdEnv end -- Prefabs skal ud af layoutet før de klones, ellers vises de selv som rækker. function SmartPickupFrame:initialize() for _, name in ipairs({"ingredientPrefab", "vehiclePrefab", "textPrefab", "productPrefab", "statPrefab", "gridPrefab", "flowPrefab", "fieldPrefab"}) do local prefab = self[name] if prefab ~= nil then prefab:unlinkElement() FocusManager:removeElement(prefab) end end if self.selectorPrefab ~= nil and self.subCategoryBox ~= nil and self.subCategoryPaging ~= nil then self.selectorPrefab:unlinkElement() FocusManager:removeElement(self.selectorPrefab) for index, key in ipairs(SmartPickupFrame.TAB_NAMES) do self.subCategoryPaging:addText(tostring(index)) local tab = self.selectorPrefab:clone(self.subCategoryBox) FocusManager:loadElementFromCustomValues(tab) tab:setText(self.i18n:getText(key)) local background = tab:getDescendantByName("background") if background ~= nil then background:setSize(tab.size[1], tab.size[2]) end local tabIndex = index tab.onClickCallback = function() self:switchTab(tabIndex) end self.subCategoryTabs[index] = tab end self.subCategoryBox:invalidateLayout() self.subCategoryPaging:setSize(self.subCategoryBox.maxFlowSize + 140 * g_pixelSizeScaledX) end local loopTexts = {self.i18n:getText("spu_loops_infinite")} for loops = 1, ADRuns.MAX_LOOPS do table.insert(loopTexts, tostring(loops)) end self.loopsOption:setTexts(loopTexts) self.returnBelowSteps = {} local percentTexts = {} for percent = ADRuns.MIN_RETURN_BELOW_PERCENT, ADRuns.MAX_RETURN_BELOW_PERCENT, 10 do table.insert(self.returnBelowSteps, percent) table.insert(percentTexts, string.format("%d %%", percent)) end self.returnBelowOption:setTexts(percentTexts) self.sellAtSteps, self.storeAboveSteps = {}, {} local sellTexts, storeTexts = {}, {} for percent = ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5 do table.insert(self.sellAtSteps, percent) table.insert(sellTexts, string.format("%d %%", percent)) end for percent = ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT, 5 do table.insert(self.storeAboveSteps, percent) table.insert(storeTexts, string.format("%d %%", percent)) end if self.sellAtOption ~= nil then self.sellAtOption:setTexts(sellTexts) end if self.storeAboveOption ~= nil then self.storeAboveOption:setTexts(storeTexts) end end -- Indeks i en trin-liste for en procent (største trin <= værdien). function SmartPickupFrame.getStepIndex(steps, value, default) local index = 1 for position, percent in ipairs(steps or {}) do if percent <= (value or default) then index = position end end return index end function SmartPickupFrame:getReturnBelowIndex(run) local index = 1 for position, percent in ipairs(self.returnBelowSteps or {}) do if percent <= (run.returnBelowPercent or ADRuns.DEFAULT_RETURN_BELOW_PERCENT) then index = position end end return index end -- FocusManager har skiftet API mellem FS-versioner; her hentes det fokuserede element -- så blødt som muligt, fordi det kun bruges til at opdage at fokus lige er slettet. function SmartPickupFrame.getFocusedElement() if FocusManager == nil then return nil end if FocusManager.getFocusedElement ~= nil then local ok, element = pcall(FocusManager.getFocusedElement, FocusManager) if ok then return element end end local data = FocusManager.currentFocusData if type(data) == "table" then return data.focusElement end return nil end function SmartPickupFrame.isDescendantOf(element, ancestor) while element ~= nil do if element == ancestor then return true end element = element.parent end return false end -- rebuild sletter de rækker der kan have fokus (fx vare-rækkerne når Varer skifter til -- Auto); uden dette står fokus på et slettet element og tastatur/controller er død. function SmartPickupFrame:restoreFocusIfLost() if self.focusLost and self.frameOpen and self.boxLayout ~= nil then FocusManager:setFocus(self.boxLayout) end self.focusLost = false end function SmartPickupFrame:onFrameOpen() SmartPickupFrame:superClass().onFrameOpen(self) self.statusOverride = nil self.frameOpen = true self.pendingRebuild = false self:safe(self.rebuild) -- Som AutoDrive's egne sider: fokus på selve listen, ikke på en enkelt række. self:safe(function(frame) FocusManager:setFocus(frame.boxLayout) end) end function SmartPickupFrame:onFrameClose() self.frameOpen = false -- en åben guide kasseres (intet er gemt før [Gem]) self:closeGuide() SmartPickupFrame:superClass().onFrameClose(self) end -- Genopbygning sker her og aldrig inde i et klik-callback: sletter man elementet midt i -- dets egen klik-håndtering, hopper værdien frem og tilbage og fokus ryger. function SmartPickupFrame:update(dt) SmartPickupFrame:superClass().update(self, dt) if self.pendingClick ~= nil then local action = self.pendingClick self.pendingClick = nil self:safe(action) elseif self.pendingRebuild then self.pendingRebuild = false self:safe(self.rebuild) end self.refreshTimer = self.refreshTimer + dt if self.refreshTimer >= SmartPickupFrame.REFRESH_INTERVAL_MS then self.refreshTimer = 0 self:safe(self.refreshLive) end end function SmartPickupFrame:safe(fn, ...) local ok, err = pcall(fn, self, ...) if not ok then Logging.warning(SmartPickupFrame.LOG_PREFIX .. "GUI-fejl: %s", tostring(err)) self.statusOverride = string.format(self.i18n:getText("spu_status_error"), tostring(err)) self:setStatusText(self.statusOverride) end end function SmartPickupFrame:setStatusText(text) if self.statusRow ~= nil then self.statusRow:setText(text) end end -- ESC-menuen kalder ikke altid update() på siden, så genopbygningen sker straks. De -- elementer der udløser den (Run/Mål/Varer-tilstand, knapperne) er statiske og slettes -- ikke af rebuild, så det er sikkert; pendingRebuild er kun bagstopper. function SmartPickupFrame:requestRebuild() self.pendingRebuild = false self:safe(self.rebuild) end function SmartPickupFrame:switchTab(tabIndex) if self.guide ~= nil and not self:hasGuideOn(tabIndex) then self:closeGuide() end self.currentTab = tabIndex for index, tab in ipairs(self.subCategoryTabs) do tab:setSelected(index == tabIndex) end if self.subCategoryPaging ~= nil then self.subCategoryPaging:setState(tabIndex, false) end self:requestRebuild() self:safe(function(frame) FocusManager:setFocus(frame.boxLayout) end) end function SmartPickupFrame:onClickTab(state) self:switchTab(state) end -- De faste rækker: Flow-vælger + Wait-pulje for alle flows; Ture/kør hjem for forsyning; Sælg fra/ -- Kør til lager for udkørsel. Fanen Lagre viser ingen. function SmartPickupFrame:setStaticRowsVisible(tab, run) -- Flows-fanen bruger tavle + resumé + guide (v1.32); de faste XML-rækker vises ikke længere local isFlows = false local hasRun = isFlows and run ~= nil local isSupply, isOutbound = hasRun and run.kind ~= "outbound", hasRun and run.kind == "outbound" local visibility = { {self.runOption, isFlows}, {self.waitPoolOption, hasRun}, {self.loopsOption, isSupply}, {self.returnBelowOption, isSupply}, {self.sellAtOption, isOutbound}, {self.storeAboveOption, isOutbound}, } for _, entry in ipairs(visibility) do local option = entry[1] if option ~= nil and option.parent ~= nil then option.parent:setVisible(entry[2]) end end end -- Samme rækkefølge som tavlen: Ind til bygning først, så Ud fra bygning, hver efter navn. function SmartPickupFrame:getRunsForTab() if ADFlowView ~= nil then return ADFlowView.orderedRuns(ADRunsController.store.runs) end return ADRunsController.store.runs end function SmartPickupFrame:getRun() return self.selectedRunId ~= nil and ADRuns.getRun(ADRunsController.store, self.selectedRunId) or nil end -- ---- opbygning ---- -- addRow melder hver klonet række ind i FocusManager; uden afmelding her ville -- FocusManager beholde slettede rækker og fokus-navigationen gå i stå. function SmartPickupFrame:clearRows() local focused = SmartPickupFrame.getFocusedElement() for _, row in ipairs(self.clonedRows) do if focused ~= nil and SmartPickupFrame.isDescendantOf(focused, row) then self.focusLost = true end FocusManager:removeElement(row) row:delete() end self.clonedRows, self.rowData = {}, {} self.statusRow = nil self.needsRows = {} self.overviewRows = {} self.productRows, self.outboundStatusRows = {}, {} self.storageRows = {} self.storageDetailRows = {} self.boardRows = {} self.ingredientModeRowOption, self.collectRoundRowOption, self.directionRowOption = nil, nil, nil end -- Ja/Nej-række som klon af ingredientPrefab (faste BinaryOption-elementer fra XML'en viser kun -- første valg grønt i FS25; klonede rækker virker begge veje). texts = {venstre, højre}. function SmartPickupFrame:addToggleRow(kind, title, texts, isOn) local row = self:addRow(self.ingredientPrefab, {kind = kind}) local option = row:getDescendantByName("option") option:setTexts(texts) option:setState(isOn and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false) row:getDescendantByName("title"):setText(title) return option end function SmartPickupFrame:addRow(prefab, data) local row = prefab:clone(self.boxLayout) FocusManager:loadElementFromCustomValues(row) table.insert(self.clonedRows, row) local option = row:getDescendantByName("option") if option ~= nil then option:setTexts({self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")}) self.rowData[option] = data end return row end -- Tekstrække uden valgmuligheder: sektionstitler, Wait-pulje-linjer og statuslinjen. -- Tegn pr. tekstrække før teksten ombrydes (rækken afkorter med "..." ved ca. 70 tegn). SmartPickupFrame.TEXT_ROW_CHARS = 66 -- Lange tekster fordeles på flere rækker i stedet for at blive afkortet. Returnerer første rækkes titel. function SmartPickupFrame:addTextRow(text) local first = nil for _, line in ipairs(ADFlowView.wrapText(text, SmartPickupFrame.TEXT_ROW_CHARS)) do local title = self:addRow(self.textPrefab, {kind = "text"}):getDescendantByName("title") title:setText(line) first = first or title end return first end -- AutoDrive farver hver række i onCreate; her sker det efter hver genopbygning, -- fordi rækkerne først findes på det tidspunkt. function SmartPickupFrame:colorRows() local colors = InGameMenuSettingsFrame ~= nil and InGameMenuSettingsFrame.COLOR_ALTERNATING or nil if colors == nil or self.boxLayout == nil then return end local unpackFn = table.unpack or unpack local isEvenRow = false for _, element in ipairs(self.boxLayout.elements) do if element.setImageColor ~= nil and colors[isEvenRow] ~= nil then element:setImageColor(nil, unpackFn(colors[isEvenRow])) end isEvenRow = not isEvenRow end end function SmartPickupFrame:rebuild() local adEnv = self.getAdEnv() local store = ADRunsController.store if self.selectedRunId == nil or ADRuns.getRun(store, self.selectedRunId) == nil then self.selectedRunId = store.runs[1] ~= nil and store.runs[1].id or nil end self:clearRows() local run = self:getRun() self:setStaticRowsVisible(self.currentTab, run) if SmartPickupFrame.getTabKind(self.currentTab) ~= nil then if adEnv ~= nil then self:fillStorageRows(adEnv) end self.statusRow = self:addTextRow("") self:colorRows() self.boxLayout:invalidateLayout() self:restoreFocusIfLost() self:refreshLive() return end if self.currentTab == SmartPickupFrame.TAB_FIELDS then if adEnv ~= nil then self:fillFieldRows(adEnv) end self.statusRow = self:addTextRow("") self:colorRows() self.boxLayout:invalidateLayout() self:refreshLive() self:restoreFocusIfLost() return end if self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then if adEnv ~= nil then self:fillFieldSiloRows(adEnv) end self.monthRebuildTicks = SmartPickupFrame.FIELDSILO_REBUILD_TICKS self:colorRows() self.boxLayout:invalidateLayout() self:refreshButtons() self:restoreFocusIfLost() return end if self.currentTab == SmartPickupFrame.TAB_MONTH then if adEnv ~= nil then self:fillMonthRows(adEnv) end self.monthRebuildTicks = SmartPickupFrame.MONTH_REBUILD_TICKS self:colorRows() self.boxLayout:invalidateLayout() self:refreshButtons() self:restoreFocusIfLost() return end if self.currentTab == SmartPickupFrame.TAB_OVERVIEW then if adEnv ~= nil then self:fillOverviewRows(adEnv) end self.statusRow = self:addTextRow("") self:addTextRow(self.i18n:getText("spu_waitPools")) self:fillPoolRows(adEnv) self:colorRows() self.boxLayout:invalidateLayout() self:refreshLive() self:restoreFocusIfLost() return end if adEnv ~= nil then self:fillFlowTabRows(adEnv) end self.statusRow = self:addTextRow("") self:colorRows() self.boxLayout:invalidateLayout() self:refreshLive() self:restoreFocusIfLost() end -- Overblik: "Cow 1 food: TMR 18.0k/27.8k · Silage 3.0k/…" (mest trængende først) function SmartPickupFrame:describeTargetLevels(target) if #target.levels == 0 then return string.format("%s: %s", target.name, self.i18n:getText("spu_needs_none")) end return string.format("%s: %s", target.name, self:formatLevels(target.levels)) end -- Felterne følger modellen: når et klik ikke er nået ind i run'et, springer feltet tilbage, -- og loggen ("run '…': waitPoolGroup=…") viser om klikket blev registreret. function SmartPickupFrame:syncOptionsFromModel(adEnv, run) local function sync(option, wantedIndex) if option ~= nil and wantedIndex ~= nil and SmartPickupFrame.getOptionState(option) ~= wantedIndex then option:setState(wantedIndex, false) end end local poolIndex = 1 for index, group in ipairs(self.poolGroups) do if group == run.waitPoolGroup then poolIndex = index end end sync(self.waitPoolOption, poolIndex) sync(self.directionRowOption, run.kind == "outbound" and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF) if run.kind == "outbound" then sync(self.sellAtOption, SmartPickupFrame.getStepIndex(self.sellAtSteps, run.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT)) sync(self.storeAboveOption, SmartPickupFrame.getStepIndex(self.storeAboveSteps, run.storeAbovePercent, ADRuns.DEFAULT_STORE_ABOVE_PERCENT)) sync(self.collectRoundRowOption, run.collectRound and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF) return end sync(self.ingredientModeRowOption, run.ingredientMode == "manual" and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF) sync(self.loopsOption, run.loops + 1) sync(self.returnBelowOption, self:getReturnBelowIndex(run)) end -- "Silage 0/33.750 l · Straw 21.375/21.375 l" — mest trængende først. -- Kompakte tal, så 4–5 varer kan stå på én række: 18.0k/27.8k function SmartPickupFrame.formatCompact(liters) if liters >= 1000000 then return string.format("%.1fM", liters / 1000000) elseif liters >= 1000 then return string.format("%.1fk", liters / 1000) end return string.format("%d", liters) end function SmartPickupFrame:formatLevels(levels) local parts = {} for _, need in ipairs(levels) do local title = g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(need.fillType) or g_fillTypeManager:getFillTypeNameByIndex(need.fillType) table.insert(parts, string.format("%s %s/%s", tostring(title), SmartPickupFrame.formatCompact(need.level), SmartPickupFrame.formatCompact(need.capacity))) end return table.concat(parts, " · ") end -- ---- overblik (øverst): ét afsnit pr. run — status, traktorernes aktivitet, målets niveauer ---- function SmartPickupFrame:describeActivity(adEnv, vehicle) local activity = ADRunsController.getVehicleActivity(adEnv, vehicle) local label = ADRunsController.getVehicleLabel(adEnv, vehicle) local fillTypeName = activity.fillType ~= nil and (g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(activity.fillType) or g_fillTypeManager:getFillTypeNameByIndex(activity.fillType)) or "?" local place = tostring(activity.place or "?") if activity.kind == "stopped" then return string.format("%s: %s", label, self.i18n:getText("spu_activity_stopped")) elseif activity.kind == "waiting" then return string.format("%s: %s", label, string.format(self.i18n:getText("spu_activity_waiting"), place, g_i18n:formatNumber(activity.liters or 0, 0), fillTypeName)) elseif activity.kind == "loaded" then return string.format("%s: %s", label, string.format(self.i18n:getText("spu_activity_loaded"), g_i18n:formatNumber(activity.liters or 0, 0), fillTypeName, place)) elseif activity.kind == "holding" then return string.format("%s: %s", label, string.format(self.i18n:getText("spu_activity_holding"), place, tostring(activity.reason or ""))) end if activity.fillType ~= nil then return string.format("%s: %s", label, string.format(self.i18n:getText("spu_activity_empty"), place, fillTypeName)) end return string.format("%s: %s", label, string.format(self.i18n:getText("spu_activity_driving"), place)) end function SmartPickupFrame:fillPoolRows(adEnv) if adEnv == nil then return end local occupied = {} for _, claim in ipairs(ADUnloadWait.getClaims()) do occupied[claim.marker.group] = occupied[claim.marker.group] or {} -- kun løbenummeret ("#31") — fulde navne klippes af i rækken local label = ADRunsController.getVehicleLabel(adEnv, claim.vehicle) table.insert(occupied[claim.marker.group], string.match(label, "%(#(%d+)%)") and ("#" .. string.match(label, "%(#(%d+)%)")) or label) end for _, pool in ipairs(ADRunsController.getWaitPools(adEnv)) do local names = occupied[pool.group] or {} local suffix = #names > 0 and (" (" .. table.concat(names, ", ") .. ")") or "" self:addTextRow(string.format(self.i18n:getText("spu_waitPoolRow"), pool.group, pool.count, #names, suffix)) end end -- BinaryOption har :getState() i FS25, men feltet .state i ældre GUI-versioner. function SmartPickupFrame.getOptionState(option) return option.getState ~= nil and option:getState() or option.state end -- setMenuButtonInfoDirty findes i FS22/25's TabbedMenuFrameElement; mangler den, -- sendes listen igen. Kaldes hvert sekund, så den må aldrig kaste. function SmartPickupFrame:refreshButtons() local wanted = self.flowButtons if self:hasGuideOn(self.currentTab) then wanted = self:getGuideButtons() elseif self.currentTab == SmartPickupFrame.TAB_OVERVIEW then wanted = self.overviewButtons elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then wanted = self:getFieldButtons() elseif self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then wanted = self.monthButtons end if self.menuButtonInfo ~= wanted then self.menuButtonInfo = wanted self:setMenuButtonInfo(wanted) end if self.setMenuButtonInfoDirty ~= nil then self:setMenuButtonInfoDirty() else self:setMenuButtonInfo(self.menuButtonInfo) end end -- Status + Start/Stop-knappens tekst, kaldes hvert sekund. function SmartPickupFrame:refreshLive() local adEnv = self.getAdEnv() local run = self:getRun() if adEnv == nil then self:setStatusText(self.i18n:getText("spu_status_noAd")) return end if self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then self.monthRebuildTicks = (self.monthRebuildTicks or 0) - 1 if self.monthRebuildTicks <= 0 then self:requestRebuild() end self:refreshButtons() return end local isOverview = self.currentTab == SmartPickupFrame.TAB_OVERVIEW if isOverview then self:refreshOverview(adEnv) end if self.currentTab == SmartPickupFrame.TAB_FIELDS then self:refreshFieldRows(adEnv) self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1 if self.statusOverrideTicks <= 0 then self.statusOverride = nil end self:refreshButtons() return end if self.currentTab == SmartPickupFrame.TAB_FLOWS then self:refreshFlowTab(adEnv) self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1 if self.statusOverrideTicks <= 0 then self.statusOverride = nil end self:refreshButtons() return end self:refreshStorageRows(adEnv) local isRunning = false if isOverview then local running, total, activeRuns = 0, 0, 0 for _, anyRun in ipairs(ADRunsController.store.runs) do local status = ADRunsController.getRunStatus(adEnv, anyRun) running, total = running + status.running, total + status.total if status.running > 0 then activeRuns = activeRuns + 1 end end self:setStatusText(self.statusOverride or string.format(self.i18n:getText("spu_status_overview"), running, total, activeRuns, #ADRunsController.store.runs)) self.stopAllButtonInfo.disabled = running == 0 elseif run ~= nil then self:syncOptionsFromModel(adEnv, run) if run.kind == "outbound" then self:refreshOutboundRows(adEnv, run) else self:refreshNeedsRows(adEnv, run) end local status = ADRunsController.getRunStatus(adEnv, run) isRunning = status.isRunning == true local text = status.running > 0 and string.format(self.i18n:getText("spu_status_running"), status.running, status.total) or string.format(self.i18n:getText("spu_status_stopped"), status.total) self:setStatusText(self.statusOverride or text) for option, data in pairs(self.rowData) do -- kun run'ets egne traktorer opdateres live (alle valideres ved rebuild) local isInRun = SmartPickupFrame.getOptionState(option) == SmartPickupFrame.STATE_ON if data.kind == "vehicle" and data.row ~= nil and isInRun then data.row:getDescendantByName("title"):setText(self:describeVehicle(adEnv, run, data.vehicle, true)) end end else self:setStatusText(self.statusOverride or self.i18n:getText("spu_noRuns")) end -- Engangsbesked ("Startet 2, sprunget over 1: …", "flyttet fra ..."): står i -- STATUS_OVERRIDE_TICKS opdateringer (sekunder), så man når at læse hvorfor. self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1 if self.statusOverrideTicks <= 0 then self.statusOverride = nil end self.startButtonInfo.text = self.i18n:getText(isRunning and "spu_button_stop" or "spu_button_start") self.startButtonInfo.disabled = run == nil self.renameButtonInfo.disabled = run == nil self.deleteButtonInfo.disabled = run == nil self:refreshButtons() end -- ---- callbacks fra XML (state = 1-baseret indeks; element = det klikkede element) ---- function SmartPickupFrame:onClickRun(state) self.selectedRunId = self.runIds[state] self.statusOverride = nil self:requestRebuild() end function SmartPickupFrame:onClickIngredient(state, element) -- guiderne og markflåden deler prefab (og dermed onClick) med vare-rækkerne local data = self.rowData[element] if data ~= nil and data.kind == "guideRow" then return self:onClickGuideRow(state, data) elseif data ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS[data.kind] then return self:onClickFieldSiloRow(state, data) elseif data ~= nil and SmartPickupFrame.FIELD_ROW_KINDS[data.kind] then return self:onClickFieldRow(state, data) end self:safe(function(frame) local run, data = frame:getRun(), frame.rowData[element] if run == nil or data == nil or data.kind ~= "ingredient" then return end local names = {} for _, name in ipairs(run.ingredients) do if name ~= data.name then table.insert(names, name) end end if state == SmartPickupFrame.STATE_ON then table.insert(names, data.name) end ADRunsController.updateRun(run.id, {ingredients = names}) end) end function SmartPickupFrame:onClickWaitPool(state) self:safe(function(frame) local run = frame:getRun() if run == nil then return end ADRunsController.updateRun(run.id, {waitPoolGroup = frame.poolGroups[state] or ""}) end) end function SmartPickupFrame:onClickVehicle(state, element) local clicked = self.rowData[element] -- markfanens maskinrækker (markflåden og markens faste maskiner) bruger samme række-type som flowets -- traktorliste; de hører til markfanen (fieldRig blev før behandlet som flow-traktor og aldrig gemt) if clicked ~= nil and clicked.kind == "fleetVehicle" then return self:onClickFieldRow(state, clicked) end if clicked ~= nil and clicked.kind == "guideRow" then return self:onClickGuideRow(state, clicked) end self:safe(function(frame) local run, data = frame:getRun(), frame.rowData[element] -- kun flowets traktorrækker (de har et køretøj); alt andet ignoreres if run == nil or data == nil or data.locked or data.vehicle == nil then return end if state == SmartPickupFrame.STATE_ON then local previousRunId = ADRunsController.assignVehicle(run.id, data.vehicle) if previousRunId ~= nil then local previous = ADRuns.getRun(ADRunsController.store, previousRunId) frame.statusOverride = string.format(frame.i18n:getText("spu_movedFrom"), previous ~= nil and previous.name or tostring(previousRunId)) frame.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS end else ADRunsController.unassignVehicle(data.vehicle) end frame:refreshLive() end) end -- ---- knapper i bundlinjen ---- function SmartPickupFrame:onClickStartStop() self:safe(function(frame) local adEnv, run = frame.getAdEnv(), frame:getRun() if adEnv == nil or run == nil then return end if ADRunsController.getRunStatus(adEnv, run).isRunning then ADRunsController.stopRun(adEnv, run.id) frame.statusOverride = nil else local started, skipped, reasons = frame:summarizeStartResults(ADRunsController.startRun(adEnv, run.id)) frame:showOverride(string.format(frame.i18n:getText("spu_status_started"), started, skipped), reasons) end ADRunsController.save() frame:requestRebuild() end) end -- Start-resultater -> antal startet/sprunget over + "Traktor: grund"-liste til statuslinjen. function SmartPickupFrame:summarizeStartResults(results) local started, skipped, reasons = 0, 0, {} for _, result in ipairs(results) do if result.ok then started = started + 1 else skipped = skipped + 1 local reasonText = self.i18n:getText("spu_reason_" .. tostring(result.reason)) if result.reason == "noMarker" and result.missingBuilding ~= nil then reasonText = string.format(reasonText, tostring(result.missingBuilding)) end table.insert(reasons, string.format("%s: %s", tostring(ADRuns.vehicleLabel(result.vehicle)), reasonText)) end end return started, skipped, reasons end function SmartPickupFrame:showOverride(text, reasons) if reasons ~= nil and #reasons > 0 then -- én statuslinje: højst MAX_STATUS_REASONS grunde, resten står i log.txt local shown = {} for index = 1, math.min(#reasons, SmartPickupFrame.MAX_STATUS_REASONS) do shown[index] = reasons[index] end text = text .. " — " .. table.concat(shown, "; ") if #reasons > #shown then text = text .. string.format(" (+%d)", #reasons - #shown) end end self.statusOverride = text self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS self:setStatusText(text) end function SmartPickupFrame:onClickStartAll() self:safe(function(frame) local adEnv = frame.getAdEnv() if adEnv == nil then return end local summary = ADRunsController.startAllRuns(adEnv) local _, skipped, reasons = frame:summarizeStartResults(summary.results) Logging.info(SmartPickupFrame.LOG_PREFIX .. "Start alle: %d flows startet (%d traktorer), %d kørte allerede, %d sprunget over", summary.runsStarted, summary.vehiclesStarted, summary.alreadyRunning, skipped) frame:showOverride(string.format(frame.i18n:getText("spu_status_startedAll"), summary.runsStarted, summary.vehiclesStarted, summary.alreadyRunning, skipped), reasons) ADRunsController.save() frame:requestRebuild() end) end function SmartPickupFrame:onClickStopAll() self:safe(function(frame) local adEnv = frame.getAdEnv() if adEnv == nil then return end local stopped = ADRunsController.stopAllRuns(adEnv) Logging.info(SmartPickupFrame.LOG_PREFIX .. "Stop alle: %d traktorer stoppet", stopped) frame:showOverride(string.format(frame.i18n:getText("spu_status_stoppedAll"), stopped)) ADRunsController.save() frame:requestRebuild() end) end -- Klik på en række i Oversigt: vælg flowet og hop til fanen Flows (kaldes fra update, aldrig inde i -- rækkens eget mouseEvent — rækken slettes af genopbygningen). function SmartPickupFrame:openFlow(runId) if ADRuns.getRun(ADRunsController.store, runId) == nil then return end self.selectedRunId = runId self.statusOverride = nil self:switchTab(SmartPickupFrame.TAB_FLOWS) end -- Nyt flow: guiden "Opsæt flow" (gui/SmartPickupFlowGuide.lua) — intet oprettes før [Gem]. function SmartPickupFrame:onClickNew() self:onClickFlowNew() end function SmartPickupFrame:onClickRename() -- Dialogen kan kaste (fx manglende i18n-nøgle) — fang det som resten af GUI'et, i stedet -- for at lade fejlen undslippe klik-callbacken uden statuslinje-besked. self:safe(function(frame) local run = frame:getRun() if run == nil then return end TextInputDialog.show(SmartPickupFrame.onRenameResult, frame, run.name, frame.i18n:getText("spu_renameTitle"), nil, 30, g_i18n:getText("button_ok")) end) end function SmartPickupFrame:onRenameResult(value, clickOk) self:safe(function(frame) local run = frame:getRun() if clickOk and run ~= nil and value ~= nil and value:match("%S") then ADRunsController.updateRun(run.id, {name = value}) frame:requestRebuild() end end) end function SmartPickupFrame:onClickDelete() -- Samme begrundelse som onClickRename: dialog-opkaldet skal ikke kunne undslippe uden om safe(). self:safe(function(frame) local run = frame:getRun() if run == nil then return end YesNoDialog.show(SmartPickupFrame.onDeleteResult, frame, string.format(frame.i18n:getText("spu_deleteConfirm"), run.name), frame.i18n:getText("spu_button_delete")) end) end function SmartPickupFrame:onDeleteResult(yes) self:safe(function(frame) local run = frame:getRun() if yes and run ~= nil then ADRunsController.deleteRun(run.id, frame.getAdEnv()) frame.selectedRunId = nil frame:requestRebuild() end end) end