mirror of https://github.com/seebye/ueberzug
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.
295 lines
11 KiB
Python
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
|