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
« 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#------------------------------------------------------------------------------------
27import tkinter as Tk
28from tkinter import colorchooser
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#------------------------------------------------------------------------------------
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
50 def enter(self, event=None):
51 self.schedule()
53 def leave(self, event=None):
54 self.unschedule()
55 self.hidetip()
57 def schedule(self):
58 self.unschedule()
59 self.id = self.widget.after(self.waittime, self.showtip)
61 def unschedule(self):
62 id = self.id
63 self.id = None
64 if id: self.widget.after_cancel(id)
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)
82 def hidetip(self):
83 tw = self.tw
84 self.tw= None
85 if tw: tw.destroy()
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#------------------------------------------------------------------------------------
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)
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()
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)
141 def enable(self):
142 self.enabled0 = True
143 self.enable_disable_checkbox()
145 def disable(self):
146 self.enabled0 = False
147 self.enable_disable_checkbox()
149 def enable1(self):
150 self.enabled1 = True
151 self.enable_disable_checkbox()
153 def disable1(self):
154 self.enabled1 = False
155 self.enable_disable_checkbox()
157 def enable2(self):
158 self.enabled2 = True
159 self.enable_disable_checkbox()
161 def disable2(self):
162 self.enabled2 = False
163 self.enable_disable_checkbox()
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)
170 def get_value(self):
171 # Will always return False if disabled
172 return (self.selection.get())
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#------------------------------------------------------------------------------------
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)
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()
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)
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()
243 def enable(self):
244 self.enabled0 = True
245 self.enable_disable_checkbox()
247 def disable(self):
248 self.enabled0 = False
249 self.enable_disable_checkbox()
251 def enable1(self):
252 self.enabled1 = True
253 self.enable_disable_checkbox()
255 def disable1(self):
256 self.enabled1 = False
257 self.enable_disable_checkbox()
259 def enable2(self):
260 self.enabled2 = True
261 self.enable_disable_checkbox()
263 def disable2(self):
264 self.enabled2 = False
265 self.enable_disable_checkbox()
267 def set_value(self, new_value:bool):
268 self.selection.set(new_value)
269 self.state = new_value
270 self.update_cb_state()
272 def get_value(self,):
273 # Will always return False if disabled
274 return (self.selection.get())
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#------------------------------------------------------------------------------------
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)
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()
324 def entry_box_cancel(self, event):
325 self.entry.set(self.value)
326 self.configure(fg='black')
327 self.parent_frame.focus()
329 def validate(self):
330 self.set_validation_status(None)
331 return(True)
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')
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("")
354 def enable(self):
355 self.enabled0 = True
356 self.enable_disable_entrybox()
358 def disable(self):
359 self.enabled0 = False
360 self.enable_disable_entrybox()
362 def enable1(self):
363 self.enabled1 = True
364 self.enable_disable_entrybox()
366 def disable1(self):
367 self.enabled1 = False
368 self.enable_disable_entrybox()
370 def enable2(self):
371 self.enabled2 = True
372 self.enable_disable_entrybox()
374 def disable2(self):
375 self.enabled2 = False
376 self.enable_disable_entrybox()
378 def set_value(self, value:str):
379 self.value = value
380 self.entry.set(value)
381 self.validate()
383 def get_value(self):
384 if self.enabled0 and self.enabled1 and self.enabled2: return(self.value)
385 else: return("")
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#------------------------------------------------------------------------------------
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)
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)
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))
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()))
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#------------------------------------------------------------------------------------
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)
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#------------------------------------------------------------------------------------
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)
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)
537 def set_value(self, value:int, item_id:int=0):
538 self.current_item_id = item_id
539 super().set_value(value)
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#------------------------------------------------------------------------------------
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)
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)
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#------------------------------------------------------------------------------------
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)
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)
668 def set_value(self, value:str, item_id:int=0):
669 self.current_item_id = item_id
670 super().set_value(value)
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#------------------------------------------------------------------------------------
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')
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)
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()
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)
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)
770 def set_font(self, font:str, font_size:int, font_style:str):
771 self.text_box.configure(font=(font, font_size, font_style))
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#------------------------------------------------------------------------------------
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()
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)
819 def set_value(self, value:int):
820 self.current_item_id = value
821 super().set_value(value)
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#------------------------------------------------------------------------------------
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)
844 def eb_updated(self):
845 if self.EB.entry.get() == "":
846 self.CB.disable()
847 else: self.CB.enable()
849 def validate(self):
850 return (self.EB.validate())
852 def enable(self):
853 self.EB.enable()
854 self.eb_updated()
856 def disable(self):
857 self.EB.disable()
858 self.eb_updated()
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()
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()])
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#------------------------------------------------------------------------------------
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)
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()
931 def validate(self):
932 self.eb_updated()
933 return(self.EB.validate())
935 def enable(self):
936 self.EB.enable()
937 self.eb_updated()
939 def disable(self):
940 self.EB.disable()
941 self.eb_updated()
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()
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() ] ])
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#------------------------------------------------------------------------------------
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
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()
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#------------------------------------------------------------------------------------
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)
1052 def updated(self):
1053 self.frame.focus()
1054 if self.callback is not None: self.callback()
1056 def set_value(self, value:int):
1057 self.value.set(value)
1059 def get_value(self):
1060 return(self.value.get())
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#------------------------------------------------------------------------------------
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")
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
1099 def get_value(self):
1100 return(self.colour)
1102 def set_value(self,colour:str):
1103 self.colour = colour
1104 self.label2.config(bg=self.colour)
1106 def is_open(self):
1107 return(self.colour_chooser_open)
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#------------------------------------------------------------------------------------
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")
1138 def apply(self):
1139 self.window.focus()
1140 self.save_callback(False)
1142 def ok(self):
1143 self.window.focus()
1144 self.save_callback(True)
1146 def reset(self):
1147 self.window.focus()
1148 self.load_callback()
1150 def cancel(self):
1151 self.cancel_callback()
1153###########################################################################################