Initial commit after repository repair and requirements update
10
tkinter_app/resources/app_config.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"screen_orientation": "Landscape",
|
||||
"screen_name": "tv-terasa",
|
||||
"quickconnect_key": "8887779",
|
||||
"server_ip": "digi-signage.moto-adv.com",
|
||||
"port": "8880",
|
||||
"screen_w": "1920",
|
||||
"screen_h": "1080",
|
||||
"playlist_version": 5
|
||||
}
|
||||
BIN
tkinter_app/resources/demo1.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
tkinter_app/resources/demo2.jpeg
Normal file
|
After Width: | Height: | Size: 537 KiB |
15
tkinter_app/resources/demo_playlist.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"playlist": [
|
||||
{
|
||||
"file_name": "demo1.jpg",
|
||||
"url": "Resurse/demo1.jpg",
|
||||
"duration": 20
|
||||
},
|
||||
{
|
||||
"file_name": "demo2.jpg",
|
||||
"url": "Resurse/demo2.jpg",
|
||||
"duration": 20
|
||||
}
|
||||
],
|
||||
"version": 1
|
||||
}
|
||||
BIN
tkinter_app/resources/home_icon.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
tkinter_app/resources/left-arrow-blue.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
tkinter_app/resources/left-arrow-green.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
20
tkinter_app/resources/local_playlist.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"playlist": [
|
||||
{
|
||||
"file_name": "1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg",
|
||||
"url": "static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg",
|
||||
"duration": 20
|
||||
},
|
||||
{
|
||||
"file_name": "wp2782770-1846651530.jpg",
|
||||
"url": "static/resurse/wp2782770-1846651530.jpg",
|
||||
"duration": 15
|
||||
},
|
||||
{
|
||||
"file_name": "SampleVideo_1280x720_1mb.mp4",
|
||||
"url": "static/resurse/SampleVideo_1280x720_1mb.mp4",
|
||||
"duration": 5
|
||||
}
|
||||
],
|
||||
"version": 5
|
||||
}
|
||||
1186
tkinter_app/resources/log.txt
Normal file
BIN
tkinter_app/resources/pause.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
tkinter_app/resources/play.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
tkinter_app/resources/right-arrow-blue.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
tkinter_app/resources/right-arrow-green.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
tkinter_app/src/__pycache__/logging_config.cpython-311.pyc
Normal file
BIN
tkinter_app/src/__pycache__/python_functions.cpython-311.pyc
Normal file
BIN
tkinter_app/src/__pycache__/virtual_keyboard.cpython-311.pyc
Normal file
27
tkinter_app/src/logging_config.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
# Path to the log file
|
||||
# Update the path to point to the new resources directory
|
||||
LOG_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt')
|
||||
|
||||
# Create a logger instance
|
||||
Logger = logging.getLogger('SignageApp')
|
||||
Logger.setLevel(logging.INFO) # Set the logging level to INFO
|
||||
|
||||
# Create a file handler to write logs to the log.txt file
|
||||
file_handler = logging.FileHandler(LOG_FILE_PATH, mode='a') # Append logs to the file
|
||||
file_handler.setLevel(logging.INFO)
|
||||
|
||||
# Create a formatter for the log messages
|
||||
formatter = logging.Formatter('[%(levelname)s] [%(name)s] %(message)s')
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
# Add the file handler to the logger
|
||||
Logger.addHandler(file_handler)
|
||||
|
||||
# Optionally, add a stream handler to log messages to the console
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setLevel(logging.INFO)
|
||||
stream_handler.setFormatter(formatter)
|
||||
Logger.addHandler(stream_handler)
|
||||
20
tkinter_app/src/main.py
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Main entry point for the tkinter-based signage player application.
|
||||
This file acts as the main executable for launching the tkinter player.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import cv2 # Import OpenCV to confirm it's available
|
||||
|
||||
# Add the current directory to the path so we can import our modules
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Import the player module
|
||||
from tkinter_simple_player import SimpleMediaPlayerApp
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Using OpenCV version: {cv2.__version__}")
|
||||
# Create and run the player
|
||||
player = SimpleMediaPlayerApp()
|
||||
player.run()
|
||||
196
tkinter_app/src/python_functions.py
Normal file
@@ -0,0 +1,196 @@
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from logging_config import Logger # Import the shared logger
|
||||
import bcrypt
|
||||
import time
|
||||
|
||||
# Update paths to use the new directory structure
|
||||
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt')
|
||||
LOCAL_PLAYLIST_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'local_playlist.json')
|
||||
|
||||
def load_config():
|
||||
"""Load configuration from app_config.txt."""
|
||||
Logger.info("python_functions: Starting load_config function.")
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r') as file:
|
||||
Logger.info("python_functions: Configuration file loaded successfully.")
|
||||
return json.load(file)
|
||||
except json.JSONDecodeError as e:
|
||||
Logger.error(f"python_functions: Failed to parse configuration file. Error: {e}")
|
||||
return {}
|
||||
else:
|
||||
Logger.error(f"python_functions: Configuration file {CONFIG_FILE} not found.")
|
||||
return {}
|
||||
Logger.info("python_functions: Finished load_config function.")
|
||||
|
||||
# Load configuration and initialize variables
|
||||
config_data = load_config()
|
||||
server = config_data.get("server_ip", "")
|
||||
host = config_data.get("screen_name", "")
|
||||
quick = config_data.get("quickconnect_key", "")
|
||||
port = config_data.get("port", "")
|
||||
|
||||
Logger.info(f"python_functions: Configuration loaded: server={server}, host={host}, quick={quick}, port={port}")
|
||||
|
||||
def load_local_playlist():
|
||||
"""Load the playlist and version from local storage."""
|
||||
Logger.info("python_functions: Starting load_local_playlist function.")
|
||||
if os.path.exists(LOCAL_PLAYLIST_FILE):
|
||||
try:
|
||||
with open(LOCAL_PLAYLIST_FILE, 'r') as local_file:
|
||||
local_playlist = json.load(local_file)
|
||||
Logger.info(f"python_functions: Local playlist loaded: {local_playlist}")
|
||||
if isinstance(local_playlist, dict) and 'playlist' in local_playlist and 'version' in local_playlist:
|
||||
Logger.info("python_functions: Finished load_local_playlist function successfully.")
|
||||
return local_playlist # Return the full playlist data
|
||||
else:
|
||||
Logger.error("python_functions: Invalid local playlist structure.")
|
||||
return {'playlist': [], 'version': 0}
|
||||
except json.JSONDecodeError as e:
|
||||
Logger.error(f"python_functions: Failed to parse local playlist file. Error: {e}")
|
||||
return {'playlist': [], 'version': 0}
|
||||
else:
|
||||
Logger.warning("python_functions: Local playlist file not found.")
|
||||
return {'playlist': [], 'version': 0}
|
||||
Logger.info("python_functions: Finished load_local_playlist function.")
|
||||
|
||||
def save_local_playlist(playlist):
|
||||
"""Save the updated playlist locally."""
|
||||
Logger.info("python_functions: Starting save_local_playlist function.")
|
||||
Logger.debug(f"python_functions: Playlist to save: {playlist}")
|
||||
if not playlist or 'playlist' not in playlist:
|
||||
Logger.error("python_functions: Invalid playlist data. Cannot save local playlist.")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(LOCAL_PLAYLIST_FILE, 'w') as local_file:
|
||||
json.dump(playlist, local_file, indent=4) # Ensure proper formatting
|
||||
Logger.info("python_functions: Updated local playlist with server data.")
|
||||
except IOError as e:
|
||||
Logger.error(f"python_functions: Failed to save local playlist: {e}")
|
||||
Logger.info("python_functions: Finished save_local_playlist function.")
|
||||
|
||||
def fetch_server_playlist():
|
||||
"""Fetch the updated playlist from the server."""
|
||||
try:
|
||||
server_ip = f'{server}:{port}' # Construct the server IP with port
|
||||
url = f'http://{server_ip}/api/playlists'
|
||||
params = {
|
||||
'hostname': host,
|
||||
'quickconnect_code': quick
|
||||
}
|
||||
Logger.info(f"Fetching playlist from URL: {url} with params: {params}")
|
||||
response = requests.get(url, params=params)
|
||||
|
||||
if response.status_code == 200:
|
||||
response_data = response.json()
|
||||
Logger.info(f"Server response: {response_data}")
|
||||
playlist = response_data.get('playlist', [])
|
||||
version = response_data.get('playlist_version', None)
|
||||
hashed_quickconnect = response_data.get('hashed_quickconnect', None)
|
||||
|
||||
if version is not None and hashed_quickconnect is not None:
|
||||
if bcrypt.checkpw(quick.encode('utf-8'), hashed_quickconnect.encode('utf-8')):
|
||||
Logger.info("Fetched updated playlist from server.")
|
||||
|
||||
# Update the playlist version in app_config.txt
|
||||
update_config_playlist_version(version)
|
||||
|
||||
return {'playlist': playlist, 'version': version}
|
||||
else:
|
||||
Logger.error("Quickconnect code validation failed.")
|
||||
else:
|
||||
Logger.error("Failed to retrieve playlist or hashed quickconnect from the response.")
|
||||
else:
|
||||
Logger.error(f"Failed to fetch playlist. Status Code: {response.status_code}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
Logger.error(f"Failed to fetch playlist: {e}")
|
||||
|
||||
return {'playlist': [], 'version': 0}
|
||||
|
||||
def download_media_files(playlist, version):
|
||||
"""Download media files from the server and update the local playlist."""
|
||||
Logger.info("python_functions: Starting media file download...")
|
||||
base_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse') # Path to the local folder
|
||||
if not os.path.exists(base_dir):
|
||||
os.makedirs(base_dir)
|
||||
Logger.info(f"python_functions: Created directory {base_dir} for media files.")
|
||||
|
||||
updated_playlist = [] # List to store updated media entries
|
||||
|
||||
for media in playlist:
|
||||
file_name = media.get('file_name', '')
|
||||
file_url = media.get('url', '')
|
||||
duration = media.get('duration', 10) # Default duration if not provided
|
||||
local_path = os.path.join(base_dir, file_name) # Local file path
|
||||
|
||||
Logger.debug(f"python_functions: Preparing to download {file_name} from {file_url}...")
|
||||
|
||||
if os.path.exists(local_path):
|
||||
Logger.info(f"python_functions: File {file_name} already exists. Skipping download.")
|
||||
else:
|
||||
try:
|
||||
response = requests.get(file_url, timeout=10)
|
||||
if response.status_code == 200:
|
||||
with open(local_path, 'wb') as file:
|
||||
file.write(response.content)
|
||||
Logger.info(f"python_functions: Successfully downloaded {file_name} to {local_path}")
|
||||
else:
|
||||
Logger.error(f"python_functions: Failed to download {file_name}. Status Code: {response.status_code}")
|
||||
continue
|
||||
except requests.exceptions.RequestException as e:
|
||||
Logger.error(f"python_functions: Error downloading {file_name}: {e}")
|
||||
continue
|
||||
|
||||
# Update the playlist entry to point to the local file path
|
||||
updated_media = {
|
||||
'file_name': file_name,
|
||||
'url': f"static/resurse/{file_name}", # Update URL to local path
|
||||
'duration': duration
|
||||
}
|
||||
Logger.debug(f"python_functions: Updated media entry: {updated_media}")
|
||||
updated_playlist.append(updated_media)
|
||||
|
||||
# Save the updated playlist locally
|
||||
save_local_playlist({'playlist': updated_playlist, 'version': version})
|
||||
Logger.info("python_functions: Finished media file download and updated local playlist.")
|
||||
|
||||
def clean_unused_files(playlist):
|
||||
"""Remove unused media files from the resource folder."""
|
||||
Logger.info("python_functions: Cleaning unused media files...")
|
||||
base_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse')
|
||||
if not os.path.exists(base_dir):
|
||||
Logger.debug(f"python_functions: Directory {base_dir} does not exist. No files to clean.")
|
||||
return
|
||||
|
||||
playlist_files = {media.get('file_name', '') for media in playlist}
|
||||
all_files = set(os.listdir(base_dir))
|
||||
unused_files = all_files - playlist_files
|
||||
|
||||
for file_name in unused_files:
|
||||
file_path = os.path.join(base_dir, file_name)
|
||||
try:
|
||||
os.remove(file_path)
|
||||
Logger.info(f"python_functions: Deleted unused file: {file_path}")
|
||||
except OSError as e:
|
||||
Logger.error(f"python_functions: Failed to delete {file_path}: {e}")
|
||||
|
||||
def update_config_playlist_version(version):
|
||||
"""Update the playlist version in app_config.txt."""
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
Logger.error(f"python_functions: Configuration file {CONFIG_FILE} not found.")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r') as file:
|
||||
config_data = json.load(file)
|
||||
|
||||
config_data['playlist_version'] = version # Add or update the playlist version
|
||||
|
||||
with open(CONFIG_FILE, 'w') as file:
|
||||
json.dump(config_data, file, indent=4)
|
||||
Logger.info(f"python_functions: Updated playlist version in app_config.txt to {version}.")
|
||||
except (IOError, json.JSONDecodeError) as e:
|
||||
Logger.error(f"python_functions: Failed to update playlist version in app_config.txt. Error: {e}")
|
||||
|
After Width: | Height: | Size: 361 KiB |
BIN
tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4
Normal file
BIN
tkinter_app/src/static/resurse/har_page_001.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_002.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_003.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_004.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_005.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_006.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_007.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_008.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_009.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_010.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/wp2782770-1846651530.jpg
Normal file
|
After Width: | Height: | Size: 794 KiB |
2107
tkinter_app/src/tkinter_simple_player.py
Normal file
360
tkinter_app/src/virtual_keyboard.py
Normal file
@@ -0,0 +1,360 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Virtual Keyboard Component for Touch Displays
|
||||
Provides an on-screen keyboard for touch-friendly input
|
||||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
class VirtualKeyboard:
|
||||
def __init__(self, parent, target_entry=None, dark_theme=True):
|
||||
self.parent = parent
|
||||
self.target_entry = target_entry
|
||||
self.dark_theme = dark_theme
|
||||
self.keyboard_window = None
|
||||
self.caps_lock = False
|
||||
self.shift_pressed = False
|
||||
|
||||
# Define color schemes
|
||||
if dark_theme:
|
||||
self.colors = {
|
||||
'bg_primary': '#1e2124',
|
||||
'bg_secondary': '#2f3136',
|
||||
'bg_tertiary': '#36393f',
|
||||
'accent': '#7289da',
|
||||
'accent_hover': '#677bc4',
|
||||
'text_primary': '#ffffff',
|
||||
'text_secondary': '#b9bbbe',
|
||||
'key_normal': '#4f545c',
|
||||
'key_hover': '#5865f2',
|
||||
'key_special': '#ed4245',
|
||||
'key_function': '#57f287'
|
||||
}
|
||||
else:
|
||||
self.colors = {
|
||||
'bg_primary': '#ffffff',
|
||||
'bg_secondary': '#f8f9fa',
|
||||
'bg_tertiary': '#e9ecef',
|
||||
'accent': '#0d6efd',
|
||||
'accent_hover': '#0b5ed7',
|
||||
'text_primary': '#000000',
|
||||
'text_secondary': '#6c757d',
|
||||
'key_normal': '#dee2e6',
|
||||
'key_hover': '#0d6efd',
|
||||
'key_special': '#dc3545',
|
||||
'key_function': '#198754'
|
||||
}
|
||||
|
||||
def show_keyboard(self, entry_widget=None):
|
||||
"""Show the virtual keyboard"""
|
||||
if entry_widget:
|
||||
self.target_entry = entry_widget
|
||||
|
||||
if self.keyboard_window and self.keyboard_window.winfo_exists():
|
||||
self.keyboard_window.lift()
|
||||
return
|
||||
|
||||
self.create_keyboard()
|
||||
|
||||
def hide_keyboard(self):
|
||||
"""Hide the virtual keyboard"""
|
||||
if self.keyboard_window and self.keyboard_window.winfo_exists():
|
||||
self.keyboard_window.destroy()
|
||||
self.keyboard_window = None
|
||||
|
||||
def create_keyboard(self):
|
||||
"""Create the virtual keyboard window"""
|
||||
self.keyboard_window = tk.Toplevel(self.parent)
|
||||
self.keyboard_window.title("Virtual Keyboard")
|
||||
self.keyboard_window.configure(bg=self.colors['bg_primary'])
|
||||
self.keyboard_window.resizable(False, False)
|
||||
|
||||
# Make keyboard stay on top
|
||||
self.keyboard_window.attributes('-topmost', True)
|
||||
|
||||
# Position keyboard at bottom of screen
|
||||
self.position_keyboard()
|
||||
|
||||
# Create keyboard layout
|
||||
self.create_keyboard_layout()
|
||||
|
||||
# Bind events
|
||||
self.keyboard_window.protocol("WM_DELETE_WINDOW", self.hide_keyboard)
|
||||
|
||||
def position_keyboard(self):
|
||||
"""Position keyboard at bottom center of screen"""
|
||||
self.keyboard_window.update_idletasks()
|
||||
|
||||
# Get screen dimensions
|
||||
screen_width = self.keyboard_window.winfo_screenwidth()
|
||||
screen_height = self.keyboard_window.winfo_screenheight()
|
||||
|
||||
# Keyboard dimensions
|
||||
kb_width = 800
|
||||
kb_height = 300
|
||||
|
||||
# Position at bottom center
|
||||
x = (screen_width - kb_width) // 2
|
||||
y = screen_height - kb_height - 50 # 50px from bottom
|
||||
|
||||
self.keyboard_window.geometry(f"{kb_width}x{kb_height}+{x}+{y}")
|
||||
|
||||
def create_keyboard_layout(self):
|
||||
"""Create the keyboard layout"""
|
||||
main_frame = tk.Frame(self.keyboard_window, bg=self.colors['bg_primary'], padx=10, pady=10)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Title bar
|
||||
title_frame = tk.Frame(main_frame, bg=self.colors['bg_secondary'], height=40)
|
||||
title_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
title_frame.pack_propagate(False)
|
||||
|
||||
title_label = tk.Label(title_frame, text="⌨️ Virtual Keyboard",
|
||||
font=('Segoe UI', 12, 'bold'),
|
||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'])
|
||||
title_label.pack(side=tk.LEFT, padx=10, pady=10)
|
||||
|
||||
# Close button
|
||||
close_btn = tk.Button(title_frame, text="✕", command=self.hide_keyboard,
|
||||
bg=self.colors['key_special'], fg=self.colors['text_primary'],
|
||||
font=('Segoe UI', 12, 'bold'), relief=tk.FLAT, width=3)
|
||||
close_btn.pack(side=tk.RIGHT, padx=10, pady=5)
|
||||
|
||||
# Keyboard rows
|
||||
self.create_keyboard_rows(main_frame)
|
||||
|
||||
def create_keyboard_rows(self, parent):
|
||||
"""Create keyboard rows"""
|
||||
# Define keyboard layout
|
||||
rows = [
|
||||
['`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 'Backspace'],
|
||||
['Tab', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\'],
|
||||
['Caps', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', "'", 'Enter'],
|
||||
['Shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', 'Shift'],
|
||||
['Ctrl', 'Alt', 'Space', 'Alt', 'Ctrl']
|
||||
]
|
||||
|
||||
# Special keys with different sizes
|
||||
special_keys = {
|
||||
'Backspace': 2,
|
||||
'Tab': 1.5,
|
||||
'Enter': 2,
|
||||
'Caps': 1.8,
|
||||
'Shift': 2.3,
|
||||
'Ctrl': 1.2,
|
||||
'Alt': 1.2,
|
||||
'Space': 6
|
||||
}
|
||||
|
||||
for row_index, row in enumerate(rows):
|
||||
row_frame = tk.Frame(parent, bg=self.colors['bg_primary'])
|
||||
row_frame.pack(fill=tk.X, pady=2)
|
||||
|
||||
for key in row:
|
||||
width = special_keys.get(key, 1)
|
||||
self.create_key_button(row_frame, key, width)
|
||||
|
||||
def create_key_button(self, parent, key, width=1):
|
||||
"""Create a keyboard key button"""
|
||||
# Determine key type and color
|
||||
if key in ['Backspace', 'Tab', 'Enter', 'Caps', 'Shift', 'Ctrl', 'Alt']:
|
||||
bg_color = self.colors['key_function']
|
||||
elif key == 'Space':
|
||||
bg_color = self.colors['key_normal']
|
||||
else:
|
||||
bg_color = self.colors['key_normal']
|
||||
|
||||
# Calculate button width
|
||||
base_width = 4
|
||||
button_width = int(base_width * width)
|
||||
|
||||
# Display text for special keys
|
||||
display_text = {
|
||||
'Backspace': '⌫',
|
||||
'Tab': '⇥',
|
||||
'Enter': '⏎',
|
||||
'Caps': '⇪',
|
||||
'Shift': '⇧',
|
||||
'Ctrl': 'Ctrl',
|
||||
'Alt': 'Alt',
|
||||
'Space': '___'
|
||||
}.get(key, key.upper() if self.caps_lock or self.shift_pressed else key)
|
||||
|
||||
button = tk.Button(parent, text=display_text,
|
||||
command=lambda k=key: self.key_pressed(k),
|
||||
bg=bg_color, fg=self.colors['text_primary'],
|
||||
font=('Segoe UI', 10, 'bold'),
|
||||
relief=tk.FLAT, bd=1,
|
||||
width=button_width, height=2)
|
||||
|
||||
# Add hover effects
|
||||
def on_enter(e, btn=button):
|
||||
btn.configure(bg=self.colors['key_hover'])
|
||||
|
||||
def on_leave(e, btn=button):
|
||||
btn.configure(bg=bg_color)
|
||||
|
||||
button.bind("<Enter>", on_enter)
|
||||
button.bind("<Leave>", on_leave)
|
||||
|
||||
button.pack(side=tk.LEFT, padx=1, pady=1)
|
||||
|
||||
def key_pressed(self, key):
|
||||
"""Handle key press"""
|
||||
if not self.target_entry:
|
||||
return
|
||||
|
||||
if key == 'Backspace':
|
||||
current_pos = self.target_entry.index(tk.INSERT)
|
||||
if current_pos > 0:
|
||||
self.target_entry.delete(current_pos - 1)
|
||||
|
||||
elif key == 'Tab':
|
||||
self.target_entry.insert(tk.INSERT, '\t')
|
||||
|
||||
elif key == 'Enter':
|
||||
# Try to trigger any bound return event
|
||||
self.target_entry.event_generate('<Return>')
|
||||
|
||||
elif key == 'Caps':
|
||||
self.caps_lock = not self.caps_lock
|
||||
self.update_key_display()
|
||||
|
||||
elif key == 'Shift':
|
||||
self.shift_pressed = not self.shift_pressed
|
||||
self.update_key_display()
|
||||
|
||||
elif key == 'Space':
|
||||
self.target_entry.insert(tk.INSERT, ' ')
|
||||
|
||||
elif key in ['Ctrl', 'Alt']:
|
||||
# These could be used for key combinations in the future
|
||||
pass
|
||||
|
||||
else:
|
||||
# Regular character
|
||||
char = key.upper() if self.caps_lock or self.shift_pressed else key
|
||||
|
||||
# Handle shifted characters
|
||||
if self.shift_pressed and not self.caps_lock:
|
||||
shift_map = {
|
||||
'1': '!', '2': '@', '3': '#', '4': '$', '5': '%',
|
||||
'6': '^', '7': '&', '8': '*', '9': '(', '0': ')',
|
||||
'-': '_', '=': '+', '[': '{', ']': '}', '\\': '|',
|
||||
';': ':', "'": '"', ',': '<', '.': '>', '/': '?',
|
||||
'`': '~'
|
||||
}
|
||||
char = shift_map.get(key, char)
|
||||
|
||||
self.target_entry.insert(tk.INSERT, char)
|
||||
|
||||
# Reset shift after character input
|
||||
if self.shift_pressed:
|
||||
self.shift_pressed = False
|
||||
self.update_key_display()
|
||||
|
||||
def update_key_display(self):
|
||||
"""Update key display based on caps lock and shift state"""
|
||||
# This would update the display of keys, but for simplicity
|
||||
# we'll just recreate the keyboard when needed
|
||||
pass
|
||||
|
||||
|
||||
class TouchOptimizedEntry(tk.Entry):
|
||||
"""Entry widget optimized for touch displays with virtual keyboard"""
|
||||
|
||||
def __init__(self, parent, virtual_keyboard=None, **kwargs):
|
||||
# Make entry larger for touch
|
||||
kwargs.setdefault('font', ('Segoe UI', 12))
|
||||
kwargs.setdefault('relief', tk.FLAT)
|
||||
kwargs.setdefault('bd', 8)
|
||||
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
self.virtual_keyboard = virtual_keyboard
|
||||
|
||||
# Bind focus events to show/hide keyboard
|
||||
self.bind('<FocusIn>', self.on_focus_in)
|
||||
self.bind('<Button-1>', self.on_click)
|
||||
|
||||
def on_focus_in(self, event):
|
||||
"""Show virtual keyboard when entry gets focus"""
|
||||
if self.virtual_keyboard:
|
||||
self.virtual_keyboard.show_keyboard(self)
|
||||
|
||||
def on_click(self, event):
|
||||
"""Show virtual keyboard when entry is clicked"""
|
||||
if self.virtual_keyboard:
|
||||
self.virtual_keyboard.show_keyboard(self)
|
||||
|
||||
|
||||
class TouchOptimizedButton(tk.Button):
|
||||
"""Button widget optimized for touch displays"""
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
# Make buttons larger for touch
|
||||
kwargs.setdefault('font', ('Segoe UI', 11, 'bold'))
|
||||
kwargs.setdefault('relief', tk.FLAT)
|
||||
kwargs.setdefault('padx', 20)
|
||||
kwargs.setdefault('pady', 12)
|
||||
kwargs.setdefault('cursor', 'hand2')
|
||||
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
# Add touch feedback
|
||||
self.bind('<Button-1>', self.on_touch_down)
|
||||
self.bind('<ButtonRelease-1>', self.on_touch_up)
|
||||
|
||||
def on_touch_down(self, event):
|
||||
"""Visual feedback when button is touched"""
|
||||
self.configure(relief=tk.SUNKEN)
|
||||
|
||||
def on_touch_up(self, event):
|
||||
"""Reset visual feedback when touch is released"""
|
||||
self.configure(relief=tk.FLAT)
|
||||
|
||||
|
||||
# Test the virtual keyboard
|
||||
if __name__ == "__main__":
|
||||
def test_virtual_keyboard():
|
||||
root = tk.Tk()
|
||||
root.title("Virtual Keyboard Test")
|
||||
root.geometry("600x400")
|
||||
root.configure(bg='#2f3136')
|
||||
|
||||
# Create virtual keyboard instance
|
||||
vk = VirtualKeyboard(root, dark_theme=True)
|
||||
|
||||
# Test frame
|
||||
test_frame = tk.Frame(root, bg='#2f3136', padx=20, pady=20)
|
||||
test_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Title
|
||||
tk.Label(test_frame, text="🎮 Touch Display Test",
|
||||
font=('Segoe UI', 16, 'bold'),
|
||||
bg='#2f3136', fg='white').pack(pady=20)
|
||||
|
||||
# Test entries
|
||||
tk.Label(test_frame, text="Click entries to show virtual keyboard:",
|
||||
bg='#2f3136', fg='white', font=('Segoe UI', 12)).pack(pady=10)
|
||||
|
||||
entry1 = TouchOptimizedEntry(test_frame, vk, width=30, bg='#36393f',
|
||||
fg='white', insertbackground='white')
|
||||
entry1.pack(pady=10)
|
||||
|
||||
entry2 = TouchOptimizedEntry(test_frame, vk, width=30, bg='#36393f',
|
||||
fg='white', insertbackground='white')
|
||||
entry2.pack(pady=10)
|
||||
|
||||
# Test buttons
|
||||
TouchOptimizedButton(test_frame, text="Show Keyboard",
|
||||
command=lambda: vk.show_keyboard(entry1),
|
||||
bg='#7289da', fg='white').pack(pady=10)
|
||||
|
||||
TouchOptimizedButton(test_frame, text="Hide Keyboard",
|
||||
command=vk.hide_keyboard,
|
||||
bg='#ed4245', fg='white').pack(pady=5)
|
||||
|
||||
root.mainloop()
|
||||
|
||||
test_virtual_keyboard()
|
||||