You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ueberzug/ueberzug/action.py

295 lines
11 KiB
Python

import abc
import enum
import os.path
import attr
import ueberzug.geometry as geometry
import ueberzug.scaling as scaling
import ueberzug.conversion as conversion
@attr.s
class Action(metaclass=abc.ABCMeta):
"""Describes the structure used to define actions classes.
Defines a general interface used to implement the building of commands
and their execution.
"""
action = attr.ib(type=str, default=attr.Factory(
lambda self: self.get_action_name(), takes_self=True))
@staticmethod
@abc.abstractmethod
def get_action_name():
"""Returns the constant name which is associated to this action."""
raise NotImplementedError()
@abc.abstractmethod
async def apply(self, windows, view, tools):
"""Executes the action on the passed view and windows."""
raise NotImplementedError()
@attr.s(kw_only=True)
class Drawable:
"""Defines the attributes of drawable actions."""
draw = attr.ib(default=True, converter=conversion.to_bool)
synchronously_draw = attr.ib(default=False, converter=conversion.to_bool)
@attr.s(kw_only=True)
class Identifiable:
"""Defines the attributes of actions
which are associated to an identifier.
"""
identifier = attr.ib(type=str)
@attr.s(kw_only=True)
class DrawAction(Action, Drawable, metaclass=abc.ABCMeta):
"""Defines actions which redraws all windows."""
# pylint: disable=abstract-method
__redraw_scheduled = False
@staticmethod
def schedule_redraw(windows):
"""Creates a async function which redraws every window
if there is no unexecuted function
(returned by this function)
which does the same.
Args:
windows (batch.BatchList of ui.CanvasWindow):
the windows to be redrawn
Returns:
function: the redraw function or None
"""
if not DrawAction.__redraw_scheduled:
DrawAction.__redraw_scheduled = True
async def redraw():
windows.draw()
DrawAction.__redraw_scheduled = False
return redraw()
return None
async def apply(self, windows, view, tools):
if self.draw:
import asyncio
if self.synchronously_draw:
windows.draw()
# force coroutine switch
await asyncio.sleep(0)
return
function = self.schedule_redraw(windows)
if function:
asyncio.ensure_future(function)
@attr.s(kw_only=True)
class ImageAction(DrawAction, Identifiable, metaclass=abc.ABCMeta):
"""Defines actions which are related to images."""
# pylint: disable=abstract-method
pass
@attr.s(kw_only=True)
class AddImageAction(ImageAction):
"""Displays the image according to the passed option.
If there's already an image with the given identifier
it's going to be replaced.
"""
x = attr.ib(type=int, converter=int)
y = attr.ib(type=int, converter=int)
path = attr.ib(type=str)
width = attr.ib(type=int, converter=int, default=0)
height = attr.ib(type=int, converter=int, default=0)
scaling_position_x = attr.ib(type=float, converter=float, default=0)
scaling_position_y = attr.ib(type=float, converter=float, default=0)
scaler = attr.ib(
type=str, default=scaling.ContainImageScaler.get_scaler_name())
# deprecated
max_width = attr.ib(type=int, converter=int, default=0)
max_height = attr.ib(type=int, converter=int, default=0)
@staticmethod
def get_action_name():
return 'add'
def __attrs_post_init__(self):
self.width = self.max_width or self.width
self.height = self.max_height or self.height
# attrs doesn't support overriding the init method
# pylint: disable=attribute-defined-outside-init
self.__scaler_class = None
self.__last_modified = None
@property
def scaler_class(self):
"""scaling.ImageScaler: the used scaler class of this placement"""
if self.__scaler_class is None:
self.__scaler_class = \
scaling.ScalerOption(self.scaler).scaler_class
return self.__scaler_class
@property
def last_modified(self):
"""float: the last modified time of the image"""
if self.__last_modified is None:
self.__last_modified = os.path.getmtime(self.path)
return self.__last_modified
def is_same_image(self, old_placement):
"""Determines whether the placement contains the same image
after applying the changes of this command.
Args:
old_placement (ui.CanvasWindow.Placement):
the old data of the placement
Returns:
bool: True if it's the same file
"""
return old_placement and not (
old_placement.last_modified < self.last_modified
or self.path != old_placement.path)
def is_full_reload_required(self, old_placement,
screen_columns, screen_rows):
"""Determines whether it's required to fully reload
the image of the placement to properly render the placement.
Args:
old_placement (ui.CanvasWindow.Placement):
the old data of the placement
screen_columns (float):
the maximum amount of columns the screen can display
screen_rows (float):
the maximum amount of rows the screen can display
Returns:
bool: True if the image should be reloaded
"""
return old_placement and (
(not self.scaler_class.is_indulgent_resizing()
and old_placement.scaler.is_indulgent_resizing())
or (old_placement.width <= screen_columns < self.width)
or (old_placement.height <= screen_rows < self.height))
def is_partly_reload_required(self, old_placement,
screen_columns, screen_rows):
"""Determines whether it's required to partly reload
the image of the placement to render the placement more quickly.
Args:
old_placement (ui.CanvasWindow.Placement):
the old data of the placement
screen_columns (float):
the maximum amount of columns the screen can display
screen_rows (float):
the maximum amount of rows the screen can display
Returns:
bool: True if the image should be reloaded
"""
return old_placement and (
(self.scaler_class.is_indulgent_resizing()
and not old_placement.scaler.is_indulgent_resizing())
or (self.width <= screen_columns < old_placement.width)
or (self.height <= screen_rows < old_placement.height))
async def apply(self, windows, view, tools):
try:
import ueberzug.ui as ui
import ueberzug.loading as loading
old_placement = view.media.pop(self.identifier, None)
cache = old_placement and old_placement.cache
image = old_placement and old_placement.image
max_font_width = max(map(
lambda i: i or 0, windows.parent_info.font_width or [0]))
max_font_height = max(map(
lambda i: i or 0, windows.parent_info.font_height or [0]))
font_size_available = max_font_width and max_font_height
screen_columns = (font_size_available and
view.screen_width / max_font_width)
screen_rows = (font_size_available and
view.screen_height / max_font_height)
# By default images are only stored up to a resolution which
# is about as big as the screen resolution.
# (loading.CoverPostLoadImageProcessor)
# The principle of spatial locality does not apply to
# resize operations of images with big resolutions
# which is why those operations should be applied
# to a resized version of those images.
# Sometimes we still need all pixels e.g.
# if the image scaler crop is used.
# So sometimes it's required to fully load them
# and sometimes it's not required anymore which is
# why they should be partly reloaded
# (to speed up the resize operations again).
if (not self.is_same_image(old_placement)
or (font_size_available and self.is_full_reload_required(
old_placement, screen_columns, screen_rows))
or (font_size_available and self.is_partly_reload_required(
old_placement, screen_columns, screen_rows))):
upper_bound_size = None
image_post_load_processor = None
if (self.scaler_class != scaling.CropImageScaler and
font_size_available):
upper_bound_size = (
max_font_width * self.width,
max_font_height * self.height)
if (self.scaler_class != scaling.CropImageScaler
and font_size_available
and self.width <= screen_columns
and self.height <= screen_rows):
image_post_load_processor = \
loading.CoverPostLoadImageProcessor(
view.screen_width, view.screen_height)
image = tools.loader.load(
self.path, upper_bound_size, image_post_load_processor)
cache = None
view.media[self.identifier] = ui.CanvasWindow.Placement(
self.x, self.y, self.width, self.height,
geometry.Point(self.scaling_position_x,
self.scaling_position_y),
self.scaler_class(),
self.path, image, self.last_modified, cache)
finally:
await super().apply(windows, view, tools)
@attr.s(kw_only=True)
class RemoveImageAction(ImageAction):
"""Removes the image with the passed identifier."""
@staticmethod
def get_action_name():
return 'remove'
async def apply(self, windows, view, tools):
try:
if self.identifier in view.media:
del view.media[self.identifier]
finally:
await super().apply(windows, view, tools)
@enum.unique
class Command(str, enum.Enum):
ADD = AddImageAction
REMOVE = RemoveImageAction
def __new__(cls, action_class):
inst = str.__new__(cls)
inst._value_ = action_class.get_action_name()
inst.action_class = action_class
return inst