with more logger

Add | more formatter and some more

Fix | type mis match

sync pyglet

Enhance | logger with Template

add lib-not-dr as requirement

sync pyglet

sync pyglet

Add | add lto=yes to nuitka_build

just incase

sync pyglet

sync lib_not_dr

Remove | external requirement lib-not-dr

some logger

sync lib-not-dr

sync pyglet

sync lib-not-dr

sync lib-not-dr

sync pyglet

sync pyglet

Fix | console thread been block

Update DR rs and DR sdk

sync lib not dr

sync lib-not-dr

sync lib-not-dr

sync pyglet and lib-not-dr

sync pyglet 0.1.8

sync lib not dr

logger almost done?

almost!

sync pyglet (clicpboard support!)

sync lib not dr

sync lib not dr

color code and sync pyglet

do not show memory and progress building localy

sync pyglet

synclibs
This commit is contained in:
shenjack 2023-10-14 15:36:43 +08:00
parent 0e0c7ef700
commit d84b490b99
Signed by: shenjack
GPG Key ID: 7B1134A979775551
68 changed files with 2832 additions and 818 deletions

5
DR.py
View File

@ -52,8 +52,9 @@ def start(start_time_ns: int) -> None:
print(crash.all_process)
for a_thread in threading.enumerate():
print(a_thread)
if a_thread.is_alive() and a_thread != threading.current_thread() and a_thread != threading.main_thread():
a_thread.join(2) # wait for 2 sec
if a_thread.is_alive() and not a_thread.daemon:
if a_thread != threading.current_thread() and a_thread != threading.main_thread():
a_thread.join(2) # wait for 2 sec
import pyglet
pyglet.app.exit() # make sure that pyglet has stopped

View File

@ -7,8 +7,8 @@ fonts_folder = "assets/fonts"
[window]
style = "None"
width = 1112
height = 793
width = 1312
height = 915
visible = true
gui_scale = 1
caption = "Difficult Rocket v{DR_version}"

View File

@ -1,9 +1,17 @@
# DR game/DR rs 更新日志
- 最新版本号
- DR game: 0.3.3.0
- DR rs: 0.2.22.0
- DR rs: 0.2.23.0
## 20231101 DR rs 0.2.23.0
### Dependency
- Update `DR rs` dependency
- `quick-xml`: `0.30.0` -> `0.31.0`
- `serde`: `1.0.186` -> `1.0.190`
- `xml-rs`: `0.8.16` -> `0.8.19`
## 20230825 DR rs 0.2.22.0

View File

@ -1,4 +1,3 @@
# DR SDK 更新日志
- 最新版本号
@ -14,6 +13,20 @@
- 不再同时维护两份代码
- No longer maintain two sets of code at the same time
### Fix
- 如果没有 DR_game 的情况下, 退出时会 join 控制台线程
- 通过检测线程是否是守护线程来判断是否 join
- If there is no DR_game, join the console thread when exiting
- Determine whether to join by detecting whether the thread is a daemon thread
### Dependency
- 更新了所有的依赖版本号
- 去看 `requirements.txt`
- Updated all dependency version numbers
- Go see `requirements.txt`
## DR sdk 0.8.7.2
### Add

View File

@ -4,4 +4,4 @@
# All rights reserved
# -------------------------------
__version__ = '0.1.7'
__version__ = '0.1.8'

View File

@ -0,0 +1,33 @@
# -------------------------------
# Difficult Rocket
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
# All rights reserved
# -------------------------------
import sys
from lib_not_dr.types.options import Options
COLOR_SUPPORT = True
if sys.platform == "win32":
try:
# https://stackoverflow.com/questions/36760127/...
# how-to-use-the-new-support-for-ansi-escape-sequences-in-the-windows-10-console
from ctypes import windll
kernel32 = windll.kernel32
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
except OSError: # pragma: no cover
COLOR_SUPPORT = False
class LogLevel(Options):
name = 'LogLevel'
notset: int = 0
trace: int = 5
fine: int = 7
debug: int = 10
info: int = 20
warn: int = 30
error: int = 40
fatal: int = 50

View File

@ -0,0 +1,285 @@
# -------------------------------
# Difficult Rocket
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
# All rights reserved
# -------------------------------
import time
from pathlib import Path
from string import Template
from typing import List, Union, Optional, Dict, Tuple, TYPE_CHECKING
from lib_not_dr.logger import LogLevel
from lib_not_dr.types.options import Options
from lib_not_dr.logger.structure import LogMessage, FormattingMessage
if TYPE_CHECKING:
from lib_not_dr.logger.formatter.colors import BaseColorFormatter
class BaseFormatter(Options):
name = 'BaseFormatter'
sub_formatter: List['BaseFormatter'] = []
color_formatters: List['BaseColorFormatter'] = []
default_template: str = '[${log_time}][${level}]|${logger_name}:${logger_tag}|${messages}'
@classmethod
def add_info(cls, match: str, to: str, description: str) -> str:
return f'- {to} -> ${{{match}}} : {description}'
@classmethod
def info(cls) -> str:
infos = {BaseFormatter.name: BaseFormatter._info()}
cache = ''
for formatter in cls.sub_formatter:
infos[formatter.name] = formatter._info()
infos[cls.name] = cls._info()
for name, info in infos.items():
cache += f"## {name}\n"
cache += info
cache += '\n'
return cache
@classmethod
def _info(cls) -> str:
info = cls.add_info('logger_name', 'logger name', 'The name of the logger')
info += '\n'
info += cls.add_info('logger_tag', 'logger tag', 'The tag of the logger')
return info
def format_message(self,
message: LogMessage,
template: Optional[Union[Template, str]] = None) -> str:
"""
Format message
:param message: 输入的消息
:param template: 日志输出模板
:return:
"""
basic_info = message.format_for_message()
message, info = self._format((message, basic_info))
if template is None:
template = Template(self.default_template)
elif isinstance(template, str):
template = Template(template)
try:
return template.substitute(**info)
except (KeyError, ValueError):
return template.safe_substitute(**info)
def _format(self, message: FormattingMessage) -> FormattingMessage:
"""
Format message
:param message:
:return:
"""
for formatter in self.sub_formatter:
message = formatter._format(message)
return message
@property
def template(self) -> str:
return self.default_template
@template.setter
def template(self, template: str) -> None:
if not isinstance(template, str):
raise TypeError(f'The template must be str, not {type(template)}')
self.default_template = template
class LevelFormatter(BaseFormatter):
name = 'LevelFormatter'
default_level: int = 20
# If True, the undefined level will be set to the higher nearest level.
level_get_higher: bool = True
level_name_map = {
LogLevel.notset: 'NOTSET',
LogLevel.trace: ' TRACE',
LogLevel.fine: ' FINE ',
LogLevel.debug: ' DEBUG',
LogLevel.info: ' INFO ',
LogLevel.warn: ' WARN ',
LogLevel.error: 'ERROR ',
LogLevel.fatal: 'FATAL ',
}
name_level_map = {
'NOTSET': LogLevel.notset,
' TRACE': LogLevel.trace,
' FINE ': LogLevel.fine,
' DEBUG': LogLevel.debug,
' INFO ': LogLevel.info,
' WARN ': LogLevel.warn,
'ERROR ': LogLevel.error,
'FATAL ': LogLevel.fatal,
}
@classmethod
def _info(cls) -> str:
return cls.add_info('level', 'log level', 'The log level')
def _format(self, message: FormattingMessage) -> FormattingMessage:
if message[0].level in self.name_level_map:
level_tag = self.level_name_map[message[0].level]
else:
if self.level_get_higher:
for level in self.name_level_map:
if message[0].level <= self.name_level_map[level]:
level_tag = level
break
else:
level_tag = 'FATAL'
else:
for level in self.name_level_map:
if message[0].level >= self.name_level_map[level]:
level_tag = level
break
else:
level_tag = 'NOTSET'
message[1]['level'] = level_tag
return message
class TraceFormatter(BaseFormatter):
name = 'TraceFormatter'
time_format: str = '%Y-%m-%d %H:%M:%S'
msec_time_format: str = '{}-{:03d}'
use_absolute_path: bool = False
@classmethod
def _info(cls) -> str:
info = 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.')
info += '\n'
info += cls.add_info('log_source', 'logging file', 'the logging file name')
info += '\n'
info += cls.add_info('log_line', 'logging line', 'the logging line number')
info += '\n'
info += cls.add_info('log_function', 'logging function', 'the logging function name')
return info
def _format(self, message: FormattingMessage) -> FormattingMessage:
message = self._time_format(message)
message = self._trace_format(message)
return message
def _time_format(self, message: FormattingMessage) -> FormattingMessage:
time_mark = time.localtime(message[0].log_time / 1000000000)
if self.msec_time_format:
time_mark = self.msec_time_format.format(time.strftime(self.time_format, time_mark),
message[0].create_msec_3)
message[1]['log_time'] = time_mark
return message
def _trace_format(self, message: FormattingMessage) -> FormattingMessage:
if message[0].stack_trace is None:
return message
path = Path(message[0].stack_trace.f_code.co_filename)
if self.use_absolute_path:
message[1]['log_source'] = path.absolute()
message[1]['log_source'] = path
message[1]['log_line'] = message[0].stack_trace.f_lineno
message[1]['log_function'] = message[0].stack_trace.f_code.co_name
return message
class StdFormatter(BaseFormatter):
name = 'StdFormatter'
enable_color: bool = True
sub_formatter: List[BaseFormatter] = [LevelFormatter(),
TraceFormatter()]
from lib_not_dr.logger.formatter.colors import (LevelColorFormatter,
LoggerColorFormatter,
TimeColorFormatter,
TraceColorFormatter,
MessageColorFormatter)
color_formatters: List[BaseFormatter] = [LevelColorFormatter(),
LoggerColorFormatter(),
TimeColorFormatter(),
TraceColorFormatter(),
MessageColorFormatter()]
def __init__(self,
enable_color: bool = True,
sub_formatter: Optional[List[BaseFormatter]] = None,
color_formatters: Optional[List[BaseFormatter]] = None,
**kwargs) -> None:
"""
Initialize the StdFormatter
:param enable_color: enable color
:param sub_formatter: list of sub formatter
:param color_formatters: list of color formatter
:param kwargs: other options
"""
# 同 structures.LogMessage.__init__ 的注释 (逃)
self.enable_color = enable_color
if sub_formatter is not None:
self.sub_formatter = sub_formatter
if color_formatters is not None:
self.color_formatters = color_formatters
super().__init__(**kwargs)
def _format(self, message: FormattingMessage) -> FormattingMessage:
super()._format(message)
if not self.enable_color:
return message
for formatter in self.color_formatters:
message = formatter._format(message)
return message
@classmethod
def _info(cls) -> str:
return 'None'
if __name__ == '__main__':
import inspect
log_message = LogMessage(messages=['Hello World!'],
level=7,
stack_trace=inspect.currentframe(),
logger_tag='tester',
logger_name='test')
print(LevelFormatter.info())
print(LevelFormatter().format_message(log_message))
print(TraceFormatter.info())
print(TraceFormatter().format_message(log_message))
print(StdFormatter.info())
print(StdFormatter().format_message(log_message))
std_format = StdFormatter()
std_format.default_template = "${log_time}|${logger_name}|${logger_tag}|${log_source}:${log_line}|${log_function}|${level}|${messages}"
test_levels = (0, 5, 7, 10, 20, 30, 40, 50)
print("with color")
for test_level in test_levels:
log_message.level = test_level
print(std_format.format_message(log_message), end='')
print("without color")
std_format.enable_color = False
for test_level in test_levels:
log_message.level = test_level
print(std_format.format_message(log_message), end='')
print(std_format.as_markdown())

View File

@ -0,0 +1,256 @@
# -------------------------------
# Difficult Rocket
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
# All rights reserved
# -------------------------------
from lib_not_dr.logger import LogLevel, COLOR_SUPPORT
from lib_not_dr.logger.formatter import BaseFormatter
from lib_not_dr.logger.structure import FormattingMessage
__all__ = [
'BaseColorFormatter',
'LevelColorFormatter',
'LoggerColorFormatter',
'TimeColorFormatter',
'TraceColorFormatter',
'MessageColorFormatter',
'RESET_COLOR'
]
RESET_COLOR = '\033[0m'
class BaseColorFormatter(BaseFormatter):
name = 'BaseColorFormatter'
# TODO 迁移老 logger 颜色
color = {
# Notset: just black
LogLevel.notset: '',
# Trace: blue
LogLevel.trace: '\033[38;2;138;173;244m',
# Fine: green
LogLevel.fine: '\033[0;32m',
# Debug: cyan
LogLevel.debug: '\033[0;36m',
# Info: white
LogLevel.info: '\033[0;37m',
# Warn: yellow
LogLevel.warn: '\033[0;33m',
# Error: red
LogLevel.error: '\033[0;31m',
# Fatal: red background
LogLevel.fatal: '\033[0;41m'
}
def get_color(self, message: FormattingMessage) -> str:
for level in self.color:
if message[0].level <= level:
break
else:
level = 90
return self.color[level]
class LevelColorFormatter(BaseColorFormatter):
name = 'LevelColorFormatter'
# TODO 迁移老 logger 颜色
color = {
# Notset: just black
LogLevel.notset: '',
# Trace: blue
LogLevel.trace: '\033[38;2;138;173;244m',
# Fine: green
LogLevel.fine: '\033[35;48;2;44;44;54m',
# Debug: cyan
LogLevel.debug: '\033[38;2;133;138;149m',
# Info: white
LogLevel.info: '\033[0;37m',
# Warn: yellow
LogLevel.warn: '\033[0;33m',
# Error: red
LogLevel.error: '\033[0;31m',
# Fatal: red background
LogLevel.fatal: '\033[0;41m'
}
@classmethod
def _info(cls) -> str:
return cls.add_info('colored level', 'level', 'A colored level')
def _format(self, message: FormattingMessage) -> FormattingMessage:
if isinstance(message[1].get('level'), int) or not COLOR_SUPPORT:
return message
# 获取颜色
color = self.get_color(message)
# 添加颜色
if color == '' or color == RESET_COLOR:
return message
message[1]['level'] = f'{color}{message[1]["level"]}{RESET_COLOR}'
return message
class LoggerColorFormatter(BaseColorFormatter):
name = 'LoggerColorFormatter'
# TODO 迁移老 logger 颜色
color = {
# Notset: just black
LogLevel.notset: '',
# Trace: blue
LogLevel.trace: '\033[38;2;138;173;244m',
# Fine: green
LogLevel.fine: '\033[0;32m',
# Debug: cyan
LogLevel.debug: '\033[0;36m',
# Info: white
LogLevel.info: '\033[0;37m',
# Warn: yellow
LogLevel.warn: '\033[0;33m',
# Error: red
LogLevel.error: '\033[0;31m',
# Fatal: red background
LogLevel.fatal: '\033[38;2;245;189;230m',
}
@classmethod
def _info(cls) -> str:
return cls.add_info('colored logger name', 'logger name', 'A colored logger name')
def _format(self, message: FormattingMessage) -> FormattingMessage:
if message[1].get('logger_name') is None or not COLOR_SUPPORT:
return message
# 获取颜色
color = self.get_color(message)
# 添加颜色
if color == '' or color == RESET_COLOR:
return message
message[1]['logger_name'] = f'{color}{message[1]["logger_name"]}{RESET_COLOR}'
if message[1].get('logger_tag') is not None and message[1].get('logger_tag') != ' ':
message[1]['logger_tag'] = f'{color}{message[1]["logger_tag"]}{RESET_COLOR}'
return message
class TimeColorFormatter(BaseColorFormatter):
name = 'TimeColorFormatter'
# TODO 迁移老 logger 颜色
color = {
# Notset: just black
LogLevel.notset: '',
# Trace: blue
LogLevel.trace: '\033[38;2;138;173;244m',
# Fine: green
LogLevel.fine: '\033[0;32m',
# Debug: cyan
LogLevel.debug: '\033[0;36m',
# Info: white
LogLevel.info: '\033[0;37m',
# Warn: yellow
LogLevel.warn: '\033[0;33m',
# Error: red
LogLevel.error: '\033[0;31m',
# Fatal: red background
LogLevel.fatal: '\033[38;2;255;255;0;48;2;120;10;10m',
}
@classmethod
def _info(cls) -> str:
return cls.add_info('colored time', 'time', 'A colored time')
def _format(self, message: FormattingMessage) -> FormattingMessage:
if message[1].get('log_time') is None or not COLOR_SUPPORT:
return message
# 获取颜色
color = self.get_color(message)
# 添加颜色
if color == '' or color == RESET_COLOR:
return message
message[1]['log_time'] = f'{color}{message[1]["log_time"]}{RESET_COLOR}'
return message
class TraceColorFormatter(BaseColorFormatter):
name = 'TraceColorFormatter'
# TODO 迁移老 logger 颜色
color = {
# Notset: just black
LogLevel.notset: '\033[38;2;0;255;180m',
# Trace: blue
LogLevel.trace: '\033[38;2;0;255;180m',
# Fine: green
LogLevel.fine: '\033[38;2;0;255;180m',
# Debug: cyan
LogLevel.debug: '\033[38;2;0;255;180m',
# Info: white
LogLevel.info: '\033[38;2;0;255;180m',
# Warn: yellow
LogLevel.warn: '\033[38;2;0;255;180m',
# Error: red
LogLevel.error: '\033[38;2;0;255;180m',
# Fatal: red background
LogLevel.fatal: '\033[38;2;255;255;0;48;2;120;10;10m',
}
@classmethod
def _info(cls) -> str:
info = cls.add_info('colored logging file', 'log_source', 'A colored logging file name')
info += '\n'
info += cls.add_info('colored logging line', 'log_line', 'A colored logging line number')
info += '\n'
info += cls.add_info('colored logging function', 'log_function', 'A colored logging function name')
return info
def _format(self, message: FormattingMessage) -> FormattingMessage:
if message[0].stack_trace is None or not COLOR_SUPPORT:
return message
# 获取颜色
color = self.get_color(message)
# 添加颜色
if color == '' or color == RESET_COLOR:
return message
message[1]['log_source'] = f'{color}{message[1]["log_source"]}{RESET_COLOR}'
message[1]['log_line'] = f'{color}{message[1]["log_line"]}{RESET_COLOR}'
message[1]['log_function'] = f'{color}{message[1]["log_function"]}{RESET_COLOR}'
return message
class MessageColorFormatter(BaseColorFormatter):
name = 'MessageColorFormatter'
color = {
# Notset: just black
LogLevel.notset: '',
# Trace: blue
LogLevel.trace: '\033[38;2;138;173;244m',
# Fine: blue
LogLevel.fine: '\033[38;2;138;173;244m',
# Debug: blue
LogLevel.debug: '\033[38;2;138;173;244m',
# Info: no color
LogLevel.info: '',
# Warn: yellow
LogLevel.warn: '\033[0;33m',
# Error: red
LogLevel.error: '\033[0;31m',
# Fatal: red background
LogLevel.fatal: '\033[38;2;255;255;0;48;2;120;10;10m',
}
@classmethod
def _info(cls) -> str:
return cls.add_info('colored message', 'message', 'A colored message')
def _format(self, message: FormattingMessage) -> FormattingMessage:
if message[1].get('messages') is None or not COLOR_SUPPORT:
return message
# 获取颜色
color = self.get_color(message)
# 添加颜色
if color == '' or color == RESET_COLOR:
return message
if message[1]['messages'][-1] == '\n':
message[1]['messages'] = f'{color}{message[1]["messages"][:-1]}{RESET_COLOR}\n'
else:
message[1]['messages'] = f'{color}{message[1]["messages"]}{RESET_COLOR}'
return message

View File

@ -0,0 +1,267 @@
# -------------------------------
# Difficult Rocket
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
# All rights reserved
# -------------------------------
import time
import inspect
from types import FrameType
from typing import List, Optional
from lib_not_dr.logger import LogLevel
from lib_not_dr.types.options import Options
from lib_not_dr.logger.structure import LogMessage
from lib_not_dr.logger.outstream import BaseOutputStream, StdioOutputStream
class Logger(Options):
name = 'Logger-v2'
outputs: List[BaseOutputStream] = [StdioOutputStream()]
log_name: str = 'root'
enable: bool = True
level: int = 20 # info
def log_for(self, level: int) -> bool:
"""
Check if logging is enabled for a specific level.
Args:
level (int): The logging level to check.
Returns:
bool: True if logging is enabled for the given level, False otherwise.
"""
return self.enable and level >= self.level
def add_output(self, output: BaseOutputStream) -> None:
"""
Add an output to the list of outputs.
Args:
output (BaseOutputStream): The output to be added.
Returns:
None
"""
self.outputs.append(output)
self.level = min(self.level, output.level)
def remove_output(self, output: BaseOutputStream) -> None:
"""
Removes the specified output from the list of outputs.
Args:
output (BaseOutputStream): The output to be removed.
Returns:
None
"""
self.outputs.remove(output)
self.level = max(self.level, *[output.level for output in self.outputs])
@property
def global_level(self) -> int:
"""
Get the global logging level.
Returns:
int: The global logging level.
"""
return self.level
@global_level.setter
def global_level(self, level: int) -> None:
"""
Set the global logging level.
Args:
level (int): The global logging level.
Returns:
None
"""
self.level = level
for output in self.outputs:
output.level = level
def make_log(self,
messages: List[str],
tag: Optional[str] = None,
end: str = '\n',
split: str = ' ',
flush: bool = True,
level: int = 20, # info
# log_time: Optional[float] = None,
# logger_name: str = 'root',
# logger_tag: Optional[str] = None,
stack_trace: Optional[FrameType] = None) -> None:
# 检查是否需要记录
if not self.log_for(level):
return
log_time = time.time_ns()
# 处理堆栈信息
if stack_trace is None:
# 尝试获取堆栈信息
if (stack := inspect.currentframe()) is not None:
# 如果可能 尝试获取上两层的堆栈信息
if (up_stack := stack.f_back) is not None:
if (upper_stack := up_stack.f_back) is not None:
stack_trace = upper_stack
else:
stack_trace = up_stack
else:
stack_trace = stack
message = LogMessage(messages=messages,
end=end,
split=split,
flush=flush,
level=level,
log_time=log_time,
logger_name=self.log_name,
logger_tag=tag,
stack_trace=stack_trace)
if level >= 30: # WARN
for output in self.outputs:
output.write_stderr(message)
else:
for output in self.outputs:
output.write_stdout(message)
# done?
# 20231106 00:06
@staticmethod
def get_logger_by_name(name: str) -> 'Logger':
"""
Get a logger by name.
Args:
name (str): The name of the logger.
Returns:
Logger: The logger with the specified name.
"""
return Logger(log_name=name)
def info(self,
*message,
tag: Optional[str] = None,
end: str = '\n',
split: str = ' ',
flush: bool = True,
stack_trace: Optional[FrameType] = None) -> None:
if not self.log_for(LogLevel.info):
return
self.make_log(messages=list(message),
tag=tag,
end=end,
split=split,
flush=flush,
level=LogLevel.info,
stack_trace=stack_trace)
def trace(self,
*message,
tag: Optional[str] = None,
end: str = '\n',
split: str = ' ',
flush: bool = True,
stack_trace: Optional[FrameType] = None) -> None:
if not self.log_for(LogLevel.trace):
return
self.make_log(messages=list(message),
tag=tag,
end=end,
split=split,
flush=flush,
level=LogLevel.trace,
stack_trace=stack_trace)
def fine(self,
*message,
tag: Optional[str] = None,
end: str = '\n',
split: str = ' ',
flush: bool = True,
stack_trace: Optional[FrameType] = None) -> None:
if not self.log_for(LogLevel.fine):
return
self.make_log(messages=list(message),
tag=tag,
end=end,
split=split,
flush=flush,
level=LogLevel.fine,
stack_trace=stack_trace)
def debug(self,
*message,
tag: Optional[str] = None,
end: str = '\n',
split: str = ' ',
flush: bool = True,
stack_trace: Optional[FrameType] = None) -> None:
if not self.log_for(LogLevel.debug):
return
self.make_log(messages=list(message),
tag=tag,
end=end,
split=split,
flush=flush,
level=LogLevel.debug,
stack_trace=stack_trace)
def warn(self,
*message,
tag: Optional[str] = None,
end: str = '\n',
split: str = ' ',
flush: bool = True,
stack_trace: Optional[FrameType] = None) -> None:
if not self.log_for(LogLevel.warn):
return
self.make_log(messages=list(message),
tag=tag,
end=end,
split=split,
flush=flush,
level=LogLevel.warn,
stack_trace=stack_trace)
def error(self,
*message,
tag: Optional[str] = None,
end: str = '\n',
split: str = ' ',
flush: bool = True,
stack_trace: Optional[FrameType] = None) -> None:
if not self.log_for(LogLevel.error):
return
self.make_log(messages=list(message),
tag=tag,
end=end,
split=split,
flush=flush,
level=LogLevel.error,
stack_trace=stack_trace)
def fatal(self,
*message,
tag: Optional[str] = None,
end: str = '\n',
split: str = ' ',
flush: bool = True,
stack_trace: Optional[FrameType] = None) -> None:
if not self.log_for(LogLevel.fatal):
return
self.make_log(messages=list(message),
tag=tag,
end=end,
split=split,
flush=flush,
level=LogLevel.fatal,
stack_trace=stack_trace)

View File

@ -0,0 +1,236 @@
# -------------------------------
# Difficult Rocket
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
# All rights reserved
# -------------------------------
import io
import sys
import time
import string
import atexit
import threading
from pathlib import Path
from typing import Optional
from lib_not_dr.logger import LogLevel
from lib_not_dr.types.options import Options
from lib_not_dr.logger.structure import LogMessage
from lib_not_dr.logger.formatter import BaseFormatter, StdFormatter
__all__ = [
'BaseOutputStream',
'StdioOutputStream',
'FileCacheOutputStream'
]
class BaseOutputStream(Options):
name = 'BaseOutputStream'
level: int = LogLevel.info
enable: bool = True
formatter: BaseFormatter
def write_stdout(self, message: LogMessage) -> None:
raise NotImplementedError(f'{self.__class__.__name__}.write_stdout is not implemented')
def write_stderr(self, message: LogMessage) -> None:
raise NotImplementedError(f'{self.__class__.__name__}.write_stderr is not implemented')
def flush(self) -> None:
raise NotImplementedError(f'{self.__class__.__name__}.flush is not implemented')
def close(self) -> None:
self.enable = False
class StdioOutputStream(BaseOutputStream):
name = 'StdioOutputStream'
level: int = LogLevel.info
formatter: BaseFormatter = StdFormatter()
use_stderr: bool = True
def write_stdout(self, message: LogMessage) -> None:
if not self.enable:
return None
if message.level < self.level:
return None
print(self.formatter.format_message(message), end='', flush=message.flush)
return None
def write_stderr(self, message: LogMessage) -> None:
if not self.enable:
return None
if message.level < self.level:
return None
if self.use_stderr:
print(self.formatter.format_message(message), end='', flush=message.flush, file=sys.stderr)
else:
print(self.formatter.format_message(message), end='', flush=message.flush)
return None
def flush(self) -> None:
"""
flush stdout and stderr
:return: None
"""
print('', end='', flush=True)
print('', end='', flush=True, file=sys.stderr)
return None
class FileCacheOutputStream(BaseOutputStream):
name = 'FileCacheOutputStream'
level: int = LogLevel.info
formatter: BaseFormatter = StdFormatter(enable_color=False)
text_cache: io.StringIO = None
flush_counter: int = 0
# 默认 10 次 flush 一次
flush_count_limit: int = 10
flush_time_limit: int = 10 # time limit in sec, 0 means no limit
flush_timer: threading.Timer = None
file_path: Optional[Path] = Path('./logs')
file_name: str
# file mode: always 'a'
file_encoding: str = 'utf-8'
# do file swap or not
file_swap: bool = False
at_exit_register: bool = False
file_swap_counter: int = 0
file_swap_name_template: str = '${name}-${counter}.log'
# ${name} -> file_name
# ${counter} -> file_swap_counter
# ${log_time} -> time when file swap ( round(time.time()) )
# ${start_time} -> start time of output stream ( round(time.time()) )
current_file_name: str = None
file_start_time: int = None
# log file swap triggers
# 0 -> no limit
file_size_limit: int = 0 # size limit in kb
file_time_limit: int = 0 # time limit in sec 0
file_swap_on_both: bool = False # swap file when both size and time limit reached
def init(self, **kwargs) -> bool:
self.file_start_time = round(time.time())
if self.text_cache is None:
self.text_cache = io.StringIO()
self.get_file_path()
return False
def _write(self, message: LogMessage) -> None:
"""
write message to text cache
默认已经检查过了
:param message: message to write
:return: None
"""
self.text_cache.write(self.formatter.format_message(message))
self.flush_counter += 1
if message.flush or self.flush_counter >= self.flush_count_limit:
self.flush()
else:
if self.flush_time_limit > 0:
if self.flush_timer is None or not self.flush_timer.is_alive():
self.flush_timer = threading.Timer(self.flush_time_limit, self.flush)
self.flush_timer.daemon = True
self.flush_timer.start()
if not self.at_exit_register:
atexit.register(self.flush)
self.at_exit_register = True
return None
def write_stdout(self, message: LogMessage) -> None:
if not self.enable:
return None
if message.level < self.level:
return None
self._write(message)
return None
def write_stderr(self, message: LogMessage) -> None:
if not self.enable:
return None
if message.level < self.level:
return None
self._write(message)
return None
def get_file_path(self) -> Path:
"""
get file path
:return:
"""
if (current_file := self.current_file_name) is None:
if not self.file_swap:
# 直接根据 file name 生成文件
current_file = Path(self.file_path) / self.file_name
self.current_file_name = str(current_file)
return current_file
template = string.Template(self.file_swap_name_template)
file_name = template.safe_substitute(name=self.file_name,
counter=self.file_swap_counter,
log_time=round(time.time()),
start_time=self.file_start_time)
current_file = Path(self.file_path) / file_name
self.current_file_name = str(current_file)
else:
current_file = Path(current_file)
return current_file
def check_flush(self) -> Path:
current_file = self.get_file_path()
# 获取当前文件的路径
if not self.file_swap:
# 不需要 swap 直接返回
return current_file
# 检查是否需要 swap
size_pass = True
if self.file_size_limit > 0:
file_size = current_file.stat().st_size / 1024 # kb
if file_size > self.file_size_limit: # kb
size_pass = False
time_pass = True
if self.file_time_limit > 0:
file_time = round(time.time()) - current_file.stat().st_mtime
if file_time > self.file_time_limit:
time_pass = False
if (self.file_swap_on_both and size_pass and time_pass) or \
(not self.file_swap_on_both and (size_pass or time_pass)):
# 两个都满足
# 或者只有一个满足
if size_pass and time_pass:
self.file_swap_counter += 1
# 生成新的文件名
return self.get_file_path()
def flush(self) -> None:
new_cache = io.StringIO() # 创建新的缓存
self.flush_counter = 0 # atomic, no lock
old_cache, self.text_cache = self.text_cache, new_cache
text = old_cache.getvalue()
old_cache.close() # 关闭旧的缓存
if text == '':
return None
current_file = self.check_flush()
if not current_file.exists():
current_file.parent.mkdir(parents=True, exist_ok=True)
current_file.touch(exist_ok=True)
with current_file.open('a', encoding=self.file_encoding) as f:
f.write(text)
return None
def close(self) -> None:
super().close()
self.flush()
self.text_cache.close()
atexit.unregister(self.flush)
return None

View File

@ -0,0 +1,99 @@
# -------------------------------
# Difficult Rocket
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
# All rights reserved
# -------------------------------
import time
from pathlib import Path
from types import FrameType
from typing import List, Optional, Tuple, Dict, Union
from lib_not_dr.types.options import Options
__all__ = ['LogMessage',
'FormattingMessage']
class LogMessage(Options):
name = 'LogMessage'
# 消息内容本身的属性
messages: List[str] = []
end: str = '\n'
split: str = ' '
# 消息的属性
flush: bool = None
level: int = 20
log_time: float = None # time.time_ns()
logger_name: str = 'root'
logger_tag: Optional[str] = None
stack_trace: Optional[FrameType] = None
def __init__(self,
messages: Optional[List[str]] = None,
end: Optional[str] = '\n',
split: Optional[str] = ' ',
flush: Optional[bool] = None,
level: Optional[int] = 20,
log_time: Optional[float] = None,
logger_name: Optional[str] = 'root',
logger_tag: Optional[str] = None,
stack_trace: Optional[FrameType] = None,
**kwargs) -> None:
"""
Init for LogMessage
:param messages: message list for log
:param end: end of message
:param split: split for messages
:param flush: do flush or not
:param level: level of message
:param log_time: time of message (default: time.time_ns())
:param logger_name: name of logger
:param logger_tag: tag of logger
:param stack_trace: stack trace of logger
:param kwargs: other options
"""
# 为了方便使用 单独覆盖了 __init__ 方法来提供代码补全的选项
super().__init__(messages=messages,
end=end,
split=split,
flush=flush,
level=level,
log_time=log_time,
logger_name=logger_name,
logger_tag=logger_tag,
stack_trace=stack_trace,
**kwargs)
def init(self, **kwargs) -> bool:
if self.log_time is None:
self.log_time = time.time_ns()
if not isinstance(self.flush, bool) and self.flush is not None:
self.flush = True if self.flush else False
return False
def format_message(self) -> str:
return self.split.join(self.messages) + self.end
def format_for_message(self) -> Dict[str, str]:
basic_info = self.option()
if self.logger_tag is None:
basic_info['logger_tag'] = ' '
basic_info['messages'] = self.format_message()
return basic_info
@property
def create_msec_3(self) -> int:
return int(self.log_time / 1000000) % 1000
FormattingMessage = Tuple[LogMessage, Dict[str, Union[str, Path]]]
if __name__ == '__main__':
print(LogMessage().as_markdown())

View File

@ -64,6 +64,7 @@ class Options:
"""
name = 'Option Base'
cached_options: Dict[str, Union[str, Any]] = {}
_check_options: bool = True
def __init__(self, **kwargs):
"""
@ -75,7 +76,7 @@ class Options:
self._options: Dict[str, Union[Callable, object]] = {}
self.flush_option()
for option, value in kwargs.items():
if option not in self.cached_options:
if option not in self.cached_options and self._check_options:
raise OptionNameNotDefined(f"option: {option} with value: {value} is not defined")
setattr(self, option, value)
run_load_file = True

View File

@ -1,12 +1,16 @@
"""Holds type aliases used throughout the codebase."""
import ctypes
import sys
from typing import Union
__all__ = [
"Buffer"
"Buffer",
]
# Backwards compatible placeholder for `collections.abc.Buffer` from Python 3.12
Buffer = Union[bytes, bytearray, memoryview, ctypes.Array]
if sys.version_info >= (3, 12):
from collections.abc import Buffer
else:
# Best-effort placeholder for older Python versions
Buffer = Union[bytes, bytearray, memoryview, ctypes.Array]

View File

@ -0,0 +1,6 @@
Experimental modules
====================
This package contains experimental modules, which are included here for
wider testing and feedback. Anything contined within may be broken, refactored,
or removed without notice.

View File

@ -1,6 +0,0 @@
"""Experimental modules.
This package contains experimental modules, included here for wider testing
and experimentation. Anything contined within may be broken, refactored, or
removed without notice.
"""

View File

@ -148,27 +148,15 @@ fragment_array_source = """#version 150 core
def get_default_shader():
try:
return pyglet.gl.current_context.pyglet_sprite_default_shader
except AttributeError:
vert_shader = graphics.shader.Shader(vertex_source, 'vertex')
geom_shader = graphics.shader.Shader(geometry_source, 'geometry')
frag_shader = graphics.shader.Shader(fragment_source, 'fragment')
default_shader_program = graphics.shader.ShaderProgram(vert_shader, geom_shader, frag_shader)
pyglet.gl.current_context.pyglet_sprite_default_shader = default_shader_program
return pyglet.gl.current_context.pyglet_sprite_default_shader
return pyglet.gl.current_context.create_program((vertex_source, 'vertex'),
(geometry_source, 'geometry'),
(fragment_source, 'fragment'))
def get_default_array_shader():
try:
return pyglet.gl.current_context.pyglet_sprite_default_array_shader
except AttributeError:
vert_shader = graphics.shader.Shader(vertex_source, 'vertex')
geom_shader = graphics.shader.Shader(geometry_source, 'geometry')
frag_shader = graphics.shader.Shader(fragment_array_source, 'fragment')
default_shader_program = graphics.shader.ShaderProgram(vert_shader, geom_shader, frag_shader)
pyglet.gl.current_context.pyglet_sprite_default_array_shader = default_shader_program
return pyglet.gl.current_context.pyglet_sprite_default_array_shader
return pyglet.gl.current_context.create_program((vertex_source, 'vertex'),
(geometry_source, 'geometry'),
(fragment_array_source, 'fragment'))
class SpriteGroup(graphics.Group):

View File

@ -0,0 +1,247 @@
"""Experimental networking
This module contains experiments in making user-friendly Server and Client
classes that integrate with pyglet's event system. These are very basic,
socket server/client examples, and are not ready to be used in production.
They are included here to solicit feedback, and possibly spark further
development. Basic Server usage::
server = net.Server(address='0.0.0.0', port=1234)
active_connections = weakref.WeakSet()
def pong(connection, message):
print(f"Received '{message}' from '{connection}'")
connection.send(b'pong')
@server.event
def on_connection(connection):
print(f"New client connected: {connection}")
connection.set_handler('on_receive', pong)
active_connections.add(connection)
@server.event
def on_disconnect(connection):
print(f"Client disconnected: {connection}")
active_connections.discard(connection)
Basic Client example::
client = net.Client(address='localhost', port=1234)
@client.event
def on_receive(client, message):
print(f"Received: {message}")
@client.event
def on_disconnect(client):
print(f"Disconnected: {client}")
client.send(b'ping')
"""
import queue as _queue
import struct as _struct
import socket as _socket
import asyncio as _asyncio
import threading as _threading
from pyglet.event import EventDispatcher as _EventDispatcher
from pyglet.util import debug_print
_debug_net = debug_print('debug_net')
class Client(_EventDispatcher):
def __init__(self, address, port):
"""Create a Client connection to a Server."""
self._socket = _socket.create_connection((address, port))
self._address = address
self._port = port
self._terminate = _threading.Event()
self._queue = _queue.Queue()
_threading.Thread(target=self._recv, daemon=True).start()
_threading.Thread(target=self._send, daemon=True).start()
self._sentinal = object() # poison pill
def close(self):
"""Close the connection."""
self._queue.put(self._sentinal)
self._socket.shutdown(1)
if not self._terminate.is_set():
self._terminate.set()
self.dispatch_event('on_disconnect', self)
def send(self, message):
"""Queue a message to send.
Put a string of bytes into the queue to send.
raises a `ConnectionError` if the connection
has been closed or dropped.
:Parameters:
`message` : bytes
A string of bytes to send.
"""
if self._terminate.is_set():
raise ConnectionError("Connection is closed.")
self._queue.put(message)
def _send(self): # Thread
"""Background Thread to send messages from the queue."""
while not self._terminate.is_set():
message = self._queue.get()
if message == self._sentinal: # bail out on poison pill
break
try:
# Attach a 4byte header to the front of the message:
packet = _struct.pack('I', len(message)) + message
self._socket.sendall(packet)
except (ConnectionError, OSError):
self.close()
break
assert _debug_net("Exiting _send thread")
def _recv(self): # Thread
socket = self._socket
while not self._terminate.is_set():
try:
header = socket.recv(4)
while len(header) < 4:
header += socket.recv(4 - len(header))
size = _struct.unpack('I', header)[0]
message = socket.recv(size)
while len(message) < size:
message += socket.recv(size)
self.dispatch_event('on_receive', self, message)
except (ConnectionError, OSError):
self.close()
break
assert _debug_net("Exiting _recv thread")
def on_receive(self, connection, message):
"""Event for received messages."""
def on_disconnect(self, connection):
"""Event for disconnection. """
def __repr__(self):
return f"Client(address={self._address}, port={self._port})"
Client.register_event_type('on_receive')
Client.register_event_type('on_disconnect')
class ClientConnection(_EventDispatcher):
def __init__(self, reader, writer):
self._reader = reader
self._writer = writer
self._closed = False
self._loop = _asyncio.get_event_loop()
_asyncio.run_coroutine_threadsafe(self._recv(), self._loop)
def close(self):
if not self._closed:
self._writer.transport.close()
self._closed = True
self.dispatch_event('on_disconnect', self)
async def _recv(self):
while not self._closed:
try:
header = await self._reader.readexactly(4)
size = _struct.unpack('I', header)[0]
message = await self._reader.readexactly(size)
self._loop.call_soon(self.dispatch_event, 'on_receive', self, message)
except _asyncio.IncompleteReadError:
self.close()
break
async def _send(self, message):
try:
packet = _struct.pack('I', len(message)) + message
self._writer.write(packet)
await self._writer.drain()
except ConnectionResetError:
self.close()
def send(self, message):
# Synchrounously send a message in an async coroutine.
if self._writer.transport is None or self._writer.transport.is_closing():
self.close()
return
_future = _asyncio.run_coroutine_threadsafe(self._send(message), self._loop)
def on_receive(self, connection, message):
"""Event for received messages."""
def on_disconnect(self, connection):
"""Event for disconnection. """
def __del__(self):
assert _debug_net(f"Connection garbage collected: {self}")
def __repr__(self):
return f"{self.__class__.__name__}({id(self)})"
ClientConnection.register_event_type('on_receive')
ClientConnection.register_event_type('on_disconnect')
class Server(_EventDispatcher):
def __init__(self, address, port):
self._address = address
self._port = port
self._server = None
self._thread = _threading.Thread(target=self._run, daemon=True)
self._thread.start()
blurb = f"Server listening on {address}:{port}"
assert _debug_net(f"{'-' * len(blurb)}\n{blurb}\n{'-' * len(blurb)}")
async def handle_connection(self, reader, writer):
connection = ClientConnection(reader, writer)
self.dispatch_event('on_connection', connection)
async def _start_server(self):
self._server = await _asyncio.start_server(self.handle_connection, self._address, self._port)
async with self._server:
await self._server.serve_forever()
def _run(self):
try:
_asyncio.run(self._start_server())
except KeyboardInterrupt:
self._server.close()
def on_connection(self, connection):
"""Event for new Client connections."""
assert _debug_net(f"Connected <--- {connection}")
connection.set_handler('on_disconnect', self.on_disconnect)
def on_disconnect(self, connection):
"""Event for disconnected Clients."""
assert _debug_net(f"Disconnected ---> {connection}")
Server.register_event_type('on_connection')
Server.register_event_type('on_disconnect')

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

@ -336,7 +336,7 @@ class GDIPlusGlyphRenderer(Win32GlyphRenderer):
pass
def _create_bitmap(self, width, height):
self._data = (ctypes.c_byte * (4 * width * height))()
self._data = (BYTE * (4 * width * height))()
self._bitmap = ctypes.c_void_p()
self._format = PixelFormat32bppARGB
gdiplus.GdipCreateBitmapFromScan0(width, height, width * 4,
@ -532,6 +532,12 @@ class GDIPlusFont(Win32Font):
self._name = name
family = ctypes.c_void_p()
# GDI will add @ in front of a localized font for some Asian languages. However, GDI will also not find it
# based on that name (???). Here we remove it before checking font collections.
if name[0] == "@":
name = name[1:]
name = ctypes.c_wchar_p(name)
# Look in private collection first:
@ -540,6 +546,9 @@ class GDIPlusFont(Win32Font):
# Then in system collection:
if not family:
if _debug_font:
print(f"Warning: Font '{name}' was not found. Defaulting to: {self._default_name}")
gdiplus.GdipCreateFontFamilyFromName(name, None, ctypes.byref(family))
# Nothing found, use default font.

View File

@ -1,6 +1,7 @@
import weakref
from enum import Enum
import threading
from typing import Tuple
import pyglet
@ -209,11 +210,12 @@ class CanvasConfig(Config):
class ObjectSpace:
def __init__(self):
# Textures and buffers scheduled for deletion
# the next time this object space is active.
# Objects scheduled for deletion the next time this object space is active.
self.doomed_textures = []
self.doomed_buffers = []
self.doomed_shader_programs = []
self.doomed_shaders = []
self.doomed_renderbuffers = []
class Context:
@ -236,6 +238,7 @@ class Context:
self.canvas = None
self.doomed_vaos = []
self.doomed_framebuffers = []
if context_share:
self.object_space = context_share.object_space
@ -277,28 +280,49 @@ class Context:
self._info = gl_info.GLInfo()
self._info.set_active_context()
# Release Textures, Buffers, and VAOs on this context scheduled for
# deletion. Note that the garbage collector may introduce a race
# condition, so operate on a copy, and clear the list afterward.
if self.object_space.doomed_textures:
textures = self.object_space.doomed_textures[:]
textures = (gl.GLuint * len(textures))(*textures)
gl.glDeleteTextures(len(textures), textures)
self.object_space.doomed_textures.clear()
self._delete_objects(self.object_space.doomed_textures, gl.glDeleteTextures)
if self.object_space.doomed_buffers:
buffers = self.object_space.doomed_buffers[:]
buffers = (gl.GLuint * len(buffers))(*buffers)
gl.glDeleteBuffers(len(buffers), buffers)
self.object_space.doomed_buffers.clear()
self._delete_objects(self.object_space.doomed_buffers, gl.glDeleteBuffers)
if self.object_space.doomed_shader_programs:
for program_id in self.object_space.doomed_shader_programs:
gl.glDeleteProgram(program_id)
self.object_space.doomed_shader_programs.clear()
self._delete_objects_one_by_one(self.object_space.doomed_shader_programs,
gl.glDeleteProgram)
if self.object_space.doomed_shaders:
self._delete_objects_one_by_one(self.object_space.doomed_shaders, gl.glDeleteShader)
if self.object_space.doomed_renderbuffers:
self._delete_objects(self.object_space.doomed_renderbuffers, gl.glDeleteRenderbuffers)
if self.doomed_vaos:
vaos = self.doomed_vaos[:]
vaos = (gl.GLuint * len(vaos))(*vaos)
gl.glDeleteVertexArrays(len(vaos), vaos)
self.doomed_vaos.clear()
self._delete_objects(self.doomed_vaos, gl.glDeleteVertexArrays)
if self.doomed_framebuffers:
self._delete_objects(self.doomed_framebuffers, gl.glDeleteFramebuffers)
# For the functions below:
# The garbage collector introduces a race condition.
# The provided list might be appended to (and only appended to) while this
# method runs, as it's a `doomed_*` list either on the context or its
# object space. If `count` wasn't stored in a local, this method might
# leak objects.
def _delete_objects(self, list_, deletion_func):
"""Release all OpenGL objects in the given list using the supplied
deletion function with the signature ``(GLuint count, GLuint *names)``.
"""
count = len(list_)
to_delete = list_[:count]
del list_[:count]
deletion_func(count, (gl.GLuint * count)(*to_delete))
def _delete_objects_one_by_one(self, list_, deletion_func):
"""Similar to ``_delete_objects``, but assumes the deletion functions's
signature to be ``(GLuint name)``, calling it once for each object.
"""
count = len(list_)
to_delete = list_[:count]
del list_[:count]
for name in to_delete:
deletion_func(gl.GLuint(name))
def destroy(self):
"""Release the context.
@ -318,6 +342,27 @@ class Context:
if gl._shadow_window is not None:
gl._shadow_window.switch_to()
def _safe_to_operate_on_object_space(self):
"""Return whether it is safe to interact with this context's object
space.
This is considered to be the case if the currently active context's
object space is the same as this context's object space and this
method is called from the main thread.
"""
return (
self.object_space is gl.current_context.object_space and
threading.current_thread() is threading.main_thread()
)
def _safe_to_operate_on(self):
"""Return whether it is safe to interact with this context.
This is considered to be the case if it's the current context and this
method is called from the main thread.
"""
return gl.current_context is self and threading.current_thread() is threading.main_thread()
def create_program(self, *sources: Tuple[str, str], program_class=None):
"""Create a ShaderProgram from OpenGL GLSL source.
@ -347,25 +392,30 @@ class Context:
return program
def delete_texture(self, texture_id):
"""Safely delete a Texture belonging to this context.
"""Safely delete a Texture belonging to this context's object space.
Usually, the Texture is released immediately using
``glDeleteTextures``, however if another context that does not share
this context's object space is currently active, the deletion will
be deferred until an appropriate context is activated.
This method will delete the texture immediately via
``glDeleteTextures`` if the current context's object space is the same
as this context's object space and it is called from the main thread.
Otherwise, the texture will only be marked for deletion, postponing
it until any context with the same object space becomes active again.
This makes it safe to call from anywhere, including other threads.
:Parameters:
`texture_id` : int
The OpenGL name of the Texture to delete.
"""
if self.object_space is gl.current_context.object_space:
if self._safe_to_operate_on_object_space():
gl.glDeleteTextures(1, gl.GLuint(texture_id))
else:
self.object_space.doomed_textures.append(texture_id)
def delete_buffer(self, buffer_id):
"""Safely delete a Buffer object belonging to this context.
"""Safely delete a Buffer object belonging to this context's object
space.
This method behaves similarly to `delete_texture`, though for
``glDeleteBuffers`` instead of ``glDeleteTextures``.
@ -376,30 +426,14 @@ class Context:
.. versionadded:: 1.1
"""
if self.object_space is gl.current_context.object_space and False:
if self._safe_to_operate_on_object_space():
gl.glDeleteBuffers(1, gl.GLuint(buffer_id))
else:
self.object_space.doomed_buffers.append(buffer_id)
def delete_vao(self, vao_id):
"""Safely delete a Vertex Array Object belonging to this context.
This method behaves similarly to `delete_texture`, though for
``glDeleteVertexArrays`` instead of ``glDeleteTextures``.
:Parameters:
`vao_id` : int
The OpenGL name of the Vertex Array to delete.
.. versionadded:: 2.0
"""
if gl.current_context is self:
gl.glDeleteVertexArrays(1, gl.GLuint(vao_id))
else:
self.doomed_vaos.append(vao_id)
def delete_shader_program(self, program_id):
"""Safely delete a Shader Program belonging to this context.
"""Safely delete a Shader Program belonging to this context's
object space.
This method behaves similarly to `delete_texture`, though for
``glDeleteProgram`` instead of ``glDeleteTextures``.
@ -410,11 +444,84 @@ class Context:
.. versionadded:: 2.0
"""
if gl.current_context is self:
gl.glDeleteProgram(program_id)
if self._safe_to_operate_on_object_space():
gl.glDeleteProgram(gl.GLuint(program_id))
else:
self.object_space.doomed_shader_programs.append(program_id)
def delete_shader(self, shader_id):
"""Safely delete a Shader belonging to this context's object space.
This method behaves similarly to `delete_texture`, though for
``glDeleteShader`` instead of ``glDeleteTextures``.
:Parameters:
`shader_id` : int
The OpenGL name of the Shader to delete.
.. versionadded:: 2.0.10
"""
if self._safe_to_operate_on_object_space():
gl.glDeleteShader(gl.GLuint(shader_id))
else:
self.object_space.doomed_shaders.append(shader_id)
def delete_renderbuffer(self, rbo_id):
"""Safely delete a Renderbuffer Object belonging to this context's
object space.
This method behaves similarly to `delete_texture`, though for
``glDeleteRenderbuffers`` instead of ``glDeleteTextures``.
:Parameters:
`rbo_id` : int
The OpenGL name of the Shader Program to delete.
.. versionadded:: 2.0.10
"""
if self._safe_to_operate_on_object_space():
gl.glDeleteRenderbuffers(1, gl.GLuint(rbo_id))
else:
self.object_space.doomed_renderbuffers.append(rbo_id)
def delete_vao(self, vao_id):
"""Safely delete a Vertex Array Object belonging to this context.
If this context is not the current context or this method is not
called from the main thread, its deletion will be postponed until
this context is next made active again.
Otherwise, this method will immediately delete the VAO via
``glDeleteVertexArrays``.
:Parameters:
`vao_id` : int
The OpenGL name of the Vertex Array to delete.
.. versionadded:: 2.0
"""
if self._safe_to_operate_on():
gl.glDeleteVertexArrays(1, gl.GLuint(vao_id))
else:
self.doomed_vaos.append(vao_id)
def delete_framebuffer(self, fbo_id):
"""Safely delete a Framebuffer Object belonging to this context.
This method behaves similarly to `delete_vao`, though for
``glDeleteFramebuffers`` instead of ``glDeleteVertexArrays``.
:Parameters:
`fbo_id` : int
The OpenGL name of the Framebuffer Object to delete.
.. versionadded:: 2.0.10
"""
if self._safe_to_operate_on():
gl.glDeleteFramebuffers(1, gl.GLuint(fbo_id))
else:
self.doomed_framebuffers.append(fbo_id)
def get_info(self):
"""Get the OpenGL information for this context.

View File

@ -62,8 +62,7 @@ def errcheck(result, func, arguments):
print(name)
from pyglet import gl
context = gl.current_context
if not context:
if not gl.current_context:
raise GLException('No GL context; create a Window first')
error = gl.glGetError()
if error:

View File

@ -244,7 +244,7 @@ class Win32ARBContext(_BaseWin32Context):
if self.config.forward_compatible:
flags |= wglext_arb.WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB
if self.config.debug:
flags |= wglext_arb.WGL_DEBUG_BIT_ARB
flags |= wglext_arb.WGL_CONTEXT_DEBUG_BIT_ARB
if flags:
attribs.extend([wglext_arb.WGL_CONTEXT_FLAGS_ARB, flags])
attribs.append(0)

View File

@ -55,10 +55,11 @@ def draw(size, mode, **data):
assert size == len(array) // attribute.count, 'Data for %s is incorrect length' % fmt
buffer = BufferObject(size * attribute.stride)
attribute.set_region(buffer, 0, size, array)
data = (attribute.c_type * len(array))(*array)
buffer.set_data(data)
attribute.enable()
attribute.set_pointer(buffer.ptr)
buffers.append(buffer) # Don't garbage collect it.
glDrawArrays(mode, 0, size)
@ -108,10 +109,12 @@ def draw_indexed(size, mode, indices, **data):
assert size == len(array) // attribute.count, 'Data for %s is incorrect length' % fmt
buffer = BufferObject(size * attribute.stride)
attribute.set_region(buffer, 0, size, array)
data = (attribute.c_type * len(array))(*array)
buffer.set_data(data)
attribute.enable()
attribute.set_pointer(buffer.ptr)
buffers.append(buffer)
buffers.append(buffer) # Don't garbage collect it.
if size <= 0xff:
index_type = GL_UNSIGNED_BYTE

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."""
@ -207,20 +207,16 @@ class Attribute:
will be ``3 * 4 = 12``.
:Parameters:
`buffer` : `AbstractMappable`
`buffer` : `AttributeBufferObject`
The buffer to map.
`start` : int
Offset of the first vertex to map.
`count` : int
Number of vertices to map
:rtype: `AbstractBufferRegion`
:rtype: `BufferObjectRegion`
"""
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.
@ -234,11 +230,7 @@ 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
data = (self.c_type * array_count)(*data)
buffer.set_data_region(data, byte_start, byte_size)
buffer.set_region(start, count, data)
def __repr__(self):
return f"Attribute(name='{self.name}', location={self.location}, count={self.count})"
@ -682,6 +674,7 @@ class Shader:
"""
def __init__(self, source_string: str, shader_type: str):
self._context = pyglet.gl.current_context
self._id = None
self.type = shader_type
@ -697,6 +690,7 @@ class Shader:
source_length = c_int(len(shader_source_utf8))
shader_id = glCreateShader(shader_type)
self._id = shader_id
glShaderSource(shader_id, 1, byref(source_buffer_pointer), source_length)
glCompileShader(shader_id)
@ -717,8 +711,6 @@ class Shader:
elif _debug_gl_shaders:
print(self._get_shader_log(shader_id))
self._id = shader_id
@property
def id(self):
return self._id
@ -743,16 +735,19 @@ class Shader:
glGetShaderSource(shader_id, source_length, None, source_str)
return source_str.value.decode('utf8')
def __del__(self):
try:
glDeleteShader(self._id)
if _debug_gl_shaders:
print(f"Destroyed {self.type} Shader '{self._id}'")
def delete(self):
glDeleteShader(self._id)
self._id = None
except Exception:
# Interpreter is shutting down,
# or Shader failed to compile.
pass
def __del__(self):
if self._id is not None:
try:
self._context.delete_shader(self._id)
if _debug_gl_shaders:
print(f"Destroyed {self.type} Shader '{self._id}'")
self._id = None
except (AttributeError, ImportError):
pass # Interpreter is shutting down
def __repr__(self):
return "{0}(id={1}, type={2})".format(self.__class__.__name__, self.id, self.type)
@ -764,6 +759,8 @@ class ShaderProgram:
__slots__ = '_id', '_context', '_attributes', '_uniforms', '_uniform_blocks', '__weakref__'
def __init__(self, *shaders: Shader):
self._id = None
assert shaders, "At least one Shader object is required."
self._id = _link_program(*shaders)
self._context = pyglet.gl.current_context
@ -807,13 +804,17 @@ class ShaderProgram:
def __exit__(self, *_):
glUseProgram(0)
def delete(self):
glDeleteProgram(self._id)
self._id = None
def __del__(self):
try:
self._context.delete_shader_program(self.id)
except Exception:
# Interpreter is shutting down,
# or ShaderProgram failed to link.
pass
if self._id is not None:
try:
self._context.delete_shader_program(self._id)
self._id = None
except (AttributeError, ImportError):
pass # Interpreter is shutting down
def __setitem__(self, key, value):
try:
@ -938,6 +939,8 @@ class ComputeShaderProgram:
def __init__(self, source: str):
"""Create an OpenGL ComputeShaderProgram from source."""
self._id = None
if not (gl_info.have_version(4, 3) or gl_info.have_extension("GL_ARB_compute_shader")):
raise ShaderException("Compute Shader not supported. OpenGL Context version must be at least "
"4.3 or higher, or 4.2 with the 'GL_ARB_compute_shader' extension.")
@ -1010,13 +1013,17 @@ class ComputeShaderProgram:
def __exit__(self, *_):
glUseProgram(0)
def delete(self):
glDeleteProgram(self._id)
self._id = None
def __del__(self):
try:
self._context.delete_shader_program(self.id)
except Exception:
# Interpreter is shutting down,
# or ShaderProgram failed to link.
pass
if self._id is not None:
try:
self._context.delete_shader_program(self._id)
self._id = None
except (AttributeError, ImportError):
pass # Interpreter is shutting down
def __setitem__(self, key, value):
try:

View File

@ -27,10 +27,8 @@ class VertexArray:
glBindVertexArray(0)
def delete(self):
try:
glDeleteVertexArrays(1, self._id)
except Exception:
pass
glDeleteVertexArrays(1, self._id)
self._id = None
__enter__ = bind
@ -38,11 +36,12 @@ class VertexArray:
glBindVertexArray(0)
def __del__(self):
try:
self._context.delete_vao(self.id)
# Python interpreter is shutting down:
except ImportError:
pass
if self._id is not None:
try:
self._context.delete_vao(self.id)
self._id = None
except (ImportError, AttributeError):
pass # Interpreter is shutting down
def __repr__(self):
return "{}(id={})".format(self.__class__.__name__, self._id.value)

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.
@ -180,7 +152,8 @@ class BufferObject(AbstractBuffer):
def map(self):
glBindBuffer(GL_ARRAY_BUFFER, self.id)
ptr = ctypes.cast(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY), ctypes.POINTER(ctypes.c_byte * self.size)).contents
ptr = ctypes.cast(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY),
ctypes.POINTER(ctypes.c_byte * self.size)).contents
return ptr
def map_range(self, start, size, ptr_type):
@ -191,21 +164,18 @@ class BufferObject(AbstractBuffer):
def unmap(self):
glUnmapBuffer(GL_ARRAY_BUFFER)
def __del__(self):
try:
if self.id is not None:
self._context.delete_buffer(self.id)
except:
pass
def delete(self):
buffer_id = GLuint(self.id)
try:
glDeleteBuffers(1, buffer_id)
except Exception:
pass
glDeleteBuffers(1, self.id)
self.id = None
def __del__(self):
if self.id is not None:
try:
self._context.delete_buffer(self.id)
self.id = None
except (AttributeError, ImportError):
pass # Interpreter is shutting down
def resize(self, size):
# Map, create a copy, then reinitialize.
temp = (ctypes.c_byte * size)()
@ -222,27 +192,31 @@ 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
self._array = self.get_region(0, size).array
def bind(self, target=GL_ARRAY_BUFFER):
super().bind(target)
size = self._dirty_max - self._dirty_min
if size > 0:
if size == self.size:
@ -252,28 +226,27 @@ class MappableBufferObject(BufferObject, AbstractMappable):
self._dirty_min = sys.maxsize
self._dirty_max = 0
def set_data(self, data):
super(MappableBufferObject, self).set_data(data)
ctypes.memmove(self.data, data, self.size)
self._dirty_min = 0
self._dirty_max = self.size
@lru_cache(maxsize=None)
def get_region(self, start, count):
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 set_data_region(self, data, start, length):
ctypes.memmove(self.data_ptr + start, data, length)
self._dirty_min = min(start, self._dirty_min)
self._dirty_max = max(start + length, self._dirty_max)
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 map(self, invalidate=False):
self._dirty_min = 0
self._dirty_max = self.size
return self.data
def set_region(self, start, count, data):
byte_start = self.attribute_stride * start # byte offset
byte_size = self.attribute_stride * count # number of bytes
def unmap(self):
pass
array_start = start * self.attribute_count
array_end = count * self.attribute_count + array_start
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)
self._array[array_start:array_end] = data
self._dirty_min = min(self._dirty_min, byte_start)
self._dirty_max = max(self._dirty_max, byte_start + byte_size)
def resize(self, size):
data = (ctypes.c_byte * size)()
@ -289,6 +262,9 @@ class MappableBufferObject(BufferObject, AbstractMappable):
self._dirty_min = sys.maxsize
self._dirty_max = 0
self._array = self.get_region(0, size).array
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,22 +64,39 @@ _gl_types = {
}
def _make_attribute_property(name):
def _attribute_getter(self):
attribute = self.domain.attribute_names[name]
region = attribute.buffer.get_region(self.start, self.count)
region.invalidate()
return region.array
def _attribute_setter(self, data):
attribute = self.domain.attribute_names[name]
attribute.buffer.set_region(self.start, self.count, data)
return property(_attribute_getter, _attribute_setter)
class VertexDomain:
"""Management of a set of vertex lists.
Construction of a vertex domain is usually done with the
:py:func:`create_domain` function.
"""
version = 0
_initial_count = 16
def __init__(self, program, attribute_meta):
self.program = program
self.program = program # Needed a reference for migration
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 +105,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(name)
# 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,29 +127,14 @@ 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:
return self.allocator.alloc(count)
except allocation.AllocatorMemoryException as e:
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)
@ -139,9 +144,8 @@ class VertexDomain:
return self.allocator.realloc(start, count, new_count)
except allocation.AllocatorMemoryException as e:
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 +161,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 +225,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 +249,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 +257,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 +286,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,13 +313,15 @@ 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:
return self.index_allocator.alloc(count)
except allocation.AllocatorMemoryException as e:
capacity = _nearest_pow2(e.requested_capacity)
self.version += 1
self.index_buffer.resize(capacity * self.index_element_size)
self.index_allocator.set_capacity(capacity)
return self.index_allocator.alloc(count)
@ -354,7 +332,6 @@ class IndexedVertexDomain(VertexDomain):
return self.index_allocator.realloc(start, count, new_count)
except allocation.AllocatorMemoryException as e:
capacity = _nearest_pow2(e.requested_capacity)
self.version += 1
self.index_buffer.resize(capacity * self.index_element_size)
self.index_allocator.set_capacity(capacity)
return self.index_allocator.realloc(start, count, new_count)
@ -371,7 +348,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

@ -433,6 +433,34 @@ class TextEntry(WidgetBase):
assert type(value) is str, "This Widget's value must be a string."
self._doc.text = value
@property
def width(self):
return self._width
@width.setter
def width(self, value):
self._width = value
self._layout.width = value
self._outline.width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
self._height = value
self._layout.height = value
self._outline.height = value
@property
def focus(self) -> bool:
return self._focus
@focus.setter
def focus(self, value: bool) -> None:
self._set_focus(value)
def _check_hit(self, x, y):
return self._x < x < self._x + self._width and self._y < y < self._y + self._height

View File

@ -1220,11 +1220,20 @@ class Texture(AbstractImage):
self.id = tex_id
self._context = pyglet.gl.current_context
def delete(self):
"""Delete this texture and the memory it occupies.
After this, it may not be used anymore.
"""
glDeleteTextures(1, self.id)
self.id = None
def __del__(self):
try:
self._context.delete_texture(self.id)
except Exception:
pass
if self.id is not None:
try:
self._context.delete_texture(self.id)
self.id = None
except (AttributeError, ImportError):
pass # Interpreter is shutting down
def bind(self, texture_unit: int = 0):
"""Bind to a specific Texture Unit by number."""
@ -1479,8 +1488,13 @@ class TextureRegion(Texture):
return "{}(id={}, size={}x{}, owner={}x{})".format(self.__class__.__name__, self.id, self.width, self.height,
self.owner.width, self.owner.height)
def delete(self):
"""Deleting a TextureRegion has no effect. Operate on the owning
texture instead.
"""
pass
def __del__(self):
# only the owner Texture should handle deletion
pass

View File

@ -13,6 +13,7 @@ class Renderbuffer:
def __init__(self, width, height, internal_format, samples=1):
"""Create an instance of a Renderbuffer object."""
self._context = pyglet.gl.current_context
self._id = GLuint()
self._width = width
self._height = height
@ -49,13 +50,15 @@ class Renderbuffer:
def delete(self):
glDeleteRenderbuffers(1, self._id)
self._id = None
def __del__(self):
try:
glDeleteRenderbuffers(1, self._id)
# Python interpreter is shutting down:
except Exception:
pass
if self._id is not None:
try:
self._context.delete_renderbuffer(self._id.value)
self._id = None
except (AttributeError, ImportError):
pass # Interpreter is shutting down
def __repr__(self):
return "{}(id={})".format(self.__class__.__name__, self._id.value)
@ -71,6 +74,7 @@ class Framebuffer:
.. versionadded:: 2.0
"""
self._context = pyglet.gl.current_context
self._id = GLuint()
glGenFramebuffers(1, self._id)
self._attachment_types = 0
@ -105,10 +109,16 @@ class Framebuffer:
self.unbind()
def delete(self):
try:
glDeleteFramebuffers(1, self._id)
except Exception:
pass
glDeleteFramebuffers(1, self._id)
self._id = None
def __del__(self):
if self._id is not None:
try:
self._context.delete_framebuffer(self._id.value)
self._id = None
except (AttributeError, ImportError):
pass # Interpreter is shutting down
@property
def is_complete(self):
@ -203,12 +213,5 @@ class Framebuffer:
self._height = max(renderbuffer.height, self._height)
self.unbind()
def __del__(self):
try:
glDeleteFramebuffers(1, self._id)
# Python interpreter is shutting down:
except Exception:
pass
def __repr__(self):
return "{}(id={})".format(self.__class__.__name__, self._id.value)

View File

@ -601,7 +601,7 @@ class WICEncoder(ImageEncoder):
frame.SetPixelFormat(byref(default_format))
data = (c_byte * size).from_buffer(bytearray(image_data))
data = (BYTE * size).from_buffer(bytearray(image_data))
frame.WritePixels(image.height, pitch, size, data)

View File

@ -4,7 +4,6 @@ import fcntl
import ctypes
import warnings
from os import readv
from ctypes import c_uint16 as _u16
from ctypes import c_int16 as _s16
from ctypes import c_uint32 as _u32
@ -22,6 +21,8 @@ from pyglet.input.base import Device, RelativeAxis, AbsoluteAxis, Button, Joysti
from pyglet.input.base import DeviceOpenException, ControllerManager
from pyglet.input.controller import get_mapping, Relation, create_guid
c = pyglet.lib.load_library('c')
_IOC_NRBITS = 8
_IOC_TYPEBITS = 8
_IOC_SIZEBITS = 14
@ -408,7 +409,7 @@ class EvdevDevice(XlibSelectDevice, Device):
return
try:
bytes_read = readv(self._fileno, self._event_buffer)
bytes_read = c.read(self._fileno, self._event_buffer, self._event_size)
except OSError:
self.close()
return

View File

@ -113,8 +113,8 @@ ERROR_SUCCESS = 0
class XINPUT_GAMEPAD(Structure):
_fields_ = [
('wButtons', WORD),
('bLeftTrigger', UBYTE),
('bRightTrigger', UBYTE),
('bLeftTrigger', BYTE),
('bRightTrigger', BYTE),
('sThumbLX', SHORT),
('sThumbLY', SHORT),
('sThumbRX', SHORT),

View File

@ -218,6 +218,7 @@ NSApplicationDidUnhideNotification = c_void_p.in_dll(appkit, 'NSApplicationDidUn
NSApplicationDidUpdateNotification = c_void_p.in_dll(appkit, 'NSApplicationDidUpdateNotification')
NSPasteboardURLReadingFileURLsOnlyKey = c_void_p.in_dll(appkit, 'NSPasteboardURLReadingFileURLsOnlyKey')
NSPasteboardTypeURL = c_void_p.in_dll(appkit, 'NSPasteboardTypeURL')
NSPasteboardTypeString = c_void_p.in_dll(appkit, 'NSPasteboardTypeString')
NSDragOperationGeneric = 4
# /System/Library/Frameworks/AppKit.framework/Headers/NSEvent.h

View File

@ -211,7 +211,18 @@ _user32.RegisterDeviceNotificationW.restype = HANDLE
_user32.RegisterDeviceNotificationW.argtypes = [HANDLE, LPVOID, DWORD]
_user32.UnregisterDeviceNotification.restype = BOOL
_user32.UnregisterDeviceNotification.argtypes = [HANDLE]
_user32.SetClipboardData.restype = HANDLE
_user32.SetClipboardData.argtypes = [UINT, HANDLE]
_user32.EmptyClipboard.restype = BOOL
_user32.EmptyClipboard.argtypes = []
_user32.OpenClipboard.restype = BOOL
_user32.OpenClipboard.argtypes = [HWND]
_user32.CloseClipboard.restype = BOOL
_user32.CloseClipboard.argtypes = []
_user32.GetClipboardData.restype = HANDLE
_user32.GetClipboardData.argtypes = [UINT]
_user32.SetClipboardData.restype = HANDLE
_user32.SetClipboardData.argtypes = [UINT, HANDLE]
# dwmapi
_dwmapi.DwmIsCompositionEnabled.restype = c_int

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

@ -1,4 +1,5 @@
import ctypes
import sys
from ctypes import *
from ctypes.wintypes import *
@ -45,7 +46,6 @@ def POINTER_(obj):
c_void_p = POINTER_(c_void)
INT = c_int
UBYTE = c_ubyte
LPVOID = c_void_p
HCURSOR = HANDLE
LRESULT = LPARAM
@ -62,6 +62,11 @@ HDROP = HANDLE
LPTSTR = LPWSTR
LPSTREAM = c_void_p
# Fixed in python 3.12. Is c_byte on other versions.
# Ensure it's the same across all versions.
if sys.version_info < (3, 12):
BYTE = c_ubyte
LF_FACESIZE = 32
CCHDEVICENAME = 32
CCHFORMNAME = 32
@ -572,6 +577,7 @@ class IStream(com.pIUnknown):
com.STDMETHOD()),
]
class DEV_BROADCAST_HDR(Structure):
_fields_ = (
('dbch_size', DWORD),
@ -579,6 +585,7 @@ class DEV_BROADCAST_HDR(Structure):
('dbch_reserved', DWORD),
)
class DEV_BROADCAST_DEVICEINTERFACE(Structure):
_fields_ = (
('dbcc_size', DWORD),

View File

@ -26,6 +26,7 @@ from collections.abc import Iterator as _Iterator
number = _typing.Union[float, int]
Mat3T = _typing.TypeVar("Mat3T", bound="Mat3")
Mat4T = _typing.TypeVar("Mat4T", bound="Mat4")
@ -627,7 +628,7 @@ class Mat3(tuple):
the "@" operator.
"""
def __new__(cls, values: _Iterable[float] = (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) -> Mat3:
def __new__(cls: type[Mat3T], values: _Iterable[float] = (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) -> Mat3T:
"""Create a 3x3 Matrix
A Mat3 can be created with a list or tuple of 9 values.
@ -639,7 +640,7 @@ class Mat3(tuple):
`values` : tuple of float or int
A tuple or list containing 9 floats or ints.
"""
new = super().__new__(Mat3, values)
new = super().__new__(cls, values)
assert len(new) == 9, "A 3x3 Matrix requires 9 values"
return new
@ -720,10 +721,10 @@ class Mat3(tuple):
class Mat4(tuple):
def __new__(cls, values: _Iterable[float] = (1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,)) -> Mat4:
def __new__(cls: type[Mat4T], values: _Iterable[float] = (1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,)) -> Mat4T:
"""Create a 4x4 Matrix.
`Mat4` is an immutable 4x4 Matrix, which includs most common
@ -738,7 +739,7 @@ class Mat4(tuple):
.. note:: Matrix multiplication is performed using the "@" operator.
"""
new = super().__new__(Mat4, values)
new = super().__new__(cls, values)
assert len(new) == 16, "A 4x4 Matrix requires 16 values"
return new
@ -1011,3 +1012,106 @@ class Mat4(tuple):
def __repr__(self) -> str:
return f"{self.__class__.__name__}{self[0:4]}\n {self[4:8]}\n {self[8:12]}\n {self[12:16]}"
class Quaternion(tuple):
"""Quaternion"""
def __new__(cls, w: float = 1.0, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> Quaternion:
return super().__new__(Quaternion, (w, x, y, z))
@classmethod
def from_mat3(cls) -> Quaternion:
raise NotImplementedError("Not yet implemented")
@classmethod
def from_mat4(cls) -> Quaternion:
raise NotImplementedError("Not yet implemented")
def to_mat4(self) -> Mat4:
w = self.w
x = self.x
y = self.y
z = self.z
a = 1 - (y ** 2 + z ** 2) * 2
b = 2 * (x * y - z * w)
c = 2 * (x * z + y * w)
e = 2 * (x * y + z * w)
f = 1 - (x ** 2 + z ** 2) * 2
g = 2 * (y * z - x * w)
i = 2 * (x * z - y * w)
j = 2 * (y * z + x * w)
k = 1 - (x ** 2 + y ** 2) * 2
# a, b, c, -
# e, f, g, -
# i, j, k, -
# -, -, -, -
return Mat4((a, b, c, 0.0, e, f, g, 0.0, i, j, k, 0.0, 0.0, 0.0, 0.0, 1.0))
def to_mat3(self) -> Mat3:
w = self.w
x = self.x
y = self.y
z = self.z
a = 1 - (y ** 2 + z ** 2) * 2
b = 2 * (x * y - z * w)
c = 2 * (x * z + y * w)
e = 2 * (x * y + z * w)
f = 1 - (x ** 2 + z ** 2) * 2
g = 2 * (y * z - x * w)
i = 2 * (x * z - y * w)
j = 2 * (y * z + x * w)
k = 1 - (x ** 2 + y ** 2) * 2
# a, b, c, -
# e, f, g, -
# i, j, k, -
# -, -, -, -
return Mat3((a, b, c, e, f, g, i, j, k))
@property
def w(self) -> float:
return self[0]
@property
def x(self) -> float:
return self[1]
@property
def y(self) -> float:
return self[2]
@property
def z(self) -> float:
return self[3]
def conjugate(self) -> Quaternion:
return Quaternion(self.w, -self.x, -self.y, -self.z)
@property
def mag(self) -> float:
return self.__abs__()
def normalize(self) -> Quaternion:
m = self.__abs__()
if m == 0:
return self
return Quaternion(self[0] / m, self[1] / m, self[2] / m, self[3] / m)
def __abs__(self) -> float:
return _math.sqrt(self.w ** 2 + self.x ** 2 + self.y ** 2 + self.z ** 2)
def __invert__(self) -> Quaternion:
raise NotImplementedError("Not yet implemented")
def __repr__(self) -> str:
return f"{self.__class__.__name__}(w={self[0]}, x={self[1]}, y={self[2]}, z={self[3]})"

View File

@ -1,6 +1,6 @@
"""Use ffmpeg to decode audio and video media.
"""
import sys
from collections import deque
from ctypes import (c_int, c_int32, c_uint8, c_char_p,
addressof, byref, cast, POINTER, Structure, create_string_buffer, memmove)
@ -8,7 +8,7 @@ from ctypes import (c_int, c_int32, c_uint8, c_char_p,
import pyglet
import pyglet.lib
from pyglet import image
from pyglet.util import asbytes, asbytes_filename, asstr
from pyglet.util import asbytes, asstr
from . import MediaDecoder
from .base import AudioData, SourceInfo, StaticSource
from .base import StreamingSource, VideoFormat, AudioFormat
@ -510,10 +510,12 @@ class FFmpegSource(StreamingSource):
self._file = None
self._memory_file = None
encoded_filename = filename.encode(sys.getfilesystemencoding())
if file:
self._file, self._memory_file = ffmpeg_open_memory_file(asbytes_filename(filename), file)
self._file, self._memory_file = ffmpeg_open_memory_file(encoded_filename, file)
else:
self._file = ffmpeg_open_filename(asbytes_filename(filename))
self._file = ffmpeg_open_filename(encoded_filename)
if not self._file:
raise FFmpegException('Could not open "{0}"'.format(filename))

View File

@ -520,9 +520,9 @@ class WMFSource(Source):
imfmedia.GetGUID(MF_MT_SUBTYPE, ctypes.byref(guid_compressed))
if guid_compressed == MFAudioFormat_PCM or guid_compressed == MFAudioFormat_Float:
assert _debug('WMFAudioDecoder: Found Uncompressed Audio:', guid_compressed)
assert _debug(f'WMFAudioDecoder: Found Uncompressed Audio: {guid_compressed}')
else:
assert _debug('WMFAudioDecoder: Found Compressed Audio:', guid_compressed)
assert _debug(f'WMFAudioDecoder: Found Compressed Audio: {guid_compressed}')
# If audio is compressed, attempt to decompress it by forcing source reader to use PCM
mf_mediatype = IMFMediaType()

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
@ -259,7 +259,7 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager):
cached_dev.state = dev_state
return cached_dev
except OSError as err:
assert _debug("No default audio output was found.", err)
assert _debug(f"No default audio output was found. {err}")
return None
def get_default_input(self) -> Optional[Win32AudioDevice]:
@ -274,7 +274,7 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager):
cached_dev.state = dev_state
return cached_dev
except OSError as err:
assert _debug("No default input output was found.", err)
assert _debug(f"No default input output was found. {err}")
return None
def get_cached_device(self, dev_id) -> Win32AudioDevice:

View File

@ -145,11 +145,11 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
def _refill(self, write_size):
while write_size > 0:
assert _debug('_refill, write_size =', write_size)
assert _debug(f'_refill, write_size = {write_size}')
audio_data = self._get_audiodata()
if audio_data is not None:
assert _debug('write', audio_data.length)
assert _debug(f'write {audio_data.length}')
length = min(write_size, audio_data.length)
self.write(audio_data, length)
write_size -= length
@ -191,14 +191,14 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
# Set the write cursor back to eos_cursor or play_cursor to prevent gaps
if self._play_cursor < self._eos_cursor:
cursor_diff = self._write_cursor - self._eos_cursor
assert _debug('Moving cursor back', cursor_diff)
assert _debug(f'Moving cursor back {cursor_diff}')
self._write_cursor = self._eos_cursor
self._write_cursor_ring -= cursor_diff
self._write_cursor_ring %= self._buffer_size
else:
cursor_diff = self._play_cursor - self._eos_cursor
assert _debug('Moving cursor back', cursor_diff)
assert _debug(f'Moving cursor back {cursor_diff}')
self._write_cursor = self._play_cursor
self._write_cursor_ring -= cursor_diff
self._write_cursor_ring %= self._buffer_size
@ -207,7 +207,7 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
for event in audio_data.events:
event_cursor = self._write_cursor + event.timestamp * \
self.source.audio_format.bytes_per_second
assert _debug('Adding event', event, 'at', event_cursor)
assert _debug(f'Adding event {event} at {event_cursor}')
self._events.append((event_cursor, event))
def _add_audiodata_timestamp(self, audio_data):

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

@ -245,7 +245,7 @@ class OpenALAudioPlayer(AbstractAudioPlayer):
return False
def _refill(self, write_size):
assert _debug('_refill', write_size)
assert _debug(f'_refill {write_size}')
while write_size > self.min_buffer_size:
audio_data = self._get_audiodata()

View File

@ -236,7 +236,7 @@ class PulseAudioPlayer(AbstractAudioPlayer):
while self._events and self._events[0][0] <= read_index:
_, event = self._events.pop(0)
assert _debug('PulseAudioPlayer: Dispatch event', event)
assert _debug(f'PulseAudioPlayer: Dispatch event {event}')
event.sync_dispatch_to_player(self.player)
def _add_event_at_write_index(self, event_name):
@ -313,7 +313,7 @@ class PulseAudioPlayer(AbstractAudioPlayer):
else:
read_index = 0
assert _debug('_get_read_index ->', read_index)
assert _debug(f'_get_read_index -> {read_index}')
return read_index
def _get_write_index(self):
@ -323,7 +323,7 @@ class PulseAudioPlayer(AbstractAudioPlayer):
else:
write_index = 0
assert _debug('_get_write_index ->', write_index)
assert _debug(f'_get_write_index -> {write_index}')
return write_index
def _get_timing_info(self):
@ -365,7 +365,7 @@ class PulseAudioPlayer(AbstractAudioPlayer):
dt /= 1000000
time = timestamp + dt
assert _debug('get_time ->', time)
assert _debug('get_time -> {time}')
return time
def set_volume(self, volume):

View File

@ -213,7 +213,7 @@ class XAudio2AudioPlayer(AbstractAudioPlayer):
def _add_audiodata_events(self, audio_data):
for event in audio_data.events:
event_cursor = self._write_cursor + event.timestamp * self.source.audio_format.bytes_per_second
assert _debug('Adding event', event, 'at', event_cursor)
assert _debug(f'Adding event {event} at {event_cursor}')
self._events.append((event_cursor, event))
def _add_audiodata_timestamp(self, audio_data):

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

@ -5,12 +5,11 @@ such as Rectangles, Circles, and Lines. These shapes are made
internally from OpenGL primitives, and provide excellent performance
when drawn as part of a :py:class:`~pyglet.graphics.Batch`.
Convenience methods are provided for positioning, changing color
and opacity, and rotation (where applicable). To create more
complex shapes than what is provided here, the lower level
graphics API is more appropriate.
You can also use the ``in`` operator to check whether a point is
inside a shape.
See the :ref:`guide_graphics` for more details.
and opacity, and rotation (where applicable).
The Python ``in`` operator to check whether a point is inside a shape.
To create more complex shapes than what is provided here, the lower level
graphics API is more appropriate. See the :ref:`guide_graphics` for more details.
A simple example of drawing shapes::
@ -1466,6 +1465,122 @@ class BorderedRectangle(ShapeBase):
self._update_color()
class Box(ShapeBase):
def __init__(self, x, y, width, height, thickness=1, color=(255, 255, 255, 255), batch=None, group=None):
"""Create an unfilled rectangular shape, with optional thickness.
The box's anchor point defaults to the (x, y) coordinates,
which are at the bottom left.
Changing the thickness of the box will extend the walls inward;
the outward dimesions will not be affected.
:Parameters:
`x` : float
The X coordinate of the box.
`y` : float
The Y coordinate of the box.
`width` : float
The width of the box.
`height` : float
The height of the box.
`thickness` : float
The thickness of the lines that make up the box.
`color` : (int, int, int, int)
The RGB or RGBA color of the box, specified as a tuple
of 3 or 4 ints in the range of 0-255. RGB colors will
be treated as having an opacity of 255.
`batch` : `~pyglet.graphics.Batch`
Optional batch to add the box to.
`group` : `~pyglet.graphics.Group`
Optional parent group of the box.
"""
self._x = x
self._y = y
self._width = width
self._height = height
self._thickness = thickness
self._num_verts = 8
r, g, b, *a = color
self._rgba = r, g, b, a[0] if a else 255
program = get_default_shader()
self._batch = batch or Batch()
self._group = self.group_class(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group)
self._create_vertex_list()
self._update_vertices()
def __contains__(self, point):
assert len(point) == 2
point = _rotate_point((self._x, self._y), point, math.radians(self._rotation))
x, y = self._x - self._anchor_x, self._y - self._anchor_y
return x < point[0] < x + self._width and y < point[1] < y + self._height
def _create_vertex_list(self):
# 3 6
# 2 7
# 1 4
# 0 5
indices = [0, 1, 2, 0, 2, 3, 0, 5, 4, 0, 4, 1, 4, 5, 6, 4, 6, 7, 2, 7, 6, 2, 6, 3]
self._vertex_list = self._group.program.vertex_list_indexed(
self._num_verts, self._draw_mode, indices, self._batch, self._group,
colors=('Bn', self._rgba * self._num_verts),
translation=('f', (self._x, self._y) * self._num_verts))
def _update_color(self):
self._vertex_list.colors[:] = self._rgba * self._num_verts
def _update_vertices(self):
if not self._visible:
self._vertex_list.position[:] = (0, 0) * self._num_verts
else:
t = self._thickness
left = -self._anchor_x
bottom = -self._anchor_y
right = left + self._width
top = bottom + self._height
x1 = left
x2 = left + t
x3 = right - t
x4 = right
y1 = bottom
y2 = bottom + t
y3 = top - t
y4 = top
# 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
self._vertex_list.position[:] = x1, y1, x2, y2, x2, y3, x1, y4, x3, y2, x4, y1, x4, y4, x3, y3
@property
def width(self):
"""The width of the Box.
:type: float
"""
return self._width
@width.setter
def width(self, value):
self._width = value
self._update_vertices()
@property
def height(self):
"""The height of the Box.
:type: float
"""
return self._height
@height.setter
def height(self, value):
self._height = value
self._update_vertices()
class Triangle(ShapeBase):
def __init__(self, x, y, x2, y2, x3, y3, color=(255, 255, 255, 255),
batch=None, group=None):
@ -1776,4 +1891,5 @@ class Polygon(ShapeBase):
self._vertex_list.position[:] = tuple(value for coordinate in triangles for value in coordinate)
__all__ = 'Arc', 'BezierCurve', 'Circle', 'Ellipse', 'Line', 'Rectangle', 'BorderedRectangle', 'Triangle', 'Star', 'Polygon', 'Sector'
__all__ = ('Arc', 'Box', 'BezierCurve', 'Circle', 'Ellipse', 'Line', 'Rectangle',
'BorderedRectangle', 'Triangle', 'Star', 'Polygon', 'Sector', 'ShapeBase')

View File

@ -271,6 +271,8 @@ class DocumentLabel(layout.TextLayout):
@color.setter
def color(self, color):
r, g, b, *a = color
color = r, g, b, a[0] if a else 255
self.document.set_style(0, len(self.document.text), {'color': color})
@property

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

@ -540,18 +540,15 @@ class _GlyphBox(_AbstractBox):
x1 = x2
if background_vertices:
background_indices = []
bg_count = len(background_vertices) // 2
bg_count = len(background_vertices) // 3
background_indices = [(0, 1, 2, 0, 2, 3)[i % 6] for i in range(bg_count * 3)]
decoration_program = get_default_decoration_shader()
for bg_idx in range(bg_count):
background_indices.extend([element + (bg_idx * 4) for element in [0, 1, 2, 0, 2, 3]])
background_list = decoration_program.vertex_list_indexed(bg_count * 4, GL_TRIANGLES, background_indices,
background_list = decoration_program.vertex_list_indexed(bg_count, GL_TRIANGLES, background_indices,
layout.batch, layout.background_decoration_group,
position=('f', background_vertices),
colors=('Bn', background_colors),
rotation=('f', (rotation,) * 4),
anchor=('f', (anchor_x, anchor_y) * 4))
rotation=('f', (rotation,) * bg_count),
anchor=('f', (anchor_x, anchor_y) * bg_count))
context.add_list(background_list)
if underline_vertices:

View File

@ -3,11 +3,13 @@
import os
import sys
from typing import Optional, Union, Callable
import pyglet
from pyglet.customtypes import Buffer
def asbytes(s):
def asbytes(s: Union[str, Buffer]) -> bytes:
if isinstance(s, bytes):
return s
elif isinstance(s, str):
@ -16,56 +18,68 @@ def asbytes(s):
return bytes(s)
def asbytes_filename(s):
if isinstance(s, bytes):
return s
elif isinstance(s, str):
return s.encode(encoding=sys.getfilesystemencoding())
def asstr(s):
def asstr(s: Optional[Union[str, Buffer]]) -> str:
if s is None:
return ''
if isinstance(s, str):
return s
return s.decode("utf-8")
return s.decode("utf-8") # type: ignore
def debug_print(enabled_or_option='debug'):
"""Get a debug printer that is enabled based on a boolean input or a pyglet option.
The debug print function returned should be used in an assert. This way it can be
optimized out when running python with the -O flag.
# Keep these outside of the function since we don't need to re-define
# the function each time we make a call since no state is persisted.
def _debug_print_real(arg: str) -> bool:
print(arg)
return True
def _debug_print_dummy(arg: str) -> bool:
return True
def debug_print(pyglet_option_name: str = 'debug') -> Callable[[str], bool]:
"""Get a debug printer controlled by the given ``pyglet.options`` name.
This allows repurposing ``assert`` to write cleaner, more efficient
debug output:
#. Debug printers fit into a one-line ``assert`` statements instead
of longer, slower key-lookup ``if`` statements
#. Running Python with the ``-O`` flag makes pyglet run faster by
skipping all ``assert`` statements
Usage example::
from pyglet.debug import debug_print
from pyglet.util import debug_print
_debug_media = debug_print('debug_media')
def some_func():
# Python will skip the line below when run with -O
assert _debug_media('My debug statement')
:parameters:
`enabled_or_options` : bool or str
If a bool is passed, debug printing is enabled if it is True. If str is passed
debug printing is enabled if the pyglet option with that name is True.
# The rest of the function will run as normal
...
For more information, please see `the Python command line
documentation <https://docs.python.org/3/using/cmdline.html#cmdoption-O>`_.
Args:
`pyglet_option_name` :
The name of a key in :attr:`pyglet.options` to read the
debug flag's value from.
Returns:
A callable which prints a passed string and returns ``True``
to allow auto-removal when running with ``-O``.
:returns: Function for debug printing.
"""
if isinstance(enabled_or_option, bool):
enabled = enabled_or_option
else:
enabled = pyglet.options.get(enabled_or_option, False)
enabled = pyglet.options.get(pyglet_option_name, False)
if enabled:
def _debug_print(*args, **kwargs):
print(*args, **kwargs)
return True
else:
def _debug_print(*args, **kwargs):
return True
return _debug_print
return _debug_print_real
return _debug_print_dummy
class CodecRegistry:

View File

@ -273,9 +273,10 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
conventions. This will ensure it is not obscured by other windows,
and appears on an appropriate screen for the user.
To render into a window, you must first call `switch_to`, to make
it the current OpenGL context. If you use only one window in the
application, there is no need to do this.
To render into a window, you must first call its :py:meth:`.switch_to`
method to make it the active OpenGL context. If you use only one
window in your application, you can skip this step as it will always
be the active context.
"""
# Filled in by metaclass with the names of all methods on this (sub)class
@ -638,7 +639,8 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
"""Clear the window.
This is a convenience method for clearing the color and depth
buffer. The window must be the active context (see `switch_to`).
buffer. The window must be the active context (see
:py:meth:`.switch_to`).
"""
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
@ -646,10 +648,12 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
"""Close the window.
After closing the window, the GL context will be invalid. The
window instance cannot be reused once closed (see also `set_visible`).
window instance cannot be reused once closed. To re-use windows,
see :py:meth:`.set_visible` instead.
The `pyglet.app.EventLoop.on_window_close` event is dispatched on
`pyglet.app.event_loop` when this method is called.
The :py:meth:`pyglet.app.EventLoop.on_window_close` event is
dispatched by the :py:attr:`pyglet.app.event_loop` when this method
is called.
"""
from pyglet import app
if not self._context:
@ -676,7 +680,7 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
and advanced applications that must integrate their event loop
into another framework.
Typical applications should use `pyglet.app.run`.
Typical applications should use :py:func:`pyglet.app.run`.
"""
raise NotImplementedError('abstract')
@ -715,11 +719,14 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
"""Swap the OpenGL front and back buffers.
Call this method on a double-buffered window to update the
visible display with the back buffer. The contents of the back buffer
is undefined after this operation.
visible display with the back buffer. Windows are
double-buffered by default unless you turn this feature off.
Windows are double-buffered by default. This method is called
automatically by `EventLoop` after the :py:meth:`~pyglet.window.Window.on_draw` event.
The contents of the back buffer are undefined after this operation.
The default :py:attr:`~pyglet.app.event_loop` automatically
calls this method after the window's
:py:meth:`~pyglet.window.Window.on_draw` event.
"""
raise NotImplementedError('abstract')
@ -791,6 +798,25 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
"""
raise NotImplementedError()
def get_clipboard_text(self) -> str:
"""Access the system clipboard and attempt to retrieve text.
:rtype: `str`
:return: A string from the clipboard. String will be empty if no text found.
"""
raise NotImplementedError()
def set_clipboard_text(self, text: str):
"""Access the system clipboard and set a text string as the clipboard data.
This will clear the existing clipboard.
:Parameters:
`text` : str
Text you want to place in the clipboard.
"""
raise NotImplementedError()
def minimize(self):
"""Minimize the window.
"""
@ -1158,10 +1184,13 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
def switch_to(self):
"""Make this window the current OpenGL rendering context.
Only one OpenGL context can be active at a time. This method sets
the current window's context to be current. You should use this
method in preference to `pyglet.gl.Context.set_current`, as it may
perform additional initialisation functions.
Only one OpenGL context can be active at a time. This method
sets the current window context as the active one.
In most cases, you should use this method instead of directly
calling :py:meth:`pyglet.gl.Context.set_current`. The latter
will not perform platform-specific state management tasks for
you.
"""
raise NotImplementedError('abstract')

View File

@ -21,6 +21,7 @@ NSColor = cocoapy.ObjCClass('NSColor')
NSEvent = cocoapy.ObjCClass('NSEvent')
NSArray = cocoapy.ObjCClass('NSArray')
NSImage = cocoapy.ObjCClass('NSImage')
NSPasteboard = cocoapy.ObjCClass('NSPasteboard')
quartz = cocoapy.quartz
cf = cocoapy.cf
@ -567,5 +568,32 @@ class CocoaWindow(BaseWindow):
NSApp = NSApplication.sharedApplication()
NSApp.setPresentationOptions_(options)
def set_clipboard_text(self, text: str):
with AutoReleasePool():
pasteboard = NSPasteboard.generalPasteboard()
pasteboard.clearContents()
array = NSArray.arrayWithObject_(cocoapy.NSPasteboardTypeString)
pasteboard.declareTypes_owner_(array, None)
text_nsstring = cocoapy.get_NSString(text)
pasteboard.setString_forType_(text_nsstring, cocoapy.NSPasteboardTypeString)
def get_clipboard_text(self) -> str:
text = ''
with AutoReleasePool():
pasteboard = NSPasteboard.generalPasteboard()
if pasteboard.types().containsObject_(cocoapy.NSPasteboardTypeString):
text_obj = pasteboard.stringForType_(cocoapy.NSPasteboardTypeString)
if text_obj:
text = text_obj.UTF8String().decode('utf-8')
return text
__all__ = ["CocoaWindow"]

View File

@ -653,6 +653,43 @@ class Win32Window(BaseWindow):
return icon
def set_clipboard_text(self, text: str):
valid = _user32.OpenClipboard(self._view_hwnd)
if not valid:
return
_user32.EmptyClipboard()
size = (len(text) + 1) * sizeof(WCHAR) # UTF-16
cb_data = _kernel32.GlobalAlloc(GMEM_MOVEABLE, size)
locked_data = _kernel32.GlobalLock(cb_data)
memmove(locked_data, text, size) # Trying to encode in utf-16 causes garbled text. Accepts str fine?
_kernel32.GlobalUnlock(cb_data)
_user32.SetClipboardData(CF_UNICODETEXT, cb_data)
_user32.CloseClipboard()
def get_clipboard_text(self) -> str:
text = ''
valid = _user32.OpenClipboard(self._view_hwnd)
if not valid:
print("Could not open clipboard")
return ''
cb_obj = _user32.GetClipboardData(CF_UNICODETEXT)
if cb_obj:
locked_data = _kernel32.GlobalLock(cb_obj)
if locked_data:
text = ctypes.wstring_at(locked_data)
_kernel32.GlobalUnlock(cb_obj)
_user32.CloseClipboard()
return text
# Private util
def _client_to_window_size(self, width, height):

View File

@ -4,6 +4,7 @@ import urllib.parse
from ctypes import *
from functools import lru_cache
from typing import Optional
import pyglet
@ -28,6 +29,7 @@ try:
except ImportError:
_have_xsync = False
_debug = pyglet.options['debug_x11']
class mwmhints_t(Structure):
_fields_ = [
@ -47,6 +49,7 @@ _can_detect_autorepeat = None
XA_CARDINAL = 6 # Xatom.h:14
XA_ATOM = 4
XA_STRING = 31
XDND_VERSION = 5
@ -142,6 +145,9 @@ class XlibWindow(BaseWindow):
if _can_detect_autorepeat:
self.pressed_keys = set()
# Store clipboard string to not query as much for pasting a lot.
self._clipboard_str: Optional[str] = None
def _recreate(self, changes):
# If flipping to/from fullscreen, need to recreate the window. (This
# is the case with both override_redirect method and
@ -287,6 +293,12 @@ class XlibWindow(BaseWindow):
# Atoms required for Xdnd
self._create_xdnd_atoms(self._x_display)
# Clipboard related atoms
self._clipboard_atom = xlib.XInternAtom(self._x_display, asbytes('CLIPBOARD'), False)
self._utf8_atom = xlib.XInternAtom(self._x_display, asbytes('UTF8_STRING'), False)
self._target_atom = xlib.XInternAtom(self._x_display, asbytes('TARGETS'), False)
self._incr_atom = xlib.XInternAtom(self._x_display, asbytes('INCR'), False)
# Support for drag and dropping files needs to be enabled.
if self._file_drops:
# Some variables set because there are 4 different drop events that need shared data.
@ -798,6 +810,73 @@ class XlibWindow(BaseWindow):
xlib.XChangeProperty(self._x_display, self._window, atom, XA_CARDINAL,
32, xlib.PropModeReplace, buffer, len(data)//sizeof(c_ulong))
def set_clipboard_text(self, text: str):
xlib.XSetSelectionOwner(self._x_display,
self._clipboard_atom,
self._window,
xlib.CurrentTime)
if xlib.XGetSelectionOwner(self._x_display, self._clipboard_atom) == self._window:
self._clipboard_str = text
str_bytes = text.encode('utf-8')
size = len(str_bytes)
xlib.XChangeProperty(self._x_display, self._window,
self._clipboard_atom, self._utf8_atom, 8, xlib.PropModeReplace,
(c_ubyte * size).from_buffer_copy(str_bytes), size)
else:
if _debug:
print("X11: Couldn't become owner of clipboard.")
def get_clipboard_text(self) -> str:
if self._clipboard_str is not None:
return self._clipboard_str
owner = xlib.XGetSelectionOwner(self._x_display, self._clipboard_atom)
if not owner:
return ''
text = ''
if owner == self._window:
data, size, actual_atom = self.get_single_property(self._window, self._clipboard_atom,
self._utf8_atom)
else:
notification = xlib.XEvent()
# Convert to selection notification.
xlib.XConvertSelection(self._x_display,
self._clipboard_atom,
self._utf8_atom,
self._clipboard_atom,
self._window,
xlib.CurrentTime)
while not xlib.XCheckTypedWindowEvent(self._x_display, self._window, xlib.SelectionNotify, byref(notification)):
self.dispatch_platform_event(notification)
if not notification.xselection.property:
return ''
data, size, actual_atom = self.get_single_property(notification.xselection.requestor, notification.xselection.property,
self._utf8_atom)
if actual_atom == self._incr_atom:
# Not implemented.
if _debug:
print("X11: Clipboard data is too large, not implemented.")
elif actual_atom == self._utf8_atom:
if data:
text_bytes = string_at(data, size)
text = text_bytes.decode('utf-8')
self._clipboard_str = text
xlib.XFree(data)
return text
# Private utility
def _set_wm_normal_hints(self):
@ -1328,7 +1407,7 @@ class XlibWindow(BaseWindow):
xlib.XFree(data)
def get_single_property(self, window, atom_property, atom_type):
""" Returns the length and data of a window property. """
""" Returns the length, data, and actual atom of a window property. """
actualAtom = xlib.Atom()
actualFormat = c_int()
itemCount = c_ulong()
@ -1343,14 +1422,14 @@ class XlibWindow(BaseWindow):
byref(bytesAfter),
data)
return data, itemCount.value
return data, itemCount.value, actualAtom.value
@XlibEventHandler(xlib.SelectionNotify)
def _event_selection_notification(self, ev):
if ev.xselection.property != 0 and ev.xselection.selection == self._xdnd_atoms['XdndSelection']:
if self._xdnd_format:
# This will get the data
data, count = self.get_single_property(ev.xselection.requestor,
data, count, _ = self.get_single_property(ev.xselection.requestor,
ev.xselection.property,
ev.xselection.target)
@ -1514,5 +1593,61 @@ class XlibWindow(BaseWindow):
self._mapped = False
self.dispatch_event('on_hide')
@XlibEventHandler(xlib.SelectionClear)
def _event_selection_clear(self, ev):
if ev.xselectionclear.selection == self._clipboard_atom:
# Another application cleared the clipboard.
self._clipboard_str = None
@XlibEventHandler(xlib.SelectionRequest)
def _event_selection_request(self, ev):
request = ev.xselectionrequest
if _debug:
rt = xlib.XGetAtomName(self._x_display, request.target)
rp = xlib.XGetAtomName(self._x_display, request.property)
print(f"X11 debug: request target {rt}")
print(f"X11 debug: request property {rp}")
out_event = xlib.XEvent()
out_event.xany.type = xlib.SelectionNotify
out_event.xselection.selection = request.selection
out_event.xselection.display = request.display
out_event.xselection.target = 0
out_event.xselection.property = 0
out_event.xselection.requestor = request.requestor
out_event.xselection.time = request.time
if (xlib.XGetSelectionOwner(self._x_display, self._clipboard_atom) == self._window and
ev.xselection.target == self._clipboard_atom):
if request.target == self._target_atom:
atoms_ar = (xlib.Atom * 1)(self._utf8_atom)
ptr = cast(pointer(atoms_ar), POINTER(c_ubyte))
xlib.XChangeProperty(self._x_display, request.requestor,
request.property, XA_ATOM, 32,
xlib.PropModeReplace,
ptr, sizeof(atoms_ar)//sizeof(c_ulong))
out_event.xselection.property = request.property
out_event.xselection.target = request.target
elif request.target == self._utf8_atom:
# We are being requested for a UTF-8 string.
text = self._clipboard_str.encode('utf-8')
size = len(self._clipboard_str)
xlib.XChangeProperty(self._x_display, request.requestor,
request.property, request.target, 8,
xlib.PropModeReplace,
(c_ubyte * size).from_buffer_copy(text), size)
out_event.xselection.property = request.property
out_event.xselection.target = request.target
# Send request event back to requestor with updated changes.
xlib.XSendEvent(self._x_display, request.requestor, 0, 0, byref(out_event))
# Seems to work find without it. May add later.
#xlib.XSync(self._x_display, False)
__all__ = ["XlibEventHandler", "XlibWindow"]

View File

@ -1086,9 +1086,6 @@ def test_logger(the_logger: Logger):
the_logger.debug('debugging')
the_logger.info("Hello World!!")
the_logger.info("Hello World!!")
the_logger.info("Hello World!!")
the_logger.info("Hello World!!")
the_logger.info("Hello World!!")
the_logger.warn('warning')
the_logger.warn('warning')
the_logger.error('error haaaa')
@ -1111,9 +1108,9 @@ if __name__ == "__main__":
a_logger.error('error haaaa')
a_logger.fatal('oh no')
logger.info('my name is:', logger.name)
for _ in range(5):
test_logger(logger)
test_logger(a_logger)
# for _ in range(5):
test_logger(logger)
test_logger(a_logger)
print(Message_content(log_time=time.time(), text='aaa', level=4, marker='abc', end='abc', flush=False,
frame=inspect.currentframe()))
print(ColorCodeEnum.code_line.name)

View File

@ -1,7 +0,0 @@
# -------------------------------
# Difficult Rocket
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
# All rights reserved
# -------------------------------

View File

@ -1,9 +0,0 @@
# -------------------------------
# Difficult Rocket
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
# All rights reserved
# -------------------------------
from lib_not_dr.types.options import Options

View File

@ -1,31 +0,0 @@
# -------------------------------
# Difficult Rocket
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
# All rights reserved
# -------------------------------
import time
from types import FrameType
from typing import List, Optional
from lib_not_dr.types.options import Options
class LogMessage(Options):
name = 'LogMessage'
# 消息内容本身的属性
messages: List[str] = []
end: str = '\n'
split: str = ' '
# 消息的属性
flush: bool = True
level: int = 20
log_time: float = time.time_ns()
logger_name: str = 'root'
logger_tag: Optional[str] = None
stack_trace: Optional[FrameType] = None

View File

@ -116,7 +116,7 @@ dependencies = [
[[package]]
name = "difficult_rocket_rs"
version = "0.2.22"
version = "0.2.23"
dependencies = [
"pyo3",
"quick-xml",
@ -415,9 +415,9 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.30.0"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"memchr",
"serde",
@ -497,9 +497,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.186"
version = "1.0.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1"
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
dependencies = [
"serde_derive",
]
@ -518,9 +518,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.186"
version = "1.0.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670"
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
dependencies = [
"proc-macro2",
"quote",
@ -708,6 +708,6 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "xml-rs"
version = "0.8.16"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47430998a7b5d499ccee752b41567bc3afc57e1327dc855b1a2aa44ce29b5fa1"
checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a"

View File

@ -1,6 +1,6 @@
[package]
name = "difficult_rocket_rs"
version = "0.2.22"
version = "0.2.23"
edition = "2021"
license-file = '../../LICENSE'
authors = [
@ -22,15 +22,15 @@ opt-level = "s"
# codegen-units = 1
[dependencies.quick-xml]
version = "0.30.0"
version = "0.31.0"
features = ["serialize"]
[dependencies.serde]
version = "1.0.186"
version = "1.0.190"
features = ["derive"]
[dependencies.xml-rs]
version = "0.8.16"
version = "0.8.19"
[dependencies.serde-xml-rs]
version = "0.6.0"

View File

@ -16,7 +16,7 @@ from Difficult_Rocket.api.mod import ModInfo
from Difficult_Rocket.client import ClientWindow
from Difficult_Rocket.api.types import Options, Version
DR_rust_version = Version("0.2.22.0") # DR_mod 的 Rust 编写部分的兼容版本
DR_rust_version = Version("0.2.23.0") # DR_mod 的 Rust 编写部分的兼容版本
logger = logging.getLogger('client.dr_game')

View File

@ -47,14 +47,10 @@ if __name__ == '__main__':
if '--report' in sys.argv:
compiler.save_report = True
sys.argv.remove('--report')
# 检测 --output xx 参数
if '--output' in sys.argv:
# 输入的是输出目录
out_path = sys.argv[sys.argv.index('--output') + 1]
compiler.output_path = Path(out_path)
sys.argv.remove('--output')
sys.argv.remove(out_path)
if '--lto=yes' in sys.argv:
compiler.use_lto = True
sys.argv.remove('--lto=yes')
# 检测 --no-pyglet-opt 参数
pyglet_optimizations = True
@ -88,6 +84,16 @@ if __name__ == '__main__':
pprint(compiler.option())
else:
compiler.output_path = Path(f'./build/nuitka-{platform.system().lower()}')
compiler.show_memory = False
compiler.show_progress = False
# 检测 --output xx 参数
if '--output' in sys.argv:
# 输入的是输出目录
out_path = sys.argv[sys.argv.index('--output') + 1]
compiler.output_path = Path(out_path)
sys.argv.remove('--output')
sys.argv.remove(out_path)
print(compiler.as_markdown())

View File

@ -4,23 +4,22 @@
# 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"
pillow >= 10.0.1; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
# for sys info
psutil >= 5.9.5
psutil >= 5.9.6
# for files
rtoml >= 0.9.0
tomlkit >= 0.11.8
tomlkit >= 0.12.1
defusedxml >= 0.7.1
# for report error
objprint >= 0.2.2
objprint >= 0.2.3
# for compile
nuitka >= 1.8.2
ordered-set >= 4.1.0
imageio >= 2.31.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
wheel >= 0.40.0
setuptools >= 67.8.0
setuptools-rust >= 1.6.0
nuitka >= 1.8.5
imageio >= 2.31.6; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
wheel >= 0.41.3
setuptools >= 68.2.2
setuptools-rust >= 1.8.1

View File

@ -5,25 +5,24 @@
# 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"
pillow >= 10.0.1; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
# for sys info
psutil >= 5.9.5
psutil >= 5.9.6
# for files
rtoml >= 0.9.0
tomlkit >= 0.11.8
tomlkit >= 0.12.1
defusedxml >= 0.7.1
# for debug
objprint >= 0.2.2
viztracer >= 0.15.6; platform_python_implementation != "PyPy"
objprint >= 0.2.3
viztracer >= 0.16.0; platform_python_implementation != "PyPy"
vizplugins >= 0.1.3; platform_python_implementation != "PyPy"
# for compile
nuitka >= 1.8.2
ordered-set >= 4.1.0
imageio >= 2.31.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
wheel >= 0.40.0
setuptools >= 67.8.0
setuptools-rust >= 1.6.0
nuitka >= 1.8.5
imageio >= 2.31.6; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
wheel >= 0.41.3
setuptools >= 68.2.2
setuptools-rust >= 1.8.1

View File

@ -3,15 +3,15 @@
# 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"
pillow >= 10.0.1; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
# for sys info
psutil >= 5.9.5
psutil >= 5.9.6
# for files
rtoml >= 0.9.0
tomlkit >= 0.11.8
tomlkit >= 0.12.1
defusedxml >= 0.7.1
# for report error
objprint >= 0.2.2
objprint >= 0.2.3