fonteditor/main.py
2024-03-16 22:00:41 +01:00

372 lines
11 KiB
Python

import os
import platform
import tkinter.filedialog
import unicodedata
from canvas import *
from project import *
##### CONFIG #####
WINDOW_SIZE = (
(1280,720),
(1920,1019)
)[0]
cross_color = (255, 60, 25)
pixel_color = (255,) * 3
bg_col = "#1e1e1e"
##################
project=Project()
def button_create_project_click(subwindow,w,h):
try:
width=int(w)
except ValueError as e:
tkinter.messagebox.showerror("Creating new project", f"Invalid value {w}: {e}")
return
try:
height=int(h)
except ValueError as e:
tkinter.messagebox.showerror("Creating new project", f"Invalid value {h}: {e}")
return
project.chars={}
project.char_res=(width,height)
project.loaded=True
canvas_editor.after_project_load()
canvas_editor.draw()
update_glyph_preview()
subwindow.destroy()
def menu_file_new_project_click():
global window
if project.modified and not tkinter.messagebox.askyesno("Creating new project","You have unsaved changes, are you sure?"):
return
subwindow=tkinter.Toplevel(window)
subwindow.title("New project")
label_width=tkinter.Label(subwindow,text="Character width")
label_width.pack(side="top")
entry_width=tkinter.Entry(subwindow,validate="all",validatecommand=((subwindow.register(number_only_validate)),"%P"))
entry_width.pack(side="top",fill="x",padx=5)
label_height=tkinter.Label(subwindow,text="Character height")
label_height.pack(side="top")
entry_height=tkinter.Entry(subwindow,validate="all",validatecommand=((subwindow.register(number_only_validate)),"%P"))
entry_height.pack(side="top",fill="x",padx=5)
button_create=tkinter.Button(subwindow,text="Create",command=lambda: button_create_project_click(subwindow,entry_width.get(),entry_height.get()))
button_create.pack(side="bottom",fill="x",padx=5)
def open_project(file_path):
try:
project.load(file_path)
except AttributeError as e:
tkinter.messagebox.showerror("Opening project",f"Project '{file_path}' is invalid: {e}")
except IOError as e:
tkinter.messagebox.showerror("Opening project",f"Failed to open project '{file_path}': {e}")
finally:
canvas_editor.after_project_load()
canvas_editor.draw()
update_glyph_preview()
def menu_file_open_project_click():
global project
global canvas_editor
file_path = tkinter.filedialog.askopenfilename(
title="Open font project",
filetypes=[("Font project (json)", "*.fontproj")]
)
if file_path == "" or file_path==():
return
# save last path to cache
with open("last_path_cache.txt", "w") as f:
f.write(file_path)
open_project(file_path)
def menu_file_open_last_project_click():
try:
cache=open("last_path_cache.txt","r")
file_path=cache.read()
cache.close()
open_project(file_path)
except IOError:
tkinter.messagebox.showerror("Opening last project","Failed to load last project cache")
def save_project(ask):
global canvas_editor
global project
if not project.loaded:
return
path=None
if project.path and not ask:
path=project.path
else:
# show file save dialog
path=tkinter.filedialog.asksaveasfilename(
title="Save font project",
filetypes=[("Font project (json)", "*.fontproj")],
defaultextension=".fontproj"
)
if path=="" or path==():
return
# save last path to cache
with open("last_path_cache.txt", "w") as f:
f.write(path)
canvas_editor.save_char()
try:
project.save(path)
except IOError as e:
tkinter.messagebox.showerror("Saving project",f"Failed to save project '{path}': {e}")
def export_project(ask):
if not project.loaded:
return
path=None
if project.export_path and not ask:
path=project.export_path
else:
path = tkinter.filedialog.askdirectory(
title="Choose folder where exported pages will be saved"
)
if path=="" or path==():
return
canvas_editor.save_char()
try:
project.export(path)
except IOError as e:
tkinter.messagebox.showerror("Exporting project",f"Failed to export project: {e}")
def button_prev_glyph_click():
global canvas_editor
global project
if not project.loaded:
return
canvas_editor.prev_glyph()
update_glyph_preview()
def button_next_glyph_click():
global canvas_editor
global project
if not project.loaded:
return
canvas_editor.next_glyph()
update_glyph_preview()
def button_glyph_search_click():
global canvas_editor
global entry_glyph_id
global project
if not project.loaded:
return
code=entry_glyph_id.get()
canvas_editor.save_char()
try:
canvas_editor.current_char=int(code,base=16)
except ValueError as e:
tkinter.messagebox.showerror("Searching glyph", f"Invalid hex value {code}: {e}")
finally:
canvas_editor.keep_current_char_in_bounds()
canvas_editor.load_char()
update_glyph_preview()
def number_only_validate(val):
return val.isdigit() or val==""
def hex_only_validate(val):
for x in val:
if not x.isdigit() and (ord(x)<ord("a") or ord(x)>ord("f")) and (ord(x)<ord("A") or ord(x)>ord("F")):
return False
return True
def update_glyph_preview():
global canvas_editor
global canvas_preview
global label_glyph_name
canvas_preview.delete("all")
canvas_preview.create_text((50,100),text=chr(canvas_editor.current_char),fill="white",font="tkDefaultFont 70")
name=unicodedata.name(chr(canvas_editor.current_char),"unknown")
label_glyph_name.config(text=f"{canvas_editor.current_char}\n{name}\nU+{canvas_editor.current_char:04x}")
def canvas_editor_handle_scroll(delta):
global canvas_editor
global project
if not project.loaded:
return
if delta>0:
canvas_editor.prev_glyph()
else:
canvas_editor.next_glyph()
update_glyph_preview()
window=tkinter.Tk()
window.title("fonteditor")
window.geometry(f"{WINDOW_SIZE[0]}x{WINDOW_SIZE[1]}")
menubar=tkinter.Menu(window)
menu_file=tkinter.Menu(menubar,tearoff=False)
menu_file.add_command(label="New project",command=menu_file_new_project_click)
menu_file.add_command(label="Open project",command=menu_file_open_project_click)
menu_file.add_command(label="Open last project",command=menu_file_open_last_project_click)
menu_file.add_command(label="Save project",command=lambda: save_project(False))
menu_file.add_command(label="Save project as",command=lambda: save_project(True))
menu_export=tkinter.Menu(menubar,tearoff=False)
menu_export.add_command(label="Export",command=lambda: export_project(False))
menu_export.add_command(label="Export as",command=lambda: export_project(True))
menu_view=tkinter.Menu(menubar,tearoff=False)
menu_view.add_command(label="Zoom in",command=lambda: canvas_editor.zoom_by(10))
menu_view.add_command(label="Zoom out",command=lambda: canvas_editor.zoom_by(-10))
menubar.add_cascade(label="File",menu=menu_file)
menubar.add_cascade(label="Export",menu=menu_export)
menubar.add_cascade(label="View",menu=menu_view)
canvas_editor=EditorCanvas(project,window,bg="black")
canvas_editor.pack(side="left",fill="both",expand=True)
if platform.system()=="Windows" or platform.system()=="Darwin":
canvas_editor.bind("<MouseWheel>",lambda event: canvas_editor_handle_scroll(event.delta))
else:
# This will probably work only on X11
canvas_editor.bind("<Button-4>",lambda _: canvas_editor_handle_scroll(1))
canvas_editor.bind("<Button-5>",lambda _: canvas_editor_handle_scroll(-1))
#### Guide pos ####
frame_label_guide_pos = tkinter.Frame(window, bg=bg_col)
frame_label_guide_pos.pack(side="right")
label_1 = tkinter.Label(frame_label_guide_pos, text="Guide 1 Y", bg=bg_col, fg="#ffffff").pack(side="top")
entry_1 = tkinter.Entry(frame_label_guide_pos, validate="all", validatecommand=((window.register(number_only_validate)),"%P"))
entry_1.pack(side="top", pady=2)
label_2 = tkinter.Label(frame_label_guide_pos, text="Guide 2 Y", bg=bg_col, fg="#ffffff").pack(side="top")
entry_2 = tkinter.Entry(frame_label_guide_pos, validate="all", validatecommand=((window.register(number_only_validate)),"%P"))
entry_2.pack(side="top", pady=2)
label_3 = tkinter.Label(frame_label_guide_pos, text="Guide 3 Y", bg=bg_col, fg="#ffffff").pack(side="top")
entry_3 = tkinter.Entry(frame_label_guide_pos, validate="all", validatecommand=((window.register(number_only_validate)),"%P"))
entry_3.pack(side="top", pady=2)
## load last values
cwd = os.path.dirname(os.path.realpath(__file__))
guide_vals_path = os.path.join(cwd, "guide_values.txt")
def get_guide_entries():
return (
int(entry_1.get()),
int(entry_2.get()),
int(entry_3.get())
)
def update_guide_pos(pos, dont_save=False):
canvas_editor.set_guide_pos(pos)
if dont_save:
return
with open(guide_vals_path, "w") as f:
f.write(f"{entry_1.get()}\n{entry_2.get()}\n{entry_3.get()}")
if os.path.exists(guide_vals_path):
with open(guide_vals_path, "r") as f:
lines = f.readlines()
entry_1.insert(0, lines[0].strip())
entry_2.insert(0, lines[1].strip())
entry_3.insert(0, lines[2].strip())
update_guide_pos(get_guide_entries(), dont_save=True)
else:
entry_1.insert(0, str(3))
entry_2.insert(0, str(5))
entry_3.insert(0, str(10))
update_guide_pos(get_guide_entries())
set_button = tkinter.Button(
frame_label_guide_pos,
text="Set",
command=lambda: update_guide_pos(get_guide_entries())
).pack(side="top", pady=[5, 30])
#### Copy ####
frame = tkinter.Frame(frame_label_guide_pos, bg=bg_col)
frame.pack(side="right")
copy_button = tkinter.Button(frame, text="Copy", command=canvas_editor.copy_char)
copy_button.pack(side="top", pady=5)
#### Preview ####
frame_controls=tkinter.Frame(frame, bg=bg_col)
frame_controls.pack(side="right")
canvas_preview=tkinter.Canvas(frame_controls,width=100,height=200,bg="black")
canvas_preview.pack(side="top")
label_glyph_name=tkinter.Label(frame_controls, bg=bg_col, fg="#ffffff")
label_glyph_name.pack(side="top")
#### Navigation ####
frame_nav=tkinter.Frame(frame_controls)
frame_nav.pack(side="top",pady=10)
button_prev_glyph=tkinter.Button(frame_nav,width=10,text="Previous",command=button_prev_glyph_click)
button_prev_glyph.pack(side="left")
button_next_glyph=tkinter.Button(frame_nav,width=10,text="Next",command=button_next_glyph_click)
button_next_glyph.pack(side="left")
#### Glyph search ####
frame_glyph_id=tkinter.Frame(frame_controls)
frame_glyph_id.pack(side="top",pady=10)
entry_glyph_id=tkinter.Entry(frame_glyph_id,validate="all",validatecommand=((window.register(hex_only_validate)),"%P"))
entry_glyph_id.pack(side="left")
button_glyph_search=tkinter.Button(frame_glyph_id,width=10,text="Search",command=button_glyph_search_click)
button_glyph_search.pack(side="left")
window.config(menu=menubar, bg=bg_col)
window.mainloop()