Compare commits

...

3 Commits

Author SHA1 Message Date
728d9aeddc
add lib-not-dr as requirement 2023-10-16 22:16:51 +08:00
79cdba7b9c
Enhance | logger with Template 2023-10-16 22:13:21 +08:00
1cfd6cc066
sync pyglet 2023-10-16 22:13:04 +08:00
13 changed files with 423 additions and 443 deletions

View File

@ -266,30 +266,29 @@ class DWRITE_CLUSTER_METRICS(ctypes.Structure):
class IDWriteFontFileStream(com.IUnknown):
_methods_ = [
('ReadFileFragment',
com.STDMETHOD(c_void_p, POINTER(c_void_p), UINT64, UINT64, POINTER(c_void_p))),
com.STDMETHOD(POINTER(c_void_p), UINT64, UINT64, POINTER(c_void_p))),
('ReleaseFileFragment',
com.STDMETHOD(c_void_p, c_void_p)),
com.STDMETHOD(c_void_p)),
('GetFileSize',
com.STDMETHOD(c_void_p, POINTER(UINT64))),
com.STDMETHOD(POINTER(UINT64))),
('GetLastWriteTime',
com.STDMETHOD(c_void_p, POINTER(UINT64))),
com.STDMETHOD(POINTER(UINT64))),
]
class IDWriteFontFileLoader_LI(com.IUnknown): # Local implementation use only.
_methods_ = [
('CreateStreamFromKey',
com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream))))
com.STDMETHOD(c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream))))
]
class IDWriteFontFileLoader(com.pIUnknown):
_methods_ = [
('CreateStreamFromKey',
com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream))))
com.STDMETHOD(c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream))))
]
class IDWriteLocalFontFileLoader(IDWriteFontFileLoader, com.pIUnknown):
_methods_ = [
('GetFilePathLengthFromKey',
@ -452,13 +451,13 @@ DWRITE_READING_DIRECTION_LEFT_TO_RIGHT = 0
class IDWriteTextAnalysisSource(com.IUnknown):
_methods_ = [
('GetTextAtPosition',
com.METHOD(HRESULT, c_void_p, UINT32, POINTER(c_wchar_p), POINTER(UINT32))),
com.STDMETHOD(UINT32, POINTER(c_wchar_p), POINTER(UINT32))),
('GetTextBeforePosition',
com.STDMETHOD(UINT32, c_wchar_p, POINTER(UINT32))),
com.STDMETHOD(UINT32, POINTER(c_wchar_p), POINTER(UINT32))),
('GetParagraphReadingDirection',
com.METHOD(DWRITE_READING_DIRECTION)),
('GetLocaleName',
com.STDMETHOD(c_void_p, UINT32, POINTER(UINT32), POINTER(c_wchar_p))),
com.STDMETHOD(UINT32, POINTER(UINT32), POINTER(c_wchar_p))),
('GetNumberSubstitution',
com.STDMETHOD(UINT32, POINTER(UINT32), c_void_p)),
]
@ -467,7 +466,7 @@ class IDWriteTextAnalysisSource(com.IUnknown):
class IDWriteTextAnalysisSink(com.IUnknown):
_methods_ = [
('SetScriptAnalysis',
com.STDMETHOD(c_void_p, UINT32, UINT32, POINTER(DWRITE_SCRIPT_ANALYSIS))),
com.STDMETHOD(UINT32, UINT32, POINTER(DWRITE_SCRIPT_ANALYSIS))),
('SetLineBreakpoints',
com.STDMETHOD(UINT32, UINT32, c_void_p)),
('SetBidiLevel',
@ -524,7 +523,7 @@ class TextAnalysis(com.COMObject):
analyzer.AnalyzeScript(self, 0, text_length, self)
def SetScriptAnalysis(self, this, textPosition, textLength, scriptAnalysis):
def SetScriptAnalysis(self, textPosition, textLength, scriptAnalysis):
# textPosition - The index of the first character in the string that the result applies to
# textLength - How many characters of the string from the index that the result applies to
# scriptAnalysis - The analysis information for all glyphs starting at position for length.
@ -542,10 +541,10 @@ class TextAnalysis(com.COMObject):
return 0
# return 0x80004001
def GetTextBeforePosition(self, this, textPosition, textString, textLength):
def GetTextBeforePosition(self, textPosition, textString, textLength):
raise Exception("Currently not implemented.")
def GetTextAtPosition(self, this, textPosition, textString, textLength):
def GetTextAtPosition(self, textPosition, textString, textLength):
# This method will retrieve a substring of the text in this layout
# to be used in an analysis step.
# Arguments:
@ -568,7 +567,7 @@ class TextAnalysis(com.COMObject):
def GetParagraphReadingDirection(self):
return 0
def GetLocaleName(self, this, textPosition, textLength, localeName):
def GetLocaleName(self, textPosition, textLength, localeName):
self.__local_name = c_wchar_p("") # TODO: Add more locales.
localeName[0] = self.__local_name
textLength[0] = self._textlength - textPosition
@ -954,16 +953,16 @@ class IDWriteTextLayout1(IDWriteTextLayout, IDWriteTextFormat, com.pIUnknown):
class IDWriteFontFileEnumerator(com.IUnknown):
_methods_ = [
('MoveNext',
com.STDMETHOD(c_void_p, POINTER(BOOL))),
com.STDMETHOD(POINTER(BOOL))),
('GetCurrentFontFile',
com.STDMETHOD(c_void_p, c_void_p)),
com.STDMETHOD(c_void_p)),
]
class IDWriteFontCollectionLoader(com.IUnknown):
_methods_ = [
('CreateEnumeratorFromKey',
com.STDMETHOD(c_void_p, c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileEnumerator)))),
com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileEnumerator)))),
]
@ -971,20 +970,12 @@ class MyFontFileStream(com.COMObject):
_interfaces_ = [IDWriteFontFileStream]
def __init__(self, data):
super().__init__()
self._data = data
self._size = len(data)
self._ptrs = []
def AddRef(self, this):
return 1
def Release(self, this):
return 1
def QueryInterface(self, this, refiid, tester):
return 0
def ReadFileFragment(self, this, fragmentStart, fileOffset, fragmentSize, fragmentContext):
def ReadFileFragment(self, fragmentStart, fileOffset, fragmentSize, fragmentContext):
if fileOffset + fragmentSize > self._size:
return 0x80004005 # E_FAIL
@ -997,14 +988,14 @@ class MyFontFileStream(com.COMObject):
fragmentContext[0] = None
return 0
def ReleaseFileFragment(self, this, fragmentContext):
def ReleaseFileFragment(self, fragmentContext):
return 0
def GetFileSize(self, this, fileSize):
def GetFileSize(self, fileSize):
fileSize[0] = self._size
return 0
def GetLastWriteTime(self, this, lastWriteTime):
def GetLastWriteTime(self, lastWriteTime):
return 0x80004001 # E_NOTIMPL
@ -1012,21 +1003,13 @@ class LegacyFontFileLoader(com.COMObject):
_interfaces_ = [IDWriteFontFileLoader_LI]
def __init__(self):
super().__init__()
self._streams = {}
def QueryInterface(self, this, refiid, tester):
return 0
def AddRef(self, this):
return 1
def Release(self, this):
return 1
def CreateStreamFromKey(self, this, fontfileReferenceKey, fontFileReferenceKeySize, fontFileStream):
def CreateStreamFromKey(self, fontfileReferenceKey, fontFileReferenceKeySize, fontFileStream):
convert_index = cast(fontfileReferenceKey, POINTER(c_uint32))
self._ptr = ctypes.cast(self._streams[convert_index.contents.value]._pointers[IDWriteFontFileStream],
self._ptr = ctypes.cast(self._streams[convert_index.contents.value].as_interface(IDWriteFontFileStream),
POINTER(IDWriteFontFileStream))
fontFileStream[0] = self._ptr
return 0
@ -1039,6 +1022,7 @@ class MyEnumerator(com.COMObject):
_interfaces_ = [IDWriteFontFileEnumerator]
def __init__(self, factory, loader):
super().__init__()
self.factory = cast(factory, IDWriteFactory)
self.key = "pyglet_dwrite"
self.size = len(self.key)
@ -1057,7 +1041,7 @@ class MyEnumerator(com.COMObject):
def AddFontData(self, fonts):
self._font_data = fonts
def MoveNext(self, this, hasCurrentFile):
def MoveNext(self, hasCurrentFile):
self.current_index += 1
if self.current_index != len(self._font_data):
@ -1087,7 +1071,7 @@ class MyEnumerator(com.COMObject):
pass
def GetCurrentFontFile(self, this, fontFile):
def GetCurrentFontFile(self, fontFile):
fontFile = cast(fontFile, POINTER(IDWriteFontFile))
fontFile[0] = self._font_files[self.current_index]
return 0
@ -1097,24 +1081,14 @@ class LegacyCollectionLoader(com.COMObject):
_interfaces_ = [IDWriteFontCollectionLoader]
def __init__(self, factory, loader):
super().__init__()
self._enumerator = MyEnumerator(factory, loader)
def AddFontData(self, fonts):
self._enumerator.AddFontData(fonts)
def AddRef(self, this):
self._i = 1
return 1
def Release(self, this):
self._i = 0
return 1
def QueryInterface(self, this, refiid, tester):
return 0
def CreateEnumeratorFromKey(self, this, factory, key, key_size, enumerator):
self._ptr = ctypes.cast(self._enumerator._pointers[IDWriteFontFileEnumerator],
def CreateEnumeratorFromKey(self, factory, key, key_size, enumerator):
self._ptr = ctypes.cast(self._enumerator.as_interface(IDWriteFontFileEnumerator),
POINTER(IDWriteFontFileEnumerator))
enumerator[0] = self._ptr
@ -2418,7 +2392,7 @@ class Win32DirectWriteFont(base.Font):
# Note: RegisterFontLoader takes a pointer. However, for legacy we implement our own callback interface.
# Therefore we need to pass to the actual pointer directly.
cls._write_factory.RegisterFontFileLoader(cls._font_loader.pointers[IDWriteFontFileLoader_LI])
cls._write_factory.RegisterFontFileLoader(cls._font_loader.as_interface(IDWriteFontFileLoader_LI))
cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader)
cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader)
@ -2472,7 +2446,7 @@ class Win32DirectWriteFont(base.Font):
cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader)
cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader)
cls._write_factory.RegisterFontFileLoader(cls._font_loader.pointers[IDWriteFontFileLoader_LI])
cls._write_factory.RegisterFontFileLoader(cls._font_loader.as_interface(IDWriteFontFileLoader_LI))
cls._font_collection_loader.AddFontData(cls._font_cache)

View File

@ -170,14 +170,14 @@ class Attribute:
self.name = name
self.location = location
self.count = count
self.gl_type = gl_type
self.c_type = _c_types[gl_type]
self.normalize = normalize
self.align = sizeof(self.c_type)
self.size = count * self.align
self.stride = self.size
self.c_type = _c_types[gl_type]
self.element_size = sizeof(self.c_type)
self.byte_size = count * self.element_size
self.stride = self.byte_size
def enable(self):
"""Enable the attribute."""
@ -216,15 +216,11 @@ class Attribute:
:rtype: `AbstractBufferRegion`
"""
byte_start = self.stride * start
byte_size = self.stride * count
array_count = self.count * count
ptr_type = POINTER(self.c_type * array_count)
return buffer.get_region(byte_start, byte_size, ptr_type)
return buffer.get_region(start, count)
def set_region(self, buffer, start, count, data):
"""Set the data over a region of the buffer.
"""Set the data over a region of the buffer.
:Parameters:
`buffer` : AbstractMappable`
The buffer to modify.
@ -234,9 +230,10 @@ class Attribute:
Number of vertices to set.
`data` : A sequence of data components.
"""
byte_start = self.stride * start
byte_size = self.stride * count
array_count = self.count * count
byte_start = self.stride * start # byte offset
byte_size = self.stride * count # number of bytes
array_count = self.count * count # umber of values
data = (self.c_type * array_count)(*data)
buffer.set_data_region(data, byte_start, byte_size)

View File

@ -11,6 +11,8 @@ the buffer.
import sys
import ctypes
from functools import lru_cache
import pyglet
from pyglet.gl import *
@ -98,36 +100,6 @@ class AbstractBuffer:
raise NotImplementedError('abstract')
class AbstractMappable:
def get_region(self, start, size, ptr_type):
"""Map a region of the buffer into a ctypes array of the desired
type. This region does not need to be unmapped, but will become
invalid if the buffer is resized.
Note that although a pointer type is required, an array is mapped.
For example::
get_region(0, ctypes.sizeof(c_int) * 20, ctypes.POINTER(c_int * 20))
will map bytes 0 to 80 of the buffer to an array of 20 ints.
Changes to the array may not be recognised until the region's
:py:meth:`AbstractBufferRegion.invalidate` method is called.
:Parameters:
`start` : int
Offset into the buffer to map from, in bytes
`size` : int
Size of the buffer region to map, in bytes
`ptr_type` : ctypes pointer type
Pointer type describing the array format to create
:rtype: :py:class:`AbstractBufferRegion`
"""
raise NotImplementedError('abstract')
class BufferObject(AbstractBuffer):
"""Lightweight representation of an OpenGL Buffer Object.
@ -222,27 +194,28 @@ class BufferObject(AbstractBuffer):
return f"{self.__class__.__name__}(id={self.id}, size={self.size})"
class MappableBufferObject(BufferObject, AbstractMappable):
class AttributeBufferObject(BufferObject):
"""A buffer with system-memory backed store.
Updates to the data via `set_data`, `set_data_region` and `map` will be
held in local memory until `bind` is called. The advantage is that fewer
OpenGL calls are needed, increasing performance.
There may also be less performance penalty for resizing this buffer.
Updates to data via :py:meth:`map` are committed immediately.
Updates to the data via `set_data` and `set_data_region` will be held
in local memory until `buffer_data` is called. The advantage is that
fewer OpenGL calls are needed, which can increasing performance at the
expense of system memory.
"""
def __init__(self, size, usage=GL_DYNAMIC_DRAW):
super(MappableBufferObject, self).__init__(size, usage)
def __init__(self, size, attribute, usage=GL_DYNAMIC_DRAW):
super().__init__(size, usage)
self._size = size
self.data = (ctypes.c_byte * size)()
self.data_ptr = ctypes.addressof(self.data)
self._dirty_min = sys.maxsize
self._dirty_max = 0
def bind(self):
# Commit pending data
super(MappableBufferObject, self).bind()
self.attribute_stride = attribute.stride
self.attribute_count = attribute.count
self.attribute_ctype = attribute.c_type
def bind(self, target=GL_ARRAY_BUFFER):
super().bind(target)
size = self._dirty_max - self._dirty_min
if size > 0:
if size == self.size:
@ -253,7 +226,7 @@ class MappableBufferObject(BufferObject, AbstractMappable):
self._dirty_max = 0
def set_data(self, data):
super(MappableBufferObject, self).set_data(data)
super().set_data(data)
ctypes.memmove(self.data, data, self.size)
self._dirty_min = 0
self._dirty_max = self.size
@ -263,17 +236,16 @@ class MappableBufferObject(BufferObject, AbstractMappable):
self._dirty_min = min(start, self._dirty_min)
self._dirty_max = max(start + length, self._dirty_max)
def map(self, invalidate=False):
self._dirty_min = 0
self._dirty_max = self.size
return self.data
@lru_cache(maxsize=None)
def get_region(self, start, count):
def unmap(self):
pass
byte_start = self.attribute_stride * start # byte offset
byte_size = self.attribute_stride * count # number of bytes
array_count = self.attribute_count * count # number of values
def get_region(self, start, size, ptr_type):
array = ctypes.cast(self.data_ptr + start, ptr_type).contents
return BufferObjectRegion(self, start, start + size, array)
ptr_type = ctypes.POINTER(self.attribute_ctype * array_count)
array = ctypes.cast(self.data_ptr + byte_start, ptr_type).contents
return BufferObjectRegion(self, byte_start, byte_start + byte_size, array)
def resize(self, size):
data = (ctypes.c_byte * size)()
@ -289,6 +261,8 @@ class MappableBufferObject(BufferObject, AbstractMappable):
self._dirty_min = sys.maxsize
self._dirty_max = 0
self.get_region.cache_clear()
class BufferObjectRegion:
"""A mapped region of a MappableBufferObject."""

View File

@ -23,11 +23,9 @@ primitives of the same OpenGL primitive mode.
import ctypes
import pyglet
from pyglet.gl import *
from pyglet.graphics import allocation, shader, vertexarray
from pyglet.graphics.vertexbuffer import BufferObject, MappableBufferObject
from pyglet.graphics.vertexbuffer import BufferObject, AttributeBufferObject
def _nearest_pow2(v):
@ -66,6 +64,22 @@ _gl_types = {
}
def _make_attribute_property(attribute):
attrname = attribute.name
def _attribute_getter(self):
region = attribute.get_region(attribute.buffer, self.start, self.count)
region.invalidate()
return region.array
def _attribute_setter(self, values):
getattr(self, attrname)[:] = values
return property(_attribute_getter, _attribute_setter)
class VertexDomain:
"""Management of a set of vertex lists.
@ -80,8 +94,10 @@ class VertexDomain:
self.attribute_meta = attribute_meta
self.allocator = allocation.Allocator(self._initial_count)
self.attributes = []
self.buffer_attributes = [] # list of (buffer, attributes)
self.attribute_names = {} # name: attribute
self.buffer_attributes = [] # list of (buffer, attributes)
self._property_dict = {} # name: property(_getter, _setter)
for name, meta in attribute_meta.items():
assert meta['format'][0] in _gl_types, f"'{meta['format']}' is not a valid atrribute format for '{name}'."
@ -90,14 +106,19 @@ class VertexDomain:
gl_type = _gl_types[meta['format'][0]]
normalize = 'n' in meta['format']
attribute = shader.Attribute(name, location, count, gl_type, normalize)
self.attributes.append(attribute)
self.attribute_names[attribute.name] = attribute
# Create buffer:
attribute.buffer = MappableBufferObject(attribute.stride * self.allocator.capacity)
attribute.buffer.element_size = attribute.stride
attribute.buffer.attributes = (attribute,)
attribute.buffer = AttributeBufferObject(attribute.stride * self.allocator.capacity, attribute)
self.buffer_attributes.append((attribute.buffer, (attribute,)))
# Create custom property to be used in the VertexList:
self._property_dict[attribute.name] = _make_attribute_property(attribute)
# Make a custom VertexList class w/ properties for each attribute in the ShaderProgram:
self._vertexlist_class = type("VertexList", (VertexList,), self._property_dict)
self.vao = vertexarray.VertexArray()
self.vao.bind()
for buffer, attributes in self.buffer_attributes:
@ -107,20 +128,6 @@ class VertexDomain:
attribute.set_pointer(buffer.ptr)
self.vao.unbind()
# Create named attributes for each attribute
self.attribute_names = {}
for attribute in self.attributes:
self.attribute_names[attribute.name] = attribute
def __del__(self):
# Break circular refs that Python GC seems to miss even when forced
# collection.
for attribute in self.attributes:
try:
del attribute.buffer
except AttributeError:
pass
def safe_alloc(self, count):
"""Allocate vertices, resizing the buffers if necessary."""
try:
@ -129,7 +136,7 @@ class VertexDomain:
capacity = _nearest_pow2(e.requested_capacity)
self.version += 1
for buffer, _ in self.buffer_attributes:
buffer.resize(capacity * buffer.element_size)
buffer.resize(capacity * buffer.attribute_stride)
self.allocator.set_capacity(capacity)
return self.allocator.alloc(count)
@ -141,7 +148,7 @@ class VertexDomain:
capacity = _nearest_pow2(e.requested_capacity)
self.version += 1
for buffer, _ in self.buffer_attributes:
buffer.resize(capacity * buffer.element_size)
buffer.resize(capacity * buffer.attribute_stride)
self.allocator.set_capacity(capacity)
return self.allocator.realloc(start, count, new_count)
@ -157,7 +164,7 @@ class VertexDomain:
:rtype: :py:class:`VertexList`
"""
start = self.safe_alloc(count)
return VertexList(self, start, count)
return self._vertexlist_class(self, start, count)
def draw(self, mode):
"""Draw all vertices in the domain.
@ -221,8 +228,6 @@ class VertexList:
self.domain = domain
self.start = start
self.count = count
self._caches = {}
self._cache_versions = {}
def draw(self, mode):
"""Draw this vertex list in the given OpenGL mode.
@ -247,7 +252,7 @@ class VertexList:
new_start = self.domain.safe_realloc(self.start, self.count, count)
if new_start != self.start:
# Copy contents to new location
for attribute in self.domain.attributes:
for attribute in self.domain.attribute_names.values():
old = attribute.get_region(attribute.buffer, self.start, self.count)
new = attribute.get_region(attribute.buffer, new_start, self.count)
new.array[:] = old.array[:]
@ -255,9 +260,6 @@ class VertexList:
self.start = new_start
self.count = count
for version in self._cache_versions:
self._cache_versions[version] = None
def delete(self):
"""Delete this group."""
self.domain.allocator.dealloc(self.start, self.count)
@ -287,33 +289,10 @@ class VertexList:
self.domain = domain
self.start = new_start
for version in self._cache_versions:
self._cache_versions[version] = None
def set_attribute_data(self, name, data):
attribute = self.domain.attribute_names[name]
attribute.set_region(attribute.buffer, self.start, self.count, data)
def __getattr__(self, name):
"""dynamic access to vertex attributes, for backwards compatibility.
"""
domain = self.domain
if self._cache_versions.get(name, None) != domain.version:
attribute = domain.attribute_names[name]
self._caches[name] = attribute.get_region(attribute.buffer, self.start, self.count)
self._cache_versions[name] = domain.version
region = self._caches[name]
region.invalidate()
return region.array
def __setattr__(self, name, value):
# Allow setting vertex attributes directly without overwriting them:
if 'domain' in self.__dict__ and name in self.__dict__['domain'].attribute_names:
getattr(self, name)[:] = value
return
super().__setattr__(name, value)
class IndexedVertexDomain(VertexDomain):
"""Management of a set of indexed vertex lists.
@ -337,6 +316,9 @@ class IndexedVertexDomain(VertexDomain):
self.index_buffer.bind_to_index_buffer()
self.vao.unbind()
# Make a custom VertexList class w/ properties for each attribute in the ShaderProgram:
self._vertexlist_class = type("IndexedVertexList", (IndexedVertexList,), self._property_dict)
def safe_index_alloc(self, count):
"""Allocate indices, resizing the buffers if necessary."""
try:
@ -371,7 +353,7 @@ class IndexedVertexDomain(VertexDomain):
"""
start = self.safe_alloc(count)
index_start = self.safe_index_alloc(index_count)
return IndexedVertexList(self, start, count, index_start, index_count)
return self._vertexlist_class(self, start, count, index_start, index_count)
def get_index_region(self, start, count):
"""Get a data from a region of the index buffer.

View File

@ -18,7 +18,7 @@ Interfaces can define methods::
...
]
Only use STDMETHOD or METHOD for the method types (not ordinary ctypes
Only use METHOD, STDMETHOD or VOIDMETHOD for the method types (not ordinary ctypes
function types). The 'this' pointer is bound automatically... e.g., call::
device = IDirectSound8()
@ -50,7 +50,7 @@ class GUID(ctypes.Structure):
('Data1', ctypes.c_ulong),
('Data2', ctypes.c_ushort),
('Data3', ctypes.c_ushort),
('Data4', ctypes.c_ubyte * 8)
('Data4', ctypes.c_ubyte * 8),
]
def __init__(self, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8):
@ -64,11 +64,6 @@ class GUID(ctypes.Structure):
return 'GUID(%x, %x, %x, %x, %x, %x, %x, %x, %x, %x, %x)' % (
self.Data1, self.Data2, self.Data3, b1, b2, b3, b4, b5, b6, b7, b8)
def __cmp__(self, other):
if isinstance(other, GUID):
return ctypes.cmp(bytes(self), bytes(other))
return -1
def __eq__(self, other):
return isinstance(other, GUID) and bytes(self) == bytes(other)
@ -80,6 +75,10 @@ LPGUID = ctypes.POINTER(GUID)
IID = GUID
REFIID = ctypes.POINTER(IID)
S_OK = 0x00000000
E_NOTIMPL = 0x80004001
E_NOINTERFACE = 0x80004002
class METHOD:
"""COM method."""
@ -88,244 +87,147 @@ class METHOD:
self.restype = restype
self.argtypes = args
def get_field(self):
# ctypes caches WINFUNCTYPE's so this should be ok.
return ctypes.WINFUNCTYPE(self.restype, *self.argtypes)
self.prototype = ctypes.WINFUNCTYPE(self.restype, *self.argtypes)
self.direct_prototype = ctypes.WINFUNCTYPE(self.restype, ctypes.c_void_p, *self.argtypes)
def get_com_proxy(self, i, name):
return self.prototype(i, name)
class STDMETHOD(METHOD):
"""COM method with HRESULT return value."""
def __init__(self, *args):
super(STDMETHOD, self).__init__(ctypes.HRESULT, *args)
super().__init__(ctypes.HRESULT, *args)
class COMMethodInstance:
"""Binds a COM interface method."""
class VOIDMETHOD(METHOD):
"""COM method with no return value."""
def __init__(self, name, i, method):
self.name = name
self.i = i
self.method = method
def __get__(self, obj, tp):
if obj is not None:
def _call(*args):
assert _debug_com('COM: #{} IN {}({}, {})'.format(self.i, self.name, obj.__class__.__name__, args))
ret = self.method.get_field()(self.i, self.name)(obj, *args)
assert _debug_com('COM: #{} OUT {}({}, {})'.format(self.i, self.name, obj.__class__.__name__, args))
assert _debug_com('COM: RETURN {}'.format(ret))
return ret
return _call
raise AttributeError()
def __init__(self, *args):
super().__init__(None, *args)
class COMInterface(ctypes.Structure):
"""Dummy struct to serve as the type of all COM pointers."""
_fields_ = [
('lpVtbl', ctypes.c_void_p),
]
_DummyPointerType = ctypes.POINTER(ctypes.c_int)
_PointerMeta = type(_DummyPointerType)
_StructMeta = type(ctypes.Structure)
class InterfacePtrMeta(type(ctypes.POINTER(COMInterface))):
"""Allows interfaces to be subclassed as ctypes POINTER and expects to be populated with data from a COM object.
TODO: Phase this out and properly use POINTER(Interface) where applicable.
"""
class _InterfaceMeta(_StructMeta):
def __new__(cls, name, bases, dct, /, create_pointer_type=True):
if len(bases) > 1:
assert _debug_com(f"Ignoring {len(bases) - 1} bases on {name}")
bases = (bases[0],)
if not '_methods_' in dct:
dct['_methods_'] = ()
inh_methods = []
if bases[0] is not ctypes.Structure: # Method does not exist for first definition below
for interface_type in (bases[0].get_interface_inheritance()):
inh_methods.extend(interface_type.__dict__['_methods_'])
inh_methods = tuple(inh_methods)
new_methods = tuple(dct['_methods_'])
vtbl_own_offset = len(inh_methods)
all_methods = tuple(inh_methods) + new_methods
for i, (method_name, mt) in enumerate(all_methods):
assert _debug_com(f"{name}[{i}]: {method_name}: "
f"{(', '.join(t.__name__ for t in mt.argtypes) or 'void')} -> "
f"{'void' if mt.restype is None else mt.restype.__name__}")
vtbl_struct_type = _StructMeta(f"Vtable_{name}",
(ctypes.Structure,),
{'_fields_': [(n, x.direct_prototype) for n, x in all_methods]})
dct['_vtbl_struct_type'] = vtbl_struct_type
dct['vtbl_own_offset'] = vtbl_own_offset
dct['_fields_'] = (('vtbl_ptr', ctypes.POINTER(vtbl_struct_type)),)
res_type = super().__new__(cls, name, bases, dct)
if create_pointer_type:
# If we're not being created from a pInterface subclass as helper Interface (so likely
# being explicitly defined from user code for later use), create the special
# pInterface pointer subclass so it registers itself into the pointer cache
_pInterfaceMeta(f"p{name}", (ctypes.POINTER(bases[0]),), {'_type_': res_type})
return res_type
class _pInterfaceMeta(_PointerMeta):
def __new__(cls, name, bases, dct):
methods = []
for base in bases[::-1]:
methods.extend(base.__dict__.get('_methods_', ()))
methods.extend(dct.get('_methods_', ()))
# Interfaces can also be declared by inheritance of pInterface subclasses.
# If this happens, create the interface and then become pointer to its struct.
for i, (n, method) in enumerate(methods):
dct[n] = COMMethodInstance(n, i, method)
target = dct.get('_type_', None)
# If we weren't created due to an Interface subclass definition (don't have a _type_),
# just define that Interface subclass from our base's _type_
if target is None:
interface_base = bases[0]._type_
dct['_type_'] = COMInterface
# Create corresponding interface type and then set it as target
target = _InterfaceMeta(f"_{name}_HelperInterface",
(interface_base,),
{'_methods_': dct.get('_methods_', ())},
create_pointer_type=False)
dct['_type_'] = target
return super(InterfacePtrMeta, cls).__new__(cls, name, bases, dct)
# Create method proxies that will forward ourselves into the interface's methods
for i, (method_name, method) in enumerate(target._methods_):
m = method.get_com_proxy(i + target.vtbl_own_offset, method_name)
def pinterface_method_forward(self, *args, _m=m, _i=i):
assert _debug_com(f'Calling COM {_i} of {target.__name__} ({_m}) through '
f'pointer: ({", ".join(map(repr, (self, *args)))})')
return _m(self, *args)
dct[method_name] = pinterface_method_forward
pointer_type = super().__new__(cls, name, bases, dct)
class pInterface(ctypes.POINTER(COMInterface), metaclass=InterfacePtrMeta):
"""Base COM interface pointer."""
class COMInterfaceMeta(type):
"""This differs in the original as an implemented interface object, not a POINTER object.
Used when the user must implement their own functions within an interface rather than
being created and generated by the COM object itself. The types are automatically inserted in the ctypes type
cache so it can recognize the type arguments.
"""
def __new__(mcs, name, bases, dct):
methods = dct.pop("_methods_", None)
cls = type.__new__(mcs, name, bases, dct)
if methods is not None:
cls._methods_ = methods
if not bases:
_ptr_bases = (cls, COMPointer)
else:
_ptr_bases = (cls, ctypes.POINTER(bases[0]))
# Class type is dynamically created inside __new__ based on metaclass inheritence; update ctypes cache manually.
# Hack selves into the ctypes pointer cache so all uses of `ctypes.POINTER` on the
# interface type will yield it instead of the inflexible standard pointer type.
# NOTE: This is done pretty much exclusively to help convert COMObjects.
# Some additional work from callers like
# RegisterCallback(callback_obj.as_interface(ICallback))
# instead of
# RegisterCallback(callback_obj)
# could make it obsolete.
from ctypes import _pointer_type_cache
_pointer_type_cache[cls] = type(COMPointer)("POINTER({})".format(cls.__name__),
_ptr_bases,
{"__interface__": cls})
_pointer_type_cache[target] = pointer_type
return cls
return pointer_type
def __get_subclassed_methodcount(self):
"""Returns the amount of COM methods in all subclasses to determine offset of methods.
Order must be exact from the source when calling COM methods.
class Interface(ctypes.Structure, metaclass=_InterfaceMeta, create_pointer_type=False):
@classmethod
def get_interface_inheritance(cls):
"""Returns the types of all interfaces implemented by this interface, up to but not
including the base `Interface`.
`Interface` does not represent an actual interface, but merely the base concept of
them, so viewing it as part of an interface's inheritance chain is meaningless.
"""
try:
result = 0
for itf in self.mro()[1:-1]:
result += len(itf.__dict__["_methods_"])
return result
except KeyError as err:
(name,) = err.args
if name == "_methods_":
raise TypeError("Interface '{}' requires a _methods_ attribute.".format(itf.__name__))
raise
return cls.__mro__[:cls.__mro__.index(Interface)]
class COMPointerMeta(type(ctypes.c_void_p), COMInterfaceMeta):
"""Required to prevent metaclass conflicts with inheritance."""
class COMPointer(ctypes.c_void_p, metaclass=COMPointerMeta):
"""COM Pointer base, could use c_void_p but need to override from_param ."""
class pInterface(_DummyPointerType, metaclass=_pInterfaceMeta):
_type_ = Interface
@classmethod
def from_param(cls, obj):
"""Allows obj to return ctypes pointers, even if its base is not a ctype.
In this case, all we simply want is a ctypes pointer matching the cls interface from the obj.
"""
if obj is None:
return
"""When dealing with a COMObject, pry a fitting interface out of it"""
try:
ptr_dct = obj._pointers
except AttributeError:
raise Exception("Interface method argument specified incorrectly, or passed wrong argument.", cls)
else:
try:
return ptr_dct[cls.__interface__]
except KeyError:
raise TypeError("Interface {} doesn't have a pointer in this class.".format(cls.__name__))
if not isinstance(obj, COMObject):
return obj
return obj.as_interface(cls._type_)
def _missing_impl(interface_name, method_name):
"""Functions that are not implemented use this to prevent errors when called."""
def missing_cb_func(*args):
"""Return E_NOTIMPL because the method is not implemented."""
assert _debug_com("Undefined method: {0} was called in interface: {1}".format(method_name, interface_name))
return 0
return missing_cb_func
def _found_impl(interface_name, method_name, method_func):
"""If a method was found in class, we can set it as a callback."""
def cb_func(*args, **kw):
try:
result = method_func(*args, **kw)
except Exception as err:
raise err
if not result: # QOL so callbacks don't need to specify a return for assumed OK's.
return 0
return result
return cb_func
def _make_callback_func(interface, name, method_func):
"""Create a callback function for ctypes if possible."""
if method_func is None:
return _missing_impl(interface, name)
return _found_impl(interface, name, method_func)
# Store structures with same fields to prevent duplicate table creations.
_cached_structures = {}
def create_vtbl_structure(fields, interface):
"""Create virtual table structure with fields for use in COM's."""
try:
return _cached_structures[fields]
except KeyError:
Vtbl = type("Vtbl_{}".format(interface.__name__), (ctypes.Structure,), {"_fields_": fields})
_cached_structures[fields] = Vtbl
return Vtbl
class COMObject:
"""A base class for defining a COM object for use with callbacks and custom implementations."""
_interfaces_ = []
def __new__(cls, *args, **kw):
new_cls = super(COMObject, cls).__new__(cls)
assert len(cls._interfaces_) > 0, "Atleast one interface must be defined to use a COMObject."
new_cls._pointers = {}
new_cls.__create_interface_pointers()
return new_cls
def __create_interface_pointers(cls):
"""Create a custom ctypes structure to handle COM functions in a COM Object."""
interfaces = tuple(cls._interfaces_)
for itf in interfaces[::-1]:
methods = []
fields = []
for interface in itf.__mro__[-2::-1]:
for method in interface._methods_:
name, com_method = method
found_method = getattr(cls, name, None)
mth = _make_callback_func(itf.__name__, name, found_method)
proto = ctypes.WINFUNCTYPE(com_method.restype, *com_method.argtypes)
fields.append((name, proto))
methods.append(proto(mth))
# Make a structure dynamically with the fields given.
itf_structure = create_vtbl_structure(tuple(fields), interface)
# Assign the methods to the fields
vtbl = itf_structure(*methods)
cls._pointers[itf] = ctypes.pointer(ctypes.pointer(vtbl))
@property
def pointers(self):
"""Returns pointers to the implemented interfaces in this COMObject. Read-only.
:type: dict
"""
return self._pointers
class Interface(metaclass=COMInterfaceMeta):
_methods_ = []
class IUnknown(metaclass=COMInterfaceMeta):
"""These methods are not implemented by default yet. Strictly for COM method ordering."""
class IUnknown(Interface):
_methods_ = [
('QueryInterface', STDMETHOD(ctypes.c_void_p, REFIID, ctypes.c_void_p)),
('AddRef', METHOD(ctypes.c_int, ctypes.c_void_p)),
('Release', METHOD(ctypes.c_int, ctypes.c_void_p))
('QueryInterface', STDMETHOD(REFIID, ctypes.c_void_p)),
('AddRef', METHOD(ctypes.c_int)),
('Release', METHOD(ctypes.c_int)),
]
@ -333,5 +235,163 @@ class pIUnknown(pInterface):
_methods_ = [
('QueryInterface', STDMETHOD(REFIID, ctypes.c_void_p)),
('AddRef', METHOD(ctypes.c_int)),
('Release', METHOD(ctypes.c_int))
('Release', METHOD(ctypes.c_int)),
]
def _missing_impl(interface_name, method_name):
"""Create a callback returning E_NOTIMPL for methods not present on a COMObject."""
def missing_cb_func(*_):
assert _debug_com(f"Non-implemented method {method_name} called in {interface_name}")
return E_NOTIMPL
return missing_cb_func
def _found_impl(interface_name, method_name, method_func, self_distance):
"""If a method was found in class, create a callback extracting self from the struct
pointer.
"""
def self_extracting_cb_func(p, *args):
assert _debug_com(f"COMObject method {method_name} called through interface {interface_name}")
self = ctypes.cast(p + self_distance, ctypes.POINTER(ctypes.py_object)).contents.value
result = method_func(self, *args)
# Assume no return statement translates to success
return S_OK if result is None else result
return self_extracting_cb_func
def _adjust_impl(interface_name, method_name, original_method, offset):
"""A method implemented in a previous interface modifies the COMOboject pointer so it
corresponds to an earlier interface and passes it on to the actual implementation.
"""
def adjustor_cb_func(p, *args):
assert _debug_com(f"COMObject method {method_name} called through interface "
f"{interface_name}, adjusting pointer by {offset}")
return original_method(p + offset, *args)
return adjustor_cb_func
class COMObject:
"""A COMObject for implementing C callbacks in Python.
Specify the interface types it supports in `_interfaces_`, and any methods to be implemented
by those interfaces as standard python methods. If the names match, they will be run as
callbacks with all arguments supplied as the types specified in the corresponding interface,
and `self` available as usual.
Remember to call `super().__init__()`.
COMObjects can be passed to ctypes functions directly as long as the corresponding argtype is
an `Interface` pointer, or a `pInterface` subclass.
IUnknown's methods will be autogenerated in case IUnknown is implemented.
"""
def __init_subclass__(cls, /, **kwargs):
super().__init_subclass__(**kwargs)
implemented_leaf_interfaces = cls.__dict__.get('_interfaces_', ())
if not implemented_leaf_interfaces:
raise TypeError("At least one interface must be defined to use a COMObject")
for interface_type in implemented_leaf_interfaces:
for other in implemented_leaf_interfaces:
if interface_type is other:
continue
if issubclass(interface_type, other):
raise TypeError("Only specify the leaf interfaces")
# Sanity check done
_ptr_size = ctypes.sizeof(ctypes.c_void_p)
_vtbl_pointers = []
implemented_methods = {}
# Map all leaf and inherited interfaces to the offset of the vtable containing
# their implementations
_interface_to_vtbl_offset = {}
for i, interface_type in enumerate(implemented_leaf_interfaces):
bases = interface_type.get_interface_inheritance()
for base in bases:
if base not in _interface_to_vtbl_offset:
_interface_to_vtbl_offset[base] = i * _ptr_size
if IUnknown in _interface_to_vtbl_offset:
def QueryInterface(self, iid_ptr, res_ptr):
ctypes.cast(res_ptr, ctypes.POINTER(ctypes.c_void_p))[0] = 0
return E_NOINTERFACE
def AddRef(self):
self._vrefcount += 1
return self._vrefcount
def Release(self):
if self._vrefcount <= 0:
assert _debug_com(
f"COMObject {self}: Release while refcount was {self._vrefcount}"
)
self._vrefcount -= 1
return self._vrefcount
cls.QueryInterface = QueryInterface
cls.AddRef = AddRef
cls.Release = Release
for i, interface_type in enumerate(implemented_leaf_interfaces):
wrappers = []
for method_name, method_type in interface_type._vtbl_struct_type._fields_:
if method_name in implemented_methods:
# Method is already implemented on a previous interface; redirect to it
# See https://devblogs.microsoft.com/oldnewthing/20040206-00/?p=40723
# NOTE: Never tested, might be totally wrong
func, implementing_vtbl_idx = implemented_methods[method_name]
mth = _adjust_impl(interface_type.__name__,
method_name,
func,
(implementing_vtbl_idx - i) * _ptr_size)
else:
if (found_method := getattr(cls, method_name, None)) is None:
mth = _missing_impl(interface_type.__name__, method_name)
else:
mth = _found_impl(interface_type.__name__,
method_name,
found_method,
(len(implemented_leaf_interfaces) - i) * _ptr_size)
implemented_methods[method_name] = (mth, i)
wrappers.append(method_type(mth))
vtbl = interface_type._vtbl_struct_type(*wrappers)
_vtbl_pointers.append(ctypes.pointer(vtbl))
fields = []
for i, itf in enumerate(implemented_leaf_interfaces):
fields.append((f'vtbl_ptr_{i}', ctypes.POINTER(itf._vtbl_struct_type)))
fields.append(('self_', ctypes.py_object))
cls._interface_to_vtbl_offset = _interface_to_vtbl_offset
cls._vtbl_pointers = _vtbl_pointers
cls._struct_type = _StructMeta(f"{cls.__name__}_Struct", (ctypes.Structure,), {'_fields_': fields})
def __init__(self):
self._vrefcount = 1
self._struct = self._struct_type(*self._vtbl_pointers, ctypes.py_object(self))
def as_interface(self, interface_type):
# This method ignores the QueryInterface mechanism completely; no GUIDs are
# associated with Interfaces on the python side, it can't be supported.
# Still works, as so far none of the python-made COMObjects are expected to
# support it by any C code.
# (Also no need to always implement it, some COMObjects do not inherit from IUnknown.)
if (offset := self._interface_to_vtbl_offset.get(interface_type, None)) is None:
raise TypeError(f"Does not implement {interface_type}")
return ctypes.byref(self._struct, offset)

View File

@ -83,15 +83,15 @@ IID_IMMDeviceEnumerator = com.GUID(0xa95664d2, 0x9614, 0x4f35, 0xa7, 0x46, 0xde,
class IMMNotificationClient(com.IUnknown):
_methods_ = [
('OnDeviceStateChanged',
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR, DWORD)),
com.STDMETHOD(LPCWSTR, DWORD)),
('OnDeviceAdded',
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR)),
com.STDMETHOD(LPCWSTR)),
('OnDeviceRemoved',
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR)),
com.STDMETHOD(LPCWSTR)),
('OnDefaultDeviceChanged',
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, EDataFlow, ERole, LPCWSTR)),
com.STDMETHOD(EDataFlow, ERole, LPCWSTR)),
('OnPropertyValueChanged',
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR, PROPERTYKEY)),
com.STDMETHOD(LPCWSTR, PROPERTYKEY)),
]
@ -113,7 +113,7 @@ class AudioNotificationCB(com.COMObject):
self.audio_devices = audio_devices
self._lost = False
def OnDeviceStateChanged(self, this, pwstrDeviceId, dwNewState):
def OnDeviceStateChanged(self, pwstrDeviceId, dwNewState):
device = self.audio_devices.get_cached_device(pwstrDeviceId)
old_state = device.state
@ -126,17 +126,17 @@ class AudioNotificationCB(com.COMObject):
device.state = dwNewState
self.audio_devices.dispatch_event('on_device_state_changed', device, pyglet_old_state, pyglet_new_state)
def OnDeviceAdded(self, this, pwstrDeviceId):
def OnDeviceAdded(self, pwstrDeviceId):
dev = self.audio_devices.add_device(pwstrDeviceId)
assert _debug(f"Audio device was added {pwstrDeviceId}: {dev}")
self.audio_devices.dispatch_event('on_device_added', dev)
def OnDeviceRemoved(self, this, pwstrDeviceId):
def OnDeviceRemoved(self, pwstrDeviceId):
dev = self.audio_devices.remove_device(pwstrDeviceId)
assert _debug(f"Audio device was removed {pwstrDeviceId} : {dev}")
self.audio_devices.dispatch_event('on_device_removed', dev)
def OnDefaultDeviceChanged(self, this, flow, role, pwstrDeviceId):
def OnDefaultDeviceChanged(self, flow, role, pwstrDeviceId):
# Only support eConsole role right now
if role == 0:
if pwstrDeviceId is None:
@ -149,7 +149,7 @@ class AudioNotificationCB(com.COMObject):
self.audio_devices.dispatch_event('on_default_changed', device, pyglet_flow)
def OnPropertyValueChanged(self, this, pwstrDeviceId, key):
def OnPropertyValueChanged(self, pwstrDeviceId, key):
pass

View File

@ -270,7 +270,6 @@ class IDirectSound(com.pIUnknown):
('Initialize',
com.STDMETHOD(com.LPGUID)),
]
_type_ = com.COMInterface
DirectSoundCreate = lib.DirectSoundCreate
DirectSoundCreate.argtypes = \

View File

@ -192,17 +192,19 @@ XAUDIO2_NO_VIRTUAL_AUDIO_CLIENT = 0x10000 # Used in CreateMasteringVoice to cr
class IXAudio2VoiceCallback(com.Interface):
_methods_ = [
('OnVoiceProcessingPassStart',
com.STDMETHOD(UINT32)),
com.VOIDMETHOD(UINT32)),
('OnVoiceProcessingPassEnd',
com.STDMETHOD()),
('onStreamEnd',
com.STDMETHOD()),
('onBufferStart',
com.STDMETHOD(ctypes.c_void_p)),
com.VOIDMETHOD()),
('OnStreamEnd',
com.VOIDMETHOD()),
('OnBufferStart',
com.VOIDMETHOD(ctypes.c_void_p)),
('OnBufferEnd',
com.STDMETHOD(ctypes.c_void_p)),
com.VOIDMETHOD(ctypes.c_void_p)),
('OnLoopEnd',
com.STDMETHOD(ctypes.c_void_p)),
com.VOIDMETHOD(ctypes.c_void_p)),
('OnVoiceError',
com.VOIDMETHOD(ctypes.c_void_p, HRESULT))
]
@ -220,20 +222,9 @@ class XA2SourceCallback(com.COMObject):
_interfaces_ = [IXAudio2VoiceCallback]
def __init__(self, xa2_player):
super().__init__()
self.xa2_player = xa2_player
def OnVoiceProcessingPassStart(self, bytesRequired):
pass
def OnVoiceProcessingPassEnd(self):
pass
def onStreamEnd(self):
pass
def onBufferStart(self, pBufferContext):
pass
def OnBufferEnd(self, pBufferContext):
"""At the end of playing one buffer, attempt to refill again.
Even if the player is out of sources, it needs to be called to purge all buffers.
@ -241,10 +232,7 @@ class XA2SourceCallback(com.COMObject):
if self.xa2_player:
self.xa2_player.refill_source_player()
def OnLoopEnd(self, this, pBufferContext):
pass
def onVoiceError(self, this, pBufferContext, hresult):
def OnVoiceError(self, pBufferContext, hresult):
raise Exception("Error occurred during audio playback.", hresult)
@ -362,24 +350,18 @@ class IXAudio2MasteringVoice(IXAudio2Voice):
class IXAudio2EngineCallback(com.Interface):
_methods_ = [
('OnProcessingPassStart',
com.METHOD(ctypes.c_void_p)),
com.VOIDMETHOD()),
('OnProcessingPassEnd',
com.METHOD(ctypes.c_void_p)),
com.VOIDMETHOD()),
('OnCriticalError',
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_ulong)),
com.VOIDMETHOD(HRESULT)),
]
class XA2EngineCallback(com.COMObject):
_interfaces_ = [IXAudio2EngineCallback]
def OnProcessingPassStart(self):
pass
def OnProcessingPassEnd(self):
pass
def OnCriticalError(self, this, hresult):
def OnCriticalError(self, hresult):
raise Exception("Critical Error:", hresult)

View File

@ -98,7 +98,7 @@ class Caret:
colors = r, g, b, self._visible_alpha, r, g, b, self._visible_alpha
self._list = self._group.program.vertex_list(2, gl.GL_LINES, batch, self._group, colors=('Bn', colors))
self._list = self._group.program.vertex_list(2, gl.GL_LINES, self._batch, self._group, colors=('Bn', colors))
self._ideal_x = None
self._ideal_line = None
self._next_attributes = {}
@ -127,6 +127,7 @@ class Caret:
Also disconnects the caret from further layout events.
"""
clock.unschedule(self._blink)
self._list.delete()
self._layout.remove_handlers(self)

View File

@ -6,6 +6,7 @@
import time
from string import Template
from typing import List
from lib_not_dr.types.options import Options
@ -51,11 +52,12 @@ class TimeFormatter(BaseFormatter):
name = 'TimeFormatter'
time_format: str = '%Y-%m-%d %H:%M:%S'
msec_time_format: str = '{}-{:03d}'
@classmethod
def _info(cls) -> str:
return cls.add_info('log_time', 'when the log message was created', 'The time format string'
'. See https://docs.python.org/3/library/time.html#time.strftime for more information.')
return cls.add_info('log_time', 'formatted time when logging', 'The time format string'
'. See https://docs.python.org/3/library/time.html#time.strftime for more information.')
def format(self, message: LogMessage) -> str:
return f'[{message.log_time}]'

View File

@ -2,6 +2,9 @@
# DR basic running from source
# DR build (by nuitka)
# for function
lib-not-dr
# for images
# not for pypy >= 3.10
pillow >= 10.0.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"

View File

@ -3,6 +3,9 @@
# DR build (by nuitka)
# DR contributing
# for function
lib-not-dr
# for images
# not for pypy >= 3.10
pillow >= 10.0.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"

View File

@ -1,6 +1,9 @@
# this requirement is for
# DR basic running from source
# for function
lib-not-dr
# for images
# not for pypy >= 3.10
pillow >= 10.0.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"