local InfoMessage = require("ui/widget/infomessage") local Menu = require("ui/widget/menu") local UIManager = require("ui/uimanager") local logger = require("logger") local _ = require("gettext") local BookInfoManager = require("bookinfomanager") -- This is a kind of "base class" for both MosaicMenu and ListMenu. -- It implements the common code shared by these, mostly the non-UI -- work : the updating of items and the management of background jobs. -- -- Here the common overridden methods of Menu are defined: -- :updateItems(select_number, no_recalculate_dimen) -- :onCloseWidget() -- -- MosaicMenu or ListMenu should implement specific UI methods: -- :_recalculateDimen() -- :_updateItemsBuildUI() -- This last method is called in the middle of :updateItems() , and -- should fill self.item_group with some specific UI layout. It may add -- not found item to self.items_to_update for us to update() them -- regularly. -- Do some collectgarbage() every few drawings local NB_DRAWINGS_BETWEEN_COLLECTGARBAGE = 5 local nb_drawings_since_last_collectgarbage = 0 -- Simple holder of methods that will replace those -- in the real Menu class or instance local CoverMenu = {} function CoverMenu:updateItems(select_number, no_recalculate_dimen) -- As done in Menu:updateItems() local old_dimen = self.dimen and self.dimen:copy() -- self.layout must be updated for focusmanager self.layout = {} self.item_group:clear() -- NOTE: Our various _recalculateDimen overloads appear to have a stronger dependency -- on the rest of the widget elements being properly laid-out, -- so we have to run it *first*, unlike in Menu. -- Otherwise, various layout issues arise (e.g., MosaicMenu's page_info is misaligned). if not no_recalculate_dimen then self:_recalculateDimen() end self.page_info:resetLayout() self.return_button:resetLayout() self.content_group:resetLayout() -- Reset the list of items not found in db that will need to -- be updated by a scheduled action self.items_to_update = {} -- Cancel any previous (now obsolete) scheduled update if self.items_update_action then UIManager:unschedule(self.items_update_action) self.items_update_action = nil end -- Force garbage collecting before drawing a new page. -- It's not really needed from a memory usage point of view, we did -- all the free() where necessary, and koreader memory usage seems -- stable when file browsing only (15-25 MB). -- But I witnessed some freezes after browsing a lot when koreader's main -- process was using 100% cpu (and some slow downs while drawing soon before -- the freeze, like the full refresh happening before the final drawing of -- new text covers), while still having a small memory usage (20/30 Mb) -- that I suspect may be some garbage collecting happening at one point -- and getting stuck... -- With this, garbage collecting may be more deterministic, and it has -- no negative impact on user experience. -- But don't do it on every drawing, to not have all of them slow -- when memory usage is already high nb_drawings_since_last_collectgarbage = nb_drawings_since_last_collectgarbage + 1 if nb_drawings_since_last_collectgarbage >= NB_DRAWINGS_BETWEEN_COLLECTGARBAGE then -- (delay it a bit so this pause is less noticeable) UIManager:scheduleIn(0.2, function() collectgarbage() collectgarbage() end) nb_drawings_since_last_collectgarbage = 0 end -- Specific UI building implementation (defined in some other module) self._has_cover_images = false select_number = self:_updateItemsBuildUI() or select_number -- As done in Menu:updateItems() self:updatePageInfo(select_number) Menu.mergeTitleBarIntoLayout(self) self.show_parent.dithered = self._has_cover_images UIManager:setDirty(self.show_parent, function() local refresh_dimen = old_dimen and old_dimen:combine(self.dimen) or self.dimen return "ui", refresh_dimen, self.show_parent.dithered end) -- As additionally done in FileChooser:updateItems() if self.path_items then self.path_items[self.path] = (self.page - 1) * self.perpage + (select_number or 1) end -- Deal with items not found in db if #self.items_to_update > 0 then -- Prepare for background info extraction job local files_to_index = {} -- table of {filepath, cover_specs} for i=1, #self.items_to_update do table.insert(files_to_index, { filepath = self.items_to_update[i].filepath, cover_specs = self.items_to_update[i].cover_specs }) end -- Launch it at nextTick, so UIManager can render us smoothly UIManager:nextTick(function() local launched = BookInfoManager:extractInBackground(files_to_index) if not launched then -- fork failed (never experienced that, but let's deal with it) -- Cancel scheduled update, as it won't get any result if self.items_update_action then UIManager:unschedule(self.items_update_action) self.items_update_action = nil end UIManager:show(InfoMessage:new{ text = _("Start-up of background extraction job failed.\nPlease restart KOReader or your device.") }) end end) -- Scheduled update action self.items_update_action = function() logger.dbg("Scheduled items update:", #self.items_to_update, "waiting") local is_still_extracting = BookInfoManager:isExtractingInBackground() local i = 1 while i <= #self.items_to_update do -- process and clean in-place local item = self.items_to_update[i] item:update() if item.bookinfo_found then logger.dbg(" found", item.text) self.show_parent.dithered = item._has_cover_image local refreshfunc = function() if item.refresh_dimen then -- MosaicMenuItem may exceed its own dimen in its paintTo -- with its "description" hint return "ui", item.refresh_dimen, self.show_parent.dithered else return "ui", item[1].dimen, self.show_parent.dithered end end UIManager:setDirty(self.show_parent, refreshfunc) table.remove(self.items_to_update, i) else logger.dbg(" not yet found", item.text) i = i + 1 end end if #self.items_to_update > 0 then -- re-schedule myself if is_still_extracting then -- we have still chances to get new stuff logger.dbg("re-scheduling items update:", #self.items_to_update, "still waiting") UIManager:scheduleIn(1, self.items_update_action) else logger.dbg("Not all items found, but background extraction has stopped, not re-scheduling") end else logger.dbg("items update completed") end end UIManager:scheduleIn(1, self.items_update_action) end end function CoverMenu:onCloseWidget() -- Due to close callback in FileManagerHistory:onShowHist, we may be called -- multiple times (witnessed that with print(debug.traceback()) -- So, avoid doing what follows twice if self._covermenu_onclose_done then return end self._covermenu_onclose_done = true -- Stop background job if any (so that full cpu is available to reader) logger.dbg("CoverMenu:onCloseWidget: terminating jobs if needed") BookInfoManager:terminateBackgroundJobs() BookInfoManager:closeDbConnection() -- sqlite connection no more needed BookInfoManager:cleanUp() -- clean temporary resources -- Cancel any still scheduled update if self.items_update_action then logger.dbg("CoverMenu:onCloseWidget: unscheduling items_update_action") UIManager:unschedule(self.items_update_action) self.items_update_action = nil end -- Propagate a call to free() to all our sub-widgets, to release memory used by their _bb self.item_group:free() -- Force garbage collecting when leaving too -- (delay it a bit so this pause is less noticeable) UIManager:scheduleIn(0.2, function() collectgarbage() collectgarbage() end) nb_drawings_since_last_collectgarbage = 0 -- Call the object's original onCloseWidget (i.e., Menu's, as none our our expected subclasses currently implement it) Menu.onCloseWidget(self) end return CoverMenu