commit 14e34bce931f0e8e5c190657fc95f31e65c1b8a9 Author: laki Date: Thu Jan 29 20:18:47 2026 +0000 Initial commit for 88x31-maker diff --git a/button-maker-advanced.py b/button-maker-advanced.py new file mode 100644 index 0000000..5ce2102 --- /dev/null +++ b/button-maker-advanced.py @@ -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() diff --git a/to do.txt b/to do.txt new file mode 100644 index 0000000..bf03b87 --- /dev/null +++ b/to do.txt @@ -0,0 +1,9 @@ +to do (author note): + +- add export imagemagick command +- add presets that come with the program +- gif needs to be fixed + - add boundaries for the gif + - make it Smooth + +- \ No newline at end of file