Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/editor/schematic.py: 97%

515 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-05 17:29 +0100

1#------------------------------------------------------------------------------------ 

2# This Module provides all the internal functions for editing the layout schematic 

3# in terms of adding/removing objects, drag/drop objects, copy/paste objects etc 

4#------------------------------------------------------------------------------------ 

5# 

6# External API functions intended for use by other editor modules: 

7# initialise(root, callback, width, height, grid, snap) - Call once on startup 

8# update_canvas(width,height,grid,snap) - Call following a size update (or layout load/canvas resize) 

9# delete_all_objects() - To delete all objects for layout 'new' and layout 'load' 

10# enable_editing() - Call when 'Edit' Mode is selected (via toolbar or on load) 

11# disable_editing() - Call when 'Run' Mode is selected (via toolbar or on load) 

12# 

13# Makes the following external API calls to other editor modules: 

14# objects.initialise (canvas,width,height,grid) - Initialise the objects package and set defaults 

15# objects.update_canvas(width,height,grid) - update the attributes (on layout load or canvas re-size) 

16# objects.create_object(obj, type, subtype) - Create a default object on the schematic 

17# objects.delete_objects(list of obj IDs) - Delete the selected objects from the canvas 

18# objects.rotate_objects(list of obj IDs) - Rotate the selected objects on the canvas 

19# objects.copy_objects(list of obj IDs) - Copy the selected objects to the clipboard 

20# objects.paste_objects() - Paste the selected objects (returns a list of new IDs) 

21# objects.undo() / objects.redo() - Undo and re-do functions as you would expect 

22# objects.enable_editing() - Call when 'Edit' Mode is selected  

23# objects.disable_editing() - Call when 'Run' Mode is selected 

24# configure_signal.edit_signal(root,object_id) - Open signal edit window (on double click) 

25# configure_point.edit_point(root,object_id) - Open point edit window (on double click) 

26# configure_section.edit_section(root,object_id) - Open section edit window (on double click) 

27# configure_instrument.edit_instrument(root,object_id) - Open inst edit window (on double click) 

28# configure_line.edit_line(root,object_id) - Open line edit window (on double click) 

29# configure_textbox.edit_textbox(root,object_id) - Open textbox edit window (on double click) 

30# configure_track_sensor.edit_track_sensor(root,object_id) - Open the edit window (on double click) 

31# 

32# Accesses the following external editor objects directly: 

33# objects.schematic_objects - the dict holding descriptions for all objects 

34# objects.object_type - used to establish the type of the schematic objects 

35# 

36# Accesses the following external library objects directly: 

37# signals_common.sig_type - Used to access the signal type 

38# signals_colour_lights.signal_sub_type - Used to access the signal subtype 

39# signals_semaphores.semaphore_sub_type - Used to access the signal subtype 

40# signals_ground_position.ground_pos_sub_type - Used to access the signal subtype 

41# signals_ground_disc.ground_disc_sub_type - Used to access the signal subtype 

42# block_instruments.instrument_type - Used to access the block_instrument type 

43# points.point_type - Used to access the point type 

44# 

45#------------------------------------------------------------------------------------ 

46 

47import tkinter as Tk 

48 

49from ..library import signals_common 

50from ..library import signals_colour_lights 

51from ..library import signals_semaphores 

52from ..library import signals_ground_position 

53from ..library import signals_ground_disc 

54from ..library import block_instruments 

55from ..library import points 

56 

57from . import objects 

58from . import configure_signal 

59from . import configure_point 

60from . import configure_section 

61from . import configure_instrument 

62from . import configure_line 

63from . import configure_textbox 

64from . import configure_track_sensor 

65 

66import importlib.resources 

67import math 

68import copy 

69 

70#------------------------------------------------------------------------------------ 

71# Global variables used to track the current selections/state of the Schematic Editor 

72#------------------------------------------------------------------------------------ 

73 

74# The schematic_state dict holds the current schematic editor status 

75schematic_state:dict = {} 

76schematic_state["startx"] = 0 

77schematic_state["starty"] = 0 

78schematic_state["lastx"] = 0 

79schematic_state["lasty"] = 0 

80schematic_state["moveobjects"] = False 

81schematic_state["editlineend1"] = False 

82schematic_state["editlineend2"] = False 

83schematic_state["selectarea"] = False 

84schematic_state["selectareabox"] = None # Tkinter drawing object 

85schematic_state["selectedobjects"] = [] 

86# The Root reference is used when calling a "configure object" module (to open a popup window) 

87# The Canvas reference is used for configuring and moving canvas widgets for schematic editing 

88# canvas_width / canvas_height / canvas_grid are used for positioning of objects. 

89root = None 

90canvas = None 

91canvas_width = 0 

92canvas_height = 0 

93canvas_grid = 0 

94canvas_grid_state = "normal" 

95canvas_snap_to_grid = True 

96# The callback to make (for selected canvas events) - Mode change, toggle Snap to Grid etc 

97# event makes this callback (to enable the application mode to be toggled between edit and run) 

98canvas_event_callback = None 

99# The following Tkinter objects are also treated as global variables as they need to remain 

100# "in scope" for the schematic editor functions (i.e. so they don't get garbage collected) 

101# The two popup menus (for right click on the canvas or a schematic object) 

102popup1 = None 

103popup2 = None 

104# This global reference to the popup edit window class is maintained for test purposes 

105edit_popup = None 

106# The Frame holding the "add object" buttons (for pack/forget on enable/disable editing) 

107button_frame = None 

108# The list of button images (which needs to be kept in scope) 

109button_images = [] 

110 

111#------------------------------------------------------------------------------------ 

112# Internal Function to return the absolute canvas coordinates for an event 

113# (which take into account any canvas scroll bar offsets) 

114#------------------------------------------------------------------------------------ 

115 

116def canvas_coordinates(event): 

117 canvas = event.widget 

118 x = canvas.canvasx(event.x) 

119 y = canvas.canvasy(event.y) 

120 return(x, y) 

121 

122#------------------------------------------------------------------------------------ 

123# Internal Function to draw (or redraw) the grid on the screen (after re-sizing) 

124# Uses the global canvas_width, canvas_height, canvas_grid variables 

125#------------------------------------------------------------------------------------ 

126 

127def draw_grid(): 

128 # Note we leave the 'state' of the grid unchanged when re-drawing 

129 # As the 'state' is set (normal or hidden) when enabling/disabling editing 

130 canvas.delete("grid") 

131 canvas.create_rectangle(0, 0, canvas_width, canvas_height, outline='#999', fill="", tags="grid", state=canvas_grid_state) 

132 for i in range(0, canvas_height, canvas_grid): 

133 canvas.create_line(0,i,canvas_width,i,fill='#999',tags="grid",state=canvas_grid_state) 

134 for i in range(0, canvas_width, canvas_grid): 

135 canvas.create_line(i,0,i,canvas_height,fill='#999',tags="grid",state=canvas_grid_state) 

136 # Push the grid to the back (behind any drawing objects) 

137 canvas.tag_lower("grid") 

138 return() 

139 

140#------------------------------------------------------------------------------------ 

141# Internal function to create an object (and make it the only selected object) 

142#------------------------------------------------------------------------------------ 

143 

144def create_object(new_object_type, item_type=None, item_subtype=None): 

145 deselect_all_objects() 

146 object_id = objects.create_object(new_object_type, item_type, item_subtype) 

147 select_object(object_id) 

148 return() 

149 

150#------------------------------------------------------------------------------------ 

151# Internal function to select an object (adding to the list of selected objects) 

152#------------------------------------------------------------------------------------ 

153 

154def select_object(object_id): 

155 global schematic_state 

156 # Add the specified object to the list of selected objects 

157 schematic_state["selectedobjects"].append(object_id) 

158 # Highlight the item to show it has been selected 

159 if objects.schematic_objects[object_id]["item"] == objects.object_type.line: 

160 canvas.itemconfigure(objects.schematic_objects[object_id]["end1"],state="normal") 

161 canvas.itemconfigure(objects.schematic_objects[object_id]["end2"],state="normal") 

162 else: 

163 canvas.itemconfigure(objects.schematic_objects[object_id]["bbox"],state="normal") 

164 return() 

165 

166#------------------------------------------------------------------------------------ 

167# Internal function to deselect an object (removing from the list of selected objects) 

168#------------------------------------------------------------------------------------ 

169 

170def deselect_object(object_id): 

171 global schematic_state 

172 # remove the specified object from the list of selected objects 

173 schematic_state["selectedobjects"].remove(object_id) 

174 # Remove the highlighting to show it has been de-selected 

175 if objects.schematic_objects[object_id]["item"] == objects.object_type.line: 

176 canvas.itemconfigure(objects.schematic_objects[object_id]["end1"],state="hidden") 

177 canvas.itemconfigure(objects.schematic_objects[object_id]["end2"],state="hidden") 

178 else: 

179 canvas.itemconfigure(objects.schematic_objects[object_id]["bbox"],state="hidden") 

180 return() 

181 

182#------------------------------------------------------------------------------------ 

183# Internal function to select all objects on the layout schematic 

184#------------------------------------------------------------------------------------ 

185 

186def select_all_objects(event=None): 

187 global schematic_state 

188 # Clear out the list of selected objects first 

189 schematic_state["selectedobjects"] = [] 

190 for object_id in objects.schematic_objects: 

191 select_object(object_id) 

192 return() 

193 

194#------------------------------------------------------------------------------------ 

195# Internal function to deselect all objects (clearing the list of selected objects) 

196#------------------------------------------------------------------------------------ 

197 

198def deselect_all_objects(event=None): 

199 selections = copy.deepcopy(schematic_state["selectedobjects"]) 

200 for object_id in selections: 

201 deselect_object(object_id) 

202 return() 

203 

204#------------------------------------------------------------------------------------ 

205# Internal function to delete all objects (for layout 'load' and layout 'new') 

206#------------------------------------------------------------------------------------ 

207 

208def delete_all_objects(): 

209 global schematic_state 

210 # Select and delete all objects (also clear the selected objects list) 

211 select_all_objects() 

212 # Delete the objects from the schematic 

213 objects.delete_objects(schematic_state["selectedobjects"],initialise_layout_after_delete=False) 

214 # Remove the objects from the list of selected objects 

215 schematic_state["selectedobjects"]=[] 

216 # Belt and braces delete of all canvas objects as I've seen issues when 

217 # running the system tests (probably because I'm not using the mainloop) 

218 canvas.delete("all") 

219 # Set the select area box to 'None' so it gets created on first use 

220 schematic_state["selectareabox"] = None 

221 return() 

222 

223#------------------------------------------------------------------------------------ 

224# Internal function to edit an object configuration (double-click and popup menu) 

225# Only a single Object will be selected when this function is called 

226#------------------------------------------------------------------------------------ 

227 

228def edit_selected_object(): 

229 global edit_popup 

230 object_id = schematic_state["selectedobjects"][0] 

231 if objects.schematic_objects[object_id]["item"] == objects.object_type.line: 

232 edit_popup = configure_line.edit_line(root, object_id) 

233 elif objects.schematic_objects[object_id]["item"] == objects.object_type.textbox: 

234 edit_popup = configure_textbox.edit_textbox(root, object_id) 

235 elif objects.schematic_objects[object_id]["item"] == objects.object_type.signal: 

236 edit_popup = configure_signal.edit_signal(root, object_id) 

237 elif objects.schematic_objects[object_id]["item"] == objects.object_type.point: 

238 edit_popup = configure_point.edit_point(root, object_id) 

239 elif objects.schematic_objects[object_id]["item"] == objects.object_type.section: 

240 edit_popup = configure_section.edit_section(root,object_id) 

241 elif objects.schematic_objects[object_id]["item"] == objects.object_type.instrument: 

242 edit_popup = configure_instrument.edit_instrument(root,object_id) 

243 elif objects.schematic_objects[object_id]["item"] == objects.object_type.track_sensor: 243 ↛ 245line 243 didn't jump to line 245, because the condition on line 243 was never false

244 edit_popup = configure_track_sensor.edit_track_sensor(root,object_id) 

245 return() 

246 

247# The following function is for test purposes only - to close the windows opened above by the system tests 

248 

249def close_edit_window (ok:bool=False, cancel:bool=False, apply:bool=False, reset:bool=False): 

250 global edit_popup 

251 if ok: edit_popup.save_state(close_window=True) 

252 elif apply: edit_popup.save_state(close_window=False) 

253 elif cancel: edit_popup.close_window() 

254 elif reset: edit_popup.load_state() 

255 

256#------------------------------------------------------------------------------------ 

257# Internal function to snap all selected objects to the grid ('s' keypress). We do 

258# this an object at a time as each object may require different offsets to be applied. 

259# Note that for lines, we need to snap each end to the grid seperately because the 

260# line ends may have been previously edited with snap-to-grid disabled. If we just 

261# snap the line to the grid, line end 2 may still be off the grid. 

262#------------------------------------------------------------------------------------ 

263 

264def snap_selected_objects_to_grid(event=None): 

265 for object_id in schematic_state["selectedobjects"]: 

266 posx = objects.schematic_objects[object_id]["posx"] 

267 posy = objects.schematic_objects[object_id]["posy"] 

268 xdiff1,ydiff1 = snap_to_grid(posx,posy, force_snap=True) 

269 if objects.schematic_objects[object_id]["item"] == objects.object_type.line: 

270 posx = objects.schematic_objects[object_id]["endx"] 

271 posy = objects.schematic_objects[object_id]["endy"] 

272 xdiff2,ydiff2 = snap_to_grid(posx,posy, force_snap=True) 

273 move_line_end_1(object_id,xdiff1,ydiff1) 

274 objects.move_objects([object_id],xdiff1=xdiff1,ydiff1=ydiff1) 

275 move_line_end_2(object_id,xdiff2,ydiff2) 

276 objects.move_objects([object_id],xdiff2=xdiff2,ydiff2=ydiff2) 

277 else: 

278 canvas.move(objects.schematic_objects[object_id]["tags"],xdiff1,ydiff1) 

279 canvas.move(objects.schematic_objects[object_id]["bbox"],xdiff1,ydiff1) 

280 objects.move_objects([object_id],xdiff1=xdiff1, ydiff1=ydiff1, xdiff2=xdiff1, ydiff2=ydiff1) 

281 return() 

282 

283#------------------------------------------------------------------------------------ 

284# Internal function to nudge all selected objects (move on arrow keys). Note that we 

285# Throttle the key-repeat (user holding the arrow key down) so Tkinter can keep up. 

286# If nothing is selected, then we scroll the canvas instead 

287#------------------------------------------------------------------------------------ 

288 

289def nudge_selected_objects(event=None): 

290 disable_arrow_keypress_events() 

291 # Move the selected objects or scroll the canvas if nothing selected 

292 if schematic_state["selectedobjects"] == []: 

293 canvas.focus_set() 

294 canvas.config(xscrollincrement=canvas_grid, yscrollincrement=canvas_grid) 

295 if event.keysym == 'Left': canvas.xview_scroll(-1, "units") 

296 if event.keysym == 'Right': canvas.xview_scroll(1, "units") 

297 if event.keysym == 'Up': canvas.yview_scroll(-1, "units") 

298 if event.keysym == 'Down': canvas.yview_scroll(1, "units") 

299 else: 

300 if canvas_snap_to_grid: delta = canvas_grid 

301 else: delta = 1 

302 if event.keysym == 'Left': xdiff, ydiff = -delta, 0 

303 if event.keysym == 'Right': xdiff, ydiff = delta, 0 

304 if event.keysym == 'Up': xdiff, ydiff = 0, -delta 

305 if event.keysym == 'Down': xdiff, ydiff = 0, delta 

306 move_selected_objects(xdiff,ydiff) 

307 objects.move_objects(schematic_state["selectedobjects"],xdiff1=xdiff, ydiff1=ydiff, xdiff2=xdiff, ydiff2=ydiff) 

308 canvas.after(50,enable_arrow_keypress_events) 

309 return() 

310 

311def enable_arrow_keypress_events(event=None): 

312 canvas.bind('<KeyPress-Left>',nudge_selected_objects) 

313 canvas.bind('<KeyPress-Right>',nudge_selected_objects) 

314 canvas.bind('<KeyPress-Up>',nudge_selected_objects) 

315 canvas.bind('<KeyPress-Down>',nudge_selected_objects) 

316 return() 

317 

318def disable_arrow_keypress_events(event=None): 

319 canvas.unbind('<KeyPress-Left>') 

320 canvas.unbind('<KeyPress-Right>') 

321 canvas.unbind('<KeyPress-Up>') 

322 canvas.unbind('<KeyPress-Down>') 

323 return() 

324 

325#------------------------------------------------------------------------------------ 

326# Internal function to edit a line object on the canvas. The "editlineend1" and 

327# "editlineend2" dictionary elements specify the line end that needs to be moved 

328# Only a single Object will be selected when this function is called 

329#------------------------------------------------------------------------------------ 

330 

331def update_end_stops(object_id): 

332 # Update the line end stops using the common function provided by the objects sub-package 

333 x1,y1,x2,y2 = canvas.coords(objects.schematic_objects[object_id]["line"]) 

334 dx,dy = objects.get_endstop_offsets(x1,y1,x2,y2) 

335 canvas.coords(objects.schematic_objects[object_id]["stop1"],x1+dx,y1+dy,x1-dx,y1-dy) 

336 canvas.coords(objects.schematic_objects[object_id]["stop2"],x2+dx,y2+dy,x2-dx,y2-dy) 

337 return() 

338 

339def move_line_end_1(object_id, xdiff:int,ydiff:int): 

340 # Move the tkinter selection circle for the 'start' of the line 

341 canvas.move(objects.schematic_objects[object_id]["end1"],xdiff,ydiff) 

342 # Update the line coordinates to reflect the changed 'start' position 

343 end2x = objects.schematic_objects[object_id]["endx"] 

344 end2y = objects.schematic_objects[object_id]["endy"] 

345 x1,y1,x2,y2 = canvas.coords(objects.schematic_objects[object_id]["end1"]) 

346 canvas.coords(objects.schematic_objects[object_id]["line"],(x1+x2)/2,(y1+y2)/2,end2x,end2y) 

347 # Update the position of the line end stops to reflect the new line geometry 

348 update_end_stops(object_id) 

349 return() 

350 

351def move_line_end_2(object_id, xdiff:int,ydiff:int): 

352 # Move the tkinter selection circle for the 'end' of the line 

353 canvas.move(objects.schematic_objects[object_id]["end2"],xdiff,ydiff) 

354 # Update the line coordinates to reflect the changed 'end' position 

355 end1x = objects.schematic_objects[object_id]["posx"] 

356 end1y = objects.schematic_objects[object_id]["posy"] 

357 x1,y1,x2,y2 = canvas.coords(objects.schematic_objects[object_id]["end2"]) 

358 canvas.coords(objects.schematic_objects[object_id]["line"],end1x,end1y,(x1+x2)/2,(y1+y2)/2) 

359 # Update the position of the line end stops to reflect the new line geometry 

360 update_end_stops(object_id) 

361 return() 

362 

363#------------------------------------------------------------------------------------ 

364# Internal function to move all selected objects on the canvas 

365#------------------------------------------------------------------------------------ 

366 

367def move_selected_objects(xdiff:int,ydiff:int): 

368 for object_id in schematic_state["selectedobjects"]: 

369 # All drawing objects should be "tagged" apart from the bbox 

370 canvas.move(objects.schematic_objects[object_id]["tags"],xdiff,ydiff) 

371 canvas.move(objects.schematic_objects[object_id]["bbox"],xdiff,ydiff) 

372 return() 

373 

374#------------------------------------------------------------------------------------ 

375# Internal function to Delete all selected objects (delete/backspace and popup menu) 

376#------------------------------------------------------------------------------------ 

377 

378def delete_selected_objects(event=None): 

379 global schematic_state 

380 # Delete the objects from the schematic 

381 objects.delete_objects(schematic_state["selectedobjects"]) 

382 # Remove the objects from the list of selected objects 

383 schematic_state["selectedobjects"]=[] 

384 return() 

385 

386#------------------------------------------------------------------------------------ 

387# Internal function to Rotate all selected Objects ('r' key and popup menu) 

388#------------------------------------------------------------------------------------ 

389 

390def rotate_selected_objects(event=None): 

391 objects.rotate_objects(schematic_state["selectedobjects"]) 

392 return() 

393 

394#------------------------------------------------------------------------------------ 

395# Internal function to Copy selected objects to the clipboard (Cntl-c and popup menu) 

396#------------------------------------------------------------------------------------ 

397 

398def copy_selected_objects(event=None): 

399 objects.copy_objects(schematic_state["selectedobjects"]) 

400 return() 

401 

402#------------------------------------------------------------------------------------ 

403# Internal function to paste previously copied objects (Cntl-V and popup menu) 

404#------------------------------------------------------------------------------------ 

405 

406def paste_clipboard_objects(event=None): 

407 # Paste the objects and re-copy (for a subsequent paste) 

408 list_of_new_object_ids = objects.paste_objects() 

409 objects.copy_objects(list_of_new_object_ids) 

410 # Select the pasted objects (in case the user wants to paste again) 

411 deselect_all_objects() 

412 for object_id in list_of_new_object_ids: 

413 select_object(object_id) 

414 return() 

415 

416#------------------------------------------------------------------------------------ 

417# Internal function to return the ID of the Object the cursor is "highlighting" 

418# Returns the UUID of the highlighted item add details of the highlighted element 

419# Main = (True,False), Secondary = (False, True), All = (True, True) 

420#------------------------------------------------------------------------------------ 

421 

422def find_highlighted_object(xpos:int,ypos:int): 

423 # We do this in a certain order - objects first then lines so we don't 

424 # select a line that is under the object the user is trying to select 

425 for object_id in objects.schematic_objects: 

426 bbox = canvas.coords(objects.schematic_objects[object_id]["bbox"]) 

427 if objects.schematic_objects[object_id]["item"] != objects.object_type.line: 

428 if bbox[0] < xpos and bbox[2] > xpos and bbox[1] < ypos and bbox[3] > ypos: 

429 return(object_id) 

430 for object_id in objects.schematic_objects: 

431 # For lines we need to check if the cursor is "close" to the line 

432 if objects.schematic_objects[object_id]["item"] == objects.object_type.line: 

433 x1 = objects.schematic_objects[object_id]["posx"] 

434 x2 = objects.schematic_objects[object_id]["endx"] 

435 y1 = objects.schematic_objects[object_id]["posy"] 

436 y2 = objects.schematic_objects[object_id]["endy"] 

437 a, b, c = y1-y2, x2-x1,(x1-x2)*y1 + (y2-y1)*x1 

438 if ( ( (xpos>x1 and xpos<x2) or (xpos>x2 and xpos<x1) or 

439 (ypos>y1 and ypos<y2) or (ypos>y2 and ypos<y1) ) and 

440 ( (abs(a * xpos + b * ypos + c)) / math.sqrt(a * a + b * b)) <= 5 ): 

441 return(object_id) 

442 return(None) 

443 

444#------------------------------------------------------------------------------------ 

445# Internal function to return the ID of the Object the cursor is "highlighting" 

446# Returns the UUID of the highlighted item add details of the highlighted element 

447# Main = (True,False), Secondary = (False, True), All = (True, True) 

448#------------------------------------------------------------------------------------ 

449 

450def find_highlighted_line_end(xpos:int,ypos:int): 

451 global schematic_state 

452 # Iterate through the selected objects to see if any "line ends" are selected 

453 for object_id in schematic_state["selectedobjects"]: 

454 if objects.schematic_objects[object_id]["item"] == objects.object_type.line: 

455 x1 = objects.schematic_objects[object_id]["posx"] 

456 x2 = objects.schematic_objects[object_id]["endx"] 

457 y1 = objects.schematic_objects[object_id]["posy"] 

458 y2 = objects.schematic_objects[object_id]["endy"] 

459 if math.sqrt((xpos - x1) ** 2 + (ypos - y1) ** 2) <= 10: 

460 schematic_state["editlineend1"] = True 

461 schematic_state["editlineend2"] = False 

462 return(object_id) 

463 elif math.sqrt((xpos - x2) ** 2 + (ypos - y2) ** 2) <= 10: 

464 schematic_state["editlineend1"] = False 

465 schematic_state["editlineend2"] = True 

466 return(object_id) 

467 return(None) 

468 

469#------------------------------------------------------------------------------------ 

470# Internal function to Snap the given coordinates to a grid (by returning the deltas) 

471# Uses the global canvas_grid variable 

472#------------------------------------------------------------------------------------ 

473 

474def snap_to_grid(xpos:int,ypos:int, force_snap:bool=False): 

475 if canvas_snap_to_grid or force_snap: 

476 remainderx = xpos%canvas_grid 

477 remaindery = ypos%canvas_grid 

478 if remainderx < canvas_grid/2: remainderx = 0 - remainderx 

479 else: remainderx = canvas_grid - remainderx 

480 if remaindery < canvas_grid/2: remaindery = 0 - remaindery 

481 else: remaindery = canvas_grid - remaindery 

482 else: 

483 remainderx = 0 

484 remaindery = 0 

485 return(remainderx,remaindery) 

486 

487#------------------------------------------------------------------------------------ 

488# Right Button Click - Bring Up Context specific Popup menu 

489# The event will only be bound to the canvas in "Edit" Mode 

490#------------------------------------------------------------------------------------ 

491 

492def right_button_click(event): 

493 # Find the object at the current cursor position (if there is one) 

494 # Note that we use the the canvas coordinates to see if the cursor 

495 # is over the object (as these take into account the current scroll 

496 # bar positions) and the event root coordinates for the popup  

497 canvas_x, canvas_y = canvas_coordinates(event) 

498 highlighted_object = find_highlighted_object(canvas_x, canvas_y) 

499 if highlighted_object: 

500 # Clear any current selections and select the highlighted item 

501 deselect_all_objects() 

502 select_object(highlighted_object) 

503 # Enable the Object popup menu (which will be for the selected object) 

504 popup1.tk_popup(event.x_root,event.y_root) 

505 else: 

506 # Enable the canvas popup menu 

507 popup2.tk_popup(event.x_root,event.y_root) 

508 return() 

509 

510#------------------------------------------------------------------------------------ 

511# Left Button Click - Select Object and/or Start of one the following functions: 

512# Initiate a Move of Selected Objects / Edit Selected Line / Select Area 

513# The event will only be bound to the canvas in "Edit" Mode 

514#------------------------------------------------------------------------------------ 

515 

516def left_button_click(event): 

517 global schematic_state 

518 # set keyboard focus for the canvas (so that any key bindings will work) 

519 canvas.focus_set() 

520 # Get the canvas coordinates (to take into account any scroll bar offsets)  

521 canvas_x, canvas_y = canvas_coordinates(event) 

522 schematic_state["startx"] = canvas_x 

523 schematic_state["starty"] = canvas_y 

524 schematic_state["lastx"] = canvas_x 

525 schematic_state["lasty"] = canvas_y 

526 # See if the cursor is over the "end" of an already selected line  

527 highlighted_object = find_highlighted_line_end(canvas_x,canvas_y) 

528 if highlighted_object: 

529 # Clear selections and select the highlighted line. Note that the edit line 

530 # mode ("editline1" or "editline2") get set by "find_highlighted_line_end" 

531 deselect_all_objects() 

532 select_object(highlighted_object) 

533 else: 

534 # See if the cursor is over any other canvas object 

535 highlighted_object = find_highlighted_object(canvas_x,canvas_y) 

536 if highlighted_object: 

537 schematic_state["moveobjects"] = True 

538 if highlighted_object not in schematic_state["selectedobjects"]: 

539 # Clear any current selections and select the highlighted object 

540 deselect_all_objects() 

541 select_object(highlighted_object) 

542 else: 

543 # Cursor is not over any object - Could be the start of a new area selection or 

544 # just clearing the current selection - In either case we deselect all objects 

545 deselect_all_objects() 

546 schematic_state["selectarea"] = True 

547 # Make the 'selectareabox' visible. This will create the box on first use 

548 # or after a 'delete_all_objects (when the box is set to 'None') 

549 if schematic_state["selectareabox"] is None: 

550 schematic_state["selectareabox"] = canvas.create_rectangle(0,0,0,0,outline="orange") 

551 canvas.coords(schematic_state["selectareabox"],canvas_x,canvas_y,canvas_x,canvas_y) 

552 canvas.itemconfigure(schematic_state["selectareabox"],state="normal") 

553 # Unbind the canvas keypresses until left button release to prevent mode changes, 

554 # rotate/delete of objects (i.e. prevent undesirable editor behavior) 

555 disable_all_keypress_events_during_move() 

556 return() 

557 

558#------------------------------------------------------------------------------------ 

559# Left-Shift-Click - Select/deselect Object 

560# The event will only be bound to the canvas in "Edit" Mode 

561#------------------------------------------------------------------------------------ 

562 

563def left_shift_click(event): 

564 # Get the canvas coordinates (to take into account any scroll bar offsets)  

565 canvas_x, canvas_y = canvas_coordinates(event) 

566 # Find the object at the current cursor position (if there is one) 

567 highlighted_object = find_highlighted_object(canvas_x,canvas_y) 

568 if highlighted_object and highlighted_object in schematic_state["selectedobjects"]: 

569 # Deselect just the highlighted object (leave everything else selected) 

570 deselect_object(highlighted_object) 

571 else: 

572 # Select the highlighted object to the list of selected objects 

573 select_object(highlighted_object) 

574 return() 

575 

576#------------------------------------------------------------------------------------ 

577# Left-Double-Click - Bring up edit object dialog for object 

578# The event will only be bound to the canvas in "Edit" Mode 

579#------------------------------------------------------------------------------------ 

580 

581def left_double_click(event): 

582 # Get the canvas coordinates (to take into account any scroll bar offsets)  

583 canvas_x, canvas_y = canvas_coordinates(event) 

584 # Find the object at the current cursor position (if there is one) 

585 highlighted_object = find_highlighted_object(canvas_x,canvas_y) 

586 if highlighted_object: 586 ↛ 592line 586 didn't jump to line 592, because the condition on line 586 was never false

587 # Clear any current selections and select the highlighted item 

588 deselect_all_objects() 

589 select_object(highlighted_object) 

590 # Call the function to open the edit dialog for the object 

591 edit_selected_object() 

592 return() 

593 

594#------------------------------------------------------------------------------------ 

595# Track Cursor - Move Selected Objects / Edit Selected Line / Change Area Selection 

596# The event will only be bound to the canvas in "Edit" Mode 

597#------------------------------------------------------------------------------------ 

598 

599def track_cursor(event): 

600 global schematic_state 

601 # Get the canvas coordinates (to take into account any scroll bar offsets)  

602 canvas_x, canvas_y = canvas_coordinates(event) 

603 if schematic_state["moveobjects"]: 

604 # Work out the delta movement since the last re-draw 

605 xdiff = canvas_x - schematic_state["lastx"] 

606 ydiff = canvas_y - schematic_state["lasty"] 

607 # Move all the objects that are selected 

608 move_selected_objects(xdiff,ydiff) 

609 # Set the 'last' position for the next move event 

610 schematic_state["lastx"] += xdiff 

611 schematic_state["lasty"] += ydiff 

612 elif schematic_state["editlineend1"] or schematic_state["editlineend2"]: 

613 xdiff = canvas_x - schematic_state["lastx"] 

614 ydiff = canvas_y - schematic_state["lasty"] 

615 # Move the selected line end (only one object will be selected) 

616 object_id = schematic_state["selectedobjects"][0] 

617 if schematic_state["editlineend1"]: move_line_end_1(object_id,xdiff,ydiff) 

618 else: move_line_end_2(object_id,xdiff,ydiff) 

619 # Reset the "start" position for the next move 

620 schematic_state["lastx"] += xdiff 

621 schematic_state["lasty"] += ydiff 

622 elif schematic_state["selectarea"]: 

623 # Dynamically resize the selection area 

624 x1 = schematic_state["startx"] 

625 y1 = schematic_state["starty"] 

626 canvas.coords(schematic_state["selectareabox"],x1,y1,canvas_x,canvas_y) 

627 return() 

628 

629#------------------------------------------------------------------------------------ 

630# Left Button Release - Finish Object or line end Moves (by snapping to grid) 

631# or select all objects within the canvas area selection box 

632# The event will only be bound to the canvas in "Edit" Mode 

633#------------------------------------------------------------------------------------ 

634 

635def left_button_release(event): 

636 global schematic_state 

637 if schematic_state["moveobjects"]: 

638 # Finish the move by snapping all objects to the grid - we only need to work 

639 # out the xdiff and xdiff for one of the selected objects to get the diff 

640 xdiff,ydiff = snap_to_grid(schematic_state["lastx"]- schematic_state["startx"], 

641 schematic_state["lasty"]- schematic_state["starty"]) 

642 move_selected_objects(xdiff,ydiff) 

643 # Calculate the total deltas for the move (from the startposition) 

644 finalx = schematic_state["lastx"] - schematic_state["startx"] + xdiff 

645 finaly = schematic_state["lasty"] - schematic_state["starty"] + ydiff 

646 # Finalise the move by updating the current object position 

647 objects.move_objects(schematic_state["selectedobjects"], 

648 xdiff1=finalx, ydiff1=finaly, xdiff2=finalx, ydiff2=finaly ) 

649 # Clear the "select object mode" - but leave all objects selected 

650 schematic_state["moveobjects"] = False 

651 elif schematic_state["editlineend1"] or schematic_state["editlineend2"]: 

652 # Finish the move by snapping the line end to the grid 

653 xdiff,ydiff = snap_to_grid(schematic_state["lastx"]- schematic_state["startx"], 

654 schematic_state["lasty"]- schematic_state["starty"]) 

655 # Move the selected line end (only one object will be selected) 

656 object_id = schematic_state["selectedobjects"][0] 

657 if schematic_state["editlineend1"]: move_line_end_1(object_id,xdiff,ydiff) 

658 else: move_line_end_2(object_id,xdiff,ydiff) 

659 # Calculate the total deltas for the move (from the startposition) 

660 finalx = schematic_state["lastx"] - schematic_state["startx"] + xdiff 

661 finaly = schematic_state["lasty"] - schematic_state["starty"] + ydiff 

662 # Finalise the move by updating the current object position 

663 if schematic_state["editlineend1"]: 

664 objects.move_objects(schematic_state["selectedobjects"], xdiff1=finalx, ydiff1=finaly) 

665 else: 

666 objects.move_objects(schematic_state["selectedobjects"], xdiff2=finalx, ydiff2=finaly) 

667 # Clear the "Edit line mode" - but leave the line selected 

668 schematic_state["editlineend1"] = False 

669 schematic_state["editlineend2"] = False 

670 # Note the defensive programming - to ensure the bbox exists 

671 elif schematic_state["selectarea"] and schematic_state["selectareabox"] is not None: 

672 # Select all Objects that are fully within the Area Selection Box 

673 abox = canvas.coords(schematic_state["selectareabox"]) 

674 for object_id in objects.schematic_objects: 

675 bbox = canvas.coords(objects.schematic_objects[object_id]["bbox"]) 

676 if bbox[0]>abox[0] and bbox[2]<abox[2] and bbox[1]>abox[1] and bbox[3]<abox[3]: 

677 select_object(object_id) 

678 # Clear the Select Area Mode and Hide the area selection rectangle 

679 canvas.itemconfigure(schematic_state["selectareabox"],state="hidden") 

680 schematic_state["selectarea"] = False 

681 # Re-bind the canvas keypresses on completion of area selection or Move Objects 

682 enable_all_keypress_events_after_completion_of_move() 

683 return() 

684 

685#------------------------------------------------------------------------------------ 

686# Left Button Release - Finish Object or line end Moves (by snapping to grid) 

687# or select all objects within the canvas area selection box 

688# The event will only be bound to the canvas in "Edit" Mode 

689#------------------------------------------------------------------------------------ 

690 

691def cancel_move_in_progress(event=None): 

692 global schematic_state 

693 if schematic_state["moveobjects"]: 

694 # Undo the move by returning all objects to their start position 

695 xdiff = schematic_state["startx"] - schematic_state["lastx"] 

696 ydiff = schematic_state["starty"] - schematic_state["lasty"] 

697 move_selected_objects(xdiff,ydiff) 

698 # Clear the "select object mode" - but leave all objects selected 

699 schematic_state["moveobjects"] = False 

700 elif schematic_state["editlineend1"] or schematic_state["editlineend2"]: 

701 # Undo the move by returning all objects to their start position 

702 xdiff = schematic_state["startx"] - schematic_state["lastx"] 

703 ydiff = schematic_state["starty"] - schematic_state["lasty"] 

704 # Move the selected line end (only one object will be selected) 

705 object_id = schematic_state["selectedobjects"][0] 

706 if schematic_state["editlineend1"]: move_line_end_1(object_id,xdiff,ydiff) 

707 else: move_line_end_2(object_id,xdiff,ydiff) 

708 # Clear the "Edit line mode" - but leave the line selected 

709 schematic_state["editlineend1"] = False 

710 schematic_state["editlineend2"] = False 

711 # Note the defensive programming - to ensure the bbox exists 

712 elif schematic_state["selectarea"] and schematic_state["selectareabox"] is not None: 712 ↛ 717line 712 didn't jump to line 717, because the condition on line 712 was never false

713 # Clear the Select Area Mode and Hide the area selection rectangle 

714 canvas.itemconfigure(schematic_state["selectareabox"],state="hidden") 

715 schematic_state["selectarea"] = False 

716 # Re-bind the canvas keypresses on completion of area selection or Move Objects 

717 enable_all_keypress_events_after_completion_of_move() 

718 return() 

719 

720#------------------------------------------------------------------------------------ 

721# Externally called Function to resize the canvas (called from menubar module on load 

722# of new schematic or re-size of canvas via menubar). Updates the global variables 

723#------------------------------------------------------------------------------------ 

724 

725def update_canvas(width:int, height:int, grid:int, snap_to_grid:bool): 

726 global canvas_width, canvas_height, canvas_grid, canvas_snap_to_grid 

727 # Update the tkinter canvas object if the size has been updated 

728 if canvas_width != width or canvas_height != height: 

729 canvas_width, canvas_height = width, height 

730 reset_window_size() 

731 else: 

732 canvas_width, canvas_height = width, height 

733 # Set the global variables (used in the 'draw_grid' function) 

734 canvas_grid, canvas_snap_to_grid = grid, snap_to_grid 

735 draw_grid() 

736 # Also update the objects module with the new settings 

737 objects.update_canvas(canvas_width, canvas_height, canvas_grid) 

738 return() 

739 

740#------------------------------------------------------------------------------------ 

741# Function to reset the root window sizeto fit the canvas. Note that the maximum 

742# window size will have been been set to the screen size on application creation 

743#------------------------------------------------------------------------------------ 

744 

745def reset_window_size(event=None): 

746 global canvas_width, canvas_height 

747 canvas.config(width=canvas_width, height=canvas_height, scrollregion=(0,0,canvas_width,canvas_height)) 

748 root.geometry("") 

749 return() 

750 

751#------------------------------------------------------------------------------------ 

752# Undo and re-do functions (to deselect all objects first) 

753#------------------------------------------------------------------------------------ 

754 

755def schematic_undo(event=None): 

756 deselect_all_objects() 

757 objects.undo() 

758 return() 

759 

760def schematic_redo(event=None): 

761 deselect_all_objects() 

762 objects.redo() 

763 return() 

764 

765#------------------------------------------------------------------------------------ 

766# Internal Functions to enable/disable all canvas keypress events during an object 

767# move, line edit or area selection function (to ensure deterministic behavior). 

768# Note that on disable (when a move or area selection has been initiated) then we 

769# re-bind the escape key to the function for canceling the move / area selection. 

770# on enable (at completion or cancel of the move/area seclection) then the Escape 

771# key will be re-bound to 'deselect_all_objects' in 'enable_edit_keypress_events' 

772#------------------------------------------------------------------------------------ 

773 

774def enable_all_keypress_events_after_completion_of_move(): 

775 enable_edit_keypress_events() 

776 enable_arrow_keypress_events() 

777 canvas.bind('<Control-Key-m>', canvas_event_callback) # Toggle Mode (Edit/Run) 

778 canvas.bind('<Control-Key-r>', reset_window_size) 

779 return() 

780 

781def disable_all_keypress_events_during_move(): 

782 disable_edit_keypress_events() 

783 disable_arrow_keypress_events() 

784 canvas.bind('<Escape>',cancel_move_in_progress) 

785 canvas.unbind('<Control-Key-m>') # Toggle Mode (Edit/Run) 

786 canvas.unbind('<Control-Key-r>') # Toggle Mode (Edit/Run) 

787 return() 

788 

789#------------------------------------------------------------------------------------ 

790# Internal Functions to enable/disable all edit-mode specific keypress events on 

791# edit enable / edit disable) - all keypress events apart from the mode toggle event 

792#------------------------------------------------------------------------------------ 

793 

794def enable_edit_keypress_events(): 

795 canvas.bind('<BackSpace>', delete_selected_objects) 

796 canvas.bind('<Delete>', delete_selected_objects) 

797 canvas.bind('<Escape>', deselect_all_objects) 

798 canvas.bind('<Control-Key-c>', copy_selected_objects) 

799 canvas.bind('<Control-Key-v>', paste_clipboard_objects) 

800 canvas.bind('<Control-Key-z>', schematic_undo) 

801 canvas.bind('<Control-Key-y>', schematic_redo) 

802 canvas.bind('<Control-Key-s>', canvas_event_callback) # Toggle Snap to Grid Mode 

803 canvas.bind('r', rotate_selected_objects) 

804 canvas.bind('s', snap_selected_objects_to_grid) 

805 return() 

806 

807def disable_edit_keypress_events(): 

808 canvas.unbind('<BackSpace>') 

809 canvas.unbind('<Delete>') 

810 canvas.unbind('<Escape>') 

811 canvas.unbind('<Control-Key-c>') 

812 canvas.unbind('<Control-Key-v>') 

813 canvas.unbind('<Control-Key-z>') 

814 canvas.unbind('<Control-Key-y>') 

815 canvas.unbind('<Control-Key-s>') 

816 canvas.unbind('r') 

817 canvas.unbind('s') 

818 return() 

819 

820#------------------------------------------------------------------------------------ 

821# Externally called Functions to enable/disable schematic editing 

822# Either from the Menubar Mode selection or the 'm' key 

823#------------------------------------------------------------------------------------ 

824 

825def enable_editing(): 

826 global canvas_grid_state 

827 canvas_grid_state = "normal" 

828 canvas.itemconfig("grid",state=canvas_grid_state) 

829 # Enable editing of the schematic objects 

830 objects.enable_editing() 

831 # Re-pack the subframe containing the "add object" buttons to display it. Note that we 

832 # first 'forget' the canvas_frame and then re-pack the button_frame first, followed by 

833 # the canvas_frame - this ensures that the buttons don't dissapear on window re-size 

834 canvas_frame.forget() 

835 button_frame.pack(side=Tk.LEFT, expand=False, fill=Tk.BOTH) 

836 canvas_frame.pack(side=Tk.LEFT, expand=True, fill=Tk.BOTH) 

837 # Bind the Canvas mouse and button events to the various callback functions 

838 canvas.bind("<Motion>", track_cursor) 

839 canvas.bind('<Button-1>', left_button_click) 

840 canvas.bind('<Button-2>', right_button_click) 

841 canvas.bind('<Button-3>', right_button_click) 

842 canvas.bind('<Shift-Button-1>', left_shift_click) 

843 canvas.bind('<ButtonRelease-1>', left_button_release) 

844 canvas.bind('<Double-Button-1>', left_double_click) 

845 # Bind the canvas keypresses to the associated functions 

846 enable_edit_keypress_events() 

847 # Bind the Toggle Mode, arrow key and window re-size keypress events (active in Edit 

848 # and Run Modes - only disabled during Edit Mode object moves and area selections) 

849 canvas.bind('<Control-Key-r>', reset_window_size) 

850 canvas.bind('<Control-Key-m>', canvas_event_callback) 

851 enable_arrow_keypress_events() 

852 # Layout Automation toggle is disabled in Edit Mode (only enabled in Run Mode) 

853 canvas.unbind('<Control-Key-a>') 

854 return() 

855 

856def disable_editing(): 

857 global canvas_grid_state 

858 canvas_grid_state = "hidden" 

859 canvas.itemconfig("grid",state=canvas_grid_state) 

860 deselect_all_objects() 

861 # Disable editing of the schematic objects 

862 objects.disable_editing() 

863 # Forget the subframe containing the "add object" buttons to hide it 

864 button_frame.forget() 

865 # Unbind the Canvas mouse and button events in Run Mode 

866 canvas.unbind("<Motion>") 

867 canvas.unbind('<Button-1>') 

868 canvas.unbind('<Button-2>') 

869 canvas.unbind('<Button-3>') 

870 canvas.unbind('<Shift-Button-1>') 

871 canvas.unbind('<ButtonRelease-1>') 

872 canvas.unbind('<Double-Button-1>') 

873 # Unbind the canvas keypresses in Run Mode (apart from 'm' to toggle modes) 

874 disable_edit_keypress_events() 

875 # Bind the Toggle Mode, arrow key and window re-size keypress events (active in Edit 

876 # and Run Modes - only disabled during Edit Mode object moves and area selections) 

877 canvas.bind('<Control-Key-r>', reset_window_size) 

878 canvas.bind('<Control-Key-m>', canvas_event_callback) 

879 enable_arrow_keypress_events() 

880 # Layout Automation toggle is only enabled in Run Mode (disabled in Edit Mode) 

881 canvas.bind('<Control-Key-a>', canvas_event_callback) 

882 return() 

883 

884#------------------------------------------------------------------------------------ 

885# Externally Called Initialisation function for the Canvas object 

886#------------------------------------------------------------------------------------ 

887 

888def initialise (root_window, event_callback, width:int, height:int, grid:int, snap_to_grid:bool, edit_mode:bool): 

889 global root, canvas, popup1, popup2 

890 global canvas_width, canvas_height, canvas_grid, canvas_grid_state, canvas_snap_to_grid 

891 global button_frame, canvas_frame, button_images 

892 global canvas_event_callback 

893 root = root_window 

894 canvas_event_callback = event_callback 

895 # Create a frame to hold the two subframes ("add" buttons and drawing canvas) 

896 frame = Tk.Frame(root_window) 

897 frame.pack (expand=True, fill=Tk.BOTH) 

898 # Note that we pack the button_frame first, followed by the canvas_frame 

899 # This ensures that the buttons don't dissapear on window re-size (shrink) 

900 # Create a subframe to hold the "add" buttons 

901 button_frame = Tk.Frame(frame, borderwidth=1) 

902 button_frame.pack(side=Tk.LEFT, expand=True, fill=Tk.BOTH) 

903 # Create a subframe to hold the canvas and scrollbars 

904 canvas_frame = Tk.Frame(frame, borderwidth=1) 

905 canvas_frame.pack(side=Tk.LEFT, expand=True, fill=Tk.BOTH) 

906 # Save the Default values for the canvas as global variables 

907 canvas_width, canvas_height, canvas_grid, canvas_snap_to_grid = width, height, grid, snap_to_grid 

908 if edit_mode: canvas_grid_state = "normal" 908 ↛ 909line 908 didn't jump to line 909, because the condition on line 908 was never false

909 else: canvas_grid_state = "hidden" 

910 # Create the canvas and scrollbars inside the parent frame 

911 # We also set focus on the canvas so the keypress events will take effect 

912 canvas = Tk.Canvas(canvas_frame ,bg="grey85", scrollregion=(0, 0, canvas_width, canvas_height)) 

913 canvas.focus_set() 

914 hbar = Tk.Scrollbar(canvas_frame, orient=Tk.HORIZONTAL) 

915 hbar.pack(side=Tk.BOTTOM, fill=Tk.X) 

916 hbar.config(command=canvas.xview) 

917 vbar = Tk.Scrollbar(canvas_frame, orient=Tk.VERTICAL) 

918 vbar.pack(side=Tk.RIGHT, fill=Tk.Y) 

919 vbar.config(command=canvas.yview) 

920 canvas.config(width=canvas_width, height=canvas_height) 

921 canvas.config(xscrollcommand=hbar.set, yscrollcommand=vbar.set) 

922 canvas.pack(side=Tk.LEFT, expand=True, fill=Tk.BOTH) 

923 # Define the Object Popup menu for Right Click (something selected) 

924 popup1 = Tk.Menu(tearoff=0) 

925 popup1.add_command(label="Copy", command=copy_selected_objects) 

926 popup1.add_command(label="Edit", command=edit_selected_object) 

927 popup1.add_command(label="Rotate", command=rotate_selected_objects) 

928 popup1.add_command(label="Delete", command=delete_selected_objects) 

929 popup1.add_command(label="Snap to Grid", command=snap_selected_objects_to_grid) 

930 # Define the Canvas Popup menu for Right Click (nothing selected) 

931 popup2 = Tk.Menu(tearoff=0) 

932 popup2.add_command(label="Paste", command=paste_clipboard_objects) 

933 popup2.add_command(label="Select all", command=select_all_objects) 

934 # Define the object buttons [filename, function_to_call] 

935 selections = [ ["textbox", lambda:create_object(objects.object_type.textbox) ], 935 ↛ exitline 935 didn't run the lambda on line 935

936 ["line", lambda:create_object(objects.object_type.line) ], 

937 ["colourlight", lambda:create_object(objects.object_type.signal, 

938 signals_common.sig_type.colour_light.value, 

939 signals_colour_lights.signal_sub_type.four_aspect.value) ], 

940 ["semaphore", lambda:create_object(objects.object_type.signal, 

941 signals_common.sig_type.semaphore.value, 

942 signals_semaphores.semaphore_sub_type.home.value) ], 

943 ["groundpos", lambda:create_object(objects.object_type.signal, 

944 signals_common.sig_type.ground_position.value, 

945 signals_ground_position.ground_pos_sub_type.standard.value) ], 

946 ["grounddisc", lambda:create_object(objects.object_type.signal, 

947 signals_common.sig_type.ground_disc.value, 

948 signals_ground_disc.ground_disc_sub_type.standard.value) ], 

949 ["lhpoint", lambda:create_object(objects.object_type.point, 

950 points.point_type.LH.value) ], 

951 ["rhpoint", lambda:create_object(objects.object_type.point, 

952 points.point_type.RH.value) ], 

953 ["section", lambda:create_object(objects.object_type.section) ], 

954 ["sensor", lambda:create_object(objects.object_type.track_sensor) ], 

955 ["instrument", lambda:create_object(objects.object_type.instrument, 

956 block_instruments.instrument_type.single_line.value) ] ] 

957 # Create the buttons we need (Note that the button images are added to a global 

958 # list so they remain in scope (otherwise the buttons won't work) 

959 resource_folder = 'model_railway_signals.editor.resources' 

960 for index, button in enumerate (selections): 

961 file_name = selections[index][0] 

962 try: 

963 # Load the image file for the button if there is one 

964 with importlib.resources.path (resource_folder,(file_name+'.png')) as file_path: 

965 button_image = Tk.PhotoImage(file=file_path) 

966 button_images.append(button_image) 

967 button = Tk.Button (button_frame, image=button_image,command=selections[index][1]) 

968 button.pack(padx=2, pady=2, fill='x') 

969 except: 

970 # Else fall back to using a text label (filename) for the button 

971 button = Tk.Button (button_frame, text=selections[index][0],command=selections[index][1], bg="grey85") 

972 button.pack(padx=2, pady=2, fill='x') 

973 # Initialise the Objects package with the required parameters 

974 objects.initialise(canvas, canvas_width, canvas_height, canvas_grid) 

975 return() 

976 

977# The following shutdown function is to overcome what seems to be a bug in TkInter where 

978# (I think) Tkinter is trying to destroy the photo-image objects after closure of the 

979# root window and this generates the following exception: 

980# Exception ignored in: <function Image.__del__ at 0xb57ce6a8> 

981# Traceback (most recent call last): 

982# File "/usr/lib/python3.7/tkinter/__init__.py", line 3508, in __del__ 

983# TypeError: catching classes that do not inherit from BaseException is not allowed 

984 

985def shutdown(): 

986 global button_images 

987 button_images = [] 

988 

989####################################################################################