@ -36,16 +36,16 @@ local ImageViewer = InputContainer:new{
file = nil ,
-- or an already made BlitBuffer (ie: made by Mupdf.renderImageFile())
image = nil ,
-- whether provided BlitBuffer should be free(), normally true
-- unless our caller wants to reuse it's provided image
-- 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 ()
-- if the table has itself an attribute image_disposable= true.
-- 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
@ -147,25 +147,12 @@ function ImageViewer:init()
self._images_list_disposable = self.image_disposable
self.image_disposable = self._images_list . image_disposable
end
self : update ( )
end
function ImageViewer : _clean_image_wg ( )
-- To be called before re-using / not needing self._image_wg
-- otherwise resources used by its blitbuffer won't be freed
if self._image_wg then
logger.dbg ( " ImageViewer:_clean_image_wg() " )
self._image_wg : free ( )
self._image_wg = nil
end
end
function ImageViewer : update ( )
self : _clean_image_wg ( ) -- clean previous if any
-- Widget layout
if self._scale_to_fit == nil then -- initialize our toggle
self._scale_to_fit = self.scale_factor == 0 and true or false
self._scale_to_fit = self.scale_factor == 0
end
local orig_dimen = self.main_frame and self.main_frame . dimen or Geom: new { }
local orig_dimen = Geom : new { }
self.align = " center "
self.region = Geom : new {
x = 0 , y = 0 ,
@ -180,180 +167,348 @@ function ImageViewer:update()
self.width = Screen : getWidth ( ) - Screen : scaleBySize ( 40 )
end
local button_table_size = 0
local button_container
if self.buttons_visible then
local buttons = {
-- Init the buttons no matter what
local buttons = {
{
{
{
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 ,
} ,
{
text = self.rotated and _ ( " No rotation " ) or _ ( " Rotate " ) ,
callback = function ( )
self.rotated = not self.rotated and true or false
self : update ( )
end ,
} ,
{
text = _ ( " Close " ) ,
callback = function ( )
UIManager : close ( self )
end ,
} ,
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 ,
} ,
}
local 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 ,
}
button_container = CenterContainer : new {
dimen = Geom : new {
w = self.width ,
h = button_table : getSize ( ) . h ,
{
id = " rotate " ,
text = self.rotated and _ ( " No rotation " ) or _ ( " Rotate " ) ,
callback = function ( )
self.rotated = not self.rotated and true or false
self : update ( )
end ,
} ,
button_table ,
}
button_table_size = button_table : getSize ( ) . h
{
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.buttons_visible then
self.button_table_size = self.button_table : getSize ( ) . h
else
self.button_table_size = 0
end
-- height available to our image
local img_container_h = self.height - button_table_size
self. img_container_h = self.height - self. button_table_size
local title_bar , title_sep
-- Init the title bar and its components no matter what
-- Toggler (white arrow) for caption, on the left of title
local ctoggler_text
if self.caption_visible then
ctoggler_text = " ▽ " -- white arrow (nicer than smaller black arrow ▼)
else
ctoggler_text = " ▷ " -- white arrow (nicer than smaller black arrow ►)
end
self.ctoggler_tw = TextWidget : new {
text = ctoggler_text ,
face = self.title_face ,
}
-- paddings chosen to align nicely with titlew
self.ctoggler = FrameContainer : new {
bordersize = 0 ,
padding = self.title_padding ,
padding_top = self.title_padding + Size.padding . small ,
padding_right = 0 ,
self.ctoggler_tw ,
}
if self.caption then
self.ctoggler_width = self.ctoggler : getSize ( ) . w
else
self.ctoggler_width = 0
end
self.closeb = CloseButton : new { window = self , padding_top = Size.padding . tiny , }
self.title_tbw = TextBoxWidget : new {
text = self.title_text ,
face = self.title_face ,
-- bold = true, -- we're already using a bold font
width = self.width - 2 * self.title_padding - 2 * self.title_margin - self.closeb : getSize ( ) . w - self.ctoggler_width ,
}
local title_tbw_padding_bottom = self.title_padding + Size.padding . small
if self.caption and self.caption_visible then
title_tbw_padding_bottom = 0 -- save room between title and caption
end
self.titlew = FrameContainer : new {
padding = self.title_padding ,
padding_top = self.title_padding + Size.padding . small ,
padding_bottom = title_tbw_padding_bottom ,
padding_left = self.caption and self.ctoggler_width or self.title_padding ,
margin = self.title_margin ,
bordersize = 0 ,
self.title_tbw ,
}
if self.caption then
self.caption_tap_area = self.titlew
end
self.title_bar = OverlapGroup : new {
dimen = {
w = self.width ,
h = self.titlew : getSize ( ) . h
} ,
self.titlew ,
self.closeb
}
if self.caption then
table.insert ( self.title_bar , 1 , self.ctoggler )
end
-- Init the caption no matter what
self.caption_tbw = TextBoxWidget : new {
text = self.caption or _ ( " N/A " ) ,
face = self.caption_face ,
width = self.width - 2 * self.title_padding - 2 * self.title_margin - 2 * self.caption_padding ,
}
local captionw = FrameContainer : new {
padding = self.caption_padding ,
padding_top = 0 , -- don't waste vertical room for bigger image
padding_bottom = 0 ,
margin = self.title_margin ,
bordersize = 0 ,
self.caption_tbw ,
}
self.captioned_title_bar = VerticalGroup : new {
align = " left " ,
self.title_bar ,
captionw
}
if self.caption and self.caption_visible then
self.full_title_bar = self.captioned_title_bar
else
self.full_title_bar = self.title_bar
end
self.title_sep = LineWidget : new {
dimen = Geom : new {
w = self.width ,
h = Size.line . thick ,
}
}
-- adjust height available to our image
if self.with_title_bar then
-- Toggler (white arrow) for caption, on the left of title
local ctoggler
local ctoggler_width = 0
if self.caption then
local ctoggler_text
if self.caption_visible then
ctoggler_text = " ▽ " -- white arrow (nicer than smaller black arrow ▼)
else
ctoggler_text = " ▷ " -- white arrow (nicer than smaller black arrow ►)
end
-- paddings chosen to align nicely with titlew
ctoggler = FrameContainer : new {
bordersize = 0 ,
padding = self.title_padding ,
padding_top = self.title_padding + Size.padding . small ,
padding_right = 0 ,
TextWidget : new {
text = ctoggler_text ,
face = self.title_face ,
}
}
ctoggler_width = ctoggler : getSize ( ) . w
self.img_container_h = self.img_container_h - self.full_title_bar : getSize ( ) . h - self.title_sep : getSize ( ) . h
end
-- Init the progress bar no matter what
-- 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
}
if self._images_list then
self.img_container_h = self.img_container_h - self.progress_container : getSize ( ) . h
end
-- 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
local rotate_clockwise = 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
local closeb = CloseButton : new { window = self , padding_top = Size.padding . tiny , }
local title_tbw = TextBoxWidget : new {
text = self.title_text ,
face = self.title_face ,
-- bold = true, -- we're already using a bold font
width = self.width - 2 * self.title_padding - 2 * self.title_margin - closeb : getSize ( ) . w - ctoggler_width ,
rotation_angle = rotate_clockwise and 90 or 270
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.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 ,
}
local frame_elements = VerticalGroup : new { align = " left " }
if self.with_title_bar then
table.insert ( frame_elements , self.full_title_bar )
table.insert ( frame_elements , self.title_sep )
end
table.insert ( frame_elements , self.image_container )
if self._images_list then
table.insert ( frame_elements , self.progress_container )
end
if self.buttons_visible then
table.insert ( frame_elements , self.button_container )
end
self.main_frame = FrameContainer : new {
radius = not self.fullscreen and 8 or nil ,
padding = 0 ,
margin = 0 ,
background = Blitbuffer.COLOR_WHITE ,
frame_elements ,
}
self [ 1 ] = WidgetContainer : new {
align = self.align ,
dimen = self.region ,
FrameContainer : new {
bordersize = 0 ,
padding = Size.padding . default ,
self.main_frame ,
}
}
-- 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
function ImageViewer : update ( )
-- Free our ImageWidget, which is the only thing we'll replace (e.g., leave the TextBoxWidgets alone).
self : _clean_image_wg ( )
-- Update window geometry
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
-- Update 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 )
self.button_table_size = self.button_table : getSize ( ) . h
else
self.button_table_size = 0
end
-- height available to our image
self.img_container_h = self.height - self.button_table_size
-- Update the title bar
if self.with_title_bar then
self.ctoggler_tw : setText ( self.caption_visible and " ▽ " or " ▷ " )
-- Padding is dynamic...
local title_tbw_padding_bottom = self.title_padding + Size.padding . small
if self.caption and self.caption_visible then
title_tbw_padding_bottom = 0 -- save room between title and caption
end
local titlew = FrameContainer : new {
padding = self.title_padding ,
padding_top = self.title_padding + Size.padding . small ,
padding_bottom = title_tbw_padding_bottom ,
padding_left = ctoggler and ctoggler_width or self.title_padding ,
margin = self.title_margin ,
bordersize = 0 ,
title_tbw ,
}
if self.caption then
self.caption_tap_area = titlew
end
title_bar = OverlapGroup : new {
dimen = {
w = self.width ,
h = titlew : getSize ( ) . h
} ,
titlew ,
closeb
}
if ctoggler then
table.insert ( title_bar , 1 , ctoggler )
title_tbw_padding_bottom = 0
end
self.titlew . padding_bottom = title_tbw_padding_bottom
self.title_bar . dimen.h = self.titlew : getSize ( ) . h
if self.caption and self.caption_visible then
local caption_tbw = TextBoxWidget : new {
text = self.caption ,
face = self.caption_face ,
width = self.width - 2 * self.title_padding - 2 * self.title_margin - 2 * self.caption_padding ,
}
local captionw = FrameContainer : new {
padding = self.caption_padding ,
padding_top = 0 , -- don't waste vertical room for bigger image
padding_bottom = 0 ,
margin = self.title_margin ,
bordersize = 0 ,
caption_tbw ,
}
title_bar = VerticalGroup : new {
align = " left " ,
title_bar ,
captionw
}
self.full_title_bar = self.captioned_title_bar
else
self.full_title_bar = self.title_bar
end
title_sep = LineWidget : new {
dimen = Geom : new {
w = self.width ,
h = Size.line . thick ,
}
}
-- adjust height available to our image
img_container_h = img_container_h - title_bar : getSize ( ) . h - title_sep : getSize ( ) . h
self.img_container_h = self.img_container_h - self.full_title_bar : getSize ( ) . h - self.title_sep : getSize ( ) . h
end
local progress_container
-- Update the progress bar
if self._images_list then
-- progress bar
local percent = 1
if self._images_list_nb > 1 then
percent = ( self._images_list_cur - 1 ) / ( self._images_list_nb - 1 )
end
local 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 ,
}
progress_container = CenterContainer : new {
dimen = Geom : new {
w = self.width ,
h = progress_bar : getSize ( ) . h + Size.padding . small ,
} ,
progress_bar
}
img_container_h = img_container_h - progress_container : getSize ( ) . h
self.progress_bar : setPercentage ( percent )
self.img_container_h = self.img_container_h - self.progress_container : getSize ( ) . h
end
-- Update the image widget itself
-- If no buttons and no title are shown, use the full screen
local max_image_h = img_container_h
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 = img_container_h - self.image_padding * 2
max_image_h = self.img_container_h - self.image_padding * 2
max_image_w = self.width - self.image_padding * 2
end
@ -383,25 +538,26 @@ function ImageViewer:update()
center_y_ratio = self._center_y_ratio ,
}
local image_container = CenterContainer : new {
self. image_container = CenterContainer : new {
dimen = Geom : new {
w = self.width ,
h = img_container_h,
h = self. img_container_h,
} ,
self._image_wg ,
}
-- Update the final layout
local frame_elements = VerticalGroup : new { align = " left " }
if self.with_title_bar then
table.insert ( frame_elements , title_bar)
table.insert ( frame_elements , title_sep)
table.insert ( frame_elements , self.full_ title_bar)
table.insert ( frame_elements , self. title_sep)
end
table.insert ( frame_elements , image_container)
if progress_container then
table.insert ( frame_elements , progress_container)
table.insert ( frame_elements , self. image_container)
if self._images_list then
table.insert ( frame_elements , self. progress_container)
end
if self.buttons_visible then
table.insert ( frame_elements , button_container)
table.insert ( frame_elements , self. button_container)
end
self.main_frame = FrameContainer : new {
@ -420,14 +576,10 @@ function ImageViewer:update()
self.main_frame ,
}
}
-- 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 )
logger.dbg ( " update image region " , update_region )
return " ui " , update_region , true
end )
end
@ -442,7 +594,7 @@ end
function ImageViewer : switchToImageNum ( image_num )
if self.image and self.image_disposable and self.image . free then
logger.dbg ( " ImageViewer: free(self.image)" )
logger.dbg ( " ImageViewer: switchToImageNum: free self.image" , self.image )
self.image : free ( )
self.image = nil
end
@ -470,7 +622,7 @@ function ImageViewer:onTap(_, ges)
return self : onSaveImageView ( )
end
end
if self. caption_tap_area and ges.pos : intersectWith ( self.caption_tap_area . dimen ) then
if self. with_title_bar and self. caption_tap_area and ges.pos : intersectWith ( self.caption_tap_area . dimen ) then
self.caption_visible = not self.caption_visible
self : update ( )
return true
@ -710,17 +862,38 @@ function ImageViewer:onAnyKeyPressed()
end
function ImageViewer : onCloseWidget ( )
-- clean all our BlitBuffer objects when UIManager:close() was called
self : _clean_image_wg ( )
-- 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:free(self.image) " )
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
-- 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 not self.with_title_bar then
self.captioned_title_bar : free ( )
end
if not self.caption then
self.ctoggler : free ( )
end
if not self._images_list then
self.progress_container : free ( )
end
if not self.buttons_visible then
self.button_container : free ( )
end
-- NOTE: Assume there's no image beneath us, so, no dithering request
UIManager : setDirty ( nil , function ( )
return " flashui " , self.main_frame . dimen