diff --git a/watchy-image-editor/__init__.py b/watchy-image-editor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/watchy-image-editor/app.py b/watchy-image-editor/app.py new file mode 100644 index 0000000..c2fb7cc --- /dev/null +++ b/watchy-image-editor/app.py @@ -0,0 +1,325 @@ +import tkinter as tk +from tkinter import ttk, filedialog, messagebox +from typing import Optional +from enum import Enum +import os.path + +from explorer import Explorer +from image_view import ImageView +from input_popup import InputPopup +from file import File +from image import Image +from bitmap import Bitmap, BitmapError + + +class MenuEntryType(Enum): + DEFAULT = 0 + NEED_FILE = 1 + NEED_IMAGE = 2 + SEPARATOR = 4 + + +class App(ttk.Frame): + MENU_ENTRIES = { + "File": [ + ("New File", "_file_new", MenuEntryType.DEFAULT), + ("Open File...", "_file_open", MenuEntryType.DEFAULT), + ("", "", MenuEntryType.SEPARATOR), + ("Save File", "_file_save", MenuEntryType.NEED_FILE), + ("Save File As...", "_file_save_as", MenuEntryType.NEED_FILE), + ("Close File", "_file_close", MenuEntryType.NEED_FILE), + ("", "", MenuEntryType.SEPARATOR), + ( + "New image...", + "_file_new_image", + MenuEntryType.NEED_FILE, + ), + ("", "", MenuEntryType.SEPARATOR), + ("Quit", "_file_quit", MenuEntryType.DEFAULT), + ], + "Image": [ + ( + "Edit Image Name...", + "_image_edit_name", + MenuEntryType.NEED_IMAGE, + ), + ( + "Edit Image Size...", + "_image_edit_size", + MenuEntryType.NEED_IMAGE, + ), # TODO _image_edit_size + ( + "Move Image Up", + "_image_move_up", + MenuEntryType.NEED_IMAGE, + ), + ( + "Move Image Down", + "_image_move_down", + MenuEntryType.NEED_IMAGE, + ), + ( + "Delete Image", + "_image_delete", + MenuEntryType.NEED_IMAGE, + ), + ], + "Bitmap": [ + ( + "Bulk .bmp Import...", + "_bmp_import_all", + MenuEntryType.NEED_FILE, + ), + ( + "Export All To .bmp...", + "_bmp_export_all", + MenuEntryType.NEED_FILE, + ), + ("", "", MenuEntryType.SEPARATOR), + ( + "Import .bmp Into Image...", + "_bmp_import_image", + MenuEntryType.NEED_IMAGE, + ), + ("Export Image To .bmp...", "_bmp_export_image", MenuEntryType.NEED_IMAGE), + ], + } + + def __init__(self, parent) -> None: + super().__init__(parent) + + parent.option_add("*tearOff", tk.FALSE) + parent.resizable(False, False) + + self.parent = parent + self.current_file = None + + self.explorer = Explorer(self, self.update) + self.explorer.grid(column=0, row=0, sticky="nsw") + + self.image_view = ImageView(self) + self.image_view.grid(column=1, row=0, sticky="nsew") + + self.init_menus() + + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + + self.open_file(None) + + self.pack(fill="both", expand=True) + + @property + def current_image(self) -> Optional[Image]: + if self.current_file is None: + return None + else: + return self.explorer.current_image + + def update(self, force: bool = False) -> None: + self.update_title() + self.update_menus() + self.image_view.update(self.current_image) + self.explorer.update(self.current_file, force) + + def update_title(self) -> None: + title = "Watchy Image Editor" + if self.current_file is not None: + title += "- " + if self.current_file.path is None: + title += "New file" + else: + title += self.current_file.filename + if self.current_file.modified: + title += "*" + self.parent.title(title) + + def init_menus(self) -> None: + self.menubar = tk.Menu(self.parent) + self.parent["menu"] = self.menubar + + self.menus = {} + + for menu_name in self.MENU_ENTRIES: + self.menus[menu_name] = tk.Menu(self.menubar) + self.menubar.add_cascade(menu=self.menus[menu_name], label=menu_name) + + for entry_name, entry_action_name, entry_type in self.MENU_ENTRIES[ + menu_name + ]: + if entry_type == MenuEntryType.SEPARATOR: + self.menus[menu_name].add_separator() + else: + try: + entry_action = getattr(self, entry_action_name) + except AttributeError: + entry_action = lambda: print("missing menu action") + self.menus[menu_name].add_command( + label=entry_name, command=entry_action + ) + + def update_menus(self) -> None: + for menu_name in self.MENU_ENTRIES: + any_enabled = False + for entry_name, entry_action, entry_type in self.MENU_ENTRIES[menu_name]: + if entry_type == MenuEntryType.NEED_FILE: + self.menus[menu_name].entryconfigure( + entry_name, + state=( + "normal" if self.current_file is not None else "disabled" + ), + ) + any_enabled |= self.current_file is not None + elif entry_type == MenuEntryType.NEED_IMAGE: + self.menus[menu_name].entryconfigure( + entry_name, + state=( + "normal" if self.current_image is not None else "disabled" + ), + ) + any_enabled |= self.current_image is not None + elif entry_type == MenuEntryType.DEFAULT: + any_enabled = True + + self.menubar.entryconfigure( + menu_name, state=("normal" if any_enabled else "disabled") + ) + + def open_file(self, path: Optional[str], new: bool = False) -> None: + if path is None and not new: + self.current_file = None + else: + self.current_file = File(path) + self.update(force=True) + + def save_file(self, path: Optional[str] = None) -> None: + if path == "": + path = filedialog.asksaveasfilename() + self.current_file.export(path) + self.open_file(path) + + def _file_new(self) -> None: + self.open_file(None, True) + + def _file_open(self) -> None: + path = filedialog.askopenfilename( + filetypes=File.FILE_TYPES, + defaultextension=File.FILE_TYPES, + initialfile=( + os.path.basename(self.current_file.path) + if self.current_file is not None + else None + ), + initialdir=( + os.path.dirname(self.current_file.path) + if self.current_file is not None + else None + ), + ) + if path: + self.open_file(path) + + def _file_save(self) -> None: + if self.current_file.path is None: + self._file_save_as() + else: + self.save_file(self.current_file.path) + + def _file_save_as(self) -> None: + path = filedialog.asksaveasfilename( + filetypes=File.FILE_TYPES, + defaultextension=File.FILE_TYPES, + initialfile=( + os.path.basename(self.current_file.path) + if self.current_file.path is not None + else None + ), + initialdir=( + os.path.dirname(self.current_file.path) + if self.current_file.path is not None + else None + ), + ) + if path: + self.save_file(path) + + def _file_close(self) -> None: + self.open_file(None) + + def _file_new_image(self) -> None: + popup = InputPopup( + self, + title="New image", + message="Please enter image name", + ) + if popup.value: + self.current_file.images += [Image(popup.value, 20, 20, empty=True)] + self.update() + + def _file_quit(self) -> None: + self.parent.destroy() + + def _image_edit_name(self) -> None: + popup = InputPopup( + self, + title="Edit image name", + message="Please enter image name", + initial_value=self.explorer.current_image.name, + ) + if popup.value: + self.explorer.current_image.name = popup.value + self.update() + + def _image_move_up(self) -> None: + self.explorer.move_up() + + def _image_move_down(self) -> None: + self.explorer.move_down() + + def _image_delete(self) -> None: + self.explorer.delete() + + def _bmp_import_all(self) -> None: + paths = filedialog.askopenfilenames( + filetypes=Bitmap.FILE_TYPES, + defaultextension=Bitmap.FILE_TYPES, + ) + if paths and len(paths) > 0: + for path in paths: + name = os.path.basename(path).rstrip(".bmp") + image = self.current_file.search(name) + if image is None: + image = Image(name, 20, 20, empty=True) + self.current_file.images += [image] + try: + image.import_bmp(path) + except BitmapError as e: + pass + self.update() + + def _bmp_export_all(self) -> None: + dir_path = filedialog.askdirectory() + if dir_path: + for image in self.current_file.images: + image.export_bmp(os.path.join(dir_path, f"{image.name}.bmp")) + + def _bmp_import_image(self) -> None: + path = filedialog.askopenfilename( + filetypes=Bitmap.FILE_TYPES, + defaultextension=Bitmap.FILE_TYPES, + ) + if path: + try: + self.current_image.import_bmp(path) + self.update() + except BitmapError as e: + messagebox.showerror(title="Bitmap import error", message=str(e)) + + def _bmp_export_image(self) -> None: + path = filedialog.asksaveasfilename( + filetypes=Bitmap.FILE_TYPES, + defaultextension=Bitmap.FILE_TYPES, + initialfile=f"{self.current_image.name}.bmp", + ) + if path: + self.current_image.export_bmp(path) diff --git a/watchy-image-editor/bitmap.py b/watchy-image-editor/bitmap.py new file mode 100644 index 0000000..5a28577 --- /dev/null +++ b/watchy-image-editor/bitmap.py @@ -0,0 +1,119 @@ +from typing import Tuple + + +class BitmapError(Exception): + pass + + +class Bitmap: + HEADER_SIZE = 54 + FILE_TYPES = [("Bitmap Image", "*.bmp"), ("All Files", "*.*")] + + @classmethod + def write_bmp(cls, path: str, width: int, color_depth: int, data: bytes) -> None: + with open(path, mode="wb") as f: + f.write(cls.__get_bmp_data(width, color_depth, data)) + + @classmethod + def __get_bmp_data(cls, width: int, color_depth: int, data: bytes) -> bytes: + height = len(data) // (width * 3) + return cls.__get_header( + width, height, color_depth, len(data) + ) + cls.__format_data(width, height, color_depth, data) + + @classmethod + def __get_header( + cls, width: int, height: int, color_depth: int, data_len: int + ) -> bytes: + header = bytes() + # BMP header + header += "BM".encode() # (0, 2) BM + header += (cls.HEADER_SIZE + data_len).to_bytes( + 4, byteorder="little" + ) # (2, 4) file size + header += bytes([0]) * 4 # (6, 4) application reserved + header += (cls.HEADER_SIZE).to_bytes( + 4, byteorder="little" + ) # (10, 4) data offset + # DIB header + header += (40).to_bytes(4, byteorder="little") # (14, 4) DIB header size + header += width.to_bytes(4, byteorder="little") # (18, 4) width + header += height.to_bytes(4, byteorder="little") # (22, 4) height + header += (1).to_bytes(2, byteorder="little") # (26, 2) color panes + header += (color_depth * 8).to_bytes( + 2, byteorder="little" + ) # (28, 2) bits per pixel + header += bytes([0]) * 4 # (30, 4) BI_RGB, no compression + header += (data_len).to_bytes( + 4, byteorder="little" + ) # (34, 4) size of raw bitmap data + header += (2835).to_bytes( + 4, byteorder="little" + ) # (38, 4) horizontal print resolution + header += (2835).to_bytes( + 4, byteorder="little" + ) # (42, 4) vertical print resolution + header += bytes([0]) * 4 # (46, 4) color in palette + header += bytes([0]) * 4 # (50, 4) 0 important colors + return header + + @classmethod + def __format_data( + cls, width: int, height: int, color_depth: int, data: bytes + ) -> bytes: + size = width * height * color_depth + if len(data) < size: + data += bytes([0]) * (size - len(data)) + elif len(data) > size: + data = data[:size] + line_padding = (width * color_depth) % 4 + output_data = bytes() + for y in range(height): + start = (height - y - 1) * color_depth * width + output_data += data[start : start + width * color_depth] + if line_padding > 0: + output_data += bytes([0]) * (4 - line_padding) + return output_data + + @classmethod + def read_bmp(cls, path: str) -> Tuple[int, int, int, bytes]: + with open(path, mode="rb") as f: + bmp_data = f.read() + width, height, color_depth, data_start, data_size = cls.__read_header(bmp_data) + content_data = bmp_data[data_start:] + if data_size > 0: + content_data = content_data[:data_size] + output_data = cls.__read_formated_data(width, height, color_depth, content_data) + return width, height, color_depth, output_data + + @classmethod + def __read_header(cls, bmp_data: bytes) -> Tuple[int, int, int, int, int]: + if bmp_data[0:2].decode() != "BM": + raise BitmapError("Not a Bitmap Image") + if int.from_bytes(bmp_data[30:34], byteorder="little") != 0: + raise BitmapError("Cannot read Bitmap: need no compression") + if int.from_bytes(bmp_data[26:28], byteorder="little") != 1: + raise BitmapError("Cannot read Bitmap: need 1 color panes") + width = int.from_bytes(bmp_data[18:22], byteorder="little") + height = int.from_bytes(bmp_data[22:26], byteorder="little") + color_depth = int.from_bytes(bmp_data[28:30], byteorder="little") // 8 + if color_depth < 1: + raise BitmapError("Cannot read Bitmap: bits per pixels is < 8") + data_start = int.from_bytes(bmp_data[10:14], byteorder="little") + data_size = int.from_bytes(bmp_data[34:38], byteorder="little") + return width, height, color_depth, data_start, data_size + + @classmethod + def __read_formated_data( + cls, width: int, height: int, color_depth: int, bmp_data: bytes + ) -> bytes: + line_padding = (width * color_depth) % 4 + if line_padding > 0: + real_width = width * color_depth + (4 - line_padding) + else: + real_width = width * color_depth + output_data = bytes() + for y in range(height): + start = (height - y - 1) * real_width + output_data += bmp_data[start : start + width * color_depth] + return output_data diff --git a/watchy-image-editor/explorer.py b/watchy-image-editor/explorer.py new file mode 100644 index 0000000..bdb3862 --- /dev/null +++ b/watchy-image-editor/explorer.py @@ -0,0 +1,111 @@ +from tkinter import ttk +from typing import Optional + +from file import File +from image import Image + + +class Explorer(ttk.Frame): + def __init__(self, parent, update_callback) -> None: + super().__init__(parent) + + self.current_file = None + self.current_id = None + self.update_callback = update_callback + + self.explorer = ttk.Treeview(self, columns=("size")) + self.explorer.heading("#0", text="name") + self.explorer.heading("size", text="size") + self.explorer.column("#0", width=150, anchor="w") + self.explorer.column("size", width=80, anchor="w") + self.explorer.grid(row=0, column=0, sticky="nsw") + self.explorer.bind("<>", self.explorer_item_click) + + yscrollbar = ttk.Scrollbar(self, orient="vertical", command=self.explorer.yview) + yscrollbar.grid(row=0, column=1, sticky="nsw") + self.explorer.configure(yscrollcommand=yscrollbar.set) + + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + + @property + def current_image(self) -> Optional[Image]: + if self.current_file is None or self.current_id is None: + return None + else: + return self.current_file.images[self.current_id] + + @property + def size(self) -> int: + if self.current_file is None: + return 0 + else: + return len(self.current_file.images) + + def focus(self, id: int) -> None: + if self.current_file is not None and id >= 0 and id < self.size: + self.current_id = id + self.explorer.selection_set(str(id)) + + def move_up(self) -> None: + if self.current_id > 0: + id = self.current_id + images = self.current_file.images + images[id], images[id - 1] = images[id - 1], images[id] + self.current_id -= 1 + self.focus(self.current_id) + self.update(self.current_file, False) + + def move_down(self) -> None: + if self.current_id < self.size - 1: + id = self.current_id + images = self.current_file.images + images[id], images[id + 1] = images[id + 1], images[id] + self.current_id += 1 + self.focus(self.current_id) + self.update(self.current_file, False) + + def delete(self) -> None: + del self.current_file.images[self.current_id] + self.current_id = min(self.current_id, self.size - 1) + self.update(self.current_file, True) + + def update(self, file: File, force: bool): + focus_id = self.current_id + + if force or file != self.current_file: + focus_id = 0 + if file is not None and file == self.current_file: + focus_id = self.current_id + ids = self.explorer.get_children() + if len(ids) > 0: + self.explorer.delete(*ids) + self.current_id = None + + self.current_file = file + + if self.current_file is not None: + for i, image in enumerate(self.current_file.images): + if self.explorer.exists(str(i)): + self.explorer.item( + str(i), + text=f"{image.name}{'*' if image.modified else ''}", + values=[f"{image.width}x{image.height}"], + ) + else: + self.explorer.insert( + "", + "end", + iid=str(i), + text=f"{image.name}{'*' if image.modified else ''}", + values=[f"{image.width}x{image.height}"], + ) + if self.size > 0 and (focus_id != self.current_id or force): + self.focus(focus_id) + + def explorer_item_click(self, event) -> None: + if self.current_file is None or len(self.explorer.selection()) == 0: + self.current_id = None + else: + self.current_id = int(self.explorer.selection()[0]) + self.update_callback() diff --git a/watchy-image-editor/file.py b/watchy-image-editor/file.py new file mode 100644 index 0000000..f3a130f --- /dev/null +++ b/watchy-image-editor/file.py @@ -0,0 +1,76 @@ +from typing import List, Optional +import re +import os.path + +from image import Image + + +class File: + FILE_TYPES = [("Header File", "*.h"), ("All Files", "*.*")] + + def __init__(self, path: str) -> None: + self.path = path + if path is None: + self.images = [] + else: + self.images = self.__read_file() + + @property + def filename(self) -> str: + if self.path is None: + return None + return os.path.basename(self.path) + + @property + def modified(self) -> bool: + return any(image.modified for image in self.images) + + def search(self, name) -> Optional[Image]: + for image in self.images: + if image.name == name: + return image + return None + + def __read_file(self) -> List[Image]: + images = [] + + current_image = None + + with open(self.path) as f: + for line in f: + header = re.match( + r"const unsigned char (\w+) \[\] PROGMEM \= \{", + line, + ) + if header: + groups = header.groups() + if current_image is None: + current_image = Image(groups[0], 0, 0) + current_image.name = groups[0] + elif current_image is not None and current_image.name is not None: + data = re.match(r"((0x\w+,? ?)+)", line.strip()) + if data: + current_image.add_data( + data.groups()[0].strip().strip(",").split(", ") + ) + else: + images += [current_image] + current_image.finalize() + current_image = None + comment_header = re.match(r"// '(\w+)', (\d+)x(\d+)px", line) + if comment_header: + groups = comment_header.groups() + current_image = Image(groups[0], int(groups[1]), int(groups[2])) + + return images + + def export(self, path: str) -> None: + with open(path, mode="w") as f: + for image in self.images: + f.write(image.export_cpp()) + + def __eq__(self, other: object) -> bool: + if isinstance(other, self.__class__): + return self.path == other.path + else: + return False diff --git a/watchy-image-editor/image.py b/watchy-image-editor/image.py new file mode 100644 index 0000000..c78c985 --- /dev/null +++ b/watchy-image-editor/image.py @@ -0,0 +1,94 @@ +from typing import List +from math import sqrt + +from bitmap import Bitmap + + +class Image: + def __init__(self, name: str, width: int, height: int, empty: bool = False) -> None: + self.name = name + self.width = width + self.height = height + self.modified = False + if empty: + self.data = [0] * ((width * height) // 8) + self.modified = True + else: + self.data = [] + + def finalize(self) -> None: + if self.width == 0: + pixels = len(self.data) * 8 + width = int(sqrt(pixels)) + while width > 1 and pixels % width != 0: + width -= 1 + self.width = width + self.height = pixels // width + + def add_data(self, raw_data: List[str]) -> None: + for v in raw_data: + self.data += [int(v, 16)] + + def __get_position(self, x: int, y: int) -> int: + real_width = (len(self.data) * 8) // self.height + return y * real_width + x + + def get_pixel(self, x: int, y: int) -> bool: + position = self.__get_position(x, y) + chunk_id = position // 8 + try: + return self.data[chunk_id] & (1 << (7 - position % 8)) > 0 + except: + return False + + def set_pixel(self, x: int, y: int, v: bool) -> None: + position = self.__get_position(x, y) + chunk_id = position // 8 + if v != self.get_pixel(x, y): + try: + if v: + self.data[chunk_id] |= 1 << (7 - position % 8) + else: + self.data[chunk_id] &= ~(1 << (7 - position % 8)) + except: + pass + self.modified = True + + def __get_color_bytes(self) -> bytes: + output = bytes() + for y in range(self.height): + for x in range(self.width): + if self.get_pixel(x, y): + output += bytes([0, 0, 0]) + else: + output += bytes([255, 255, 255]) + return output + + def export_bmp(self, path: str) -> None: + Bitmap.write_bmp(path, self.width, 3, self.__get_color_bytes()) + + def __set_color_bytes(self, color_depth: int, data: bytes) -> None: + for y in range(self.height): + for x in range(self.width): + position = (y * self.width + x) * color_depth + colors = data[position : position + color_depth] + mean_color = sum(c for c in colors) / color_depth + if mean_color < 128: + self.set_pixel(x, y, True) + + def import_bmp(self, path: str) -> None: + self.width, self.height, color_depth, bmp_data = Bitmap.read_bmp(path) + self.data = [0] * ((self.width * self.height) // 8) + self.__set_color_bytes(color_depth, bmp_data) + + def export_cpp(self) -> str: + # 16 per line + output = [ + f"// '{self.name}', {self.width}x{self.height}px", + f"const unsigned char {self.name} [] PROGMEM = {{", + ] + while len(self.data) > 16: + output += ["\t" + ", ".join(f"0x{v:02x}" for v in self.data[0:16]) + ","] + self.data = self.data[16:] + output += ["\t" + ", ".join(f"0x{v:02x}" for v in self.data), "};", ""] + return "\n".join(output) diff --git a/watchy-image-editor/image_view.py b/watchy-image-editor/image_view.py new file mode 100644 index 0000000..f9a840c --- /dev/null +++ b/watchy-image-editor/image_view.py @@ -0,0 +1,103 @@ +import tkinter as tk +from tkinter import ttk + +from image import Image + + +class ImageView(ttk.Frame): + INITIAL_DRAW_SCALE = 3 + + def __init__(self, parent) -> None: + super().__init__(parent, height=650, width=650) + + self.draw_scale = self.INITIAL_DRAW_SCALE + + self.current_image = None + + self.canvas = tk.Canvas(self, width=0, height=0, background="white") + self.canvas.place(in_=self, anchor="c", relx=0.5, rely=0.5) + self.canvas.bind("", self.click_canvas_b1) + self.canvas.bind("", self.click_canvas_b1) + self.canvas.bind( + "", lambda event: self.update(self.current_image) + ) + self.canvas.bind("", self.click_canvas_b3) + self.canvas.bind("", self.click_canvas_b3) + self.canvas.bind( + "", lambda event: self.update(self.current_image) + ) + + self.canvas.bind("", self.zoom_canvas) + self.canvas.bind("", self.zoom_canvas_up) + self.canvas.bind("", self.zoom_canvas_down) + + self.bind("", self.zoom_canvas) + self.bind("", self.zoom_canvas_up) + self.bind("", self.zoom_canvas_down) + + def update(self, image: Image) -> None: + if self.current_image != image: + self.draw_scale = self.INITIAL_DRAW_SCALE + if image is None: + self.canvas.configure( + width=0, + height=0, + background="white", + ) + else: + try: + self.canvas.configure( + width=(image.width * self.draw_scale), + height=(image.height * self.draw_scale), + background="white", + ) + self.canvas.delete("all") + for x in range(image.width): + for y in range(image.height): + if image.get_pixel(x, y): + self.canvas.create_rectangle( + x * self.draw_scale + 1, + y * self.draw_scale + 1, + (x + 1) * self.draw_scale + 1, + (y + 1) * self.draw_scale + 1, + fill="black", + outline="", + ) + except tk.TclError: + pass + self.current_image = image + + def click_canvas_b1(self, event): + self.click_canvas(True, event) + + def click_canvas_b3(self, event): + self.click_canvas(False, event) + + def click_canvas(self, value: bool, event): + if self.current_image is None: + return + x = int(event.x / self.draw_scale) + y = int(event.y / self.draw_scale) + self.current_image.set_pixel(x, y, value) + self.canvas.create_rectangle( + x * self.draw_scale + 1, + y * self.draw_scale + 1, + (x + 1) * self.draw_scale + 1, + (y + 1) * self.draw_scale + 1, + fill=("black" if value else "white"), + outline="", + ) + + def zoom_canvas(self, event): + if event.delta > 0: + self.zoom_canvas_up() + else: + self.zoom_canvas_down() + + def zoom_canvas_up(self, event=None): + self.draw_scale *= 2 + self.update(self.current_image) + + def zoom_canvas_down(self, event=None): + self.draw_scale /= 2 + self.update(self.current_image) diff --git a/watchy-image-editor/input_popup.py b/watchy-image-editor/input_popup.py new file mode 100644 index 0000000..4ecb54c --- /dev/null +++ b/watchy-image-editor/input_popup.py @@ -0,0 +1,26 @@ +import tkinter as tk +from tkinter import ttk + + +class InputPopup(tk.Toplevel): + def __init__(self, parent, *, title: str, message: str, initial_value: str = ""): + super().__init__(parent) + self.title(title) + + self.value = None + + label = ttk.Label(self, text=message) + label.pack() + + self.entry = ttk.Entry(self) + self.entry.insert(0, initial_value) + self.entry.pack() + + button = ttk.Button(self, text="Ok", command=self.cleanup) + button.pack() + + parent.wait_window(self) + + def cleanup(self): + self.value = self.entry.get() + self.destroy() \ No newline at end of file diff --git a/watchy-image-editor/main.py b/watchy-image-editor/main.py new file mode 100644 index 0000000..30b0417 --- /dev/null +++ b/watchy-image-editor/main.py @@ -0,0 +1,9 @@ +import tkinter as tk +import os.path + +from app import App + +if __name__ == "__main__": + app = App(tk.Tk()) + + app.mainloop() diff --git a/watchy-image-editor/preview.png b/watchy-image-editor/preview.png new file mode 100644 index 0000000..4b133d3 Binary files /dev/null and b/watchy-image-editor/preview.png differ