Initial commit for 88x31-maker
This commit is contained in:
commit
14e34bce93
893
button-maker-advanced.py
Normal file
893
button-maker-advanced.py
Normal file
@ -0,0 +1,893 @@
|
|||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '2.0')
|
||||||
|
gi.require_version('Gdk', '2.0')
|
||||||
|
from gi.repository import Gtk, Gdk, GdkPixbuf, Pango
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
PREVIEW_FILENAME = "preview_ultimate.png"
|
||||||
|
|
||||||
|
class ButtonEngine:
|
||||||
|
def __init__(self):
|
||||||
|
self.magick_binary = None
|
||||||
|
self.find_binary()
|
||||||
|
self.fetch_system_fonts()
|
||||||
|
|
||||||
|
# --- State ---
|
||||||
|
# General / Background
|
||||||
|
self.bg_type = "Solid" # Solid, Gradient, Pattern, Template
|
||||||
|
self.bg_color = "#ffffff"
|
||||||
|
self.bg_color_end = "#cccccc" # For gradient
|
||||||
|
self.bg_gradient_dir = "Vertical" # Vertical, Horizontal
|
||||||
|
self.bg_pattern = "Checkerboard"
|
||||||
|
self.bg_template_path = None
|
||||||
|
|
||||||
|
self.border_enabled = True
|
||||||
|
self.border_style = "Solid" # Solid, Raised, Sunken
|
||||||
|
self.border_color = "#000000"
|
||||||
|
|
||||||
|
# Image
|
||||||
|
self.img_path = None
|
||||||
|
self.img_x = 2
|
||||||
|
self.img_y = 2
|
||||||
|
self.img_w = 26
|
||||||
|
self.img_h = 26
|
||||||
|
|
||||||
|
# Text 1
|
||||||
|
default_font_path = self.resolve_font_path("Sans")
|
||||||
|
self.text1_content = "HELLO"
|
||||||
|
self.text1_color = "#ff0000"
|
||||||
|
self.text1_font = default_font_path
|
||||||
|
self.text1_size = 10
|
||||||
|
self.text1_x = 4
|
||||||
|
self.text1_y = 20
|
||||||
|
self.text1_outline_enabled = False
|
||||||
|
self.text1_outline_color = "#000000"
|
||||||
|
|
||||||
|
# Text 2
|
||||||
|
self.text2_content = "WORLD"
|
||||||
|
self.text2_color = "#000000"
|
||||||
|
self.text2_font = default_font_path
|
||||||
|
self.text2_size = 10
|
||||||
|
self.text2_x = 44
|
||||||
|
self.text2_y = 20
|
||||||
|
self.text2_outline_enabled = False
|
||||||
|
self.text2_outline_color = "#ffffff"
|
||||||
|
|
||||||
|
# Animation
|
||||||
|
self.text1_anim_type = "None"
|
||||||
|
self.text1_anim_speed = 5
|
||||||
|
self.text2_anim_type = "None"
|
||||||
|
self.text2_anim_speed = 5
|
||||||
|
self.gif_delay = 10 # centiseconds
|
||||||
|
self.gif_frames = 20
|
||||||
|
|
||||||
|
# Global Effects
|
||||||
|
self.text_antialias = True
|
||||||
|
self.effect_noise = False
|
||||||
|
self.effect_scanlines = False
|
||||||
|
self.effect_dither = False
|
||||||
|
|
||||||
|
def get_state(self):
|
||||||
|
state = {}
|
||||||
|
fields = [
|
||||||
|
"bg_type", "bg_color", "bg_color_end", "bg_gradient_dir", "bg_pattern", "bg_template_path",
|
||||||
|
"border_enabled", "border_style", "border_color",
|
||||||
|
"img_path", "img_x", "img_y", "img_w", "img_h",
|
||||||
|
"text1_content", "text1_color", "text1_font", "text1_size", "text1_x", "text1_y", "text1_outline_enabled", "text1_outline_color",
|
||||||
|
"text2_content", "text2_color", "text2_font", "text2_size", "text2_x", "text2_y", "text2_outline_enabled", "text2_outline_color",
|
||||||
|
"text1_anim_type", "text1_anim_speed", "text2_anim_type", "text2_anim_speed", "gif_delay", "gif_frames",
|
||||||
|
"text_antialias", "effect_noise", "effect_scanlines", "effect_dither"
|
||||||
|
]
|
||||||
|
for f in fields:
|
||||||
|
state[f] = getattr(self, f)
|
||||||
|
return state
|
||||||
|
|
||||||
|
def set_state(self, state):
|
||||||
|
for k, v in state.items():
|
||||||
|
if hasattr(self, k):
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
def hex_to_rgba(self, hex_color, alpha):
|
||||||
|
hex_color = hex_color.lstrip('#')
|
||||||
|
if len(hex_color) == 3:
|
||||||
|
hex_color = ''.join([c*2 for c in hex_color])
|
||||||
|
try:
|
||||||
|
r = int(hex_color[0:2], 16)
|
||||||
|
g = int(hex_color[2:4], 16)
|
||||||
|
b = int(hex_color[4:6], 16)
|
||||||
|
return f"rgba({r},{g},{b},{alpha})"
|
||||||
|
except:
|
||||||
|
return f"rgba(0,0,0,{alpha})"
|
||||||
|
|
||||||
|
def find_binary(self):
|
||||||
|
for binary in ["magick", "convert"]:
|
||||||
|
try:
|
||||||
|
startupinfo = None
|
||||||
|
if os.name == 'nt':
|
||||||
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
subprocess.run([binary, "-version"], check=True, stdout=subprocess.DEVNULL, stderr=None, startupinfo=startupinfo)
|
||||||
|
self.magick_binary = binary
|
||||||
|
return True
|
||||||
|
except (FileNotFoundError, Exception):
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
def fetch_system_fonts(self):
|
||||||
|
self.system_fonts = ["Arial", "Courier", "Fixed", "Times", "Verdana"] # Fallback
|
||||||
|
if not self.magick_binary: return
|
||||||
|
try:
|
||||||
|
startupinfo = None
|
||||||
|
if os.name == 'nt':
|
||||||
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
# Run -list font
|
||||||
|
res = subprocess.run([self.magick_binary, "-list", "font"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, startupinfo=startupinfo)
|
||||||
|
if res.returncode == 0:
|
||||||
|
fonts = []
|
||||||
|
for line in res.stdout.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("Font:"):
|
||||||
|
font_name = line.split(":", 1)[1].strip()
|
||||||
|
fonts.append(font_name)
|
||||||
|
if fonts:
|
||||||
|
self.system_fonts = sorted(fonts)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching fonts: {e}")
|
||||||
|
|
||||||
|
def resolve_font_path(self, font_name):
|
||||||
|
# Use fc-match to get the file path
|
||||||
|
try:
|
||||||
|
startupinfo = None
|
||||||
|
if os.name == 'nt':
|
||||||
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
res = subprocess.run(["fc-match", font_name, "-f", "%{file}"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, startupinfo=startupinfo)
|
||||||
|
if res.returncode == 0:
|
||||||
|
path = res.stdout.strip()
|
||||||
|
if path: return path
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return font_name # Fallback
|
||||||
|
|
||||||
|
def generate(self, output_file):
|
||||||
|
if not self.magick_binary: return False
|
||||||
|
|
||||||
|
animated = (self.text1_anim_type != "None" or self.text2_anim_type != "None")
|
||||||
|
frames = self.gif_frames if animated else 1
|
||||||
|
|
||||||
|
cmd_base = [self.magick_binary]
|
||||||
|
if animated:
|
||||||
|
cmd_base.extend(["-delay", str(self.gif_delay), "-loop", "0"])
|
||||||
|
|
||||||
|
for f_idx in range(frames):
|
||||||
|
if animated: cmd_base.append("(")
|
||||||
|
|
||||||
|
# 1. Background
|
||||||
|
if self.bg_type == "Template" and self.bg_template_path:
|
||||||
|
cmd_base.extend([self.bg_template_path, "-resize", "88x31!", "-extent", "88x31"])
|
||||||
|
elif self.bg_type == "Gradient":
|
||||||
|
cmd_base.extend(["-size", "88x31", "xc:none", "-sparse-color", "Barycentric",
|
||||||
|
f"0,0 {self.bg_color} " + ("%w,0 " if self.bg_gradient_dir == "Horizontal" else "0,%h ") + self.bg_color_end])
|
||||||
|
elif self.bg_type == "Pattern":
|
||||||
|
pat = self.bg_pattern.lower()
|
||||||
|
if "checker" in pat: pat = "checkerboard"
|
||||||
|
cmd_base.extend(["-size", "88x31", f"pattern:{pat}"])
|
||||||
|
else:
|
||||||
|
cmd_base.extend(["-size", "88x31", f"xc:{self.bg_color}"])
|
||||||
|
|
||||||
|
# 2. Image
|
||||||
|
if self.img_path:
|
||||||
|
cmd_base.extend(["(", self.img_path, "-resize", f"{self.img_w}x{self.img_h}!", ")",
|
||||||
|
"-gravity", "NorthWest", "-geometry", f"+{self.img_x}+{self.img_y}", "-composite"])
|
||||||
|
|
||||||
|
# 3. Text Layers
|
||||||
|
def add_text_layer_to_frame(idx, content, font, size, color, x, y, outline, outline_col, anim_type, anim_speed):
|
||||||
|
if not content: return
|
||||||
|
|
||||||
|
# Animation logic
|
||||||
|
draw_x, draw_y = x, y
|
||||||
|
opacity = 1.0
|
||||||
|
|
||||||
|
if anim_type == "Marquee":
|
||||||
|
# Smooth scrolling wrap
|
||||||
|
draw_x = (x + (f_idx * anim_speed)) % 140 - 40
|
||||||
|
elif anim_type == "Blink":
|
||||||
|
# Smooth Fade: triangular wave
|
||||||
|
# Frequency is scaled by speed
|
||||||
|
phase = (f_idx * anim_speed) % 20
|
||||||
|
opacity = 1.0 - abs(10 - phase) / 10.0
|
||||||
|
|
||||||
|
if opacity < 0.05: return
|
||||||
|
|
||||||
|
common_opts = ["-font", font, "-pointsize", str(size), "-gravity", "NorthWest"]
|
||||||
|
if not self.text_antialias: common_opts.append("+antialias")
|
||||||
|
else: common_opts.append("-antialias")
|
||||||
|
|
||||||
|
fill_rgba = self.hex_to_rgba(color, opacity)
|
||||||
|
|
||||||
|
if outline:
|
||||||
|
out_rgba = self.hex_to_rgba(outline_col, opacity)
|
||||||
|
cmd_base.extend(common_opts + ["-fill", fill_rgba, "-stroke", out_rgba, "-strokewidth", "1", "-annotate", f"+{draw_x}+{draw_y}", content, "-stroke", "none"])
|
||||||
|
|
||||||
|
cmd_base.extend(common_opts + ["-fill", fill_rgba, "-annotate", f"+{draw_x}+{draw_y}", content])
|
||||||
|
|
||||||
|
add_text_layer_to_frame(1, self.text1_content, self.text1_font, self.text1_size, self.text1_color, self.text1_x, self.text1_y, self.text1_outline_enabled, self.text1_outline_color, self.text1_anim_type, self.text1_anim_speed)
|
||||||
|
add_text_layer_to_frame(2, self.text2_content, self.text2_font, self.text2_size, self.text2_color, self.text2_x, self.text2_y, self.text2_outline_enabled, self.text2_outline_color, self.text2_anim_type, self.text2_anim_speed)
|
||||||
|
|
||||||
|
# 4. Border
|
||||||
|
if self.border_enabled:
|
||||||
|
if self.border_style == "Raised":
|
||||||
|
cmd_base.extend(["-fill", "none", "-strokewidth", "1", "-stroke", "white", "-draw", "line 0,0 87,0 line 0,0 0,30", "-stroke", "gray50", "-draw", "line 0,30 87,30 line 87,0 87,30"])
|
||||||
|
elif self.border_style == "Sunken":
|
||||||
|
cmd_base.extend(["-fill", "none", "-strokewidth", "1", "-stroke", "gray50", "-draw", "line 0,0 87,0 line 0,0 0,30", "-stroke", "white", "-draw", "line 0,30 87,30 line 87,0 87,30"])
|
||||||
|
elif self.border_style == "Dashed":
|
||||||
|
cmd_base.extend(["-fill", "none", "-stroke", self.border_color, "-strokewidth", "1", "-draw", "stroke-dasharray 3 3 rectangle 0,0 87,30"])
|
||||||
|
elif self.border_style == "Dotted":
|
||||||
|
cmd_base.extend(["-fill", "none", "-stroke", self.border_color, "-strokewidth", "1", "-draw", "stroke-dasharray 1 2 rectangle 0,0 87,30"])
|
||||||
|
else:
|
||||||
|
cmd_base.extend(["-fill", "none", "-stroke", self.border_color, "-strokewidth", "1", "-draw", "rectangle 0,0 87,30"])
|
||||||
|
|
||||||
|
# 5. Effects
|
||||||
|
if self.effect_noise: cmd_base.extend(["+noise", "Gaussian", "-attenuate", "0.5"])
|
||||||
|
if self.effect_scanlines:
|
||||||
|
lines = " ".join([f"line 0,{i} 88,{i}" for i in range(0, 31, 2)])
|
||||||
|
cmd_base.extend(["-fill", "rgba(0,0,0,0.2)", "-draw", lines])
|
||||||
|
if self.effect_dither: cmd_base.extend(["-dither", "FloydSteinberg", "-colors", "16"])
|
||||||
|
|
||||||
|
if animated: cmd_base.append(")")
|
||||||
|
|
||||||
|
cmd_base.append(output_file)
|
||||||
|
|
||||||
|
try:
|
||||||
|
startupinfo = None
|
||||||
|
if os.name == 'nt':
|
||||||
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
subprocess.run(cmd_base, check=True, stdout=subprocess.DEVNULL, stderr=None, startupinfo=startupinfo)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Engine Error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
class UltimateButtonApp:
|
||||||
|
def __init__(self):
|
||||||
|
self.engine = ButtonEngine()
|
||||||
|
self.window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL)
|
||||||
|
self.window.set_title("88x31 Ultimate Studio")
|
||||||
|
self.window.set_resizable(True)
|
||||||
|
self.window.connect("destroy", lambda w: Gtk.main_quit())
|
||||||
|
self.window.set_border_width(10)
|
||||||
|
|
||||||
|
self.zoom_level = 1 # 1, 2, 4
|
||||||
|
self.create_widgets()
|
||||||
|
self.update_preview()
|
||||||
|
self.window.show_all()
|
||||||
|
|
||||||
|
# Trigger initial visibility state
|
||||||
|
self.on_bg_type_changed(self.cmb_bg_type)
|
||||||
|
|
||||||
|
def create_combo(self, items):
|
||||||
|
store = Gtk.ListStore(str)
|
||||||
|
for item in items: store.append([item])
|
||||||
|
combo = Gtk.ComboBox(model=store)
|
||||||
|
cell = Gtk.CellRendererText()
|
||||||
|
combo.pack_start(cell, True)
|
||||||
|
combo.add_attribute(cell, 'text', 0)
|
||||||
|
combo.set_active(0)
|
||||||
|
return combo
|
||||||
|
|
||||||
|
def get_combo_text(self, combo):
|
||||||
|
itr = combo.get_active_iter()
|
||||||
|
if itr:
|
||||||
|
return combo.get_model()[itr][0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def gdk_color_to_hex(self, gdk_color):
|
||||||
|
return "#%02x%02x%02x" % (gdk_color.red >> 8, gdk_color.green >> 8, gdk_color.blue >> 8)
|
||||||
|
|
||||||
|
def create_widgets(self):
|
||||||
|
main_hbox = Gtk.HBox(spacing=10)
|
||||||
|
self.window.add(main_hbox)
|
||||||
|
|
||||||
|
notebook = Gtk.Notebook()
|
||||||
|
notebook.set_size_request(340, -1)
|
||||||
|
main_hbox.pack_start(notebook, True, True, 0)
|
||||||
|
|
||||||
|
self.build_tab_general(notebook)
|
||||||
|
self.build_tab_image(notebook)
|
||||||
|
self.build_tab_text(notebook)
|
||||||
|
self.build_tab_anim(notebook)
|
||||||
|
self.build_tab_effects(notebook)
|
||||||
|
# Preview
|
||||||
|
vbox_preview = Gtk.VBox(spacing=10)
|
||||||
|
main_hbox.pack_start(vbox_preview, False, False, 0)
|
||||||
|
|
||||||
|
# Zoom Control
|
||||||
|
hb_zoom = Gtk.HBox(spacing=5)
|
||||||
|
vbox_preview.pack_start(hb_zoom, False, False, 0)
|
||||||
|
hb_zoom.pack_start(Gtk.Label(label="Zoom:"), False, False, 0)
|
||||||
|
self.cmb_zoom = self.create_combo(["1x", "2x", "4x"])
|
||||||
|
self.cmb_zoom.set_active(0)
|
||||||
|
self.cmb_zoom.connect("changed", self.on_zoom_changed)
|
||||||
|
hb_zoom.pack_start(self.cmb_zoom, True, True, 0)
|
||||||
|
|
||||||
|
frame_preview = Gtk.Frame(label="Preview")
|
||||||
|
vbox_preview.pack_start(frame_preview, False, False, 0)
|
||||||
|
|
||||||
|
self.preview_image = Gtk.Image()
|
||||||
|
alignment = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0, yscale=0)
|
||||||
|
alignment.add(self.preview_image)
|
||||||
|
alignment.set_padding(20, 20, 20, 20)
|
||||||
|
frame_preview.add(alignment)
|
||||||
|
|
||||||
|
btn_update = Gtk.Button(label="Update Preview")
|
||||||
|
btn_update.connect("clicked", self.on_update_clicked)
|
||||||
|
vbox_preview.pack_start(btn_update, False, False, 0)
|
||||||
|
|
||||||
|
btn_save = Gtk.Button(label="Save Button...")
|
||||||
|
btn_save.connect("clicked", self.save_image)
|
||||||
|
vbox_preview.pack_start(btn_save, False, False, 0)
|
||||||
|
|
||||||
|
# Presets Section
|
||||||
|
frame_presets = Gtk.Frame(label="Presets")
|
||||||
|
vbox_preview.pack_start(frame_presets, False, False, 5)
|
||||||
|
hb_presets = Gtk.HBox(spacing=5)
|
||||||
|
hb_presets.set_border_width(5)
|
||||||
|
frame_presets.add(hb_presets)
|
||||||
|
|
||||||
|
btn_save_p = Gtk.Button(label="Save...")
|
||||||
|
btn_save_p.connect("clicked", self.on_save_preset)
|
||||||
|
hb_presets.pack_start(btn_save_p, True, True, 0)
|
||||||
|
|
||||||
|
btn_load_p = Gtk.Button(label="Load...")
|
||||||
|
btn_load_p.connect("clicked", self.on_load_preset)
|
||||||
|
hb_presets.pack_start(btn_load_p, True, True, 0)
|
||||||
|
|
||||||
|
def build_tab_general(self, notebook):
|
||||||
|
vbox = Gtk.VBox(spacing=5)
|
||||||
|
vbox.set_border_width(10)
|
||||||
|
|
||||||
|
# Background
|
||||||
|
frame_bg = Gtk.Frame(label="Background")
|
||||||
|
vbox.pack_start(frame_bg, False, False, 0)
|
||||||
|
vb_bg = Gtk.VBox(spacing=5); vb_bg.set_border_width(5)
|
||||||
|
frame_bg.add(vb_bg)
|
||||||
|
|
||||||
|
# Type Selector
|
||||||
|
hb_type = Gtk.HBox(spacing=5)
|
||||||
|
vb_bg.pack_start(hb_type, False, False, 0)
|
||||||
|
hb_type.pack_start(Gtk.Label(label="Type:"), False, False, 0)
|
||||||
|
|
||||||
|
self.cmb_bg_type = self.create_combo(["Solid", "Gradient", "Pattern", "Template"])
|
||||||
|
self.cmb_bg_type.connect("changed", self.on_bg_type_changed)
|
||||||
|
hb_type.pack_start(self.cmb_bg_type, True, True, 0)
|
||||||
|
|
||||||
|
# Controls Container (hide/show these)
|
||||||
|
self.vb_bg_controls = Gtk.VBox(spacing=5)
|
||||||
|
vb_bg.pack_start(self.vb_bg_controls, False, False, 0)
|
||||||
|
|
||||||
|
# -- Solid Controls --
|
||||||
|
self.hb_solid = Gtk.HBox(spacing=5)
|
||||||
|
self.hb_solid.pack_start(Gtk.Label(label="Color:"), False, False, 0)
|
||||||
|
self.bg_color_btn = Gtk.ColorButton()
|
||||||
|
self.bg_color_btn.set_color(Gdk.Color.parse(self.engine.bg_color)[1])
|
||||||
|
self.hb_solid.pack_start(self.bg_color_btn, False, False, 0)
|
||||||
|
|
||||||
|
# -- Gradient Controls --
|
||||||
|
self.vb_grad = Gtk.VBox(spacing=5)
|
||||||
|
hb_g1 = Gtk.HBox(spacing=5)
|
||||||
|
hb_g1.pack_start(Gtk.Label(label="Start Color:"), False, False, 0)
|
||||||
|
self.grad_start_btn = Gtk.ColorButton()
|
||||||
|
self.grad_start_btn.set_color(Gdk.Color.parse(self.engine.bg_color)[1])
|
||||||
|
hb_g1.pack_start(self.grad_start_btn, False, False, 0)
|
||||||
|
self.vb_grad.pack_start(hb_g1, False, False, 0)
|
||||||
|
|
||||||
|
hb_g2 = Gtk.HBox(spacing=5)
|
||||||
|
hb_g2.pack_start(Gtk.Label(label="End Color:"), False, False, 0)
|
||||||
|
self.grad_end_btn = Gtk.ColorButton()
|
||||||
|
self.grad_end_btn.set_color(Gdk.Color.parse(self.engine.bg_color_end)[1])
|
||||||
|
hb_g2.pack_start(self.grad_end_btn, False, False, 0)
|
||||||
|
self.vb_grad.pack_start(hb_g2, False, False, 0)
|
||||||
|
|
||||||
|
hb_g3 = Gtk.HBox(spacing=5)
|
||||||
|
hb_g3.pack_start(Gtk.Label(label="Direction:"), False, False, 0)
|
||||||
|
self.cmb_grad_dir = self.create_combo(["Vertical", "Horizontal"])
|
||||||
|
hb_g3.pack_start(self.cmb_grad_dir, False, False, 0)
|
||||||
|
self.vb_grad.pack_start(hb_g3, False, False, 0)
|
||||||
|
|
||||||
|
# -- Pattern Controls --
|
||||||
|
self.hb_pat = Gtk.HBox(spacing=5)
|
||||||
|
self.hb_pat.pack_start(Gtk.Label(label="Pattern:"), False, False, 0)
|
||||||
|
self.cmb_pat = self.create_combo(["Checkerboard", "Hexagons", "Bricks", "Circles", "CrossHatch"])
|
||||||
|
self.hb_pat.pack_start(self.cmb_pat, True, True, 0)
|
||||||
|
|
||||||
|
# -- Template Controls --
|
||||||
|
self.hb_tmpl = Gtk.HBox(spacing=5)
|
||||||
|
btn_tmpl = Gtk.Button(label="Select...")
|
||||||
|
btn_tmpl.connect("clicked", self.select_template)
|
||||||
|
self.hb_tmpl.pack_start(btn_tmpl, True, True, 0)
|
||||||
|
self.lbl_bg_tmpl = Gtk.Label(label="No template")
|
||||||
|
self.hb_tmpl.pack_start(self.lbl_bg_tmpl, False, False, 0)
|
||||||
|
|
||||||
|
# Add all to controls box
|
||||||
|
self.vb_bg_controls.pack_start(self.hb_solid, False, False, 0)
|
||||||
|
self.vb_bg_controls.pack_start(self.vb_grad, False, False, 0)
|
||||||
|
self.vb_bg_controls.pack_start(self.hb_pat, False, False, 0)
|
||||||
|
self.vb_bg_controls.pack_start(self.hb_tmpl, False, False, 0)
|
||||||
|
|
||||||
|
# Border
|
||||||
|
frame_brd = Gtk.Frame(label="Border")
|
||||||
|
vbox.pack_start(frame_brd, False, False, 5)
|
||||||
|
vb_brd = Gtk.VBox(spacing=5); vb_brd.set_border_width(5)
|
||||||
|
frame_brd.add(vb_brd)
|
||||||
|
|
||||||
|
self.chk_border = Gtk.CheckButton(label="Enable Border")
|
||||||
|
self.chk_border.set_active(self.engine.border_enabled)
|
||||||
|
vb_brd.pack_start(self.chk_border, False, False, 0)
|
||||||
|
|
||||||
|
hb_bstyle = Gtk.HBox(spacing=5)
|
||||||
|
vb_brd.pack_start(hb_bstyle, False, False, 0)
|
||||||
|
hb_bstyle.pack_start(Gtk.Label(label="Style:"), False, False, 0)
|
||||||
|
self.cmb_brd_style = self.create_combo(["Solid", "Raised", "Sunken", "Dashed", "Dotted"])
|
||||||
|
hb_bstyle.pack_start(self.cmb_brd_style, True, True, 0)
|
||||||
|
|
||||||
|
hb_bcol = Gtk.HBox(spacing=5)
|
||||||
|
vb_brd.pack_start(hb_bcol, False, False, 0)
|
||||||
|
hb_bcol.pack_start(Gtk.Label(label="Color:"), False, False, 0)
|
||||||
|
self.brd_color_btn = Gtk.ColorButton()
|
||||||
|
self.brd_color_btn.set_color(Gdk.Color.parse(self.engine.border_color)[1])
|
||||||
|
hb_bcol.pack_start(self.brd_color_btn, False, False, 0)
|
||||||
|
|
||||||
|
notebook.append_page(vbox, Gtk.Label(label="General"))
|
||||||
|
|
||||||
|
def on_bg_type_changed(self, combo):
|
||||||
|
# Determine which controls to show
|
||||||
|
txt = self.get_combo_text(combo)
|
||||||
|
if not txt: return
|
||||||
|
|
||||||
|
# Hide all first
|
||||||
|
self.hb_solid.hide()
|
||||||
|
self.vb_grad.hide()
|
||||||
|
self.hb_pat.hide()
|
||||||
|
self.hb_tmpl.hide()
|
||||||
|
|
||||||
|
if txt == "Solid":
|
||||||
|
self.hb_solid.show()
|
||||||
|
elif txt == "Gradient":
|
||||||
|
self.vb_grad.show()
|
||||||
|
elif txt == "Pattern":
|
||||||
|
self.hb_pat.show()
|
||||||
|
elif txt == "Template":
|
||||||
|
self.hb_tmpl.show()
|
||||||
|
|
||||||
|
def build_tab_image(self, notebook):
|
||||||
|
vbox = Gtk.VBox(spacing=5)
|
||||||
|
vbox.set_border_width(10)
|
||||||
|
|
||||||
|
btn_sel = Gtk.Button(label="Select Image URL/File...")
|
||||||
|
btn_sel.connect("clicked", self.select_img)
|
||||||
|
vbox.pack_start(btn_sel, False, False, 0)
|
||||||
|
self.lbl_img = Gtk.Label(label="No image")
|
||||||
|
vbox.pack_start(self.lbl_img, False, False, 0)
|
||||||
|
|
||||||
|
hb_dim = Gtk.HBox(spacing=5)
|
||||||
|
vbox.pack_start(hb_dim, False, False, 5)
|
||||||
|
hb_dim.pack_start(Gtk.Label(label="W:"), False, False, 0)
|
||||||
|
self.spin_img_w = Gtk.SpinButton()
|
||||||
|
self.spin_img_w.set_range(1, 88); self.spin_img_w.set_value(self.engine.img_w); self.spin_img_w.set_increments(1, 10)
|
||||||
|
hb_dim.pack_start(self.spin_img_w, False, False, 0)
|
||||||
|
hb_dim.pack_start(Gtk.Label(label="H:"), False, False, 0)
|
||||||
|
self.spin_img_h = Gtk.SpinButton()
|
||||||
|
self.spin_img_h.set_range(1, 31); self.spin_img_h.set_value(self.engine.img_h); self.spin_img_h.set_increments(1, 10)
|
||||||
|
hb_dim.pack_start(self.spin_img_h, False, False, 0)
|
||||||
|
|
||||||
|
hb_pos = Gtk.HBox(spacing=5)
|
||||||
|
vbox.pack_start(hb_pos, False, False, 0)
|
||||||
|
hb_pos.pack_start(Gtk.Label(label="X:"), False, False, 0)
|
||||||
|
self.spin_img_x = Gtk.SpinButton()
|
||||||
|
self.spin_img_x.set_range(-20, 88); self.spin_img_x.set_value(self.engine.img_x); self.spin_img_x.set_increments(1, 10)
|
||||||
|
hb_pos.pack_start(self.spin_img_x, False, False, 0)
|
||||||
|
hb_pos.pack_start(Gtk.Label(label="Y:"), False, False, 0)
|
||||||
|
self.spin_img_y = Gtk.SpinButton()
|
||||||
|
self.spin_img_y.set_range(-20, 31); self.spin_img_y.set_value(self.engine.img_y); self.spin_img_y.set_increments(1, 10)
|
||||||
|
hb_pos.pack_start(self.spin_img_y, False, False, 0)
|
||||||
|
|
||||||
|
notebook.append_page(vbox, Gtk.Label(label="Image"))
|
||||||
|
|
||||||
|
def build_tab_text(self, notebook):
|
||||||
|
scrolled = Gtk.ScrolledWindow()
|
||||||
|
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||||
|
vbox_main = Gtk.VBox(spacing=10)
|
||||||
|
vbox_main.set_border_width(10)
|
||||||
|
scrolled.add_with_viewport(vbox_main)
|
||||||
|
|
||||||
|
for index in [1, 2]:
|
||||||
|
frame = Gtk.Frame(label=f"Text Layer {index}")
|
||||||
|
vbox_main.pack_start(frame, False, False, 0)
|
||||||
|
vbox = Gtk.VBox(spacing=5)
|
||||||
|
vbox.set_border_width(5)
|
||||||
|
frame.add(vbox)
|
||||||
|
|
||||||
|
# Content
|
||||||
|
vbox.pack_start(Gtk.Label(label=f"Content:"), False, False, 0)
|
||||||
|
entry_txt = Gtk.Entry()
|
||||||
|
entry_txt.set_text(getattr(self.engine, f"text{index}_content"))
|
||||||
|
vbox.pack_start(entry_txt, False, False, 0)
|
||||||
|
|
||||||
|
# Color & Font
|
||||||
|
hb_style = Gtk.HBox(spacing=5)
|
||||||
|
vbox.pack_start(hb_style, False, False, 0)
|
||||||
|
|
||||||
|
btn_col = Gtk.ColorButton()
|
||||||
|
btn_col.set_color(Gdk.Color.parse(getattr(self.engine, f"text{index}_color"))[1])
|
||||||
|
hb_style.pack_start(btn_col, False, False, 0)
|
||||||
|
|
||||||
|
# Font Picker
|
||||||
|
curr_font = getattr(self.engine, f"text{index}_font")
|
||||||
|
curr_size = getattr(self.engine, f"text{index}_size")
|
||||||
|
|
||||||
|
btn_font = Gtk.FontButton()
|
||||||
|
btn_font.set_title(f"Font for Text {index}")
|
||||||
|
# Try to set initial font name - might need accurate name
|
||||||
|
btn_font.set_font_name(f"{curr_font} {curr_size}")
|
||||||
|
btn_font.connect("font-set", self.on_font_set, index)
|
||||||
|
|
||||||
|
hb_style.pack_start(btn_font, True, True, 0)
|
||||||
|
|
||||||
|
# Size (Keep SpinButton for manual tweaks, but it syncs with FontButton)
|
||||||
|
hb_size = Gtk.HBox(spacing=5)
|
||||||
|
vbox.pack_start(hb_size, False, False, 0)
|
||||||
|
hb_size.pack_start(Gtk.Label(label="Size:"), False, False, 0)
|
||||||
|
spin_size = Gtk.SpinButton()
|
||||||
|
spin_size.set_range(4, 72); spin_size.set_value(curr_size); spin_size.set_increments(1, 5)
|
||||||
|
hb_size.pack_start(spin_size, False, False, 0)
|
||||||
|
|
||||||
|
# Position
|
||||||
|
hb_pos = Gtk.HBox(spacing=5)
|
||||||
|
vbox.pack_start(hb_pos, False, False, 0)
|
||||||
|
hb_pos.pack_start(Gtk.Label(label="X:"), False, False, 0)
|
||||||
|
spin_x = Gtk.SpinButton()
|
||||||
|
spin_x.set_range(-20, 100); spin_x.set_value(getattr(self.engine, f"text{index}_x")); spin_x.set_increments(1, 10)
|
||||||
|
hb_pos.pack_start(spin_x, False, False, 0)
|
||||||
|
|
||||||
|
hb_pos.pack_start(Gtk.Label(label="Y:"), False, False, 0)
|
||||||
|
spin_y = Gtk.SpinButton()
|
||||||
|
spin_y.set_range(-20, 50); spin_y.set_value(getattr(self.engine, f"text{index}_y")); spin_y.set_increments(1, 10)
|
||||||
|
hb_pos.pack_start(spin_y, False, False, 0)
|
||||||
|
|
||||||
|
# Outline
|
||||||
|
vbox.pack_start(Gtk.HSeparator(), False, False, 5)
|
||||||
|
chk_out = Gtk.CheckButton(label="Enable Outline")
|
||||||
|
chk_out.set_active(getattr(self.engine, f"text{index}_outline_enabled"))
|
||||||
|
vbox.pack_start(chk_out, False, False, 0)
|
||||||
|
|
||||||
|
hb_ocol = Gtk.HBox(spacing=5)
|
||||||
|
vbox.pack_start(hb_ocol, False, False, 0)
|
||||||
|
hb_ocol.pack_start(Gtk.Label(label="Color:"), False, False, 0)
|
||||||
|
btn_ocol = Gtk.ColorButton()
|
||||||
|
btn_ocol.set_color(Gdk.Color.parse(getattr(self.engine, f"text{index}_outline_color"))[1])
|
||||||
|
hb_ocol.pack_start(btn_ocol, False, False, 0)
|
||||||
|
|
||||||
|
setattr(self, f"entry_t{index}", entry_txt)
|
||||||
|
setattr(self, f"btn_col_t{index}", btn_col)
|
||||||
|
setattr(self, f"btn_font_t{index}", btn_font) # Changed from cmb_font_t{index}
|
||||||
|
setattr(self, f"spin_sz_t{index}", spin_size)
|
||||||
|
setattr(self, f"spin_x_t{index}", spin_x)
|
||||||
|
setattr(self, f"spin_y_t{index}", spin_y)
|
||||||
|
setattr(self, f"spin_x_t{index}", spin_x)
|
||||||
|
setattr(self, f"spin_y_t{index}", spin_y)
|
||||||
|
setattr(self, f"chk_out_t{index}", chk_out)
|
||||||
|
setattr(self, f"btn_ocol_t{index}", btn_ocol)
|
||||||
|
|
||||||
|
notebook.append_page(scrolled, Gtk.Label(label="Text"))
|
||||||
|
|
||||||
|
def on_font_set(self, widget, index):
|
||||||
|
font_name = widget.get_font_name()
|
||||||
|
desc = Pango.FontDescription.from_string(font_name)
|
||||||
|
|
||||||
|
# Resolve path
|
||||||
|
family = desc.get_family()
|
||||||
|
resolved = self.engine.resolve_font_path(family)
|
||||||
|
setattr(self.engine, f"text{index}_font", resolved)
|
||||||
|
|
||||||
|
size = int(desc.get_size() / Pango.SCALE)
|
||||||
|
|
||||||
|
# Update spin button
|
||||||
|
spin = getattr(self, f"spin_sz_t{index}")
|
||||||
|
spin.set_value(size)
|
||||||
|
|
||||||
|
self.update_preview()
|
||||||
|
|
||||||
|
def build_tab_anim(self, notebook):
|
||||||
|
vbox = Gtk.VBox(spacing=10)
|
||||||
|
vbox.set_border_width(10)
|
||||||
|
|
||||||
|
for i in [1, 2]:
|
||||||
|
frame = Gtk.Frame(label=f"Text Layer {i} Animation")
|
||||||
|
vbox.pack_start(frame, False, False, 0)
|
||||||
|
vb = Gtk.VBox(spacing=5); vb.set_border_width(5); frame.add(vb)
|
||||||
|
|
||||||
|
hb1 = Gtk.HBox(spacing=5); vb.pack_start(hb1, False, False, 0)
|
||||||
|
hb1.pack_start(Gtk.Label(label="Type:"), False, False, 0)
|
||||||
|
cmb = self.create_combo(["None", "Marquee", "Blink"])
|
||||||
|
hb1.pack_start(cmb, True, True, 0)
|
||||||
|
setattr(self, f"cmb_anim_t{i}", cmb)
|
||||||
|
|
||||||
|
hb2 = Gtk.HBox(spacing=5); vb.pack_start(hb2, False, False, 0)
|
||||||
|
hb2.pack_start(Gtk.Label(label="Speed:"), False, False, 0)
|
||||||
|
spin = Gtk.SpinButton()
|
||||||
|
spin.set_range(1, 20); spin.set_increments(1, 5)
|
||||||
|
hb2.pack_start(spin, False, False, 0)
|
||||||
|
setattr(self, f"spin_anim_sz_t{i}", spin)
|
||||||
|
|
||||||
|
cmb.connect("changed", lambda w: self.update_preview())
|
||||||
|
spin.connect("value-changed", lambda w: self.update_preview())
|
||||||
|
|
||||||
|
# Global
|
||||||
|
frame_glob = Gtk.Frame(label="Global GIF")
|
||||||
|
vbox.pack_start(frame_glob, False, False, 0)
|
||||||
|
vb_g = Gtk.VBox(spacing=5); vb_g.set_border_width(5); frame_glob.add(vb_g)
|
||||||
|
|
||||||
|
hb_d = Gtk.HBox(spacing=5); vb_g.pack_start(hb_d, False, False, 0)
|
||||||
|
hb_d.pack_start(Gtk.Label(label="Delay (cs):"), False, False, 0)
|
||||||
|
self.spin_gif_delay = Gtk.SpinButton()
|
||||||
|
self.spin_gif_delay.set_range(1, 100); self.spin_gif_delay.set_value(10)
|
||||||
|
self.spin_gif_delay.set_increments(1, 10)
|
||||||
|
self.spin_gif_delay.connect("value-changed", lambda w: self.update_preview())
|
||||||
|
hb_d.pack_start(self.spin_gif_delay, False, False, 0)
|
||||||
|
|
||||||
|
hb_f = Gtk.HBox(spacing=5); vb_g.pack_start(hb_f, False, False, 0)
|
||||||
|
hb_f.pack_start(Gtk.Label(label="Frames:"), False, False, 0)
|
||||||
|
self.spin_gif_frames = Gtk.SpinButton()
|
||||||
|
self.spin_gif_frames.set_range(2, 100); self.spin_gif_frames.set_value(20)
|
||||||
|
self.spin_gif_frames.set_increments(1, 10)
|
||||||
|
self.spin_gif_frames.connect("value-changed", lambda w: self.update_preview())
|
||||||
|
hb_f.pack_start(self.spin_gif_frames, False, False, 0)
|
||||||
|
|
||||||
|
notebook.append_page(vbox, Gtk.Label(label="Animation"))
|
||||||
|
|
||||||
|
def build_tab_effects(self, notebook):
|
||||||
|
vbox = Gtk.VBox(spacing=5)
|
||||||
|
vbox.set_border_width(10)
|
||||||
|
|
||||||
|
self.chk_aa = Gtk.CheckButton(label="Text Antialiasing")
|
||||||
|
self.chk_aa.set_active(self.engine.text_antialias)
|
||||||
|
vbox.pack_start(self.chk_aa, False, False, 0)
|
||||||
|
|
||||||
|
self.chk_noise = Gtk.CheckButton(label="Gaussian Noise")
|
||||||
|
vbox.pack_start(self.chk_noise, False, False, 0)
|
||||||
|
self.chk_scanlines = Gtk.CheckButton(label="TV Scanlines")
|
||||||
|
vbox.pack_start(self.chk_scanlines, False, False, 0)
|
||||||
|
self.chk_dither = Gtk.CheckButton(label="Reduce Colors (Dither)")
|
||||||
|
vbox.pack_start(self.chk_dither, False, False, 0)
|
||||||
|
|
||||||
|
notebook.append_page(vbox, Gtk.Label(label="Effects"))
|
||||||
|
|
||||||
|
def select_template(self, w):
|
||||||
|
path = self.file_picker("Choose Background")
|
||||||
|
if path:
|
||||||
|
self.engine.bg_template_path = path
|
||||||
|
self.lbl_bg_tmpl.set_text(os.path.basename(path))
|
||||||
|
self.update_preview()
|
||||||
|
|
||||||
|
def clear_template(self, w):
|
||||||
|
self.engine.bg_template_path = None
|
||||||
|
self.lbl_bg_tmpl.set_text("No template")
|
||||||
|
self.update_preview()
|
||||||
|
|
||||||
|
def select_img(self, w):
|
||||||
|
path = self.file_picker("Choose Image")
|
||||||
|
if path:
|
||||||
|
self.engine.img_path = path
|
||||||
|
self.lbl_img.set_text(os.path.basename(path))
|
||||||
|
self.update_preview()
|
||||||
|
|
||||||
|
def file_picker(self, title):
|
||||||
|
dialog = Gtk.FileChooserDialog(title=title, action=Gtk.FileChooserAction.OPEN)
|
||||||
|
dialog.set_transient_for(self.window)
|
||||||
|
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
|
||||||
|
f = Gtk.FileFilter(); f.set_name("Images"); f.add_pattern("*.png"); f.add_pattern("*.jpg"); f.add_pattern("*.gif"); dialog.add_filter(f)
|
||||||
|
path = None
|
||||||
|
if dialog.run() == Gtk.ResponseType.OK: path = dialog.get_filename()
|
||||||
|
dialog.destroy()
|
||||||
|
return path
|
||||||
|
|
||||||
|
def sync_ui(self):
|
||||||
|
# General
|
||||||
|
self.engine.bg_type = self.get_combo_text(self.cmb_bg_type)
|
||||||
|
if self.engine.bg_type == "Gradient":
|
||||||
|
self.engine.bg_color = self.gdk_color_to_hex(self.grad_start_btn.get_color())
|
||||||
|
else:
|
||||||
|
self.engine.bg_color = self.gdk_color_to_hex(self.bg_color_btn.get_color())
|
||||||
|
|
||||||
|
self.engine.bg_color_end = self.gdk_color_to_hex(self.grad_end_btn.get_color()) # Gradient End
|
||||||
|
self.engine.bg_gradient_dir = self.get_combo_text(self.cmb_grad_dir)
|
||||||
|
self.engine.bg_pattern = self.get_combo_text(self.cmb_pat)
|
||||||
|
|
||||||
|
self.engine.border_enabled = self.chk_border.get_active()
|
||||||
|
self.engine.border_style = self.get_combo_text(self.cmb_brd_style)
|
||||||
|
self.engine.border_color = self.gdk_color_to_hex(self.brd_color_btn.get_color())
|
||||||
|
|
||||||
|
# Image
|
||||||
|
self.engine.img_w = int(self.spin_img_w.get_value())
|
||||||
|
self.engine.img_h = int(self.spin_img_h.get_value())
|
||||||
|
self.engine.img_x = int(self.spin_img_x.get_value())
|
||||||
|
self.engine.img_y = int(self.spin_img_y.get_value())
|
||||||
|
|
||||||
|
# Text 1 & 2
|
||||||
|
for i in [1, 2]:
|
||||||
|
setattr(self.engine, f"text{i}_content", getattr(self, f"entry_t{i}").get_text())
|
||||||
|
setattr(self.engine, f"text{i}_color", self.gdk_color_to_hex(getattr(self, f"btn_col_t{i}").get_color()))
|
||||||
|
setattr(self.engine, f"text{i}_size", int(getattr(self, f"spin_sz_t{i}").get_value()))
|
||||||
|
setattr(self.engine, f"text{i}_x", int(getattr(self, f"spin_x_t{i}").get_value()))
|
||||||
|
setattr(self.engine, f"text{i}_y", int(getattr(self, f"spin_y_t{i}").get_value()))
|
||||||
|
setattr(self.engine, f"text{i}_outline_enabled", getattr(self, f"chk_out_t{i}").get_active())
|
||||||
|
setattr(self.engine, f"text{i}_outline_color", self.gdk_color_to_hex(getattr(self, f"btn_ocol_t{i}").get_color()))
|
||||||
|
|
||||||
|
# Font
|
||||||
|
btn = getattr(self, f"btn_font_t{i}")
|
||||||
|
# Do NOT update engine font from button name directly here,
|
||||||
|
# trust on_font_set or just keep what engine has.
|
||||||
|
# Actually sync_ui should probably NOT overwrite font path with name from button
|
||||||
|
# unless i resolve it again. But on_font_set handles it.
|
||||||
|
# So skip font sync here to avoid resolving "Sans" back to "Sans" (unresolved).
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Effects
|
||||||
|
self.engine.text_antialias = self.chk_aa.get_active()
|
||||||
|
self.engine.effect_noise = self.chk_noise.get_active()
|
||||||
|
self.engine.effect_scanlines = self.chk_scanlines.get_active()
|
||||||
|
self.engine.effect_dither = self.chk_dither.get_active()
|
||||||
|
|
||||||
|
# Animations
|
||||||
|
for i in [1, 2]:
|
||||||
|
setattr(self.engine, f"text{i}_anim_type", self.get_combo_text(getattr(self, f"cmb_anim_t{i}")))
|
||||||
|
setattr(self.engine, f"text{i}_anim_speed", int(getattr(self, f"spin_anim_sz_t{i}").get_value()))
|
||||||
|
self.engine.gif_delay = int(self.spin_gif_delay.get_value())
|
||||||
|
self.engine.gif_frames = int(self.spin_gif_frames.get_value())
|
||||||
|
|
||||||
|
def set_combo_by_text(self, combo, text):
|
||||||
|
model = combo.get_model()
|
||||||
|
it = model.get_iter_first()
|
||||||
|
while it:
|
||||||
|
if model[it][0] == text:
|
||||||
|
combo.set_active_iter(it)
|
||||||
|
return
|
||||||
|
it = model.iter_next(it)
|
||||||
|
|
||||||
|
def sync_ui_from_engine(self):
|
||||||
|
# Background
|
||||||
|
self.set_combo_by_text(self.cmb_bg_type, self.engine.bg_type)
|
||||||
|
self.bg_color_btn.set_color(Gdk.Color.parse(self.engine.bg_color)[1])
|
||||||
|
self.grad_start_btn.set_color(Gdk.Color.parse(self.engine.bg_color)[1])
|
||||||
|
self.grad_end_btn.set_color(Gdk.Color.parse(self.engine.bg_color_end)[1])
|
||||||
|
self.set_combo_by_text(self.cmb_grad_dir, self.engine.bg_gradient_dir)
|
||||||
|
self.set_combo_by_text(self.cmb_pat, self.engine.bg_pattern)
|
||||||
|
self.lbl_bg_tmpl.set_text(os.path.basename(self.engine.bg_template_path) if self.engine.bg_template_path else "No template")
|
||||||
|
|
||||||
|
# Border
|
||||||
|
self.chk_border.set_active(self.engine.border_enabled)
|
||||||
|
self.set_combo_by_text(self.cmb_brd_style, self.engine.border_style)
|
||||||
|
self.brd_color_btn.set_color(Gdk.Color.parse(self.engine.border_color)[1])
|
||||||
|
|
||||||
|
# Image
|
||||||
|
self.lbl_img.set_text(os.path.basename(self.engine.img_path) if self.engine.img_path else "No image")
|
||||||
|
self.spin_img_w.set_value(self.engine.img_w)
|
||||||
|
self.spin_img_h.set_value(self.engine.img_h)
|
||||||
|
self.spin_img_x.set_value(self.engine.img_x)
|
||||||
|
self.spin_img_y.set_value(self.engine.img_y)
|
||||||
|
|
||||||
|
# Text 1 & 2
|
||||||
|
for i in [1, 2]:
|
||||||
|
getattr(self, f"entry_t{i}").set_text(getattr(self.engine, f"text{i}_content"))
|
||||||
|
getattr(self, f"btn_col_t{i}").set_color(Gdk.Color.parse(getattr(self.engine, f"text{i}_color"))[1])
|
||||||
|
getattr(self, f"spin_sz_t{i}").set_value(getattr(self.engine, f"text{i}_size"))
|
||||||
|
getattr(self, f"spin_x_t{i}").set_value(getattr(self.engine, f"text{i}_x"))
|
||||||
|
getattr(self, f"spin_y_t{i}").set_value(getattr(self.engine, f"text{i}_y"))
|
||||||
|
getattr(self, f"chk_out_t{i}").set_active(getattr(self.engine, f"text{i}_outline_enabled"))
|
||||||
|
getattr(self, f"btn_ocol_t{i}").set_color(Gdk.Color.parse(getattr(self.engine, f"text{i}_outline_color"))[1])
|
||||||
|
# Note: don't easily sync font button back unless have the Pango description string
|
||||||
|
# but at least set the font button label if possible.
|
||||||
|
# In Gtk2 FontButton, set_font_name works.
|
||||||
|
name = getattr(self.engine, f"text{i}_font")
|
||||||
|
size = getattr(self.engine, f"text{i}_size")
|
||||||
|
getattr(self, f"btn_font_t{i}").set_font_name(f"{os.path.basename(name)} {size}")
|
||||||
|
|
||||||
|
# Effects
|
||||||
|
self.chk_aa.set_active(self.engine.text_antialias)
|
||||||
|
self.chk_noise.set_active(self.engine.effect_noise)
|
||||||
|
self.chk_scanlines.set_active(self.engine.effect_scanlines)
|
||||||
|
self.chk_dither.set_active(self.engine.effect_dither)
|
||||||
|
|
||||||
|
# Animation
|
||||||
|
for i in [1, 2]:
|
||||||
|
self.set_combo_by_text(getattr(self, f"cmb_anim_t{i}"), getattr(self.engine, f"text{i}_anim_type"))
|
||||||
|
getattr(self, f"spin_anim_sz_t{i}").set_value(getattr(self.engine, f"text{i}_anim_speed"))
|
||||||
|
self.spin_gif_delay.set_value(self.engine.gif_delay)
|
||||||
|
self.spin_gif_frames.set_value(self.engine.gif_frames)
|
||||||
|
|
||||||
|
# Show/Hide bg controls
|
||||||
|
self.on_bg_type_changed(self.cmb_bg_type)
|
||||||
|
|
||||||
|
def on_save_preset(self, w):
|
||||||
|
self.sync_ui()
|
||||||
|
dialog = Gtk.FileChooserDialog(title="Save Preset", action=Gtk.FileChooserAction.SAVE)
|
||||||
|
dialog.set_transient_for(self.window)
|
||||||
|
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
|
||||||
|
dialog.set_do_overwrite_confirmation(True)
|
||||||
|
f = Gtk.FileFilter(); f.set_name("JSON Presets"); f.add_pattern("*.json"); dialog.add_filter(f)
|
||||||
|
|
||||||
|
if dialog.run() == Gtk.ResponseType.OK:
|
||||||
|
path = dialog.get_filename()
|
||||||
|
if not path.endswith(".json"): path += ".json"
|
||||||
|
state = self.engine.get_state()
|
||||||
|
try:
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
json.dump(state, f, indent=4)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving preset: {e}")
|
||||||
|
dialog.destroy()
|
||||||
|
|
||||||
|
def on_load_preset(self, w):
|
||||||
|
dialog = Gtk.FileChooserDialog(title="Load Preset", action=Gtk.FileChooserAction.OPEN)
|
||||||
|
dialog.set_transient_for(self.window)
|
||||||
|
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
|
||||||
|
f = Gtk.FileFilter(); f.set_name("JSON Presets"); f.add_pattern("*.json"); dialog.add_filter(f)
|
||||||
|
|
||||||
|
if dialog.run() == Gtk.ResponseType.OK:
|
||||||
|
path = dialog.get_filename()
|
||||||
|
try:
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
state = json.load(f)
|
||||||
|
self.engine.set_state(state)
|
||||||
|
self.sync_ui_from_engine()
|
||||||
|
self.update_preview()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading preset: {e}")
|
||||||
|
dialog.destroy()
|
||||||
|
|
||||||
|
def on_zoom_changed(self, combo):
|
||||||
|
txt = self.get_combo_text(combo)
|
||||||
|
if txt == "1x": self.zoom_level = 1
|
||||||
|
elif txt == "2x": self.zoom_level = 2
|
||||||
|
elif txt == "4x": self.zoom_level = 4
|
||||||
|
self.update_preview()
|
||||||
|
|
||||||
|
def on_update_clicked(self, w): self.update_preview()
|
||||||
|
|
||||||
|
def update_preview(self):
|
||||||
|
self.sync_ui()
|
||||||
|
# Preview filename extension depends on animation
|
||||||
|
is_gif = (self.engine.text1_anim_type != "None" or self.engine.text2_anim_type != "None")
|
||||||
|
out_ext = ".gif" if is_gif else ".png"
|
||||||
|
preview_file = PREVIEW_FILENAME.replace(".png", out_ext)
|
||||||
|
|
||||||
|
if self.engine.generate(preview_file):
|
||||||
|
if is_gif:
|
||||||
|
# Use PixbufAnimation for GIFs
|
||||||
|
anim = GdkPixbuf.PixbufAnimation.new_from_file(preview_file)
|
||||||
|
self.preview_image.set_from_animation(anim)
|
||||||
|
else:
|
||||||
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file(preview_file)
|
||||||
|
if self.zoom_level > 1:
|
||||||
|
w = pixbuf.get_width() * self.zoom_level
|
||||||
|
h = pixbuf.get_height() * self.zoom_level
|
||||||
|
pixbuf = pixbuf.scale_simple(w, h, GdkPixbuf.InterpType.NEAREST)
|
||||||
|
self.preview_image.set_from_pixbuf(pixbuf)
|
||||||
|
|
||||||
|
def save_image(self, w):
|
||||||
|
self.sync_ui()
|
||||||
|
dialog = Gtk.FileChooserDialog(title="Save", action=Gtk.FileChooserAction.SAVE)
|
||||||
|
dialog.set_transient_for(self.window)
|
||||||
|
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
|
||||||
|
dialog.set_do_overwrite_confirmation(True)
|
||||||
|
if dialog.run() == Gtk.ResponseType.OK: self.engine.generate(dialog.get_filename())
|
||||||
|
dialog.destroy()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = UltimateButtonApp()
|
||||||
|
Gtk.main()
|
||||||
Loading…
Reference in New Issue
Block a user