--[[-- ImageViewer displays an image with some simple manipulation options. ]] local BD = require("ui/bidi") local Blitbuffer = require("ffi/blitbuffer") local ButtonTable = require("ui/widget/buttontable") local CenterContainer = require("ui/widget/container/centercontainer") local DataStorage = require("datastorage") local Device = require("device") local Event = require("ui/event") local Geom = require("ui/geometry") local GestureRange = require("ui/gesturerange") local FrameContainer = require("ui/widget/container/framecontainer") local ImageWidget = require("ui/widget/imagewidget") local InputContainer = require("ui/widget/container/inputcontainer") local ProgressWidget = require("ui/widget/progresswidget") local Size = require("ui/size") local TitleBar = require("ui/widget/titlebar") local VerticalGroup = require("ui/widget/verticalgroup") local WidgetContainer = require("ui/widget/container/widgetcontainer") local UIManager = require("ui/uimanager") local logger = require("logger") local _ = require("gettext") local Screen = Device.screen local ImageViewer = InputContainer:extend{ -- Allow for providing same different input types as ImageWidget : -- a path to a file file = nil, -- or an already made BlitBuffer (ie: made by Mupdf.renderImageFile()) image = nil, -- whether the provided BlitBuffer should be free'd. Usually true, -- unless our caller wants to reuse the image it provided image_disposable = true, -- 'image' can alternatively be a table (list) of multiple BlitBuffers -- (or functions returning BlitBuffers). -- The table will have its .free() called onClose according to -- the image_disposable provided here. -- Each BlitBuffer in the table (or returned by functions) will be free'd -- if the table itself has an image_disposable field set to true. -- With images list, when switching image, whether to keep previous -- image pan & zoom images_keep_pan_and_zoom = true, fullscreen = false, -- false will add some padding around widget (so footer can be visible) with_title_bar = true, title_text = _("Viewing image"), -- default title text -- A caption can be toggled with tap on title_text (so, it needs with_title_bar=true): caption = nil, caption_visible = true, -- caption visible by default -- Start with buttons hidden (tap on screen will toggle their visibility) buttons_visible = false, width = nil, height = nil, scale_factor = 0, -- start with image scaled for best fit rotated = false, image_padding = Size.margin.small, button_padding = Size.padding.default, -- sensitivity for hold (trigger full refresh) vs pan (move image) pan_threshold = Screen:scaleBySize(5), _scale_to_fit = nil, -- state of toggle between our 2 pre-defined scales (scale to fit / original size) _panning = false, -- Default centering on center of image if oversized _center_x_ratio = 0.5, _center_y_ratio = 0.5, -- Reference to current ImageWidget instance, for cleaning _image_wg = nil, _images_list = nil, _images_list_disposable = nil, _scaled_image_func = nil, } function ImageViewer:init() if Device:hasKeys() then if type(self.image) == "table" then -- if self.image is a table, then use hardware keys to change image self.key_events = { Close = { { Device.input.group.Back } }, ShowPrevImage = { { Device.input.group.PgBack } }, ShowNextImage = { { Device.input.group.PgFwd } }, } else -- otherwise, use hardware keys to zoom in/out self.key_events = { Close = { { Device.input.group.Back } }, ZoomIn = { { Device.input.group.PgBack } }, ZoomOut = { { Device.input.group.PgFwd } }, } end end if Device:isTouchDevice() then local range = Geom:new{ x = 0, y = 0, w = Screen:getWidth(), h = Screen:getHeight(), } local diagonal = math.sqrt(Screen:getWidth()^2 + Screen:getHeight()^2) self.ges_events = { Tap = { GestureRange:new{ ges = "tap", range = range } }, -- Zoom in/out (Pinch & Spread are not triggered if user is too -- slow and Hold event is decided first) Spread = { GestureRange:new{ ges = "spread", range = range } }, Pinch = { GestureRange:new{ ges = "pinch", range = range } }, -- All the following gestures will allow easy panning -- Hold happens if we hold at start -- Pan happens if we don't hold at start, but hold at end -- Swipe happens if we don't hold at any moment Hold = { GestureRange:new{ ges = "hold", range = range } }, HoldRelease = { GestureRange:new{ ges = "hold_release", range = range } }, Pan = { GestureRange:new{ ges = "pan", range = range } }, PanRelease = { GestureRange:new{ ges = "pan_release", range = range } }, Swipe = { GestureRange:new{ ges = "swipe", range = range } }, -- Allow saving the image as Screenshoter does (with two fingers tap, -- swipe being reserved for panning - on non multitouch Devices, this -- is also available with a tap in the bottom left corner) TapDiagonal = { GestureRange:new{ ges = "two_finger_tap", scale = {diagonal - Screen:scaleBySize(200), diagonal}, rate = 1.0, } }, -- Allow closing with any multiswipe MultiSwipe = { GestureRange:new{ ges = "multiswipe", range = range } }, } end if self.fullscreen then self.covers_fullscreen = true -- hint for UIManager:_repaint() end -- if self.image is a list of images, swap it with first image to be displayed if type(self.image) == "table" then self._images_list = self.image self.image = self._images_list[1] if type(self.image) == "function" then self.image = self.image() end self._images_list_cur = 1 self._images_list_nb = #self._images_list self._images_orig_scale_factor = self.scale_factor -- also swap disposable status self._images_list_disposable = self.image_disposable self.image_disposable = self._images_list.image_disposable end -- If self.image is a function (scalable SVG image object provided by crengine), -- it can be used to get the perfect bb for any scale_factor if type(self.image) == "function" then self._scaled_image_func = self.image self.image = self._scaled_image_func(1) -- native image size, that we need to know end -- Widget layout if self._scale_to_fit == nil then -- initialize our toggle self._scale_to_fit = self.scale_factor == 0 end self.align = "center" self.region = Geom:new{ x = 0, y = 0, w = Screen:getWidth(), h = Screen:getHeight(), } if self.fullscreen then self.height = Screen:getHeight() self.width = Screen:getWidth() else self.height = Screen:getHeight() - Screen:scaleBySize(40) self.width = Screen:getWidth() - Screen:scaleBySize(40) end -- Build all the widgets we may have to show local buttons = { { { id = "scale", text = self._scale_to_fit and _("Original size") or _("Scale"), callback = function() self.scale_factor = self._scale_to_fit and 1 or 0 self._scale_to_fit = not self._scale_to_fit -- Reset center ratio (may have been modified if some panning was done) self._center_x_ratio = 0.5 self._center_y_ratio = 0.5 self:update() end, }, { id = "rotate", text = self.rotated and _("No rotation") or _("Rotate"), callback = function() self.rotated = not self.rotated and true or false self:update() end, }, { id = "close", text = _("Close"), callback = function() self:onClose() end, }, }, } self.button_table = ButtonTable:new{ width = self.width - 2*self.button_padding, button_font_face = "cfont", button_font_size = 20, buttons = buttons, zero_sep = true, show_parent = self, } self.button_container = CenterContainer:new{ dimen = Geom:new{ w = self.width, h = self.button_table:getSize().h, }, self.button_table, } if self.with_title_bar then -- (We don't provide fullscreen=true so to use the non-fullscreen smaller font size) if self.caption then -- Toggling caption will have us swap these two title bars self.title_bar = TitleBar:new{ -- when caption hidden width = self.width, align = "left", title = self.title_text, title_multilines = true, with_bottom_line = true, left_icon = "triangle", left_icon_rotation_angle = BD.mirroredUILayout() and 90 or 270, left_icon_tap_callback = function() self.caption_visible = not self.caption_visible self:update() end, close_callback = function() self:onClose() end, show_parent = self, } self.captioned_title_bar = TitleBar:new{ -- when caption shown width = self.width, align = "left", title = self.title_text, title_multilines = true, subtitle = self.caption, subtitle_multilines = true, subtitle_fullwidth = true, with_bottom_line = true, left_icon = "triangle", left_icon_rotation_angle = 180, left_icon_tap_callback = function() self.caption_visible = not self.caption_visible self:update() end, close_callback = function() self:onClose() end, show_parent = self, } else self.title_bar = TitleBar:new{ width = self.width, align = "left", title = self.title_text, title_multilines = true, with_bottom_line = true, close_callback = function() self:onClose() end, show_parent = self, } end end if self._images_list then -- progress bar local percent = 1 if self._images_list and self._images_list_nb > 1 then percent = (self._images_list_cur - 1) / (self._images_list_nb - 1) end self.progress_bar = ProgressWidget:new{ width = self.width - 2*self.button_padding, height = Screen:scaleBySize(5), percentage = percent, margin_h = 0, margin_v = 0, radius = 0, ticks = nil, last = nil, } self.progress_container = CenterContainer:new{ dimen = Geom:new{ w = self.width, h = self.progress_bar:getSize().h + Size.padding.small, }, self.progress_bar } end -- Container for the above elements, that we will reset and refill self.frame_elements = VerticalGroup:new{ align = "left" } self.main_frame = FrameContainer:new{ radius = not self.fullscreen and 8 or nil, padding = 0, margin = 0, background = Blitbuffer.COLOR_WHITE, self.frame_elements, } self[1] = WidgetContainer:new{ align = self.align, dimen = self.region, self.main_frame, } self:update() end function ImageViewer:update() -- Free our ImageWidget, which is the only thing we'll replace (we reuse -- all the other text widgets and containers) self:_clean_image_wg() -- Update window geometry (fullscreen can be toggled, but without any title -- and buttons, which allow us to not have to update their width) local orig_dimen = self.main_frame.dimen if self.fullscreen then self.height = Screen:getHeight() self.width = Screen:getWidth() else self.height = Screen:getHeight() - Screen:scaleBySize(40) self.width = Screen:getWidth() - Screen:scaleBySize(40) end -- Remove elements (not freeing them) from frame_elements while table.remove(self.frame_elements) do end self.frame_elements:resetLayout() -- And put back those that we should show -- Title bar if self.with_title_bar then if self.caption and self.caption_visible then table.insert(self.frame_elements, self.captioned_title_bar) else table.insert(self.frame_elements, self.title_bar) end end -- Image container (we'll insert it once all others are added and we know the height remaining) local image_container_idx = #self.frame_elements + 1 -- Progress bar if self._images_list then local percent = 1 if self._images_list_nb > 1 then percent = (self._images_list_cur - 1) / (self._images_list_nb - 1) end self.progress_bar:setPercentage(percent) table.insert(self.frame_elements, self.progress_container) end -- Bottom buttons if self.buttons_visible then local scale_btn = self.button_table:getButtonById("scale") scale_btn:setText(self._scale_to_fit and _("Original size") or _("Scale"), scale_btn.width) local rotate_btn = self.button_table:getButtonById("rotate") rotate_btn:setText(self.rotated and _("No rotation") or _("Rotate"), rotate_btn.width) table.insert(self.frame_elements, self.button_container) end -- Get the available height and update the image widget itself self.img_container_h = self.height - self.frame_elements:getSize().h self:_new_image_wg() -- Insert image widget in our vertical group table.insert(self.frame_elements, image_container_idx, self.image_container) self.frame_elements:resetLayout() self.main_frame.radius = not self.fullscreen and 8 or nil -- NOTE: We use UI instead of partial, because we do NOT want to end up using a REAGL waveform... -- NOTE: Disabling dithering here makes for a perfect test-case of how well it works: -- page turns will show color quantization artefacts (i.e., banding) like crazy, -- while a long touch will trigger a dithered, flashing full-refresh that'll make everything shiny :). self.dithered = true UIManager:setDirty(self, function() local update_region = self.main_frame.dimen:combine(orig_dimen) return "ui", update_region, true end) end function ImageViewer:_clean_image_wg() -- To be called before re-using / disposing of self._image_wg, -- otherwise resources used by its blitbuffer won't be free'd if self._image_wg then logger.dbg("ImageViewer:_clean_image_wg") self._image_wg:free() self._image_wg = nil end end -- Used in init & update to instantiate a new ImageWidget & its container function ImageViewer:_new_image_wg() -- If no buttons and no title are shown, use the full screen local max_image_h = self.img_container_h local max_image_w = self.width -- Otherwise, add paddings around image if self.buttons_visible or self.with_title_bar then max_image_h = self.img_container_h - self.image_padding*2 max_image_w = self.width - self.image_padding*2 end local rotation_angle = 0 if self.rotated then -- in portrait mode, rotate according to this global setting so we are -- like in landscape mode -- NOTE: This is the sole user of this legacy global left! local rotate_clockwise = G_defaults:readSetting("DLANDSCAPE_CLOCKWISE_ROTATION") if Screen:getWidth() > Screen:getHeight() then -- in landscape mode, counter-rotate landscape rotation so we are -- back like in portrait mode rotate_clockwise = not rotate_clockwise end rotation_angle = rotate_clockwise and 90 or 270 end if self._scaled_image_func then local scale_factor_used self.image, scale_factor_used = self._scaled_image_func(self.scale_factor, max_image_w, max_image_h) if self.scale_factor == 0 then -- onZoomIn/Out need to know the current scale factor, that they won't be -- able to fetch from _image_wg as we force it to be 1. So, remember it. self._scale_factor_0 = scale_factor_used end end self._image_wg = ImageWidget:new{ file = self.file, image = self.image, image_disposable = false, -- we may re-use self.image alpha = true, -- we might be showing images with an alpha channel (e.g., from Wikipedia) width = max_image_w, height = max_image_h, rotation_angle = rotation_angle, scale_factor = self._scaled_image_func and 1 or self.scale_factor, center_x_ratio = self._center_x_ratio, center_y_ratio = self._center_y_ratio, } self.image_container = CenterContainer:new{ dimen = Geom:new{ w = self.width, h = self.img_container_h, }, self._image_wg, } end function ImageViewer:onShow() self.dithered = true UIManager:setDirty(self, function() return "full", self.main_frame.dimen, true end) return true end function ImageViewer:switchToImageNum(image_num) if self.image and self.image_disposable and self.image.free then logger.dbg("ImageViewer:switchToImageNum: free self.image", self.image) self.image:free() self.image = nil end self.image = self._images_list[image_num] if type(self.image) == "function" then self.image = self.image() end self._images_list_cur = image_num if not self.images_keep_pan_and_zoom then self._center_x_ratio = 0.5 self._center_y_ratio = 0.5 self.scale_factor = self._images_orig_scale_factor end self:update() end -- Image switching events function ImageViewer:onShowNextImage() if self._images_list_cur < self._images_list_nb then self:switchToImageNum(self._images_list_cur + 1) end end function ImageViewer:onShowPrevImage() if self._images_list_cur > 1 then self:switchToImageNum(self._images_list_cur - 1) end end function ImageViewer:onTap(_, ges) if ges.pos:notIntersectWith(self.main_frame.dimen) then self:onClose() return true end if not Device:hasMultitouch() then -- Allow saving screenshot with tap in bottom left corner if not self.buttons_visible and ges.pos.x < Screen:getWidth()/10 and ges.pos.y > Screen:getHeight()*9/10 then return self:onSaveImageView() end end if self.with_title_bar then -- Ignore tap in title/caption (button and caption toggler are managed by TitleBar itself), -- the user is most probably trying to toggle caption, but failed hitting the toggle: don't -- have this toggle bottom buttons. if ges.pos.y < self.frame_elements[1]:getSize().h then return true end end if self._images_list then -- If it's a list of image (e.g. animated gifs), tap left/right 1/3 of screen to navigate local show_prev_image, show_next_image if ges.pos.x < Screen:getWidth()/3 then show_prev_image = not BD.mirroredUILayout() show_next_image = BD.mirroredUILayout() elseif ges.pos.x > Screen:getWidth()*2/3 then show_prev_image = BD.mirroredUILayout() show_next_image = not BD.mirroredUILayout() end if show_prev_image then self:onShowPrevImage() elseif show_next_image then self:onShowNextImage() else -- toggle buttons when tap on middle 1/3 of screen width self.buttons_visible = not self.buttons_visible self:update() end else -- No image list: tap on any part of screen toggles buttons visibility self.buttons_visible = not self.buttons_visible self:update() end return true end function ImageViewer:panBy(x, y) if self._image_wg then -- ImageWidget:panBy() returns new center ratio, so we update ours, -- so we'll be centered the same way when we zoom in or out self._center_x_ratio, self._center_y_ratio = self._image_wg:panBy(x, y) end end -- Panning events function ImageViewer:onSwipe(_, ges) -- Panning with swipe is less accurate, as we don't get both coordinates, -- only start point + direction (with only 45° granularity) local direction = ges.direction local distance = ges.distance local sq_distance = math.sqrt(distance*distance/2) if direction == "north" then if ges.pos.x < Screen:getWidth() * 1/8 or ges.pos.x > Screen:getWidth() * 7/8 then -- allow for zooming with vertical swipe on screen sides -- (for devices without multi touch where pinch and spread don't work) -- c.f., onSpread for details about the choice between screen & scaled image height. local inc = ges.distance / math.min(Screen:getHeight(), self._image_wg:getCurrentHeight()) self:onZoomIn(inc) else self:panBy(0, distance) end elseif direction == "south" then if ges.pos.x < Screen:getWidth() * 1/8 or ges.pos.x > Screen:getWidth() * 7/8 then -- allow for zooming with vertical swipe on screen sides local dec = ges.distance / math.min(Screen:getHeight(), self._image_wg:getCurrentHeight()) self:onZoomOut(dec) elseif self.scale_factor == 0 then -- When scaled to fit (on initial launch, or after one has tapped -- "Scale"), as we are then sure that there is no use for panning, -- allow swipe south to close the widget. self:onClose() else self:panBy(0, -distance) end elseif direction == "east" then self:panBy(-distance, 0) elseif direction == "west" then self:panBy(distance, 0) elseif direction == "northeast" then self:panBy(-sq_distance, sq_distance) elseif direction == "northwest" then self:panBy(sq_distance, sq_distance) elseif direction == "southeast" then self:panBy(-sq_distance, -sq_distance) elseif direction == "southwest" then self:panBy(sq_distance, -sq_distance) end return true end function ImageViewer:onMultiSwipe(_, ges) -- As swipe south to close is only enabled when scaled to fit, but not -- when we are zoomed in/out, allow any multiswipe to close. self:onClose() return true end function ImageViewer:onHold(_, ges) -- Start of pan self._panning = true self._pan_relative_x = ges.pos.x self._pan_relative_y = ges.pos.y return true end function ImageViewer:onHoldRelease(_, ges) -- End of pan if self._panning then self._panning = false self._pan_relative_x = ges.pos.x - self._pan_relative_x self._pan_relative_y = ges.pos.y - self._pan_relative_y if math.abs(self._pan_relative_x) < self.pan_threshold and math.abs(self._pan_relative_y) < self.pan_threshold then -- Hold with no move (or less than pan_threshold): use this to trigger full refresh self.dithered = true UIManager:setDirty(nil, "full", nil, true) else self:panBy(-self._pan_relative_x, -self._pan_relative_y) end end return true end function ImageViewer:onPan(_, ges) self._panning = true self._pan_relative_x = ges.relative.x self._pan_relative_y = ges.relative.y return true end function ImageViewer:onPanRelease(_, ges) if self._panning then self._panning = false self:panBy(-self._pan_relative_x, -self._pan_relative_y) end return true end -- Zoom events function ImageViewer:_refreshScaleFactor() if self.scale_factor == 0 then -- Get the scale_factor made out for best fit self.scale_factor = self._scale_factor_0 or self._image_wg:getScaleFactor() end end function ImageViewer:_applyNewScaleFactor(new_factor) -- Make sure self.scale_factor is up-to-date self:_refreshScaleFactor() -- We destroy ImageWidget on update, so only request this the first time, -- in order to avoid jitter in the results given differing memory consumption at different zoom levels... if not self._min_scale_factor or not self._max_scale_factor then self._min_scale_factor, self._max_scale_factor = self._image_wg:getScaleFactorExtrema() end -- Clamp to sane values new_factor = math.min(new_factor, self._max_scale_factor) new_factor = math.max(new_factor, self._min_scale_factor) if new_factor ~= self.scale_factor then self.scale_factor = new_factor self:update() else if self.scale_factor == self._min_scale_factor then logger.dbg("ImageViewer: Hit the min scaling factor:", self.scale_factor) elseif self.scale_factor == self._max_scale_factor then logger.dbg("ImageViewer: Hit the max scaling factor:", self.scale_factor) else logger.dbg("ImageViewer: No change in scaling factor:", self.scale_factor) end end end function ImageViewer:onZoomIn(inc) self:_refreshScaleFactor() if not inc then -- default for key zoom event inc = 0.2 end -- Compute new scale factor for rescaled image dimensions local new_factor = self.scale_factor * (1 + inc) self:_applyNewScaleFactor(new_factor) return true end function ImageViewer:onZoomOut(dec) self:_refreshScaleFactor() if not dec then dec = 0.2 elseif dec >= 0.75 then -- Larger reductions tend to be fairly jarring, so limit to 75%. -- (Also, we can't go above 1 because maths). dec = 0.75 end local new_factor = self.scale_factor * (1 - dec) self:_applyNewScaleFactor(new_factor) return true end --[[ function ImageViewer:onZoomToHeight(height) local new_factor = height / self._image_wg:getOriginalHeight() self:_applyNewScaleFactor(new_factor) return true end function ImageViewer:onZoomToWidth(width) local new_factor = width / self._image_wg:getOriginalWidth() self:_applyNewScaleFactor(new_factor) return true end function ImageViewer:onZoomToDiagonal(d) -- It's trigonometry time! -- c.f., https://math.stackexchange.com/a/3369637 local r = self._image_wg:getOriginalWidth() / self._image_wg:getOriginalHeight() local h = math.sqrt(d^2 / (r^2 + 1)) local w = h * r -- Matches ImageWidget's best-fit computation in _render local new_factor = math.min(w / self._image_wg:getOriginalWidth(), h / self._image_wg:getOriginalHeight()) self:_applyNewScaleFactor(new_factor) return true end --]] function ImageViewer:onSpread(_, ges) if not self._image_wg then return end -- We get the position where spread was done -- First, get center ratio we would have had if we did a pan to there, -- so we can have the zoom centered on there self._center_x_ratio, self._center_y_ratio = self._image_wg:getPanByCenterRatio(ges.pos.x - Screen:getWidth()/2, ges.pos.y - Screen:getHeight()/2) -- We compute a scaling percentage (which will *modify* the current scaling factor), -- based on the gesture distance (it's the sum of the travel of both fingers). -- Making this distance relative to the smallest dimension between -- the currently scaled image or the Screen makes it less annoying when approaching both very small scale factors -- (where the image dimensions are many times smaller than the screen), -- meaning using the image dimensions here takes less zoom steps to get it back to a sensible size; -- *and* large scale factors (where the image dimensions are larger than the screen), -- meaning using the screen dimensions here makes zoom steps, again, slightly more potent. if ges.direction == "vertical" then local img_h = self._image_wg:getCurrentHeight() local screen_h = Screen:getHeight() self:onZoomIn(ges.distance / math.min(screen_h, img_h)) elseif ges.direction == "horizontal" then local img_w = self._image_wg:getCurrentWidth() local screen_w = Screen:getWidth() self:onZoomIn(ges.distance / math.min(screen_w, img_w)) else local img_d = self._image_wg:getCurrentDiagonal() local screen_d = math.sqrt(Screen:getWidth()^2 + Screen:getHeight()^2) self:onZoomIn(ges.distance / math.min(screen_d, img_d)) end return true end function ImageViewer:onPinch(_, ges) if not self._image_wg then return end -- With Pinch, unlike Spread, it feels more natural if we keep the same center point. if ges.direction == "vertical" then local img_h = self._image_wg:getCurrentHeight() local screen_h = Screen:getHeight() self:onZoomOut(ges.distance / math.min(screen_h, img_h)) elseif ges.direction == "horizontal" then local img_w = self._image_wg:getCurrentWidth() local screen_w = Screen:getWidth() self:onZoomOut(ges.distance / math.min(screen_w, img_w)) else local img_d = self._image_wg:getCurrentDiagonal() local screen_d = math.sqrt(Screen:getWidth()^2 + Screen:getHeight()^2) self:onZoomOut(ges.distance / math.min(screen_d, img_d)) end return true end function ImageViewer:onTapDiagonal() return self:onSaveImageView() end function ImageViewer:onSaveImageView() -- We save the currently displayed blitbuffer (panned or zoomed) -- after getting fullscreen and removing UI elements if needed. local restore_settings_func if self.with_title_bar or self.buttons_visible or not self.fullscreen then local with_title_bar = self.with_title_bar local buttons_visible = self.buttons_visible local fullscreen = self.fullscreen restore_settings_func = function() self.with_title_bar = with_title_bar self.buttons_visible = buttons_visible self.fullscreen = fullscreen self:update() end self.with_title_bar = false self.buttons_visible = false self.fullscreen = true self:update() UIManager:forceRePaint() end local screenshots_dir = G_reader_settings:readSetting("screenshot_dir") or DataStorage:getDataDir() .. "/screenshots/" local screenshot_name = os.date(screenshots_dir .. "ImageViewer" .. "_%Y-%m-%d_%H%M%S.png") UIManager:sendEvent(Event:new("Screenshot", screenshot_name, restore_settings_func)) return true end function ImageViewer:onClose() UIManager:close(self) return true end function ImageViewer:onCloseWidget() -- Our ImageWidget (self._image_wg) is always a proper child widget, so it'll receive this event, -- and attempt to free its resources accordingly. -- But, if it didn't have to touch the original BB (self.image) passed to ImageViewer (e.g., no scaling needed), -- it will *re-use* self.image, and flag it as non-disposable, meaning it will not have been free'd earlier. -- Since we're the ones who ultimately truly know whether we should dispose of self.image or not, do that now ;). if self.image and self.image_disposable and self.image.free then logger.dbg("ImageViewer:onCloseWidget: free self.image", self.image) self.image:free() self.image = nil end -- also clean _images_list if it provides a method for that if self._images_list and self._images_list_disposable and self._images_list.free then logger.dbg("ImageViewer:onCloseWidget: free self._images_list", self._images_list) self._images_list:free() end if self._scaled_image_func then self._scaled_image_func(false) -- invoke :free() on the creimage object self._scaled_image_func = nil end -- Those, on the other hand, are always initialized, but may not actually be in our widget tree right now, -- depending on what we needed to show, so they might not get sent a CloseWidget event. -- They (and their FFI/C resources) would eventually get released by the GC, but let's be pedantic ;). if self.with_title_bar then self.title_bar:free() if self.caption then self.captioned_title_bar:free() end end if self._images_list then self.progress_container:free() end self.button_container:free() -- NOTE: Assume there's no image beneath us, so, no dithering request UIManager:setDirty(nil, function() return "flashui", self.main_frame.dimen end) end return ImageViewer