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

111 statements  

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

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

2# This module contains all the ui functions for configuring Line objects 

3#------------------------------------------------------------------------------------ 

4# 

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

6# edit_line - Open the edit line top level window 

7# 

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

9# objects.update_object(obj_id,new_obj) - Update the configuration on save 

10# objects.line_exists(item_id) - to see if a line of that ID already exists ################### 

11# 

12# Accesses the following external editor objects directly: 

13# objects.schematic_objects - To load/save the object configuration 

14# 

15# Inherits the following common editor base classes (from common): 

16# common.Createtool_tip 

17# common.window_controls 

18# common.colour_selection 

19# common.object_id_selection 

20# common.check_box 

21# 

22#------------------------------------------------------------------------------------ 

23 

24import copy 

25import importlib.resources 

26 

27import tkinter as Tk 

28 

29from . import common 

30from . import objects 

31 

32#------------------------------------------------------------------------------------ 

33# We maintain a global dictionary of open edit windows (where the key is the UUID 

34# of the object being edited) to prevent duplicate windows being opened. If the user 

35# tries to edit an object which is already being edited, then we just bring the 

36# existing edit window to the front (expanding if necessary) and set focus on it 

37#------------------------------------------------------------------------------------ 

38 

39open_windows={} 

40 

41##################################################################################### 

42# Classes for the Edit Line UI Elements 

43##################################################################################### 

44 

45class line_attributes(): 

46 def __init__(self, parent_window): 

47 # Create a labelframe to hold the tkinter widgets 

48 # The parent class is responsible for packing the frame 

49 self.frame= Tk.LabelFrame(parent_window,text="Attributes") 

50 # The Tk IntVar to hold the line end selection 

51 self.selection = Tk.IntVar(self.frame, 0) 

52 # Define the Available selections [filename,configuration] 

53 self.selections = [ ["None",[0,0,0] ], 

54 ["endstop",[1,1,1] ], 

55 ["arrow1",[20,20,5] ], 

56 ["arrow2",[16,20,5] ], 

57 ["arrow3",[20,20,8] ], 

58 ["arrow4",[16,20,8] ] ] 

59 # Create a frame for the radiobuttons 

60 self.subframe2 = Tk.Frame(self.frame) 

61 self.subframe2.pack() 

62 # Create the buttons we need (adding the references to the buttons, tooltips and 

63 # images to a list so they don't go out of scope and dont get garbage collected) 

64 self.buttons = [] 

65 self.tooltips = [] 

66 self.images = [] 

67 tooltip = " Select the style to apply to one or both line ends" 

68 resource_folder = 'model_railway_signals.editor.resources' 

69 for index, button in enumerate (self.selections): 

70 file_name = button[0] 

71 try: 

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

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

74 self.images.append(Tk.PhotoImage(file=file_path)) 

75 self.buttons.append(Tk.Radiobutton(self.subframe2, anchor='w',indicatoron=0, 

76 command=self.selection_updated, variable=self.selection, value=index)) 

77 self.buttons[-1].config(image=self.images[-1]) 

78 except: 

79 # Else fall back to using a text label (and use a standard radio button) 

80 self.buttons.append(Tk.Radiobutton(self.subframe2, text=file_name, anchor='w', 

81 command=self.selection_updated, variable=self.selection, value=index)) 

82 self.buttons[-1].pack(side=Tk.LEFT, padx=2, pady=2) 

83 self.tooltips.append(common.CreateToolTip(self.buttons[-1], tooltip)) 

84 # Create a frame for the two side by side checkboxes 

85 self.subframe2 = Tk.Frame(self.frame) 

86 self.subframe2.pack() 

87 self.CB1 = common.check_box(self.subframe2,label="Apply to start", 

88 tool_tip="Select to apply the style to the start of the line") 

89 self.CB1.pack(side=Tk.LEFT, padx=2, pady=2) 

90 self.CB2 = common.check_box(self.subframe2,label="Apply to end", 

91 tool_tip="Select to apply the style to the end of the line") 

92 self.CB2.pack(side=Tk.LEFT, padx=2, pady=2) 

93 

94 def selection_updated(self): 

95 if self.selection.get() > 0: 

96 self.CB1.enable() 

97 self.CB2.enable() 

98 else: 

99 self.CB1.disable() 

100 self.CB2.disable() 

101 

102 def set_values(self, arrow_ends, arrow_type): 

103 # Set the arrow ends (Default will remain '0' if not supported) 

104 for index, selection_to_test in enumerate (self.selections): 104 ↛ 109line 104 didn't jump to line 109, because the loop on line 104 didn't complete

105 if selection_to_test[1] == arrow_type: 

106 self.selection.set(index) 

107 break 

108 # Set the arrowe type (0=none, 1=start, 2=end, 3=both) 

109 boolean_list = [bool(arrow_ends & (1<<n)) for n in range(2)] 

110 self.CB1.set_value(boolean_list[0]) 

111 self.CB2.set_value(boolean_list[1]) 

112 self.selection_updated() 

113 

114 def get_values(self): 

115 # arrow_ends is a list of 3 values defining the arrow head 

116 # Case of 'None' or 'End-Stops' will be returned as [0,0,0] 

117 # arrow_type is either 0=none, 1=start, 2=end, 3=both 

118 arrow_type = self.selections[self.selection.get()][1] 

119 boolean_list = [self.CB2.get_value(),self.CB1.get_value()] 

120 arrow_ends = sum(v << i for i, v in enumerate(boolean_list[::-1])) 

121 return(arrow_ends,arrow_type) 

122 

123##################################################################################### 

124# Top level Class for the Edit Line window 

125# This window doesn't have any tabs (unlike the other object configuration windows) 

126##################################################################################### 

127 

128class edit_line(): 

129 def __init__(self, root, object_id): 

130 global open_windows 

131 # If there is already a window open then we just make it jump to the top and exit 

132 if object_id in open_windows.keys(): 132 ↛ 133line 132 didn't jump to line 133, because the condition on line 132 was never true

133 open_windows[object_id].lift() 

134 open_windows[object_id].state('normal') 

135 open_windows[object_id].focus_force() 

136 else: 

137 # This is the UUID for the object being edited 

138 self.object_id = object_id 

139 # Create the (non-resizable) top level window 

140 self.window = Tk.Toplevel(root) 

141 self.window.protocol("WM_DELETE_WINDOW", self.close_window) 

142 self.window.resizable(False, False) 

143 open_windows[object_id] = self.window 

144 # Create a frame to hold all UI elements (so they don't expand on window resize 

145 # to provide consistent behavior with the other configure object popup windows) 

146 self.main_frame = Tk.Frame(self.window) 

147 self.main_frame.pack() 

148 # Create a Frame to hold the Line ID and Line Colour Selections 

149 self.frame = Tk.Frame(self.main_frame) 

150 self.frame.pack(padx=2, pady=2, fill='x') 

151 # Create the UI Element for Line ID selection 

152 self.lineid = common.object_id_selection(self.frame, "Line ID", 

153 exists_function = objects.line_exists) 

154 self.lineid.frame.pack(side=Tk.LEFT, padx=2, pady=2, fill='y') 

155 # Create the line colour selection element 

156 self.colour = common.colour_selection(self.frame, label="Colour") 

157 self.colour.frame.pack(padx=2, pady=2, fill='x') 

158 # Create the line Attributes UI Element 

159 self.attributes = line_attributes(self.main_frame) 

160 self.attributes.frame.pack(padx=2, pady=2) 

161 # Create the common Apply/OK/Reset/Cancel buttons for the window 

162 self.controls = common.window_controls(self.window, self.load_state, self.save_state, self.close_window) 

163 self.controls.frame.pack(padx=2, pady=2) 

164 # Create the Validation error message (this gets packed/unpacked on apply/save) 

165 self.validation_error = Tk.Label(self.window, text="Errors on Form need correcting", fg="red") 

166 # load the initial UI state 

167 self.load_state() 

168 

169#------------------------------------------------------------------------------------ 

170# Functions for load, save and close window 

171#------------------------------------------------------------------------------------ 

172 

173 def load_state(self): 

174 # Check the line we are editing still exists (hasn't been deleted from the schematic) 

175 # If it no longer exists then we just destroy the window and exit without saving 

176 if self.object_id not in objects.schematic_objects.keys(): 176 ↛ 177line 176 didn't jump to line 177, because the condition on line 176 was never true

177 self.close_window() 

178 else: 

179 item_id = objects.schematic_objects[self.object_id]["itemid"] 

180 # Label the edit window 

181 self.window.title("Line "+str(item_id)) 

182 # Set the Initial UI state from the current object settings 

183 self.lineid.set_value(item_id) 

184 self.colour.set_value(objects.schematic_objects[self.object_id]["colour"]) 

185 arrow_type = objects.schematic_objects[self.object_id]["arrowtype"] 

186 arrow_ends = objects.schematic_objects[self.object_id]["arrowends"] 

187 self.attributes.set_values(arrow_ends, arrow_type) 

188 # Hide the validation error message 

189 self.validation_error.pack_forget() 

190 return() 

191 

192 def save_state(self, close_window:bool): 

193 # Check the object we are editing still exists (hasn't been deleted from the schematic) 

194 # If it no longer exists then we just destroy the window and exit without saving 

195 if self.object_id not in objects.schematic_objects.keys(): 195 ↛ 196line 195 didn't jump to line 196, because the condition on line 195 was never true

196 self.close_window() 

197 # Validate all user entries prior to applying the changes. Each of these would have 

198 # been validated on entry, but changes to other objects may have been made since then 

199 elif self.lineid.validate(): 199 ↛ 215line 199 didn't jump to line 215, because the condition on line 199 was never false

200 # Copy the original object Configuration (elements get overwritten as required) 

201 new_object_configuration = copy.deepcopy(objects.schematic_objects[self.object_id]) 

202 # Update the object coniguration elements from the current user selections 

203 new_object_configuration["itemid"] = self.lineid.get_value() 

204 new_object_configuration["colour"] = self.colour.get_value() 

205 arrow_ends, arrow_type = self.attributes.get_values() 

206 new_object_configuration["arrowtype"] = arrow_type 

207 new_object_configuration["arrowends"] = arrow_ends 

208 # Save the updated configuration (and re-draw the object) 

209 objects.update_object(self.object_id, new_object_configuration) 

210 # Close window on "OK" or re-load UI for "apply" 

211 if close_window: self.close_window() 

212 else: self.load_state() 

213 else: 

214 # Display the validation error message 

215 self.validation_error.pack(side=Tk.BOTTOM, before=self.controls.frame) 

216 return() 

217 

218 def close_window(self): 

219 # Prevent the dialog being closed if the colour chooser is still open as 

220 # for some reason this doesn't get destroyed when the parent is destroyed 

221 if not self.colour.is_open(): 221 ↛ exitline 221 didn't return from function 'close_window', because the condition on line 221 was never false

222 self.window.destroy() 

223 del open_windows[self.object_id] 

224 

225#############################################################################################