class Classifier(tk.Frame): """ A GUI component that displays an image and allows users to classify the image as one of four colours. """ def __init__(self, master, callback: Callable[[int], None], bg: str = "#ffffff"): """ Initialise this Classifier :param master: parent container of this Classifier :param callback: function to call when the ColourSelector sub-component of this Classifier is clicked :param bg: the background colour of this Classifier """ super().__init__(master, bg=bg, padx=5) self._callback = callback self._preview = Preview(self, REF_CANVAS_WIDTH, REF_CANVAS_HEIGHT) self._preview.pack(side=tk.TOP) self._selector = ColourSelector( self, ["#000000", "#555555", "#aaaaaa", "#ffffff"], ["#ffffff", "#aaaaaa", "#555555", "#000000"], self._callback, bg=bg) self._selector.pack(side=tk.TOP) self._img = None def display_image(self, arr: np.ndarray): """ Display an image on the Preview component of this Classifier. :param arr: the image to display """ self._preview.display_image(arr) def get_selector(self) -> ColourSelector: """ :return: the ColourSelector component of this Classifier. """ return self._selector def display_no_image_warning(self) -> None: """ Display a warning that no reference image is loaded. """ self._preview.delete(tk.ALL) self._preview.create_text(REF_CANVAS_WIDTH / 2, REF_CANVAS_HEIGHT / 2, text="Please load a reference image\n " "by going to File > Load Reference")
class PopRevApp(object): """ The Picture of Picture Reverser application. """ def __init__(self, master, bg: str = BACKGROUND_COLOUR): """ Initialise the application. :param master: parent container of this application :param bg: background colour of this application """ self._master = master self._bg = bg self._file_menu = None self._poprev = PopRev() self._x = 0 self._y = 0 self._classifier = None self._preview = None self._navigator = None self._setup_menu() self._setup_view() self.refresh_components() def _smart_ask_save_before_doing(self, do_fn: Callable[[], None], title: str, message: str) -> None: """ If the current drawing has unsaved changes, ask the user if they would like to save before performing the action specified by do_fn. Otherwise, just execute do_fn. :param do_fn: function to execute :param title: title of dialog prompting save :param message: save prompt message """ if self._poprev.unsaved_changes(): ask_save_before_doing(lambda: self._smart_save(), do_fn, title, message) else: do_fn() def _setup_view(self) -> None: """ Set up components of the GUI """ self._navigator = Navigator(self._master, self.handle_move_callback, self.handle_jump_callback, bg=self._bg) self._navigator.pack(side=tk.TOP) frame = tk.Frame(self._master, bg=self._bg) frame.pack(side=tk.TOP) self._classifier = Classifier(frame, self.handle_select_callback, bg=self._bg) self._classifier.pack(side=tk.LEFT) self._preview = Preview(frame, PREVIEW_WIDTH, PREVIEW_HEIGHT, callback=self._handle_preview_click) self._preview.pack(side=tk.LEFT) self._master.config(bg=self._bg) self._master.title("poprev") self._master.protocol("WM_DELETE_WINDOW", self._exit) def _exit(self) -> None: """ Exit the application after confirmation """ title = "Save Before Exiting?" message = "Would you like to save this drawing before exiting?" self._smart_ask_save_before_doing(lambda: self._master.destroy(), title, message) def _setup_menu(self) -> None: """ Set up the menu bar """ menu_bar = tk.Menu(self._master) self._master.config(menu=menu_bar) file_menu = tk.Menu(menu_bar) menu_bar.add_cascade(label="File", menu=file_menu) reference_menu = tk.Menu(file_menu) file_menu.add_cascade(label="Reference", menu=reference_menu) drawing_menu = tk.Menu(file_menu) file_menu.add_cascade(label="Drawing", menu=drawing_menu) reference_menu.add_command(label="Load Reference", command=self.load_reference) drawing_menu.add_command(label="New Drawing", command=self.try_new_drawing) drawing_menu.add_command(label="Load Drawing", command=self.try_load_drawing) drawing_menu.add_command(label="Save Drawing", command=self._smart_save) drawing_menu.add_command(label="Save Drawing As", command=self.save_drawing_as) drawing_menu.add_command(label="Export Drawing", command=self.export_drawing) self._file_menu = file_menu def _smart_save(self) -> None: """ If the current drawing has a save file name, save it to that file. Otherwise, bring up the save as dialog. """ if self._poprev.get_save_name() is None: self.save_drawing_as() else: self.save_drawing() def _handle_preview_click(self, x: int, y: int) -> None: """ Handle the event when the drawing preview is clicked. :param x: the x coordinate that was clicked :param y: the y coordinate that was clicked """ jumpx = int(x * DRAW_WIDTH / PREVIEW_WIDTH) jumpy = int(y * DRAW_HEIGHT / PREVIEW_HEIGHT) self.jump_to(jumpx, jumpy) def load_reference(self) -> None: """ Load a reference image """ filename = filedialog.askopenfilename( title="Open Reference Image", filetypes=(("jpeg files", "*.jpg"), ("jpeg files", "*.jpeg"), ("png files", "*.png"))) if filename != "": self._poprev.load_reference(filename) self.refresh_components() def next_pixel(self) -> None: """ Go to the 'next' pixel i.e. right neighbour of current pixel, or left-most pixel in the next row down if the former does not apply (or go back to top left if current is bottom right) """ self._x = (self._x + 1) % DRAW_WIDTH if self._x == 0: self._y = (self._y + 1) % DRAW_HEIGHT self.refresh_components() def move_to(self, direction: str) -> None: """ Move to the next pixel in the specified direction. :param direction: the direction to move """ if direction == "up": self._y = (self._y - 1) % DRAW_HEIGHT elif direction == "right": self._x = (self._x + 1) % DRAW_WIDTH elif direction == "down": self._y = (self._y + 1) % DRAW_HEIGHT elif direction == "left": self._x = (self._x - 1) % DRAW_WIDTH self.refresh_components() def jump_to(self, x: int, y: int) -> None: """ Jump to the pixel specified by the given (x, y) coordinate :param x: x coordinate of position to jump to :param y: y coordinate of position to jump to """ self._x = x self._y = y self.refresh_components() def refresh_components(self) -> None: """ Refresh appearance of GUI components """ self.refresh_selector() self.refresh_preview() self.refresh_title() self.refresh_navigator() def refresh_preview(self) -> None: """ Refresh the appearance of the drawing preview """ self._preview.display_image(self._poprev.get_preview(self._x, self._y)) if self._poprev.has_reference(): image = self._poprev.get_ref_context(self._x, self._y, REF_CANVAS_WIDTH, REF_CANVAS_HEIGHT, 1) self._classifier.display_image(image) else: self._classifier.display_no_image_warning() def refresh_selector(self) -> None: """ Refresh the appearance of the colour selector """ selector = self._classifier.get_selector() colour = self._poprev.get_selection(self._x, self._y) if not (0 <= colour < 4): colour = None selector.set_selected(colour) def refresh_navigator(self) -> None: """ Refresh the appearance of the navigator """ self._navigator.display_position(self._x + 1, self._y + 1) def handle_select_callback(self, identifier: int) -> None: """ Handle the event when a colour is selected :param identifier: the id of the colour that was selected """ self._poprev.edit_drawing(self._x, self._y, identifier) self.next_pixel() def export_drawing(self) -> None: """ Display a dialog allowing user to export the current drawing as an image. """ filename = filedialog.asksaveasfilename(title="Export Drawing", filetypes=(("png files", "*.png"), )) if filename != "": self._poprev.export_drawing(filename) def save_drawing(self) -> None: """ Save changes to the current drawing. """ self._poprev.save_drawing() self.refresh_title() def save_drawing_as(self) -> None: """ Display a dialog allowing the user to save the current drawing to a particular file """ filename = filedialog.asksaveasfilename(title="Save Drawing As", filetypes=(("poprev drawing", ".poprev"), )) if filename != "": self._poprev.save_drawing_as(filename) self.refresh_title() def load_drawing(self) -> None: """ Display a dialog allowing the user to load a drawing """ filename = filedialog.askopenfilename(title="Select Drawing", filetypes=(("poprev drawing", ".poprev"), )) if filename != "": self._poprev.load_drawing(filename) self.refresh_components() def try_load_drawing(self) -> None: """ If there are unsaved changes to the current drawing, ask the user if they would like to save before loading another drawing. """ title = "Save Before Loading?" message = "Would you like to save this drawing before loading " \ "another?" self._smart_ask_save_before_doing(lambda: self.load_drawing(), title, message) def new_drawing(self) -> None: """ Clear the current drawing and start a new one. """ self._poprev.new_drawing() self.refresh_components() def try_new_drawing(self) -> None: """ If there are unsaved changes to the current drawing, ask the user if they would like to save before starting a new drawing. """ title = "Save Before Starting Over?" message = "Would you like to save this drawing before starting " \ "another?" self._smart_ask_save_before_doing(lambda: self.new_drawing(), title, message) def handle_move_callback(self, direction: str) -> None: """ Handle the event when one of the up, down, left, or right buttons in the navigator are clicked. :param direction: the direction that was clicked """ self.move_to(direction) def handle_jump_callback(self, x: int, y: int) -> None: """ Handle the event when the jump button of the navigator is clicked. :param x: the x coordinate to jump to :param y: the y coordinate to jump to """ self.jump_to(x - 1, y - 1) def refresh_title(self) -> None: """ Refresh the title of the application window """ unsaved_changes = "" if self._poprev.unsaved_changes(): unsaved_changes = "*" name = "untitled" if self._poprev.get_save_name() is not None: name = self._poprev.get_save_name() self._master.title("poprev ({}){}".format(name, unsaved_changes))