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:
parent
9e9c7d5e16
commit
b3c52d2a3f
3
DR.py
3
DR.py
@ -52,7 +52,8 @@ def start(start_time_ns: int) -> None:
|
|||||||
print(crash.all_process)
|
print(crash.all_process)
|
||||||
for a_thread in threading.enumerate():
|
for a_thread in threading.enumerate():
|
||||||
print(a_thread)
|
print(a_thread)
|
||||||
if a_thread.is_alive() and a_thread != threading.current_thread() and a_thread != threading.main_thread():
|
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
|
a_thread.join(2) # wait for 2 sec
|
||||||
import pyglet
|
import pyglet
|
||||||
pyglet.app.exit() # make sure that pyglet has stopped
|
pyglet.app.exit() # make sure that pyglet has stopped
|
||||||
|
@ -7,8 +7,8 @@ fonts_folder = "assets/fonts"
|
|||||||
|
|
||||||
[window]
|
[window]
|
||||||
style = "None"
|
style = "None"
|
||||||
width = 1112
|
width = 1312
|
||||||
height = 793
|
height = 915
|
||||||
visible = true
|
visible = true
|
||||||
gui_scale = 1
|
gui_scale = 1
|
||||||
caption = "Difficult Rocket v{DR_version}"
|
caption = "Difficult Rocket v{DR_version}"
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
|
|
||||||
# DR game/DR rs 更新日志
|
# DR game/DR rs 更新日志
|
||||||
|
|
||||||
- 最新版本号
|
- 最新版本号
|
||||||
- DR game: 0.3.3.0
|
- 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
|
## 20230825 DR rs 0.2.22.0
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# DR SDK 更新日志
|
# DR SDK 更新日志
|
||||||
|
|
||||||
- 最新版本号
|
- 最新版本号
|
||||||
@ -14,6 +13,20 @@
|
|||||||
- 不再同时维护两份代码
|
- 不再同时维护两份代码
|
||||||
- No longer maintain two sets of code at the same time
|
- 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
|
## DR sdk 0.8.7.2
|
||||||
|
|
||||||
### Add
|
### Add
|
||||||
|
@ -4,4 +4,4 @@
|
|||||||
# All rights reserved
|
# All rights reserved
|
||||||
# -------------------------------
|
# -------------------------------
|
||||||
|
|
||||||
__version__ = '0.1.7'
|
__version__ = '0.1.8'
|
||||||
|
33
libs/lib_not_dr/logger/__init__.py
Normal file
33
libs/lib_not_dr/logger/__init__.py
Normal 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
|
285
libs/lib_not_dr/logger/formatter/__init__.py
Normal file
285
libs/lib_not_dr/logger/formatter/__init__.py
Normal 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())
|
256
libs/lib_not_dr/logger/formatter/colors.py
Normal file
256
libs/lib_not_dr/logger/formatter/colors.py
Normal 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
|
267
libs/lib_not_dr/logger/logger.py
Normal file
267
libs/lib_not_dr/logger/logger.py
Normal 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)
|
236
libs/lib_not_dr/logger/outstream.py
Normal file
236
libs/lib_not_dr/logger/outstream.py
Normal 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
|
99
libs/lib_not_dr/logger/structure.py
Normal file
99
libs/lib_not_dr/logger/structure.py
Normal 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())
|
@ -64,6 +64,7 @@ class Options:
|
|||||||
"""
|
"""
|
||||||
name = 'Option Base'
|
name = 'Option Base'
|
||||||
cached_options: Dict[str, Union[str, Any]] = {}
|
cached_options: Dict[str, Union[str, Any]] = {}
|
||||||
|
_check_options: bool = True
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -75,7 +76,7 @@ class Options:
|
|||||||
self._options: Dict[str, Union[Callable, object]] = {}
|
self._options: Dict[str, Union[Callable, object]] = {}
|
||||||
self.flush_option()
|
self.flush_option()
|
||||||
for option, value in kwargs.items():
|
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")
|
raise OptionNameNotDefined(f"option: {option} with value: {value} is not defined")
|
||||||
setattr(self, option, value)
|
setattr(self, option, value)
|
||||||
run_load_file = True
|
run_load_file = True
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
"""Holds type aliases used throughout the codebase."""
|
"""Holds type aliases used throughout the codebase."""
|
||||||
import ctypes
|
import ctypes
|
||||||
|
import sys
|
||||||
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Buffer"
|
"Buffer",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Backwards compatible placeholder for `collections.abc.Buffer` from Python 3.12
|
|
||||||
|
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]
|
Buffer = Union[bytes, bytearray, memoryview, ctypes.Array]
|
||||||
|
6
libs/pyglet/experimental/README.md
Normal file
6
libs/pyglet/experimental/README.md
Normal 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.
|
@ -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.
|
|
||||||
"""
|
|
@ -148,27 +148,15 @@ fragment_array_source = """#version 150 core
|
|||||||
|
|
||||||
|
|
||||||
def get_default_shader():
|
def get_default_shader():
|
||||||
try:
|
return pyglet.gl.current_context.create_program((vertex_source, 'vertex'),
|
||||||
return pyglet.gl.current_context.pyglet_sprite_default_shader
|
(geometry_source, 'geometry'),
|
||||||
except AttributeError:
|
(fragment_source, 'fragment'))
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def get_default_array_shader():
|
def get_default_array_shader():
|
||||||
try:
|
return pyglet.gl.current_context.create_program((vertex_source, 'vertex'),
|
||||||
return pyglet.gl.current_context.pyglet_sprite_default_array_shader
|
(geometry_source, 'geometry'),
|
||||||
except AttributeError:
|
(fragment_array_source, 'fragment'))
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class SpriteGroup(graphics.Group):
|
class SpriteGroup(graphics.Group):
|
||||||
|
247
libs/pyglet/experimental/net.py
Normal file
247
libs/pyglet/experimental/net.py
Normal 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')
|
@ -266,30 +266,29 @@ class DWRITE_CLUSTER_METRICS(ctypes.Structure):
|
|||||||
class IDWriteFontFileStream(com.IUnknown):
|
class IDWriteFontFileStream(com.IUnknown):
|
||||||
_methods_ = [
|
_methods_ = [
|
||||||
('ReadFileFragment',
|
('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',
|
('ReleaseFileFragment',
|
||||||
com.STDMETHOD(c_void_p, c_void_p)),
|
com.STDMETHOD(c_void_p)),
|
||||||
('GetFileSize',
|
('GetFileSize',
|
||||||
com.STDMETHOD(c_void_p, POINTER(UINT64))),
|
com.STDMETHOD(POINTER(UINT64))),
|
||||||
('GetLastWriteTime',
|
('GetLastWriteTime',
|
||||||
com.STDMETHOD(c_void_p, POINTER(UINT64))),
|
com.STDMETHOD(POINTER(UINT64))),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class IDWriteFontFileLoader_LI(com.IUnknown): # Local implementation use only.
|
class IDWriteFontFileLoader_LI(com.IUnknown): # Local implementation use only.
|
||||||
_methods_ = [
|
_methods_ = [
|
||||||
('CreateStreamFromKey',
|
('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):
|
class IDWriteFontFileLoader(com.pIUnknown):
|
||||||
_methods_ = [
|
_methods_ = [
|
||||||
('CreateStreamFromKey',
|
('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):
|
class IDWriteLocalFontFileLoader(IDWriteFontFileLoader, com.pIUnknown):
|
||||||
_methods_ = [
|
_methods_ = [
|
||||||
('GetFilePathLengthFromKey',
|
('GetFilePathLengthFromKey',
|
||||||
@ -452,13 +451,13 @@ DWRITE_READING_DIRECTION_LEFT_TO_RIGHT = 0
|
|||||||
class IDWriteTextAnalysisSource(com.IUnknown):
|
class IDWriteTextAnalysisSource(com.IUnknown):
|
||||||
_methods_ = [
|
_methods_ = [
|
||||||
('GetTextAtPosition',
|
('GetTextAtPosition',
|
||||||
com.METHOD(HRESULT, c_void_p, UINT32, POINTER(c_wchar_p), POINTER(UINT32))),
|
com.STDMETHOD(UINT32, POINTER(c_wchar_p), POINTER(UINT32))),
|
||||||
('GetTextBeforePosition',
|
('GetTextBeforePosition',
|
||||||
com.STDMETHOD(UINT32, c_wchar_p, POINTER(UINT32))),
|
com.STDMETHOD(UINT32, POINTER(c_wchar_p), POINTER(UINT32))),
|
||||||
('GetParagraphReadingDirection',
|
('GetParagraphReadingDirection',
|
||||||
com.METHOD(DWRITE_READING_DIRECTION)),
|
com.METHOD(DWRITE_READING_DIRECTION)),
|
||||||
('GetLocaleName',
|
('GetLocaleName',
|
||||||
com.STDMETHOD(c_void_p, UINT32, POINTER(UINT32), POINTER(c_wchar_p))),
|
com.STDMETHOD(UINT32, POINTER(UINT32), POINTER(c_wchar_p))),
|
||||||
('GetNumberSubstitution',
|
('GetNumberSubstitution',
|
||||||
com.STDMETHOD(UINT32, POINTER(UINT32), c_void_p)),
|
com.STDMETHOD(UINT32, POINTER(UINT32), c_void_p)),
|
||||||
]
|
]
|
||||||
@ -467,7 +466,7 @@ class IDWriteTextAnalysisSource(com.IUnknown):
|
|||||||
class IDWriteTextAnalysisSink(com.IUnknown):
|
class IDWriteTextAnalysisSink(com.IUnknown):
|
||||||
_methods_ = [
|
_methods_ = [
|
||||||
('SetScriptAnalysis',
|
('SetScriptAnalysis',
|
||||||
com.STDMETHOD(c_void_p, UINT32, UINT32, POINTER(DWRITE_SCRIPT_ANALYSIS))),
|
com.STDMETHOD(UINT32, UINT32, POINTER(DWRITE_SCRIPT_ANALYSIS))),
|
||||||
('SetLineBreakpoints',
|
('SetLineBreakpoints',
|
||||||
com.STDMETHOD(UINT32, UINT32, c_void_p)),
|
com.STDMETHOD(UINT32, UINT32, c_void_p)),
|
||||||
('SetBidiLevel',
|
('SetBidiLevel',
|
||||||
@ -524,7 +523,7 @@ class TextAnalysis(com.COMObject):
|
|||||||
|
|
||||||
analyzer.AnalyzeScript(self, 0, text_length, self)
|
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
|
# 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
|
# 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.
|
# scriptAnalysis - The analysis information for all glyphs starting at position for length.
|
||||||
@ -542,10 +541,10 @@ class TextAnalysis(com.COMObject):
|
|||||||
return 0
|
return 0
|
||||||
# return 0x80004001
|
# return 0x80004001
|
||||||
|
|
||||||
def GetTextBeforePosition(self, this, textPosition, textString, textLength):
|
def GetTextBeforePosition(self, textPosition, textString, textLength):
|
||||||
raise Exception("Currently not implemented.")
|
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
|
# This method will retrieve a substring of the text in this layout
|
||||||
# to be used in an analysis step.
|
# to be used in an analysis step.
|
||||||
# Arguments:
|
# Arguments:
|
||||||
@ -568,7 +567,7 @@ class TextAnalysis(com.COMObject):
|
|||||||
def GetParagraphReadingDirection(self):
|
def GetParagraphReadingDirection(self):
|
||||||
return 0
|
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.
|
self.__local_name = c_wchar_p("") # TODO: Add more locales.
|
||||||
localeName[0] = self.__local_name
|
localeName[0] = self.__local_name
|
||||||
textLength[0] = self._textlength - textPosition
|
textLength[0] = self._textlength - textPosition
|
||||||
@ -954,16 +953,16 @@ class IDWriteTextLayout1(IDWriteTextLayout, IDWriteTextFormat, com.pIUnknown):
|
|||||||
class IDWriteFontFileEnumerator(com.IUnknown):
|
class IDWriteFontFileEnumerator(com.IUnknown):
|
||||||
_methods_ = [
|
_methods_ = [
|
||||||
('MoveNext',
|
('MoveNext',
|
||||||
com.STDMETHOD(c_void_p, POINTER(BOOL))),
|
com.STDMETHOD(POINTER(BOOL))),
|
||||||
('GetCurrentFontFile',
|
('GetCurrentFontFile',
|
||||||
com.STDMETHOD(c_void_p, c_void_p)),
|
com.STDMETHOD(c_void_p)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class IDWriteFontCollectionLoader(com.IUnknown):
|
class IDWriteFontCollectionLoader(com.IUnknown):
|
||||||
_methods_ = [
|
_methods_ = [
|
||||||
('CreateEnumeratorFromKey',
|
('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]
|
_interfaces_ = [IDWriteFontFileStream]
|
||||||
|
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
|
super().__init__()
|
||||||
self._data = data
|
self._data = data
|
||||||
self._size = len(data)
|
self._size = len(data)
|
||||||
self._ptrs = []
|
self._ptrs = []
|
||||||
|
|
||||||
def AddRef(self, this):
|
def ReadFileFragment(self, fragmentStart, fileOffset, fragmentSize, fragmentContext):
|
||||||
return 1
|
|
||||||
|
|
||||||
def Release(self, this):
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def QueryInterface(self, this, refiid, tester):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def ReadFileFragment(self, this, fragmentStart, fileOffset, fragmentSize, fragmentContext):
|
|
||||||
if fileOffset + fragmentSize > self._size:
|
if fileOffset + fragmentSize > self._size:
|
||||||
return 0x80004005 # E_FAIL
|
return 0x80004005 # E_FAIL
|
||||||
|
|
||||||
@ -997,14 +988,14 @@ class MyFontFileStream(com.COMObject):
|
|||||||
fragmentContext[0] = None
|
fragmentContext[0] = None
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def ReleaseFileFragment(self, this, fragmentContext):
|
def ReleaseFileFragment(self, fragmentContext):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def GetFileSize(self, this, fileSize):
|
def GetFileSize(self, fileSize):
|
||||||
fileSize[0] = self._size
|
fileSize[0] = self._size
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def GetLastWriteTime(self, this, lastWriteTime):
|
def GetLastWriteTime(self, lastWriteTime):
|
||||||
return 0x80004001 # E_NOTIMPL
|
return 0x80004001 # E_NOTIMPL
|
||||||
|
|
||||||
|
|
||||||
@ -1012,21 +1003,13 @@ class LegacyFontFileLoader(com.COMObject):
|
|||||||
_interfaces_ = [IDWriteFontFileLoader_LI]
|
_interfaces_ = [IDWriteFontFileLoader_LI]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self._streams = {}
|
self._streams = {}
|
||||||
|
|
||||||
def QueryInterface(self, this, refiid, tester):
|
def CreateStreamFromKey(self, fontfileReferenceKey, fontFileReferenceKeySize, fontFileStream):
|
||||||
return 0
|
|
||||||
|
|
||||||
def AddRef(self, this):
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def Release(self, this):
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def CreateStreamFromKey(self, this, fontfileReferenceKey, fontFileReferenceKeySize, fontFileStream):
|
|
||||||
convert_index = cast(fontfileReferenceKey, POINTER(c_uint32))
|
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))
|
POINTER(IDWriteFontFileStream))
|
||||||
fontFileStream[0] = self._ptr
|
fontFileStream[0] = self._ptr
|
||||||
return 0
|
return 0
|
||||||
@ -1039,6 +1022,7 @@ class MyEnumerator(com.COMObject):
|
|||||||
_interfaces_ = [IDWriteFontFileEnumerator]
|
_interfaces_ = [IDWriteFontFileEnumerator]
|
||||||
|
|
||||||
def __init__(self, factory, loader):
|
def __init__(self, factory, loader):
|
||||||
|
super().__init__()
|
||||||
self.factory = cast(factory, IDWriteFactory)
|
self.factory = cast(factory, IDWriteFactory)
|
||||||
self.key = "pyglet_dwrite"
|
self.key = "pyglet_dwrite"
|
||||||
self.size = len(self.key)
|
self.size = len(self.key)
|
||||||
@ -1057,7 +1041,7 @@ class MyEnumerator(com.COMObject):
|
|||||||
def AddFontData(self, fonts):
|
def AddFontData(self, fonts):
|
||||||
self._font_data = fonts
|
self._font_data = fonts
|
||||||
|
|
||||||
def MoveNext(self, this, hasCurrentFile):
|
def MoveNext(self, hasCurrentFile):
|
||||||
|
|
||||||
self.current_index += 1
|
self.current_index += 1
|
||||||
if self.current_index != len(self._font_data):
|
if self.current_index != len(self._font_data):
|
||||||
@ -1087,7 +1071,7 @@ class MyEnumerator(com.COMObject):
|
|||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def GetCurrentFontFile(self, this, fontFile):
|
def GetCurrentFontFile(self, fontFile):
|
||||||
fontFile = cast(fontFile, POINTER(IDWriteFontFile))
|
fontFile = cast(fontFile, POINTER(IDWriteFontFile))
|
||||||
fontFile[0] = self._font_files[self.current_index]
|
fontFile[0] = self._font_files[self.current_index]
|
||||||
return 0
|
return 0
|
||||||
@ -1097,24 +1081,14 @@ class LegacyCollectionLoader(com.COMObject):
|
|||||||
_interfaces_ = [IDWriteFontCollectionLoader]
|
_interfaces_ = [IDWriteFontCollectionLoader]
|
||||||
|
|
||||||
def __init__(self, factory, loader):
|
def __init__(self, factory, loader):
|
||||||
|
super().__init__()
|
||||||
self._enumerator = MyEnumerator(factory, loader)
|
self._enumerator = MyEnumerator(factory, loader)
|
||||||
|
|
||||||
def AddFontData(self, fonts):
|
def AddFontData(self, fonts):
|
||||||
self._enumerator.AddFontData(fonts)
|
self._enumerator.AddFontData(fonts)
|
||||||
|
|
||||||
def AddRef(self, this):
|
def CreateEnumeratorFromKey(self, factory, key, key_size, enumerator):
|
||||||
self._i = 1
|
self._ptr = ctypes.cast(self._enumerator.as_interface(IDWriteFontFileEnumerator),
|
||||||
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],
|
|
||||||
POINTER(IDWriteFontFileEnumerator))
|
POINTER(IDWriteFontFileEnumerator))
|
||||||
|
|
||||||
enumerator[0] = self._ptr
|
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.
|
# Note: RegisterFontLoader takes a pointer. However, for legacy we implement our own callback interface.
|
||||||
# Therefore we need to pass to the actual pointer directly.
|
# 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._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader)
|
||||||
cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_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._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader)
|
||||||
|
|
||||||
cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_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)
|
cls._font_collection_loader.AddFontData(cls._font_cache)
|
||||||
|
|
||||||
|
@ -336,7 +336,7 @@ class GDIPlusGlyphRenderer(Win32GlyphRenderer):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def _create_bitmap(self, width, height):
|
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._bitmap = ctypes.c_void_p()
|
||||||
self._format = PixelFormat32bppARGB
|
self._format = PixelFormat32bppARGB
|
||||||
gdiplus.GdipCreateBitmapFromScan0(width, height, width * 4,
|
gdiplus.GdipCreateBitmapFromScan0(width, height, width * 4,
|
||||||
@ -532,6 +532,12 @@ class GDIPlusFont(Win32Font):
|
|||||||
self._name = name
|
self._name = name
|
||||||
|
|
||||||
family = ctypes.c_void_p()
|
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)
|
name = ctypes.c_wchar_p(name)
|
||||||
|
|
||||||
# Look in private collection first:
|
# Look in private collection first:
|
||||||
@ -540,6 +546,9 @@ class GDIPlusFont(Win32Font):
|
|||||||
|
|
||||||
# Then in system collection:
|
# Then in system collection:
|
||||||
if not family:
|
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))
|
gdiplus.GdipCreateFontFamilyFromName(name, None, ctypes.byref(family))
|
||||||
|
|
||||||
# Nothing found, use default font.
|
# Nothing found, use default font.
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import weakref
|
import weakref
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
import threading
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
import pyglet
|
import pyglet
|
||||||
@ -209,11 +210,12 @@ class CanvasConfig(Config):
|
|||||||
|
|
||||||
class ObjectSpace:
|
class ObjectSpace:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Textures and buffers scheduled for deletion
|
# Objects scheduled for deletion the next time this object space is active.
|
||||||
# the next time this object space is active.
|
|
||||||
self.doomed_textures = []
|
self.doomed_textures = []
|
||||||
self.doomed_buffers = []
|
self.doomed_buffers = []
|
||||||
self.doomed_shader_programs = []
|
self.doomed_shader_programs = []
|
||||||
|
self.doomed_shaders = []
|
||||||
|
self.doomed_renderbuffers = []
|
||||||
|
|
||||||
|
|
||||||
class Context:
|
class Context:
|
||||||
@ -236,6 +238,7 @@ class Context:
|
|||||||
self.canvas = None
|
self.canvas = None
|
||||||
|
|
||||||
self.doomed_vaos = []
|
self.doomed_vaos = []
|
||||||
|
self.doomed_framebuffers = []
|
||||||
|
|
||||||
if context_share:
|
if context_share:
|
||||||
self.object_space = context_share.object_space
|
self.object_space = context_share.object_space
|
||||||
@ -277,28 +280,49 @@ class Context:
|
|||||||
self._info = gl_info.GLInfo()
|
self._info = gl_info.GLInfo()
|
||||||
self._info.set_active_context()
|
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:
|
if self.object_space.doomed_textures:
|
||||||
textures = self.object_space.doomed_textures[:]
|
self._delete_objects(self.object_space.doomed_textures, gl.glDeleteTextures)
|
||||||
textures = (gl.GLuint * len(textures))(*textures)
|
|
||||||
gl.glDeleteTextures(len(textures), textures)
|
|
||||||
self.object_space.doomed_textures.clear()
|
|
||||||
if self.object_space.doomed_buffers:
|
if self.object_space.doomed_buffers:
|
||||||
buffers = self.object_space.doomed_buffers[:]
|
self._delete_objects(self.object_space.doomed_buffers, gl.glDeleteBuffers)
|
||||||
buffers = (gl.GLuint * len(buffers))(*buffers)
|
|
||||||
gl.glDeleteBuffers(len(buffers), buffers)
|
|
||||||
self.object_space.doomed_buffers.clear()
|
|
||||||
if self.object_space.doomed_shader_programs:
|
if self.object_space.doomed_shader_programs:
|
||||||
for program_id in self.object_space.doomed_shader_programs:
|
self._delete_objects_one_by_one(self.object_space.doomed_shader_programs,
|
||||||
gl.glDeleteProgram(program_id)
|
gl.glDeleteProgram)
|
||||||
self.object_space.doomed_shader_programs.clear()
|
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:
|
if self.doomed_vaos:
|
||||||
vaos = self.doomed_vaos[:]
|
self._delete_objects(self.doomed_vaos, gl.glDeleteVertexArrays)
|
||||||
vaos = (gl.GLuint * len(vaos))(*vaos)
|
if self.doomed_framebuffers:
|
||||||
gl.glDeleteVertexArrays(len(vaos), vaos)
|
self._delete_objects(self.doomed_framebuffers, gl.glDeleteFramebuffers)
|
||||||
self.doomed_vaos.clear()
|
|
||||||
|
# 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):
|
def destroy(self):
|
||||||
"""Release the context.
|
"""Release the context.
|
||||||
@ -318,6 +342,27 @@ class Context:
|
|||||||
if gl._shadow_window is not None:
|
if gl._shadow_window is not None:
|
||||||
gl._shadow_window.switch_to()
|
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):
|
def create_program(self, *sources: Tuple[str, str], program_class=None):
|
||||||
"""Create a ShaderProgram from OpenGL GLSL source.
|
"""Create a ShaderProgram from OpenGL GLSL source.
|
||||||
|
|
||||||
@ -347,25 +392,30 @@ class Context:
|
|||||||
return program
|
return program
|
||||||
|
|
||||||
def delete_texture(self, texture_id):
|
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
|
This method will delete the texture immediately via
|
||||||
``glDeleteTextures``, however if another context that does not share
|
``glDeleteTextures`` if the current context's object space is the same
|
||||||
this context's object space is currently active, the deletion will
|
as this context's object space and it is called from the main thread.
|
||||||
be deferred until an appropriate context is activated.
|
|
||||||
|
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:
|
:Parameters:
|
||||||
`texture_id` : int
|
`texture_id` : int
|
||||||
The OpenGL name of the Texture to delete.
|
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))
|
gl.glDeleteTextures(1, gl.GLuint(texture_id))
|
||||||
else:
|
else:
|
||||||
self.object_space.doomed_textures.append(texture_id)
|
self.object_space.doomed_textures.append(texture_id)
|
||||||
|
|
||||||
def delete_buffer(self, buffer_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
|
This method behaves similarly to `delete_texture`, though for
|
||||||
``glDeleteBuffers`` instead of ``glDeleteTextures``.
|
``glDeleteBuffers`` instead of ``glDeleteTextures``.
|
||||||
@ -376,30 +426,14 @@ class Context:
|
|||||||
|
|
||||||
.. versionadded:: 1.1
|
.. 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))
|
gl.glDeleteBuffers(1, gl.GLuint(buffer_id))
|
||||||
else:
|
else:
|
||||||
self.object_space.doomed_buffers.append(buffer_id)
|
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):
|
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
|
This method behaves similarly to `delete_texture`, though for
|
||||||
``glDeleteProgram`` instead of ``glDeleteTextures``.
|
``glDeleteProgram`` instead of ``glDeleteTextures``.
|
||||||
@ -410,11 +444,84 @@ class Context:
|
|||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
if gl.current_context is self:
|
if self._safe_to_operate_on_object_space():
|
||||||
gl.glDeleteProgram(program_id)
|
gl.glDeleteProgram(gl.GLuint(program_id))
|
||||||
else:
|
else:
|
||||||
self.object_space.doomed_shader_programs.append(program_id)
|
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):
|
def get_info(self):
|
||||||
"""Get the OpenGL information for this context.
|
"""Get the OpenGL information for this context.
|
||||||
|
|
||||||
|
@ -62,8 +62,7 @@ def errcheck(result, func, arguments):
|
|||||||
print(name)
|
print(name)
|
||||||
|
|
||||||
from pyglet import gl
|
from pyglet import gl
|
||||||
context = gl.current_context
|
if not gl.current_context:
|
||||||
if not context:
|
|
||||||
raise GLException('No GL context; create a Window first')
|
raise GLException('No GL context; create a Window first')
|
||||||
error = gl.glGetError()
|
error = gl.glGetError()
|
||||||
if error:
|
if error:
|
||||||
|
@ -244,7 +244,7 @@ class Win32ARBContext(_BaseWin32Context):
|
|||||||
if self.config.forward_compatible:
|
if self.config.forward_compatible:
|
||||||
flags |= wglext_arb.WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB
|
flags |= wglext_arb.WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB
|
||||||
if self.config.debug:
|
if self.config.debug:
|
||||||
flags |= wglext_arb.WGL_DEBUG_BIT_ARB
|
flags |= wglext_arb.WGL_CONTEXT_DEBUG_BIT_ARB
|
||||||
if flags:
|
if flags:
|
||||||
attribs.extend([wglext_arb.WGL_CONTEXT_FLAGS_ARB, flags])
|
attribs.extend([wglext_arb.WGL_CONTEXT_FLAGS_ARB, flags])
|
||||||
attribs.append(0)
|
attribs.append(0)
|
||||||
|
@ -55,10 +55,11 @@ def draw(size, mode, **data):
|
|||||||
assert size == len(array) // attribute.count, 'Data for %s is incorrect length' % fmt
|
assert size == len(array) // attribute.count, 'Data for %s is incorrect length' % fmt
|
||||||
|
|
||||||
buffer = BufferObject(size * attribute.stride)
|
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.enable()
|
||||||
attribute.set_pointer(buffer.ptr)
|
attribute.set_pointer(buffer.ptr)
|
||||||
|
|
||||||
buffers.append(buffer) # Don't garbage collect it.
|
buffers.append(buffer) # Don't garbage collect it.
|
||||||
|
|
||||||
glDrawArrays(mode, 0, size)
|
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
|
assert size == len(array) // attribute.count, 'Data for %s is incorrect length' % fmt
|
||||||
|
|
||||||
buffer = BufferObject(size * attribute.stride)
|
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.enable()
|
||||||
attribute.set_pointer(buffer.ptr)
|
attribute.set_pointer(buffer.ptr)
|
||||||
buffers.append(buffer)
|
buffers.append(buffer) # Don't garbage collect it.
|
||||||
|
|
||||||
if size <= 0xff:
|
if size <= 0xff:
|
||||||
index_type = GL_UNSIGNED_BYTE
|
index_type = GL_UNSIGNED_BYTE
|
||||||
|
@ -170,14 +170,14 @@ class Attribute:
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.location = location
|
self.location = location
|
||||||
self.count = count
|
self.count = count
|
||||||
|
|
||||||
self.gl_type = gl_type
|
self.gl_type = gl_type
|
||||||
self.c_type = _c_types[gl_type]
|
|
||||||
self.normalize = normalize
|
self.normalize = normalize
|
||||||
|
|
||||||
self.align = sizeof(self.c_type)
|
self.c_type = _c_types[gl_type]
|
||||||
self.size = count * self.align
|
|
||||||
self.stride = self.size
|
self.element_size = sizeof(self.c_type)
|
||||||
|
self.byte_size = count * self.element_size
|
||||||
|
self.stride = self.byte_size
|
||||||
|
|
||||||
def enable(self):
|
def enable(self):
|
||||||
"""Enable the attribute."""
|
"""Enable the attribute."""
|
||||||
@ -207,20 +207,16 @@ class Attribute:
|
|||||||
will be ``3 * 4 = 12``.
|
will be ``3 * 4 = 12``.
|
||||||
|
|
||||||
:Parameters:
|
:Parameters:
|
||||||
`buffer` : `AbstractMappable`
|
`buffer` : `AttributeBufferObject`
|
||||||
The buffer to map.
|
The buffer to map.
|
||||||
`start` : int
|
`start` : int
|
||||||
Offset of the first vertex to map.
|
Offset of the first vertex to map.
|
||||||
`count` : int
|
`count` : int
|
||||||
Number of vertices to map
|
Number of vertices to map
|
||||||
|
|
||||||
:rtype: `AbstractBufferRegion`
|
:rtype: `BufferObjectRegion`
|
||||||
"""
|
"""
|
||||||
byte_start = self.stride * start
|
return buffer.get_region(start, count)
|
||||||
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)
|
|
||||||
|
|
||||||
def set_region(self, buffer, start, count, data):
|
def set_region(self, buffer, start, count, data):
|
||||||
"""Set the data over a region of the buffer.
|
"""Set the data over a region of the buffer.
|
||||||
@ -234,11 +230,7 @@ class Attribute:
|
|||||||
Number of vertices to set.
|
Number of vertices to set.
|
||||||
`data` : A sequence of data components.
|
`data` : A sequence of data components.
|
||||||
"""
|
"""
|
||||||
byte_start = self.stride * start
|
buffer.set_region(start, count, data)
|
||||||
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)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"Attribute(name='{self.name}', location={self.location}, count={self.count})"
|
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):
|
def __init__(self, source_string: str, shader_type: str):
|
||||||
|
self._context = pyglet.gl.current_context
|
||||||
self._id = None
|
self._id = None
|
||||||
self.type = shader_type
|
self.type = shader_type
|
||||||
|
|
||||||
@ -697,6 +690,7 @@ class Shader:
|
|||||||
source_length = c_int(len(shader_source_utf8))
|
source_length = c_int(len(shader_source_utf8))
|
||||||
|
|
||||||
shader_id = glCreateShader(shader_type)
|
shader_id = glCreateShader(shader_type)
|
||||||
|
self._id = shader_id
|
||||||
glShaderSource(shader_id, 1, byref(source_buffer_pointer), source_length)
|
glShaderSource(shader_id, 1, byref(source_buffer_pointer), source_length)
|
||||||
glCompileShader(shader_id)
|
glCompileShader(shader_id)
|
||||||
|
|
||||||
@ -717,8 +711,6 @@ class Shader:
|
|||||||
elif _debug_gl_shaders:
|
elif _debug_gl_shaders:
|
||||||
print(self._get_shader_log(shader_id))
|
print(self._get_shader_log(shader_id))
|
||||||
|
|
||||||
self._id = shader_id
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def id(self):
|
||||||
return self._id
|
return self._id
|
||||||
@ -743,16 +735,19 @@ class Shader:
|
|||||||
glGetShaderSource(shader_id, source_length, None, source_str)
|
glGetShaderSource(shader_id, source_length, None, source_str)
|
||||||
return source_str.value.decode('utf8')
|
return source_str.value.decode('utf8')
|
||||||
|
|
||||||
def __del__(self):
|
def delete(self):
|
||||||
try:
|
|
||||||
glDeleteShader(self._id)
|
glDeleteShader(self._id)
|
||||||
|
self._id = None
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self._id is not None:
|
||||||
|
try:
|
||||||
|
self._context.delete_shader(self._id)
|
||||||
if _debug_gl_shaders:
|
if _debug_gl_shaders:
|
||||||
print(f"Destroyed {self.type} Shader '{self._id}'")
|
print(f"Destroyed {self.type} Shader '{self._id}'")
|
||||||
|
self._id = None
|
||||||
except Exception:
|
except (AttributeError, ImportError):
|
||||||
# Interpreter is shutting down,
|
pass # Interpreter is shutting down
|
||||||
# or Shader failed to compile.
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "{0}(id={1}, type={2})".format(self.__class__.__name__, self.id, self.type)
|
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__'
|
__slots__ = '_id', '_context', '_attributes', '_uniforms', '_uniform_blocks', '__weakref__'
|
||||||
|
|
||||||
def __init__(self, *shaders: Shader):
|
def __init__(self, *shaders: Shader):
|
||||||
|
self._id = None
|
||||||
|
|
||||||
assert shaders, "At least one Shader object is required."
|
assert shaders, "At least one Shader object is required."
|
||||||
self._id = _link_program(*shaders)
|
self._id = _link_program(*shaders)
|
||||||
self._context = pyglet.gl.current_context
|
self._context = pyglet.gl.current_context
|
||||||
@ -807,13 +804,17 @@ class ShaderProgram:
|
|||||||
def __exit__(self, *_):
|
def __exit__(self, *_):
|
||||||
glUseProgram(0)
|
glUseProgram(0)
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
glDeleteProgram(self._id)
|
||||||
|
self._id = None
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
|
if self._id is not None:
|
||||||
try:
|
try:
|
||||||
self._context.delete_shader_program(self.id)
|
self._context.delete_shader_program(self._id)
|
||||||
except Exception:
|
self._id = None
|
||||||
# Interpreter is shutting down,
|
except (AttributeError, ImportError):
|
||||||
# or ShaderProgram failed to link.
|
pass # Interpreter is shutting down
|
||||||
pass
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
try:
|
try:
|
||||||
@ -938,6 +939,8 @@ class ComputeShaderProgram:
|
|||||||
|
|
||||||
def __init__(self, source: str):
|
def __init__(self, source: str):
|
||||||
"""Create an OpenGL ComputeShaderProgram from source."""
|
"""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")):
|
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 "
|
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.")
|
"4.3 or higher, or 4.2 with the 'GL_ARB_compute_shader' extension.")
|
||||||
@ -1010,13 +1013,17 @@ class ComputeShaderProgram:
|
|||||||
def __exit__(self, *_):
|
def __exit__(self, *_):
|
||||||
glUseProgram(0)
|
glUseProgram(0)
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
glDeleteProgram(self._id)
|
||||||
|
self._id = None
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
|
if self._id is not None:
|
||||||
try:
|
try:
|
||||||
self._context.delete_shader_program(self.id)
|
self._context.delete_shader_program(self._id)
|
||||||
except Exception:
|
self._id = None
|
||||||
# Interpreter is shutting down,
|
except (AttributeError, ImportError):
|
||||||
# or ShaderProgram failed to link.
|
pass # Interpreter is shutting down
|
||||||
pass
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
try:
|
try:
|
||||||
|
@ -27,10 +27,8 @@ class VertexArray:
|
|||||||
glBindVertexArray(0)
|
glBindVertexArray(0)
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
try:
|
|
||||||
glDeleteVertexArrays(1, self._id)
|
glDeleteVertexArrays(1, self._id)
|
||||||
except Exception:
|
self._id = None
|
||||||
pass
|
|
||||||
|
|
||||||
__enter__ = bind
|
__enter__ = bind
|
||||||
|
|
||||||
@ -38,11 +36,12 @@ class VertexArray:
|
|||||||
glBindVertexArray(0)
|
glBindVertexArray(0)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
|
if self._id is not None:
|
||||||
try:
|
try:
|
||||||
self._context.delete_vao(self.id)
|
self._context.delete_vao(self.id)
|
||||||
# Python interpreter is shutting down:
|
self._id = None
|
||||||
except ImportError:
|
except (ImportError, AttributeError):
|
||||||
pass
|
pass # Interpreter is shutting down
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "{}(id={})".format(self.__class__.__name__, self._id.value)
|
return "{}(id={})".format(self.__class__.__name__, self._id.value)
|
||||||
|
@ -11,6 +11,8 @@ the buffer.
|
|||||||
import sys
|
import sys
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
import pyglet
|
import pyglet
|
||||||
from pyglet.gl import *
|
from pyglet.gl import *
|
||||||
|
|
||||||
@ -98,36 +100,6 @@ class AbstractBuffer:
|
|||||||
raise NotImplementedError('abstract')
|
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):
|
class BufferObject(AbstractBuffer):
|
||||||
"""Lightweight representation of an OpenGL Buffer Object.
|
"""Lightweight representation of an OpenGL Buffer Object.
|
||||||
|
|
||||||
@ -180,7 +152,8 @@ class BufferObject(AbstractBuffer):
|
|||||||
|
|
||||||
def map(self):
|
def map(self):
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, self.id)
|
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
|
return ptr
|
||||||
|
|
||||||
def map_range(self, start, size, ptr_type):
|
def map_range(self, start, size, ptr_type):
|
||||||
@ -191,21 +164,18 @@ class BufferObject(AbstractBuffer):
|
|||||||
def unmap(self):
|
def unmap(self):
|
||||||
glUnmapBuffer(GL_ARRAY_BUFFER)
|
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):
|
def delete(self):
|
||||||
buffer_id = GLuint(self.id)
|
glDeleteBuffers(1, self.id)
|
||||||
try:
|
|
||||||
glDeleteBuffers(1, buffer_id)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self.id = None
|
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):
|
def resize(self, size):
|
||||||
# Map, create a copy, then reinitialize.
|
# Map, create a copy, then reinitialize.
|
||||||
temp = (ctypes.c_byte * size)()
|
temp = (ctypes.c_byte * size)()
|
||||||
@ -222,27 +192,31 @@ class BufferObject(AbstractBuffer):
|
|||||||
return f"{self.__class__.__name__}(id={self.id}, size={self.size})"
|
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.
|
"""A buffer with system-memory backed store.
|
||||||
|
|
||||||
Updates to the data via `set_data`, `set_data_region` and `map` will be
|
Updates to the data via `set_data` and `set_data_region` will be held
|
||||||
held in local memory until `bind` is called. The advantage is that fewer
|
in local memory until `buffer_data` is called. The advantage is that
|
||||||
OpenGL calls are needed, increasing performance.
|
fewer OpenGL calls are needed, which can increasing performance at the
|
||||||
|
expense of system memory.
|
||||||
There may also be less performance penalty for resizing this buffer.
|
|
||||||
|
|
||||||
Updates to data via :py:meth:`map` are committed immediately.
|
|
||||||
"""
|
"""
|
||||||
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 = (ctypes.c_byte * size)()
|
||||||
self.data_ptr = ctypes.addressof(self.data)
|
self.data_ptr = ctypes.addressof(self.data)
|
||||||
self._dirty_min = sys.maxsize
|
self._dirty_min = sys.maxsize
|
||||||
self._dirty_max = 0
|
self._dirty_max = 0
|
||||||
|
|
||||||
def bind(self):
|
self.attribute_stride = attribute.stride
|
||||||
# Commit pending data
|
self.attribute_count = attribute.count
|
||||||
super(MappableBufferObject, self).bind()
|
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
|
size = self._dirty_max - self._dirty_min
|
||||||
if size > 0:
|
if size > 0:
|
||||||
if size == self.size:
|
if size == self.size:
|
||||||
@ -252,28 +226,27 @@ class MappableBufferObject(BufferObject, AbstractMappable):
|
|||||||
self._dirty_min = sys.maxsize
|
self._dirty_min = sys.maxsize
|
||||||
self._dirty_max = 0
|
self._dirty_max = 0
|
||||||
|
|
||||||
def set_data(self, data):
|
@lru_cache(maxsize=None)
|
||||||
super(MappableBufferObject, self).set_data(data)
|
def get_region(self, start, count):
|
||||||
ctypes.memmove(self.data, data, self.size)
|
byte_start = self.attribute_stride * start # byte offset
|
||||||
self._dirty_min = 0
|
byte_size = self.attribute_stride * count # number of bytes
|
||||||
self._dirty_max = self.size
|
array_count = self.attribute_count * count # number of values
|
||||||
|
|
||||||
def set_data_region(self, data, start, length):
|
ptr_type = ctypes.POINTER(self.attribute_ctype * array_count)
|
||||||
ctypes.memmove(self.data_ptr + start, data, length)
|
array = ctypes.cast(self.data_ptr + byte_start, ptr_type).contents
|
||||||
self._dirty_min = min(start, self._dirty_min)
|
return BufferObjectRegion(self, byte_start, byte_start + byte_size, array)
|
||||||
self._dirty_max = max(start + length, self._dirty_max)
|
|
||||||
|
|
||||||
def map(self, invalidate=False):
|
def set_region(self, start, count, data):
|
||||||
self._dirty_min = 0
|
byte_start = self.attribute_stride * start # byte offset
|
||||||
self._dirty_max = self.size
|
byte_size = self.attribute_stride * count # number of bytes
|
||||||
return self.data
|
|
||||||
|
|
||||||
def unmap(self):
|
array_start = start * self.attribute_count
|
||||||
pass
|
array_end = count * self.attribute_count + array_start
|
||||||
|
|
||||||
def get_region(self, start, size, ptr_type):
|
self._array[array_start:array_end] = data
|
||||||
array = ctypes.cast(self.data_ptr + start, ptr_type).contents
|
|
||||||
return BufferObjectRegion(self, start, start + size, array)
|
self._dirty_min = min(self._dirty_min, byte_start)
|
||||||
|
self._dirty_max = max(self._dirty_max, byte_start + byte_size)
|
||||||
|
|
||||||
def resize(self, size):
|
def resize(self, size):
|
||||||
data = (ctypes.c_byte * size)()
|
data = (ctypes.c_byte * size)()
|
||||||
@ -289,6 +262,9 @@ class MappableBufferObject(BufferObject, AbstractMappable):
|
|||||||
self._dirty_min = sys.maxsize
|
self._dirty_min = sys.maxsize
|
||||||
self._dirty_max = 0
|
self._dirty_max = 0
|
||||||
|
|
||||||
|
self._array = self.get_region(0, size).array
|
||||||
|
self.get_region.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
class BufferObjectRegion:
|
class BufferObjectRegion:
|
||||||
"""A mapped region of a MappableBufferObject."""
|
"""A mapped region of a MappableBufferObject."""
|
||||||
|
@ -23,11 +23,9 @@ primitives of the same OpenGL primitive mode.
|
|||||||
|
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|
||||||
import pyglet
|
|
||||||
|
|
||||||
from pyglet.gl import *
|
from pyglet.gl import *
|
||||||
from pyglet.graphics import allocation, shader, vertexarray
|
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):
|
def _nearest_pow2(v):
|
||||||
@ -66,23 +64,40 @@ _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:
|
class VertexDomain:
|
||||||
"""Management of a set of vertex lists.
|
"""Management of a set of vertex lists.
|
||||||
|
|
||||||
Construction of a vertex domain is usually done with the
|
Construction of a vertex domain is usually done with the
|
||||||
:py:func:`create_domain` function.
|
:py:func:`create_domain` function.
|
||||||
"""
|
"""
|
||||||
version = 0
|
|
||||||
_initial_count = 16
|
_initial_count = 16
|
||||||
|
|
||||||
def __init__(self, program, attribute_meta):
|
def __init__(self, program, attribute_meta):
|
||||||
self.program = program
|
self.program = program # Needed a reference for migration
|
||||||
self.attribute_meta = attribute_meta
|
self.attribute_meta = attribute_meta
|
||||||
self.allocator = allocation.Allocator(self._initial_count)
|
self.allocator = allocation.Allocator(self._initial_count)
|
||||||
|
|
||||||
self.attributes = []
|
self.attribute_names = {} # name: attribute
|
||||||
self.buffer_attributes = [] # list of (buffer, attributes)
|
self.buffer_attributes = [] # list of (buffer, attributes)
|
||||||
|
|
||||||
|
self._property_dict = {} # name: property(_getter, _setter)
|
||||||
|
|
||||||
for name, meta in attribute_meta.items():
|
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}'."
|
assert meta['format'][0] in _gl_types, f"'{meta['format']}' is not a valid atrribute format for '{name}'."
|
||||||
location = meta['location']
|
location = meta['location']
|
||||||
@ -90,14 +105,19 @@ class VertexDomain:
|
|||||||
gl_type = _gl_types[meta['format'][0]]
|
gl_type = _gl_types[meta['format'][0]]
|
||||||
normalize = 'n' in meta['format']
|
normalize = 'n' in meta['format']
|
||||||
attribute = shader.Attribute(name, location, count, gl_type, normalize)
|
attribute = shader.Attribute(name, location, count, gl_type, normalize)
|
||||||
self.attributes.append(attribute)
|
self.attribute_names[attribute.name] = attribute
|
||||||
|
|
||||||
# Create buffer:
|
# Create buffer:
|
||||||
attribute.buffer = MappableBufferObject(attribute.stride * self.allocator.capacity)
|
attribute.buffer = AttributeBufferObject(attribute.stride * self.allocator.capacity, attribute)
|
||||||
attribute.buffer.element_size = attribute.stride
|
|
||||||
attribute.buffer.attributes = (attribute,)
|
|
||||||
self.buffer_attributes.append((attribute.buffer, (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 = vertexarray.VertexArray()
|
||||||
self.vao.bind()
|
self.vao.bind()
|
||||||
for buffer, attributes in self.buffer_attributes:
|
for buffer, attributes in self.buffer_attributes:
|
||||||
@ -107,29 +127,14 @@ class VertexDomain:
|
|||||||
attribute.set_pointer(buffer.ptr)
|
attribute.set_pointer(buffer.ptr)
|
||||||
self.vao.unbind()
|
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):
|
def safe_alloc(self, count):
|
||||||
"""Allocate vertices, resizing the buffers if necessary."""
|
"""Allocate vertices, resizing the buffers if necessary."""
|
||||||
try:
|
try:
|
||||||
return self.allocator.alloc(count)
|
return self.allocator.alloc(count)
|
||||||
except allocation.AllocatorMemoryException as e:
|
except allocation.AllocatorMemoryException as e:
|
||||||
capacity = _nearest_pow2(e.requested_capacity)
|
capacity = _nearest_pow2(e.requested_capacity)
|
||||||
self.version += 1
|
|
||||||
for buffer, _ in self.buffer_attributes:
|
for buffer, _ in self.buffer_attributes:
|
||||||
buffer.resize(capacity * buffer.element_size)
|
buffer.resize(capacity * buffer.attribute_stride)
|
||||||
self.allocator.set_capacity(capacity)
|
self.allocator.set_capacity(capacity)
|
||||||
return self.allocator.alloc(count)
|
return self.allocator.alloc(count)
|
||||||
|
|
||||||
@ -139,9 +144,8 @@ class VertexDomain:
|
|||||||
return self.allocator.realloc(start, count, new_count)
|
return self.allocator.realloc(start, count, new_count)
|
||||||
except allocation.AllocatorMemoryException as e:
|
except allocation.AllocatorMemoryException as e:
|
||||||
capacity = _nearest_pow2(e.requested_capacity)
|
capacity = _nearest_pow2(e.requested_capacity)
|
||||||
self.version += 1
|
|
||||||
for buffer, _ in self.buffer_attributes:
|
for buffer, _ in self.buffer_attributes:
|
||||||
buffer.resize(capacity * buffer.element_size)
|
buffer.resize(capacity * buffer.attribute_stride)
|
||||||
self.allocator.set_capacity(capacity)
|
self.allocator.set_capacity(capacity)
|
||||||
return self.allocator.realloc(start, count, new_count)
|
return self.allocator.realloc(start, count, new_count)
|
||||||
|
|
||||||
@ -157,7 +161,7 @@ class VertexDomain:
|
|||||||
:rtype: :py:class:`VertexList`
|
:rtype: :py:class:`VertexList`
|
||||||
"""
|
"""
|
||||||
start = self.safe_alloc(count)
|
start = self.safe_alloc(count)
|
||||||
return VertexList(self, start, count)
|
return self._vertexlist_class(self, start, count)
|
||||||
|
|
||||||
def draw(self, mode):
|
def draw(self, mode):
|
||||||
"""Draw all vertices in the domain.
|
"""Draw all vertices in the domain.
|
||||||
@ -221,8 +225,6 @@ class VertexList:
|
|||||||
self.domain = domain
|
self.domain = domain
|
||||||
self.start = start
|
self.start = start
|
||||||
self.count = count
|
self.count = count
|
||||||
self._caches = {}
|
|
||||||
self._cache_versions = {}
|
|
||||||
|
|
||||||
def draw(self, mode):
|
def draw(self, mode):
|
||||||
"""Draw this vertex list in the given OpenGL 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)
|
new_start = self.domain.safe_realloc(self.start, self.count, count)
|
||||||
if new_start != self.start:
|
if new_start != self.start:
|
||||||
# Copy contents to new location
|
# 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)
|
old = attribute.get_region(attribute.buffer, self.start, self.count)
|
||||||
new = attribute.get_region(attribute.buffer, new_start, self.count)
|
new = attribute.get_region(attribute.buffer, new_start, self.count)
|
||||||
new.array[:] = old.array[:]
|
new.array[:] = old.array[:]
|
||||||
@ -255,9 +257,6 @@ class VertexList:
|
|||||||
self.start = new_start
|
self.start = new_start
|
||||||
self.count = count
|
self.count = count
|
||||||
|
|
||||||
for version in self._cache_versions:
|
|
||||||
self._cache_versions[version] = None
|
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""Delete this group."""
|
"""Delete this group."""
|
||||||
self.domain.allocator.dealloc(self.start, self.count)
|
self.domain.allocator.dealloc(self.start, self.count)
|
||||||
@ -287,33 +286,10 @@ class VertexList:
|
|||||||
self.domain = domain
|
self.domain = domain
|
||||||
self.start = new_start
|
self.start = new_start
|
||||||
|
|
||||||
for version in self._cache_versions:
|
|
||||||
self._cache_versions[version] = None
|
|
||||||
|
|
||||||
def set_attribute_data(self, name, data):
|
def set_attribute_data(self, name, data):
|
||||||
attribute = self.domain.attribute_names[name]
|
attribute = self.domain.attribute_names[name]
|
||||||
attribute.set_region(attribute.buffer, self.start, self.count, data)
|
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):
|
class IndexedVertexDomain(VertexDomain):
|
||||||
"""Management of a set of indexed vertex lists.
|
"""Management of a set of indexed vertex lists.
|
||||||
@ -337,13 +313,15 @@ class IndexedVertexDomain(VertexDomain):
|
|||||||
self.index_buffer.bind_to_index_buffer()
|
self.index_buffer.bind_to_index_buffer()
|
||||||
self.vao.unbind()
|
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):
|
def safe_index_alloc(self, count):
|
||||||
"""Allocate indices, resizing the buffers if necessary."""
|
"""Allocate indices, resizing the buffers if necessary."""
|
||||||
try:
|
try:
|
||||||
return self.index_allocator.alloc(count)
|
return self.index_allocator.alloc(count)
|
||||||
except allocation.AllocatorMemoryException as e:
|
except allocation.AllocatorMemoryException as e:
|
||||||
capacity = _nearest_pow2(e.requested_capacity)
|
capacity = _nearest_pow2(e.requested_capacity)
|
||||||
self.version += 1
|
|
||||||
self.index_buffer.resize(capacity * self.index_element_size)
|
self.index_buffer.resize(capacity * self.index_element_size)
|
||||||
self.index_allocator.set_capacity(capacity)
|
self.index_allocator.set_capacity(capacity)
|
||||||
return self.index_allocator.alloc(count)
|
return self.index_allocator.alloc(count)
|
||||||
@ -354,7 +332,6 @@ class IndexedVertexDomain(VertexDomain):
|
|||||||
return self.index_allocator.realloc(start, count, new_count)
|
return self.index_allocator.realloc(start, count, new_count)
|
||||||
except allocation.AllocatorMemoryException as e:
|
except allocation.AllocatorMemoryException as e:
|
||||||
capacity = _nearest_pow2(e.requested_capacity)
|
capacity = _nearest_pow2(e.requested_capacity)
|
||||||
self.version += 1
|
|
||||||
self.index_buffer.resize(capacity * self.index_element_size)
|
self.index_buffer.resize(capacity * self.index_element_size)
|
||||||
self.index_allocator.set_capacity(capacity)
|
self.index_allocator.set_capacity(capacity)
|
||||||
return self.index_allocator.realloc(start, count, new_count)
|
return self.index_allocator.realloc(start, count, new_count)
|
||||||
@ -371,7 +348,7 @@ class IndexedVertexDomain(VertexDomain):
|
|||||||
"""
|
"""
|
||||||
start = self.safe_alloc(count)
|
start = self.safe_alloc(count)
|
||||||
index_start = self.safe_index_alloc(index_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):
|
def get_index_region(self, start, count):
|
||||||
"""Get a data from a region of the index buffer.
|
"""Get a data from a region of the index buffer.
|
||||||
|
@ -433,6 +433,34 @@ class TextEntry(WidgetBase):
|
|||||||
assert type(value) is str, "This Widget's value must be a string."
|
assert type(value) is str, "This Widget's value must be a string."
|
||||||
self._doc.text = value
|
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):
|
def _check_hit(self, x, y):
|
||||||
return self._x < x < self._x + self._width and self._y < y < self._y + self._height
|
return self._x < x < self._x + self._width and self._y < y < self._y + self._height
|
||||||
|
|
||||||
|
@ -1220,11 +1220,20 @@ class Texture(AbstractImage):
|
|||||||
self.id = tex_id
|
self.id = tex_id
|
||||||
self._context = pyglet.gl.current_context
|
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):
|
def __del__(self):
|
||||||
|
if self.id is not None:
|
||||||
try:
|
try:
|
||||||
self._context.delete_texture(self.id)
|
self._context.delete_texture(self.id)
|
||||||
except Exception:
|
self.id = None
|
||||||
pass
|
except (AttributeError, ImportError):
|
||||||
|
pass # Interpreter is shutting down
|
||||||
|
|
||||||
def bind(self, texture_unit: int = 0):
|
def bind(self, texture_unit: int = 0):
|
||||||
"""Bind to a specific Texture Unit by number."""
|
"""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,
|
return "{}(id={}, size={}x{}, owner={}x{})".format(self.__class__.__name__, self.id, self.width, self.height,
|
||||||
self.owner.width, self.owner.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):
|
def __del__(self):
|
||||||
# only the owner Texture should handle deletion
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ class Renderbuffer:
|
|||||||
|
|
||||||
def __init__(self, width, height, internal_format, samples=1):
|
def __init__(self, width, height, internal_format, samples=1):
|
||||||
"""Create an instance of a Renderbuffer object."""
|
"""Create an instance of a Renderbuffer object."""
|
||||||
|
self._context = pyglet.gl.current_context
|
||||||
self._id = GLuint()
|
self._id = GLuint()
|
||||||
self._width = width
|
self._width = width
|
||||||
self._height = height
|
self._height = height
|
||||||
@ -49,13 +50,15 @@ class Renderbuffer:
|
|||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
glDeleteRenderbuffers(1, self._id)
|
glDeleteRenderbuffers(1, self._id)
|
||||||
|
self._id = None
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
|
if self._id is not None:
|
||||||
try:
|
try:
|
||||||
glDeleteRenderbuffers(1, self._id)
|
self._context.delete_renderbuffer(self._id.value)
|
||||||
# Python interpreter is shutting down:
|
self._id = None
|
||||||
except Exception:
|
except (AttributeError, ImportError):
|
||||||
pass
|
pass # Interpreter is shutting down
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "{}(id={})".format(self.__class__.__name__, self._id.value)
|
return "{}(id={})".format(self.__class__.__name__, self._id.value)
|
||||||
@ -71,6 +74,7 @@ class Framebuffer:
|
|||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
|
self._context = pyglet.gl.current_context
|
||||||
self._id = GLuint()
|
self._id = GLuint()
|
||||||
glGenFramebuffers(1, self._id)
|
glGenFramebuffers(1, self._id)
|
||||||
self._attachment_types = 0
|
self._attachment_types = 0
|
||||||
@ -105,10 +109,16 @@ class Framebuffer:
|
|||||||
self.unbind()
|
self.unbind()
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
try:
|
|
||||||
glDeleteFramebuffers(1, self._id)
|
glDeleteFramebuffers(1, self._id)
|
||||||
except Exception:
|
self._id = None
|
||||||
pass
|
|
||||||
|
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
|
@property
|
||||||
def is_complete(self):
|
def is_complete(self):
|
||||||
@ -203,12 +213,5 @@ class Framebuffer:
|
|||||||
self._height = max(renderbuffer.height, self._height)
|
self._height = max(renderbuffer.height, self._height)
|
||||||
self.unbind()
|
self.unbind()
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
try:
|
|
||||||
glDeleteFramebuffers(1, self._id)
|
|
||||||
# Python interpreter is shutting down:
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "{}(id={})".format(self.__class__.__name__, self._id.value)
|
return "{}(id={})".format(self.__class__.__name__, self._id.value)
|
||||||
|
@ -601,7 +601,7 @@ class WICEncoder(ImageEncoder):
|
|||||||
|
|
||||||
frame.SetPixelFormat(byref(default_format))
|
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)
|
frame.WritePixels(image.height, pitch, size, data)
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ import fcntl
|
|||||||
import ctypes
|
import ctypes
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from os import readv
|
|
||||||
from ctypes import c_uint16 as _u16
|
from ctypes import c_uint16 as _u16
|
||||||
from ctypes import c_int16 as _s16
|
from ctypes import c_int16 as _s16
|
||||||
from ctypes import c_uint32 as _u32
|
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.base import DeviceOpenException, ControllerManager
|
||||||
from pyglet.input.controller import get_mapping, Relation, create_guid
|
from pyglet.input.controller import get_mapping, Relation, create_guid
|
||||||
|
|
||||||
|
c = pyglet.lib.load_library('c')
|
||||||
|
|
||||||
_IOC_NRBITS = 8
|
_IOC_NRBITS = 8
|
||||||
_IOC_TYPEBITS = 8
|
_IOC_TYPEBITS = 8
|
||||||
_IOC_SIZEBITS = 14
|
_IOC_SIZEBITS = 14
|
||||||
@ -408,7 +409,7 @@ class EvdevDevice(XlibSelectDevice, Device):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bytes_read = readv(self._fileno, self._event_buffer)
|
bytes_read = c.read(self._fileno, self._event_buffer, self._event_size)
|
||||||
except OSError:
|
except OSError:
|
||||||
self.close()
|
self.close()
|
||||||
return
|
return
|
||||||
|
@ -113,8 +113,8 @@ ERROR_SUCCESS = 0
|
|||||||
class XINPUT_GAMEPAD(Structure):
|
class XINPUT_GAMEPAD(Structure):
|
||||||
_fields_ = [
|
_fields_ = [
|
||||||
('wButtons', WORD),
|
('wButtons', WORD),
|
||||||
('bLeftTrigger', UBYTE),
|
('bLeftTrigger', BYTE),
|
||||||
('bRightTrigger', UBYTE),
|
('bRightTrigger', BYTE),
|
||||||
('sThumbLX', SHORT),
|
('sThumbLX', SHORT),
|
||||||
('sThumbLY', SHORT),
|
('sThumbLY', SHORT),
|
||||||
('sThumbRX', SHORT),
|
('sThumbRX', SHORT),
|
||||||
|
@ -218,6 +218,7 @@ NSApplicationDidUnhideNotification = c_void_p.in_dll(appkit, 'NSApplicationDidUn
|
|||||||
NSApplicationDidUpdateNotification = c_void_p.in_dll(appkit, 'NSApplicationDidUpdateNotification')
|
NSApplicationDidUpdateNotification = c_void_p.in_dll(appkit, 'NSApplicationDidUpdateNotification')
|
||||||
NSPasteboardURLReadingFileURLsOnlyKey = c_void_p.in_dll(appkit, 'NSPasteboardURLReadingFileURLsOnlyKey')
|
NSPasteboardURLReadingFileURLsOnlyKey = c_void_p.in_dll(appkit, 'NSPasteboardURLReadingFileURLsOnlyKey')
|
||||||
NSPasteboardTypeURL = c_void_p.in_dll(appkit, 'NSPasteboardTypeURL')
|
NSPasteboardTypeURL = c_void_p.in_dll(appkit, 'NSPasteboardTypeURL')
|
||||||
|
NSPasteboardTypeString = c_void_p.in_dll(appkit, 'NSPasteboardTypeString')
|
||||||
NSDragOperationGeneric = 4
|
NSDragOperationGeneric = 4
|
||||||
|
|
||||||
# /System/Library/Frameworks/AppKit.framework/Headers/NSEvent.h
|
# /System/Library/Frameworks/AppKit.framework/Headers/NSEvent.h
|
||||||
|
@ -211,7 +211,18 @@ _user32.RegisterDeviceNotificationW.restype = HANDLE
|
|||||||
_user32.RegisterDeviceNotificationW.argtypes = [HANDLE, LPVOID, DWORD]
|
_user32.RegisterDeviceNotificationW.argtypes = [HANDLE, LPVOID, DWORD]
|
||||||
_user32.UnregisterDeviceNotification.restype = BOOL
|
_user32.UnregisterDeviceNotification.restype = BOOL
|
||||||
_user32.UnregisterDeviceNotification.argtypes = [HANDLE]
|
_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
|
||||||
_dwmapi.DwmIsCompositionEnabled.restype = c_int
|
_dwmapi.DwmIsCompositionEnabled.restype = c_int
|
||||||
|
@ -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::
|
function types). The 'this' pointer is bound automatically... e.g., call::
|
||||||
|
|
||||||
device = IDirectSound8()
|
device = IDirectSound8()
|
||||||
@ -50,7 +50,7 @@ class GUID(ctypes.Structure):
|
|||||||
('Data1', ctypes.c_ulong),
|
('Data1', ctypes.c_ulong),
|
||||||
('Data2', ctypes.c_ushort),
|
('Data2', ctypes.c_ushort),
|
||||||
('Data3', 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):
|
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)' % (
|
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)
|
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):
|
def __eq__(self, other):
|
||||||
return isinstance(other, GUID) and bytes(self) == bytes(other)
|
return isinstance(other, GUID) and bytes(self) == bytes(other)
|
||||||
|
|
||||||
@ -80,6 +75,10 @@ LPGUID = ctypes.POINTER(GUID)
|
|||||||
IID = GUID
|
IID = GUID
|
||||||
REFIID = ctypes.POINTER(IID)
|
REFIID = ctypes.POINTER(IID)
|
||||||
|
|
||||||
|
S_OK = 0x00000000
|
||||||
|
E_NOTIMPL = 0x80004001
|
||||||
|
E_NOINTERFACE = 0x80004002
|
||||||
|
|
||||||
|
|
||||||
class METHOD:
|
class METHOD:
|
||||||
"""COM method."""
|
"""COM method."""
|
||||||
@ -88,244 +87,147 @@ class METHOD:
|
|||||||
self.restype = restype
|
self.restype = restype
|
||||||
self.argtypes = args
|
self.argtypes = args
|
||||||
|
|
||||||
def get_field(self):
|
self.prototype = ctypes.WINFUNCTYPE(self.restype, *self.argtypes)
|
||||||
# ctypes caches WINFUNCTYPE's so this should be ok.
|
self.direct_prototype = ctypes.WINFUNCTYPE(self.restype, ctypes.c_void_p, *self.argtypes)
|
||||||
return ctypes.WINFUNCTYPE(self.restype, *self.argtypes)
|
|
||||||
|
def get_com_proxy(self, i, name):
|
||||||
|
return self.prototype(i, name)
|
||||||
|
|
||||||
|
|
||||||
class STDMETHOD(METHOD):
|
class STDMETHOD(METHOD):
|
||||||
"""COM method with HRESULT return value."""
|
"""COM method with HRESULT return value."""
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
super(STDMETHOD, self).__init__(ctypes.HRESULT, *args)
|
super().__init__(ctypes.HRESULT, *args)
|
||||||
|
|
||||||
|
|
||||||
class COMMethodInstance:
|
class VOIDMETHOD(METHOD):
|
||||||
"""Binds a COM interface method."""
|
"""COM method with no return value."""
|
||||||
|
|
||||||
def __init__(self, name, i, method):
|
def __init__(self, *args):
|
||||||
self.name = name
|
super().__init__(None, *args)
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
class COMInterface(ctypes.Structure):
|
_DummyPointerType = ctypes.POINTER(ctypes.c_int)
|
||||||
"""Dummy struct to serve as the type of all COM pointers."""
|
_PointerMeta = type(_DummyPointerType)
|
||||||
_fields_ = [
|
_StructMeta = type(ctypes.Structure)
|
||||||
('lpVtbl', ctypes.c_void_p),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class InterfacePtrMeta(type(ctypes.POINTER(COMInterface))):
|
class _InterfaceMeta(_StructMeta):
|
||||||
"""Allows interfaces to be subclassed as ctypes POINTER and expects to be populated with data from a COM object.
|
def __new__(cls, name, bases, dct, /, create_pointer_type=True):
|
||||||
TODO: Phase this out and properly use POINTER(Interface) where applicable.
|
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):
|
def __new__(cls, name, bases, dct):
|
||||||
methods = []
|
# Interfaces can also be declared by inheritance of pInterface subclasses.
|
||||||
for base in bases[::-1]:
|
# If this happens, create the interface and then become pointer to its struct.
|
||||||
methods.extend(base.__dict__.get('_methods_', ()))
|
|
||||||
methods.extend(dct.get('_methods_', ()))
|
|
||||||
|
|
||||||
for i, (n, method) in enumerate(methods):
|
target = dct.get('_type_', None)
|
||||||
dct[n] = COMMethodInstance(n, i, method)
|
# 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):
|
# Hack selves into the ctypes pointer cache so all uses of `ctypes.POINTER` on the
|
||||||
"""Base COM interface pointer."""
|
# 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
|
||||||
class COMInterfaceMeta(type):
|
# RegisterCallback(callback_obj.as_interface(ICallback))
|
||||||
"""This differs in the original as an implemented interface object, not a POINTER object.
|
# instead of
|
||||||
Used when the user must implement their own functions within an interface rather than
|
# RegisterCallback(callback_obj)
|
||||||
being created and generated by the COM object itself. The types are automatically inserted in the ctypes type
|
# could make it obsolete.
|
||||||
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.
|
|
||||||
from ctypes import _pointer_type_cache
|
from ctypes import _pointer_type_cache
|
||||||
_pointer_type_cache[cls] = type(COMPointer)("POINTER({})".format(cls.__name__),
|
_pointer_type_cache[target] = pointer_type
|
||||||
_ptr_bases,
|
|
||||||
{"__interface__": cls})
|
|
||||||
|
|
||||||
return cls
|
return pointer_type
|
||||||
|
|
||||||
def __get_subclassed_methodcount(self):
|
|
||||||
"""Returns the amount of COM methods in all subclasses to determine offset of methods.
|
class Interface(ctypes.Structure, metaclass=_InterfaceMeta, create_pointer_type=False):
|
||||||
Order must be exact from the source when calling COM methods.
|
@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:
|
return cls.__mro__[:cls.__mro__.index(Interface)]
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class COMPointerMeta(type(ctypes.c_void_p), COMInterfaceMeta):
|
class pInterface(_DummyPointerType, metaclass=_pInterfaceMeta):
|
||||||
"""Required to prevent metaclass conflicts with inheritance."""
|
_type_ = Interface
|
||||||
|
|
||||||
|
|
||||||
class COMPointer(ctypes.c_void_p, metaclass=COMPointerMeta):
|
|
||||||
"""COM Pointer base, could use c_void_p but need to override from_param ."""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_param(cls, obj):
|
def from_param(cls, obj):
|
||||||
"""Allows obj to return ctypes pointers, even if its base is not a ctype.
|
"""When dealing with a COMObject, pry a fitting interface out of it"""
|
||||||
In this case, all we simply want is a ctypes pointer matching the cls interface from the obj.
|
|
||||||
"""
|
|
||||||
if obj is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
if not isinstance(obj, COMObject):
|
||||||
ptr_dct = obj._pointers
|
return obj
|
||||||
except AttributeError:
|
|
||||||
raise Exception("Interface method argument specified incorrectly, or passed wrong argument.", cls)
|
return obj.as_interface(cls._type_)
|
||||||
else:
|
|
||||||
try:
|
|
||||||
return ptr_dct[cls.__interface__]
|
|
||||||
except KeyError:
|
|
||||||
raise TypeError("Interface {} doesn't have a pointer in this class.".format(cls.__name__))
|
|
||||||
|
|
||||||
|
|
||||||
def _missing_impl(interface_name, method_name):
|
class IUnknown(Interface):
|
||||||
"""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."""
|
|
||||||
_methods_ = [
|
_methods_ = [
|
||||||
('QueryInterface', STDMETHOD(ctypes.c_void_p, REFIID, ctypes.c_void_p)),
|
('QueryInterface', STDMETHOD(REFIID, ctypes.c_void_p)),
|
||||||
('AddRef', METHOD(ctypes.c_int, ctypes.c_void_p)),
|
('AddRef', METHOD(ctypes.c_int)),
|
||||||
('Release', METHOD(ctypes.c_int, ctypes.c_void_p))
|
('Release', METHOD(ctypes.c_int)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -333,5 +235,163 @@ class pIUnknown(pInterface):
|
|||||||
_methods_ = [
|
_methods_ = [
|
||||||
('QueryInterface', STDMETHOD(REFIID, ctypes.c_void_p)),
|
('QueryInterface', STDMETHOD(REFIID, ctypes.c_void_p)),
|
||||||
('AddRef', METHOD(ctypes.c_int)),
|
('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)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import ctypes
|
import ctypes
|
||||||
|
import sys
|
||||||
|
|
||||||
from ctypes import *
|
from ctypes import *
|
||||||
from ctypes.wintypes import *
|
from ctypes.wintypes import *
|
||||||
@ -45,7 +46,6 @@ def POINTER_(obj):
|
|||||||
|
|
||||||
c_void_p = POINTER_(c_void)
|
c_void_p = POINTER_(c_void)
|
||||||
INT = c_int
|
INT = c_int
|
||||||
UBYTE = c_ubyte
|
|
||||||
LPVOID = c_void_p
|
LPVOID = c_void_p
|
||||||
HCURSOR = HANDLE
|
HCURSOR = HANDLE
|
||||||
LRESULT = LPARAM
|
LRESULT = LPARAM
|
||||||
@ -62,6 +62,11 @@ HDROP = HANDLE
|
|||||||
LPTSTR = LPWSTR
|
LPTSTR = LPWSTR
|
||||||
LPSTREAM = c_void_p
|
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
|
LF_FACESIZE = 32
|
||||||
CCHDEVICENAME = 32
|
CCHDEVICENAME = 32
|
||||||
CCHFORMNAME = 32
|
CCHFORMNAME = 32
|
||||||
@ -572,6 +577,7 @@ class IStream(com.pIUnknown):
|
|||||||
com.STDMETHOD()),
|
com.STDMETHOD()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class DEV_BROADCAST_HDR(Structure):
|
class DEV_BROADCAST_HDR(Structure):
|
||||||
_fields_ = (
|
_fields_ = (
|
||||||
('dbch_size', DWORD),
|
('dbch_size', DWORD),
|
||||||
@ -579,6 +585,7 @@ class DEV_BROADCAST_HDR(Structure):
|
|||||||
('dbch_reserved', DWORD),
|
('dbch_reserved', DWORD),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DEV_BROADCAST_DEVICEINTERFACE(Structure):
|
class DEV_BROADCAST_DEVICEINTERFACE(Structure):
|
||||||
_fields_ = (
|
_fields_ = (
|
||||||
('dbcc_size', DWORD),
|
('dbcc_size', DWORD),
|
||||||
|
@ -26,6 +26,7 @@ from collections.abc import Iterator as _Iterator
|
|||||||
|
|
||||||
|
|
||||||
number = _typing.Union[float, int]
|
number = _typing.Union[float, int]
|
||||||
|
Mat3T = _typing.TypeVar("Mat3T", bound="Mat3")
|
||||||
Mat4T = _typing.TypeVar("Mat4T", bound="Mat4")
|
Mat4T = _typing.TypeVar("Mat4T", bound="Mat4")
|
||||||
|
|
||||||
|
|
||||||
@ -627,7 +628,7 @@ class Mat3(tuple):
|
|||||||
the "@" operator.
|
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
|
"""Create a 3x3 Matrix
|
||||||
|
|
||||||
A Mat3 can be created with a list or tuple of 9 values.
|
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
|
`values` : tuple of float or int
|
||||||
A tuple or list containing 9 floats or ints.
|
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"
|
assert len(new) == 9, "A 3x3 Matrix requires 9 values"
|
||||||
return new
|
return new
|
||||||
|
|
||||||
@ -720,10 +721,10 @@ class Mat3(tuple):
|
|||||||
|
|
||||||
class Mat4(tuple):
|
class Mat4(tuple):
|
||||||
|
|
||||||
def __new__(cls, values: _Iterable[float] = (1.0, 0.0, 0.0, 0.0,
|
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, 1.0, 0.0, 0.0,
|
||||||
0.0, 0.0, 1.0, 0.0,
|
0.0, 0.0, 1.0, 0.0,
|
||||||
0.0, 0.0, 0.0, 1.0,)) -> Mat4:
|
0.0, 0.0, 0.0, 1.0,)) -> Mat4T:
|
||||||
"""Create a 4x4 Matrix.
|
"""Create a 4x4 Matrix.
|
||||||
|
|
||||||
`Mat4` is an immutable 4x4 Matrix, which includs most common
|
`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.
|
.. 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"
|
assert len(new) == 16, "A 4x4 Matrix requires 16 values"
|
||||||
return new
|
return new
|
||||||
|
|
||||||
@ -1011,3 +1012,106 @@ class Mat4(tuple):
|
|||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"{self.__class__.__name__}{self[0:4]}\n {self[4:8]}\n {self[8:12]}\n {self[12:16]}"
|
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]})"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Use ffmpeg to decode audio and video media.
|
"""Use ffmpeg to decode audio and video media.
|
||||||
"""
|
"""
|
||||||
|
import sys
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from ctypes import (c_int, c_int32, c_uint8, c_char_p,
|
from ctypes import (c_int, c_int32, c_uint8, c_char_p,
|
||||||
addressof, byref, cast, POINTER, Structure, create_string_buffer, memmove)
|
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
|
||||||
import pyglet.lib
|
import pyglet.lib
|
||||||
from pyglet import image
|
from pyglet import image
|
||||||
from pyglet.util import asbytes, asbytes_filename, asstr
|
from pyglet.util import asbytes, asstr
|
||||||
from . import MediaDecoder
|
from . import MediaDecoder
|
||||||
from .base import AudioData, SourceInfo, StaticSource
|
from .base import AudioData, SourceInfo, StaticSource
|
||||||
from .base import StreamingSource, VideoFormat, AudioFormat
|
from .base import StreamingSource, VideoFormat, AudioFormat
|
||||||
@ -510,10 +510,12 @@ class FFmpegSource(StreamingSource):
|
|||||||
self._file = None
|
self._file = None
|
||||||
self._memory_file = None
|
self._memory_file = None
|
||||||
|
|
||||||
|
encoded_filename = filename.encode(sys.getfilesystemencoding())
|
||||||
|
|
||||||
if file:
|
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:
|
else:
|
||||||
self._file = ffmpeg_open_filename(asbytes_filename(filename))
|
self._file = ffmpeg_open_filename(encoded_filename)
|
||||||
|
|
||||||
if not self._file:
|
if not self._file:
|
||||||
raise FFmpegException('Could not open "{0}"'.format(filename))
|
raise FFmpegException('Could not open "{0}"'.format(filename))
|
||||||
|
@ -520,9 +520,9 @@ class WMFSource(Source):
|
|||||||
imfmedia.GetGUID(MF_MT_SUBTYPE, ctypes.byref(guid_compressed))
|
imfmedia.GetGUID(MF_MT_SUBTYPE, ctypes.byref(guid_compressed))
|
||||||
|
|
||||||
if guid_compressed == MFAudioFormat_PCM or guid_compressed == MFAudioFormat_Float:
|
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:
|
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
|
# If audio is compressed, attempt to decompress it by forcing source reader to use PCM
|
||||||
mf_mediatype = IMFMediaType()
|
mf_mediatype = IMFMediaType()
|
||||||
|
|
||||||
|
@ -83,15 +83,15 @@ IID_IMMDeviceEnumerator = com.GUID(0xa95664d2, 0x9614, 0x4f35, 0xa7, 0x46, 0xde,
|
|||||||
class IMMNotificationClient(com.IUnknown):
|
class IMMNotificationClient(com.IUnknown):
|
||||||
_methods_ = [
|
_methods_ = [
|
||||||
('OnDeviceStateChanged',
|
('OnDeviceStateChanged',
|
||||||
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR, DWORD)),
|
com.STDMETHOD(LPCWSTR, DWORD)),
|
||||||
('OnDeviceAdded',
|
('OnDeviceAdded',
|
||||||
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR)),
|
com.STDMETHOD(LPCWSTR)),
|
||||||
('OnDeviceRemoved',
|
('OnDeviceRemoved',
|
||||||
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR)),
|
com.STDMETHOD(LPCWSTR)),
|
||||||
('OnDefaultDeviceChanged',
|
('OnDefaultDeviceChanged',
|
||||||
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, EDataFlow, ERole, LPCWSTR)),
|
com.STDMETHOD(EDataFlow, ERole, LPCWSTR)),
|
||||||
('OnPropertyValueChanged',
|
('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.audio_devices = audio_devices
|
||||||
self._lost = False
|
self._lost = False
|
||||||
|
|
||||||
def OnDeviceStateChanged(self, this, pwstrDeviceId, dwNewState):
|
def OnDeviceStateChanged(self, pwstrDeviceId, dwNewState):
|
||||||
device = self.audio_devices.get_cached_device(pwstrDeviceId)
|
device = self.audio_devices.get_cached_device(pwstrDeviceId)
|
||||||
|
|
||||||
old_state = device.state
|
old_state = device.state
|
||||||
@ -126,17 +126,17 @@ class AudioNotificationCB(com.COMObject):
|
|||||||
device.state = dwNewState
|
device.state = dwNewState
|
||||||
self.audio_devices.dispatch_event('on_device_state_changed', device, pyglet_old_state, pyglet_new_state)
|
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)
|
dev = self.audio_devices.add_device(pwstrDeviceId)
|
||||||
assert _debug(f"Audio device was added {pwstrDeviceId}: {dev}")
|
assert _debug(f"Audio device was added {pwstrDeviceId}: {dev}")
|
||||||
self.audio_devices.dispatch_event('on_device_added', 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)
|
dev = self.audio_devices.remove_device(pwstrDeviceId)
|
||||||
assert _debug(f"Audio device was removed {pwstrDeviceId} : {dev}")
|
assert _debug(f"Audio device was removed {pwstrDeviceId} : {dev}")
|
||||||
self.audio_devices.dispatch_event('on_device_removed', 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
|
# Only support eConsole role right now
|
||||||
if role == 0:
|
if role == 0:
|
||||||
if pwstrDeviceId is None:
|
if pwstrDeviceId is None:
|
||||||
@ -149,7 +149,7 @@ class AudioNotificationCB(com.COMObject):
|
|||||||
|
|
||||||
self.audio_devices.dispatch_event('on_default_changed', device, pyglet_flow)
|
self.audio_devices.dispatch_event('on_default_changed', device, pyglet_flow)
|
||||||
|
|
||||||
def OnPropertyValueChanged(self, this, pwstrDeviceId, key):
|
def OnPropertyValueChanged(self, pwstrDeviceId, key):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -259,7 +259,7 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager):
|
|||||||
cached_dev.state = dev_state
|
cached_dev.state = dev_state
|
||||||
return cached_dev
|
return cached_dev
|
||||||
except OSError as err:
|
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
|
return None
|
||||||
|
|
||||||
def get_default_input(self) -> Optional[Win32AudioDevice]:
|
def get_default_input(self) -> Optional[Win32AudioDevice]:
|
||||||
@ -274,7 +274,7 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager):
|
|||||||
cached_dev.state = dev_state
|
cached_dev.state = dev_state
|
||||||
return cached_dev
|
return cached_dev
|
||||||
except OSError as err:
|
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
|
return None
|
||||||
|
|
||||||
def get_cached_device(self, dev_id) -> Win32AudioDevice:
|
def get_cached_device(self, dev_id) -> Win32AudioDevice:
|
||||||
|
@ -145,11 +145,11 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
|
|||||||
|
|
||||||
def _refill(self, write_size):
|
def _refill(self, write_size):
|
||||||
while write_size > 0:
|
while write_size > 0:
|
||||||
assert _debug('_refill, write_size =', write_size)
|
assert _debug(f'_refill, write_size = {write_size}')
|
||||||
audio_data = self._get_audiodata()
|
audio_data = self._get_audiodata()
|
||||||
|
|
||||||
if audio_data is not None:
|
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)
|
length = min(write_size, audio_data.length)
|
||||||
self.write(audio_data, length)
|
self.write(audio_data, length)
|
||||||
write_size -= 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
|
# Set the write cursor back to eos_cursor or play_cursor to prevent gaps
|
||||||
if self._play_cursor < self._eos_cursor:
|
if self._play_cursor < self._eos_cursor:
|
||||||
cursor_diff = self._write_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 = self._eos_cursor
|
||||||
self._write_cursor_ring -= cursor_diff
|
self._write_cursor_ring -= cursor_diff
|
||||||
self._write_cursor_ring %= self._buffer_size
|
self._write_cursor_ring %= self._buffer_size
|
||||||
|
|
||||||
else:
|
else:
|
||||||
cursor_diff = self._play_cursor - self._eos_cursor
|
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 = self._play_cursor
|
||||||
self._write_cursor_ring -= cursor_diff
|
self._write_cursor_ring -= cursor_diff
|
||||||
self._write_cursor_ring %= self._buffer_size
|
self._write_cursor_ring %= self._buffer_size
|
||||||
@ -207,7 +207,7 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
|
|||||||
for event in audio_data.events:
|
for event in audio_data.events:
|
||||||
event_cursor = self._write_cursor + event.timestamp * \
|
event_cursor = self._write_cursor + event.timestamp * \
|
||||||
self.source.audio_format.bytes_per_second
|
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))
|
self._events.append((event_cursor, event))
|
||||||
|
|
||||||
def _add_audiodata_timestamp(self, audio_data):
|
def _add_audiodata_timestamp(self, audio_data):
|
||||||
|
@ -270,7 +270,6 @@ class IDirectSound(com.pIUnknown):
|
|||||||
('Initialize',
|
('Initialize',
|
||||||
com.STDMETHOD(com.LPGUID)),
|
com.STDMETHOD(com.LPGUID)),
|
||||||
]
|
]
|
||||||
_type_ = com.COMInterface
|
|
||||||
|
|
||||||
DirectSoundCreate = lib.DirectSoundCreate
|
DirectSoundCreate = lib.DirectSoundCreate
|
||||||
DirectSoundCreate.argtypes = \
|
DirectSoundCreate.argtypes = \
|
||||||
|
@ -245,7 +245,7 @@ class OpenALAudioPlayer(AbstractAudioPlayer):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def _refill(self, write_size):
|
def _refill(self, write_size):
|
||||||
assert _debug('_refill', write_size)
|
assert _debug(f'_refill {write_size}')
|
||||||
|
|
||||||
while write_size > self.min_buffer_size:
|
while write_size > self.min_buffer_size:
|
||||||
audio_data = self._get_audiodata()
|
audio_data = self._get_audiodata()
|
||||||
|
@ -236,7 +236,7 @@ class PulseAudioPlayer(AbstractAudioPlayer):
|
|||||||
|
|
||||||
while self._events and self._events[0][0] <= read_index:
|
while self._events and self._events[0][0] <= read_index:
|
||||||
_, event = self._events.pop(0)
|
_, 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)
|
event.sync_dispatch_to_player(self.player)
|
||||||
|
|
||||||
def _add_event_at_write_index(self, event_name):
|
def _add_event_at_write_index(self, event_name):
|
||||||
@ -313,7 +313,7 @@ class PulseAudioPlayer(AbstractAudioPlayer):
|
|||||||
else:
|
else:
|
||||||
read_index = 0
|
read_index = 0
|
||||||
|
|
||||||
assert _debug('_get_read_index ->', read_index)
|
assert _debug(f'_get_read_index -> {read_index}')
|
||||||
return read_index
|
return read_index
|
||||||
|
|
||||||
def _get_write_index(self):
|
def _get_write_index(self):
|
||||||
@ -323,7 +323,7 @@ class PulseAudioPlayer(AbstractAudioPlayer):
|
|||||||
else:
|
else:
|
||||||
write_index = 0
|
write_index = 0
|
||||||
|
|
||||||
assert _debug('_get_write_index ->', write_index)
|
assert _debug(f'_get_write_index -> {write_index}')
|
||||||
return write_index
|
return write_index
|
||||||
|
|
||||||
def _get_timing_info(self):
|
def _get_timing_info(self):
|
||||||
@ -365,7 +365,7 @@ class PulseAudioPlayer(AbstractAudioPlayer):
|
|||||||
dt /= 1000000
|
dt /= 1000000
|
||||||
time = timestamp + dt
|
time = timestamp + dt
|
||||||
|
|
||||||
assert _debug('get_time ->', time)
|
assert _debug('get_time -> {time}')
|
||||||
return time
|
return time
|
||||||
|
|
||||||
def set_volume(self, volume):
|
def set_volume(self, volume):
|
||||||
|
@ -213,7 +213,7 @@ class XAudio2AudioPlayer(AbstractAudioPlayer):
|
|||||||
def _add_audiodata_events(self, audio_data):
|
def _add_audiodata_events(self, audio_data):
|
||||||
for event in audio_data.events:
|
for event in audio_data.events:
|
||||||
event_cursor = self._write_cursor + event.timestamp * self.source.audio_format.bytes_per_second
|
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))
|
self._events.append((event_cursor, event))
|
||||||
|
|
||||||
def _add_audiodata_timestamp(self, audio_data):
|
def _add_audiodata_timestamp(self, audio_data):
|
||||||
|
@ -192,17 +192,19 @@ XAUDIO2_NO_VIRTUAL_AUDIO_CLIENT = 0x10000 # Used in CreateMasteringVoice to cr
|
|||||||
class IXAudio2VoiceCallback(com.Interface):
|
class IXAudio2VoiceCallback(com.Interface):
|
||||||
_methods_ = [
|
_methods_ = [
|
||||||
('OnVoiceProcessingPassStart',
|
('OnVoiceProcessingPassStart',
|
||||||
com.STDMETHOD(UINT32)),
|
com.VOIDMETHOD(UINT32)),
|
||||||
('OnVoiceProcessingPassEnd',
|
('OnVoiceProcessingPassEnd',
|
||||||
com.STDMETHOD()),
|
com.VOIDMETHOD()),
|
||||||
('onStreamEnd',
|
('OnStreamEnd',
|
||||||
com.STDMETHOD()),
|
com.VOIDMETHOD()),
|
||||||
('onBufferStart',
|
('OnBufferStart',
|
||||||
com.STDMETHOD(ctypes.c_void_p)),
|
com.VOIDMETHOD(ctypes.c_void_p)),
|
||||||
('OnBufferEnd',
|
('OnBufferEnd',
|
||||||
com.STDMETHOD(ctypes.c_void_p)),
|
com.VOIDMETHOD(ctypes.c_void_p)),
|
||||||
('OnLoopEnd',
|
('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]
|
_interfaces_ = [IXAudio2VoiceCallback]
|
||||||
|
|
||||||
def __init__(self, xa2_player):
|
def __init__(self, xa2_player):
|
||||||
|
super().__init__()
|
||||||
self.xa2_player = xa2_player
|
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):
|
def OnBufferEnd(self, pBufferContext):
|
||||||
"""At the end of playing one buffer, attempt to refill again.
|
"""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.
|
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:
|
if self.xa2_player:
|
||||||
self.xa2_player.refill_source_player()
|
self.xa2_player.refill_source_player()
|
||||||
|
|
||||||
def OnLoopEnd(self, this, pBufferContext):
|
def OnVoiceError(self, pBufferContext, hresult):
|
||||||
pass
|
|
||||||
|
|
||||||
def onVoiceError(self, this, pBufferContext, hresult):
|
|
||||||
raise Exception("Error occurred during audio playback.", hresult)
|
raise Exception("Error occurred during audio playback.", hresult)
|
||||||
|
|
||||||
|
|
||||||
@ -362,24 +350,18 @@ class IXAudio2MasteringVoice(IXAudio2Voice):
|
|||||||
class IXAudio2EngineCallback(com.Interface):
|
class IXAudio2EngineCallback(com.Interface):
|
||||||
_methods_ = [
|
_methods_ = [
|
||||||
('OnProcessingPassStart',
|
('OnProcessingPassStart',
|
||||||
com.METHOD(ctypes.c_void_p)),
|
com.VOIDMETHOD()),
|
||||||
('OnProcessingPassEnd',
|
('OnProcessingPassEnd',
|
||||||
com.METHOD(ctypes.c_void_p)),
|
com.VOIDMETHOD()),
|
||||||
('OnCriticalError',
|
('OnCriticalError',
|
||||||
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_ulong)),
|
com.VOIDMETHOD(HRESULT)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class XA2EngineCallback(com.COMObject):
|
class XA2EngineCallback(com.COMObject):
|
||||||
_interfaces_ = [IXAudio2EngineCallback]
|
_interfaces_ = [IXAudio2EngineCallback]
|
||||||
|
|
||||||
def OnProcessingPassStart(self):
|
def OnCriticalError(self, hresult):
|
||||||
pass
|
|
||||||
|
|
||||||
def OnProcessingPassEnd(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def OnCriticalError(self, this, hresult):
|
|
||||||
raise Exception("Critical Error:", hresult)
|
raise Exception("Critical Error:", hresult)
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,12 +5,11 @@ such as Rectangles, Circles, and Lines. These shapes are made
|
|||||||
internally from OpenGL primitives, and provide excellent performance
|
internally from OpenGL primitives, and provide excellent performance
|
||||||
when drawn as part of a :py:class:`~pyglet.graphics.Batch`.
|
when drawn as part of a :py:class:`~pyglet.graphics.Batch`.
|
||||||
Convenience methods are provided for positioning, changing color
|
Convenience methods are provided for positioning, changing color
|
||||||
and opacity, and rotation (where applicable). To create more
|
and opacity, and rotation (where applicable).
|
||||||
complex shapes than what is provided here, the lower level
|
The Python ``in`` operator to check whether a point is inside a shape.
|
||||||
graphics API is more appropriate.
|
|
||||||
You can also use the ``in`` operator to check whether a point is
|
To create more complex shapes than what is provided here, the lower level
|
||||||
inside a shape.
|
graphics API is more appropriate. See the :ref:`guide_graphics` for more details.
|
||||||
See the :ref:`guide_graphics` for more details.
|
|
||||||
|
|
||||||
A simple example of drawing shapes::
|
A simple example of drawing shapes::
|
||||||
|
|
||||||
@ -1466,6 +1465,122 @@ class BorderedRectangle(ShapeBase):
|
|||||||
self._update_color()
|
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):
|
class Triangle(ShapeBase):
|
||||||
def __init__(self, x, y, x2, y2, x3, y3, color=(255, 255, 255, 255),
|
def __init__(self, x, y, x2, y2, x3, y3, color=(255, 255, 255, 255),
|
||||||
batch=None, group=None):
|
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)
|
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')
|
||||||
|
@ -271,6 +271,8 @@ class DocumentLabel(layout.TextLayout):
|
|||||||
|
|
||||||
@color.setter
|
@color.setter
|
||||||
def color(self, color):
|
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})
|
self.document.set_style(0, len(self.document.text), {'color': color})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -98,7 +98,7 @@ class Caret:
|
|||||||
|
|
||||||
colors = r, g, b, self._visible_alpha, r, g, b, self._visible_alpha
|
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_x = None
|
||||||
self._ideal_line = None
|
self._ideal_line = None
|
||||||
self._next_attributes = {}
|
self._next_attributes = {}
|
||||||
@ -127,6 +127,7 @@ class Caret:
|
|||||||
|
|
||||||
Also disconnects the caret from further layout events.
|
Also disconnects the caret from further layout events.
|
||||||
"""
|
"""
|
||||||
|
clock.unschedule(self._blink)
|
||||||
self._list.delete()
|
self._list.delete()
|
||||||
self._layout.remove_handlers(self)
|
self._layout.remove_handlers(self)
|
||||||
|
|
||||||
|
@ -540,18 +540,15 @@ class _GlyphBox(_AbstractBox):
|
|||||||
x1 = x2
|
x1 = x2
|
||||||
|
|
||||||
if background_vertices:
|
if background_vertices:
|
||||||
background_indices = []
|
bg_count = len(background_vertices) // 3
|
||||||
bg_count = len(background_vertices) // 2
|
background_indices = [(0, 1, 2, 0, 2, 3)[i % 6] for i in range(bg_count * 3)]
|
||||||
decoration_program = get_default_decoration_shader()
|
decoration_program = get_default_decoration_shader()
|
||||||
for bg_idx in range(bg_count):
|
background_list = decoration_program.vertex_list_indexed(bg_count, GL_TRIANGLES, background_indices,
|
||||||
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,
|
|
||||||
layout.batch, layout.background_decoration_group,
|
layout.batch, layout.background_decoration_group,
|
||||||
position=('f', background_vertices),
|
position=('f', background_vertices),
|
||||||
colors=('Bn', background_colors),
|
colors=('Bn', background_colors),
|
||||||
rotation=('f', (rotation,) * 4),
|
rotation=('f', (rotation,) * bg_count),
|
||||||
anchor=('f', (anchor_x, anchor_y) * 4))
|
anchor=('f', (anchor_x, anchor_y) * bg_count))
|
||||||
context.add_list(background_list)
|
context.add_list(background_list)
|
||||||
|
|
||||||
if underline_vertices:
|
if underline_vertices:
|
||||||
|
@ -3,11 +3,13 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Optional, Union, Callable
|
||||||
|
|
||||||
import pyglet
|
import pyglet
|
||||||
|
from pyglet.customtypes import Buffer
|
||||||
|
|
||||||
|
|
||||||
def asbytes(s):
|
def asbytes(s: Union[str, Buffer]) -> bytes:
|
||||||
if isinstance(s, bytes):
|
if isinstance(s, bytes):
|
||||||
return s
|
return s
|
||||||
elif isinstance(s, str):
|
elif isinstance(s, str):
|
||||||
@ -16,56 +18,68 @@ def asbytes(s):
|
|||||||
return bytes(s)
|
return bytes(s)
|
||||||
|
|
||||||
|
|
||||||
def asbytes_filename(s):
|
def asstr(s: Optional[Union[str, Buffer]]) -> str:
|
||||||
if isinstance(s, bytes):
|
|
||||||
return s
|
|
||||||
elif isinstance(s, str):
|
|
||||||
return s.encode(encoding=sys.getfilesystemencoding())
|
|
||||||
|
|
||||||
|
|
||||||
def asstr(s):
|
|
||||||
if s is None:
|
if s is None:
|
||||||
return ''
|
return ''
|
||||||
if isinstance(s, str):
|
if isinstance(s, str):
|
||||||
return s
|
return s
|
||||||
return s.decode("utf-8")
|
return s.decode("utf-8") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def debug_print(enabled_or_option='debug'):
|
# Keep these outside of the function since we don't need to re-define
|
||||||
"""Get a debug printer that is enabled based on a boolean input or a pyglet option.
|
# the function each time we make a call since no state is persisted.
|
||||||
The debug print function returned should be used in an assert. This way it can be
|
def _debug_print_real(arg: str) -> bool:
|
||||||
optimized out when running python with the -O flag.
|
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::
|
Usage example::
|
||||||
|
|
||||||
from pyglet.debug import debug_print
|
from pyglet.util import debug_print
|
||||||
|
|
||||||
|
|
||||||
_debug_media = debug_print('debug_media')
|
_debug_media = debug_print('debug_media')
|
||||||
|
|
||||||
|
|
||||||
def some_func():
|
def some_func():
|
||||||
|
# Python will skip the line below when run with -O
|
||||||
assert _debug_media('My debug statement')
|
assert _debug_media('My debug statement')
|
||||||
|
|
||||||
:parameters:
|
# The rest of the function will run as normal
|
||||||
`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.
|
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 = pyglet.options.get(pyglet_option_name, False)
|
||||||
enabled = enabled_or_option
|
|
||||||
else:
|
|
||||||
enabled = pyglet.options.get(enabled_or_option, False)
|
|
||||||
|
|
||||||
if enabled:
|
if enabled:
|
||||||
def _debug_print(*args, **kwargs):
|
return _debug_print_real
|
||||||
print(*args, **kwargs)
|
return _debug_print_dummy
|
||||||
return True
|
|
||||||
|
|
||||||
else:
|
|
||||||
def _debug_print(*args, **kwargs):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return _debug_print
|
|
||||||
|
|
||||||
|
|
||||||
class CodecRegistry:
|
class CodecRegistry:
|
||||||
|
@ -273,9 +273,10 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
|
|||||||
conventions. This will ensure it is not obscured by other windows,
|
conventions. This will ensure it is not obscured by other windows,
|
||||||
and appears on an appropriate screen for the user.
|
and appears on an appropriate screen for the user.
|
||||||
|
|
||||||
To render into a window, you must first call `switch_to`, to make
|
To render into a window, you must first call its :py:meth:`.switch_to`
|
||||||
it the current OpenGL context. If you use only one window in the
|
method to make it the active OpenGL context. If you use only one
|
||||||
application, there is no need to do this.
|
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
|
# 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.
|
"""Clear the window.
|
||||||
|
|
||||||
This is a convenience method for clearing the color and depth
|
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)
|
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
|
||||||
|
|
||||||
@ -646,10 +648,12 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
|
|||||||
"""Close the window.
|
"""Close the window.
|
||||||
|
|
||||||
After closing the window, the GL context will be invalid. The
|
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
|
The :py:meth:`pyglet.app.EventLoop.on_window_close` event is
|
||||||
`pyglet.app.event_loop` when this method is called.
|
dispatched by the :py:attr:`pyglet.app.event_loop` when this method
|
||||||
|
is called.
|
||||||
"""
|
"""
|
||||||
from pyglet import app
|
from pyglet import app
|
||||||
if not self._context:
|
if not self._context:
|
||||||
@ -676,7 +680,7 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
|
|||||||
and advanced applications that must integrate their event loop
|
and advanced applications that must integrate their event loop
|
||||||
into another framework.
|
into another framework.
|
||||||
|
|
||||||
Typical applications should use `pyglet.app.run`.
|
Typical applications should use :py:func:`pyglet.app.run`.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError('abstract')
|
raise NotImplementedError('abstract')
|
||||||
|
|
||||||
@ -715,11 +719,14 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
|
|||||||
"""Swap the OpenGL front and back buffers.
|
"""Swap the OpenGL front and back buffers.
|
||||||
|
|
||||||
Call this method on a double-buffered window to update the
|
Call this method on a double-buffered window to update the
|
||||||
visible display with the back buffer. The contents of the back buffer
|
visible display with the back buffer. Windows are
|
||||||
is undefined after this operation.
|
double-buffered by default unless you turn this feature off.
|
||||||
|
|
||||||
Windows are double-buffered by default. This method is called
|
The contents of the back buffer are undefined after this operation.
|
||||||
automatically by `EventLoop` after the :py:meth:`~pyglet.window.Window.on_draw` event.
|
|
||||||
|
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')
|
raise NotImplementedError('abstract')
|
||||||
|
|
||||||
@ -791,6 +798,25 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
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):
|
def minimize(self):
|
||||||
"""Minimize the window.
|
"""Minimize the window.
|
||||||
"""
|
"""
|
||||||
@ -1158,10 +1184,13 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
|
|||||||
def switch_to(self):
|
def switch_to(self):
|
||||||
"""Make this window the current OpenGL rendering context.
|
"""Make this window the current OpenGL rendering context.
|
||||||
|
|
||||||
Only one OpenGL context can be active at a time. This method sets
|
Only one OpenGL context can be active at a time. This method
|
||||||
the current window's context to be current. You should use this
|
sets the current window context as the active one.
|
||||||
method in preference to `pyglet.gl.Context.set_current`, as it may
|
|
||||||
perform additional initialisation functions.
|
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')
|
raise NotImplementedError('abstract')
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ NSColor = cocoapy.ObjCClass('NSColor')
|
|||||||
NSEvent = cocoapy.ObjCClass('NSEvent')
|
NSEvent = cocoapy.ObjCClass('NSEvent')
|
||||||
NSArray = cocoapy.ObjCClass('NSArray')
|
NSArray = cocoapy.ObjCClass('NSArray')
|
||||||
NSImage = cocoapy.ObjCClass('NSImage')
|
NSImage = cocoapy.ObjCClass('NSImage')
|
||||||
|
NSPasteboard = cocoapy.ObjCClass('NSPasteboard')
|
||||||
|
|
||||||
quartz = cocoapy.quartz
|
quartz = cocoapy.quartz
|
||||||
cf = cocoapy.cf
|
cf = cocoapy.cf
|
||||||
@ -567,5 +568,32 @@ class CocoaWindow(BaseWindow):
|
|||||||
NSApp = NSApplication.sharedApplication()
|
NSApp = NSApplication.sharedApplication()
|
||||||
NSApp.setPresentationOptions_(options)
|
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"]
|
__all__ = ["CocoaWindow"]
|
||||||
|
@ -653,6 +653,43 @@ class Win32Window(BaseWindow):
|
|||||||
|
|
||||||
return icon
|
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
|
# Private util
|
||||||
|
|
||||||
def _client_to_window_size(self, width, height):
|
def _client_to_window_size(self, width, height):
|
||||||
|
@ -4,6 +4,7 @@ import urllib.parse
|
|||||||
|
|
||||||
from ctypes import *
|
from ctypes import *
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import pyglet
|
import pyglet
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
_have_xsync = False
|
_have_xsync = False
|
||||||
|
|
||||||
|
_debug = pyglet.options['debug_x11']
|
||||||
|
|
||||||
class mwmhints_t(Structure):
|
class mwmhints_t(Structure):
|
||||||
_fields_ = [
|
_fields_ = [
|
||||||
@ -47,6 +49,7 @@ _can_detect_autorepeat = None
|
|||||||
|
|
||||||
XA_CARDINAL = 6 # Xatom.h:14
|
XA_CARDINAL = 6 # Xatom.h:14
|
||||||
XA_ATOM = 4
|
XA_ATOM = 4
|
||||||
|
XA_STRING = 31
|
||||||
|
|
||||||
XDND_VERSION = 5
|
XDND_VERSION = 5
|
||||||
|
|
||||||
@ -142,6 +145,9 @@ class XlibWindow(BaseWindow):
|
|||||||
if _can_detect_autorepeat:
|
if _can_detect_autorepeat:
|
||||||
self.pressed_keys = set()
|
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):
|
def _recreate(self, changes):
|
||||||
# If flipping to/from fullscreen, need to recreate the window. (This
|
# If flipping to/from fullscreen, need to recreate the window. (This
|
||||||
# is the case with both override_redirect method and
|
# is the case with both override_redirect method and
|
||||||
@ -287,6 +293,12 @@ class XlibWindow(BaseWindow):
|
|||||||
# Atoms required for Xdnd
|
# Atoms required for Xdnd
|
||||||
self._create_xdnd_atoms(self._x_display)
|
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.
|
# Support for drag and dropping files needs to be enabled.
|
||||||
if self._file_drops:
|
if self._file_drops:
|
||||||
# Some variables set because there are 4 different drop events that need shared data.
|
# 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,
|
xlib.XChangeProperty(self._x_display, self._window, atom, XA_CARDINAL,
|
||||||
32, xlib.PropModeReplace, buffer, len(data)//sizeof(c_ulong))
|
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
|
# Private utility
|
||||||
|
|
||||||
def _set_wm_normal_hints(self):
|
def _set_wm_normal_hints(self):
|
||||||
@ -1328,7 +1407,7 @@ class XlibWindow(BaseWindow):
|
|||||||
xlib.XFree(data)
|
xlib.XFree(data)
|
||||||
|
|
||||||
def get_single_property(self, window, atom_property, atom_type):
|
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()
|
actualAtom = xlib.Atom()
|
||||||
actualFormat = c_int()
|
actualFormat = c_int()
|
||||||
itemCount = c_ulong()
|
itemCount = c_ulong()
|
||||||
@ -1343,14 +1422,14 @@ class XlibWindow(BaseWindow):
|
|||||||
byref(bytesAfter),
|
byref(bytesAfter),
|
||||||
data)
|
data)
|
||||||
|
|
||||||
return data, itemCount.value
|
return data, itemCount.value, actualAtom.value
|
||||||
|
|
||||||
@XlibEventHandler(xlib.SelectionNotify)
|
@XlibEventHandler(xlib.SelectionNotify)
|
||||||
def _event_selection_notification(self, ev):
|
def _event_selection_notification(self, ev):
|
||||||
if ev.xselection.property != 0 and ev.xselection.selection == self._xdnd_atoms['XdndSelection']:
|
if ev.xselection.property != 0 and ev.xselection.selection == self._xdnd_atoms['XdndSelection']:
|
||||||
if self._xdnd_format:
|
if self._xdnd_format:
|
||||||
# This will get the data
|
# 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.property,
|
||||||
ev.xselection.target)
|
ev.xselection.target)
|
||||||
|
|
||||||
@ -1514,5 +1593,61 @@ class XlibWindow(BaseWindow):
|
|||||||
self._mapped = False
|
self._mapped = False
|
||||||
self.dispatch_event('on_hide')
|
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"]
|
__all__ = ["XlibEventHandler", "XlibWindow"]
|
||||||
|
@ -1086,9 +1086,6 @@ def test_logger(the_logger: Logger):
|
|||||||
the_logger.debug('debugging')
|
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.info("Hello World!!")
|
|
||||||
the_logger.info("Hello World!!")
|
|
||||||
the_logger.warn('warning')
|
the_logger.warn('warning')
|
||||||
the_logger.warn('warning')
|
the_logger.warn('warning')
|
||||||
the_logger.error('error haaaa')
|
the_logger.error('error haaaa')
|
||||||
@ -1111,7 +1108,7 @@ if __name__ == "__main__":
|
|||||||
a_logger.error('error haaaa')
|
a_logger.error('error haaaa')
|
||||||
a_logger.fatal('oh no')
|
a_logger.fatal('oh no')
|
||||||
logger.info('my name is:', logger.name)
|
logger.info('my name is:', logger.name)
|
||||||
for _ in range(5):
|
# for _ in range(5):
|
||||||
test_logger(logger)
|
test_logger(logger)
|
||||||
test_logger(a_logger)
|
test_logger(a_logger)
|
||||||
print(Message_content(log_time=time.time(), text='aaa', level=4, marker='abc', end='abc', flush=False,
|
print(Message_content(log_time=time.time(), text='aaa', level=4, marker='abc', end='abc', flush=False,
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
# -------------------------------
|
|
||||||
# Difficult Rocket
|
|
||||||
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
|
|
||||||
# All rights reserved
|
|
||||||
# -------------------------------
|
|
||||||
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
18
mods/dr_game/Difficult_Rocket_rs/src/Cargo.lock
generated
18
mods/dr_game/Difficult_Rocket_rs/src/Cargo.lock
generated
@ -116,7 +116,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "difficult_rocket_rs"
|
name = "difficult_rocket_rs"
|
||||||
version = "0.2.22"
|
version = "0.2.23"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pyo3",
|
"pyo3",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
@ -415,9 +415,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.30.0"
|
version = "0.31.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
|
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"serde",
|
"serde",
|
||||||
@ -497,9 +497,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.186"
|
version = "1.0.190"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1"
|
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
@ -518,9 +518,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.186"
|
version = "1.0.190"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670"
|
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -708,6 +708,6 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xml-rs"
|
name = "xml-rs"
|
||||||
version = "0.8.16"
|
version = "0.8.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47430998a7b5d499ccee752b41567bc3afc57e1327dc855b1a2aa44ce29b5fa1"
|
checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "difficult_rocket_rs"
|
name = "difficult_rocket_rs"
|
||||||
version = "0.2.22"
|
version = "0.2.23"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license-file = '../../LICENSE'
|
license-file = '../../LICENSE'
|
||||||
authors = [
|
authors = [
|
||||||
@ -22,15 +22,15 @@ opt-level = "s"
|
|||||||
# codegen-units = 1
|
# codegen-units = 1
|
||||||
|
|
||||||
[dependencies.quick-xml]
|
[dependencies.quick-xml]
|
||||||
version = "0.30.0"
|
version = "0.31.0"
|
||||||
features = ["serialize"]
|
features = ["serialize"]
|
||||||
|
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
version = "1.0.186"
|
version = "1.0.190"
|
||||||
features = ["derive"]
|
features = ["derive"]
|
||||||
|
|
||||||
[dependencies.xml-rs]
|
[dependencies.xml-rs]
|
||||||
version = "0.8.16"
|
version = "0.8.19"
|
||||||
|
|
||||||
[dependencies.serde-xml-rs]
|
[dependencies.serde-xml-rs]
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
|
@ -16,7 +16,7 @@ from Difficult_Rocket.api.mod import ModInfo
|
|||||||
from Difficult_Rocket.client import ClientWindow
|
from Difficult_Rocket.client import ClientWindow
|
||||||
from Difficult_Rocket.api.types import Options, Version
|
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')
|
logger = logging.getLogger('client.dr_game')
|
||||||
|
|
||||||
|
@ -48,13 +48,9 @@ if __name__ == '__main__':
|
|||||||
compiler.save_report = True
|
compiler.save_report = True
|
||||||
sys.argv.remove('--report')
|
sys.argv.remove('--report')
|
||||||
|
|
||||||
# 检测 --output xx 参数
|
if '--lto=yes' in sys.argv:
|
||||||
if '--output' in sys.argv:
|
compiler.use_lto = True
|
||||||
# 输入的是输出目录
|
sys.argv.remove('--lto=yes')
|
||||||
out_path = sys.argv[sys.argv.index('--output') + 1]
|
|
||||||
compiler.output_path = Path(out_path)
|
|
||||||
sys.argv.remove('--output')
|
|
||||||
sys.argv.remove(out_path)
|
|
||||||
|
|
||||||
# 检测 --no-pyglet-opt 参数
|
# 检测 --no-pyglet-opt 参数
|
||||||
pyglet_optimizations = True
|
pyglet_optimizations = True
|
||||||
@ -88,6 +84,16 @@ if __name__ == '__main__':
|
|||||||
pprint(compiler.option())
|
pprint(compiler.option())
|
||||||
else:
|
else:
|
||||||
compiler.output_path = Path(f'./build/nuitka-{platform.system().lower()}')
|
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())
|
print(compiler.as_markdown())
|
||||||
|
|
||||||
|
@ -4,23 +4,22 @@
|
|||||||
|
|
||||||
# for images
|
# for images
|
||||||
# not for pypy >= 3.10
|
# 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
|
# for sys info
|
||||||
psutil >= 5.9.5
|
psutil >= 5.9.6
|
||||||
|
|
||||||
# for files
|
# for files
|
||||||
rtoml >= 0.9.0
|
rtoml >= 0.9.0
|
||||||
tomlkit >= 0.11.8
|
tomlkit >= 0.12.1
|
||||||
defusedxml >= 0.7.1
|
defusedxml >= 0.7.1
|
||||||
|
|
||||||
# for report error
|
# for report error
|
||||||
objprint >= 0.2.2
|
objprint >= 0.2.3
|
||||||
|
|
||||||
# for compile
|
# for compile
|
||||||
nuitka >= 1.8.2
|
nuitka >= 1.8.5
|
||||||
ordered-set >= 4.1.0
|
imageio >= 2.31.6; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
|
||||||
imageio >= 2.31.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
|
wheel >= 0.41.3
|
||||||
wheel >= 0.40.0
|
setuptools >= 68.2.2
|
||||||
setuptools >= 67.8.0
|
setuptools-rust >= 1.8.1
|
||||||
setuptools-rust >= 1.6.0
|
|
@ -5,25 +5,24 @@
|
|||||||
|
|
||||||
# for images
|
# for images
|
||||||
# not for pypy >= 3.10
|
# 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
|
# for sys info
|
||||||
psutil >= 5.9.5
|
psutil >= 5.9.6
|
||||||
|
|
||||||
# for files
|
# for files
|
||||||
rtoml >= 0.9.0
|
rtoml >= 0.9.0
|
||||||
tomlkit >= 0.11.8
|
tomlkit >= 0.12.1
|
||||||
defusedxml >= 0.7.1
|
defusedxml >= 0.7.1
|
||||||
|
|
||||||
# for debug
|
# for debug
|
||||||
objprint >= 0.2.2
|
objprint >= 0.2.3
|
||||||
viztracer >= 0.15.6; platform_python_implementation != "PyPy"
|
viztracer >= 0.16.0; platform_python_implementation != "PyPy"
|
||||||
vizplugins >= 0.1.3; platform_python_implementation != "PyPy"
|
vizplugins >= 0.1.3; platform_python_implementation != "PyPy"
|
||||||
|
|
||||||
# for compile
|
# for compile
|
||||||
nuitka >= 1.8.2
|
nuitka >= 1.8.5
|
||||||
ordered-set >= 4.1.0
|
imageio >= 2.31.6; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
|
||||||
imageio >= 2.31.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
|
wheel >= 0.41.3
|
||||||
wheel >= 0.40.0
|
setuptools >= 68.2.2
|
||||||
setuptools >= 67.8.0
|
setuptools-rust >= 1.8.1
|
||||||
setuptools-rust >= 1.6.0
|
|
||||||
|
@ -3,15 +3,15 @@
|
|||||||
|
|
||||||
# for images
|
# for images
|
||||||
# not for pypy >= 3.10
|
# 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
|
# for sys info
|
||||||
psutil >= 5.9.5
|
psutil >= 5.9.6
|
||||||
|
|
||||||
# for files
|
# for files
|
||||||
rtoml >= 0.9.0
|
rtoml >= 0.9.0
|
||||||
tomlkit >= 0.11.8
|
tomlkit >= 0.12.1
|
||||||
defusedxml >= 0.7.1
|
defusedxml >= 0.7.1
|
||||||
|
|
||||||
# for report error
|
# for report error
|
||||||
objprint >= 0.2.2
|
objprint >= 0.2.3
|
||||||
|
Loading…
Reference in New Issue
Block a user