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

581 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-10 15:08 +0100

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

2# These are common classes used across multiple UI Elements 

3# 

4# Provides the following 'primitive' classes for use across the editor UI 

5# CreateToolTip(widget,tool_tip) 

6# check_box(Tk.Checkbutton) 

7# state_box(Tk.Checkbutton) 

8# entry_box(Tk.Entry) 

9# integer_entry_box(entry_box) 

10# dcc_entry_box(integer_entry_box) 

11# int_item_id_entry_box (integer_entry_box) 

12# str_item_id_entry_box(entry_box) 

13# int_str_item_id_entry_box(entry_box) 

14# scrollable_text_frame(Tk.Frame) 

15# 

16# Provides the following 'compound' UI elements for the application 

17# object_id_selection(Tk.integer_entry_box) 

18# point_interlocking_entry() - combines int_item_id_entry_box and state_box 

19# dcc_command_entry() - combines dcc_entry_box and state_box 

20# signal_route_selections() - combines int_item_id_entry_box and 5 state_boxes 

21# signal_route_frame() - read only list of signal_route_selections() 

22# selection_buttons() - combines up to 5 RadioButtons 

23# colour_selection() - Allows the colour of an item to be changed 

24# window_controls() - apply/ok/reset/cancel 

25#------------------------------------------------------------------------------------ 

26 

27import tkinter as Tk 

28from tkinter import colorchooser 

29 

30#------------------------------------------------------------------------------------ 

31# Class to create a tooltip for a tkinter widget - Acknowledgements to Stack Overflow 

32# https://stackoverflow.com/questions/3221956/how-do-i-display-tooltips-in-tkinter 

33# 

34# Class instance elements intended for external use are: 

35# "text" - to change the tooltip text (e.g. to show error messages) 

36#------------------------------------------------------------------------------------ 

37 

38class CreateToolTip(): 

39 def __init__(self, widget, text:str='widget info'): 

40 self.waittime = 500 #miliseconds 

41 self.wraplength = 180 #pixels 

42 self.widget = widget 

43 self.text = text 

44 self.widget.bind("<Enter>", self.enter) 

45 self.widget.bind("<Leave>", self.leave) 

46 self.widget.bind("<ButtonPress>", self.leave) 

47 self.id = None 

48 self.tw = None 

49 

50 def enter(self, event=None): 

51 self.schedule() 

52 

53 def leave(self, event=None): 

54 self.unschedule() 

55 self.hidetip() 

56 

57 def schedule(self): 

58 self.unschedule() 

59 self.id = self.widget.after(self.waittime, self.showtip) 

60 

61 def unschedule(self): 

62 id = self.id 

63 self.id = None 

64 if id: self.widget.after_cancel(id) 

65 

66 def showtip(self, event=None): 

67 x = y = 0 

68 x, y, cx, cy = self.widget.bbox("insert") 

69 x += self.widget.winfo_rootx() + 25 

70 y += self.widget.winfo_rooty() + 20 

71 # creates a toplevel window 

72 self.tw = Tk.Toplevel(self.widget) 

73 self.tw.attributes('-topmost',True) 

74 # Leaves only the label and removes the app window 

75 self.tw.wm_overrideredirect(True) 

76 self.tw.wm_geometry("+%d+%d" % (x, y)) 

77 label = Tk.Label(self.tw, text=self.text, justify='left', 

78 background="#ffffff", relief='solid', borderwidth=1, 

79 wraplength = self.wraplength) 

80 label.pack(ipadx=1) 

81 

82 def hidetip(self): 

83 tw = self.tw 

84 self.tw= None 

85 if tw: tw.destroy() 

86 

87#------------------------------------------------------------------------------------ 

88# Base class for a generic check_box - Builds on the tkinter checkbutton class. 

89# Note the responsibility of the instantiating func/class to 'pack' the check_box. 

90# 

91# Public class methods provided are: 

92# "set_value" - will set the check_box state (bool) 

93# "get_value" - will return the state (False if disabled) (bool) 

94# "disable/disable1/disable2" - disables/blanks the check_box 

95# "enable/enable1/enable2" enables/loads the check_box (with the last state) 

96# 

97# Class methods/objects intended for use by child classes that inherit: 

98# "TT.text" - The tooltip for the check_box (to change the tooltip text) 

99# "state" - is the current check_box value 

100# 

101# Note that check_box is created as 'enabled' - the individual functions provide 

102# an AND function where all three flags need to be 'enabled' to enable the  

103# check_box. Any of the 3 flags can be 'disabled' to disable the check_box. 

104#------------------------------------------------------------------------------------ 

105 

106class check_box(Tk.Checkbutton): 

107 def __init__(self, parent_frame, label:str, tool_tip:str, width:int=None, callback=None): 

108 # Create the local instance configuration variables 

109 # 'selection' is the current CB state and 'state' is the last entered state 

110 # 'enabled' is the flag to track whether the checkbox is enabled or not 

111 self.parent_frame = parent_frame 

112 self.callback = callback 

113 self.selection = Tk.BooleanVar(self.parent_frame, False) 

114 self.state = False 

115 self.enabled0 = True 

116 self.enabled1 = True 

117 self.enabled2 = True 

118 # Create the checkbox and associated tool tip 

119 if width is None: 

120 super().__init__(self.parent_frame, text=label, anchor="w", 

121 variable=self.selection, command=self.cb_updated) 

122 else: 

123 super().__init__(self.parent_frame, width = width, text=label, anchor="w", 

124 variable=self.selection, command=self.cb_updated) 

125 self.TT = CreateToolTip(self, tool_tip) 

126 

127 def cb_updated(self): 

128 # Focus on the Checkbox to remove focus from other widgets (such as EBs) 

129 self.parent_frame.focus() 

130 self.state = self.selection.get() 

131 if self.callback is not None: self.callback() 

132 

133 def enable_disable_checkbox(self): 

134 if self.enabled0 and self.enabled1 and self.enabled2: 

135 self.selection.set(self.state) 

136 self.configure(state="normal") 

137 else: 

138 self.configure(state="disabled") 

139 self.selection.set(False) 

140 

141 def enable(self): 

142 self.enabled0 = True 

143 self.enable_disable_checkbox() 

144 

145 def disable(self): 

146 self.enabled0 = False 

147 self.enable_disable_checkbox() 

148 

149 def enable1(self): 

150 self.enabled1 = True 

151 self.enable_disable_checkbox() 

152 

153 def disable1(self): 

154 self.enabled1 = False 

155 self.enable_disable_checkbox() 

156 

157 def enable2(self): 

158 self.enabled2 = True 

159 self.enable_disable_checkbox() 

160 

161 def disable2(self): 

162 self.enabled2 = False 

163 self.enable_disable_checkbox() 

164 

165 def set_value(self, new_value:bool): 

166 self.state = new_value 

167 if self.enabled0 and self.enabled1 and self.enabled2: 

168 self.selection.set(new_value) 

169 

170 def get_value(self): 

171 # Will always return False if disabled 

172 return (self.selection.get()) 

173 

174#------------------------------------------------------------------------------------ 

175# Base class for a generic state_box (like a check box but with labels for off/on  

176# and blank when disabled) - Builds on the tkinter checkbutton class. 

177# Note the responsibility of the instantiating func/class to 'pack' the state_box. 

178# 

179# Public class methods provided are: 

180# "set_value" - will set the state_box state (bool) 

181# "get_value" - will return the current state (False if disabled) (bool) 

182# "disable/disable1/disable2" - disables/blanks the state_box 

183# "enable/enable1/enable2" enables/loads the state_box (with the last state) 

184# 

185# Class methods/objects intended for use by child classes that inherit: 

186# "TT.text" - The tooltip for the check_box (to change the tooltip text) 

187# "state" - is the current check_box value 

188# 

189# Note that state_box is created as 'enabled' - the individual functions provide 

190# an AND function where all three flags need to be 'enabled' to enable the  

191# state_box. Any of the 3 flags can be 'disabled' to disable the state_box. 

192#------------------------------------------------------------------------------------ 

193 

194class state_box(Tk.Checkbutton): 

195 def __init__(self, parent_frame, label_off:str, label_on:str, tool_tip:str, 

196 width:int=None, callback=None, read_only:bool=False): 

197 # Create the local instance configuration variables 

198 # 'selection' is the current CB state and 'state' is the last entered state 

199 # 'enabled' is the flag to track whether the checkbox is enabled or not 

200 self.parent_frame = parent_frame 

201 self.callback = callback 

202 self.labelon = label_on 

203 self.labeloff = label_off 

204 self.read_only = read_only 

205 self.selection = Tk.BooleanVar(self.parent_frame, False) 

206 self.state = False 

207 self.enabled0 = True 

208 self.enabled1 = True 

209 self.enabled2 = True 

210 # Create the checkbox and associated tool tip 

211 if width is None: 211 ↛ 212line 211 didn't jump to line 212, because the condition on line 211 was never true

212 super().__init__(parent_frame, indicatoron = False, 

213 text=self.labeloff, variable=self.selection, command=self.cb_updated) 

214 else: 

215 super().__init__(parent_frame, indicatoron = False, width=width, 

216 text=self.labeloff, variable=self.selection, command=self.cb_updated) 

217 if self.read_only: self.configure(state="disabled") 

218 self.TT = CreateToolTip(self, tool_tip) 

219 

220 def cb_updated(self): 

221 # Focus on the Checkbox to remove focus from other widgets (such as EBs) 

222 self.parent_frame.focus() 

223 self.update_cb_state() 

224 if self.callback is not None: self.callback() 

225 

226 def update_cb_state(self): 

227 if self.enabled0 and self.enabled1 and self.enabled2: 

228 self.state = self.selection.get() 

229 if self.state: self.configure(text=self.labelon) 

230 else: self.configure(text=self.labeloff) 

231 else: 

232 self.configure(text="") 

233 self.selection.set(False) 

234 

235 def enable_disable_checkbox(self): 

236 if not self.read_only: 236 ↛ exitline 236 didn't return from function 'enable_disable_checkbox', because the condition on line 236 was never false

237 if self.enabled0 and self.enabled1 and self.enabled2: 

238 self.selection.set(self.state) 

239 else: 

240 self.selection.set(False) 

241 self.update_cb_state() 

242 

243 def enable(self): 

244 self.enabled0 = True 

245 self.enable_disable_checkbox() 

246 

247 def disable(self): 

248 self.enabled0 = False 

249 self.enable_disable_checkbox() 

250 

251 def enable1(self): 

252 self.enabled1 = True 

253 self.enable_disable_checkbox() 

254 

255 def disable1(self): 

256 self.enabled1 = False 

257 self.enable_disable_checkbox() 

258 

259 def enable2(self): 

260 self.enabled2 = True 

261 self.enable_disable_checkbox() 

262 

263 def disable2(self): 

264 self.enabled2 = False 

265 self.enable_disable_checkbox() 

266 

267 def set_value(self, new_value:bool): 

268 self.selection.set(new_value) 

269 self.state = new_value 

270 self.update_cb_state() 

271 

272 def get_value(self,): 

273 # Will always return False if disabled 

274 return (self.selection.get()) 

275 

276#------------------------------------------------------------------------------------ 

277# Common Base Class for a generic entry_box - Builds on the tkinter Entry class. 

278# This will accept any string value to be entered/displayed with no validation. 

279# Note the responsibility of the instantiating func/class to 'pack' the entry_box. 

280# 

281# Public class methods provided are: 

282# "set_value" - set the initial value of the entry_box (string)  

283# "get_value" - get the last "validated" value of the entry_box (string)  

284# "validate" - This gets overridden by the child class function 

285# "disable/disable1/disable2" - disables/blanks the entry_box 

286# "enable/enable1/enable2" enables/loads the entry_box (with the last value) 

287# 

288# Class methods/objects intended for use by child classes that inherit: 

289# "set_validation_status" - to be called following external validation 

290# "TT.text" - The tooltip for the entry_box (to change the tooltip text) 

291# "entry" - is the current entry_box value (may or may not be valid) 

292# 

293# Note that entry_box is created as 'enabled' - the individual functions provide 

294# an AND function where all three flags need to be 'enabled' to enable the  

295# entry_box. Any of the 3 flags can be 'disabled' to disable the entry_box. 

296#------------------------------------------------------------------------------------ 

297 

298class entry_box(Tk.Entry): 

299 def __init__(self, parent_frame, width:int, tool_tip:str, callback=None): 

300 # Create the local instance configuration variables 

301 # 'entry' is the current EB value and 'value' is the last entered value 

302 # 'enabled' is the flag to track whether the rmtry box is enabled or not 

303 # 'tooltip' is the default tooltip text(if no validation errors are present) 

304 self.parent_frame = parent_frame 

305 self.callback = callback 

306 self.tool_tip = tool_tip 

307 self.entry = Tk.StringVar(self.parent_frame, "") 

308 self.value = "" 

309 self.enabled0 = True 

310 self.enabled1 = True 

311 self.enabled2 = True 

312 # Create the entry box, event bindings and associated default tooltip 

313 super().__init__(self.parent_frame, width=width, textvariable=self.entry, justify='center') 

314 self.bind('<Return>', self.entry_box_updated) 

315 self.bind('<Escape>', self.entry_box_cancel) 

316 self.bind('<FocusOut>', self.entry_box_updated) 

317 self.TT = CreateToolTip(self, self.tool_tip) 

318 

319 def entry_box_updated(self, event): 

320 self.validate() 

321 if event.keysym == 'Return': self.parent_frame.focus() 

322 if self.callback is not None: self.callback() 

323 

324 def entry_box_cancel(self, event): 

325 self.entry.set(self.value) 

326 self.configure(fg='black') 

327 self.parent_frame.focus() 

328 

329 def validate(self): 

330 self.set_validation_status(None) 

331 return(True) 

332 

333 def set_validation_status(self, valid:bool): 

334 # Colour of text is set according to validation status (red=error) 

335 # The inheriting validation function will override the default tool tip 

336 if valid is None: 

337 self.value = self.entry.get() 

338 elif valid == True: 338 ↛ 343line 338 didn't jump to line 343, because the condition on line 338 was never false

339 self.configure(fg='black') 

340 self.TT.text = self.tool_tip 

341 self.value = self.entry.get() 

342 else: 

343 self.configure(fg='red') 

344 

345 def enable_disable_entrybox(self): 

346 if self.enabled0 and self.enabled1 and self.enabled2: 

347 self.configure(state="normal") 

348 self.entry.set(self.value) 

349 self.validate() 

350 else: 

351 self.configure(state="disabled") 

352 self.entry.set("") 

353 

354 def enable(self): 

355 self.enabled0 = True 

356 self.enable_disable_entrybox() 

357 

358 def disable(self): 

359 self.enabled0 = False 

360 self.enable_disable_entrybox() 

361 

362 def enable1(self): 

363 self.enabled1 = True 

364 self.enable_disable_entrybox() 

365 

366 def disable1(self): 

367 self.enabled1 = False 

368 self.enable_disable_entrybox() 

369 

370 def enable2(self): 

371 self.enabled2 = True 

372 self.enable_disable_entrybox() 

373 

374 def disable2(self): 

375 self.enabled2 = False 

376 self.enable_disable_entrybox() 

377 

378 def set_value(self, value:str): 

379 self.value = value 

380 self.entry.set(value) 

381 self.validate() 

382 

383 def get_value(self): 

384 if self.enabled0 and self.enabled1 and self.enabled2: return(self.value) 

385 else: return("") 

386 

387#------------------------------------------------------------------------------------ 

388# Common Class for an integer_entry_box - builds on the entry_box class (above). 

389# This will only allow valid integers (within the defined range) to be entered. 

390# Note the responsibility of the instantiating func/class to 'pack' the entry_box. 

391# 

392# Public class instance methods inherited from the base Entry Box class are: 

393# "disable/disable1/disable2" - disables/blanks the entry_box 

394# "enable/enable1/enable2" enables/loads the entry_box (with the last value) 

395# 

396# Public class instance methods provided/overridden by this class are 

397# "set_value" - set the initial value of the entry_box (int)  

398# "get_value" - get the last "validated" value of the entry_box (int)  

399# "validate" - Validates an integer, within range and whether empty 

400# 

401# Inherited class methods/objects intended for use by child classes that inherit: 

402# "set_validation_status" - to be called following external validation 

403# "TT.text" - The tooltip for the entry_box (to change the tooltip text) 

404# "entry" - is the current entry_box value (may or may not be valid) 

405# 

406# Note that entry_box is created as 'enabled' - the individual functions provide 

407# an AND function where all three flags need to be 'enabled' to enable the  

408# entry_box. Any of the 3 flags can be 'disabled' to disable the entry_box. 

409#------------------------------------------------------------------------------------ 

410 

411class integer_entry_box(entry_box): 

412 def __init__(self, parent_frame, width:int, min_value:int, max_value:int, 

413 tool_tip:str, callback=None, allow_empty:bool=True, empty_equals_zero:bool=True): 

414 # Store the local instance configuration variables 

415 self.empty_equals_zero = empty_equals_zero 

416 self.empty_allowed = allow_empty 

417 self.max_value = max_value 

418 self.min_value = min_value 

419 # Create the entry box, event bindings and associated default tooltip 

420 super().__init__(parent_frame, width=width, tool_tip=tool_tip, callback=callback) 

421 

422 def validate(self, update_validation_status=True): 

423 entered_value = self.entry.get() 

424 if entered_value == "" or entered_value == "#": 

425 # The EB value can be blank if the entry box is inhibited (get_value will return zero) 

426 if self.empty_allowed or not (self.enabled0 and self.enabled1 and self.enabled2): 426 ↛ 431line 426 didn't jump to line 431, because the condition on line 426 was never false

427 valid = True 

428 else: 

429 # If empty is not allowed we need to put a character into the entry box 

430 # to give a visual indication that there is an error on the form 

431 self.entry.set("#") 

432 self.TT.text = ("Must specify a value between "+ 

433 str(self.min_value)+ " and "+str(self.max_value) ) 

434 valid = False 

435 elif not entered_value.isdigit(): 435 ↛ 436line 435 didn't jump to line 436, because the condition on line 435 was never true

436 self.TT.text = "Not a valid integer" 

437 valid = False 

438 elif int(entered_value) < self.min_value or int(entered_value) > self.max_value: 438 ↛ 439line 438 didn't jump to line 439

439 self.TT.text = ("Value out of range - enter a value between "+ 

440 str(self.min_value)+ " and "+str(self.max_value) ) 

441 valid = False 

442 else: 

443 valid = True 

444 if update_validation_status: self.set_validation_status(valid) 

445 return(valid) 

446 

447 def set_value(self, value:int): 

448 if self.empty_allowed and (value==None or (value==0 and self.empty_equals_zero)) : 

449 super().set_value("") 

450 elif value==None: super().set_value(str(0)) 450 ↛ exitline 450 didn't return from function 'set_value'

451 else: super().set_value(str(value)) 

452 

453 def get_value(self): 

454 if super().get_value() == "" or super().get_value() == "#": 

455 if self.empty_equals_zero: return(0) 455 ↛ 456line 455 didn't jump to line 456, because the condition on line 455 was never false

456 else: return(None) 

457 else: return(int(super().get_value())) 

458 

459#------------------------------------------------------------------------------------ 

460# Common class for a DCC address entry box - builds on the integer_entry_box class 

461# Adds additional validation to ensure the DCC Address is within the valid range. 

462# Note the responsibility of the instantiating func/class to 'pack' the entry_box. 

463# 

464# Public class instance methods inherited from the base entry_box class are: 

465# "set_value" - set the initial value of the entry_box (int)  

466# "get_value" - get the last "validated" value of the entry_box (int)  

467# "validate" - Validates an integer, within range and whether empty  

468# "disable/disable1/disable2" - disables/blanks the entry_box 

469# "enable/enable1/enable2" enables/loads the entry_box (with the last value) 

470# 

471# Inherited class methods/objects intended for use by child classes that inherit: 

472# "set_validation_status" - to be called following external validation 

473# "TT.text" - The tooltip for the entry_box (to change the tooltip text) 

474# "entry" - is the current entry_box value (may or may not be valid) 

475#------------------------------------------------------------------------------------ 

476 

477class dcc_entry_box(integer_entry_box): 

478 def __init__(self, parent_frame, callback=None, 

479 tool_tip:str="Enter a DCC address (1-2047) or leave blank"): 

480 # Call the common base class init function to create the EB 

481 super().__init__(parent_frame, width=4 , min_value=1, max_value=2047, 

482 tool_tip=tool_tip, callback=callback) 

483 

484#------------------------------------------------------------------------------------ 

485# Common class for an int_item_id_entry_box - builds on the integer_entry_box 

486# These classes are for entering local signal/point/instrument/section IDs (integers) 

487# They do not accept remote Signal or Instrument IDs (where the ID can be an int or str) 

488# The class uses the 'exists_function' to check that the item exists on the schematic 

489# If the the current item ID is specified (via the set_item_id function) then the class 

490# also validates the entered value is not the same as the current item ID. 

491# Note the responsibility of the instantiating func/class to 'pack' the entry_box. 

492# 

493# Public class instance methods inherited from the base integer_entry_box are: 

494# "get_value" - get the last "validated" value of the entry_box (int) 

495# "disable/disable1/disable2" - disables/blanks the entry_box 

496# "enable/enable1/enable2" enables/loads the entry_box (with the last value) 

497# 

498# Public class instance methods provided/overridden by this class are 

499# "validate" - Validation as described above  

500# "set_value" - set the initial value of the entry_box (int) - Also takes the 

501# optional current item ID (int) for validation purposes (default=0) 

502# 

503# Inherited class methods/objects intended for use by child classes that inherit: 

504# "set_validation_status" - to be called following external validation 

505# "TT.text" - The tooltip for the entry_box (to change the tooltip text) 

506# "entry" - is the current entry_box value (may or may not be valid) 

507# "current_item_id" - for any additional validation that may be required 

508#------------------------------------------------------------------------------------ 

509 

510class int_item_id_entry_box(integer_entry_box): 

511 def __init__(self, parent_frame, tool_tip:str, width:int=3, 

512 callback=None, allow_empty=True, exists_function=None): 

513 # We need to know the current item ID for validation purposes 

514 self.current_item_id = 0 

515 # The exists_function is the function we need to call to see if an item exists 

516 self.exists_function = exists_function 

517 # Call the common base class init function to create the EB 

518 super().__init__(parent_frame, width=width , min_value=1, max_value=99, 

519 allow_empty=allow_empty, tool_tip=tool_tip, callback=callback) 

520 

521 def validate(self, update_validation_status=True): 

522 # Do the basic integer validation (integer, in range) 

523 valid = super().validate(update_validation_status=False) 

524 # Now do the additional validation 

525 if valid: 525 ↛ 534line 525 didn't jump to line 534, because the condition on line 525 was never false

526 if self.exists_function is not None: 

527 if self.entry.get() != "" and not self.exists_function(int(self.entry.get())): 527 ↛ 528line 527 didn't jump to line 528, because the condition on line 527 was never true

528 self.TT.text = "Specified ID does not exist" 

529 valid = False 

530 if self.current_item_id > 0: 

531 if self.entry.get() == str(self.current_item_id): 531 ↛ 532line 531 didn't jump to line 532, because the condition on line 531 was never true

532 self.TT.text = "Entered ID is the same as the current Item ID" 

533 valid = False 

534 if update_validation_status: self.set_validation_status(valid) 

535 return(valid) 

536 

537 def set_value(self, value:int, item_id:int=0): 

538 self.current_item_id = item_id 

539 super().set_value(value) 

540 

541#------------------------------------------------------------------------------------ 

542# Common class for a str_item_id_entry_box - builds on the common entry_box class. 

543# This class is for REMOTE item IDs (subscribed to via MQTT networking) where the ID 

544# is a str in the format 'NODE-ID'. If the 'exists_function' is specified then the  

545# validation function checks that the item exists (i.e. has been subscribed to). 

546# Note the responsibility of the instantiating func/class to 'pack' the entry_box. 

547# 

548# Public class instance methods inherited from the base entry_box class are: 

549# "set_value" - set the initial value of the entry_box (str)  

550# "get_value" - get the last "validated" value of the entry_box (str) 

551# "disable/disable1/disable2" - disables/blanks the entry_box 

552# "enable/enable1/enable2" enables/loads the entry_box (with the last value) 

553# 

554# Public class instance methods provided/overridden by this class are 

555# "validate" - Validation as described above 

556# 

557# Inherited class methods/objects intended for use by child classes that inherit: 

558# "set_validation_status" - to be called following external validation 

559# "TT.text" - The tooltip for the entry_box (to change the tooltip text) 

560# "entry" - is the current entry_box value (may or may not be valid) 

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

562 

563class str_item_id_entry_box(entry_box): 

564 def __init__(self, parent_frame, tool_tip:str, width:int=8, callback=None, exists_function=None): 

565 # The exists_function is the function we need to call to see if an item exists 

566 self.exists_function = exists_function 

567 # Call the common base class init function to create the EB 

568 super().__init__(parent_frame, width=width, tool_tip=tool_tip, callback=callback) 

569 

570 def validate(self, update_validation_status=True): 

571 # Validate that the entry is in the correct format for a remote Item (<NODE>-<ID>) 

572 # where the NODE element can be any non-on zero length string but the ID element 

573 # must be a valid integer between 1 and 99 

574 entered_value = self.entry.get() 

575 node_id = entered_value.rpartition("-")[0] 

576 item_id = entered_value.rpartition("-")[2] 

577 if entered_value == "": 

578 # Entered value is blank - this is valid 

579 valid = True 

580 elif node_id !="" and item_id.isdigit() and int(item_id) > 0 and int(item_id) < 100: 

581 # We know that the entered value is a valid remote item identifier so now we need to 

582 # do the optional validation that the item exists (i.e. has been subscribed to) 

583 if self.exists_function is not None and not self.exists_function(entered_value): 

584 # An exists_function has been specified and the item does not exist - therefore invalid 

585 valid = False 

586 self.TT.text = "Specified ID has not been subscribed to via MQTT networking" 

587 else: 

588 # An exists_function has been specified and the item exists - therefore valid 

589 valid = True 

590 else: 

591 # The entered value is not a valid remote identifier 

592 valid = False 

593 self.TT.text = ("Invalid ID - must be a remote item ID of the form "+ 

594 "'node-ID' with the 'ID' element between 1 and 99 (for a remote ID)") 

595 if update_validation_status: self.set_validation_status(valid) 

596 return(valid) 

597 

598#------------------------------------------------------------------------------------ 

599# Common class for an int_str_item_id_entry_box - builds on the str_item_id_entry_box class. 

600# This class is for LOCAL IDs (on the current schematic) where the entered ID is a number 

601# between 1 and 99), or REMOTE item IDs (subscribed to via MQTT networking) where the ID 

602# is a str in the format 'NODE-ID'. If the 'exists_function' is specified then the  

603# validation function checks that the item exists (i.e. has been subscribed to). 

604# If the the current item ID is specified (via the set_item_id function) then the class 

605# also validates the entered value is not the same as the current item ID. 

606# Note the responsibility of the instantiating func/class to 'pack' the entry_box. 

607# 

608# Public class instance methods inherited from the base entry_box class are: 

609# "get_value" - get the last "validated" value of the entry_box (str) 

610# "disable/disable1/disable2" - disables/blanks the entry_box 

611# "enable/enable1/enable2" enables/loads the entry_box (with the last value) 

612# 

613# Public class instance methods provided/overridden by this class are 

614# "validate" - Validation as described above  

615# "set_value" - set the initial value of the entry_box (str) - Also takes the 

616# optional current item ID (int) for validation purposes (default=0) 

617# 

618# Inherited class methods/objects intended for use by child classes that inherit: 

619# "set_validation_status" - to be called following external validation 

620# "TT.text" - The tooltip for the entry_box (to change the tooltip text) 

621# "entry" - is the current entry_box value (may or may not be valid) 

622# "current_item_id" - for any additional validation that may be required 

623#------------------------------------------------------------------------------------ 

624 

625class str_int_item_id_entry_box (entry_box): 

626 def __init__(self, parent_frame, tool_tip:str, width:int=8, callback=None, exists_function=None): 

627 # We need to know the current item ID for validation purposes 

628 self.current_item_id = 0 

629 # The exists_function is the function we need to call to see if an item exists 

630 self.exists_function = exists_function 

631 # Call the common base class init function to create the EB 

632 super().__init__(parent_frame, width=width, tool_tip=tool_tip, callback=callback) 

633 

634 def validate(self, update_validation_status=True): 

635 # Validate that the entry is in the correct format for a local item id (integer range 1-99) 

636 # or a remote item id (string in the form 'NODE-ID' where the NODE element can be any  

637 # non-zero length string but the ID element must be a valid integer between 1 and 99) 

638 entered_value = self.entry.get() 

639 node_id = entered_value.rpartition("-")[0] 

640 item_id = entered_value.rpartition("-")[2] 

641 if entered_value == "": 

642 # Entered value is blank - this is valid 

643 valid = True 

644 elif ( (entered_value.isdigit() and int(entered_value) > 0 and int(entered_value) < 100) or 644 ↛ 662line 644 didn't jump to line 662, because the condition on line 644 was never false

645 (node_id !="" and item_id.isdigit() and int(item_id) > 0 and int(item_id) < 100) ): 

646 # The entered value is a valid local or remote item identifier. but we still need to perform 

647 # the optional validation that the item exists on the schematic (or has been subscribed to) 

648 if self.exists_function is not None and not self.exists_function(entered_value): 648 ↛ 650line 648 didn't jump to line 650, because the condition on line 648 was never true

649 # An exists_function has been specified but the item does not exist - therefore invalid 

650 valid = False 

651 self.TT.text = ("Specified ID does not exist on the schematic "+ 

652 "(or has not been subscribed to via MQTT networking)") 

653 # So far, so good, but we still need to perform the optional validation that the item id 

654 # is not the same item id as the id of the item we are currently editing  

655 elif self.current_item_id > 0 and entered_value == str(self.current_item_id): 655 ↛ 657line 655 didn't jump to line 657, because the condition on line 655 was never true

656 # An current_id_function and the entered id is the same as the current id - therefore invalid 

657 valid = False 

658 self.TT.text = "Entered ID is the same as the current Item ID" 

659 else: 

660 valid = True 

661 else: 

662 valid = False 

663 self.TT.text = ("Invalid ID - must be a local ID (integer between 1 and 99) or a remote item ID "+ 

664 "of the form 'node-ID' (with the 'ID' element an integer between 1 and 99 ") 

665 if update_validation_status: self.set_validation_status(valid) 

666 return(valid) 

667 

668 def set_value(self, value:str, item_id:int=0): 

669 self.current_item_id = item_id 

670 super().set_value(value) 

671 

672#------------------------------------------------------------------------------------ 

673# Class for a scrollable_text_frame - can be editable (e.g. entering layout info) 

674# or non-editable (e.g. displaying a list of warnings)- can also be configured 

675# to re-size automatically (within the specified limits) as text is entered. 

676# The text box will 'fit' to the content unless max or min dimentions are 

677# specified for the width and/or height - then the scrollbars can be used. 

678# Note the responsibility of the instantiating func/class to 'pack' the entry_box. 

679# 

680# Public class instance methods provided by this class are 

681# "set_value" - will set the current value (str) 

682# "get_value" - will return the current value (str) 

683# "set_justification" - set justification (int - 1=left, 2=center, 3=right) 

684# "set_font" - set the font (font:str, font_size:int, font_style:str) 

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

686 

687class scrollable_text_frame(Tk.Frame): 

688 def __init__(self, parent_window, max_height:int=None, min_height:int=None, editable:bool=False, 

689 max_width:int=None, min_width:int=None, auto_resize:bool=False): 

690 # Store the parameters we need 

691 self.min_height = min_height 

692 self.max_height = max_height 

693 self.min_width = min_width 

694 self.max_width = max_width 

695 self.editable = editable 

696 self.auto_resize = auto_resize 

697 self.text="" 

698 # Create a frame for the text widget and scrollbars 

699 super().__init__(parent_window) 

700 # Create a subframe for the text and scrollbars 

701 self.subframe = Tk.Frame(self) 

702 self.subframe.pack(fill=Tk.BOTH, expand=True) 

703 # Create the text widget and vertical scrollbars in the subframe 

704 self.text_box = Tk.Text(self.subframe, wrap=Tk.NONE) 

705 self.text_box.insert(Tk.END,self.text) 

706 hbar = Tk.Scrollbar(self.subframe, orient=Tk.HORIZONTAL) 

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

708 hbar.config(command=self.text_box.xview) 

709 vbar = Tk.Scrollbar(self.subframe, orient=Tk.VERTICAL) 

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

711 vbar.config(command=self.text_box.yview) 

712 self.text_box.config(xscrollcommand=hbar.set, yscrollcommand=vbar.set) 

713 self.text_box.pack(side=Tk.LEFT, expand=True, fill=Tk.BOTH) 

714 # configure the window for editable or non-editable 

715 if not self.editable: self.text_box.config(state="disabled") 

716 # Set up the callback for auto re-size (if specified) 

717 if self.auto_resize: self.text_box.bind('<KeyRelease>', self.resize_text_box) 

718 # Set the initial size for the text box 

719 self.resize_text_box() 

720 # Define the tags we are goint to use for justifying the text 

721 self.text_box.tag_configure("justify_center", justify='center') 

722 self.text_box.tag_configure("justify_left", justify='left') 

723 self.text_box.tag_configure("justify_right", justify='right') 

724 

725 def resize_text_box(self, event=None): 

726 # Calculate the height and width of the text 

727 self.text = self.text_box.get("1.0",Tk.END) 

728 list_of_lines = self.text.splitlines() 

729 number_of_lines = len(list_of_lines) 

730 # Find the maximum line length (to set the width of the text box) 

731 max_line_length = 0 

732 for line in list_of_lines: 

733 if len(line) > max_line_length: max_line_length = len(line) 

734 # Apply the specified size constraints 

735 if self.min_height is not None and number_of_lines < self.min_height: 735 ↛ 736line 735 didn't jump to line 736, because the condition on line 735 was never true

736 number_of_lines = self.min_height 

737 if self.max_height is not None and number_of_lines > self.max_height: 737 ↛ 738line 737 didn't jump to line 738, because the condition on line 737 was never true

738 number_of_lines = self.max_height 

739 if self.min_width is not None and max_line_length < self.min_width: 

740 max_line_length = self.min_width 

741 if self.max_width is not None and max_line_length > self.max_width: 

742 max_line_length = self.max_width 

743 # re-size the text box 

744 self.text_box.config(height=number_of_lines, width=max_line_length+1) 

745 

746 def set_value(self, text:str): 

747 self.text = text 

748 if not self.editable: self.text_box.config(state="normal") 

749 self.text_box.delete("1.0",Tk.END) 

750 self.text_box.insert(Tk.INSERT, self.text) 

751 if not self.editable: self.text_box.config(state="disabled") 

752 self.resize_text_box() 

753 

754 def get_value(self): 

755 self.text = self.text_box.get("1.0",Tk.END) 

756 # Remove the spurious new line (text widget always inserts one) 

757 if self.text.endswith('\r\n'): self.text = self.text[:-2] ## Windows 757 ↛ 759line 757 didn't jump to line 759

758 elif self.text.endswith('\n'): self.text = self.text[:-1] ## Everything else 

759 return(self.text) 

760 

761 def set_justification(self, value:int): 

762 # Define the tags we are goint to use for justifying the text 

763 self.text_box.tag_remove("justify_left",1.0,Tk.END) 

764 self.text_box.tag_remove("justify_center",1.0,Tk.END) 

765 self.text_box.tag_remove("justify_right",1.0,Tk.END) 

766 if value == 1: self.text_box.tag_add("justify_left",1.0,Tk.END) 

767 if value == 2: self.text_box.tag_add("justify_center",1.0,Tk.END) 

768 if value == 3: self.text_box.tag_add("justify_right",1.0,Tk.END) 

769 

770 def set_font(self, font:str, font_size:int, font_style:str): 

771 self.text_box.configure(font=(font, font_size, font_style)) 

772 

773#------------------------------------------------------------------------------------ 

774# Compound UI element for an object_id_selection LabelFrame - uses the integer_entry_box. 

775# This is used across all object windows for displaying / changing the item ID. 

776# Note the responsibility of the instantiating func/class to 'pack' the Frame of 

777# the UI element - i.e. '<class_instance>.frame.pack()' 

778# 

779# Public class instance methods inherited from the base integer_entry_box are: 

780# "get_value" - get the last "validated" value of the entry_box (int)  

781# "disable/disable1/disable2" - disables/blanks the entry_box 

782# "enable/enable1/enable2" enables/loads the entry_box (with the last value) 

783# 

784# Public class instance methods provided/overridden by this class are 

785# "set_value" - set the initial value of the entry_box (int) 

786# "validate" - Validates that the entered Item ID is "free" (and can therefore be 

787# assigned to this item) or is being changed back to the initial value. 

788#------------------------------------------------------------------------------------ 

789 

790class object_id_selection(integer_entry_box): 

791 def __init__(self, parent_frame, label:str, exists_function): 

792 # We need to know the current Item ID for validation purposes 

793 self.current_item_id = 0 

794 # This is the function to call to see if the object already exists 

795 self.exists_function = exists_function 

796 # Create a Label Frame for the UI element 

797 self.frame = Tk.LabelFrame(parent_frame, text=label) 

798 # Call the common base class init function to create the EB 

799 tool_tip = ("Enter new ID (1-99) \n" + "Once saved/applied any references "+ 

800 "to this object will be updated in other objects") 

801 super().__init__(self.frame, width=3, min_value=1, max_value=99, 

802 tool_tip=tool_tip, allow_empty=False) 

803 # Pack the Entry box centrally in the label frame 

804 self.pack() 

805 

806 def validate(self): 

807 # Do the basic integer validation first (integer, in range, not empty) 

808 valid = super().validate(update_validation_status=False) 

809 if valid: 809 ↛ 816line 809 didn't jump to line 816, because the condition on line 809 was never false

810 # Validate that the entered ID is not assigned to another item 

811 # Ignoring the initial value set at load time (which is the current ID) 

812 entered_item_id = int(self.entry.get()) 

813 if self.exists_function(entered_item_id) and entered_item_id != self.current_item_id: 813 ↛ 814line 813 didn't jump to line 814, because the condition on line 813 was never true

814 self.TT.text = "ID already assigned" 

815 valid = False 

816 self.set_validation_status(valid) 

817 return(valid) 

818 

819 def set_value(self, value:int): 

820 self.current_item_id = value 

821 super().set_value(value) 

822 

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

824# Class for a point interlocking entry element (point_id + point_state) 

825# Uses the common int_item_id_entry_box and state_box classes 

826# Public class instance methods provided are: 

827# "validate" - validate the current entry box value and return True/false 

828# "set_value" - will set the current value [point_id:int, state:bool] 

829# "get_value" - will return the last "valid" value [point_id:int, state:bool] 

830# "disable" - disables/blanks the entry box (and associated state button) 

831# "enable" enables/loads the entry box (and associated state button) 

832#------------------------------------------------------------------------------------ 

833 

834class point_interlocking_entry(): 

835 def __init__(self, parent_frame, point_exists_function, tool_tip:str): 

836 # Create the point ID entry box and associated state box (packed in the parent frame) 

837 self.EB = int_item_id_entry_box(parent_frame, exists_function=point_exists_function, 

838 tool_tip = tool_tip, callback=self.eb_updated) 

839 self.EB.pack(side=Tk.LEFT) 

840 self.CB = state_box(parent_frame, label_off=u"\u2192", label_on="\u2191", width=2, 

841 tool_tip="Select the required state for the point (normal or switched)") 

842 self.CB.pack(side=Tk.LEFT) 

843 

844 def eb_updated(self): 

845 if self.EB.entry.get() == "": 

846 self.CB.disable() 

847 else: self.CB.enable() 

848 

849 def validate(self): 

850 return (self.EB.validate()) 

851 

852 def enable(self): 

853 self.EB.enable() 

854 self.eb_updated() 

855 

856 def disable(self): 

857 self.EB.disable() 

858 self.eb_updated() 

859 

860 def set_value(self, point:[int, bool]): 

861 # A Point comprises a 2 element list of [Point_id, Point_state] 

862 self.EB.set_value(point[0]) 

863 self.CB.set_value(point[1]) 

864 self.eb_updated() 

865 

866 def get_value(self): 

867 # Returns a 2 element list of [Point_id, Point_state] 

868 return([self.EB.get_value(), self.CB.get_value()]) 

869 

870#------------------------------------------------------------------------------------ 

871# Compound UI Element for a signal route selections (Sig ID EB + route selection CBs) 

872# Note the responsibility of the instantiating func/class to 'pack' the Frame of 

873# the UI element - i.e. '<class_instance>.frame.pack()' 

874# 

875# Public class instance methods provided by this class are 

876# "validate" - Checks whether the entry is a valid Item Id  

877# "set_values" - Sets the EB value and all route selection CBs- Also takes the 

878# optional current item ID (int) for validation purposes (default=0)  

879# "get_values" - Gets the EB value and all route selection CBs  

880# "enable" - Enables/loads the EB value and all route selection CBs  

881# "disable" - Disables/blanks EB value and all route selection CBs 

882#------------------------------------------------------------------------------------ 

883 

884class signal_route_selections(): 

885 def __init__(self, parent_frame, tool_tip:str, exists_function=None, read_only:bool=False): 

886 self.read_only = read_only 

887 # We need to know the current Signal ID for validation (for the non read-only 

888 # instance of this class used for the interlocking conflicting signals window 

889 self.signal_id = 0 

890 # Create a Frame to hold all the elements 

891 self.frame = Tk.Frame(parent_frame) 

892 # Call the common base class init function to create the EB 

893 self.EB = int_item_id_entry_box(self.frame, tool_tip=tool_tip, 

894 callback=self.eb_updated, exists_function=exists_function) 

895 self.EB.pack(side=Tk.LEFT) 

896 # Disable the EB (we don't use the disable method as we want to display the value_ 

897 if self.read_only: self.EB.configure(state="disabled") 

898 # Create the UI Elements for each of the possible route selections 

899 self.main = state_box(self.frame, label_off="MAIN", label_on="MAIN", 

900 width=5, tool_tip=tool_tip, read_only=read_only) 

901 self.main.pack(side=Tk.LEFT) 

902 self.lh1 = state_box(self.frame, label_off="LH1", label_on="LH1", 

903 width=4, tool_tip=tool_tip, read_only=read_only) 

904 self.lh1.pack(side=Tk.LEFT) 

905 self.lh2 = state_box(self.frame, label_off="LH2", label_on="LH2", 

906 width=4, tool_tip=tool_tip, read_only=read_only) 

907 self.lh2.pack(side=Tk.LEFT) 

908 self.rh1 = state_box(self.frame, label_off="RH1", label_on="RH1", 

909 width=4, tool_tip=tool_tip, read_only=read_only) 

910 self.rh1.pack(side=Tk.LEFT) 

911 self.rh2 = state_box(self.frame, label_off="RH2", label_on="RH2", 

912 width=4, tool_tip=tool_tip, read_only=read_only) 

913 self.rh2.pack(side=Tk.LEFT) 

914 

915 def eb_updated(self): 

916 # Enable/disable the checkboxes depending on the EB state 

917 if not self.read_only: 

918 if self.EB.entry.get() == "": 

919 self.main.disable() 

920 self.lh1.disable() 

921 self.lh2.disable() 

922 self.rh1.disable() 

923 self.rh2.disable() 

924 else: 

925 self.main.enable() 

926 self.lh1.enable() 

927 self.lh2.enable() 

928 self.rh1.enable() 

929 self.rh2.enable() 

930 

931 def validate(self): 

932 self.eb_updated() 

933 return(self.EB.validate()) 

934 

935 def enable(self): 

936 self.EB.enable() 

937 self.eb_updated() 

938 

939 def disable(self): 

940 self.EB.disable() 

941 self.eb_updated() 

942 

943 def set_values(self, signal:[int,[bool,bool,bool,bool,bool]], signal_id:int=0): 

944 # Each signal comprises [sig_id, [main, lh1, lh2, rh1, rh2]] 

945 # Where each route element is a boolean value (True or False) 

946 self.EB.set_value(signal[0], signal_id) 

947 self.main.set_value(signal[1][0]) 

948 self.lh1.set_value(signal[1][1]) 

949 self.lh2.set_value(signal[1][2]) 

950 self.rh1.set_value(signal[1][3]) 

951 self.rh2.set_value(signal[1][4]) 

952 self.eb_updated() 

953 

954 def get_values(self): 

955 # each signal comprises [sig_id, [main, lh1, lh2, rh1, rh2]] 

956 # Where each route element is a boolean value (True or False) 

957 return ( [ self.EB.get_value(), [ self.main.get_value(), 

958 self.lh1.get_value(), 

959 self.lh2.get_value(), 

960 self.rh1.get_value(), 

961 self.rh2.get_value() ] ]) 

962 

963#------------------------------------------------------------------------------------ 

964# Compound UI Element for a "read only" signal_route_frame (LabelFrame) - creates a  

965# variable number of instances of the signal_route_selection_element when "set_values"  

966# is called (according to the length of the supplied list). Note the responsibility of 

967# the instantiating func/class to 'pack' the Frame of the UI element. 

968# 

969# Public class instance methods provided by this class are: 

970# "set_values" - Populates the list of signals and their routes 

971#------------------------------------------------------------------------------------ 

972 

973class signal_route_frame(): 

974 def __init__(self, parent_frame, label:str, tool_tip:str): 

975 # Create the Label Frame for the Signal Interlocking List  

976 self.frame = Tk.LabelFrame(parent_frame, text=label) 

977 # These are the lists that hold the references to the subframes and subclasses 

978 self.tooltip = tool_tip 

979 self.sigelements = [] 

980 self.subframe = None 

981 

982 def set_values(self, sig_interlocking_frame:[[int,[bool,bool,bool,bool,bool]],]): 

983 # If the lists are not empty (case of "reloading" the config) then destroy 

984 # all the UI elements and create them again (the list may have changed) 

985 if self.subframe: self.subframe.destroy() 

986 self.subframe = Tk.Frame(self.frame) 

987 self.subframe.pack() 

988 self.sigelements = [] 

989 # sig_interlocking_frame is a variable length list where each element is [sig_id, interlocked_routes] 

990 if sig_interlocking_frame: 

991 for sig_interlocking_routes in sig_interlocking_frame: 

992 # sig_interlocking_routes comprises [sig_id, [main, lh1, lh2, rh1, rh2]] 

993 # Where each route element is a boolean value (True or False)  

994 self.sigelements.append(signal_route_selections(self.subframe, read_only=True, tool_tip=self.tooltip)) 

995 self.sigelements[-1].frame.pack() 

996 self.sigelements[-1].set_values (sig_interlocking_routes) 

997 else: 

998 self.label = Tk.Label(self.subframe, text="Nothing configured") 

999 self.label.pack() 

1000 

1001#------------------------------------------------------------------------------------ 

1002# Compound UI Element for a LabelFrame containing up to 5 radio buttons 

1003# Note the responsibility of the instantiating func/class to 'pack' the Frame of 

1004# the UI element - i.e. '<class_instance>.frame.pack()' 

1005# 

1006# Class instance elements to use externally are: 

1007# "B1" to "B5 - to access the button widgets (i.e. for reconfiguration) 

1008# 

1009# Class instance functions to use externally are: 

1010# "set_value" - will set the current value (integer 1-5) 

1011# "get_value" - will return the last "valid" value (integer 1-5) 

1012#------------------------------------------------------------------------------------ 

1013 

1014class selection_buttons(): 

1015 def __init__(self, parent_frame, label:str, tool_tip:str, callback=None, 

1016 b1=None, b2=None, b3=None, b4=None, b5=None): 

1017 # Create a labelframe to hold the buttons 

1018 self.frame = Tk.LabelFrame(parent_frame, text=label) 

1019 self.value = Tk.IntVar(self.frame, 0) 

1020 # This is the external callback to make when a selection is made 

1021 self.callback = callback 

1022 # Create a subframe (so the buttons are centered) 

1023 self.subframe = Tk.Frame(self.frame) 

1024 self.subframe.pack() 

1025 # Only create as many buttons as we need 

1026 if b1 is not None: 1026 ↛ 1031line 1026 didn't jump to line 1031, because the condition on line 1026 was never false

1027 self.B1 = Tk.Radiobutton(self.subframe, text=b1, anchor='w', 

1028 command=self.updated, variable=self.value, value=1) 

1029 self.B1.pack(side=Tk.LEFT, padx=2, pady=2) 

1030 self.B1TT = CreateToolTip(self.B1, tool_tip) 

1031 if b2 is not None: 1031 ↛ 1036line 1031 didn't jump to line 1036, because the condition on line 1031 was never false

1032 self.B2 = Tk.Radiobutton(self.subframe, text=b2, anchor='w', 

1033 command=self.updated, variable=self.value, value=2) 

1034 self.B2.pack(side=Tk.LEFT, padx=2, pady=2) 

1035 self.B2TT = CreateToolTip(self.B2, tool_tip) 

1036 if b3 is not None: 

1037 self.B3 = Tk.Radiobutton(self.subframe, text=b3, anchor='w', 

1038 command=self.updated, variable=self.value, value=3) 

1039 self.B3.pack(side=Tk.LEFT, padx=2, pady=2) 

1040 self.B3TT = CreateToolTip(self.B3, tool_tip) 

1041 if b4 is not None: 

1042 self.B4 = Tk.Radiobutton(self.subframe, text=b4, anchor='w', 

1043 command=self.updated, variable=self.value, value=4) 

1044 self.B4.pack(side=Tk.LEFT, padx=2, pady=2) 

1045 self.B4TT = CreateToolTip(self.B4, tool_tip) 

1046 if b5 is not None: 

1047 self.B5 = Tk.Radiobutton(self.subframe, text=b5, anchor='w', 

1048 command=self.updated, variable=self.value, value=5) 

1049 self.B5.pack(side=Tk.LEFT, padx=2, pady=2) 

1050 self.B5TT = CreateToolTip(self.B5, tool_tip) 

1051 

1052 def updated(self): 

1053 self.frame.focus() 

1054 if self.callback is not None: self.callback() 

1055 

1056 def set_value(self, value:int): 

1057 self.value.set(value) 

1058 

1059 def get_value(self): 

1060 return(self.value.get()) 

1061 

1062#------------------------------------------------------------------------------------ 

1063# Compound UI Element for Colour selection 

1064# Note the responsibility of the instantiating func/class to 'pack' the Frame of 

1065# the UI element - i.e. '<class_instance>.frame.pack()' 

1066# 

1067# Class instance functions to use externally are: 

1068# "set_value" - will set the current value (colour code string) 

1069# "get_value" - will return the last "valid" value (colour code string) 

1070# "is_open" - Test if the colour chooser is still open 

1071#------------------------------------------------------------------------------------ 

1072 

1073class colour_selection(): 

1074 def __init__(self, parent_frame, label:str): 

1075 # Flag to test if a colour chooser window is open or not 

1076 self.colour_chooser_open = False 

1077 # Variable to hold the currently selected colour: 

1078 self.colour ='black' 

1079 # Create a frame to hold the tkinter widgets 

1080 # The parent class is responsible for packing the frame 

1081 self.frame = Tk.LabelFrame(parent_frame,text=label) 

1082 # Create a sub frame for the UI elements to centre them 

1083 self.subframe = Tk.Frame(self.frame) 

1084 self.subframe.pack() 

1085 self.label2 = Tk.Label(self.subframe, width=3, bg=self.colour, borderwidth=1, relief="solid") 

1086 self.label2.pack(side=Tk.LEFT, padx=2, pady=2) 

1087 self.TT2 = CreateToolTip(self.label2, "Currently selected colour") 

1088 self.B1 = Tk.Button(self.subframe, text="Change", command=self.update) 

1089 self.B1.pack(side=Tk.LEFT, padx=2, pady=2) 

1090 self.TT2 = CreateToolTip(self.B1, "Open colour chooser dialog") 

1091 

1092 def update(self): 

1093 self.colour_chooser_open = True 

1094 colour_code = colorchooser.askcolor(self.colour, parent=self.frame, title ="Select Colour") 

1095 self.colour = colour_code[1] 

1096 self.label2.config(bg=self.colour) 

1097 self.colour_chooser_open = False 

1098 

1099 def get_value(self): 

1100 return(self.colour) 

1101 

1102 def set_value(self,colour:str): 

1103 self.colour = colour 

1104 self.label2.config(bg=self.colour) 

1105 

1106 def is_open(self): 

1107 return(self.colour_chooser_open) 

1108 

1109#------------------------------------------------------------------------------------ 

1110# Compound UI element for the Apply/OK/Reset/Cancel Buttons - will make callbacks 

1111# to the specified "load_callback" and "save_callback" functions as appropriate  

1112# Note the responsibility of the instantiating func/class to 'pack' the Frame of 

1113# the UI element - i.e. '<class_instance>.frame.pack()' 

1114#------------------------------------------------------------------------------------ 

1115 

1116class window_controls(): 

1117 def __init__(self, parent_window, load_callback, save_callback, cancel_callback): 

1118 # Create the class instance variables 

1119 self.window = parent_window 

1120 self.save_callback = save_callback 

1121 self.load_callback = load_callback 

1122 self.cancel_callback = cancel_callback 

1123 self.frame = Tk.Frame(self.window) 

1124 # Create the buttons and tooltips 

1125 self.B1 = Tk.Button (self.frame, text = "Ok",command=self.ok) 

1126 self.B1.pack(side=Tk.LEFT, padx=2, pady=2) 

1127 self.TT1 = CreateToolTip(self.B1, "Apply selections and close window") 

1128 self.B2 = Tk.Button (self.frame, text = "Apply",command=self.apply) 

1129 self.B2.pack(side=Tk.LEFT, padx=2, pady=2) 

1130 self.TT2 = CreateToolTip(self.B2, "Apply selections") 

1131 self.B3 = Tk.Button (self.frame, text = "Reset",command=self.reset) 

1132 self.B3.pack(side=Tk.LEFT, padx=2, pady=2) 

1133 self.TT3 = CreateToolTip(self.B3, "Abandon edit and reload original configuration") 

1134 self.B4 = Tk.Button (self.frame, text = "Cancel",command=self.cancel) 

1135 self.B4.pack(side=Tk.LEFT, padx=2, pady=2) 

1136 self.TT4 = CreateToolTip(self.B4, "Abandon edit and close window") 

1137 

1138 def apply(self): 

1139 self.window.focus() 

1140 self.save_callback(False) 

1141 

1142 def ok(self): 

1143 self.window.focus() 

1144 self.save_callback(True) 

1145 

1146 def reset(self): 

1147 self.window.focus() 

1148 self.load_callback() 

1149 

1150 def cancel(self): 

1151 self.cancel_callback() 

1152 

1153###########################################################################################