class Gui(tk.Tk): """A custom graphical user interface for the GameOfLife Class First working Iteration() at 10:08Pm 2nd July 2008""" def __init__(self, parent=None): tk.Tk.__init__(self, parent) self.parent = parent self.initialise() def initialise(self): self.title('Game Of Life') self.grid() # leave at defaults for a 2 dimensional game of life with # John Conway standard rules (3/23) self.gol = GameOfLife() self.create_widgets() self.bindings() self.reset() self.reset_colour() def reset(self, event=None): print('Reset') self.go_now: bool = False self.change_go_now(False) self.side = 5 self.gap = 1 self.invert_zoom = False self.zoom_increment = 1 self.gol.reset() self.reset_origin_click() self.display() self.display_going_to_string() @property def side(self) -> int: return self._side @side.setter def side(self, side: int) -> None: self._side = max(int(side), minimum_size) def reset_colour(self) -> None: self.change_foreground(default_foreground_colour) self.change_background(default_background_colour) def key(self, event) -> None: if event.keysym in self.key_dict: self.key_dict[event.keysym]() def close_program(self) -> None: self.change_go_now(False) self.destroy() def create_widgets(self) -> None: self.v_go_stop = tk.StringVar() f_cont = tk.Frame(self) tk.Button( f_cont, text='Reset', command=self.reset).pack(side=tk.LEFT) tk.Button( f_cont, text='Iterate', command=self.iterate).pack(side=tk.LEFT) tk.Button( f_cont, textvariable=self.v_go_stop, command=self.go_stop).pack(side=tk.LEFT) tk.Button( f_cont, text='Reset Colour', command=self.reset_colour).pack(side=tk.LEFT) tk.Button( f_cont, text=colour_type_to_change_string(True), command=self.change_foreground).pack(side=tk.LEFT) tk.Button( f_cont, text=colour_type_to_change_string(False), command=self.change_background).pack(side=tk.LEFT) tk.Button( f_cont, text='Load Cells', command=self.load_cells).pack(side=tk.LEFT) tk.Button( f_cont, text='Save Cells', command=self.save_cells).pack(side=tk.LEFT) f_cont.grid(sticky=tk.W) # put canvas in same GUI rather than #master = Tk() (new one) self.cnvs = tk.Canvas(bd=0) self.cnvs.grid(row=1, column=0, columnspan=2, sticky=tk.NSEW) self.rowconfigure(1, weight=1) self.columnconfigure(1, weight=1) def bindings(self) -> None: self.protocol('WM_DELETE_WINDOW', self.close_program) self.key_dict: Dict[str, Callable[..., None]] = { 'Escape': self.close_program, 'Delete': self.reset, 'r': self.reset, 'Return': self.iterate, 'i': self.iterate, 'g': self.go, 's': self.stop, 'space': self.go_stop, 'f': self.change_foreground, 'b': self.change_background, # 'Left': '', # 'Right': '', # 'Up': '', # 'Down': '', } self.bind('<Key>', self.key) self.bind('<F2>', self.reset) self.bind('<Control-o>', self.load_cells) # 'O' for Open. self.bind('<Control-l>', self.load_cells) # 'L' for Load. self.bind('<Control-s>', self.save_cells) self.cnvs.bind('<Button-1>', self.place_point_click) # left mouse self.cnvs.bind('<B1-Motion>', self.place_point_drag) self.cnvs.bind('<Button-2>', self.reset_origin_click) # right mouse self.cnvs.bind('<Button-3>', self.change_origin_click) self.cnvs.bind('<B3-Motion>', self.change_origin_drag) self.bind('<MouseWheel>', self.mouse_wheel_zoom) # mouse wheel self.bind('<Button-4>', self.mouse_wheel_zoom) self.bind('<Button-5>', self.mouse_wheel_zoom) self.cnvs.bind('<Configure>', self.canvas_resize) def point_dim_to_cell_dim(self, i: int) -> int: return int(i // (self.side + self.gap)) def point_to_cell(self, *point): '''Internal method to Convert a Point on the canvas to a Tuple refering to the Cell it is in. ''' canvased_points = ( self.cnvs.canvasx(point[0]), self.cnvs.canvasy(point[1])) return tuple(map(self.point_dim_to_cell_dim, canvased_points)) def place_point_click(self, event): '''Using normal variable rather than object one stops doubling up of turning on and off or vice versa of a single point (re-entrancy). ''' point_place = self.point_to_cell(event.x, event.y) self.gol.toggle_cell(point_place) self.point_place = point_place # store the value so all those in any directly subsequent # place_point_drag will use it self.point_place_value = self.gol.is_cell_alive(point_place) self.display() def place_point_drag(self, event) -> None: '''Assumes that PlacePointClick sets self.point otherwise in setup it must be set to "= None". ''' point_place = self.point_to_cell(event.x, event.y) if not point_place == self.point_place: # remove final argument to replace the value of all cells # rather than keep value self.gol.set_cell(point_place, self.point_place_value) self.point_place = point_place self.display() def reset_origin_click(self, event=None) -> None: '''Default scroll to 0,0 in top left. ''' self.cnvs.config(scrollregion=( 0, 0, self.cnvs.cget('width'), self.cnvs.cget('height'), )) def change_origin_click(self, event) -> None: self.point_scroll = ( self.cnvs.canvasx(event.x), self.cnvs.canvasy(event.y)) def change_origin_drag(self, event) -> None: # for the first two remove float part and convert to integer cur_scroll = ( floor(float(value)) for value in self.cnvs.cget('scrollregion').split()[:2]) to_scroll = ( self.point_scroll[0] - self.cnvs.canvasx(event.x), self.point_scroll[1] - self.cnvs.canvasy(event.y)) going_scroll = tuple(map(add, cur_scroll, to_scroll)) self.cnvs.config(scrollregion=( going_scroll[0], going_scroll[1], going_scroll[0] + int(self.cnvs.cget('width')), going_scroll[1] + int(self.cnvs.cget('height')), )) self.point_scroll = ( self.cnvs.canvasx(event.x), self.cnvs.canvasy(event.y)) def zoom_step(self) -> int: return bool_to_plus_minus_one(not self.invert_zoom) * \ self.zoom_increment def mouse_wheel_zoom(self, event) -> None: '''Respond to Linux (event.num) or Windows (event.delta) wheel events. Zooms in and out. Focused on (0,0) i.e the original top left corner place. ''' delta = 120 zoom_step = self.zoom_step() if event.num == 4 or event.delta == delta: # mouse wheel up self.side += zoom_step elif event.num == 5 or event.delta == -delta: # mouse wheel down self.side -= zoom_step self.display() def cell_to_point(self, end, *cell): return tuple([ coordinate * (self.side + self.gap) + (end * self.side) for coordinate in cell]) def place_cell_on_canvas(self, cell) -> None: self.cnvs.create_rectangle( self.cell_to_point(False, *cell), self.cell_to_point(True, *cell), fill=self.foreground_tk_colour, width=0) def display(self) -> None: '''Deletes any relevant contents of the canvas called "cnvs" replacing them with an updated view of the cells. the canvas is always double-buffered. Just create or modify canvas items as usual. When Tk returns to the event loop, the canvas is redrawn as soon as possible. http://mail.python.org/pipermail/python-list/2001-March/073778.html ''' self.cnvs.delete(tk.ALL) list(map(self.place_cell_on_canvas, self.gol())) def iterate(self) -> None: self.change_go_now(False) self.gol.iterate() self.display() def go(self) -> None: if not self.go_now: self.go_stop() def stop(self) -> None: if self.go_now: self.go_stop() def go_stop(self) -> None: '''Must be implemented as a separate thread or check GUI for events at set intervals otherwise locks up. ''' self.change_go_now() while self.go_now: self.gol.iterate() self.display() self.cnvs.update() # time.sleep(.1) def display_going_to_string(self) -> None: self.v_go_stop.set(going_to_string(self.go_now)) def change_go_now(self, new_value: Optional[bool] = None) -> None: if new_value is None: self.go_now = not self.go_now else: self.go_now = bool(new_value) self.display_going_to_string() def ensure_colour(self, colour: Optional[int] = None) -> int: if colour is None: colour = colourutils.random_colour() return colour def print_colour(self, colour: int, is_foreground: bool) -> None: print('%s Colour Changed To: %s' % ( colour_type_to_string(is_foreground), colourutils.standard_hex_colour_padded(colour))) def change_foreground(self, colour: Optional[int] = None) -> None: '''If Foreground colour is unspecified a random one will be chosen. ''' colour = self.ensure_colour(colour) tk_colour = colourutils.tk_hex_colour_padded(colour) self.print_colour(colour, is_foreground=True) self.cnvs.itemconfig(tk.ALL, fill=tk_colour) # storing choices self.foreground = colour self.foreground_tk_colour = tk_colour def change_background(self, colour: Optional[int] = None) -> None: '''If Background colour is unspecified a random one will be chosen. ''' colour = self.ensure_colour(colour) tk_colour = colourutils.tk_hex_colour_padded(colour) self.print_colour(colour, is_foreground=False) self.cnvs.config(bg=tk_colour) # storing choices self.background = colour self.background_tk_colour = tk_colour def load_cells(self, event=None) -> None: try: cells = cellfile.load(file_cell_path) except IOError: print('File "%s" not found.' % file_cell_path) else: self.gol.cells = cells print('Loaded from file "%s"' % file_cell_path) self.display() def save_cells(self, event=None) -> None: try: cellfile.save(file_cell_path, self.gol.cells) except IOError: print('Unable to save to file "%s".' % file_cell_path) else: print('Saved to file "%s".' % file_cell_path) def canvas_resize(self, event) -> None: '''Change the size stored in the canvas when the window is resized so ChangeOriginDrag works properly. ''' self.cnvs['width'], self.cnvs['height'] = event.width-4, event.height-4