| 1 | import tkinter as tk
|
|---|
| 2 | import tkinter.ttk as ttk
|
|---|
| 3 | from tkinter import StringVar, simpledialog, messagebox
|
|---|
| 4 | from gui.widgets.glFrame import AppOgl
|
|---|
| 5 | from typing import List
|
|---|
| 6 | from functools import partial
|
|---|
| 7 | from gui.framsutils.FramsInterface import TreeNode, InterfaceType
|
|---|
| 8 | from gui.widgets.mainTreeView import TreeView
|
|---|
| 9 | from gui.widgets.listGenePoolWindow import ListGenePoolWindow
|
|---|
| 10 | from gui.widgets.listPopulationsWindow import ListPopulationsWindow
|
|---|
| 11 | from gui.widgets.dialogBox import DirectoryDialgoBox, FileOpenDialogBox, FileSaveDialogBox
|
|---|
| 12 | from gui.widgets.ConsoleWindow import ConsoleWindow
|
|---|
| 13 | from gui.widgets.importWindow import ImportWindow
|
|---|
| 14 | from gui.widgets.propertyWindow import PropertyWindow
|
|---|
| 15 | from gui.libInterface import LibInterface
|
|---|
| 16 | from gui.socketInterface import SocketInterface
|
|---|
| 17 | from gui.utils import debounce
|
|---|
| 18 | from gui.widgets.ToolTip import CreateToolTip
|
|---|
| 19 | from time import perf_counter
|
|---|
| 20 |
|
|---|
| 21 | class MainPage(tk.Tk):
|
|---|
| 22 | OPENGL_WIDTH = 720
|
|---|
| 23 | OPENGL_HEIGHT = 480
|
|---|
| 24 |
|
|---|
| 25 | SIDEBAR_WIDTH = 400
|
|---|
| 26 | CONTROL_HEIGHT = 50
|
|---|
| 27 | OPTIONS_WIDTH = 100
|
|---|
| 28 | STATUSBAR_HEIGHT = 20
|
|---|
| 29 |
|
|---|
| 30 | OFFSET_HEIGHT = 60
|
|---|
| 31 |
|
|---|
| 32 | OPENGL_ANIMATE_DELAY = 1
|
|---|
| 33 |
|
|---|
| 34 | MENU_CONNECT_TO_SERVER = "Connect to server"
|
|---|
| 35 | MENU_CONNECT_TO_LIB = "Connect to library"
|
|---|
| 36 |
|
|---|
| 37 | WORKAROUND_TKINTER_FREEZE_BUG = True # There is a bug in tkinter that freezes whole app when dialogs are called too fast, hint: https://stackoverflow.com/questions/40666956/tkinter-hangs-on-rapidly-repeated-dialog
|
|---|
| 38 |
|
|---|
| 39 | refresh_rate_dict = {"0.1s": 100, "0.2s": 200, "0.5s": 500, "1s": 1000, "2s": 2000, "5s": 5000, "10s": 10000}
|
|---|
| 40 |
|
|---|
| 41 | #paths which can reload world and tree
|
|---|
| 42 | reload_path = ["/Experiment", "/Advanced scripting", "/World", "/User scripts"]
|
|---|
| 43 |
|
|---|
| 44 | def __init__(self, parent, networkAddress: str = None, libPath: str = None):
|
|---|
| 45 | super().__init__(parent)
|
|---|
| 46 | self.parent = parent
|
|---|
| 47 | self.protocol("WM_DELETE_WINDOW", self._dismiss)
|
|---|
| 48 | self.title("Framsticks GUI for library/server")
|
|---|
| 49 | self.option_add('*tearOff', tk.FALSE)
|
|---|
| 50 |
|
|---|
| 51 | self.listRefreshRate = 1000
|
|---|
| 52 | self.frams = None
|
|---|
| 53 | self.canStep = False #disable step while drawing
|
|---|
| 54 |
|
|---|
| 55 | #OPENGL FRAME
|
|---|
| 56 | self.frame_opengl = AppOgl(self, width=self.OPENGL_WIDTH, height=self.OPENGL_HEIGHT)
|
|---|
| 57 | self.frame_opengl.animate = self.OPENGL_ANIMATE_DELAY
|
|---|
| 58 | self.frame_opengl.bind("<Configure>", self.frame_opengl.onResize)
|
|---|
| 59 | self.frame_opengl.bind("<Motion>", self.frame_opengl.onMouseMotion)
|
|---|
| 60 | self.frame_opengl.bind("<MouseWheel>", self.frame_opengl.onScroll)
|
|---|
| 61 | self.frame_opengl.bind("<Button>", self.frame_opengl.onMouseClick)
|
|---|
| 62 | self.frame_opengl.bind("<ButtonRelease>", self.frame_opengl.onMouseRelease)
|
|---|
| 63 | self.frame_opengl.bind("<Enter>", self.frame_opengl.onMouseEnter)
|
|---|
| 64 |
|
|---|
| 65 | #SIDE FRAME
|
|---|
| 66 | frame_sidebar = tk.Frame(master=self)
|
|---|
| 67 | frame_sidebar.rowconfigure(0, weight=0)
|
|---|
| 68 | frame_sidebar.rowconfigure(1, weight=1)
|
|---|
| 69 | frame_sidebar.columnconfigure(0, weight=1)
|
|---|
| 70 |
|
|---|
| 71 | ##CONTROL PANEL
|
|---|
| 72 | frame_control_panel = tk.Frame(master=frame_sidebar, width=self.SIDEBAR_WIDTH, height=self.CONTROL_HEIGHT)
|
|---|
| 73 | frame_control_panel.columnconfigure(0, weight=1, minsize=0)
|
|---|
| 74 | frame_control_panel.columnconfigure(1, weight=1, minsize=0)
|
|---|
| 75 | frame_control_panel.columnconfigure(2, weight=1, minsize=0)
|
|---|
| 76 | frame_control_panel.columnconfigure(3, weight=1, minsize=0)
|
|---|
| 77 | frame_control_panel.rowconfigure(0, weight=1, minsize=0)
|
|---|
| 78 | frame_control_panel.grid_propagate(0)
|
|---|
| 79 |
|
|---|
| 80 | frame_control_panel_combobox = tk.Frame(master=frame_control_panel, width=int(self.SIDEBAR_WIDTH/4))
|
|---|
| 81 | frame_control_panel_combobox.rowconfigure(0, weight=1, minsize=0)
|
|---|
| 82 | frame_control_panel_combobox.rowconfigure(1, weight=1, minsize=0)
|
|---|
| 83 | frame_control_panel_combobox.columnconfigure(0, weight=1, minsize=0)
|
|---|
| 84 | frame_control_panel_combobox.grid_propagate(0)
|
|---|
| 85 | self.combobox_control_panel_fps = ttk.Combobox(master=frame_control_panel_combobox, state="readonly")
|
|---|
| 86 | self.combobox_control_panel_fps.bind("<<ComboboxSelected>>", self.FPSCbCallback)
|
|---|
| 87 | self.combobox_control_panel_refresh_rate = ttk.Combobox(master=frame_control_panel_combobox, values=list(self.refresh_rate_dict.keys()), state="readonly")
|
|---|
| 88 | self.combobox_control_panel_refresh_rate.set(next(k for k, v in self.refresh_rate_dict.items() if v == self.listRefreshRate))
|
|---|
| 89 | self.combobox_control_panel_refresh_rate.bind("<<ComboboxSelected>>", self.refreshRateCbCallback)
|
|---|
| 90 | CreateToolTip(self.combobox_control_panel_fps, "Simulation steps to show")
|
|---|
| 91 | CreateToolTip(self.combobox_control_panel_refresh_rate, "Refresh rate of gene pools and populations windows")
|
|---|
| 92 |
|
|---|
| 93 | frame_control_panel_buttons = tk.Frame(master=frame_control_panel)
|
|---|
| 94 | frame_control_panel_buttons.columnconfigure(0, weight=1, minsize=0)
|
|---|
| 95 | frame_control_panel_buttons.columnconfigure(1, weight=1, minsize=0)
|
|---|
| 96 | frame_control_panel_buttons.columnconfigure(2, weight=1, minsize=0)
|
|---|
| 97 | frame_control_panel_buttons.rowconfigure(0, weight=1)
|
|---|
| 98 | self.button_control_panel_start = tk.Button(master=frame_control_panel_buttons, text="start", command=self.controlPanelStartCommand)
|
|---|
| 99 | self.button_control_panel_stop = tk.Button(master=frame_control_panel_buttons, text="stop", command=self.controlPanelStopCommand)
|
|---|
| 100 | self.button_control_panel_step = tk.Button(master=frame_control_panel_buttons, text="step", command=self.controlPanelStepCommand)
|
|---|
| 101 | self.button_control_panel_start["state"] = tk.DISABLED
|
|---|
| 102 | self.button_control_panel_stop["state"] = tk.DISABLED
|
|---|
| 103 | self.button_control_panel_step["state"] = tk.DISABLED
|
|---|
| 104 | self.button_control_panel_start.grid(row=0, column=0, sticky="NSEW")
|
|---|
| 105 | self.button_control_panel_stop.grid(row=0, column=1, sticky="NSEW")
|
|---|
| 106 | self.button_control_panel_step.grid(row=0, column=2, sticky="NSEW")
|
|---|
| 107 | self.combobox_control_panel_fps.grid(row=0, column=0, sticky="NSEW")
|
|---|
| 108 | self.combobox_control_panel_refresh_rate.grid(row=1, column=0, sticky="NSEW")
|
|---|
| 109 | frame_control_panel_combobox.grid(row=0, column=0, sticky="NSEW")
|
|---|
| 110 | frame_control_panel_buttons.grid(row=0, column=1, columnspan=3, sticky="NSEW")
|
|---|
| 111 | frame_control_panel.grid(row=0, column=0, sticky="NSEW")
|
|---|
| 112 |
|
|---|
| 113 | ##TREEVIEW
|
|---|
| 114 | frame_treeview = tk.Frame(master=frame_sidebar, width=self.SIDEBAR_WIDTH, height=self.OPENGL_HEIGHT - self.CONTROL_HEIGHT)
|
|---|
| 115 | frame_treeview.columnconfigure(0, weight=1)
|
|---|
| 116 | frame_treeview.columnconfigure(1, weight=0)
|
|---|
| 117 | frame_treeview.rowconfigure(0, weight=1)
|
|---|
| 118 | frame_treeview.rowconfigure(1, weight=0)
|
|---|
| 119 |
|
|---|
| 120 | self.treeview_treeview = TreeView(master=frame_treeview, iconPath="gui/res/icons/", selectmode="browse")
|
|---|
| 121 | scrollbar_treeview = ttk.Scrollbar(master=frame_treeview, orient=tk.VERTICAL, command=self.treeview_treeview.yview)
|
|---|
| 122 | self.treeview_treeview.configure(yscrollcommand=scrollbar_treeview.set)
|
|---|
| 123 | self.treeview_treeview.bind("<Double-1>", self.onTreeViewDoubleClick)
|
|---|
| 124 | button_treeviewRefresh = tk.Button(master=frame_treeview, text="Refresh", command=self.refreshInfoTreeCommand)
|
|---|
| 125 |
|
|---|
| 126 | self.treeview_treeview.grid(row=0, column=0, sticky="NSEW")
|
|---|
| 127 | scrollbar_treeview.grid(row=0, column=1, sticky="NSEW")
|
|---|
| 128 | button_treeviewRefresh.grid(row=1, column=0, sticky="NSEW")
|
|---|
| 129 | frame_treeview.grid(row=1, column=0, sticky="NSEW")
|
|---|
| 130 |
|
|---|
| 131 | #STATUS BAR
|
|---|
| 132 | self.motd_text = StringVar("")
|
|---|
| 133 | label_statusbar_motd = tk.Label(self, textvariable=self.motd_text, bd=1, height=1, relief=tk.SUNKEN, anchor=tk.W)
|
|---|
| 134 |
|
|---|
| 135 | #MENU BAR
|
|---|
| 136 | menu = tk.Menu(self)
|
|---|
| 137 | self.menu_open = tk.Menu(menu, tearoff=0)
|
|---|
| 138 | self.menu_open.add_command(label=self.MENU_CONNECT_TO_SERVER, command=self.menuConnectServerCommand)
|
|---|
| 139 | self.menu_open.add_command(label=self.MENU_CONNECT_TO_LIB, command=self.menuConnectLibCommand)
|
|---|
| 140 | self.menu_open.add_command(label="Disconnect", command=self.menuDisconnectCommand)
|
|---|
| 141 | self.menu_open.entryconfig("Disconnect", state="disabled")
|
|---|
| 142 | self.menu_open.add_separator()
|
|---|
| 143 | self.menu_open.add_command(label="Exit", command=self.menuExitCommand)
|
|---|
| 144 | menu.add_cascade(label="Main", menu=self.menu_open)
|
|---|
| 145 | self.menu_file = tk.Menu(menu, tearoff=0)
|
|---|
| 146 | self.menu_file.add_command(label="Load", command=self.menuFileLoadCommand, state="disabled")
|
|---|
| 147 | self.menu_file.add_command(label="Import", command=self.menuFileImportCommand, state="disabled")
|
|---|
| 148 | self.menu_file.add_command(label="Save experiment state as...", command=self.menuFileSaveESCommand, state="disabled")
|
|---|
| 149 | self.menu_file.add_command(label="Save genotypes as...", command=self.menuFileSaveGCommand, state="disabled")
|
|---|
| 150 | self.menu_file.add_command(label="Save simulator parameters as...", command=self.menuFileSaveSPCommand, state="disabled")
|
|---|
| 151 | menu.add_cascade(label="File", menu=self.menu_file)
|
|---|
| 152 | self.menu_options = tk.Menu(menu, tearoff=0)
|
|---|
| 153 | self.menu_options.add_command(label="Console", command=self.menuConsoleCommand, state="disabled")
|
|---|
| 154 | self.menu_options.add_command(label="Refresh world", command=self.refreshWorld, state="disabled")
|
|---|
| 155 | self.enableColorsVar = tk.BooleanVar(value=False)
|
|---|
| 156 | self.enableColorsVar.trace_add("write", self.menuEnableColorsCommand)
|
|---|
| 157 | self.menu_options.add_checkbutton(label="Enable colors", onvalue=True, offvalue=False, variable=self.enableColorsVar)
|
|---|
| 158 | self.menu_options.add_command(label="Restore windows", command=self.menuRestoreWindowsCommand)
|
|---|
| 159 | menu.add_cascade(label="Options", menu=self.menu_options)
|
|---|
| 160 | self.config(menu=menu)
|
|---|
| 161 |
|
|---|
| 162 | #WINDOW
|
|---|
| 163 | self.columnconfigure(0, weight=1)
|
|---|
| 164 | self.columnconfigure(1, weight=0)
|
|---|
| 165 | self.rowconfigure(0, weight=1)
|
|---|
| 166 | self.rowconfigure(1, weight=0)
|
|---|
| 167 | self.frame_opengl.grid(row=0, column=0, sticky="NSEW")
|
|---|
| 168 | frame_sidebar.grid(row=0, column=1, sticky="NSEW")
|
|---|
| 169 | label_statusbar_motd.grid(row=1, column=0, columnspan=2, sticky="NSEW")
|
|---|
| 170 |
|
|---|
| 171 | #ORGANIZE WINDOWS POSITIONS
|
|---|
| 172 | ## need to do some workaround to determine screen width and height
|
|---|
| 173 | self.attributes("-alpha", 0)
|
|---|
| 174 | self.state('zoomed')
|
|---|
| 175 | self.update()
|
|---|
| 176 | maxHeight = self.winfo_rooty() + self.winfo_height()
|
|---|
| 177 | maxWidth = self.winfo_rootx() + self.winfo_width()
|
|---|
| 178 | self.rootx = self.winfo_rootx()
|
|---|
| 179 | self.state("normal")
|
|---|
| 180 | self.update()
|
|---|
| 181 | self.geometry("%dx%d+%d+%d" % (self.OPENGL_WIDTH + self.SIDEBAR_WIDTH, self.OPENGL_HEIGHT, self.rootx, 0))
|
|---|
| 182 | self.attributes("-alpha", 1)
|
|---|
| 183 | self.update()
|
|---|
| 184 | height = self.winfo_rooty() + self.winfo_height() - self.winfo_y()
|
|---|
| 185 |
|
|---|
| 186 | self.list_gene_pool_window_height = int((maxHeight - height) / 2)
|
|---|
| 187 | self.list_gene_pool_window_pos_y = height
|
|---|
| 188 | self.list_gene_pool_window = ListGenePoolWindow(self, self.rootx, self.list_gene_pool_window_pos_y, self.list_gene_pool_window_height, self.listRefreshRate, self.frame_opengl.read_creature_semaphore, self.frams)
|
|---|
| 189 | height2 = self.list_gene_pool_window.winfo_rooty() + self.list_gene_pool_window.winfo_height() - self.list_gene_pool_window.winfo_y()
|
|---|
| 190 | self.list_populations_window_height = int((maxHeight - height) / 2)
|
|---|
| 191 | self.list_populations_window_pos_y = height + height2
|
|---|
| 192 | self.list_populations_window = ListPopulationsWindow(self, self.rootx, self.list_populations_window_pos_y, self.list_populations_window_height, self.listRefreshRate, self.frame_opengl.read_creature_semaphore, self.frams)
|
|---|
| 193 |
|
|---|
| 194 | self.bind("<FocusIn>", self.on_focus_in)
|
|---|
| 195 | self.bind("<FocusOut>", self.on_focus_out)
|
|---|
| 196 |
|
|---|
| 197 | self.fps = 50
|
|---|
| 198 | self.c_steps = 1
|
|---|
| 199 |
|
|---|
| 200 | if networkAddress and not libPath:
|
|---|
| 201 | self.menuConnectServerCommand(networkAddress)
|
|---|
| 202 | elif libPath and not networkAddress:
|
|---|
| 203 | self.menuConnectLibCommand(libPath)
|
|---|
| 204 |
|
|---|
| 205 | self.sock_adr = "127.0.0.1:9009"
|
|---|
| 206 | self.lib_adr = "D:\\Framsticks50rc25\\"
|
|---|
| 207 |
|
|---|
| 208 | self.FPSCbCallback(None)
|
|---|
| 209 |
|
|---|
| 210 | #step counter
|
|---|
| 211 | self.nb_frames = 0
|
|---|
| 212 | self.c_time = perf_counter()
|
|---|
| 213 | self.sps = 0
|
|---|
| 214 |
|
|---|
| 215 | def _dismiss(self):
|
|---|
| 216 | """dismiss main window, close all connections, release all resources."""
|
|---|
| 217 | if self.frams:
|
|---|
| 218 | self.frams.disconnect()
|
|---|
| 219 |
|
|---|
| 220 | self.destroy()
|
|---|
| 221 |
|
|---|
| 222 | def on_focus_in(self, event):
|
|---|
| 223 | """restart rendering on focusing on main window."""
|
|---|
| 224 | self.frame_opengl.animate = self.OPENGL_ANIMATE_DELAY
|
|---|
| 225 | self.frame_opengl.tkExpose(None)
|
|---|
| 226 |
|
|---|
| 227 | def on_focus_out(self, event):
|
|---|
| 228 | """stop the rendering when main window lose focus."""
|
|---|
| 229 | self.frame_opengl.animate = 0
|
|---|
| 230 |
|
|---|
| 231 | def menuConnectServerCommand(self, adr: str = None):
|
|---|
| 232 | """on "connect to server" button command."""
|
|---|
| 233 | #if connection started from command argument
|
|---|
| 234 | if adr:
|
|---|
| 235 | address = adr
|
|---|
| 236 | #else ask for server address
|
|---|
| 237 | else:
|
|---|
| 238 | address = simpledialog.askstring("Server address", "Address", parent=self, initialvalue=self.sock_adr)
|
|---|
| 239 | if address:
|
|---|
| 240 | ip, port = address.split(":")
|
|---|
| 241 | try:
|
|---|
| 242 | self.frams = SocketInterface()
|
|---|
| 243 | self.frams.registerRunningChangeEventCallback(self.refreshControlPanelButtons)
|
|---|
| 244 | self.frams.registerTreeviewRefreshEventCallback(self.refreshInfoTree)
|
|---|
| 245 | self.frams.connect(ip, int(port)) #try to connect to server
|
|---|
| 246 | self.sock_adr = address
|
|---|
| 247 | except ConnectionError: #if connection cannot be established
|
|---|
| 248 | messagebox.showerror(message="Cannot connect to server")
|
|---|
| 249 | if self.frams:
|
|---|
| 250 | self.frams.disconnect()
|
|---|
| 251 | self.frams = None
|
|---|
| 252 |
|
|---|
| 253 | #if connected successfully
|
|---|
| 254 | if self.frams and self.frams.frams.comm.connected:
|
|---|
| 255 | self._connect(address)
|
|---|
| 256 |
|
|---|
| 257 | def menuConnectLibCommand(self, path: str = None):
|
|---|
| 258 | """on "connect to library" button command."""
|
|---|
| 259 | #if connection started from command argument
|
|---|
| 260 | if path:
|
|---|
| 261 | address = path
|
|---|
| 262 | #else ask for library path
|
|---|
| 263 | else:
|
|---|
| 264 | address = DirectoryDialgoBox("Framsticks library path", "Path", parent=self, initialvalue=self.lib_adr)
|
|---|
| 265 | address = address.result
|
|---|
| 266 | if address:
|
|---|
| 267 | try:
|
|---|
| 268 | self.frams = LibInterface()
|
|---|
| 269 | self.frame_opengl.onDraw = self.onDraw
|
|---|
| 270 | self.prev_run = False
|
|---|
| 271 | self.frams.connect(address, 0)
|
|---|
| 272 | self.lib_adr = address
|
|---|
| 273 | except ConnectionError:
|
|---|
| 274 | messagebox.showerror(message="Cannot find Framsticks library")
|
|---|
| 275 |
|
|---|
| 276 | self._connect(address)
|
|---|
| 277 |
|
|---|
| 278 | def _connect(self, address):
|
|---|
| 279 | """set all control's states if connected."""
|
|---|
| 280 | if address and self.frams:
|
|---|
| 281 | #set creatures read callback
|
|---|
| 282 | self.frame_opengl.frams_readCreatures = self.frams.readCreatures
|
|---|
| 283 | #prepare populations and gene pools windows
|
|---|
| 284 | self.list_populations_window.frams = self.frams
|
|---|
| 285 | self.list_gene_pool_window.frams = self.frams
|
|---|
| 286 | self.list_populations_window.refresh = True
|
|---|
| 287 | self.list_gene_pool_window.refresh = True
|
|---|
| 288 | self.list_populations_window.refreshPopulations()
|
|---|
| 289 | self.list_gene_pool_window.refreshGenePools()
|
|---|
| 290 | #enable control buttons
|
|---|
| 291 | self.button_control_panel_start["state"] = tk.NORMAL
|
|---|
| 292 | self.button_control_panel_stop["state"] = tk.NORMAL
|
|---|
| 293 | self.button_control_panel_step["state"] = tk.NORMAL
|
|---|
| 294 | self.refreshInfoTree()
|
|---|
| 295 | self.motd_text.set(self.frams.getMotd())
|
|---|
| 296 | #setup all menus
|
|---|
| 297 | self.menu_open.entryconfig("Disconnect", state="normal")
|
|---|
| 298 | self.menu_open.entryconfig(self.MENU_CONNECT_TO_SERVER, state="disabled")
|
|---|
| 299 | self.menu_open.entryconfig(self.MENU_CONNECT_TO_LIB, state="disabled")
|
|---|
| 300 | self.menu_file.entryconfig("Load", state="normal")
|
|---|
| 301 | self.menu_file.entryconfig("Import", state="normal")
|
|---|
| 302 | self.menu_file.entryconfig("Save experiment state as...", state="normal")
|
|---|
| 303 | self.menu_file.entryconfig("Save genotypes as...", state="normal")
|
|---|
| 304 | self.menu_file.entryconfig("Save simulator parameters as...", state="normal")
|
|---|
| 305 | self.menu_options.entryconfig("Refresh world", state="normal")
|
|---|
| 306 |
|
|---|
| 307 | if self.frams.interfaceType == InterfaceType.SOCKET:
|
|---|
| 308 | self.menu_options.entryconfig("Console", state="normal")
|
|---|
| 309 | else:
|
|---|
| 310 | self.menu_options.entryconfig("Console", state="disabled")
|
|---|
| 311 |
|
|---|
| 312 | self.fps_values = self.frams.getFPSDefinitions()
|
|---|
| 313 | def mapper(fps, step):
|
|---|
| 314 | return "Every{}".format(", {} fps".format(fps) if fps > 0 else "") if step == 1 else "1:{}".format(step)
|
|---|
| 315 | self.fps_mapvalues = [mapper(fps, step) for fps, step in self.fps_values]
|
|---|
| 316 | init_val = mapper(self.fps, self.c_steps)
|
|---|
| 317 | self.combobox_control_panel_fps['values'] = self.fps_mapvalues
|
|---|
| 318 | self.combobox_control_panel_fps.set(init_val)
|
|---|
| 319 |
|
|---|
| 320 | self.FPSCbCallback(None)
|
|---|
| 321 |
|
|---|
| 322 | self.refreshWorld()
|
|---|
| 323 | self.canStep = True #enable step while drawing
|
|---|
| 324 |
|
|---|
| 325 | def menuDisconnectCommand(self):
|
|---|
| 326 | """set all control's states if disconnected."""
|
|---|
| 327 | if self.frams:
|
|---|
| 328 | self.canStep = False
|
|---|
| 329 | self.frams.disconnect()
|
|---|
| 330 | self.button_control_panel_start["state"] = tk.DISABLED
|
|---|
| 331 | self.button_control_panel_stop["state"] = tk.DISABLED
|
|---|
| 332 | self.button_control_panel_step["state"] = tk.DISABLED
|
|---|
| 333 | self.list_populations_window.refresh = False
|
|---|
| 334 | self.list_gene_pool_window.refresh = False
|
|---|
| 335 | self.list_populations_window.clearList()
|
|---|
| 336 | self.list_gene_pool_window.clearList()
|
|---|
| 337 | self.frame_opengl.frams_readCreatures = None
|
|---|
| 338 | self.frame_opengl.onDraw = lambda: None
|
|---|
| 339 | self.refreshInfoTree()
|
|---|
| 340 | self.frams = None
|
|---|
| 341 | self.frame_opengl.swap_buffer.clear()
|
|---|
| 342 | self.motd_text.set("")
|
|---|
| 343 | self.menu_open.entryconfig("Disconnect", state="disabled")
|
|---|
| 344 | self.menu_open.entryconfig(self.MENU_CONNECT_TO_SERVER, state="normal")
|
|---|
| 345 | self.menu_open.entryconfig(self.MENU_CONNECT_TO_LIB, state="normal")
|
|---|
| 346 | self.menu_options.entryconfig("Console", state="disabled")
|
|---|
| 347 | self.menu_file.entryconfig("Load", state="disabled")
|
|---|
| 348 | self.menu_file.entryconfig("Import", state="disabled")
|
|---|
| 349 | self.menu_file.entryconfig("Save experiment state as...", state="disabled")
|
|---|
| 350 | self.menu_file.entryconfig("Save genotypes as...", state="disabled")
|
|---|
| 351 | self.menu_file.entryconfig("Save simulator parameters as...", state="disabled")
|
|---|
| 352 | self.menu_options.entryconfig("Refresh world", state="disabled")
|
|---|
| 353 |
|
|---|
| 354 | def menuExitCommand(self):
|
|---|
| 355 | self._dismiss()
|
|---|
| 356 |
|
|---|
| 357 | def FPSCbCallback(self, event):
|
|---|
| 358 | """handle fps change."""
|
|---|
| 359 | value = self.combobox_control_panel_fps.get()
|
|---|
| 360 | if value:
|
|---|
| 361 | i = self.fps_mapvalues.index(value)
|
|---|
| 362 | fps, step = self.fps_values[i]
|
|---|
| 363 | self.fps = fps
|
|---|
| 364 | self.c_steps = step
|
|---|
| 365 |
|
|---|
| 366 | if fps == -1:
|
|---|
| 367 | self.frame_opengl.animate = self.OPENGL_ANIMATE_DELAY = 1
|
|---|
| 368 | self.frame_opengl.REFRESH_RATE = 0.001 #set to "as fast as possible" because how often redrawing is called is decided by the timer
|
|---|
| 369 | else:
|
|---|
| 370 | self.frame_opengl.animate = self.OPENGL_ANIMATE_DELAY = int(1000 / fps)
|
|---|
| 371 | self.frame_opengl.REFRESH_RATE = 1 / fps
|
|---|
| 372 |
|
|---|
| 373 | def refreshRateCbCallback(self, event):
|
|---|
| 374 | """handle change of refresh rate of gene pools and populations windows."""
|
|---|
| 375 | value = self.combobox_control_panel_refresh_rate.get()
|
|---|
| 376 | if value:
|
|---|
| 377 | self.listRefreshRate = self.refresh_rate_dict[value]
|
|---|
| 378 | self.list_gene_pool_window.refreshRate = self.listRefreshRate
|
|---|
| 379 | self.list_populations_window.refreshRate = self.listRefreshRate
|
|---|
| 380 |
|
|---|
| 381 | def controlPanelStartCommand(self):
|
|---|
| 382 | if self.frams:
|
|---|
| 383 | self.frams.start()
|
|---|
| 384 |
|
|---|
| 385 | def controlPanelStopCommand(self):
|
|---|
| 386 | if self.frams:
|
|---|
| 387 | self.frams.stop()
|
|---|
| 388 | self.motd_text.set("")
|
|---|
| 389 |
|
|---|
| 390 | def refreshControlPanelButtons(self, is_running: bool):
|
|---|
| 391 | if is_running:
|
|---|
| 392 | self.button_control_panel_start["state"] = tk.DISABLED
|
|---|
| 393 | self.button_control_panel_stop["state"] = tk.NORMAL
|
|---|
| 394 | self.button_control_panel_step["state"] = tk.DISABLED
|
|---|
| 395 | else:
|
|---|
| 396 | self.button_control_panel_start["state"] = tk.NORMAL
|
|---|
| 397 | self.button_control_panel_stop["state"] = tk.DISABLED
|
|---|
| 398 | self.button_control_panel_step["state"] = tk.NORMAL
|
|---|
| 399 |
|
|---|
| 400 | def controlPanelStepCommand(self):
|
|---|
| 401 | """hangle step button."""
|
|---|
| 402 | if self.frams: #if connected to frams
|
|---|
| 403 | if self.frams.interfaceType == InterfaceType.LIB:
|
|---|
| 404 | self.canStep = False #disable step while drawing
|
|---|
| 405 | run = self.frams.getSimulationStatus()
|
|---|
| 406 | self.frams.start()
|
|---|
| 407 | self.frams.step()
|
|---|
| 408 | if not run:
|
|---|
| 409 | self.frams.stop()
|
|---|
| 410 | self.canStep = True #enable step while drawing
|
|---|
| 411 | else:
|
|---|
| 412 | self.canStep = False #disable step while drawing
|
|---|
| 413 | self.frams.step()
|
|---|
| 414 | self.canStep = True #enable step while drawing
|
|---|
| 415 |
|
|---|
| 416 | def refreshInfoTreeCommand(self):
|
|---|
| 417 | self.refreshInfoTree()
|
|---|
| 418 | self.refreshWorld()
|
|---|
| 419 |
|
|---|
| 420 | @debounce(1)
|
|---|
| 421 | def refreshInfoTree(self):
|
|---|
| 422 | """refresh info tree
|
|---|
| 423 | debounce decorator prevents refreshing too often."""
|
|---|
| 424 | self.treeview_treeview.delete(*self.treeview_treeview.get_children())
|
|---|
| 425 | if self.frams:
|
|---|
| 426 | tree: List[TreeNode] = self.frams.makeInfoTree()
|
|---|
| 427 | self._recRefreshInfoTree(tree, True)
|
|---|
| 428 |
|
|---|
| 429 | def _recRefreshInfoTree(self, node: TreeNode, open: bool = False):
|
|---|
| 430 | #self.treeview_treeview.insert("" if not node.parent else node.parent.node.Id, index="end", iid=node.node.Id, text=node.node.Name)
|
|---|
| 431 | self.treeview_treeview.insert(self._generateInfoTreeParent(node), index="end", iid=self._generateInfoTreeId(node), text=node.node.p["name"], ico=node.node.p["id"], open=open)
|
|---|
| 432 | for child in node.children:
|
|---|
| 433 | self._recRefreshInfoTree(child)
|
|---|
| 434 |
|
|---|
| 435 | def _generateInfoTreeParent(self, node: TreeNode):
|
|---|
| 436 | if not node.parent:
|
|---|
| 437 | return ""
|
|---|
| 438 | return self._generateInfoTreeId(node.parent)
|
|---|
| 439 |
|
|---|
| 440 | def _generateInfoTreeId(self, node: TreeNode):
|
|---|
| 441 | """generate unique id for info tree using paths and ids."""
|
|---|
| 442 | response = node.node.p["id"]
|
|---|
| 443 | while node.parent:
|
|---|
| 444 | if node.parent:
|
|---|
| 445 | if node.parent.node.p["id"] != '/':
|
|---|
| 446 | response = node.parent.node.p["id"] + "/" + response
|
|---|
| 447 | else:
|
|---|
| 448 | response = node.parent.node.p["id"] + response
|
|---|
| 449 | node = node.parent
|
|---|
| 450 | return response
|
|---|
| 451 |
|
|---|
| 452 | def onTreeViewDoubleClick(self, event):
|
|---|
| 453 | if self.frams:
|
|---|
| 454 | item = self.treeview_treeview.identify_row(event.y)
|
|---|
| 455 | if item:
|
|---|
| 456 | info = partial(self.frams.readParameterDetails, item)
|
|---|
| 457 | update = partial(self.frams.writeParameterDetail, item)
|
|---|
| 458 |
|
|---|
| 459 | if any(item.lower().endswith(x.lower()) for x in self.reload_path):
|
|---|
| 460 | pw = PropertyWindow(self, item, self.rootx, info, update, self.frams.getError, self.frams, self.frame_opengl.read_creature_semaphore, self.refreshWorld)
|
|---|
| 461 | else:
|
|---|
| 462 | pw = PropertyWindow(self, item, self.rootx, info, update, self.frams.getError, self.frams, self.frame_opengl.read_creature_semaphore)
|
|---|
| 463 | return 'break'
|
|---|
| 464 |
|
|---|
| 465 | def onDraw(self):
|
|---|
| 466 | """on draw callback, only for frams library."""
|
|---|
| 467 | if self.frams:
|
|---|
| 468 | run = self.frams.getSimulationStatus()
|
|---|
| 469 | if run != self.prev_run: #check if simulation status changed since last onDraw
|
|---|
| 470 | self.refreshControlPanelButtons(run) #if so, refresh command buttons
|
|---|
| 471 | self.prev_run = run
|
|---|
| 472 | if run:
|
|---|
| 473 | if self.canStep: #if running and can step, perform N steps
|
|---|
| 474 | with self.frame_opengl.read_creature_semaphore:
|
|---|
| 475 | for i in range(self.c_steps):
|
|---|
| 476 | self.frams.step()
|
|---|
| 477 |
|
|---|
| 478 | #calculate steps per second
|
|---|
| 479 | c_time = perf_counter()
|
|---|
| 480 | self.nb_frames += 1
|
|---|
| 481 | e_time = c_time - self.c_time
|
|---|
| 482 | if e_time >= 1.0:
|
|---|
| 483 | self.sps = int(self.nb_frames / e_time)
|
|---|
| 484 | self.c_time = perf_counter()
|
|---|
| 485 | self.nb_frames = 0
|
|---|
| 486 | self.motd_text.set("{} steps/second".format(self.sps))
|
|---|
| 487 | error = self.frams.getError() #check for errors, not used anymore
|
|---|
| 488 | if error:
|
|---|
| 489 | messagebox.showerror("Framsticks error", error)
|
|---|
| 490 |
|
|---|
| 491 | def menuConsoleCommand(self):
|
|---|
| 492 | if self.frams.interfaceType == InterfaceType.SOCKET:
|
|---|
| 493 | ConsoleWindow(self, self.frams.frams)
|
|---|
| 494 | else:
|
|---|
| 495 | self.menu_options.entryconfig("Console", state="disabled")
|
|---|
| 496 |
|
|---|
| 497 | def menuFileLoadCommand(self):
|
|---|
| 498 | if self.frams:
|
|---|
| 499 | self.canStep = False
|
|---|
| 500 | path = FileOpenDialogBox("Open File", "Path", parent=self, initialvalue=self.lib_adr, filetypes=[("Framsticks files", "*.expt;*.gen;*.sim"), ("Experiment state", "*.expt"), ("Genotypes", "*.gen"), ("Simulator parameters", "*.sim"), ("All files", "*.*")])
|
|---|
| 501 | path = path.result
|
|---|
| 502 | if path:
|
|---|
| 503 | #acquire read creature semaphore, so render thread could not ask for them, while they are changing
|
|---|
| 504 | with self.frame_opengl.read_creature_semaphore:
|
|---|
| 505 | self.frams.loadFile(path)
|
|---|
| 506 | self.refreshWorld()
|
|---|
| 507 | self.canStep = True
|
|---|
| 508 |
|
|---|
| 509 | def menuFileImportCommand(self):
|
|---|
| 510 | if self.frams:
|
|---|
| 511 | self.canStep = False
|
|---|
| 512 | path = FileOpenDialogBox("Open File", "Path", parent=self, init_val=self.lib_adr, filetypes=[("Framsticks files", "*.expt;*.gen;*.sim"), ("Experiment state", "*.expt"), ("Genotypes", "*.gen"), ("Simulator parameters", "*.sim"), ("All files", "*.*")])
|
|---|
| 513 | path = path.result
|
|---|
| 514 | if path:
|
|---|
| 515 | #acquire read creature semaphore, so render thread could not ask for them, while they are changing
|
|---|
| 516 | with self.frame_opengl.read_creature_semaphore:
|
|---|
| 517 | def handle_import_options():
|
|---|
| 518 | options = ImportWindow(parent=self, filename=path)
|
|---|
| 519 | options = options.result
|
|---|
| 520 | self.frams.importFile(path, options)
|
|---|
| 521 | if self.WORKAROUND_TKINTER_FREEZE_BUG:
|
|---|
| 522 | self.after(1, handle_import_options)
|
|---|
| 523 | else:
|
|---|
| 524 | handle_import_options()
|
|---|
| 525 |
|
|---|
| 526 | self.canStep = True
|
|---|
| 527 |
|
|---|
| 528 | def menuFileSaveESCommand(self):
|
|---|
| 529 | if self.frams:
|
|---|
| 530 | path = FileSaveDialogBox("Save experiment state to:", "Path", parent=self, initialvalue=self.lib_adr, filetypes=[("Experiment state", "*.expt")])
|
|---|
| 531 | path = path.result
|
|---|
| 532 | if path:
|
|---|
| 533 | if path.split(".")[-1] != "expt":
|
|---|
| 534 | path += ".expt"
|
|---|
| 535 | self.frams.saveFile(path, 1)
|
|---|
| 536 |
|
|---|
| 537 | def menuFileSaveGCommand(self):
|
|---|
| 538 | if self.frams:
|
|---|
| 539 | path = FileSaveDialogBox("Save genotypes to:", "Path", parent=self, initialvalue=self.lib_adr, filetypes=[("Genotypes", "*.gen")])
|
|---|
| 540 | path = path.result
|
|---|
| 541 | if path:
|
|---|
| 542 | if path.split(".")[-1] != "gen":
|
|---|
| 543 | path += ".gen"
|
|---|
| 544 | self.frams.saveFile(path, 2)
|
|---|
| 545 |
|
|---|
| 546 | def menuFileSaveSPCommand(self):
|
|---|
| 547 | if self.frams:
|
|---|
| 548 | path = FileSaveDialogBox("Save simulator parameters to:", "Path", parent=self, initialvalue=self.lib_adr, filetypes=[("Simulator parameters", "*.sim")])
|
|---|
| 549 | path = path.result
|
|---|
| 550 | if path:
|
|---|
| 551 | if path.split(".")[-1] != "sim":
|
|---|
| 552 | path += ".sim"
|
|---|
| 553 | self.frams.saveFile(path, 4)
|
|---|
| 554 |
|
|---|
| 555 | def refreshWorld(self):
|
|---|
| 556 | """refresh world parameters and shape."""
|
|---|
| 557 | worldType = self.frams.getWorldType()
|
|---|
| 558 | worldSize = self.frams.getWorldSize()
|
|---|
| 559 | worldWaterLevel = self.frams.getWorldWaterLevel()
|
|---|
| 560 | worldBoundaries = self.frams.getWorldBoundaries()
|
|---|
| 561 | worldMap = self.frams.getWorldMap()
|
|---|
| 562 | simType = self.frams.getSimtype()
|
|---|
| 563 | self.frame_opengl.reloadWorld(worldType, simType, worldSize, worldMap, worldBoundaries, worldWaterLevel)
|
|---|
| 564 |
|
|---|
| 565 | def menuEnableColorsCommand(self, *_):
|
|---|
| 566 | v = self.enableColorsVar.get()
|
|---|
| 567 | self.frame_opengl.frams_readCreatures_color = v
|
|---|
| 568 |
|
|---|
| 569 | def menuRestoreWindowsCommand(self):
|
|---|
| 570 | self.geometry("%dx%d+%d+%d" % (self.OPENGL_WIDTH + self.SIDEBAR_WIDTH, self.OPENGL_HEIGHT, self.rootx, 0))
|
|---|
| 571 |
|
|---|
| 572 | if self.list_gene_pool_window.opened:
|
|---|
| 573 | self.list_gene_pool_window._dismiss()
|
|---|
| 574 | self.list_gene_pool_window = ListGenePoolWindow(self, self.rootx, self.list_gene_pool_window_pos_y, self.list_gene_pool_window_height, self.listRefreshRate, self.frame_opengl.read_creature_semaphore, self.frams)
|
|---|
| 575 | if self.frams:
|
|---|
| 576 | self.list_gene_pool_window.frams = self.frams
|
|---|
| 577 | self.list_gene_pool_window.refresh = True
|
|---|
| 578 | self.list_gene_pool_window.refreshGenePools()
|
|---|
| 579 |
|
|---|
| 580 | if self.list_populations_window.opened:
|
|---|
| 581 | self.list_populations_window._dismiss()
|
|---|
| 582 | self.list_populations_window = ListPopulationsWindow(self, self.rootx, self.list_populations_window_pos_y, self.list_populations_window_height, self.listRefreshRate, self.frame_opengl.read_creature_semaphore, self.frams)
|
|---|
| 583 | if self.frams:
|
|---|
| 584 | self.list_populations_window.frams = self.frams
|
|---|
| 585 | self.list_populations_window.refresh = True
|
|---|
| 586 | self.list_populations_window.refreshPopulations() |
|---|