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
« 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#------------------------------------------------------------------------------------
24import copy
25import importlib.resources
27import tkinter as Tk
29from . import common
30from . import objects
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#------------------------------------------------------------------------------------
39open_windows={}
41#####################################################################################
42# Classes for the Edit Line UI Elements
43#####################################################################################
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)
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()
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()
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)
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#####################################################################################
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()
169#------------------------------------------------------------------------------------
170# Functions for load, save and close window
171#------------------------------------------------------------------------------------
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()
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()
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]
225#############################################################################################