V2-202509
|
After Width: | Height: | Size: 376 B |
|
After Width: | Height: | Size: 345 B |
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Icon=/usr/share/pixmaps/screen-remap.png
|
||||
GenericName=RemapTouchAuto
|
||||
GenericName[zh_CN]=映射触摸屏(自启动)
|
||||
Name=RemapTouchAuto
|
||||
Name[zh_CN]=映射触摸屏(自启动)
|
||||
Comment=Remap Touch-Screens Now.
|
||||
Comment[zh_CN]=立即修复触摸紊乱
|
||||
Exec=/opt/ktouch/kscreen-remap -s
|
||||
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Icon=/usr/share/pixmaps/screen-config.png
|
||||
GenericName=ktouch-setup
|
||||
GenericName[zh_CN]=配置触摸映射
|
||||
Name=ConfigTouchMAP
|
||||
Name[zh_CN]=配置触摸映射
|
||||
Comment=Config MAP between Touch-devices and Screens
|
||||
Comment[zh_CN]=配置触摸设备和屏幕的映射关系
|
||||
Exec=/opt/ktouch/kscreen-setup
|
||||
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Icon=/usr/share/pixmaps/screen-remap.png
|
||||
GenericName=RemapTouchNOW
|
||||
GenericName[zh_CN]=映射触摸屏
|
||||
Name=RemapTouch
|
||||
Name[zh_CN]=映射触摸屏
|
||||
Comment=Remap Touch-Screens Now.
|
||||
Comment[zh_CN]=立即修复触摸紊乱
|
||||
Exec=/opt/ktouch/kscreen-remap
|
||||
@@ -0,0 +1,943 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
import re
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# 配置日志
|
||||
def setup_logging():
|
||||
apppath="/opt/ktouch"
|
||||
log_file = os.path.join(apppath, 'sub_modules.log')
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s display_monitor [%(levelname)s] %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(log_file, encoding='utf-8'),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
logger = setup_logging()
|
||||
|
||||
class DisplayChecker:
|
||||
def __init__(self, display_env=":0"):
|
||||
"""初始化显示器检查器"""
|
||||
self.display_env = display_env
|
||||
self.display_name = None
|
||||
self.config_data = None
|
||||
self.current_settings = None
|
||||
|
||||
def parse_config_file(self, config_path):
|
||||
"""
|
||||
解析配置文件
|
||||
格式: 分辨率@刷新率|坐标|旋转方向|是否是主屏|模式标志
|
||||
示例: 1920x1080@60.0Hz|0,0|1|P|A 或 1920x1080@60.0Hz|0,0|1|P|F
|
||||
F: 必须通过cvt生成分辨率并设置
|
||||
A: 沿用原来的方法(即xrandr存在分辨率就使用系统分辨率,否则使用cvt)
|
||||
"""
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
content = f.read().strip()
|
||||
|
||||
# 从文件名获取显示器名称(去掉后缀)
|
||||
self.display_name = Path(config_path).stem
|
||||
|
||||
# 解析配置内容
|
||||
parts = content.split('|')
|
||||
if len(parts) != 5:
|
||||
raise ValueError("配置文件格式错误,应该有5个部分")
|
||||
|
||||
# 解析分辨率和刷新率
|
||||
res_refresh_match = re.match(r'(\d+)x(\d+)@([\d.]+)Hz', parts[0])
|
||||
if not res_refresh_match:
|
||||
raise ValueError("分辨率刷新率格式错误")
|
||||
|
||||
width, height, refresh_rate = res_refresh_match.groups()
|
||||
|
||||
# 解析坐标
|
||||
pos_match = re.match(r'(-?\d+),(-?\d+)', parts[1])
|
||||
if not pos_match:
|
||||
raise ValueError("坐标格式错误")
|
||||
|
||||
pos_x, pos_y = pos_match.groups()
|
||||
|
||||
# 解析旋转方向
|
||||
rotation_map = {'1': 'normal', '2': 'left', '4': 'inverted', '8': 'right'}
|
||||
rotation = rotation_map.get(parts[2])
|
||||
if not rotation:
|
||||
raise ValueError(f"旋转方向错误: {parts[2]}")
|
||||
|
||||
# 解析主屏设置
|
||||
primary = parts[3] == 'P'
|
||||
|
||||
# 解析模式标志
|
||||
mode_flag = parts[4].strip()
|
||||
if mode_flag not in ['F', 'A']:
|
||||
raise ValueError(f"模式标志错误: {mode_flag},应该是 F 或 A")
|
||||
|
||||
self.config_data = {
|
||||
'width': int(width),
|
||||
'height': int(height),
|
||||
'refresh_rate': float(refresh_rate),
|
||||
'pos_x': int(pos_x),
|
||||
'pos_y': int(pos_y),
|
||||
'rotation': rotation,
|
||||
'primary': primary,
|
||||
'mode_flag': mode_flag # 新增字段
|
||||
}
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"解析配置文件错误: {e}")
|
||||
return False
|
||||
|
||||
def is_display_connected(self):
|
||||
"""检查显示器是否连接"""
|
||||
env = os.environ.copy()
|
||||
env['DISPLAY'] = self.display_env
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
['xrandr'],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error("获取显示器信息失败")
|
||||
return False
|
||||
|
||||
# 查找显示器连接状态
|
||||
lines = result.stdout.split('\n')
|
||||
for line in lines:
|
||||
if line.startswith(f'{self.display_name} '):
|
||||
if 'connected' in line and 'disconnected' not in line:
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"显示器 {self.display_name} 未连接或已断开")
|
||||
return False
|
||||
|
||||
logger.warning(f"未找到显示器 {self.display_name}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"检查显示器连接状态错误: {e}")
|
||||
return False
|
||||
|
||||
def run_xrandr_command(self, cmd, ignore_errors=False):
|
||||
"""运行xrandr命令"""
|
||||
env = os.environ.copy()
|
||||
env['DISPLAY'] = self.display_env
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(cmd, shell=True, env=env, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
if ignore_errors:
|
||||
# 检查是否是"已经存在"之类的错误
|
||||
error_lower = result.stderr.lower()
|
||||
if any(keyword in error_lower for keyword in ['already exists', 'already set', 'exist']):
|
||||
logger.info(f"忽略预期中的错误: {result.stderr.strip()}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"命令执行失败但忽略错误: {cmd}")
|
||||
logger.warning(f"错误信息: {result.stderr}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"命令执行失败: {cmd}")
|
||||
logger.error(f"错误信息: {result.stderr}")
|
||||
return False
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"执行命令错误: {e}")
|
||||
return False
|
||||
|
||||
def get_current_display_settings(self):
|
||||
"""获取当前显示器的设置"""
|
||||
# 首先检查显示器是否连接
|
||||
if not self.is_display_connected():
|
||||
return None
|
||||
|
||||
env = os.environ.copy()
|
||||
env['DISPLAY'] = self.display_env
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
['xrandr'],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error("获取显示器信息失败")
|
||||
return None
|
||||
|
||||
# 解析xrandr输出,找到指定显示器的当前设置
|
||||
lines = result.stdout.split('\n')
|
||||
current_settings = {}
|
||||
|
||||
for line in lines:
|
||||
# 查找目标显示器的连接状态行
|
||||
if line.startswith(f'{self.display_name} '):
|
||||
# 检查是否是主屏
|
||||
current_settings['primary'] = 'primary' in line
|
||||
|
||||
# 提取当前分辨率
|
||||
res_match = re.search(r'(\d+x\d+)\+(-?\d+)\+(-?\d+)', line)
|
||||
if res_match:
|
||||
current_settings['resolution'] = res_match.group(1)
|
||||
current_settings['pos_x'] = int(res_match.group(2))
|
||||
current_settings['pos_y'] = int(res_match.group(3))
|
||||
|
||||
# 提取旋转信息
|
||||
if 'inverted' in line and 'x axis y axis' not in line:
|
||||
current_settings['rotation'] = 'inverted'
|
||||
elif 'left' in line and 'x axis y axis' not in line:
|
||||
current_settings['rotation'] = 'left'
|
||||
elif 'right' in line and 'x axis y axis' not in line:
|
||||
current_settings['rotation'] = 'right'
|
||||
else:
|
||||
current_settings['rotation'] = 'normal'
|
||||
|
||||
# 继续查找当前模式行以获取刷新率
|
||||
for next_line in lines[lines.index(line)+1:]:
|
||||
if next_line.strip() and not next_line.startswith(' '):
|
||||
break
|
||||
|
||||
# 查找当前模式(带*的)
|
||||
if '*'+'+' in next_line or '* ' in next_line:
|
||||
rate_match = re.search(r'(\d+\.\d+)\*', next_line)
|
||||
if rate_match:
|
||||
current_settings['refresh_rate'] = float(rate_match.group(1))
|
||||
break
|
||||
|
||||
self.current_settings = current_settings
|
||||
return current_settings
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取当前显示器设置错误: {e}")
|
||||
return None
|
||||
|
||||
def get_display_modes(self):
|
||||
"""
|
||||
获取显示器支持的模式列表
|
||||
返回: 字典,键为分辨率,值为该分辨率下支持的刷新率列表
|
||||
"""
|
||||
# 首先检查显示器是否连接
|
||||
if not self.is_display_connected():
|
||||
return {}
|
||||
|
||||
env = os.environ.copy()
|
||||
env['DISPLAY'] = self.display_env
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
['xrandr'],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error("获取显示器信息失败")
|
||||
return {}
|
||||
|
||||
# 解析xrandr输出,找到指定显示器的模式
|
||||
lines = result.stdout.split('\n')
|
||||
modes = {}
|
||||
in_target_display = False
|
||||
current_resolution = None
|
||||
|
||||
for line in lines:
|
||||
# 检查是否进入目标显示器的部分
|
||||
if line.startswith(f'{self.display_name} '):
|
||||
in_target_display = True
|
||||
continue
|
||||
elif in_target_display and line.strip() and not line.startswith(' '):
|
||||
# 新的显示器部分开始,退出
|
||||
break
|
||||
|
||||
if in_target_display:
|
||||
# 解析分辨率行
|
||||
res_match = re.search(r'^\s*(\d+x\d+i?)\s+', line)
|
||||
if res_match:
|
||||
current_resolution = res_match.group(1)
|
||||
modes[current_resolution] = []
|
||||
|
||||
# 解析刷新率
|
||||
if current_resolution:
|
||||
rate_matches = re.findall(r'(\d+\.\d+)\*?\+?', line)
|
||||
for rate in rate_matches:
|
||||
modes[current_resolution].append(float(rate))
|
||||
|
||||
return modes
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取显示器模式错误: {e}")
|
||||
return {}
|
||||
|
||||
def get_existing_modelines(self):
|
||||
"""获取已经存在的自定义模式"""
|
||||
env = os.environ.copy()
|
||||
env['DISPLAY'] = self.display_env
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
['xrandr'],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
|
||||
# 查找自定义模式
|
||||
lines = result.stdout.split('\n')
|
||||
modelines = []
|
||||
capturing_modelines = True
|
||||
|
||||
for line in lines:
|
||||
if re.match(r'^\S+ connected', line):
|
||||
capturing_modelines = False
|
||||
continue
|
||||
|
||||
if capturing_modelines and line.strip():
|
||||
mode_match = re.match(r'^\s*(\S+)\s+.*?(\d+\.\d+)\s+.*?(\d+)\s+.*?(\d+)\s+.*?(\d+)\s+.*?(\d+)\s+.*?(\d+)\s+.*?(\d+)\s+.*?(\d+)', line)
|
||||
if mode_match:
|
||||
modelines.append(mode_match.group(1))
|
||||
|
||||
return modelines
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取已存在模式错误: {e}")
|
||||
return []
|
||||
|
||||
def find_matching_mode(self, modes):
|
||||
"""在支持的模式中查找匹配的模式(允许刷新率误差)"""
|
||||
target_res = f"{self.config_data['width']}x{self.config_data['height']}"
|
||||
target_refresh = self.config_data['refresh_rate']
|
||||
|
||||
# 检查精确匹配的分辨率
|
||||
if target_res in modes:
|
||||
for refresh_rate in modes[target_res]:
|
||||
# 允许±2fps的误差
|
||||
if abs(refresh_rate - target_refresh) <= 2.0:
|
||||
return f"{target_res} {refresh_rate}"
|
||||
|
||||
# 检查隔行扫描变体
|
||||
interlaced_res = f"{target_res}i"
|
||||
if interlaced_res in modes:
|
||||
for refresh_rate in modes[interlaced_res]:
|
||||
if abs(refresh_rate - target_refresh) <= 2.0:
|
||||
return f"{interlaced_res} {refresh_rate}"
|
||||
|
||||
return None
|
||||
|
||||
def create_and_add_mode(self):
|
||||
"""使用cvt创建并添加新的显示模式"""
|
||||
width = self.config_data['width']
|
||||
height = self.config_data['height']
|
||||
refresh_rate = self.config_data['refresh_rate']
|
||||
|
||||
# 生成模式名称
|
||||
mode_name = f"{width}x{height}_{refresh_rate}"
|
||||
|
||||
# 检查模式是否已经存在
|
||||
existing_modelines = self.get_existing_modelines()
|
||||
if mode_name in existing_modelines:
|
||||
logger.info(f"模式 {mode_name} 已经存在,跳过创建")
|
||||
else:
|
||||
# 使用cvt生成模式行
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
['cvt', str(width), str(height), str(refresh_rate)],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error("cvt命令执行失败")
|
||||
return None
|
||||
|
||||
# 从cvt输出中提取模式行
|
||||
lines = result.stdout.split('\n')
|
||||
modeline = None
|
||||
for line in lines:
|
||||
if line.startswith('Modeline '):
|
||||
modeline = line.replace('Modeline ', '').strip()
|
||||
break
|
||||
|
||||
if not modeline:
|
||||
logger.error("无法从cvt输出中提取模式行")
|
||||
return None
|
||||
|
||||
# 添加新模式
|
||||
add_mode_cmd = f"xrandr --newmode {mode_name} {modeline.split(' ', 1)[1]}"
|
||||
if not self.run_xrandr_command(add_mode_cmd, ignore_errors=True):
|
||||
return None
|
||||
logger.info(f"成功创建新模式: {mode_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建模式错误: {e}")
|
||||
return None
|
||||
|
||||
# 将新模式添加到显示器
|
||||
add_to_output_cmd = f"xrandr --addmode {self.display_name} {mode_name}"
|
||||
if not self.run_xrandr_command(add_to_output_cmd, ignore_errors=True):
|
||||
return None
|
||||
|
||||
return mode_name
|
||||
|
||||
def parse_resolution(self, resolution_str):
|
||||
"""解析分辨率字符串,返回宽高元组"""
|
||||
try:
|
||||
if 'x' in resolution_str:
|
||||
width, height = resolution_str.split('x')
|
||||
height = height.replace('i', '')
|
||||
return int(width), int(height)
|
||||
return 0, 0
|
||||
except:
|
||||
return 0, 0
|
||||
|
||||
def compare_settings(self):
|
||||
"""比较当前设置与配置设置"""
|
||||
if not self.current_settings or not self.config_data:
|
||||
return False, "无法获取当前设置或配置数据"
|
||||
|
||||
# 检查分辨率(允许±10像素误差)
|
||||
current_res = self.current_settings.get('resolution', '')
|
||||
current_width, current_height = self.parse_resolution(current_res)
|
||||
target_width = self.config_data['width']
|
||||
target_height = self.config_data['height']
|
||||
|
||||
width_diff = abs(current_width - target_width)
|
||||
height_diff = abs(current_height - target_height)
|
||||
|
||||
if width_diff > 10 or height_diff > 10:
|
||||
return False, f"分辨率不匹配: 当前 {current_res}, 配置 {target_width}x{target_height}"
|
||||
|
||||
# 检查刷新率(允许±2fps误差)
|
||||
current_refresh = self.current_settings.get('refresh_rate', 0)
|
||||
target_refresh = self.config_data['refresh_rate']
|
||||
refresh_diff = abs(current_refresh - target_refresh)
|
||||
|
||||
if refresh_diff > 2.0:
|
||||
return False, f"刷新率不匹配: 当前 {current_refresh:.1f}Hz, 配置 {target_refresh:.1f}Hz"
|
||||
|
||||
# 检查旋转
|
||||
current_rotation = self.current_settings.get('rotation', 'normal')
|
||||
if current_rotation != self.config_data['rotation']:
|
||||
return False, f"旋转不匹配: 当前 {current_rotation}, 配置 {self.config_data['rotation']}"
|
||||
|
||||
# 检查主屏设置
|
||||
current_primary = self.current_settings.get('primary', False)
|
||||
if current_primary != self.config_data['primary']:
|
||||
primary_status_current = "是" if current_primary else "否"
|
||||
primary_status_target = "是" if self.config_data['primary'] else "否"
|
||||
return False, f"主屏设置不匹配: 当前 {primary_status_current}, 配置 {primary_status_target}"
|
||||
|
||||
# 位置不进行校验
|
||||
match_details = [
|
||||
f"分辨率: {current_res}",
|
||||
f"刷新率: {current_refresh:.1f}Hz",
|
||||
f"旋转: {current_rotation}",
|
||||
f"主屏: {'是' if current_primary else '否'}",
|
||||
"位置: 跳过校验"
|
||||
]
|
||||
|
||||
return True, " | ".join(match_details)
|
||||
|
||||
def configure_display(self):
|
||||
"""配置显示器"""
|
||||
if not self.config_data:
|
||||
logger.error("没有可用的配置数据")
|
||||
return False
|
||||
|
||||
# 检查显示器是否连接
|
||||
if not self.is_display_connected():
|
||||
logger.warning(f"显示器 {self.display_name} 未连接,跳过配置")
|
||||
return True # 返回True表示跳过而不是失败
|
||||
|
||||
# 获取当前支持的模式
|
||||
modes = self.get_display_modes()
|
||||
if not modes:
|
||||
logger.error(f"无法获取显示器 {self.display_name} 的模式信息")
|
||||
return False
|
||||
|
||||
logger.info(f"显示器 {self.display_name} 支持的模式:")
|
||||
for res, rates in modes.items():
|
||||
logger.info(f" {res}: {rates}")
|
||||
|
||||
# 根据模式标志决定行为
|
||||
mode_flag = self.config_data.get('mode_flag', 'A') # 默认为A
|
||||
|
||||
if mode_flag == 'F':
|
||||
# F模式:必须使用cvt生成分辨率
|
||||
logger.info("模式标志为F,强制使用cvt生成分辨率")
|
||||
mode_name = self.create_and_add_mode()
|
||||
if not mode_name:
|
||||
logger.error("创建新模式失败")
|
||||
return False
|
||||
else:
|
||||
# A模式:沿用原来的方法
|
||||
logger.info("模式标志为A,使用自动模式选择")
|
||||
mode_name = self.find_matching_mode(modes)
|
||||
|
||||
# 如果不支持,创建新模式
|
||||
if not mode_name:
|
||||
logger.info("显示器不支持该模式,尝试创建新模式...")
|
||||
mode_name = self.create_and_add_mode()
|
||||
if not mode_name:
|
||||
logger.error("创建新模式失败")
|
||||
return False
|
||||
else:
|
||||
logger.info(f"找到匹配模式: {mode_name}")
|
||||
|
||||
# 构建xrandr命令
|
||||
cmd_parts = ["xrandr", f"--output {self.display_name}"]
|
||||
|
||||
# 添加模式
|
||||
if ' ' in mode_name:
|
||||
resolution, rate = mode_name.split(' ', 1)
|
||||
cmd_parts.append(f"--mode {resolution}")
|
||||
cmd_parts.append(f"--rate {rate}")
|
||||
else:
|
||||
cmd_parts.append(f"--mode {mode_name}")
|
||||
|
||||
# 添加位置
|
||||
cmd_parts.append(f"--pos {self.config_data['pos_x']}x{self.config_data['pos_y']}")
|
||||
|
||||
# 添加旋转
|
||||
cmd_parts.append(f"--rotate {self.config_data['rotation']}")
|
||||
|
||||
# 如果是主屏
|
||||
if self.config_data['primary']:
|
||||
cmd_parts.append("--primary")
|
||||
|
||||
# 启用显示器
|
||||
cmd_parts.append("--auto")
|
||||
|
||||
# 执行最终配置命令
|
||||
final_cmd = " ".join(cmd_parts)
|
||||
logger.info(f"执行命令: {final_cmd}")
|
||||
|
||||
if self.run_xrandr_command(final_cmd):
|
||||
logger.info("显示器配置成功!")
|
||||
return True
|
||||
else:
|
||||
logger.error("显示器配置失败!")
|
||||
return False
|
||||
|
||||
def check_and_configure(self, config_file_path, is_first=False):
|
||||
"""检查并配置显示器 - 主要入口方法"""
|
||||
# 解析配置文件
|
||||
if not self.parse_config_file(config_file_path):
|
||||
logger.error(f"解析配置文件失败: {config_file_path}")
|
||||
return False
|
||||
|
||||
# 检查显示器是否连接
|
||||
if not self.is_display_connected():
|
||||
logger.warning(f"显示器 {self.display_name} 未连接,跳过配置")
|
||||
return True # 返回True表示跳过而不是失败
|
||||
|
||||
# 如果是首次运行,无视显示器配置
|
||||
if is_first:
|
||||
# logger.info("启动程序后首次设置,开始配置显示器")
|
||||
return self.configure_display()
|
||||
|
||||
# 获取当前设置
|
||||
current_settings = self.get_current_display_settings()
|
||||
if not current_settings:
|
||||
logger.warning("无法获取当前显示器设置,尝试直接配置...")
|
||||
return self.configure_display()
|
||||
|
||||
logger.info("当前显示器设置:")
|
||||
for key, value in current_settings.items():
|
||||
logger.info(f" {key}: {value}")
|
||||
|
||||
# 比较设置
|
||||
match, message = self.compare_settings()
|
||||
|
||||
if match:
|
||||
logger.info(f"✓ 设置匹配: {message}")
|
||||
return True
|
||||
else:
|
||||
logger.info(f"✗ 设置不匹配: {message}")
|
||||
logger.info("开始配置显示器...")
|
||||
return self.configure_display()
|
||||
|
||||
|
||||
class FileMonitor:
|
||||
def __init__(self):
|
||||
self.os_version_file = Path("/etc/os-version")
|
||||
self.screen_file = Path("/tmp/ktouch/screen.txt")
|
||||
self.settings_dir = Path("/opt/ktouch/display_config")
|
||||
self.display_update_file = Path("/tmp/ktouch/display_update.txt")
|
||||
|
||||
# 存储文件最后修改时间和内容
|
||||
self.last_mod_time = None
|
||||
self.last_content = None # 新增:存储文件内容
|
||||
self.last_display_update_time = 0
|
||||
|
||||
# 防止重复触发的机制
|
||||
self.last_operation_time = None
|
||||
self.cooldown_period = 15 # 冷却时间15秒
|
||||
|
||||
# 设置信号处理,用于优雅退出
|
||||
signal.signal(signal.SIGINT, self.signal_handler)
|
||||
signal.signal(signal.SIGTERM, self.signal_handler)
|
||||
|
||||
self.running = True
|
||||
self.display_checker = DisplayChecker()
|
||||
|
||||
def signal_handler(self, signum, frame):
|
||||
"""处理中断信号"""
|
||||
logger.info(f"接收到信号 {signum},正在退出...")
|
||||
self.running = False
|
||||
|
||||
def check_os_version(self):
|
||||
"""检查 /etc/os-version 文件是否存在"""
|
||||
if not self.os_version_file.exists():
|
||||
logger.error(f"文件 {self.os_version_file} 不存在,退出脚本")
|
||||
sys.exit(1)
|
||||
logger.info(f"检测到 {self.os_version_file} 文件,继续执行")
|
||||
|
||||
def wait_for_screen_file(self):
|
||||
"""等待 screen.txt 文件生成"""
|
||||
while not self.screen_file.exists() and self.running:
|
||||
logger.info(f"等待 {self.screen_file} 文件生成...")
|
||||
time.sleep(5)
|
||||
|
||||
if not self.running:
|
||||
return False
|
||||
|
||||
# 获取初始修改时间和内容
|
||||
self.last_mod_time = self.screen_file.stat().st_mtime
|
||||
self.last_content = self.read_file_content(self.screen_file)
|
||||
logger.info(f"检测到 {self.screen_file} 文件,初始修改时间: {time.ctime(self.last_mod_time)}")
|
||||
return True
|
||||
|
||||
def read_file_content(self, file_path):
|
||||
"""读取文件内容"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return f.read().strip()
|
||||
except Exception as e:
|
||||
logger.error(f"读取文件内容失败 {file_path}: {e}")
|
||||
return None
|
||||
|
||||
def is_in_cooldown_period(self):
|
||||
"""检查是否在冷却期内"""
|
||||
if self.last_operation_time is None:
|
||||
return False
|
||||
|
||||
elapsed_time = time.time() - self.last_operation_time
|
||||
return elapsed_time < self.cooldown_period
|
||||
|
||||
def update_file_modification_time(self):
|
||||
"""更新文件修改时间(用于防止重复触发)"""
|
||||
try:
|
||||
current_time = time.time()
|
||||
os.utime(self.screen_file, (current_time, current_time))
|
||||
self.last_mod_time = current_time
|
||||
logger.info(f"已更新文件修改时间以防止重复触发")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"更新文件修改时间失败: {e}")
|
||||
return False
|
||||
|
||||
def check_file_modified(self):
|
||||
"""检查文件是否被修改(同时检查修改时间和内容)"""
|
||||
if not self.screen_file.exists():
|
||||
logger.warning(f"文件 {self.screen_file} 不存在")
|
||||
return False
|
||||
|
||||
# 检查是否在冷却期内
|
||||
if self.is_in_cooldown_period():
|
||||
logger.info("在冷却期内,跳过文件修改检查")
|
||||
return False
|
||||
|
||||
try:
|
||||
current_mod_time = self.screen_file.stat().st_mtime
|
||||
current_content = self.read_file_content(self.screen_file)
|
||||
|
||||
# 如果无法读取内容,则跳过
|
||||
if current_content is None:
|
||||
return False
|
||||
|
||||
# 检查修改时间是否变化
|
||||
if current_mod_time != self.last_mod_time:
|
||||
logger.info(f"检测到文件修改时间变化 - 原时间: {time.ctime(self.last_mod_time)}, 新时间: {time.ctime(current_mod_time)}")
|
||||
|
||||
# 检查内容是否变化
|
||||
if current_content != self.last_content:
|
||||
logger.info("检测到文件内容实际发生变化,需要执行分辨率设置")
|
||||
self.last_mod_time = current_mod_time
|
||||
self.last_content = current_content
|
||||
return True
|
||||
else:
|
||||
logger.info("文件修改时间变化但内容未变,只更新修改时间")
|
||||
self.last_mod_time = current_mod_time
|
||||
# 更新文件修改时间防止重复触发
|
||||
self.update_file_modification_time()
|
||||
return False
|
||||
|
||||
except OSError as e:
|
||||
logger.error(f"无法获取文件状态: {e}")
|
||||
|
||||
return False
|
||||
|
||||
def check_display_update_file(self):
|
||||
"""检查display_update.txt文件内容是否为1"""
|
||||
if not self.display_update_file.exists():
|
||||
# 文件不存在,忽略检测
|
||||
return False
|
||||
|
||||
try:
|
||||
# 检查文件修改时间,避免重复处理
|
||||
current_mtime = self.display_update_file.stat().st_mtime
|
||||
if current_mtime <= self.last_display_update_time:
|
||||
return False
|
||||
|
||||
self.last_display_update_time = current_mtime
|
||||
|
||||
content = self.read_file_content(self.display_update_file)
|
||||
if content == "1":
|
||||
logger.info(f"检测到 {self.display_update_file} 文件内容为1,立即进行分辨率设置")
|
||||
|
||||
# 将文件内容改为0
|
||||
try:
|
||||
with open(self.display_update_file, 'w') as f:
|
||||
f.write("0")
|
||||
logger.info(f"已将 {self.display_update_file} 文件内容修改为0")
|
||||
except Exception as e:
|
||||
logger.error(f"修改display_update.txt文件失败: {e}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"检查display_update.txt文件失败: {e}")
|
||||
|
||||
return False
|
||||
|
||||
def execute_display_settings(self, is_first=False):
|
||||
"""执行 display_settings 文件夹中的所有文件 - 直接调用方法"""
|
||||
# 检查目录是否存在
|
||||
if not self.settings_dir.exists():
|
||||
logger.error(f"目录 {self.settings_dir} 不存在")
|
||||
return False
|
||||
|
||||
# 获取目录中的所有文件
|
||||
setting_files = list(self.settings_dir.glob("*"))
|
||||
if not setting_files:
|
||||
logger.warning(f"目录 {self.settings_dir} 中没有文件")
|
||||
return True
|
||||
|
||||
success_count = 0
|
||||
total_count = len(setting_files)
|
||||
skipped_count = 0
|
||||
|
||||
for file_path in setting_files:
|
||||
if file_path.is_file():
|
||||
try:
|
||||
logger.info(f"处理显示器配置文件: {file_path}")
|
||||
|
||||
# 直接调用DisplayChecker的方法,而不是通过子进程
|
||||
result = self.display_checker.check_and_configure(str(file_path), is_first)
|
||||
if result:
|
||||
logger.info(f"成功处理: {file_path}")
|
||||
success_count += 1
|
||||
else:
|
||||
# 检查是否是跳过(显示器未连接)
|
||||
if not self.display_checker.is_display_connected():
|
||||
logger.warning(f"跳过未连接的显示器: {file_path}")
|
||||
skipped_count += 1
|
||||
else:
|
||||
logger.error(f"处理失败: {file_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理异常: {file_path}, 异常: {e}")
|
||||
|
||||
logger.info(f"执行完成: 成功 {success_count}, 跳过 {skipped_count}, 总计 {total_count} 个文件")
|
||||
|
||||
# 记录操作时间并更新文件修改时间
|
||||
self.last_operation_time = time.time()
|
||||
|
||||
# 等待15秒后更新文件修改时间,防止重复触发
|
||||
logger.info(f"等待 {self.cooldown_period} 秒后更新文件修改时间...")
|
||||
time.sleep(self.cooldown_period)
|
||||
|
||||
if self.running:
|
||||
self.update_file_modification_time()
|
||||
|
||||
return success_count > 0 # 只要有一个成功就返回True
|
||||
|
||||
def initial_resolution_check(self):
|
||||
"""初始分辨率检查 - 检查十次,每次间隔15秒"""
|
||||
logger.info("开始初始分辨率检查流程...")
|
||||
|
||||
# 检查目录是否存在
|
||||
if not self.settings_dir.exists():
|
||||
logger.error(f"目录 {self.settings_dir} 不存在")
|
||||
return False
|
||||
|
||||
# 获取目录中的所有文件
|
||||
setting_files = list(self.settings_dir.glob("*"))
|
||||
if not setting_files:
|
||||
logger.warning(f"目录 {self.settings_dir} 中没有文件")
|
||||
return True
|
||||
|
||||
# 进行十次检查,每次间隔15秒
|
||||
max_attempts = 10
|
||||
check_interval = 15 # 秒
|
||||
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
if not self.running:
|
||||
logger.info("程序正在退出,终止初始分辨率检查")
|
||||
return False
|
||||
|
||||
logger.info(f"初始分辨率检查 - 第 {attempt}/{max_attempts} 次")
|
||||
|
||||
success_count = 0
|
||||
total_count = len(setting_files)
|
||||
need_adjustment = False
|
||||
|
||||
for file_path in setting_files:
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
if file_path.is_file():
|
||||
try:
|
||||
logger.info(f"检查显示器配置文件: {file_path}")
|
||||
|
||||
# 为每个文件创建新的DisplayChecker实例
|
||||
display_checker = DisplayChecker()
|
||||
|
||||
# 解析配置文件
|
||||
if not display_checker.parse_config_file(str(file_path)):
|
||||
logger.error(f"解析配置文件失败: {file_path}")
|
||||
continue
|
||||
|
||||
# 检查显示器是否连接
|
||||
if not display_checker.is_display_connected():
|
||||
logger.warning(f"显示器 {display_checker.display_name} 未连接,跳过检查")
|
||||
continue
|
||||
|
||||
# 获取当前设置
|
||||
current_settings = display_checker.get_current_display_settings()
|
||||
if not current_settings:
|
||||
logger.warning(f"无法获取显示器 {display_checker.display_name} 的当前设置")
|
||||
need_adjustment = True
|
||||
continue
|
||||
|
||||
# 比较设置
|
||||
match, message = display_checker.compare_settings()
|
||||
|
||||
if match:
|
||||
logger.info(f"✓ 显示器 {display_checker.display_name} 设置匹配: {message}")
|
||||
success_count += 1
|
||||
else:
|
||||
logger.info(f"✗ 显示器 {display_checker.display_name} 设置不匹配: {message}")
|
||||
need_adjustment = True
|
||||
|
||||
# 立即进行配置
|
||||
logger.info(f"立即配置显示器 {display_checker.display_name}...")
|
||||
if display_checker.configure_display():
|
||||
logger.info(f"显示器 {display_checker.display_name} 配置成功")
|
||||
success_count += 1
|
||||
else:
|
||||
logger.error(f"显示器 {display_checker.display_name} 配置失败")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理异常: {file_path}, 异常: {e}")
|
||||
|
||||
# 检查是否所有显示器都配置正确
|
||||
if success_count == total_count:
|
||||
logger.info(f"✓ 第 {attempt} 次检查完成: 所有显示器配置正确")
|
||||
break
|
||||
else:
|
||||
logger.info(f"第 {attempt} 次检查完成: {success_count}/{total_count} 个显示器配置正确")
|
||||
|
||||
# 如果不是最后一次检查,等待间隔时间
|
||||
if attempt < max_attempts:
|
||||
if need_adjustment:
|
||||
logger.info(f"等待 {check_interval} 秒后进行下一次检查...")
|
||||
for i in range(check_interval):
|
||||
if not self.running:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
logger.info("初始分辨率检查流程完成")
|
||||
return True
|
||||
|
||||
def monitor(self):
|
||||
"""主监控循环"""
|
||||
logger.info("启动文件监控脚本...")
|
||||
|
||||
# 检查os-version文件
|
||||
self.check_os_version()
|
||||
|
||||
logger.info("进行首次分辨率设置")
|
||||
self.execute_display_settings(is_first=True)
|
||||
time.sleep(10)
|
||||
|
||||
# 等待screen文件生成
|
||||
if not self.wait_for_screen_file():
|
||||
return
|
||||
|
||||
# 在screen.txt生成后,进行初始分辨率检查
|
||||
logger.info("screen.txt文件已生成,开始初始分辨率检查...")
|
||||
self.initial_resolution_check()
|
||||
|
||||
logger.info("开始监控文件变化...")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
# 检查display_update.txt文件
|
||||
if self.check_display_update_file():
|
||||
logger.info("检测到display_update.txt触发,立即执行分辨率设置...")
|
||||
self.execute_display_settings()
|
||||
logger.info("display_update.txt触发操作完成,继续监控...")
|
||||
|
||||
# 检查screen.txt文件是否被修改
|
||||
if self.check_file_modified():
|
||||
logger.info("检测到screen.txt文件内容变化,执行display_settings中的文件...")
|
||||
self.execute_display_settings()
|
||||
logger.info("screen.txt变化操作完成,继续监控...")
|
||||
|
||||
# 等待1秒后继续检查
|
||||
time.sleep(1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("用户中断监控")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"监控过程中发生异常: {e}")
|
||||
time.sleep(10)
|
||||
|
||||
logger.info("监控脚本已停止")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
monitor = FileMonitor()
|
||||
monitor.monitor()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,53 @@
|
||||
# check_save.c - 保存确认对话框
|
||||
|
||||
## 概述
|
||||
这是一个GTK+图形界面程序,用于在多显示器环境下显示保存确认对话框。用户可以在任意显示器上点击"保存"或"取消"按钮,程序会返回相应的退出代码供调用方判断。
|
||||
|
||||
## 程序结构
|
||||
|
||||
### 数据结构
|
||||
```c
|
||||
typedef struct {
|
||||
GtkWidget *window; // GTK窗口指针
|
||||
int monitor_num; // 显示器编号
|
||||
int x, y; // 显示器位置
|
||||
int width, height; // 显示器分辨率
|
||||
char *name; // 显示器名称/接口名
|
||||
} MonitorInfo;
|
||||
```
|
||||
|
||||
### 全局变量
|
||||
- `monitors`: 显示器信息数组
|
||||
- `monitor_count`: 显示器数量
|
||||
- `app`: GTK应用程序实例
|
||||
- `exit_code`: 退出代码(默认1=取消)
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. **初始化阶段**: 检查DISPLAY环境变量,创建GTK应用
|
||||
2. **显示器枚举**: 使用`gdk_screen_get_n_monitors()`获取所有显示器信息
|
||||
3. **窗口创建**: 为每个显示器创建一个400x200的居中确认窗口
|
||||
4. **用户交互**:
|
||||
- 点击"保存" → 打印"0",退出代码=0
|
||||
- 点击"取消并退出" → 打印"1",退出代码=1
|
||||
- 关闭窗口 → 打印"1",退出代码=1
|
||||
|
||||
## 关键函数
|
||||
|
||||
| 函数 | 功能 |
|
||||
|------|------|
|
||||
| `activate()` | 应用启动回调,枚举显示器并创建窗口 |
|
||||
| `create_monitor_window()` | 为指定显示器创建确认窗口 |
|
||||
| `on_save_clicked()` | 保存按钮回调 |
|
||||
| `on_cancel_clicked()` | 取消按钮回调 |
|
||||
| `show_error_dialog()` | 显示错误对话框 |
|
||||
|
||||
## 调用方式
|
||||
```bash
|
||||
./check_save
|
||||
# 返回值: 0=保存, 1=取消
|
||||
```
|
||||
|
||||
## 依赖
|
||||
- GTK+ 3.x
|
||||
- GDK (含X11后端)
|
||||
@@ -0,0 +1,53 @@
|
||||
# demo.c - 多屏幕信息窗口演示程序
|
||||
|
||||
## 概述
|
||||
一个基于GTK+和Xrandr的多屏幕信息演示程序。在所有连接的显示器上依次创建信息窗口,显示屏幕分辨率、位置等信息,支持多轮测试和窗口位置校验。
|
||||
|
||||
## 程序结构
|
||||
|
||||
### 全局变量
|
||||
| 变量 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `info_windows` | `GtkWidget**` | 各屏幕信息窗口指针数组 |
|
||||
| `total_screens` | `int` | 连接的显示器总数 |
|
||||
| `screen_widths/heights` | `int*` | 各屏幕宽高 |
|
||||
| `screen_x_offsets/y_offsets` | `int*` | 各屏幕坐标偏移 |
|
||||
| `screen_names` | `char**` | 屏幕接口名(如HDMI-1) |
|
||||
| `current_test_round` | `int` | 当前测试轮次 |
|
||||
| `total_test_rounds` | `int` | 总测试轮次 |
|
||||
| `win_time_delay` | `int` | 窗口创建间隔(ms),默认50 |
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. **屏幕信息获取**: 使用Xrandr扩展查询所有已连接的输出设备及其CRTC信息
|
||||
2. **窗口创建**: 每个屏幕创建一个比屏幕尺寸小20像素的窗口,偏移(10,10)
|
||||
3. **定时创建**: 窗口按`win_time_delay`毫秒间隔依次创建
|
||||
4. **空格键控制**: 用户按空格键关闭所有窗口,进入下一轮测试
|
||||
5. **窗口校验**: 每轮结束后校验窗口位置和大小(允许±5像素误差)
|
||||
6. **日志记录**: 所有操作记录到`screen_demo.log`文件
|
||||
|
||||
## 关键函数
|
||||
|
||||
| 函数 | 功能 |
|
||||
|------|------|
|
||||
| `get_screen_info()` | 通过Xrandr获取所有屏幕信息 |
|
||||
| `create_info_window()` | 创建单个屏幕的信息窗口 |
|
||||
| `create_next_window()` | 定时回调,依次创建窗口 |
|
||||
| `verify_windows()` | 校验窗口位置和大小 |
|
||||
| `start_next_test_round()` | 开始新一轮测试 |
|
||||
| `on_key_press()` | 空格键事件处理 |
|
||||
|
||||
## 窗口布局
|
||||
```
|
||||
窗口位置: (screen_x + 10, screen_y + 10)
|
||||
窗口大小: (screen_width - 20) x (screen_height - 20)
|
||||
```
|
||||
|
||||
## 调用方式
|
||||
```bash
|
||||
./demo
|
||||
```
|
||||
|
||||
## 依赖
|
||||
- GTK+ 3.x
|
||||
- X11 + Xrandr扩展
|
||||
@@ -0,0 +1,26 @@
|
||||
# gdk_test.c - GDK类型测试
|
||||
|
||||
## 概述
|
||||
一个极简的编译测试程序,用于验证`GdkMonitor`类型在当前GTK/GDK版本中是否可用。
|
||||
|
||||
## 代码
|
||||
```c
|
||||
#include <gdk/gdk.h>
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
gtk_init(&argc, &argv);
|
||||
GdkMonitor *monitor; // 仅测试该类型是否可识别
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## 用途
|
||||
该文件不包含实际功能逻辑,仅用于:
|
||||
- 验证编译环境中GDK头文件是否包含`GdkMonitor`类型定义
|
||||
- 确认GTK版本是否支持该类型(GTK 3.22+引入)
|
||||
|
||||
## 编译测试
|
||||
```bash
|
||||
gcc gdk_test.c -o gdk_test $(pkg-config --cflags --libs gtk+-3.0)
|
||||
```
|
||||
@@ -0,0 +1,89 @@
|
||||
# screen_binder.c - 触摸屏与显示器绑定程序
|
||||
|
||||
## 概述
|
||||
核心模块之一,用于将触摸屏设备与显示器进行一一绑定。程序在每个显示器上依次弹出校准窗口,用户点击触摸屏后,程序记录触摸设备与当前显示器的对应关系,最终保存到`touch_dis_table.txt`文件。
|
||||
|
||||
## 程序结构
|
||||
|
||||
### 数据结构
|
||||
```c
|
||||
// 屏幕信息
|
||||
int *screen_widths, *screen_heights; // 屏幕宽高
|
||||
int *screen_x_offsets, *screen_y_offsets; // 屏幕坐标
|
||||
char **screen_names; // 屏幕接口名
|
||||
|
||||
// 触摸绑定信息
|
||||
int *touch_bindings; // 每个屏幕绑定的触摸设备ID
|
||||
char **touch_names; // 触摸设备名称
|
||||
char **touch_paths; // 触摸设备路径
|
||||
```
|
||||
|
||||
### 线程模型
|
||||
| 线程 | 功能 |
|
||||
|------|------|
|
||||
| 主线程 | GTK事件循环,窗口管理 |
|
||||
| `touch_thread` | 触摸事件监听线程 |
|
||||
| `enter_thread` | 全局回车键监听线程(X11) |
|
||||
|
||||
### 原子变量(线程安全)
|
||||
```c
|
||||
_Atomic int global_enter_pressed; // 全局回车键按下标志
|
||||
_Atomic int program_active; // 程序活动状态
|
||||
_Atomic int calibration_active; // 校准进行中标志
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. **屏幕枚举**: 通过Xrandr获取所有已连接显示器
|
||||
2. **触摸初始化**: 初始化触摸监听器,启动触摸事件监听线程
|
||||
3. **全局回车监听**: 启动独立线程监听全局回车键(用于跳过非触摸屏)
|
||||
4. **信息窗口创建**: 依次为每个非当前校准屏幕创建提示窗口
|
||||
5. **校准流程**:
|
||||
- 在当前屏幕创建全屏校准窗口
|
||||
- 等待用户点击触摸屏(60秒超时)
|
||||
- 记录触摸设备ID、名称、路径
|
||||
- 用户按回车键可跳过当前屏幕
|
||||
6. **保存结果**: 将绑定关系保存到`touch_dis_table.txt`
|
||||
|
||||
## 校准窗口交互
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 请在触摸屏上点击此屏幕。 │
|
||||
│ │
|
||||
│ 屏幕接口: HDMI-1 │
|
||||
│ │
|
||||
│ 如果此屏幕不是触摸屏, │
|
||||
│ 那么请敲击回车或者等待60秒。 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 关键函数
|
||||
|
||||
| 函数 | 功能 |
|
||||
|------|------|
|
||||
| `get_screen_info()` | Xrandr获取屏幕信息 |
|
||||
| `init_touch_listener()` | 初始化触摸监听 |
|
||||
| `touch_callback()` | 触摸事件回调,记录绑定 |
|
||||
| `global_enter_listener_thread()` | X11全局回车键监听 |
|
||||
| `show_next_screen()` | 显示下一个校准屏幕 |
|
||||
| `save_mapping_table()` | 保存绑定关系到文件 |
|
||||
| `create_calibration_window()` | 创建校准窗口 |
|
||||
| `create_info_window()` | 创建信息提示窗口 |
|
||||
|
||||
## 输出格式
|
||||
文件`touch_dis_table.txt`格式:
|
||||
```
|
||||
屏幕接口名|触摸设备名|触摸设备ID|设备路径
|
||||
HDMI-1|USB Touchscreen|12345|/dev/input/event5
|
||||
```
|
||||
|
||||
## 调用方式
|
||||
```bash
|
||||
sudo ./screen_binder # 需要sudo权限访问触摸设备
|
||||
```
|
||||
|
||||
## 依赖
|
||||
- GTK+ 3.x
|
||||
- X11 + Xrandr扩展
|
||||
- touch_listen模块(触摸事件监听)
|
||||
- 需要root权限访问`/dev/input/`设备
|
||||
@@ -0,0 +1,70 @@
|
||||
# screen_ds.c - 显示器信息持续监控程序
|
||||
|
||||
## 概述
|
||||
一个基于Xrandr的显示器状态持续监控程序。持续监听显示器的连接/断开事件,当检测到变化时(防抖3秒后)输出所有显示器的当前信息。
|
||||
|
||||
## 程序结构
|
||||
|
||||
### 数据结构
|
||||
```c
|
||||
typedef struct {
|
||||
char* output_name; // 输出接口名(如HDMI-1)
|
||||
int width, height; // 分辨率
|
||||
int x, y; // 位置坐标
|
||||
int rotation; // 旋转方向
|
||||
} MonitorInfo;
|
||||
|
||||
typedef struct {
|
||||
MonitorInfo* monitors; // 显示器数组
|
||||
int count; // 显示器数量
|
||||
} MonitorList;
|
||||
```
|
||||
|
||||
### 旋转方向编码
|
||||
| 值 | 字母 | 说明 |
|
||||
|----|------|------|
|
||||
| RR_Rotate_0 | N | 正常方向 |
|
||||
| RR_Rotate_90 | L | 左旋转90度 |
|
||||
| RR_Rotate_180 | I | 翻转180度 |
|
||||
| RR_Rotate_270 | R | 右旋转90度 |
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. **初始化**: 打开X显示连接,注册RandR事件监听
|
||||
2. **首次输出**: 获取并输出当前所有显示器信息
|
||||
3. **事件循环**:
|
||||
- 监听`RRScreenChangeNotify`和`RRNotify`事件
|
||||
- 检测到事件后设置`need_update`标志
|
||||
- 防抖3秒后(`DEBOUNCE_TIME`)重新获取并输出显示器信息
|
||||
4. **无事件时**: 休眠100ms减少CPU占用
|
||||
|
||||
## 输出格式
|
||||
```
|
||||
HDMI-1|1920x1080|0x0|N
|
||||
DP-2|1920x1080|1920x0|N
|
||||
```
|
||||
每行格式: `接口名|分辨率|位置|旋转方向`
|
||||
|
||||
## 关键函数
|
||||
|
||||
| 函数 | 功能 |
|
||||
|------|------|
|
||||
| `get_monitor_info()` | 获取所有连接的显示器信息 |
|
||||
| `output_monitor_info()` | 输出显示器信息到文件或stdout |
|
||||
| `rotation_to_char()` | 旋转值转字母表示 |
|
||||
| `current_timestamp()` | 获取毫秒级时间戳 |
|
||||
|
||||
## 调用方式
|
||||
```bash
|
||||
./screen_ds [输出文件路径]
|
||||
# 例: ./screen_ds /tmp/screen.txt
|
||||
# 不指定参数则输出到标准输出
|
||||
```
|
||||
|
||||
## 特性
|
||||
- 3秒防抖:避免频繁热插拔导致的重复输出
|
||||
- 持续运行,直到Ctrl+C退出
|
||||
- 支持输出到文件或标准输出
|
||||
|
||||
## 依赖
|
||||
- X11 + Xrandr扩展
|
||||
@@ -0,0 +1,50 @@
|
||||
# screen_ds_once.c - 显示器信息单次查询程序
|
||||
|
||||
## 概述
|
||||
screen_ds.c的简化版本,仅执行一次显示器信息查询并输出结果后退出。适用于需要快速获取当前显示器配置的场景。
|
||||
|
||||
## 与screen_ds.c的区别
|
||||
| 特性 | screen_ds | screen_ds_once |
|
||||
|------|-----------|----------------|
|
||||
| 执行模式 | 持续监控 | 单次查询 |
|
||||
| 事件监听 | 有 | 无 |
|
||||
| 退出方式 | Ctrl+C | 自动退出 |
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. 打开X显示连接
|
||||
2. 查询RandR扩展是否可用
|
||||
3. 获取所有已连接显示器信息
|
||||
4. 输出到文件或标准输出
|
||||
5. 释放资源并退出
|
||||
|
||||
## 数据结构
|
||||
与screen_ds.c相同:
|
||||
```c
|
||||
typedef struct {
|
||||
char* output_name;
|
||||
int width, height;
|
||||
int x, y;
|
||||
int rotation;
|
||||
} MonitorInfo;
|
||||
```
|
||||
|
||||
## 输出格式
|
||||
```
|
||||
HDMI-1|1920x1080|0x0|N
|
||||
```
|
||||
|
||||
## 关键函数
|
||||
| 函数 | 功能 |
|
||||
|------|------|
|
||||
| `get_monitor_info()` | 获取显示器信息 |
|
||||
| `output_monitor_info()` | 输出信息 |
|
||||
| `rotation_to_char()` | 旋转方向编码转换 |
|
||||
|
||||
## 调用方式
|
||||
```bash
|
||||
./screen_ds_once [输出文件路径]
|
||||
```
|
||||
|
||||
## 依赖
|
||||
- X11 + Xrandr扩展
|
||||
@@ -0,0 +1,73 @@
|
||||
# touch_ds.c - 触摸屏坐标矩阵监控程序
|
||||
|
||||
## 概述
|
||||
持续监控所有触摸屏设备的坐标转换矩阵(Coordinate Transformation Matrix)。当检测到某个触摸设备的矩阵发生变化时,设置flag文件通知外部程序。
|
||||
|
||||
## 程序结构
|
||||
|
||||
### 数据结构
|
||||
```c
|
||||
typedef struct {
|
||||
XID deviceid; // XInput设备ID
|
||||
char name[100]; // 设备名称
|
||||
char node[100]; // 设备节点路径
|
||||
char vid_pid[20]; // VID:PID
|
||||
char matrix[120]; // 坐标转换矩阵字符串
|
||||
} TouchscreenInfo;
|
||||
```
|
||||
|
||||
### 配置常量
|
||||
```c
|
||||
int watch_time_delay = 30; // 监控间隔(秒)
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. **读取配置**: 从触摸屏信息文件读取设备列表(含期望的矩阵值)
|
||||
2. **X11连接**: 打开X显示连接,检查XInput扩展
|
||||
3. **主循环**:
|
||||
- 检查flag文件是否为1(暂停检测)
|
||||
- 重新读取触摸屏信息文件
|
||||
- 对每个设备获取当前矩阵值
|
||||
- 比较矩阵是否与配置值不同
|
||||
- 若变化,设置flag文件为1
|
||||
- 等待30秒后继续
|
||||
|
||||
### 矩阵获取流程
|
||||
```
|
||||
设备 → XInput属性查询 → "Coordinate Transformation Matrix"
|
||||
→ 或 "libinput Calibration Matrix"
|
||||
→ 返回3x3浮点矩阵
|
||||
```
|
||||
|
||||
## 关键函数
|
||||
|
||||
| 函数 | 功能 |
|
||||
|------|------|
|
||||
| `get_device_matrix_property()` | 获取设备的3x3矩阵属性 |
|
||||
| `get_matrix_string()` | 获取矩阵的字符串表示 |
|
||||
| `device_exists()` | 检查设备是否仍然存在 |
|
||||
| `is_touchscreen_device()` | 通过udev判断是否为触摸屏 |
|
||||
| `read_touchscreens_from_file()` | 读取触摸屏配置文件 |
|
||||
| `is_flag_set()` / `set_flag()` | flag文件读写 |
|
||||
|
||||
## 输入文件格式
|
||||
触摸屏信息文件每行格式:
|
||||
```
|
||||
设备ID|设备名|设备节点|VID:PID|矩阵值
|
||||
12345|USB Touchscreen|/dev/input/event5|046d:c52b|1.000000,0.000000,...
|
||||
```
|
||||
|
||||
## flag文件机制
|
||||
- flag文件值为`0`:正常监控
|
||||
- flag文件值为`1`:暂停监控,等待外部恢复为`0`
|
||||
|
||||
## 调用方式
|
||||
```bash
|
||||
./touch_ds <触摸屏信息文件> [flag文件路径]
|
||||
# 例: ./touch_ds /tmp/touch_info.txt /tmp/touch_matrix.flag
|
||||
```
|
||||
|
||||
## 依赖
|
||||
- X11 + XInput2扩展
|
||||
- libudev
|
||||
@@ -0,0 +1,99 @@
|
||||
# touch_listen.c / touch_listen.h - 触摸事件监听库
|
||||
|
||||
## 概述
|
||||
一个触摸屏事件监听库,通过直接读取Linux输入设备(`/dev/input/event*`)获取触摸事件。支持多点触控,提供回调函数接口供上层模块使用。
|
||||
|
||||
## 数据结构
|
||||
|
||||
### touch_device (设备信息)
|
||||
```c
|
||||
struct touch_device {
|
||||
int fd; // 文件描述符
|
||||
char name[256]; // 设备名称
|
||||
char path[256]; // 设备路径
|
||||
int min_x, max_x; // X轴范围
|
||||
int min_y, max_y; // Y轴范围
|
||||
};
|
||||
```
|
||||
|
||||
### touch_event (触摸事件)
|
||||
```c
|
||||
struct touch_event {
|
||||
int device_id; // 设备标识(路径哈希值)
|
||||
int x, y; // 坐标
|
||||
int pressure; // 压力值
|
||||
int touch_id; // 触摸点ID(多点触控)
|
||||
int event_type; // 事件类型: 0=按下, 1=移动, 2=释放
|
||||
char device_name[256]; // 设备名称
|
||||
char device_path[256]; // 设备路径
|
||||
struct timespec timestamp; // 时间戳
|
||||
};
|
||||
```
|
||||
|
||||
### 回调函数类型
|
||||
```c
|
||||
typedef void (*touch_event_callback)(struct touch_event event);
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 设备发现
|
||||
1. 遍历`/dev/input/`目录下所有`event*`设备
|
||||
2. 通过ioctl检查设备是否支持`EV_ABS`和`EV_KEY`
|
||||
3. 进一步检查是否支持`ABS_MT_POSITION_X`(多点)或`ABS_X`(单点)
|
||||
4. 获取设备名称和坐标范围
|
||||
|
||||
### 事件监听
|
||||
1. 使用`select()`多路复用监听所有设备文件描述符
|
||||
2. 读取`input_event`结构体
|
||||
3. 处理事件类型:
|
||||
- `EV_ABS`: 处理坐标、压力、触摸点ID
|
||||
- `EV_KEY`: 处理`BTN_TOUCH`按键事件
|
||||
- `EV_SYN`: 同步事件,触发回调
|
||||
4. 通过回调函数通知上层
|
||||
|
||||
### 事件类型映射
|
||||
| Linux事件 | 触摸事件类型 | 说明 |
|
||||
|-----------|-------------|------|
|
||||
| ABS_MT_TRACKING_ID != -1 | 0 (按下) | 新触摸点 |
|
||||
| ABS_MT_TRACKING_ID == -1 | 2 (释放) | 触摸释放 |
|
||||
| BTN_TOUCH = 1 | 0 (按下) | 触摸开始 |
|
||||
| BTN_TOUCH = 0 | 2 (释放) | 触摸结束 |
|
||||
| SYN_REPORT | 1 (移动) | 坐标更新 |
|
||||
|
||||
## API接口
|
||||
|
||||
| 函数 | 功能 |
|
||||
|------|------|
|
||||
| `init_touch_listener(callback)` | 初始化监听器,注册回调 |
|
||||
| `listen_touch_events()` | 开始监听(阻塞) |
|
||||
| `stop_touch_listener()` | 停止监听 |
|
||||
| `cleanup_touch_listener()` | 清理资源 |
|
||||
| `get_active_touch_devices()` | 获取活跃设备列表 |
|
||||
|
||||
## 调用示例
|
||||
```c
|
||||
#include "touch_listen.h"
|
||||
|
||||
void my_callback(struct touch_event event) {
|
||||
printf("设备 %s: %s at (%d,%d)\n",
|
||||
event.device_name,
|
||||
event.event_type == 0 ? "按下" :
|
||||
event.event_type == 1 ? "移动" : "释放",
|
||||
event.x, event.y);
|
||||
}
|
||||
|
||||
int main() {
|
||||
init_touch_listener(my_callback);
|
||||
listen_touch_events(); // 阻塞
|
||||
cleanup_touch_listener();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## 使用者
|
||||
- `screen_binder.c` - 触摸屏绑定程序
|
||||
|
||||
## 依赖
|
||||
- Linux输入子系统(`/dev/input/event*`)
|
||||
- 需要root权限访问输入设备
|
||||
@@ -0,0 +1,152 @@
|
||||
# touch_set.c - 触摸屏坐标转换矩阵设置程序
|
||||
|
||||
## 概述
|
||||
核心模块之一,负责根据配置文件和屏幕信息,自动计算并设置每个触摸屏设备的坐标转换矩阵(Coordinate Transformation Matrix),实现触摸屏与显示器的正确映射。
|
||||
|
||||
## 程序结构
|
||||
|
||||
### 数据结构
|
||||
```c
|
||||
// 设备信息
|
||||
typedef struct {
|
||||
int device_id; // XInput设备ID
|
||||
char name[256]; // 设备名称
|
||||
char device_node[256]; // 设备节点路径
|
||||
char vid[10]; // 供应商ID
|
||||
char pid[10]; // 产品ID
|
||||
char usb_path[100]; // 物理路径
|
||||
int is_touchscreen; // 是否为触摸屏
|
||||
float matrix[9]; // 3x3坐标转换矩阵
|
||||
int matched; // 是否已匹配
|
||||
} InputDeviceInfo;
|
||||
|
||||
// 配置项
|
||||
typedef struct {
|
||||
char display_name[50]; // 显示器名称
|
||||
char touchscreen_name[50]; // 触摸屏名称
|
||||
char vid[10]; // 供应商ID
|
||||
char pid[10]; // 产品ID
|
||||
char usb_path[100]; // USB路径
|
||||
int matched; // 是否已匹配
|
||||
} TouchConfig;
|
||||
|
||||
// 屏幕信息
|
||||
typedef struct {
|
||||
char name[50]; // 屏幕名称
|
||||
int width, height; // 分辨率
|
||||
int x, y; // 位置
|
||||
char rotation; // 旋转: N/L/R/I
|
||||
} ScreenInfo;
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 整体流程
|
||||
```
|
||||
读取配置文件(/opt/ktouch/config)
|
||||
↓
|
||||
读取屏幕信息(/tmp/ktouch/screen.txt)
|
||||
↓
|
||||
计算虚拟桌面大小
|
||||
↓
|
||||
获取所有触摸设备(XInput + udev)
|
||||
↓
|
||||
多轮匹配(严格→宽松)
|
||||
↓
|
||||
计算坐标转换矩阵
|
||||
↓
|
||||
通过XInput设置矩阵
|
||||
↓
|
||||
保存结果到输出文件
|
||||
```
|
||||
|
||||
### 矩阵计算原理
|
||||
|
||||
坐标转换矩阵将触摸屏的原始坐标映射到虚拟桌面坐标系:
|
||||
|
||||
**正常方向(N)**:
|
||||
```
|
||||
| sx 0 dx |
|
||||
| 0 sy dy |
|
||||
| 0 0 1 |
|
||||
```
|
||||
|
||||
**左旋转(L)**:
|
||||
```
|
||||
| 0 -sx sx+dx |
|
||||
| sy 0 dy |
|
||||
| 0 0 1 |
|
||||
```
|
||||
|
||||
**右旋转(R)**:
|
||||
```
|
||||
| 0 sx dx |
|
||||
| -sy 0 sy+dy |
|
||||
| 0 0 1 |
|
||||
```
|
||||
|
||||
**翻转(I)**:
|
||||
```
|
||||
| -sx 0 sx+dx |
|
||||
| 0 -sy sy+dy |
|
||||
| 0 0 1 |
|
||||
```
|
||||
|
||||
其中:
|
||||
- `sx = 屏幕宽度 / 虚拟桌面宽度`
|
||||
- `sy = 屏幕高度 / 虚拟桌面高度`
|
||||
- `dx = 屏幕X偏移 / 虚拟桌面宽度`
|
||||
- `dy = 屏幕Y偏移 / 虚拟桌面高度`
|
||||
|
||||
### 设备匹配策略
|
||||
|
||||
**第一轮(严格匹配)**:
|
||||
1. 设备名完全匹配(去除空格后比较)
|
||||
2. 若多个匹配,按VID:PID筛选
|
||||
3. 若仍多个,按USB路径筛选
|
||||
|
||||
**第二轮及以后(宽松匹配)**:
|
||||
1. 仅按VID:PID匹配
|
||||
2. 若多个匹配,返回失败(-1)
|
||||
|
||||
## 关键函数
|
||||
|
||||
| 函数 | 功能 |
|
||||
|------|------|
|
||||
| `get_input_devices()` | 通过XInput+udev获取所有触摸设备 |
|
||||
| `read_config()` | 读取触摸屏配置文件 |
|
||||
| `read_screen_info()` | 读取屏幕信息文件 |
|
||||
| `find_matching_device()` | 多策略设备匹配 |
|
||||
| `calculate_virtual_desktop()` | 计算虚拟桌面总尺寸 |
|
||||
| `calculate_ctm()` | 计算坐标转换矩阵 |
|
||||
| `set_ctm()` | 通过XInput设置矩阵属性 |
|
||||
| `devnode_to_syspath()` | 设备节点转sysfs路径 |
|
||||
|
||||
## 输入文件格式
|
||||
|
||||
### 配置文件(/opt/ktouch/config)
|
||||
```
|
||||
显示器名|触摸屏名|VID|PID|USB路径
|
||||
HDMI-1|USB Touchscreen|046d|c52b|usb-0000:00:14.0-1
|
||||
```
|
||||
|
||||
### 屏幕信息文件(/tmp/ktouch/screen.txt)
|
||||
```
|
||||
HDMI-1|1920x1080|0x0|N
|
||||
DP-2|1920x1080|1920x0|N
|
||||
```
|
||||
|
||||
### 输出文件格式
|
||||
```
|
||||
设备ID|设备名|设备节点|VID:PID|矩阵值
|
||||
12345|USB Touchscreen|/dev/input/event5|046d:c52b|1.000000, 0.000000, ...
|
||||
```
|
||||
|
||||
## 调用方式
|
||||
```bash
|
||||
./touch_set [输出文件路径]
|
||||
```
|
||||
|
||||
## 依赖
|
||||
- X11 + XInput2扩展
|
||||
- libudev
|
||||
@@ -0,0 +1,77 @@
|
||||
# usb_ds.c - USB触摸屏设备热插拔监控程序
|
||||
|
||||
## 概述
|
||||
通过udev监控USB设备的热插拔事件,当检测到新的触摸屏设备插入时,通过控制文件通知外部程序。支持轮询间隔配置和控制文件暂停机制。
|
||||
|
||||
## 程序结构
|
||||
|
||||
### 配置常量
|
||||
```c
|
||||
#define DEFAULT_POLL_INTERVAL 5 // 默认轮询间隔(秒)
|
||||
#define MAX_RETRY_ATTEMPTS 3 // 最大重试次数
|
||||
#define RETRY_DELAY 1 // 重试延迟(秒)
|
||||
```
|
||||
|
||||
### 控制变量
|
||||
```c
|
||||
volatile sig_atomic_t keep_running = 1; // 运行标志
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 监控流程
|
||||
```
|
||||
创建udev监控对象
|
||||
↓
|
||||
过滤USB和input子系统事件
|
||||
↓
|
||||
主循环:
|
||||
├── 检查控制文件状态
|
||||
│ └── 若非0,跳过本轮
|
||||
├── select等待事件(超时=轮询间隔)
|
||||
├── 有事件:
|
||||
│ ├── 获取设备信息
|
||||
│ ├── 检查是否为"add"动作
|
||||
│ ├── 检查是否为指针设备
|
||||
│ └── 检查是否为触摸屏
|
||||
└── 更新控制文件状态
|
||||
```
|
||||
|
||||
### 触摸屏判断
|
||||
```c
|
||||
int is_touchscreen(struct udev_device *dev) {
|
||||
// 1. 检查ID_INPUT_TOUCHSCREEN属性
|
||||
// 2. 检查设备名称是否包含"touch"/"touchscreen"/"tablet"
|
||||
}
|
||||
```
|
||||
|
||||
### 控制文件机制
|
||||
- `0`: 正常监控状态
|
||||
- `1`: 检测到触摸屏,等待外部处理后恢复为`0`
|
||||
|
||||
### 文件操作重试
|
||||
所有文件读写操作最多重试3次,每次间隔1秒。
|
||||
|
||||
## 关键函数
|
||||
|
||||
| 函数 | 功能 |
|
||||
|------|------|
|
||||
| `monitor_usb_devices()` | 主监控循环 |
|
||||
| `is_touchscreen()` | 判断设备是否为触摸屏 |
|
||||
| `read_file_content_with_retry()` | 带重试的文件读取 |
|
||||
| `write_file_content_with_retry()` | 带重试的文件写入 |
|
||||
| `sigint_handler()` | SIGINT信号处理 |
|
||||
|
||||
## 调用方式
|
||||
```bash
|
||||
./usb_ds [控制文件路径] [轮询间隔秒数]
|
||||
# 例: ./usb_ds /tmp/usb_monitor.ctl 5
|
||||
# 不带参数: 无控制文件,5秒轮询
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
当USB触摸屏热插拔时,通过控制文件通知主程序重新配置触摸映射。
|
||||
|
||||
## 依赖
|
||||
- libudev
|
||||
- POSIX信号处理
|
||||
@@ -0,0 +1,541 @@
|
||||
# Copyright (c) 2021 rdbende <rdbende@gmail.com>
|
||||
|
||||
# The Forest theme is a beautiful and modern ttk theme inspired by Excel.
|
||||
|
||||
package require Tk 8.6
|
||||
|
||||
namespace eval ttk::theme::forest-light {
|
||||
|
||||
variable version 1.0
|
||||
package provide ttk::theme::forest-light $version
|
||||
variable colors
|
||||
array set colors {
|
||||
-fg "#313131"
|
||||
-bg "#ffffff"
|
||||
-disabledfg "#595959"
|
||||
-disabledbg "#ffffff"
|
||||
-selectfg "#ffffff"
|
||||
-selectbg "#217346"
|
||||
}
|
||||
|
||||
proc LoadImages {imgdir} {
|
||||
variable I
|
||||
foreach file [glob -directory $imgdir *.png] {
|
||||
set img [file tail [file rootname $file]]
|
||||
set I($img) [image create photo -file $file -format png]
|
||||
}
|
||||
}
|
||||
|
||||
LoadImages [file join [file dirname [info script]] forest-light]
|
||||
|
||||
# Settings
|
||||
ttk::style theme create forest-light -parent default -settings {
|
||||
ttk::style configure . \
|
||||
-background $colors(-bg) \
|
||||
-foreground $colors(-fg) \
|
||||
-troughcolor $colors(-bg) \
|
||||
-focuscolor $colors(-selectbg) \
|
||||
-selectbackground $colors(-selectbg) \
|
||||
-selectforeground $colors(-selectfg) \
|
||||
-insertwidth 1 \
|
||||
-insertcolor $colors(-fg) \
|
||||
-fieldbackground $colors(-selectbg) \
|
||||
-font {TkDefaultFont 10} \
|
||||
-borderwidth 1 \
|
||||
-relief flat
|
||||
|
||||
ttk::style map . -foreground [list disabled $colors(-disabledfg)]
|
||||
|
||||
tk_setPalette background [ttk::style lookup . -background] \
|
||||
foreground [ttk::style lookup . -foreground] \
|
||||
highlightColor [ttk::style lookup . -focuscolor] \
|
||||
selectBackground [ttk::style lookup . -selectbackground] \
|
||||
selectForeground [ttk::style lookup . -selectforeground] \
|
||||
activeBackground [ttk::style lookup . -selectbackground] \
|
||||
activeForeground [ttk::style lookup . -selectforeground]
|
||||
|
||||
option add *font [ttk::style lookup . -font]
|
||||
|
||||
|
||||
# Layouts
|
||||
ttk::style layout TButton {
|
||||
Button.button -children {
|
||||
Button.padding -children {
|
||||
Button.label -side left -expand true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout Toolbutton {
|
||||
Toolbutton.button -children {
|
||||
Toolbutton.padding -children {
|
||||
Toolbutton.label -side left -expand true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout TMenubutton {
|
||||
Menubutton.button -children {
|
||||
Menubutton.padding -children {
|
||||
Menubutton.indicator -side right
|
||||
Menubutton.label -side right -expand true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout TOptionMenu {
|
||||
OptionMenu.button -children {
|
||||
OptionMenu.padding -children {
|
||||
OptionMenu.indicator -side right
|
||||
OptionMenu.label -side right -expand true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout Accent.TButton {
|
||||
AccentButton.button -children {
|
||||
AccentButton.padding -children {
|
||||
AccentButton.label -side left -expand true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout TCheckbutton {
|
||||
Checkbutton.button -children {
|
||||
Checkbutton.padding -children {
|
||||
Checkbutton.indicator -side left
|
||||
Checkbutton.label -side right -expand true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout Switch {
|
||||
Switch.button -children {
|
||||
Switch.padding -children {
|
||||
Switch.indicator -side left
|
||||
Switch.label -side right -expand true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout ToggleButton {
|
||||
ToggleButton.button -children {
|
||||
ToggleButton.padding -children {
|
||||
ToggleButton.label -side left -expand true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout TRadiobutton {
|
||||
Radiobutton.button -children {
|
||||
Radiobutton.padding -children {
|
||||
Radiobutton.indicator -side left
|
||||
Radiobutton.label -side right -expand true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout Vertical.TScrollbar {
|
||||
Vertical.Scrollbar.trough -sticky ns -children {
|
||||
Vertical.Scrollbar.thumb -expand true
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout Horizontal.TScrollbar {
|
||||
Horizontal.Scrollbar.trough -sticky ew -children {
|
||||
Horizontal.Scrollbar.thumb -expand true
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout TCombobox {
|
||||
Combobox.field -sticky nswe -children {
|
||||
Combobox.padding -expand true -sticky nswe -children {
|
||||
Combobox.textarea -sticky nswe
|
||||
}
|
||||
}
|
||||
Combobox.button -side right -sticky ns -children {
|
||||
Combobox.arrow -sticky nsew
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout TSpinbox {
|
||||
Spinbox.field -sticky nsew -children {
|
||||
Spinbox.padding -expand true -sticky nswe -children {
|
||||
Spinbox.textarea -sticky nsew
|
||||
}
|
||||
|
||||
}
|
||||
null -side right -sticky nsew -children {
|
||||
Spinbox.uparrow -side right -sticky nsew -children {
|
||||
Spinbox.symuparrow
|
||||
}
|
||||
Spinbox.downarrow -side left -sticky nsew -children {
|
||||
Spinbox.symdownarrow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout Horizontal.TSeparator {
|
||||
Horizontal.separator -sticky nswe
|
||||
}
|
||||
|
||||
ttk::style layout Vertical.TSeparator {
|
||||
Vertical.separator -sticky nswe
|
||||
}
|
||||
|
||||
ttk::style layout Card {
|
||||
Card.field {
|
||||
Card.padding -expand 1
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout TLabelframe {
|
||||
Labelframe.border {
|
||||
Labelframe.padding -expand 1 -children {
|
||||
Labelframe.label -side left
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout TNotebook {
|
||||
Notebook.border -children {
|
||||
TNotebook.Tab -expand 1 -side top
|
||||
Notebook.client -sticky nsew
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout TNotebook.Tab {
|
||||
Notebook.tab -children {
|
||||
Notebook.padding -side top -children {
|
||||
Notebook.label
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ttk::style layout Treeview.Item {
|
||||
Treeitem.padding -sticky nswe -children {
|
||||
Treeitem.indicator -side left -sticky {}
|
||||
Treeitem.image -side left -sticky {}
|
||||
Treeitem.text -side left -sticky {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Elements
|
||||
|
||||
# Button
|
||||
ttk::style configure TButton -padding {8 4 8 4} -width -10 -anchor center
|
||||
|
||||
ttk::style element create Button.button image \
|
||||
[list $I(rect-basic) \
|
||||
{selected disabled} $I(rect-basic) \
|
||||
disabled $I(rect-basic) \
|
||||
selected $I(rect-basic) \
|
||||
pressed $I(rect-basic) \
|
||||
active $I(rect-hover) \
|
||||
] -border 4 -sticky nsew
|
||||
|
||||
# Toolbutton
|
||||
ttk::style configure Toolbutton -padding {8 4 8 4} -width -10 -anchor center
|
||||
|
||||
ttk::style element create Toolbutton.button image \
|
||||
[list $I(empty) \
|
||||
{selected disabled} $I(empty) \
|
||||
disabled $I(empty) \
|
||||
selected $I(rect-basic) \
|
||||
pressed $I(rect-basic) \
|
||||
active $I(rect-basic) \
|
||||
] -border 4 -sticky nsew
|
||||
|
||||
# Menubutton
|
||||
ttk::style configure TMenubutton -padding {8 4 4 4}
|
||||
|
||||
ttk::style element create Menubutton.button image \
|
||||
[list $I(rect-basic) \
|
||||
disabled $I(rect-basic) \
|
||||
pressed $I(rect-basic) \
|
||||
active $I(rect-hover) \
|
||||
] -border 4 -sticky nsew
|
||||
|
||||
ttk::style element create Menubutton.indicator image \
|
||||
[list $I(down) \
|
||||
active $I(down) \
|
||||
pressed $I(down) \
|
||||
disabled $I(down) \
|
||||
] -width 15 -sticky e
|
||||
|
||||
# OptionMenu
|
||||
ttk::style configure TOptionMenu -padding {8 4 4 4}
|
||||
|
||||
ttk::style element create OptionMenu.button image \
|
||||
[list $I(rect-basic) \
|
||||
disabled $I(rect-basic) \
|
||||
pressed $I(rect-basic) \
|
||||
active $I(rect-hover) \
|
||||
] -border 4 -sticky nsew
|
||||
|
||||
ttk::style element create OptionMenu.indicator image \
|
||||
[list $I(down) \
|
||||
active $I(down) \
|
||||
pressed $I(down) \
|
||||
disabled $I(down) \
|
||||
] -width 15 -sticky e
|
||||
|
||||
# AccentButton
|
||||
ttk::style configure Accent.TButton -padding {8 4 8 4} -width -10 -anchor center -foreground #eeeeee
|
||||
|
||||
ttk::style element create AccentButton.button image \
|
||||
[list $I(rect-accent) \
|
||||
{selected disabled} $I(rect-accent-hover) \
|
||||
disabled $I(rect-accent-hover) \
|
||||
selected $I(rect-accent) \
|
||||
pressed $I(rect-accent) \
|
||||
active $I(rect-accent-hover) \
|
||||
] -border 4 -sticky nsew
|
||||
|
||||
# Checkbutton
|
||||
ttk::style configure TCheckbutton -padding 4
|
||||
|
||||
ttk::style element create Checkbutton.indicator image \
|
||||
[list $I(check-unsel-accent) \
|
||||
{alternate disabled} $I(check-tri-basic) \
|
||||
{selected disabled} $I(check-basic) \
|
||||
disabled $I(check-unsel-basic) \
|
||||
{pressed alternate} $I(check-tri-hover) \
|
||||
{active alternate} $I(check-tri-hover) \
|
||||
alternate $I(check-tri-accent) \
|
||||
{pressed selected} $I(check-hover) \
|
||||
{active selected} $I(check-hover) \
|
||||
selected $I(check-accent) \
|
||||
{pressed !selected} $I(check-unsel-pressed) \
|
||||
active $I(check-unsel-hover) \
|
||||
] -width 26 -sticky w
|
||||
|
||||
# Switch
|
||||
ttk::style element create Switch.indicator image \
|
||||
[list $I(off-accent) \
|
||||
{selected disabled} $I(on-basic) \
|
||||
disabled $I(off-basic) \
|
||||
{pressed selected} $I(on-accent) \
|
||||
{active selected} $I(on-hover) \
|
||||
selected $I(on-accent) \
|
||||
{pressed !selected} $I(off-accent) \
|
||||
active $I(off-hover) \
|
||||
] -width 46 -sticky w
|
||||
|
||||
# ToggleButton
|
||||
ttk::style configure ToggleButton -padding {8 4 8 4} -width -10 -anchor center -foregound $colors(-fg)
|
||||
|
||||
ttk::style map ToggleButton -foreground \
|
||||
[list {pressed selected} $colors(-fg) \
|
||||
{pressed !selected} #ffffff \
|
||||
selected #ffffff]
|
||||
|
||||
ttk::style element create ToggleButton.button image \
|
||||
[list $I(rect-basic) \
|
||||
{selected disabled} $I(rect-accent-hover) \
|
||||
disabled $I(rect-basic) \
|
||||
{pressed selected} $I(rect-basic) \
|
||||
{active selected} $I(rect-accent-hover) \
|
||||
selected $I(rect-accent) \
|
||||
{pressed !selected} $I(rect-accent) \
|
||||
active $I(rect-hover) \
|
||||
] -border 4 -sticky nsew
|
||||
|
||||
# Radiobutton
|
||||
ttk::style configure TRadiobutton -padding 4
|
||||
|
||||
ttk::style element create Radiobutton.indicator image \
|
||||
[list $I(radio-unsel-accent) \
|
||||
{alternate disabled} $I(radio-tri-basic) \
|
||||
{selected disabled} $I(radio-basic) \
|
||||
disabled $I(radio-unsel-basic) \
|
||||
{pressed alternate} $I(radio-tri-hover) \
|
||||
{active alternate} $I(radio-tri-hover) \
|
||||
alternate $I(radio-tri-accent) \
|
||||
{pressed selected} $I(radio-hover) \
|
||||
{active selected} $I(radio-hover) \
|
||||
selected $I(radio-accent) \
|
||||
{pressed !selected} $I(radio-unsel-pressed) \
|
||||
active $I(radio-unsel-hover) \
|
||||
] -width 26 -sticky w
|
||||
|
||||
# Scrollbar
|
||||
ttk::style element create Horizontal.Scrollbar.trough image $I(hor-basic) \
|
||||
-sticky ew
|
||||
|
||||
ttk::style element create Horizontal.Scrollbar.thumb image \
|
||||
[list $I(hor-accent) \
|
||||
disabled $I(hor-basic) \
|
||||
pressed $I(hor-hover) \
|
||||
active $I(hor-hover) \
|
||||
] -sticky ew
|
||||
|
||||
ttk::style element create Vertical.Scrollbar.trough image $I(vert-basic) \
|
||||
-sticky ns
|
||||
|
||||
ttk::style element create Vertical.Scrollbar.thumb image \
|
||||
[list $I(vert-accent) \
|
||||
disabled $I(vert-basic) \
|
||||
pressed $I(vert-hover) \
|
||||
active $I(vert-hover) \
|
||||
] -sticky ns
|
||||
|
||||
# Scale
|
||||
ttk::style element create Horizontal.Scale.trough image $I(scale-hor) \
|
||||
-border 5 -padding 0
|
||||
|
||||
ttk::style element create Horizontal.Scale.slider image \
|
||||
[list $I(thumb-hor-accent) \
|
||||
disabled $I(thumb-hor-basic) \
|
||||
pressed $I(thumb-hor-hover) \
|
||||
active $I(thumb-hor-hover) \
|
||||
] -sticky {}
|
||||
|
||||
ttk::style element create Vertical.Scale.trough image $I(scale-vert) \
|
||||
-border 5 -padding 0
|
||||
|
||||
ttk::style element create Vertical.Scale.slider image \
|
||||
[list $I(thumb-vert-accent) \
|
||||
disabled $I(thumb-vert-basic) \
|
||||
pressed $I(thumb-vert-hover) \
|
||||
active $I(thumb-vert-hover) \
|
||||
] -sticky {}
|
||||
|
||||
# Progressbar
|
||||
ttk::style element create Horizontal.Progressbar.trough image $I(hor-basic) \
|
||||
-sticky ew
|
||||
|
||||
ttk::style element create Horizontal.Progressbar.pbar image $I(hor-accent) \
|
||||
-sticky ew
|
||||
|
||||
ttk::style element create Vertical.Progressbar.trough image $I(vert-basic) \
|
||||
-sticky ns
|
||||
|
||||
ttk::style element create Vertical.Progressbar.pbar image $I(vert-accent) \
|
||||
-sticky ns
|
||||
|
||||
# Entry
|
||||
ttk::style element create Entry.field image \
|
||||
[list $I(border-basic) \
|
||||
{focus hover} $I(border-accent) \
|
||||
invalid $I(border-invalid) \
|
||||
disabled $I(border-basic) \
|
||||
focus $I(border-accent) \
|
||||
hover $I(border-hover) \
|
||||
] -border 5 -padding {8} -sticky nsew
|
||||
|
||||
# Combobox
|
||||
ttk::style map TCombobox -selectbackground [list \
|
||||
{!focus} $colors(-selectbg) \
|
||||
{readonly hover} $colors(-selectbg) \
|
||||
{readonly focus} $colors(-selectbg) \
|
||||
]
|
||||
|
||||
ttk::style map TCombobox -selectforeground [list \
|
||||
{!focus} $colors(-selectfg) \
|
||||
{readonly hover} $colors(-selectfg) \
|
||||
{readonly focus} $colors(-selectfg) \
|
||||
]
|
||||
|
||||
ttk::style element create Combobox.field image \
|
||||
[list $I(border-basic) \
|
||||
{readonly disabled} $I(rect-basic) \
|
||||
{readonly pressed} $I(rect-basic) \
|
||||
{readonly focus hover} $I(rect-hover) \
|
||||
{readonly focus} $I(rect-hover) \
|
||||
{readonly hover} $I(rect-hover) \
|
||||
{focus hover} $I(border-accent) \
|
||||
readonly $I(rect-basic) \
|
||||
invalid $I(border-invalid) \
|
||||
disabled $I(border-basic) \
|
||||
focus $I(border-accent) \
|
||||
hover $I(border-hover) \
|
||||
] -border 5 -padding {8 8 28 8}
|
||||
|
||||
ttk::style element create Combobox.button image \
|
||||
[list $I(combo-button-basic) \
|
||||
{!readonly focus} $I(combo-button-focus) \
|
||||
{readonly focus} $I(combo-button-hover) \
|
||||
{readonly hover} $I(combo-button-hover)
|
||||
] -border 5 -padding {2 6 6 6}
|
||||
|
||||
ttk::style element create Combobox.arrow image $I(down) -width 15 -sticky e
|
||||
|
||||
# Spinbox
|
||||
ttk::style element create Spinbox.field image \
|
||||
[list $I(border-basic) \
|
||||
invalid $I(border-invalid) \
|
||||
disabled $I(border-basic) \
|
||||
focus $I(border-accent) \
|
||||
hover $I(border-hover) \
|
||||
] -border 5 -padding {8 8 54 8} -sticky nsew
|
||||
|
||||
ttk::style element create Spinbox.uparrow image $I(spin-button-up) -border 4 -sticky nsew
|
||||
|
||||
ttk::style element create Spinbox.downarrow image \
|
||||
[list $I(spin-button-down-basic) \
|
||||
focus $I(spin-button-down-focus) \
|
||||
] -border 4 -sticky nsew
|
||||
|
||||
ttk::style element create Spinbox.symuparrow image $I(up) -width 15 -sticky {}
|
||||
ttk::style element create Spinbox.symdownarrow image $I(down) -width 17 -sticky {}
|
||||
|
||||
# Sizegrip
|
||||
ttk::style element create Sizegrip.sizegrip image $I(sizegrip) \
|
||||
-sticky nsew
|
||||
|
||||
# Separator
|
||||
ttk::style element create Horizontal.separator image $I(separator)
|
||||
|
||||
ttk::style element create Vertical.separator image $I(separator)
|
||||
|
||||
# Card
|
||||
ttk::style element create Card.field image $I(card) \
|
||||
-border 10 -padding 4 -sticky nsew
|
||||
|
||||
# Labelframe
|
||||
ttk::style element create Labelframe.border image $I(card) \
|
||||
-border 5 -padding 4 -sticky nsew
|
||||
|
||||
# Notebook
|
||||
ttk::style configure TNotebook -padding 2
|
||||
|
||||
ttk::style element create Notebook.border image $I(card) -border 5
|
||||
|
||||
ttk::style element create Notebook.client image $I(notebook) -border 5
|
||||
|
||||
ttk::style element create Notebook.tab image \
|
||||
[list $I(tab-basic) \
|
||||
selected $I(tab-accent) \
|
||||
active $I(tab-hover) \
|
||||
] -border 5 -padding {14 4}
|
||||
|
||||
# Treeview
|
||||
ttk::style element create Treeview.field image $I(card) \
|
||||
-border 5
|
||||
|
||||
ttk::style element create Treeheading.cell image \
|
||||
[list $I(tree-basic) \
|
||||
pressed $I(tree-pressed)
|
||||
] -border 5 -padding 6 -sticky nsew
|
||||
|
||||
ttk::style element create Treeitem.indicator image \
|
||||
[list $I(right) \
|
||||
user2 $I(empty) \
|
||||
{user1 focus} $I(down-focus) \
|
||||
focus $I(right-focus) \
|
||||
user1 $I(down) \
|
||||
] -width 17 -sticky {}
|
||||
|
||||
ttk::style configure Treeview -background $colors(-bg)
|
||||
ttk::style configure Treeview.Item -padding {2 0 0 0}
|
||||
|
||||
ttk::style map Treeview \
|
||||
-background [list selected $colors(-selectbg)] \
|
||||
-foreground [list selected $colors(-selectfg)]
|
||||
|
||||
# Sashes
|
||||
#ttk::style map TPanedwindow -background [list hover $colors(-bg)]
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 445 B |
|
After Width: | Height: | Size: 463 B |
|
After Width: | Height: | Size: 311 B |
|
After Width: | Height: | Size: 324 B |
|
After Width: | Height: | Size: 444 B |
|
After Width: | Height: | Size: 353 B |
|
After Width: | Height: | Size: 526 B |
|
After Width: | Height: | Size: 390 B |
|
After Width: | Height: | Size: 531 B |
|
After Width: | Height: | Size: 358 B |
|
After Width: | Height: | Size: 281 B |
|
After Width: | Height: | Size: 358 B |
|
After Width: | Height: | Size: 403 B |
|
After Width: | Height: | Size: 311 B |
|
After Width: | Height: | Size: 398 B |
|
After Width: | Height: | Size: 335 B |
|
After Width: | Height: | Size: 247 B |
|
After Width: | Height: | Size: 260 B |
|
After Width: | Height: | Size: 244 B |
|
After Width: | Height: | Size: 200 B |
|
After Width: | Height: | Size: 266 B |
|
After Width: | Height: | Size: 130 B |
|
After Width: | Height: | Size: 154 B |
|
After Width: | Height: | Size: 157 B |
|
After Width: | Height: | Size: 154 B |
|
After Width: | Height: | Size: 190 B |
|
After Width: | Height: | Size: 765 B |
|
After Width: | Height: | Size: 547 B |
|
After Width: | Height: | Size: 771 B |
|
After Width: | Height: | Size: 754 B |
|
After Width: | Height: | Size: 538 B |
|
After Width: | Height: | Size: 764 B |
|
After Width: | Height: | Size: 674 B |
|
After Width: | Height: | Size: 486 B |
|
After Width: | Height: | Size: 679 B |
|
After Width: | Height: | Size: 549 B |
|
After Width: | Height: | Size: 390 B |
|
After Width: | Height: | Size: 550 B |
|
After Width: | Height: | Size: 676 B |
|
After Width: | Height: | Size: 504 B |
|
After Width: | Height: | Size: 674 B |
|
After Width: | Height: | Size: 512 B |
|
After Width: | Height: | Size: 335 B |
|
After Width: | Height: | Size: 335 B |
|
After Width: | Height: | Size: 254 B |
|
After Width: | Height: | Size: 272 B |
|
After Width: | Height: | Size: 190 B |
|
After Width: | Height: | Size: 284 B |
|
After Width: | Height: | Size: 161 B |
|
After Width: | Height: | Size: 162 B |
|
After Width: | Height: | Size: 128 B |
|
After Width: | Height: | Size: 471 B |
|
After Width: | Height: | Size: 156 B |
|
After Width: | Height: | Size: 163 B |
|
After Width: | Height: | Size: 223 B |
|
After Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 183 B |
|
After Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 316 B |
|
After Width: | Height: | Size: 242 B |
|
After Width: | Height: | Size: 316 B |
|
After Width: | Height: | Size: 311 B |
|
After Width: | Height: | Size: 234 B |
|
After Width: | Height: | Size: 310 B |
|
After Width: | Height: | Size: 149 B |
|
After Width: | Height: | Size: 168 B |
|
After Width: | Height: | Size: 278 B |
|
After Width: | Height: | Size: 158 B |
|
After Width: | Height: | Size: 158 B |
|
After Width: | Height: | Size: 158 B |
@@ -0,0 +1,170 @@
|
||||
#!/bin/bash
|
||||
|
||||
get_touchdevice()
|
||||
{
|
||||
# echo -n "" > $touchlist
|
||||
for touch_id in $(xinput | grep -i -E 'touch|ILITEK|TSD' | cut -d '=' -f 2 | cut -f 1)
|
||||
do
|
||||
input_dev=$(xinput list-props $touch_id | grep "Device Node" | awk -F : '{print $2}' | awk -F \" '{print $2}' | awk '{print $1}')
|
||||
touch_screen=$(udevadm info $input_dev | grep "ID_INPUT_TOUCHSCREEN")
|
||||
|
||||
# dev=$(udevadm info $input_dev | grep "ID_SERIAL=" | cut -d "=" -f 2)
|
||||
dev=$(xinput list-props $touch_id | grep "Device '" | cut -d "'" -f 2)
|
||||
vid_t=$(udevadm info $input_dev | grep "ID_VENDOR_ID=" | cut -d "=" -f 2)
|
||||
pid_t=$(udevadm info $input_dev | grep "ID_MODEL_ID=" | cut -d "=" -f 2)
|
||||
path_t=$(udevadm info $input_dev | grep "ID_PATH=" | cut -d "=" -f 2)
|
||||
# echo "$vid_t,$pid_t,$path_t,$dev,$touch_id"
|
||||
echo "$dev"
|
||||
echo " 输入设备id:$touch_id, 硬件id:$vid_t:$pid_t, 设备路径:$path_t"
|
||||
done
|
||||
|
||||
}
|
||||
|
||||
|
||||
get_screeninfo()
|
||||
{
|
||||
xrandr | grep connected | grep -v disconnected | while read -r screen ; do
|
||||
# 提取显示器名称
|
||||
name=$(echo $screen | awk '{print $1}')
|
||||
|
||||
# 检查是否是主屏幕
|
||||
if echo $screen | grep -q "primary"; then
|
||||
primary="true"
|
||||
# 主屏幕的信息在第4个字段
|
||||
resolution=$(echo $screen | awk '{print $4}')
|
||||
else
|
||||
primary="false"
|
||||
# 非主屏幕的信息在第3个字段
|
||||
resolution=$(echo $screen | awk '{print $3}')
|
||||
fi
|
||||
|
||||
# 分离分辨率和坐标
|
||||
screen_resolution=$(echo $resolution | cut -d'+' -f1)
|
||||
screen_x=$(echo $resolution | cut -d'+' -f2)
|
||||
screen_y=$(echo $resolution | cut -d'+' -f3)
|
||||
|
||||
# 输出结果
|
||||
echo "显示器名称: $name"
|
||||
echo "分辨率: $screen_resolution"
|
||||
echo "X坐标: $screen_x"
|
||||
echo "Y坐标: $screen_y"
|
||||
echo "主屏幕: $primary"
|
||||
echo "---"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
show_help(){
|
||||
echo ktouch debug。输入字母可以执行对应的功能
|
||||
echo " h help,帮助"
|
||||
echo " q 退出debug程序"
|
||||
echo " dev [id] 查看触摸设备详细信息,比如 t 10 查看id=10的触摸设备详细信息"
|
||||
echo " map [屏幕] [id] 手动将对应id的设备校准到指定屏幕上,可能与系统设置冲突,需要重启才能恢复正常。"
|
||||
echo " log 收集本工具的日志,生成到【主目录/家目录/个人文件夹】中"
|
||||
echo " setting 开始进行一次触摸屏校准设置,并在终端打印出详细日志,但是不会保存。"
|
||||
echo " automap 执行一次自动校准,并在终端打印出详细日志"
|
||||
echo " service stop/start 停止或启动监控服务,包括其自启动也受影响"
|
||||
echo " fix 修复程序,已经配置的信息会被删除"
|
||||
echo " "
|
||||
}
|
||||
|
||||
map_to_output(){
|
||||
xinput map-to-output $2 $1
|
||||
}
|
||||
|
||||
show_dev_info(){
|
||||
xinput list-props $1
|
||||
}
|
||||
|
||||
service_control(){
|
||||
|
||||
if [ "$1" = "stop" ];then
|
||||
sudo systemctl stop ktouch_daemon.service
|
||||
sudo systemctl disable ktouch_daemon.service
|
||||
elif [ "$1" = "start" ];then
|
||||
sudo systemctl start ktouch_daemon.service
|
||||
sudo systemctl enable ktouch_daemon.service
|
||||
else
|
||||
echo 参数不支持!
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
|
||||
echo " ktouch 调试信息 "
|
||||
echo ============================
|
||||
echo "系统触摸屏、签字笔及其输入节点包括:"
|
||||
get_touchdevice
|
||||
|
||||
echo
|
||||
echo
|
||||
|
||||
echo 系统显示器有:
|
||||
get_screeninfo
|
||||
echo
|
||||
echo
|
||||
|
||||
|
||||
echo 当前的触摸屏映射配置为:
|
||||
cat /opt/ktouch/config
|
||||
echo
|
||||
echo
|
||||
|
||||
show_help
|
||||
while [[ 1 ]]
|
||||
do
|
||||
|
||||
echo -n "请输入命令: "
|
||||
|
||||
read CMD
|
||||
|
||||
if [ "$CMD" = "h" ];then
|
||||
show_help
|
||||
|
||||
elif [ "$CMD" = "q" ];then
|
||||
echo -n 0 > /tmp/ktouch/setflag
|
||||
exit 0
|
||||
|
||||
elif [ "$(echo $CMD | cut -d' ' -f1)" = "dev" ];then
|
||||
show_dev_info $(echo $CMD | cut -d' ' -f2)
|
||||
|
||||
elif [ "$(echo $CMD | cut -d' ' -f1)" = "map" ];then
|
||||
#进行一次映射,则需要停掉触摸监控,在程序结束后再打开
|
||||
echo -n 1 > /tmp/ktouch/setflag
|
||||
map_to_output $(echo $CMD | cut -d' ' -f2) $(echo $CMD | cut -d' ' -f3)
|
||||
|
||||
elif [ "$CMD" = "log" ];then
|
||||
kscreen-log
|
||||
|
||||
elif [ "$CMD" = "setting" ];then
|
||||
/opt/ktouch/screen_binder
|
||||
|
||||
elif [ "$CMD" = "automap" ];then
|
||||
kscreen-remap -s
|
||||
|
||||
elif [ "$(echo $CMD | cut -d' ' -f1)" = "service" ];then
|
||||
echo "请注意输入sudo密码"
|
||||
service_control $(echo $CMD | cut -d' ' -f2)
|
||||
echo 完成,执行结果为 $?
|
||||
|
||||
elif [ "$CMD" = "fix" ];then
|
||||
# 修复程序,流程为关闭程序监控,删除tmp,删除config,reconfigure一下,开启自启动
|
||||
echo "请注意输入sudo密码"
|
||||
service_control stop
|
||||
rm -rf /tmp/ktouch
|
||||
rm -f /opt/ktouch/config
|
||||
sudo dpkg-reconfigure ktouch >/dev/null 2>&1
|
||||
service_control start
|
||||
echo 完成,执行结果为 $?
|
||||
|
||||
|
||||
else
|
||||
echo "命令有误,请输入 h 查看支持的命令。"
|
||||
fi
|
||||
echo
|
||||
|
||||
|
||||
|
||||
|
||||
done
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
# 脚本名称
|
||||
# SCRIPT_NAME="ktouch-fix-daemon"
|
||||
SCRIPT_NAME=$0
|
||||
service_name="ktouch_daemon.service"
|
||||
SERVICE_FILE="/etc/systemd/system/ktouch_daemon.service"
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 打印函数
|
||||
print_info() {
|
||||
echo -e "${GREEN}[INFO ]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARN ]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
print_question() {
|
||||
echo -e "${BLUE}[ ? ]${NC} $1"
|
||||
}
|
||||
|
||||
# 检查并获取root权限
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
print_info "需要root权限来修改系统服务"
|
||||
|
||||
|
||||
if sudo -n true 2>/dev/null; then
|
||||
print_info "权限已自动获取,重启脚本"
|
||||
# 已经有sudo权限,直接重新运行脚本
|
||||
exec sudo "$0" "$@"
|
||||
else
|
||||
# 需要输入密码
|
||||
print_info "请输入sudo密码"
|
||||
exec sudo "$0" "$@"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查用户是否存在
|
||||
check_user_exists() {
|
||||
local username="$1"
|
||||
if id "$username" &>/dev/null; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 获取系统所有用户名列表
|
||||
get_all_users() {
|
||||
# 获取所有普通用户和系统用户(UID >= 1000 或 UID < 1000但具有登录shell)
|
||||
# getent passwd | cut -d: -f1 | sort
|
||||
awk -F: '$3 >= 1000 && $7 ~ /\/(bash|sh|zsh|csh|ksh)$/ {print $1}' /etc/passwd
|
||||
}
|
||||
|
||||
|
||||
|
||||
# 停止服务
|
||||
stop_service() {
|
||||
local service_name="ktouch_daemon.service"
|
||||
|
||||
print_info "停止 $service_name 服务..."
|
||||
|
||||
if systemctl is-active --quiet "$service_name"; then
|
||||
systemctl stop "$service_name"
|
||||
if [[ $? -eq 0 ]]; then
|
||||
print_info "服务已停止"
|
||||
else
|
||||
print_error "停止服务失败"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
print_info "服务未运行"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# 禁用服务自启动
|
||||
disable_service() {
|
||||
local service_name="ktouch_daemon.service"
|
||||
|
||||
print_info "禁用服务自启动..."
|
||||
|
||||
if systemctl is-enabled --quiet "$service_name" 2>/dev/null; then
|
||||
systemctl disable "$service_name"
|
||||
if [[ $? -eq 0 ]]; then
|
||||
print_info "服务自启动已禁用"
|
||||
else
|
||||
print_error "禁用服务自启动失败"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
print_info "服务自启动未启用"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# 修改服务文件中的用户名
|
||||
modify_service_user() {
|
||||
local service_file="$1"
|
||||
local new_user="$2"
|
||||
local backup_file="${service_file}.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
print_info "备份服务文件: $backup_file"
|
||||
cp "$service_file" "$backup_file"
|
||||
|
||||
print_info "修改服务文件中的用户名..."
|
||||
|
||||
# 检查服务文件是否包含User=行
|
||||
if grep -q "^User=" "$service_file"; then
|
||||
# 替换现有的User=行
|
||||
sed -i "s/^User=.*/User=$new_user/" "$service_file"
|
||||
else
|
||||
# 在[Service]部分添加User=行
|
||||
if grep -q "\[Service\]" "$service_file"; then
|
||||
sed -i "/\[Service\]/a User=$new_user" "$service_file"
|
||||
else
|
||||
# 如果没有[Service]部分,在文件末尾添加
|
||||
echo -e "\n[Service]\nUser=$new_user" >> "$service_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $? -eq 0 ]]; then
|
||||
print_info "服务文件修改成功"
|
||||
return 0
|
||||
else
|
||||
print_error "服务文件修改失败"
|
||||
# 恢复备份
|
||||
cp "$backup_file" "$service_file"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 重新加载systemd配置
|
||||
reload_systemd() {
|
||||
print_info "重新加载systemd配置..."
|
||||
systemctl daemon-reload
|
||||
if [[ $? -eq 0 ]]; then
|
||||
print_info "systemd配置已重新加载"
|
||||
return 0
|
||||
else
|
||||
print_error "重新加载systemd配置失败"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 启用服务自启动
|
||||
enable_service() {
|
||||
local service_name="ktouch_daemon.service"
|
||||
|
||||
print_info "启用服务自启动..."
|
||||
|
||||
systemctl enable "$service_name"
|
||||
if [[ $? -eq 0 ]]; then
|
||||
print_info "服务自启动已启用"
|
||||
return 0
|
||||
else
|
||||
print_error "启用服务自启动失败"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 启动服务
|
||||
start_service() {
|
||||
local service_name="ktouch_daemon.service"
|
||||
|
||||
print_info "启动服务..."
|
||||
|
||||
systemctl start "$service_name"
|
||||
if [[ $? -eq 0 ]]; then
|
||||
print_info "服务已启动"
|
||||
return 0
|
||||
else
|
||||
print_error "启动服务失败"
|
||||
print_info "可以使用 'systemctl status $service_name' 查看详细错误信息"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 显示服务状态
|
||||
show_service_status() {
|
||||
local service_name="ktouch_daemon.service"
|
||||
|
||||
print_info "当前服务状态:"
|
||||
systemctl status "$service_name" --no-pager -l
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
print_info "=== ktouch_daemon 服务用户修改工具 ==="
|
||||
|
||||
if [ ! -f $SERVICE_FILE ];then
|
||||
print_error "服务文件不存在,请重新安装ktouch软件!"
|
||||
exit 1
|
||||
|
||||
fi
|
||||
|
||||
# 检查root权限
|
||||
check_root "$@"
|
||||
|
||||
# 显示当前服务配置
|
||||
print_info "当前服务配置:"
|
||||
grep -E "User=" "$SERVICE_FILE" 2>/dev/null || print_warning "无法获取当前配置"
|
||||
|
||||
# 获取用户名
|
||||
while true; do
|
||||
echo
|
||||
print_question "正在修改 ktouch_daemon 监控服务的执行用户,请在下面输入需要执行的用户名"
|
||||
echo -n "用户名: "
|
||||
read -r username
|
||||
|
||||
# 检查输入是否为空
|
||||
if [[ -z "$username" ]]; then
|
||||
print_error "用户名不能为空,请重新输入"
|
||||
continue
|
||||
fi
|
||||
|
||||
# 检查用户是否存在
|
||||
if check_user_exists "$username"; then
|
||||
# print_info "用户 '$username' 存在"
|
||||
break
|
||||
else
|
||||
print_error "用户 '$username' 不存在,请重新输入"
|
||||
print_info "可用的用户列表:"
|
||||
get_all_users | head -10 | tr '\n' ' '
|
||||
echo -e "\n(显示前10个用户,完整列表请查看 /etc/passwd)"
|
||||
fi
|
||||
done
|
||||
|
||||
# 确认操作
|
||||
echo
|
||||
print_info "即将执行以下操作:"
|
||||
print_info "1. 停止 ktouch_daemon.service 服务"
|
||||
print_info "2. 禁用服务自启动"
|
||||
print_info "3. 修改服务文件中的用户为: $username"
|
||||
print_info "4. 重新启用服务自启动"
|
||||
print_info "5. 启动服务"
|
||||
echo
|
||||
echo -n " 是否继续? (y/N): "
|
||||
read -r confirm
|
||||
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
print_info "操作已取消"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 执行操作
|
||||
echo
|
||||
print_info "开始修改服务配置..."
|
||||
|
||||
# 停止服务
|
||||
if ! stop_service; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 禁用服务自启动
|
||||
if ! disable_service; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 修改服务文件
|
||||
if ! modify_service_user "$SERVICE_FILE" "$username"; then
|
||||
print_error "修改失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 重新加载systemd配置
|
||||
if ! reload_systemd; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 启用服务自启动
|
||||
if ! enable_service; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 启动服务
|
||||
if ! start_service; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 显示最终状态
|
||||
echo
|
||||
print_info "=== 操作完成 ==="
|
||||
show_service_status
|
||||
|
||||
echo
|
||||
print_info "服务用户已成功修改为: $username"
|
||||
}
|
||||
|
||||
# 错误处理
|
||||
set -euo pipefail
|
||||
|
||||
# 执行主函数
|
||||
main "$@"
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# 搜集日志和配置文件,用于debug分析
|
||||
|
||||
log_path="/opt/ktouch"
|
||||
now=$(date "+%m%d-%H%M%S")
|
||||
|
||||
|
||||
tdir="/tmp/klog_$now"
|
||||
|
||||
mkdir $tdir && cd $tdir
|
||||
|
||||
echo "正在收集日志文件,请注意需要输入sudo密码。"
|
||||
|
||||
# cp /opt/ktouch/*log* $tdir
|
||||
# cp /opt/ktouch/config $tdir
|
||||
# cp /opt/ktouch/*.txt $tdir
|
||||
|
||||
|
||||
# 采集系统信息
|
||||
export DISPLAY=:0
|
||||
xinput > $tdir/xinput_info.txt
|
||||
for id in $(xinput list --id-only); do
|
||||
xinput --list-props "$id" >> $tdir/xinput_props.txt
|
||||
echo -e "\n\n" $tdir/xinput_props.txt
|
||||
done
|
||||
xrandr > $tdir/xrandr_info.txt
|
||||
sudo dmidecode > $tdir/dmidecode.txt
|
||||
lsusb >> $tdir/lsusb.txt
|
||||
lsusb -t >> $tdir/lsusb.txt
|
||||
lsblk -f >> $tdir/lsblk.txt
|
||||
lspci >> $tdir/lspci.txt
|
||||
|
||||
if [ -f /etc/kylin-release ]; then
|
||||
cat /etc/os-release /etc/kylin-release 2>/dev/null >> $tdir/os-version.txt
|
||||
elif [ -f /etc/uos-release ]; then
|
||||
cat /etc/os-release /etc/uos-release /etc/os-version 2>/dev/null >> $tdir/os-version.txt
|
||||
else
|
||||
cat /etc/os-release 2>/dev/null >> $tdir/os-version.txt
|
||||
fi
|
||||
|
||||
# 采集系统的日志
|
||||
sudo cp /var/log/messages* $tdir >/dev/null 2>&1
|
||||
sudo cp /var/log/boot* $tdir >/dev/null 2>&1
|
||||
sudo cp /var/log/kern* $tdir >/dev/null 2>&1
|
||||
sudo cp /var/log/Xorg* $tdir >/dev/null 2>&1
|
||||
sudo cp /var/log/syslog* $tdir >/dev/null 2>&1
|
||||
|
||||
|
||||
# 将程序和日志整体打包
|
||||
sudo cp /etc/systemd/system/ktouch_daemon.service $tdir/ktouch_daemon.service
|
||||
sudo chmod 777 -R $tdir
|
||||
cp -r /opt/ktouch $tdir/ktouch
|
||||
|
||||
|
||||
ls -la ./ >> $tdir/fileinfo.txt
|
||||
ls -la ./ktouch >> $tdir/fileinfo.txt
|
||||
|
||||
# 打包
|
||||
echo "正在打包文件,将会生成在主目录/家目录/个人文件夹"
|
||||
|
||||
cd /tmp
|
||||
tar -czf ~/ktouch_log-$now.tar.gz ./klog_$now
|
||||
|
||||
rm -rf /tmp/klog
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
export DISPLAY=:0
|
||||
|
||||
LOG_FILE=/opt/ktouch/touchscreen.log
|
||||
|
||||
# 日志函数
|
||||
log_message() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') kscreen-remap : $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
log_message "用户($USER)手动执行映射"
|
||||
|
||||
# 清空UOS的触摸屏设置信息
|
||||
if [ -f /usr/bin/gsettings ];then
|
||||
gsettings set com.deepin.dde.display map-output '' > /dev/null 2>&1
|
||||
if [ "$?" == "0" ];then
|
||||
echo "成功清除UOS系统设置中的触摸屏设置信息"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 更新一次显示器信息
|
||||
if [ ! -d /tmp/ktouch ];then
|
||||
mkdir -p /tmp/ktouch
|
||||
fi
|
||||
touch /tmp/ktouch/screen.txt
|
||||
/opt/ktouch/screen_ds_once /tmp/ktouch/screen.txt
|
||||
|
||||
|
||||
if [ ! -f "/opt/ktouch/config" ];then
|
||||
# 配置文件不存在
|
||||
log_message "配置文件不存在,不执行"
|
||||
notify-send -u critical --icon=dialog-warning -t 9000 "触摸屏映射工具 ktouch" "配置文件不存在,请先进行配置!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$1" == "" ];then
|
||||
/opt/ktouch/touch_set
|
||||
notify-send -u low --icon=view-fullscreen -t 3000 "触摸屏映射工具 ktouch" "映射操作完成,请点击屏幕检查。"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$1" == "-s" ];then
|
||||
# 单次映射,用于开机自启动脚本保障使用。
|
||||
log_message "$1 参数 = 单次执行"
|
||||
/opt/ktouch/touch_set
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "请不要携带任何参数执行。本次未作任何操作。"
|
||||
log_message "映射命令有参数传入($1), 忽略"
|
||||
|
||||
exit 127
|
||||
@@ -0,0 +1,539 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import signal
|
||||
import hashlib
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
|
||||
# 配置X11环境变量
|
||||
os.environ['DISPLAY'] = ':0'
|
||||
# os.environ['XAUTHORITY'] = '/home/pi/.Xauthority' # 根据实际情况调整路径
|
||||
|
||||
# 全局变量
|
||||
processes = {} # 存储进程对象
|
||||
monitor_thread = None # 监控线程
|
||||
running = True # 控制循环运行
|
||||
isSettings = False
|
||||
SettingFlagPath="/tmp/ktouch/setflag"
|
||||
apppath="/opt/ktouch"
|
||||
display_monitor_process = None # 存储display_monitor进程对象
|
||||
|
||||
|
||||
# 配置日志
|
||||
def setup_logging():
|
||||
log_file = os.path.join(apppath, 'touchscreen.log')
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s daemon.py [%(levelname)s] %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(log_file, encoding='utf-8'),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
logger = setup_logging()
|
||||
|
||||
# 信号处理函数
|
||||
def signal_handler(signum, frame):
|
||||
global running
|
||||
logger.info("Received signal %d, shutting down...", signum)
|
||||
running = False
|
||||
cleanup()
|
||||
|
||||
def cleanup():
|
||||
"""清理函数,停止所有子进程"""
|
||||
logger.info("清理子进程")
|
||||
for proc_name, proc_info in processes.items():
|
||||
try:
|
||||
proc = proc_info['process']
|
||||
if proc and proc.poll() is None: # 进程还在运行
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
logger.info("关闭了进程: %s", proc_name)
|
||||
except Exception as e:
|
||||
logger.error("关闭进程失败 %s: %s", proc_name, e)
|
||||
processes.clear()
|
||||
|
||||
|
||||
# 检查display_monitor.py是否在运行
|
||||
def check_display_monitor_running():
|
||||
"""检查display_monitor.py是否已经在运行"""
|
||||
try:
|
||||
# 使用pgrep检查进程
|
||||
result = subprocess.run(['pgrep', '-f', 'display_monitor.py'],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
return result.returncode == 0 and len(result.stdout.strip()) > 0
|
||||
except Exception as e:
|
||||
logger.error("检查display_monitor.py进程状态失败: %s", e)
|
||||
return False
|
||||
|
||||
# 启动display_monitor.py
|
||||
def start_display_monitor():
|
||||
"""启动display_monitor.py脚本"""
|
||||
global display_monitor_process
|
||||
|
||||
# 检查脚本是否存在
|
||||
display_monitor_script = "/opt/ktouch/display_monitor.py"
|
||||
if not os.path.exists(display_monitor_script):
|
||||
logger.warning("display_monitor.py脚本不存在: %s", display_monitor_script)
|
||||
return False
|
||||
|
||||
# 检查是否已经在运行
|
||||
if check_display_monitor_running():
|
||||
logger.info("display_monitor.py已经在运行")
|
||||
return True
|
||||
|
||||
try:
|
||||
# 设置X11环境
|
||||
env = os.environ.copy()
|
||||
env['DISPLAY'] = ':0'
|
||||
|
||||
# 启动display_monitor.py,使用nohup使其在后台独立运行
|
||||
display_monitor_process = subprocess.Popen(
|
||||
['nohup', 'python3', display_monitor_script, '&'],
|
||||
env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
preexec_fn=os.setpgrp # 创建新的进程组,使子进程独立
|
||||
)
|
||||
|
||||
# 等待一下让进程启动
|
||||
time.sleep(2)
|
||||
|
||||
# 检查是否成功启动
|
||||
if check_display_monitor_running():
|
||||
logger.info("成功启动display_monitor.py (PID: %d)", display_monitor_process.pid)
|
||||
return True
|
||||
else:
|
||||
logger.error("启动display_monitor.py失败")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error("启动display_monitor.py时发生错误: %s", e)
|
||||
return False
|
||||
|
||||
# 1. 日志文件轮转函数
|
||||
def rotate_logs():
|
||||
log_files = [os.path.join(apppath,'sub_modules.log'), os.path.join(apppath,'touchscreen.log')]
|
||||
max_size = 10 * 1024 * 1024 # 10MB
|
||||
max_backups = 7
|
||||
|
||||
for log_file in log_files:
|
||||
if os.path.exists(log_file) and os.path.getsize(log_file) > max_size:
|
||||
#print(f'文件{log_file}的大小是{os.path.getsize(log_file)}')
|
||||
# 删除最旧的备份文件
|
||||
oldest_backup = "%s.%d" % (log_file, max_backups)
|
||||
if os.path.exists(oldest_backup):
|
||||
os.remove(oldest_backup)
|
||||
logger.info("清理过早的日志: %s", oldest_backup)
|
||||
|
||||
# 重命名现有备份文件
|
||||
for i in range(max_backups-1, 0, -1):
|
||||
old_name = "%s.%d" % (log_file, i)
|
||||
new_name = "%s.%d" % (log_file, i+1)
|
||||
if os.path.exists(old_name):
|
||||
os.rename(old_name, new_name)
|
||||
# logger.info("Renamed %s to %s", old_name, new_name)
|
||||
|
||||
# 重命名当前日志文件
|
||||
os.rename(log_file, "%s.1" % log_file)
|
||||
# logger.info("Rotated log file: %s -> %s.1", log_file, log_file)
|
||||
|
||||
# 2. 检查并创建所需文件
|
||||
def initialize_files():
|
||||
ktouch_dir = Path("/tmp/ktouch")
|
||||
files_to_create = [
|
||||
"screen.txt",
|
||||
"touch.txt",
|
||||
"touch_need_update.txt",
|
||||
"usbadd.txt",
|
||||
"setflag"
|
||||
]
|
||||
|
||||
# 创建目录
|
||||
if not ktouch_dir.exists():
|
||||
ktouch_dir.mkdir()
|
||||
logger.info("创建状态缓存目录 /tmp/ktouch ")
|
||||
|
||||
# 创建或初始化文件
|
||||
for file_name in files_to_create:
|
||||
file_path = ktouch_dir / file_name
|
||||
with open(str(file_path), 'w') as f:
|
||||
f.write('1')
|
||||
logger.info("初始化缓存文件 %s: to 1", file_path)
|
||||
|
||||
|
||||
# 启动单个后台程序
|
||||
def start_process(proc_name, proc_args):
|
||||
"""启动单个进程并添加到监控列表"""
|
||||
# 设置X11环境
|
||||
env = os.environ.copy()
|
||||
env['DISPLAY'] = ':0'
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(proc_args, env=env)
|
||||
processes[proc_name] = {
|
||||
'process': proc,
|
||||
'args': proc_args,
|
||||
'start_time': time.time(),
|
||||
'restart_count': 0
|
||||
}
|
||||
logger.info("拉起子进程 %s (PID: %d)", proc_name, proc.pid)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("进程启动失败 %s: %s", proc_name, e)
|
||||
return False
|
||||
|
||||
|
||||
# 3. 启动后台程序(添加X11环境支持)
|
||||
def start_background_processes():
|
||||
process_configs = {
|
||||
'screen_ds': ["/opt/ktouch/screen_ds", "/tmp/ktouch/screen.txt"],
|
||||
'touch_ds': ["/opt/ktouch/touch_ds", "/tmp/ktouch/touch.txt", "/tmp/ktouch/touch_need_update.txt"],
|
||||
'usb_ds': ["/opt/ktouch/usb_ds", "/tmp/ktouch/usbadd.txt"]
|
||||
}
|
||||
|
||||
for proc_name, proc_args in process_configs.items():
|
||||
start_process(proc_name, proc_args)
|
||||
|
||||
|
||||
# 停止所有子进程
|
||||
def stop_all_processes():
|
||||
"""停止所有监控的子进程"""
|
||||
logger.info("停止所有子进程")
|
||||
for proc_name, proc_info in list(processes.items()):
|
||||
try:
|
||||
proc = proc_info['process']
|
||||
if proc and proc.poll() is None: # 进程还在运行
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=3)
|
||||
logger.info("成功停止进程: %s", proc_name)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
logger.warning("强制停止进程: %s", proc_name)
|
||||
# 从监控列表中移除
|
||||
del processes[proc_name]
|
||||
except Exception as e:
|
||||
logger.error("停止进程失败 %s: %s", proc_name, e)
|
||||
|
||||
|
||||
# 检查暂停标志文件
|
||||
def check_pause_flag():
|
||||
"""检查setflag文件,返回是否需要暂停"""
|
||||
|
||||
if not os.path.exists(SettingFlagPath):
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(SettingFlagPath, 'r') as f:
|
||||
content = f.read().strip()
|
||||
return content == '1'
|
||||
except Exception as e:
|
||||
# logger.error("读取setflag文件失败: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
# 进入设置轮空的状态
|
||||
def handle_pause_state():
|
||||
"""处理暂停和恢复逻辑"""
|
||||
global isSettings
|
||||
|
||||
if should_pause and not paused:
|
||||
# 进入暂停状态
|
||||
logger.info("检测到setflag=1,进入暂停状态")
|
||||
paused = True
|
||||
stop_all_processes()
|
||||
logger.info("已暂停所有监控和子进程")
|
||||
|
||||
elif not should_pause and paused:
|
||||
# 恢复运行状态
|
||||
logger.info("检测到setflag=0或文件不存在,恢复运行状态")
|
||||
paused = False
|
||||
# 重新启动后台进程
|
||||
start_background_processes()
|
||||
logger.info("已恢复所有监控和子进程")
|
||||
|
||||
return paused
|
||||
|
||||
|
||||
|
||||
# 监控和重启进程
|
||||
def monitor_processes():
|
||||
"""监控进程状态,如果进程退出则重启"""
|
||||
global running, isSettings
|
||||
|
||||
buff_isSettings = isSettings
|
||||
|
||||
while running:
|
||||
try:
|
||||
if not isSettings == buff_isSettings:
|
||||
# 值变化,开始操作
|
||||
if isSettings:
|
||||
stop_all_processes()
|
||||
else:
|
||||
start_background_processes()
|
||||
|
||||
buff_isSettings = isSettings
|
||||
|
||||
if isSettings:
|
||||
time.sleep(5)
|
||||
continue
|
||||
|
||||
|
||||
for proc_name, proc_info in list(processes.items()):
|
||||
proc = proc_info['process']
|
||||
returncode = proc.poll()
|
||||
|
||||
if returncode is not None: # 进程已退出
|
||||
logger.warning("进程 %s (PID: %d) 已退出,代码 %d", proc_name, proc.pid, returncode)
|
||||
|
||||
# 限制重启次数,避免无限重启
|
||||
proc_info['restart_count'] += 1
|
||||
if proc_info['restart_count'] > 128: # 最多重启128次
|
||||
logger.error("启动 %s 进程被结束超过128次,决定不再重启。请检查日志文件。", proc_name)
|
||||
del processes[proc_name]
|
||||
continue
|
||||
|
||||
# 等待一段时间再重启
|
||||
time.sleep(2)
|
||||
|
||||
# 重新启动进程
|
||||
if start_process(proc_name, proc_info['args']):
|
||||
logger.info("重启进程: %s", proc_name)
|
||||
else:
|
||||
logger.error("重启进程失败: %s", proc_name)
|
||||
|
||||
# 检查是否有进程需要启动(初始启动失败的情况)
|
||||
expected_processes = ['screen_ds', 'touch_ds', 'usb_ds']
|
||||
for proc_name in expected_processes:
|
||||
if proc_name not in processes:
|
||||
logger.warning("发现 %s 消失, 准备重新启动", proc_name)
|
||||
# 这里需要根据进程名重新构建参数
|
||||
if proc_name == 'screen_ds':
|
||||
start_process(proc_name, ["/opt/ktouch/screen_ds", "/tmp/ktouch/screen.txt"])
|
||||
elif proc_name == 'touch_ds':
|
||||
start_process(proc_name, ["/opt/ktouch/touch_ds", "/tmp/ktouch/touchmap.txt", "/tmp/ktouch/touch_need_update.txt"])
|
||||
elif proc_name == 'usb_ds':
|
||||
start_process(proc_name, ["/opt/ktouch/usb_ds", "/tmp/ktouch/usbadd.txt"])
|
||||
|
||||
time.sleep(5) # 每2秒检查一次进程状态
|
||||
|
||||
except Exception as e:
|
||||
logger.error("进程监控出现问题: %s", e)
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
# 4. 执行touch_remap并重置文件(添加X11环境支持)
|
||||
def run_touch_remap():
|
||||
# 设置X11环境
|
||||
env = os.environ.copy()
|
||||
env['DISPLAY'] = ':0'
|
||||
|
||||
try:
|
||||
# 执行touch_remap程序
|
||||
result = subprocess.run(["/opt/ktouch/touch_set", "/tmp/ktouch/touch.txt"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, env=env)
|
||||
if result.returncode == 0:
|
||||
logger.info("执行映射(touch_set)成功")
|
||||
else:
|
||||
logger.error("执行映射(touch_set)失败,错误信息为 %d: %s", result.returncode, result.stderr)
|
||||
except Exception as e:
|
||||
logger.error("无法执行touch_set映射程序!!!!!: %s", e)
|
||||
|
||||
# 重置文件为0
|
||||
ktouch_dir = Path("/tmp/ktouch")
|
||||
files_to_reset = [
|
||||
"touch_need_update.txt",
|
||||
"usbadd.txt",
|
||||
"setflag"
|
||||
]
|
||||
|
||||
for file_name in files_to_reset:
|
||||
file_path = ktouch_dir / file_name
|
||||
try:
|
||||
with open(str(file_path), 'w') as f:
|
||||
f.write('0')
|
||||
# logger.info("Reset %s to 0", file_path)
|
||||
except Exception as e:
|
||||
# logger.error("Failed to reset %s: %s", file_path, e)
|
||||
pass
|
||||
|
||||
# 5. 监控文件变化
|
||||
def monitor_files():
|
||||
global isSettings
|
||||
# 记录screen.txt的初始状态
|
||||
screen_file = "/tmp/ktouch/screen.txt"
|
||||
last_screen_content = ""
|
||||
|
||||
# 启动时读取以一下屏幕文件
|
||||
if os.path.exists(screen_file):
|
||||
with open(screen_file, 'r') as f:
|
||||
last_screen_content = f.read().strip()
|
||||
|
||||
logger.info("开始主程序监控")
|
||||
buff_mainloop_isSettings = check_pause_flag()
|
||||
|
||||
while running:
|
||||
# 检查是否在设置过程中,是的话则暂停循环,并关闭三个子进程
|
||||
isSettings = check_pause_flag()
|
||||
if not buff_mainloop_isSettings == isSettings:
|
||||
logger.info("设置标志变化:%d", isSettings)
|
||||
# 变化,如果设置结束则触发一起映射
|
||||
if not isSettings:
|
||||
run_touch_remap()
|
||||
|
||||
buff_mainloop_isSettings = isSettings
|
||||
|
||||
if isSettings:
|
||||
sleep(2)
|
||||
continue
|
||||
|
||||
|
||||
# 检查是否需要执行touch_remap
|
||||
need_update = False
|
||||
|
||||
# 检查usbadd.txt和touch_need_update.txt
|
||||
for file_name in ["/tmp/ktouch/usbadd.txt", "/tmp/ktouch/touch_need_update.txt"]:
|
||||
try:
|
||||
with open(file_name, 'r') as f:
|
||||
content = f.read().strip()
|
||||
if content == '1':
|
||||
logger.info("文件 %s 内容变为1,触发重映射", file_name)
|
||||
need_update = True
|
||||
except Exception as e:
|
||||
logger.error("读取文件失败 %s: %s", file_name, e)
|
||||
|
||||
# 检查screen.txt是否发生变化
|
||||
try:
|
||||
with open(screen_file, 'r') as f:
|
||||
current_content = f.read().strip()
|
||||
if current_content != last_screen_content:
|
||||
logger.info("文件 screen.txt 内容变化,触发重映射")
|
||||
need_update = True
|
||||
last_screen_content = current_content
|
||||
except Exception as e:
|
||||
logger.error("读取文件失败 %s: %s", screen_file, e)
|
||||
|
||||
# 如果需要更新,执行touch_remap
|
||||
if need_update:
|
||||
run_touch_remap()
|
||||
|
||||
# 等待一段时间再次检查
|
||||
time.sleep(1)
|
||||
|
||||
# 检查X11环境是否可用
|
||||
def check_x11_environment():
|
||||
try:
|
||||
# 尝试运行一个简单的X11命令来检查环境
|
||||
result = subprocess.run(['xset', '-q'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
logger.info("X11 environment is available")
|
||||
return True
|
||||
else:
|
||||
logger.warning("X11 environment check failed")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("Error checking X11 environment: %s", e)
|
||||
return False
|
||||
|
||||
# 主函数
|
||||
def main():
|
||||
global monitor_thread, running
|
||||
has_config_file=True
|
||||
try:
|
||||
# 注册信号处理
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
logger.info("==============kscreen_remap_daemon 程序启动====================")
|
||||
|
||||
# 检查X11环境
|
||||
x11_try=3
|
||||
while x11_try:
|
||||
if not check_x11_environment():
|
||||
logger.warning("主程序:X11 environment may not be available, 程序将继续运行但无效果")
|
||||
x11_try -= 1
|
||||
sleep(1)
|
||||
else:
|
||||
x11_try = 0
|
||||
|
||||
# 1. 日志轮转
|
||||
rotate_logs()
|
||||
logger.info("主程序:处理日志 OK")
|
||||
|
||||
# 2. 初始化文件
|
||||
initialize_files()
|
||||
logger.info("主程序:初始化和检查文件 OK")
|
||||
|
||||
|
||||
# 2.5 启动屏幕设置的后台监控进程
|
||||
# 检查并启动display_monitor.py(UOS中)
|
||||
if os.path.exists('/etc/os-version'):
|
||||
logger.info("检查并启动display_monitor.py...")
|
||||
if start_display_monitor():
|
||||
logger.info("display_monitor.py启动成功或已在运行")
|
||||
else:
|
||||
logger.warning("display_monitor.py启动失败,但主程序将继续运行")
|
||||
|
||||
# ========如果没有配置文件,则不用运行监控===========
|
||||
if not os.path.exists("/opt/ktouch/config"):
|
||||
logger.info("配置文件不存在, 进入等待模式")
|
||||
has_config_file = False
|
||||
while True:
|
||||
if not running:
|
||||
break
|
||||
time.sleep(5)
|
||||
if os.path.exists("/opt/ktouch/config"):
|
||||
logger.info("配置文件出现, 进行一次映射,然后开始监控")
|
||||
run_touch_remap()
|
||||
break
|
||||
|
||||
|
||||
|
||||
# 3. 启动后台进程
|
||||
start_background_processes()
|
||||
logger.info("主程序:启动子进程 OK")
|
||||
|
||||
# 启动进程监控线程
|
||||
monitor_thread = Thread(target=monitor_processes)
|
||||
monitor_thread.daemon = True
|
||||
monitor_thread.start()
|
||||
logger.info("主程序:启动进程监控 OK")
|
||||
|
||||
sleep(5)
|
||||
# 4. 首次执行touch_remap
|
||||
while True:
|
||||
if os.path.exists("/tmp/ktouch/screen.txt"):
|
||||
break
|
||||
# 如果screen.txt不存在可能引起故障
|
||||
|
||||
run_touch_remap()
|
||||
logger.info("主程序:执行一次映射 OK")
|
||||
|
||||
sleep(2)
|
||||
# 5. 开始监控文件变化
|
||||
logger.info("主程序:开始监控loop")
|
||||
monitor_files()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Unexpected error in main: %s", e)
|
||||
running = False
|
||||
cleanup()
|
||||
sys.exit(1)
|
||||
finally:
|
||||
cleanup()
|
||||
logger.info("Daemon script stopped")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/bin/bash
|
||||
|
||||
workpath=/opt/ktouch
|
||||
tmppath=/tmp/ktouch
|
||||
INPUT_FILE="$workpath/touch_dis_table.txt"
|
||||
OUTPUT_FILE="$workpath/config"
|
||||
LOG_FILE="$workpath/touchscreen.log"
|
||||
|
||||
# 日志函数
|
||||
log_message() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') kscreen-setup : $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
mkdir -p /tmp/ktouch
|
||||
cd $workpath
|
||||
|
||||
# 首先往setflag写入1停止监控
|
||||
echo -n "1" > $tmppath/setflag
|
||||
|
||||
# 消灭监控进程
|
||||
log_message "关闭监控进程"
|
||||
sleep 2
|
||||
|
||||
# 环境变量
|
||||
export DISPLAY=:0
|
||||
|
||||
# 备份config文件
|
||||
echo -n "" > $workpath/config.bk
|
||||
cp $workpath/config $workpath/config.bk
|
||||
|
||||
# 启动检测
|
||||
log_message "启动GUI化配置程序"
|
||||
$workpath/screen_binder
|
||||
log_message "配置过程结束"
|
||||
|
||||
|
||||
# ==============解析文件===============
|
||||
|
||||
# 检查输入文件是否存在
|
||||
if [[ ! -f "$INPUT_FILE" ]]; then
|
||||
log_message "错误: 输入文件 $INPUT_FILE 不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n "" > $workpath/config
|
||||
|
||||
log_message "开始处理触摸屏设备信息"
|
||||
# 逐行读取输入文件
|
||||
while IFS='|' read -r display_name touchscreen_name touchscreen_id event_path; do
|
||||
# 去除可能的空白字符
|
||||
display_name=$(echo "$display_name" | xargs)
|
||||
touchscreen_name=$(echo "$touchscreen_name" | xargs)
|
||||
event_path=$(echo "$event_path" | xargs)
|
||||
|
||||
log_message "处理设备: $touchscreen_name, 事件路径: $event_path"
|
||||
|
||||
# 检查事件路径是否为空
|
||||
if [[ -z "$event_path" ]]; then
|
||||
log_message "警告: 事件路径为空,跳过此行"
|
||||
continue
|
||||
fi
|
||||
|
||||
# 检查设备是否存在
|
||||
if [[ ! -e "$event_path" ]]; then
|
||||
log_message "警告: 设备路径 $event_path 不存在,跳过"
|
||||
continue
|
||||
fi
|
||||
|
||||
# 使用udevadm查询设备信息
|
||||
vid=""
|
||||
pid=""
|
||||
usb_path=""
|
||||
|
||||
# 获取设备信息
|
||||
device_info=$(udevadm info --query=property --path="$(udevadm info --query=path --name="$event_path" 2>/dev/null)" 2>/dev/null)
|
||||
|
||||
if [[ -n "$device_info" ]]; then
|
||||
# 提取VID和PID
|
||||
vid=$(echo "$device_info" | grep -i "ID_VENDOR_ID" | cut -d= -f2 | head -n1)
|
||||
pid=$(echo "$device_info" | grep -i "ID_MODEL_ID" | cut -d= -f2 | head -n1)
|
||||
|
||||
# 提取USB路径
|
||||
usb_path=$(echo "$device_info" | grep -i "ID_PATH" | grep -i "usb" | cut -d= -f2 | head -n1)
|
||||
|
||||
# # 如果没找到USB路径,尝试其他方式
|
||||
# if [[ -z "$usb_path" ]]; then
|
||||
# usb_path=$(echo "$device_info" | grep -i "DEVPATH" | cut -d= -f2 | head -n1)
|
||||
# fi
|
||||
fi
|
||||
|
||||
|
||||
log_message "获取到信息: VID=$vid, PID=$pid, USB路径=$usb_path"
|
||||
|
||||
# 组合新格式并写入输出文件
|
||||
new_line="${display_name}|${touchscreen_name}|${vid}|${pid}|${usb_path}"
|
||||
echo "$new_line" >> "$OUTPUT_FILE"
|
||||
log_message "写入: $new_line"
|
||||
|
||||
done < "$INPUT_FILE"
|
||||
|
||||
# 清空UOS的触摸屏设置信息
|
||||
if [ -f /usr/bin/gsettings ];then
|
||||
gsettings set com.deepin.dde.display map-output ''> /dev/null 2>&1
|
||||
if [ "$?" == "0" ];then
|
||||
log_message "成功清除UOS系统设置中的触摸屏设置信息"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 配置文件更新完成,进行一次试用
|
||||
log_message "开始进行触摸效果测试,并进行二次确认。"
|
||||
$workpath/screen_ds_once $tmppath/screen.txt
|
||||
$workpath/touch_set
|
||||
|
||||
sleep 0.5
|
||||
|
||||
# 试用后询问用户是否保存
|
||||
$workpath/check_save
|
||||
if [ ! "$?" = "0" ];then
|
||||
# 用户点击了取消,恢复之前的设置
|
||||
log_message 用户点击了取消,恢复之前的设置.
|
||||
rm $workpath/config
|
||||
mv $workpath/config.bk $workpath/config
|
||||
|
||||
# 最后写入0重新开启监控
|
||||
echo -n "0" > $tmppath/setflag
|
||||
|
||||
notify-send -u low --icon=view-fullscreen -t 15000 "取消保存,已恢复之前的配置。"
|
||||
else
|
||||
log_message 用户点击了确认,新的config将会保存
|
||||
|
||||
# 最后写入0重新开启监控
|
||||
echo -n "0" > $tmppath/setflag
|
||||
|
||||
log_message "处理完成,结果已保存到 $OUTPUT_FILE"
|
||||
notify-send -u low --icon=view-fullscreen -t 15000 "触摸屏映射工具 ktouch" "设置完成,请稍等15秒查看效果。"
|
||||
fi
|
||||
|
||||
if [ -f $workpath/config.bk ];then
|
||||
rm $workpath/config.bk
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# 定义编译器
|
||||
CC = gcc
|
||||
|
||||
# 定义pkg-config参数
|
||||
GTK_FLAGS = $(shell pkg-config --cflags --libs gtk+-3.0)
|
||||
|
||||
# 定义目标文件
|
||||
TARGETS = screen_binder screen_ds touch_ds usb_ds touch_set screen_ds_once check_save
|
||||
|
||||
# 默认目标
|
||||
all: $(TARGETS)
|
||||
|
||||
# 编译screen_binder
|
||||
screen_binder: screen_binder.c touch_listen.c
|
||||
$(CC) -o $@ $^ $(GTK_FLAGS) -lpthread -lX11 -lXrandr
|
||||
install -m 755 $@ ../
|
||||
|
||||
# 编译screen_ds
|
||||
screen_ds: screen_ds.c
|
||||
$(CC) -o $@ $^ -lX11 -lXrandr
|
||||
install -m 755 $@ ../
|
||||
|
||||
# 编译screen_ds_once
|
||||
screen_ds_once: screen_ds_once.c
|
||||
$(CC) -o $@ $^ -lX11 -lXrandr
|
||||
install -m 755 $@ ../
|
||||
|
||||
# 编译touch_set
|
||||
touch_set: touch_set.c
|
||||
$(CC) -o $@ $^ -lX11 -lXi -ludev
|
||||
install -m 755 $@ ../
|
||||
|
||||
# 编译touch_ds
|
||||
touch_ds: touch_ds.c
|
||||
$(CC) -o $@ $^ -lX11 -lXi -ludev
|
||||
install -m 755 $@ ../
|
||||
|
||||
# 编译usb_ds
|
||||
usb_ds: usb_ds.c
|
||||
$(CC) -o $@ $^ -ludev
|
||||
install -m 755 $@ ../
|
||||
|
||||
# 编译check_save
|
||||
check_save: check_save.c
|
||||
$(CC) -o $@ $^ $(GTK_FLAGS)
|
||||
install -m 755 $@ ../
|
||||
|
||||
# 清理生成的文件
|
||||
clean:
|
||||
rm -f $(TARGETS)
|
||||
|
||||
# 声明伪目标
|
||||
.PHONY: all clean
|
||||
@@ -0,0 +1,217 @@
|
||||
#include <gtk/gtk.h>
|
||||
#include <gdk/gdk.h>
|
||||
#include <gdk/gdkx.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <libgen.h>
|
||||
|
||||
typedef struct {
|
||||
GtkWidget *window;
|
||||
int monitor_num;
|
||||
int x;
|
||||
int y;
|
||||
int width;
|
||||
int height;
|
||||
char *name;
|
||||
} MonitorInfo;
|
||||
|
||||
MonitorInfo *monitors = NULL;
|
||||
int monitor_count = 0;
|
||||
GtkApplication *app;
|
||||
int exit_code = 1; // 默认退出代码为1(取消)
|
||||
|
||||
// 显示错误对话框
|
||||
void show_error_dialog(const char *message) {
|
||||
GtkWidget *dialog = gtk_message_dialog_new(NULL,
|
||||
GTK_DIALOG_MODAL,
|
||||
GTK_MESSAGE_ERROR,
|
||||
GTK_BUTTONS_OK,
|
||||
"%s", message);
|
||||
gtk_window_set_title(GTK_WINDOW(dialog), "错误");
|
||||
gtk_dialog_run(GTK_DIALOG(dialog));
|
||||
gtk_widget_destroy(dialog);
|
||||
}
|
||||
|
||||
// 关闭所有窗口
|
||||
void close_all_windows() {
|
||||
for (int i = 0; i < monitor_count; i++) {
|
||||
if (monitors[i].window) {
|
||||
gtk_widget_destroy(monitors[i].window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存按钮点击回调函数
|
||||
void on_save_clicked(GtkWidget *widget, gpointer data) {
|
||||
printf("0\n"); // 打印0
|
||||
exit_code = 0; // 设置退出代码为0
|
||||
|
||||
close_all_windows();
|
||||
g_application_quit(G_APPLICATION(app));
|
||||
}
|
||||
|
||||
// 取消按钮点击回调函数
|
||||
void on_cancel_clicked(GtkWidget *widget, gpointer data) {
|
||||
printf("1\n"); // 打印1
|
||||
exit_code = 1; // 设置退出代码为1
|
||||
|
||||
close_all_windows();
|
||||
g_application_quit(G_APPLICATION(app));
|
||||
}
|
||||
|
||||
// 窗口关闭事件回调函数
|
||||
gboolean on_window_delete_event(GtkWidget *widget, GdkEvent *event, gpointer data) {
|
||||
printf("1\n"); // 打印1
|
||||
exit_code = 1; // 设置退出代码为1
|
||||
|
||||
close_all_windows();
|
||||
g_application_quit(G_APPLICATION(app));
|
||||
|
||||
return TRUE; // 阻止默认关闭行为,因为我们自己处理
|
||||
}
|
||||
|
||||
// 创建显示器确认窗口
|
||||
void create_monitor_window(MonitorInfo *info) {
|
||||
// 窗口尺寸
|
||||
int window_width = 400;
|
||||
int window_height = 200;
|
||||
|
||||
// 计算居中位置
|
||||
int center_x = info->x + (info->width - window_width) / 2;
|
||||
int center_y = info->y + (info->height - window_height) / 2;
|
||||
|
||||
// 创建窗口标题,包含显示器名称
|
||||
char window_title[256];
|
||||
if (info->name && strlen(info->name) > 0) {
|
||||
snprintf(window_title, sizeof(window_title), "ktouch保存确认-%s", info->name);
|
||||
} else {
|
||||
snprintf(window_title, sizeof(window_title), "ktouch保存确认-显示器%d", info->monitor_num + 1);
|
||||
}
|
||||
|
||||
// 创建窗口
|
||||
info->window = gtk_application_window_new(app);
|
||||
gtk_window_set_title(GTK_WINDOW(info->window), window_title);
|
||||
gtk_window_set_default_size(GTK_WINDOW(info->window), window_width, window_height);
|
||||
gtk_window_move(GTK_WINDOW(info->window), center_x, center_y);
|
||||
gtk_window_set_keep_above(GTK_WINDOW(info->window), TRUE);
|
||||
gtk_window_set_decorated(GTK_WINDOW(info->window), TRUE);
|
||||
gtk_window_set_position(GTK_WINDOW(info->window), GTK_WIN_POS_CENTER);
|
||||
|
||||
// 连接窗口关闭事件
|
||||
g_signal_connect(info->window, "delete-event", G_CALLBACK(on_window_delete_event), NULL);
|
||||
|
||||
// 创建容器
|
||||
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
gtk_container_set_border_width(GTK_CONTAINER(box), 20);
|
||||
gtk_container_add(GTK_CONTAINER(info->window), box);
|
||||
|
||||
// 提示文字
|
||||
GtkWidget *label = gtk_label_new("是否保存刚刚的配置的信息?\n任意窗口都可以操作。");
|
||||
gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
|
||||
gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
|
||||
gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 0);
|
||||
|
||||
// 按钮容器(水平排列)
|
||||
GtkWidget *button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
|
||||
gtk_box_set_homogeneous(GTK_BOX(button_box), TRUE);
|
||||
gtk_box_pack_start(GTK_BOX(box), button_box, FALSE, FALSE, 0);
|
||||
|
||||
// 保存按钮
|
||||
GtkWidget *save_button = gtk_button_new_with_label("保存");
|
||||
g_signal_connect(save_button, "clicked", G_CALLBACK(on_save_clicked), NULL);
|
||||
gtk_box_pack_start(GTK_BOX(button_box), save_button, TRUE, TRUE, 0);
|
||||
|
||||
// 取消按钮
|
||||
GtkWidget *cancel_button = gtk_button_new_with_label("取消并退出");
|
||||
g_signal_connect(cancel_button, "clicked", G_CALLBACK(on_cancel_clicked), NULL);
|
||||
gtk_box_pack_start(GTK_BOX(button_box), cancel_button, TRUE, TRUE, 0);
|
||||
|
||||
// 显示窗口
|
||||
gtk_widget_show_all(info->window);
|
||||
}
|
||||
|
||||
// 应用启动回调函数
|
||||
static void activate(GApplication *application, gpointer user_data) {
|
||||
GdkScreen *screen = gdk_screen_get_default();
|
||||
if (!screen) {
|
||||
fprintf(stderr, "无法获取屏幕信息\n");
|
||||
show_error_dialog("无法获取屏幕信息");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// 获取显示器数量 - 使用兼容旧版本的方法
|
||||
monitor_count = gdk_screen_get_n_monitors(screen);
|
||||
if (monitor_count <= 0) {
|
||||
fprintf(stderr, "未找到显示器\n");
|
||||
show_error_dialog("未找到显示器");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
printf("找到 %d 个显示器\n", monitor_count);
|
||||
|
||||
// 分配内存存储显示器信息
|
||||
monitors = malloc(monitor_count * sizeof(MonitorInfo));
|
||||
memset(monitors, 0, monitor_count * sizeof(MonitorInfo));
|
||||
|
||||
// 收集所有显示器信息
|
||||
for (int i = 0; i < monitor_count; i++) {
|
||||
GdkRectangle geometry;
|
||||
gdk_screen_get_monitor_geometry(screen, i, &geometry);
|
||||
|
||||
monitors[i].monitor_num = i;
|
||||
monitors[i].x = geometry.x;
|
||||
monitors[i].y = geometry.y;
|
||||
monitors[i].width = geometry.width;
|
||||
monitors[i].height = geometry.height;
|
||||
|
||||
// 尝试获取显示器名称/接口
|
||||
const char *monitor_name = gdk_screen_get_monitor_plug_name(screen, i);
|
||||
if (monitor_name && strlen(monitor_name) > 0) {
|
||||
monitors[i].name = strdup(monitor_name);
|
||||
} else {
|
||||
// 如果无法获取显示器名称,使用默认名称
|
||||
char default_name[50];
|
||||
snprintf(default_name, sizeof(default_name), "显示器%d", i + 1);
|
||||
monitors[i].name = strdup(default_name);
|
||||
}
|
||||
|
||||
printf("显示器 %d: 位置(%d, %d), 分辨率 %dx%d, 名称: %s\n",
|
||||
i+1, geometry.x, geometry.y, geometry.width, geometry.height,
|
||||
monitors[i].name);
|
||||
}
|
||||
|
||||
// 为每个显示器创建窗口
|
||||
for (int i = 0; i < monitor_count; i++) {
|
||||
create_monitor_window(&monitors[i]);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
// 检查DISPLAY环境变量,确保图形环境可用
|
||||
if (getenv("DISPLAY") == NULL) {
|
||||
fprintf(stderr, "DISPLAY环境变量未设置,尝试设置默认值\n");
|
||||
setenv("DISPLAY", ":0", 1);
|
||||
}
|
||||
|
||||
// 创建GTK应用
|
||||
app = gtk_application_new("com.example.touchscreen_confirm", G_APPLICATION_FLAGS_NONE);
|
||||
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
|
||||
|
||||
int status = g_application_run(G_APPLICATION(app), 0, NULL);
|
||||
g_object_unref(app);
|
||||
|
||||
// 释放内存
|
||||
if (monitors) {
|
||||
for (int i = 0; i < monitor_count; i++) {
|
||||
if (monitors[i].name) {
|
||||
free(monitors[i].name);
|
||||
}
|
||||
}
|
||||
free(monitors);
|
||||
}
|
||||
|
||||
// 返回相应的退出代码
|
||||
return exit_code;
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
#include <gtk/gtk.h>
|
||||
#include <gdk/gdkx.h>
|
||||
#include <gdk/gdkkeysyms.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
// 添加Xrandr头文件和X11显示类型头文件
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <X11/extensions/Xrandr.h>
|
||||
#include <gdk/x11/gdkx11display.h>
|
||||
#endif
|
||||
|
||||
int win_time_delay = 50;
|
||||
|
||||
// 全局变量
|
||||
GtkWidget **info_windows = NULL;
|
||||
int total_screens = 0;
|
||||
int *screen_widths = NULL;
|
||||
int *screen_heights = NULL;
|
||||
int *screen_x_offsets = NULL;
|
||||
int *screen_y_offsets = NULL;
|
||||
char **screen_names = NULL;
|
||||
FILE *log_file = NULL;
|
||||
int current_test_round = 0;
|
||||
int total_test_rounds = 0;
|
||||
int space_pressed = 0;
|
||||
guint create_timeout_id = 0;
|
||||
int current_window_index = 0;
|
||||
|
||||
// 函数声明
|
||||
void get_screen_info();
|
||||
void log_message(const char *format, ...);
|
||||
GtkWidget* create_info_window(int screen_index);
|
||||
gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data);
|
||||
gboolean create_next_window(gpointer user_data);
|
||||
void close_all_windows();
|
||||
void verify_windows();
|
||||
void start_next_test_round();
|
||||
void on_info_window_destroy(GtkWidget *widget, gpointer user_data);
|
||||
|
||||
// 日志函数
|
||||
void log_message(const char *format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
|
||||
time_t now = time(NULL);
|
||||
struct tm *tm_info = localtime(&now);
|
||||
char time_str[20];
|
||||
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info);
|
||||
|
||||
printf("[%s] screen_demo \t", time_str);
|
||||
vprintf(format, args);
|
||||
|
||||
if (log_file) {
|
||||
fprintf(log_file, "[%s] screen_demo \t", time_str);
|
||||
vfprintf(log_file, format, args);
|
||||
fflush(log_file);
|
||||
}
|
||||
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
// 获取屏幕信息 (使用Xrandr方法)
|
||||
void get_screen_info() {
|
||||
GdkDisplay *gdk_display = gdk_display_get_default();
|
||||
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
if (!GDK_IS_X11_DISPLAY(gdk_display)) {
|
||||
log_message("错误: 非X11显示环境\n");
|
||||
return;
|
||||
}
|
||||
|
||||
Display *xdisplay = GDK_DISPLAY_XDISPLAY(gdk_display);
|
||||
Window xroot = GDK_WINDOW_XID(gdk_get_default_root_window());
|
||||
|
||||
int rr_event_base, rr_error_base;
|
||||
if (!XRRQueryExtension(xdisplay, &rr_event_base, &rr_error_base)) {
|
||||
log_message("错误: XRandR扩展不可用\n");
|
||||
return;
|
||||
}
|
||||
|
||||
XRRScreenResources *screen_res = XRRGetScreenResourcesCurrent(xdisplay, xroot);
|
||||
if (!screen_res) {
|
||||
log_message("错误: 无法获取屏幕资源\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算连接的显示器数量
|
||||
total_screens = 0;
|
||||
for (int i = 0; i < screen_res->noutput; i++) {
|
||||
XRROutputInfo *output_info = XRRGetOutputInfo(xdisplay, screen_res, screen_res->outputs[i]);
|
||||
if (output_info && output_info->connection == RR_Connected) {
|
||||
total_screens++;
|
||||
}
|
||||
if (output_info) XRRFreeOutputInfo(output_info);
|
||||
}
|
||||
|
||||
log_message("找到 %d 个连接的显示器\n", total_screens);
|
||||
|
||||
// 分配内存存储屏幕信息
|
||||
screen_widths = malloc(total_screens * sizeof(int));
|
||||
screen_heights = malloc(total_screens * sizeof(int));
|
||||
screen_x_offsets = malloc(total_screens * sizeof(int));
|
||||
screen_y_offsets = malloc(total_screens * sizeof(int));
|
||||
screen_names = malloc(total_screens * sizeof(char*));
|
||||
info_windows = malloc(total_screens * sizeof(GtkWidget*));
|
||||
|
||||
// 获取每个显示器的详细信息
|
||||
int screen_index = 0;
|
||||
for (int i = 0; i < screen_res->noutput; i++) {
|
||||
XRROutputInfo *output_info = XRRGetOutputInfo(xdisplay, screen_res, screen_res->outputs[i]);
|
||||
if (output_info && output_info->connection == RR_Connected) {
|
||||
if (output_info->crtc) {
|
||||
XRRCrtcInfo *crtc_info = XRRGetCrtcInfo(xdisplay, screen_res, output_info->crtc);
|
||||
if (crtc_info) {
|
||||
screen_names[screen_index] = strdup(output_info->name);
|
||||
screen_widths[screen_index] = crtc_info->width;
|
||||
screen_heights[screen_index] = crtc_info->height;
|
||||
screen_x_offsets[screen_index] = crtc_info->x;
|
||||
screen_y_offsets[screen_index] = crtc_info->y;
|
||||
|
||||
info_windows[screen_index] = NULL;
|
||||
|
||||
log_message("显示器 %d: %s, 分辨率: %dx%d, 位置: %dx%d\n",
|
||||
screen_index, screen_names[screen_index],
|
||||
screen_widths[screen_index], screen_heights[screen_index],
|
||||
screen_x_offsets[screen_index], screen_y_offsets[screen_index]);
|
||||
|
||||
XRRFreeCrtcInfo(crtc_info);
|
||||
screen_index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (output_info) XRRFreeOutputInfo(output_info);
|
||||
}
|
||||
|
||||
XRRFreeScreenResources(screen_res);
|
||||
#else
|
||||
log_message("错误: 非X11环境,无法使用Xrandr\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
// 创建信息窗口
|
||||
GtkWidget* create_info_window(int screen_index) {
|
||||
// 计算窗口位置和大小:偏移(10,10),尺寸比屏幕小20
|
||||
int window_x = screen_x_offsets[screen_index] + 10;
|
||||
int window_y = screen_y_offsets[screen_index] + 10;
|
||||
int window_width = screen_widths[screen_index] - 20;
|
||||
int window_height = screen_heights[screen_index] - 20;
|
||||
|
||||
// 确保窗口尺寸不会为负值
|
||||
if (window_width < 100) window_width = 100;
|
||||
if (window_height < 100) window_height = 100;
|
||||
|
||||
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
||||
gtk_window_set_title(GTK_WINDOW(window), "屏幕信息演示");
|
||||
gtk_window_move(GTK_WINDOW(window), window_x, window_y);
|
||||
gtk_window_set_default_size(GTK_WINDOW(window), window_width, window_height);
|
||||
gtk_window_set_decorated(GTK_WINDOW(window), TRUE);
|
||||
gtk_window_set_keep_above(GTK_WINDOW(window), TRUE);
|
||||
|
||||
char message[512];
|
||||
snprintf(message, sizeof(message),
|
||||
"<span font='24' weight='bold'>屏幕 %d: %s</span>\n\n"
|
||||
"<span font='18'>分辨率: %dx%d</span>\n"
|
||||
"<span font='18'>位置: (%d, %d)</span>\n\n"
|
||||
"<span font='18'>窗口位置: (%d, %d)</span>\n"
|
||||
"<span font='18'>窗口大小: %dx%d</span>\n\n"
|
||||
"<span font='18'>测试轮次: %d/%d</span>\n\n"
|
||||
"<span font='18' foreground='blue'>按空格键继续...</span>",
|
||||
screen_index, screen_names[screen_index],
|
||||
screen_widths[screen_index], screen_heights[screen_index],
|
||||
screen_x_offsets[screen_index], screen_y_offsets[screen_index],
|
||||
window_x, window_y, window_width, window_height,
|
||||
current_test_round + 1, total_test_rounds);
|
||||
|
||||
GtkWidget *label = gtk_label_new(NULL);
|
||||
gtk_label_set_markup(GTK_LABEL(label), message);
|
||||
gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
|
||||
gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
|
||||
|
||||
// 添加容器使标签居中
|
||||
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 0);
|
||||
gtk_container_add(GTK_CONTAINER(window), box);
|
||||
|
||||
g_signal_connect(window, "key-press-event", G_CALLBACK(on_key_press), NULL);
|
||||
g_signal_connect(window, "destroy", G_CALLBACK(on_info_window_destroy), GINT_TO_POINTER(screen_index));
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
// 键盘事件处理
|
||||
gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data) {
|
||||
if (event->keyval == GDK_KEY_space) {
|
||||
log_message("用户按下空格键\n");
|
||||
space_pressed = 1;
|
||||
|
||||
// 关闭所有窗口并开始下一轮测试
|
||||
close_all_windows();
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// 信息窗口销毁事件处理
|
||||
void on_info_window_destroy(GtkWidget *widget, gpointer user_data) {
|
||||
int screen_index = GPOINTER_TO_INT(user_data);
|
||||
log_message("屏幕 %d (%s) 的信息窗口已销毁\n", screen_index, screen_names[screen_index]);
|
||||
info_windows[screen_index] = NULL;
|
||||
}
|
||||
|
||||
// 创建下一个窗口(间隔3秒)
|
||||
gboolean create_next_window(gpointer user_data) {
|
||||
if (current_window_index >= total_screens) {
|
||||
log_message("所有窗口已创建完成\n");
|
||||
verify_windows();
|
||||
create_timeout_id = 0;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
log_message("正在为屏幕 %d (%s) 创建信息窗口...\n",
|
||||
current_window_index, screen_names[current_window_index]);
|
||||
|
||||
info_windows[current_window_index] = create_info_window(current_window_index);
|
||||
gtk_widget_show_all(info_windows[current_window_index]);
|
||||
|
||||
log_message("屏幕 %d 的信息窗口已显示\n", current_window_index);
|
||||
|
||||
current_window_index++;
|
||||
|
||||
if (current_window_index < total_screens) {
|
||||
// 设置3秒后创建下一个窗口
|
||||
// create_timeout_id = g_timeout_add_seconds(3, create_next_window, NULL);
|
||||
create_timeout_id = g_timeout_add(win_time_delay, create_next_window, NULL);
|
||||
} else {
|
||||
log_message("所有窗口已创建完成\n");
|
||||
verify_windows();
|
||||
}
|
||||
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
// 关闭所有窗口
|
||||
void close_all_windows() {
|
||||
log_message("正在关闭所有窗口...\n");
|
||||
|
||||
for (int i = 0; i < total_screens; i++) {
|
||||
if (info_windows[i] != NULL) {
|
||||
gtk_widget_destroy(info_windows[i]);
|
||||
info_windows[i] = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消超时计时器
|
||||
if (create_timeout_id > 0) {
|
||||
g_source_remove(create_timeout_id);
|
||||
create_timeout_id = 0;
|
||||
}
|
||||
|
||||
log_message("所有窗口已关闭\n");
|
||||
|
||||
// 开始下一轮测试
|
||||
start_next_test_round();
|
||||
}
|
||||
|
||||
// 校验窗口的显示状态、位置和大小
|
||||
void verify_windows() {
|
||||
log_message("开始校验窗口状态...\n");
|
||||
|
||||
int all_windows_ok = 1;
|
||||
|
||||
for (int i = 0; i < total_screens; i++) {
|
||||
if (info_windows[i] == NULL) {
|
||||
log_message("错误: 屏幕 %d 的窗口未创建\n", i);
|
||||
all_windows_ok = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!gtk_widget_get_visible(info_windows[i])) {
|
||||
log_message("错误: 屏幕 %d 的窗口不可见\n", i);
|
||||
all_windows_ok = 0;
|
||||
}
|
||||
|
||||
// 获取窗口实际位置和大小
|
||||
int x, y, width, height;
|
||||
gtk_window_get_position(GTK_WINDOW(info_windows[i]), &x, &y);
|
||||
gtk_window_get_size(GTK_WINDOW(info_windows[i]), &width, &height);
|
||||
|
||||
// 计算期望的位置和大小
|
||||
int expected_x = screen_x_offsets[i] + 10;
|
||||
int expected_y = screen_y_offsets[i] + 10;
|
||||
int expected_width = screen_widths[i] - 20;
|
||||
int expected_height = screen_heights[i] - 20;
|
||||
|
||||
// 确保期望尺寸不会为负值
|
||||
if (expected_width < 100) expected_width = 100;
|
||||
if (expected_height < 100) expected_height = 100;
|
||||
|
||||
// 校验位置(允许±5像素的误差)
|
||||
if (abs(x - expected_x) > 5 || abs(y - expected_y) > 5) {
|
||||
log_message("警告: 屏幕 %d 的窗口位置不匹配。预期: (%d, %d), 实际: (%d, %d)\n",
|
||||
i, expected_x, expected_y, x, y);
|
||||
}
|
||||
|
||||
// 校验大小(允许±5像素的误差)
|
||||
if (abs(width - expected_width) > 5 || abs(height - expected_height) > 5) {
|
||||
log_message("警告: 屏幕 %d 的窗口大小不匹配。预期: %dx%d, 实际: %dx%d\n",
|
||||
i, expected_width, expected_height, width, height);
|
||||
}
|
||||
|
||||
log_message("屏幕 %d 校验完成: 位置=(%d, %d), 大小=%dx%d\n", i, x, y, width, height);
|
||||
}
|
||||
|
||||
if (all_windows_ok) {
|
||||
log_message("所有窗口校验成功\n");
|
||||
} else {
|
||||
log_message("部分窗口校验失败\n");
|
||||
}
|
||||
}
|
||||
|
||||
// 开始下一轮测试
|
||||
void start_next_test_round() {
|
||||
current_test_round++;
|
||||
|
||||
if (current_test_round >= total_test_rounds) {
|
||||
log_message("所有测试轮次已完成,程序结束\n");
|
||||
|
||||
// 清理资源
|
||||
for (int i = 0; i < total_screens; i++) {
|
||||
free(screen_names[i]);
|
||||
}
|
||||
free(screen_widths);
|
||||
free(screen_heights);
|
||||
free(screen_x_offsets);
|
||||
free(screen_y_offsets);
|
||||
free(screen_names);
|
||||
free(info_windows);
|
||||
|
||||
// 关闭日志文件
|
||||
if (log_file) {
|
||||
fclose(log_file);
|
||||
log_file = NULL;
|
||||
}
|
||||
|
||||
gtk_main_quit();
|
||||
return;
|
||||
}
|
||||
|
||||
log_message("开始第 %d/%d 轮测试\n", current_test_round + 1, total_test_rounds);
|
||||
|
||||
// 重置窗口索引
|
||||
current_window_index = 0;
|
||||
space_pressed = 0;
|
||||
|
||||
// 开始创建窗口(第一个窗口立即创建,后续窗口间隔3秒)
|
||||
create_timeout_id = g_timeout_add(100, create_next_window, NULL);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
log_file = fopen("screen_demo.log", "a");
|
||||
if (!log_file) {
|
||||
printf("无法打开日志文件,将只输出到控制台\n");
|
||||
}
|
||||
|
||||
log_message("=== 多屏幕信息窗口演示程序 ===\n");
|
||||
|
||||
gtk_init(&argc, &argv);
|
||||
|
||||
// 获取屏幕信息
|
||||
get_screen_info();
|
||||
|
||||
if (total_screens == 0) {
|
||||
log_message("未找到任何屏幕,程序退出\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 设置测试轮次为屏幕数量
|
||||
total_test_rounds = total_screens;
|
||||
log_message("将执行 %d 轮测试(每轮在所有屏幕上创建窗口)\n", total_test_rounds);
|
||||
|
||||
// 开始第一轮测试
|
||||
current_test_round = -1;
|
||||
start_next_test_round();
|
||||
|
||||
gtk_main();
|
||||
|
||||
log_message("程序结束\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,677 @@
|
||||
#!/usr/bin/env python3
|
||||
import dbus
|
||||
import logging
|
||||
import time
|
||||
import os
|
||||
from typing import List, Dict, Any
|
||||
import sys
|
||||
|
||||
setting_file_dir="/opt/ktouch/display_config"
|
||||
|
||||
|
||||
class DisplayControl:
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('DisplayControl')
|
||||
self.logger.setLevel(logging.INFO)
|
||||
|
||||
# 创建控制台处理器
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
ch.setFormatter(formatter)
|
||||
self.logger.addHandler(ch)
|
||||
|
||||
self.bus = None
|
||||
self.display_obj = None
|
||||
self.display_props = None
|
||||
self.display_interface = None
|
||||
|
||||
self.connect_to_dbus()
|
||||
|
||||
def connect_to_dbus(self):
|
||||
"""连接到DBus服务"""
|
||||
try:
|
||||
self.bus = dbus.SessionBus()
|
||||
self.display_obj = self.bus.get_object('com.deepin.daemon.Display', '/com/deepin/daemon/Display')
|
||||
self.display_props = dbus.Interface(self.display_obj, 'org.freedesktop.DBus.Properties')
|
||||
self.display_interface = dbus.Interface(self.display_obj, 'com.deepin.daemon.Display')
|
||||
self.logger.info("成功连接到DBus显示服务")
|
||||
except Exception as e:
|
||||
self.logger.error(f"连接DBus显示服务失败: {str(e)}")
|
||||
raise
|
||||
|
||||
# def get_monitors(self) -> List[Dict[str, Any]]:
|
||||
# """获取所有显示器信息"""
|
||||
# self.logger.info("开始获取显示器信息")
|
||||
# try:
|
||||
# monitors_paths = self.display_props.Get('com.deepin.daemon.Display', 'Monitors')
|
||||
# self.logger.info(f"找到 {len(monitors_paths)} 个显示器")
|
||||
|
||||
# monitors = []
|
||||
# for monitor_path in monitors_paths:
|
||||
# monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path)
|
||||
# monitor_props = dbus.Interface(monitor_obj, 'org.freedesktop.DBus.Properties')
|
||||
|
||||
# # 获取显示器属性
|
||||
# name = str(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Name'))
|
||||
# enabled = str(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Enabled'))
|
||||
# modes = monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Modes')
|
||||
# current_mode = monitor_props.Get('com.deepin.daemon.Display.Monitor', 'CurrentMode')
|
||||
# x = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'X'))
|
||||
# y = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Y'))
|
||||
# width = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Width'))
|
||||
# height = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Height'))
|
||||
# rotation = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Rotation'))
|
||||
|
||||
# # 获取主显示器
|
||||
# primary = self.display_props.Get('com.deepin.daemon.Display', 'Primary')
|
||||
# is_primary = (primary == name)
|
||||
|
||||
# # 检查是否存在配置文件
|
||||
# config_exists = self.check_config_exists(name)
|
||||
|
||||
# # 解析可用模式,过滤出60fps的模式
|
||||
# available_modes = []
|
||||
# for mode in modes:
|
||||
# mode_id = int(mode[0])
|
||||
# mode_width = int(mode[1])
|
||||
# mode_height = int(mode[2])
|
||||
# refresh_rate = int(mode[3])
|
||||
|
||||
# # 过滤55-65Hz的刷新率
|
||||
# if 55 <= refresh_rate <= 65:
|
||||
# available_modes.append({
|
||||
# 'id': mode_id,
|
||||
# 'width': mode_width,
|
||||
# 'height': mode_height,
|
||||
# 'refresh_rate': refresh_rate,
|
||||
# 'label': f"{mode_width}x{mode_height} @ {refresh_rate:.2f}Hz"
|
||||
# })
|
||||
|
||||
# # 添加自定义分辨率选项
|
||||
# available_modes.append({
|
||||
# 'id': 'custom',
|
||||
# 'width': width, # 默认使用当前宽度
|
||||
# 'height': height, # 默认使用当前高度
|
||||
# 'refresh_rate': 60.0, # 默认刷新率
|
||||
# 'label': "自定义分辨率"
|
||||
# })
|
||||
|
||||
# # 如果有配置文件,设置current_mode为id=0
|
||||
# if config_exists:
|
||||
# current_mode = [0, width, height, 60] # id=0表示自定义模式
|
||||
# self.logger.info(f"显示器 {name} 使用自定义分辨率模式")
|
||||
|
||||
# monitors.append({
|
||||
# 'path': monitor_path,
|
||||
# 'name': name,
|
||||
# 'enabled': enabled,
|
||||
# 'x': x,
|
||||
# 'y': y,
|
||||
# 'width': width,
|
||||
# 'height': height,
|
||||
# 'rotation': rotation,
|
||||
# 'is_primary': is_primary,
|
||||
# 'current_mode': [int(current_mode[0]), int(current_mode[1]), int(current_mode[2]), int(current_mode[3])],
|
||||
# 'modes': available_modes,
|
||||
# 'custom_width': width, # 自定义宽度
|
||||
# 'custom_height': height, # 自定义高度
|
||||
# 'custom_refresh_rate': 60.0, # 自定义刷新率
|
||||
# 'has_custom_config': config_exists # 是否有自定义配置
|
||||
# })
|
||||
# self.logger.info(f"显示器: {name}, 启用: {enabled}, 位置: ({x}, {y}), 分辨率: {width}x{height}, 旋转: {rotation}, 主屏: {is_primary}")
|
||||
|
||||
# # 估算显示器的位置顺序(基于X坐标)
|
||||
# self.estimate_monitor_positions(monitors)
|
||||
|
||||
# self.logger.info("显示器信息获取完成")
|
||||
# return monitors
|
||||
|
||||
# except Exception as e:
|
||||
# self.logger.error(f"获取显示器信息时出错: {str(e)}")
|
||||
# raise
|
||||
|
||||
|
||||
def get_monitors(self) -> List[Dict[str, Any]]:
|
||||
"""获取所有显示器信息"""
|
||||
self.logger.info("开始获取显示器信息")
|
||||
try:
|
||||
monitors_paths = self.display_props.Get('com.deepin.daemon.Display', 'Monitors')
|
||||
self.logger.info(f"找到 {len(monitors_paths)} 个显示器")
|
||||
|
||||
monitors = []
|
||||
for monitor_path in monitors_paths:
|
||||
monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path)
|
||||
monitor_props = dbus.Interface(monitor_obj, 'org.freedesktop.DBus.Properties')
|
||||
|
||||
# 获取显示器基础属性
|
||||
name = str(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Name'))
|
||||
enabled = str(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Enabled'))
|
||||
modes = monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Modes')
|
||||
x = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'X'))
|
||||
y = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Y'))
|
||||
width = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Width'))
|
||||
height = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Height'))
|
||||
rotation = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Rotation'))
|
||||
|
||||
# 检查是否存在配置文件并加载
|
||||
config_exists = self.check_config_exists(name)
|
||||
custom_config = None
|
||||
if config_exists:
|
||||
custom_config = self.load_custom_resolution(name)
|
||||
if custom_config:
|
||||
# 从配置文件更新分辨率信息
|
||||
width = custom_config['width']
|
||||
height = custom_config['height']
|
||||
self.logger.info(f"显示器 {name} 加载配置文件中的分辨率: {width}x{height}")
|
||||
else:
|
||||
self.logger.warning(f"显示器 {name} 配置文件存在但解析失败,将使用系统分辨率")
|
||||
|
||||
# 确定当前模式(优先使用配置文件信息)
|
||||
if config_exists and custom_config:
|
||||
# 配置文件存在且有效时,使用配置的分辨率,模式ID为0,刷新率60
|
||||
current_mode = [0, width, height, 60]
|
||||
self.logger.info(f"显示器 {name} 使用配置文件中的当前模式: {current_mode}")
|
||||
else:
|
||||
# 否则使用系统当前模式
|
||||
current_mode = monitor_props.Get('com.deepin.daemon.Display.Monitor', 'CurrentMode')
|
||||
current_mode = [int(current_mode[0]), int(current_mode[1]), int(current_mode[2]), int(current_mode[3])]
|
||||
|
||||
# 获取主显示器信息
|
||||
primary = self.display_props.Get('com.deepin.daemon.Display', 'Primary')
|
||||
is_primary = (primary == name)
|
||||
|
||||
# 解析可用模式,过滤出55-65Hz的刷新率
|
||||
available_modes = []
|
||||
for mode in modes:
|
||||
mode_id = int(mode[0])
|
||||
mode_width = int(mode[1])
|
||||
mode_height = int(mode[2])
|
||||
refresh_rate = int(mode[3])
|
||||
|
||||
if 55 <= refresh_rate <= 65:
|
||||
available_modes.append({
|
||||
'id': mode_id,
|
||||
'width': mode_width,
|
||||
'height': mode_height,
|
||||
'refresh_rate': refresh_rate,
|
||||
'label': f"{mode_width}x{mode_height} @ {refresh_rate:.2f}Hz"
|
||||
})
|
||||
|
||||
# 添加自定义分辨率选项(使用当前有效分辨率作为默认值)
|
||||
available_modes.append({
|
||||
'id': 'custom',
|
||||
'width': width,
|
||||
'height': height,
|
||||
'refresh_rate': 60.0,
|
||||
'label': "自定义分辨率"
|
||||
})
|
||||
|
||||
monitors.append({
|
||||
'path': monitor_path,
|
||||
'name': name,
|
||||
'enabled': enabled,
|
||||
'x': x,
|
||||
'y': y,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'rotation': rotation,
|
||||
'is_primary': is_primary,
|
||||
'current_mode': current_mode,
|
||||
'modes': available_modes,
|
||||
'custom_width': width,
|
||||
'custom_height': height,
|
||||
'custom_refresh_rate': 60.0,
|
||||
'has_custom_config': config_exists
|
||||
})
|
||||
self.logger.info(f"显示器: {name}, 启用: {enabled}, 位置: ({x}, {y}), 分辨率: {width}x{height}, 旋转: {rotation}, 主屏: {is_primary}")
|
||||
|
||||
# 估算显示器的位置顺序(基于X坐标)
|
||||
self.estimate_monitor_positions(monitors)
|
||||
|
||||
self.logger.info("显示器信息获取完成")
|
||||
return monitors
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取显示器信息时出错: {str(e)}")
|
||||
raise
|
||||
|
||||
def check_config_exists(self, monitor_name: str) -> bool:
|
||||
"""检查显示器配置文件是否存在"""
|
||||
config_dir = os.path.expanduser(setting_file_dir)
|
||||
config_file = os.path.join(config_dir, f"{monitor_name}.conf")
|
||||
return os.path.exists(config_file)
|
||||
|
||||
def estimate_monitor_positions(self, monitors: List[Dict[str, Any]]):
|
||||
"""根据X坐标估算显示器的位置顺序"""
|
||||
# 按X坐标排序
|
||||
sorted_monitors = sorted(monitors, key=lambda m: m['x'])
|
||||
|
||||
seq = 0
|
||||
last_x = -1
|
||||
# 分配位置编号
|
||||
for i, monitor in enumerate(sorted_monitors):
|
||||
if monitor['x'] == last_x:
|
||||
monitor['position'] = seq
|
||||
last_x = int(monitor['x'])
|
||||
else:
|
||||
seq += 1
|
||||
monitor['position'] = seq
|
||||
last_x = int(monitor['x'])
|
||||
self.logger.info(f"显示器 {monitor['name']} 估算位置: {monitor['position']} (X坐标: {monitor['x']})")
|
||||
|
||||
def enable_monitor(self, monitor_path: str, enabled: bool):
|
||||
"""启用或禁用显示器"""
|
||||
try:
|
||||
monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path)
|
||||
monitor_interface = dbus.Interface(monitor_obj, 'com.deepin.daemon.Display.Monitor')
|
||||
|
||||
if enabled:
|
||||
self.logger.info(f"启用显示器: {monitor_path}")
|
||||
monitor_interface.Enable()
|
||||
else:
|
||||
self.logger.info(f"禁用显示器: {monitor_path}")
|
||||
monitor_interface.Disable()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置显示器启用状态时出错: {str(e)}")
|
||||
raise
|
||||
|
||||
def set_mode(self, monitor_path: str, mode_id: int, monitor_name: str = None):
|
||||
"""通过Mode ID设置显示器模式"""
|
||||
try:
|
||||
monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path)
|
||||
monitor_interface = dbus.Interface(monitor_obj, 'com.deepin.daemon.Display.Monitor')
|
||||
|
||||
self.logger.info(f"设置显示器 {monitor_path} 的Mode ID为 {mode_id}")
|
||||
monitor_interface.SetMode(mode_id)
|
||||
|
||||
# 如果是系统模式,删除配置文件
|
||||
if mode_id != 'custom' and monitor_name:
|
||||
self.delete_config(monitor_name)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置显示器Mode时出错: {str(e)}")
|
||||
raise
|
||||
|
||||
# 设置显示模式为填充
|
||||
try:
|
||||
monitor_props = dbus.Interface(monitor_obj, 'org.freedesktop.DBus.Properties')
|
||||
monitor_props.Set('com.deepin.daemon.Display.Monitor', 'CurrentFillMode', dbus.String('Full'))
|
||||
self.logger.info(f"已设置显示器 {monitor_path} 的CurrentFillMode为Full")
|
||||
except Exception as fill_mode_error:
|
||||
self.logger.warning(f"设置CurrentFillMode失败: {str(fill_mode_error)}")
|
||||
|
||||
def set_mode_by_size(self, monitor_path: str, width: int, height: int, monitor_name: str = None, is_custom: bool = False):
|
||||
"""通过分辨率设置显示器模式"""
|
||||
try:
|
||||
monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path)
|
||||
monitor_interface = dbus.Interface(monitor_obj, 'com.deepin.daemon.Display.Monitor')
|
||||
|
||||
self.logger.info(f"设置显示器 {monitor_path} 的分辨率为 {width}x{height}")
|
||||
monitor_interface.SetModeBySize(width, height)
|
||||
|
||||
# 如果是自定义分辨率,保存配置并创建need_update文件
|
||||
if is_custom and monitor_name:
|
||||
self.save_custom_resolution(monitor_name, width, height, 60.0)
|
||||
self.create_need_update_file()
|
||||
# 如果是系统模式,删除配置文件
|
||||
elif monitor_name:
|
||||
self.delete_config(monitor_name)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置显示器分辨率时出错: {str(e)}")
|
||||
raise
|
||||
|
||||
try:
|
||||
monitor_props = dbus.Interface(monitor_obj, 'org.freedesktop.DBus.Properties')
|
||||
monitor_props.Set('com.deepin.daemon.Display.Monitor', 'CurrentFillMode', dbus.String('Full'))
|
||||
self.logger.info(f"已设置显示器 {monitor_path} 的CurrentFillMode为Full")
|
||||
except Exception as fill_mode_error:
|
||||
self.logger.warning(f"设置CurrentFillMode失败: {str(fill_mode_error)}")
|
||||
|
||||
|
||||
def set_position(self, monitor_path: str, x: int, y: int):
|
||||
"""设置显示器位置"""
|
||||
try:
|
||||
monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path)
|
||||
monitor_interface = dbus.Interface(monitor_obj, 'com.deepin.daemon.Display.Monitor')
|
||||
|
||||
self.logger.info(f"设置显示器 {monitor_path} 的位置为 ({x}, {y})")
|
||||
monitor_interface.SetPosition(x, y)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置显示器位置时出错: {str(e)}")
|
||||
raise
|
||||
|
||||
def set_refresh_rate(self, monitor_path: str, refresh_rate: float, monitor_name: str = None, is_custom: bool = False):
|
||||
"""设置显示器刷新率"""
|
||||
try:
|
||||
monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path)
|
||||
monitor_interface = dbus.Interface(monitor_obj, 'com.deepin.daemon.Display.Monitor')
|
||||
|
||||
self.logger.info(f"设置显示器 {monitor_path} 的刷新率为 {refresh_rate}Hz")
|
||||
monitor_interface.SetRefreshRate(refresh_rate)
|
||||
|
||||
# 如果是自定义刷新率,保存配置并创建need_update文件
|
||||
if is_custom and monitor_name:
|
||||
# 需要先获取当前分辨率
|
||||
monitor_props = dbus.Interface(monitor_obj, 'org.freedesktop.DBus.Properties')
|
||||
width = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Width'))
|
||||
height = int(monitor_props.Get('com.deepin.daemon.Display.Monitor', 'Height'))
|
||||
self.save_custom_resolution(monitor_name, width, height, refresh_rate)
|
||||
self.create_need_update_file()
|
||||
# 如果是系统模式,删除配置文件
|
||||
elif monitor_name:
|
||||
self.delete_config(monitor_name)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置显示器刷新率时出错: {str(e)}")
|
||||
raise
|
||||
|
||||
def set_rotation(self, monitor_path: str, rotation: int):
|
||||
"""设置显示器旋转方向"""
|
||||
try:
|
||||
monitor_obj = self.bus.get_object('com.deepin.daemon.Display', monitor_path)
|
||||
monitor_interface = dbus.Interface(monitor_obj, 'com.deepin.daemon.Display.Monitor')
|
||||
|
||||
rotation_names = {
|
||||
1: "正常",
|
||||
2: "向左90度",
|
||||
4: "翻转",
|
||||
8: "向右90度"
|
||||
}
|
||||
self.logger.info(f"设置显示器 {monitor_path} 的旋转方向为 {rotation} ({rotation_names.get(rotation, '未知')})")
|
||||
monitor_interface.SetRotation(rotation)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置显示器旋转方向时出错: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
def switch_mode(self):
|
||||
"""切换显示模式,为设置主显示器做准备"""
|
||||
try:
|
||||
self.logger.info("调用SwitchMode方法准备设置主显示器")
|
||||
# 调用SwitchMode方法,第一个参数是byte类型的2,第二个参数是空字符串
|
||||
self.display_interface.SwitchMode(dbus.Byte(2), dbus.String(""))
|
||||
self.logger.info("SwitchMode方法调用成功")
|
||||
except Exception as e:
|
||||
self.logger.error(f"调用SwitchMode方法时出错: {str(e)}")
|
||||
raise
|
||||
|
||||
def set_primary(self, monitor_name: str):
|
||||
"""设置主显示器"""
|
||||
try:
|
||||
self.logger.info(f"设置主显示器: {monitor_name}")
|
||||
self.display_interface.SetPrimary(monitor_name)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置主显示器时出错: {str(e)}")
|
||||
raise
|
||||
|
||||
def apply_changes(self):
|
||||
"""应用更改"""
|
||||
try:
|
||||
self.logger.info("应用显示设置更改")
|
||||
self.display_interface.ApplyChanges()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"应用显示设置时出错: {str(e)}")
|
||||
raise
|
||||
|
||||
def save_settings(self):
|
||||
"""保存设置"""
|
||||
try:
|
||||
self.logger.info("保存显示设置")
|
||||
self.display_interface.Save()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"保存显示设置时出错: {str(e)}")
|
||||
raise
|
||||
|
||||
def save_custom_resolution(self, monitor_name: str, config: str):
|
||||
"""保存自定义配置到文件,配置格式为“{width}x{height}@{rate}|x,y|rotation|isPrimary”"""
|
||||
try:
|
||||
config_dir = os.path.expanduser(setting_file_dir)
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
|
||||
config_file = os.path.join(config_dir, f"{monitor_name}.conf")
|
||||
|
||||
with open(config_file, 'w') as f:
|
||||
f.write(config)
|
||||
|
||||
self.logger.info(f"已保存自定义配置到 {config_file}: {config}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"保存自定义配置时出错: {str(e)}")
|
||||
|
||||
def remove_custom_resolution(self, monitor_name):
|
||||
config_dir = os.path.expanduser(setting_file_dir)
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
|
||||
config_file = os.path.join(config_dir, f"{monitor_name}.conf")
|
||||
|
||||
if os.path.exists(config_file):
|
||||
try:
|
||||
# 删除配置文件
|
||||
os.remove(config_file)
|
||||
# logging.info(f"已删除显示器 {monitor_name} 的自定义分辨率配置文件")
|
||||
return True
|
||||
except Exception as e:
|
||||
# logging.error(f"删除显示器 {monitor_name} 的自定义分辨率配置文件时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def create_need_update_file(self):
|
||||
"""创建need_update文件,如果打开失败则再尝试一次"""
|
||||
try:
|
||||
config_dir = os.path.expanduser(setting_file_dir)
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
|
||||
need_update_file = os.path.join(config_dir, "need_update")
|
||||
|
||||
try:
|
||||
with open(need_update_file, 'w') as f:
|
||||
f.write('1')
|
||||
except:
|
||||
# 如果失败,再尝试一次
|
||||
try:
|
||||
with open(need_update_file, 'w') as f:
|
||||
f.write('1')
|
||||
except Exception as e:
|
||||
self.logger.error(f"写入need_update文件失败: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"创建need_update文件时出错: {str(e)}")
|
||||
|
||||
def delete_config(self, monitor_name: str):
|
||||
"""删除配置文件"""
|
||||
try:
|
||||
config_dir = os.path.expanduser(setting_file_dir)
|
||||
config_file = os.path.join(config_dir, f"{monitor_name}.conf")
|
||||
|
||||
if os.path.exists(config_file):
|
||||
os.remove(config_file)
|
||||
self.logger.info(f"已删除配置文件: {config_file}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"删除配置文件时出错: {str(e)}")
|
||||
|
||||
# def load_custom_resolution(self, monitor_name: str):
|
||||
# """
|
||||
# 加载自定义分辨率配置文件
|
||||
|
||||
# 配置文件新格式: {width}x{height}@{rate}|x,y|rotation|isPrimary
|
||||
# 例如: 1920x1080@60|1920,0|0|True
|
||||
# 配置文件路径: 同目录下的display_settings目录,以显示器名称.conf命名
|
||||
|
||||
# Args:
|
||||
# monitor_name: 显示器名称
|
||||
|
||||
# Returns:
|
||||
# 包含解析后的配置信息的字典,解析失败返回None
|
||||
# """
|
||||
# try:
|
||||
# # 构建配置文件路径:同目录下的display_settings目录,文件名格式为"显示器名称.conf"
|
||||
# import os
|
||||
# # 获取当前文件所在目录
|
||||
# current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# # 构建display_settings目录路径
|
||||
# settings_dir = setting_file_dir
|
||||
# # 构建完整配置文件路径
|
||||
# config_path = os.path.join(settings_dir, f"{monitor_name}.conf")
|
||||
|
||||
# # 检查配置文件目录是否存在
|
||||
# if not os.path.exists(settings_dir):
|
||||
# self.logger.warning(f"配置文件目录不存在: {settings_dir}")
|
||||
# return None
|
||||
|
||||
# # 检查配置文件是否存在
|
||||
# if not os.path.exists(config_path):
|
||||
# self.logger.warning(f"显示器 {monitor_name} 配置文件不存在: {config_path}")
|
||||
# return None
|
||||
|
||||
# with open(config_path, 'r', encoding='utf-8') as f:
|
||||
# content = f.read().strip()
|
||||
|
||||
# # 按分隔符分割配置项
|
||||
# parts = content.split('|')
|
||||
# if len(parts) != 4:
|
||||
# self.logger.error(f"显示器 {monitor_name} 配置文件格式错误,需要4个部分,实际有{len(parts)}个")
|
||||
# return None
|
||||
|
||||
# # 解析分辨率和刷新率部分 (格式: {width}x{height}@{rate})
|
||||
# resolution_part = parts[0].strip()
|
||||
# if '@' not in resolution_part or 'x' not in resolution_part:
|
||||
# self.logger.error(f"显示器 {monitor_name} 分辨率格式错误: {resolution_part}")
|
||||
# return None
|
||||
|
||||
# # 分离分辨率和刷新率
|
||||
# resolution, rate_str = resolution_part.split('@', 1)
|
||||
# width_str, height_str = resolution.split('x', 1)
|
||||
# rate_str = rate_str[0:-2]
|
||||
# print(rate_str)
|
||||
|
||||
# # 解析位置信息 (格式: x,y)
|
||||
# position_part = parts[1].strip()
|
||||
# x_str, y_str = position_part.split(',', 1)
|
||||
|
||||
# # 解析旋转角度和主显示器标识
|
||||
# rotation_str = parts[2].strip()
|
||||
# is_primary_str = parts[3].strip().lower()
|
||||
|
||||
# # 转换为相应的数据类型
|
||||
# return {
|
||||
# 'width': int(width_str),
|
||||
# 'height': int(height_str),
|
||||
# 'refresh_rate': float(rate_str),
|
||||
# 'x': int(x_str),
|
||||
# 'y': int(y_str),
|
||||
# 'rotation': int(rotation_str),
|
||||
# 'is_primary': is_primary_str in ('true', '1', 'yes')
|
||||
# }
|
||||
|
||||
# except ValueError as e:
|
||||
# self.logger.error(f"显示器 {monitor_name} 配置文件数值解析失败: {str(e)}")
|
||||
# except FileNotFoundError:
|
||||
# self.logger.warning(f"显示器 {monitor_name} 配置文件未找到: {config_path}")
|
||||
# except Exception as e:
|
||||
# self.logger.error(f"加载显示器 {monitor_name} 配置文件时出错: {str(e)}")
|
||||
|
||||
# return None
|
||||
|
||||
|
||||
def load_custom_resolution(self, monitor_name: str):
|
||||
"""
|
||||
加载自定义分辨率配置文件
|
||||
|
||||
配置文件新格式: {width}x{height}@{rate}|x,y|rotation|isPrimary|forceFlag
|
||||
例如: 1920x1080@60|1920,0|0|True|F 或 1920x1080@60|1920,0|0|True|A
|
||||
配置文件路径: 同目录下的display_settings目录,以显示器名称.conf命名
|
||||
|
||||
Args:
|
||||
monitor_name: 显示器名称
|
||||
|
||||
Returns:
|
||||
包含解析后的配置信息的字典,解析失败返回None
|
||||
"""
|
||||
try:
|
||||
# 构建配置文件路径:同目录下的display_settings目录,文件名格式为"显示器名称.conf"
|
||||
import os
|
||||
# 获取当前文件所在目录
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# 构建display_settings目录路径
|
||||
settings_dir = setting_file_dir
|
||||
# 构建完整配置文件路径
|
||||
config_path = os.path.join(settings_dir, f"{monitor_name}.conf")
|
||||
|
||||
# 检查配置文件目录是否存在
|
||||
if not os.path.exists(settings_dir):
|
||||
self.logger.warning(f"配置文件目录不存在: {settings_dir}")
|
||||
return None
|
||||
|
||||
# 检查配置文件是否存在
|
||||
if not os.path.exists(config_path):
|
||||
self.logger.warning(f"显示器 {monitor_name} 配置文件不存在: {config_path}")
|
||||
return None
|
||||
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read().strip()
|
||||
|
||||
# 按分隔符分割配置项,现在可能有4或5个部分(兼容旧格式)
|
||||
parts = content.split('|')
|
||||
if len(parts) < 4:
|
||||
self.logger.error(f"显示器 {monitor_name} 配置文件格式错误,需要至少4个部分,实际有{len(parts)}个")
|
||||
return None
|
||||
|
||||
# 解析分辨率和刷新率部分 (格式: {width}x{height}@{rate})
|
||||
resolution_part = parts[0].strip()
|
||||
if '@' not in resolution_part or 'x' not in resolution_part:
|
||||
self.logger.error(f"显示器 {monitor_name} 分辨率格式错误: {resolution_part}")
|
||||
return None
|
||||
|
||||
# 分离分辨率和刷新率
|
||||
resolution, rate_str = resolution_part.split('@', 1)
|
||||
width_str, height_str = resolution.split('x', 1)
|
||||
rate_str = rate_str[0:-2] # 移除"Hz"
|
||||
|
||||
# 解析位置信息 (格式: x,y)
|
||||
position_part = parts[1].strip()
|
||||
x_str, y_str = position_part.split(',', 1)
|
||||
|
||||
# 解析旋转角度和主显示器标识
|
||||
rotation_str = parts[2].strip()
|
||||
is_primary_str = parts[3].strip().lower()
|
||||
|
||||
# 转换为相应的数据类型
|
||||
result = {
|
||||
'width': int(width_str),
|
||||
'height': int(height_str),
|
||||
'refresh_rate': float(rate_str),
|
||||
'x': int(x_str),
|
||||
'y': int(y_str),
|
||||
'rotation': int(rotation_str),
|
||||
'is_primary': is_primary_str in ('true', '1', 'yes', 'p')
|
||||
}
|
||||
|
||||
# 如果有第5个部分(强制标志),则解析它
|
||||
if len(parts) >= 5:
|
||||
force_flag = parts[4].strip().upper()
|
||||
result['force_custom'] = (force_flag == 'F')
|
||||
else:
|
||||
result['force_custom'] = False # 默认值
|
||||
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
self.logger.error(f"显示器 {monitor_name} 配置文件数值解析失败: {str(e)}")
|
||||
except FileNotFoundError:
|
||||
self.logger.warning(f"显示器 {monitor_name} 配置文件未找到: {config_path}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"加载显示器 {monitor_name} 配置文件时出错: {str(e)}")
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,535 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
显示器配置检查与设置脚本
|
||||
环境变量: DISPLAY=:0
|
||||
参数: 配置文件路径
|
||||
配置文件格式: 分辨率@刷新率|坐标|旋转方向|是否是主屏
|
||||
示例: 1920x1080@60.0Hz|0,0|1|P
|
||||
功能: 检查当前分辨率是否与配置文件一致,否则进行设置
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import subprocess
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
class DisplayChecker:
|
||||
def __init__(self, display_env=":0"):
|
||||
"""初始化显示器检查器"""
|
||||
self.display_env = display_env
|
||||
self.display_name = None
|
||||
self.config_data = None
|
||||
self.current_settings = None
|
||||
|
||||
def parse_config_file(self, config_path):
|
||||
"""
|
||||
解析配置文件
|
||||
格式: 分辨率@刷新率|坐标|旋转方向|是否是主屏
|
||||
示例: 1920x1080@60.0Hz|0,0|1|P
|
||||
"""
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
content = f.read().strip()
|
||||
|
||||
# 从文件名获取显示器名称(去掉后缀)
|
||||
self.display_name = Path(config_path).stem
|
||||
|
||||
# 解析配置内容
|
||||
parts = content.split('|')
|
||||
if len(parts) != 4:
|
||||
raise ValueError("配置文件格式错误,应该有4个部分")
|
||||
|
||||
# 解析分辨率和刷新率
|
||||
res_refresh_match = re.match(r'(\d+)x(\d+)@([\d.]+)Hz', parts[0])
|
||||
if not res_refresh_match:
|
||||
raise ValueError("分辨率刷新率格式错误")
|
||||
|
||||
width, height, refresh_rate = res_refresh_match.groups()
|
||||
|
||||
# 解析坐标
|
||||
pos_match = re.match(r'(-?\d+),(-?\d+)', parts[1])
|
||||
if not pos_match:
|
||||
raise ValueError("坐标格式错误")
|
||||
|
||||
pos_x, pos_y = pos_match.groups()
|
||||
|
||||
# 解析旋转方向
|
||||
rotation_map = {'1': 'normal', '2': 'left', '4': 'inverted', '8': 'right'}
|
||||
rotation = rotation_map.get(parts[2])
|
||||
if not rotation:
|
||||
raise ValueError(f"旋转方向错误: {parts[2]}")
|
||||
|
||||
# 解析主屏设置
|
||||
primary = parts[3] == 'P'
|
||||
|
||||
self.config_data = {
|
||||
'width': int(width),
|
||||
'height': int(height),
|
||||
'refresh_rate': float(refresh_rate),
|
||||
'pos_x': int(pos_x),
|
||||
'pos_y': int(pos_y),
|
||||
'rotation': rotation,
|
||||
'primary': primary
|
||||
}
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"解析配置文件错误: {e}")
|
||||
return False
|
||||
|
||||
def run_xrandr_command(self, cmd, ignore_errors=False):
|
||||
"""运行xrandr命令"""
|
||||
env = os.environ.copy()
|
||||
env['DISPLAY'] = self.display_env
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, shell=True, env=env, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
if ignore_errors:
|
||||
# 检查是否是"已经存在"之类的错误
|
||||
error_lower = result.stderr.lower()
|
||||
if any(keyword in error_lower for keyword in ['already exists', 'already set', 'exist']):
|
||||
print(f"忽略预期中的错误: {result.stderr.strip()}")
|
||||
return True
|
||||
else:
|
||||
print(f"命令执行失败但忽略错误: {cmd}")
|
||||
print(f"错误信息: {result.stderr}")
|
||||
return True
|
||||
else:
|
||||
print(f"命令执行失败: {cmd}")
|
||||
print(f"错误信息: {result.stderr}")
|
||||
return False
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"执行命令错误: {e}")
|
||||
return False
|
||||
|
||||
def get_current_display_settings(self):
|
||||
"""获取当前显示器的设置"""
|
||||
env = os.environ.copy()
|
||||
env['DISPLAY'] = self.display_env
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['xrandr'],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print("获取显示器信息失败")
|
||||
return None
|
||||
|
||||
# 解析xrandr输出,找到指定显示器的当前设置
|
||||
lines = result.stdout.split('\n')
|
||||
current_settings = {}
|
||||
|
||||
for line in lines:
|
||||
# 查找目标显示器的连接状态行
|
||||
if line.startswith(f'{self.display_name} '):
|
||||
# 示例: "HDMI-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 527mm x 296mm"
|
||||
# 或者: "HDMI-1 connected 1920x1080+0+0"
|
||||
|
||||
# 检查是否是主屏
|
||||
current_settings['primary'] = 'primary' in line
|
||||
|
||||
# 提取当前分辨率
|
||||
res_match = re.search(r'(\d+x\d+)\+(-?\d+)\+(-?\d+)', line)
|
||||
if res_match:
|
||||
current_settings['resolution'] = res_match.group(1)
|
||||
current_settings['pos_x'] = int(res_match.group(2))
|
||||
current_settings['pos_y'] = int(res_match.group(3))
|
||||
|
||||
# 提取旋转信息
|
||||
if 'inverted' in line and 'x axis y axis' not in line:
|
||||
current_settings['rotation'] = 'inverted'
|
||||
elif 'left' in line and 'x axis y axis' not in line:
|
||||
current_settings['rotation'] = 'left'
|
||||
elif 'right' in line and 'x axis y axis' not in line:
|
||||
current_settings['rotation'] = 'right'
|
||||
else:
|
||||
current_settings['rotation'] = 'normal'
|
||||
|
||||
# 继续查找当前模式行以获取刷新率
|
||||
for next_line in lines[lines.index(line)+1:]:
|
||||
if next_line.strip() and not next_line.startswith(' '):
|
||||
break
|
||||
|
||||
# 查找当前模式(带*的)
|
||||
if '*'+'+' in next_line or '* ' in next_line:
|
||||
rate_match = re.search(r'(\d+\.\d+)\*', next_line)
|
||||
if rate_match:
|
||||
current_settings['refresh_rate'] = float(rate_match.group(1))
|
||||
break
|
||||
|
||||
self.current_settings = current_settings
|
||||
return current_settings
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取当前显示器设置错误: {e}")
|
||||
return None
|
||||
|
||||
def get_display_modes(self):
|
||||
"""
|
||||
获取显示器支持的模式列表
|
||||
返回: 字典,键为分辨率,值为该分辨率下支持的刷新率列表
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
env['DISPLAY'] = self.display_env
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['xrandr'],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print("获取显示器信息失败")
|
||||
return {}
|
||||
|
||||
# 解析xrandr输出,找到指定显示器的模式
|
||||
lines = result.stdout.split('\n')
|
||||
modes = {}
|
||||
in_target_display = False
|
||||
current_resolution = None
|
||||
|
||||
for line in lines:
|
||||
# 检查是否进入目标显示器的部分
|
||||
if line.startswith(f'{self.display_name} '):
|
||||
in_target_display = True
|
||||
continue
|
||||
elif in_target_display and line.strip() and not line.startswith(' '):
|
||||
# 新的显示器部分开始,退出
|
||||
break
|
||||
|
||||
if in_target_display:
|
||||
# 解析分辨率行,例如: "1920x1080" 或 "1280x720i" (隔行扫描)
|
||||
res_match = re.search(r'^\s*(\d+x\d+i?)\s+', line)
|
||||
if res_match:
|
||||
current_resolution = res_match.group(1)
|
||||
modes[current_resolution] = []
|
||||
|
||||
# 解析刷新率,例如: "60.00*+", "59.94", "50.00"
|
||||
if current_resolution:
|
||||
rate_matches = re.findall(r'(\d+\.\d+)\*?\+?', line)
|
||||
for rate in rate_matches:
|
||||
modes[current_resolution].append(float(rate))
|
||||
|
||||
return modes
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取显示器模式错误: {e}")
|
||||
return {}
|
||||
|
||||
def get_existing_modelines(self):
|
||||
"""获取已经存在的自定义模式"""
|
||||
env = os.environ.copy()
|
||||
env['DISPLAY'] = self.display_env
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['xrandr'],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
|
||||
# 查找自定义模式(通常在输出顶部)
|
||||
lines = result.stdout.split('\n')
|
||||
modelines = []
|
||||
capturing_modelines = True
|
||||
|
||||
for line in lines:
|
||||
# 当遇到第一个显示器输出时停止捕获
|
||||
if re.match(r'^\S+ connected', line):
|
||||
capturing_modelines = False
|
||||
continue
|
||||
|
||||
if capturing_modelines and line.strip():
|
||||
# 模式行通常包含分辨率
|
||||
mode_match = re.match(r'^\s*(\S+)\s+.*?(\d+\.\d+)\s+.*?(\d+)\s+.*?(\d+)\s+.*?(\d+)\s+.*?(\d+)\s+.*?(\d+)\s+.*?(\d+)\s+.*?(\d+)', line)
|
||||
if mode_match:
|
||||
modelines.append(mode_match.group(1))
|
||||
|
||||
return modelines
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取已存在模式错误: {e}")
|
||||
return []
|
||||
|
||||
def find_matching_mode(self, modes):
|
||||
"""在支持的模式中查找匹配的模式(允许刷新率误差)"""
|
||||
target_res = f"{self.config_data['width']}x{self.config_data['height']}"
|
||||
target_refresh = self.config_data['refresh_rate']
|
||||
|
||||
# 检查精确匹配的分辨率
|
||||
if target_res in modes:
|
||||
for refresh_rate in modes[target_res]:
|
||||
# 允许±2fps的误差
|
||||
if abs(refresh_rate - target_refresh) <= 2.0:
|
||||
return f"{target_res} {refresh_rate}"
|
||||
|
||||
# 检查隔行扫描变体(如果有)
|
||||
interlaced_res = f"{target_res}i"
|
||||
if interlaced_res in modes:
|
||||
for refresh_rate in modes[interlaced_res]:
|
||||
# 允许±2fps的误差
|
||||
if abs(refresh_rate - target_refresh) <= 2.0:
|
||||
return f"{interlaced_res} {refresh_rate}"
|
||||
|
||||
return None
|
||||
|
||||
def create_and_add_mode(self):
|
||||
"""使用cvt创建并添加新的显示模式"""
|
||||
width = self.config_data['width']
|
||||
height = self.config_data['height']
|
||||
refresh_rate = self.config_data['refresh_rate']
|
||||
|
||||
# 生成模式名称
|
||||
mode_name = f"{width}x{height}_{refresh_rate}"
|
||||
|
||||
# 检查模式是否已经存在
|
||||
existing_modelines = self.get_existing_modelines()
|
||||
if mode_name in existing_modelines:
|
||||
print(f"模式 {mode_name} 已经存在,跳过创建")
|
||||
else:
|
||||
# 使用cvt生成模式行
|
||||
try:
|
||||
# 运行cvt命令
|
||||
result = subprocess.run(
|
||||
['cvt', str(width), str(height), str(refresh_rate)],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print("cvt命令执行失败")
|
||||
return None
|
||||
|
||||
# 从cvt输出中提取模式行
|
||||
lines = result.stdout.split('\n')
|
||||
modeline = None
|
||||
for line in lines:
|
||||
if line.startswith('Modeline '):
|
||||
modeline = line.replace('Modeline ', '').strip()
|
||||
break
|
||||
|
||||
if not modeline:
|
||||
print("无法从cvt输出中提取模式行")
|
||||
return None
|
||||
|
||||
# 添加新模式(忽略已存在的错误)
|
||||
add_mode_cmd = f"xrandr --newmode {mode_name} {modeline.split(' ', 1)[1]}"
|
||||
if not self.run_xrandr_command(add_mode_cmd, ignore_errors=True):
|
||||
return None
|
||||
print(f"成功创建新模式: {mode_name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"创建模式错误: {e}")
|
||||
return None
|
||||
|
||||
# 将新模式添加到显示器(忽略已添加的错误)
|
||||
add_to_output_cmd = f"xrandr --addmode {self.display_name} {mode_name}"
|
||||
if not self.run_xrandr_command(add_to_output_cmd, ignore_errors=True):
|
||||
return None
|
||||
|
||||
return mode_name
|
||||
|
||||
def parse_resolution(self, resolution_str):
|
||||
"""解析分辨率字符串,返回宽高元组"""
|
||||
try:
|
||||
if 'x' in resolution_str:
|
||||
width, height = resolution_str.split('x')
|
||||
# 处理隔行扫描(如1080i)
|
||||
height = height.replace('i', '')
|
||||
return int(width), int(height)
|
||||
return 0, 0
|
||||
except:
|
||||
return 0, 0
|
||||
|
||||
def compare_settings(self):
|
||||
"""比较当前设置与配置设置"""
|
||||
if not self.current_settings or not self.config_data:
|
||||
return False, "无法获取当前设置或配置数据"
|
||||
|
||||
# 检查分辨率(允许±10像素误差)
|
||||
current_res = self.current_settings.get('resolution', '')
|
||||
current_width, current_height = self.parse_resolution(current_res)
|
||||
target_width = self.config_data['width']
|
||||
target_height = self.config_data['height']
|
||||
|
||||
width_diff = abs(current_width - target_width)
|
||||
height_diff = abs(current_height - target_height)
|
||||
|
||||
if width_diff > 10 or height_diff > 10:
|
||||
return False, f"分辨率不匹配: 当前 {current_res} ({width_diff}, {height_diff} 像素差异), 配置 {target_width}x{target_height}"
|
||||
|
||||
# 检查刷新率(允许±2fps误差)
|
||||
current_refresh = self.current_settings.get('refresh_rate', 0)
|
||||
target_refresh = self.config_data['refresh_rate']
|
||||
refresh_diff = abs(current_refresh - target_refresh)
|
||||
|
||||
if refresh_diff > 2.0:
|
||||
return False, f"刷新率不匹配: 当前 {current_refresh:.1f}Hz ({refresh_diff:.1f}Hz 差异), 配置 {target_refresh:.1f}Hz"
|
||||
|
||||
# 检查旋转(必须精确匹配)
|
||||
current_rotation = self.current_settings.get('rotation', 'normal')
|
||||
if current_rotation != self.config_data['rotation']:
|
||||
return False, f"旋转不匹配: 当前 {current_rotation}, 配置 {self.config_data['rotation']}"
|
||||
|
||||
# 检查主屏设置(必须精确匹配)
|
||||
current_primary = self.current_settings.get('primary', False)
|
||||
if current_primary != self.config_data['primary']:
|
||||
primary_status_current = "是" if current_primary else "否"
|
||||
primary_status_target = "是" if self.config_data['primary'] else "否"
|
||||
return False, f"主屏设置不匹配: 当前 {primary_status_current}, 配置 {primary_status_target}"
|
||||
|
||||
# 位置不进行校验(根据要求)
|
||||
|
||||
# 生成详细的匹配信息
|
||||
match_details = []
|
||||
if width_diff > 0 or height_diff > 0:
|
||||
match_details.append(f"分辨率: {current_res} (差异: {width_diff}x{height_diff} 像素)")
|
||||
else:
|
||||
match_details.append(f"分辨率: {current_res} (精确匹配)")
|
||||
|
||||
if refresh_diff > 0:
|
||||
match_details.append(f"刷新率: {current_refresh:.1f}Hz (差异: {refresh_diff:.1f}Hz)")
|
||||
else:
|
||||
match_details.append(f"刷新率: {current_refresh:.1f}Hz (精确匹配)")
|
||||
|
||||
match_details.append(f"旋转: {current_rotation}")
|
||||
match_details.append(f"主屏: {'是' if current_primary else '否'}")
|
||||
match_details.append("位置: 跳过校验")
|
||||
|
||||
return True, " | ".join(match_details)
|
||||
|
||||
def configure_display(self):
|
||||
"""配置显示器"""
|
||||
if not self.config_data:
|
||||
print("没有可用的配置数据")
|
||||
return False
|
||||
|
||||
# 获取当前支持的模式
|
||||
modes = self.get_display_modes()
|
||||
if not modes:
|
||||
print(f"无法获取显示器 {self.display_name} 的模式信息")
|
||||
return False
|
||||
|
||||
print(f"显示器 {self.display_name} 支持的模式:")
|
||||
for res, rates in modes.items():
|
||||
print(f" {res}: {rates}")
|
||||
|
||||
# 查找匹配的模式
|
||||
mode_name = self.find_matching_mode(modes)
|
||||
|
||||
# 如果不支持,创建新模式
|
||||
if not mode_name:
|
||||
print("显示器不支持该模式,尝试创建新模式...")
|
||||
mode_name = self.create_and_add_mode()
|
||||
if not mode_name:
|
||||
print("创建新模式失败")
|
||||
return False
|
||||
else:
|
||||
print(f"找到匹配模式: {mode_name}")
|
||||
|
||||
# 构建xrandr命令
|
||||
cmd_parts = ["xrandr", f"--output {self.display_name}"]
|
||||
|
||||
# 添加模式
|
||||
if ' ' in mode_name:
|
||||
resolution, rate = mode_name.split(' ', 1)
|
||||
cmd_parts.append(f"--mode {resolution}")
|
||||
cmd_parts.append(f"--rate {rate}")
|
||||
else:
|
||||
# 自定义模式
|
||||
cmd_parts.append(f"--mode {mode_name}")
|
||||
|
||||
# 添加位置(即使不校验也设置)
|
||||
cmd_parts.append(f"--pos {self.config_data['pos_x']}x{self.config_data['pos_y']}")
|
||||
|
||||
# 添加旋转
|
||||
cmd_parts.append(f"--rotate {self.config_data['rotation']}")
|
||||
|
||||
# 如果是主屏
|
||||
if self.config_data['primary']:
|
||||
cmd_parts.append("--primary")
|
||||
|
||||
# 启用显示器
|
||||
cmd_parts.append("--auto")
|
||||
|
||||
# 执行最终配置命令
|
||||
final_cmd = " ".join(cmd_parts)
|
||||
print(f"执行命令: {final_cmd}")
|
||||
|
||||
if self.run_xrandr_command(final_cmd):
|
||||
print("显示器配置成功!")
|
||||
return True
|
||||
else:
|
||||
print("显示器配置失败!")
|
||||
return False
|
||||
|
||||
def check_and_configure(self):
|
||||
"""检查并配置显示器"""
|
||||
# 获取当前设置
|
||||
current_settings = self.get_current_display_settings()
|
||||
if not current_settings:
|
||||
print("无法获取当前显示器设置,尝试直接配置...")
|
||||
return self.configure_display()
|
||||
|
||||
print("当前显示器设置:")
|
||||
for key, value in current_settings.items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
# 比较设置
|
||||
match, message = self.compare_settings()
|
||||
|
||||
if match:
|
||||
print(f"✓ 设置匹配: {message}")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ 设置不匹配: {message}")
|
||||
print("开始配置显示器...")
|
||||
return self.configure_display()
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
# 检查DISPLAY环境变量
|
||||
if 'DISPLAY' not in os.environ:
|
||||
os.environ['DISPLAY'] = ':0'
|
||||
print("设置DISPLAY环境变量为:0")
|
||||
|
||||
# 解析命令行参数
|
||||
parser = argparse.ArgumentParser(description='显示器配置检查与设置脚本')
|
||||
parser.add_argument('config_file', help='配置文件路径')
|
||||
args = parser.parse_args()
|
||||
|
||||
# 检查配置文件是否存在
|
||||
if not os.path.exists(args.config_file):
|
||||
print(f"配置文件不存在: {args.config_file}")
|
||||
sys.exit(1)
|
||||
|
||||
# 创建检查器并执行检查与配置
|
||||
checker = DisplayChecker()
|
||||
|
||||
if checker.parse_config_file(args.config_file):
|
||||
if checker.check_and_configure():
|
||||
print("显示器检查与配置完成")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("显示器检查与配置失败")
|
||||
sys.exit(1)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,8 @@
|
||||
#include <gdk/gdk.h>
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
gtk_init(&argc, &argv);
|
||||
GdkMonitor *monitor; // 仅测试该类型是否可识别
|
||||
return 0;
|
||||
}
|
||||