Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/editor/configure_textbox.py: 89%
120 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-05 17:29 +0100
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-05 17:29 +0100
1#------------------------------------------------------------------------------------
2# This module contains all the ui functions for configuring Textbox objects
3#------------------------------------------------------------------------------------
4#
5# External API functions intended for use by other editor modules:
6# edit_textbox - Open the edit textbox 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#
11# Accesses the following external editor objects directly:
12# objects.schematic_objects - To load/save the object configuration
13#
14# Inherits the following common editor base classes (from common):
15# common.integer_entry_box
16# common.scrollable_text_frame
17# common.window_controls
18# common.colour_selection
19# common.selection_buttons
20# common.check_box
21#
22#------------------------------------------------------------------------------------
24import copy
26import tkinter as Tk
28from . import common
29from . import objects
31#------------------------------------------------------------------------------------
32# We maintain a global dictionary of open edit windows (where the key is the UUID
33# of the object being edited) to prevent duplicate windows being opened. If the user
34# tries to edit an object which is already being edited, then we just bring the
35# existing edit window to the front (expanding if necessary) and set focus on it
36#------------------------------------------------------------------------------------
38open_windows={}
40#####################################################################################
41# Classes for the Edit Text Box UI Elements
42#####################################################################################
44class text_style_entry():
45 def __init__(self, parent_frame, callback):
46 # Create a Frame for the Text Style Entry components
47 self.frame = Tk.LabelFrame(parent_frame,text="Text Style")
48 # Create a subframe to hold the font size and border configuration elements
49 self.subframe1 = Tk.Frame(self.frame)
50 self.subframe1.pack()
51 self.label1 = Tk.Label(self.subframe1, text="Font size")
52 self.label1.pack(padx=2, pady=2, fill='x', side=Tk.LEFT)
53 self.fontsize = common.integer_entry_box(self.subframe1, width=3, min_value=4, max_value=24,
54 tool_tip="Select font size (between 4 and 24)", callback=callback, allow_empty=False)
55 self.fontsize.pack(padx=2, pady=2, fill='x', side=Tk.LEFT)
56 self.label2 = Tk.Label(self.subframe1, text=" Border width")
57 self.label2.pack(padx=2, pady=2, fill='x', side=Tk.LEFT)
58 self.border = common.integer_entry_box(self.subframe1, width=3, min_value=0, max_value=5, allow_empty=False,
59 tool_tip="Select border width between 0 and 5 (0 to disable border)", callback=callback)
60 self.border.pack(padx=2, pady=2, fill='x', side=Tk.LEFT)
61 # Create a subframe to hold the bold, italic and underline configuration elements
62 self.subframe2 = Tk.Frame(self.frame)
63 self.subframe2.pack()
64 self.bold = common.check_box(self.subframe2,label="Bold",tool_tip="Bold text",callback=callback)
65 self.bold.pack(padx=2, pady=2, fill='x', side=Tk.LEFT)
66 self.italic = common.check_box(self.subframe2,label="Italic",tool_tip="Italic text",callback=callback)
67 self.italic.pack(padx=2, pady=2, fill='x', side=Tk.LEFT)
68 self.underline = common.check_box(self.subframe2,label="Underline",tool_tip="Underline text",callback=callback)
69 self.underline.pack(padx=2, pady=2, fill='x', side=Tk.LEFT)
71 def set_values(self, font:str, font_size:int, font_style:str, border:int):
72 self.font = font
73 self.fontsize.set_value(font_size)
74 self.bold.set_value("bold" in font_style)
75 self.italic.set_value("italic" in font_style)
76 self.underline.set_value("underline" in font_style)
77 self.border.set_value(border)
79 def get_values(self):
80 font_style = ""
81 if self.bold.get_value(): font_style=font_style + "bold "
82 if self.italic.get_value(): font_style=font_style + "italic "
83 if self.underline.get_value(): font_style=font_style + "underline "
84 return (self.font, self.fontsize.get_value(),font_style, self.border.get_value())
86 def validate(self):
87 return (self.fontsize.validate() and self.border.validate() )
89#####################################################################################
90# Top level Class for the Edit Textbox window
91# This window doesn't have any tabs (unlike other object configuration windows)
92#####################################################################################
94class edit_textbox():
95 def __init__(self, root, object_id):
96 global open_windows
97 # If there is already a window open then we just make it jump to the top and exit
98 if object_id in open_windows.keys(): 98 ↛ 99line 98 didn't jump to line 99, because the condition on line 98 was never true
99 open_windows[object_id].lift()
100 open_windows[object_id].state('normal')
101 open_windows[object_id].focus_force()
102 else:
103 # This is the UUID for the object being edited
104 self.object_id = object_id
105 # Creatre the basic Top Level window
106 self.window = Tk.Toplevel(root)
107 self.window.protocol("WM_DELETE_WINDOW", self.close_window)
108 self.window.resizable(False, False)
109 open_windows[object_id] = self.window
110 # Create the common Apply/OK/Reset/Cancel buttons for the window (packed first to remain visible)
111 self.controls = common.window_controls(self.window, self.load_state, self.save_state, self.close_window)
112 self.controls.frame.pack(side=Tk.BOTTOM, padx=2, pady=2)
113 # Create the Validation error message (this gets packed/unpacked on apply/save)
114 self.validation_error = Tk.Label(self.window, text="Errors on Form need correcting", fg="red")
115 # Create a frame to hold all UI elements (so they don't expand on window resize
116 # to provide consistent behavior with the other configure object popup windows)
117 self.main_frame = Tk.Frame(self.window)
118 self.main_frame.pack(fill='both', expand=True)
119 self.frame1 = Tk.Frame(self.main_frame)
120 self.frame1.pack(padx=2, pady=2, fill='both', expand=True)
121 self.text = common.scrollable_text_frame(self.frame1, max_height=15,max_width=50,
122 min_height=1, min_width=10, editable=True, auto_resize=True)
123 self.text.pack(padx=2, pady=2, fill='both', expand=True)
124 # Create a Frame for the colour selections
125 self.frame2 = Tk.Frame(self.main_frame)
126 self.frame2.pack(padx=2, pady=2, fill='x')
127 # Create the text colour and text background colour selection elements
128 self.colour = common.colour_selection(self.frame2, label="Text colour")
129 self.colour.frame.pack(padx=2, pady=2, fill='x', side=Tk.LEFT, expand=1)
130 self.background = common.colour_selection(self.frame2, label="Background colour")
131 self.background.frame.pack(padx=2, pady=2, fill='x', side=Tk.LEFT, expand=1)
132 # Create a Frame for the Text Justification
133 self.frame3 = Tk.Frame(self.main_frame)
134 self.frame3.pack(padx=2, pady=2, fill='x')
135 # Use radio buttons for the text justification selection
136 self.justify = common.selection_buttons(self.frame3, label="Text Justification",
137 tool_tip= "select text justification", b1="Left", b2="Centre", b3="Right",
138 callback=self.justification_updated)
139 self.justify.frame.pack(padx=2, pady=2, fill='x')
140 # Create a Frame for the Text Style Entry widgey
141 self.textstyle = text_style_entry (self.main_frame, callback = self.text_style_updated)
142 self.textstyle.frame.pack(padx=2, pady=2, fill='x')
143 # load the initial UI state
144 self.load_state()
146 def justification_updated(self):
147 self.text.set_justification(self.justify.get_value())
149 def text_style_updated(self):
150 font, font_size, font_style, border = self.textstyle.get_values()
151 self.text.set_font (font, font_size, font_style)
153 def load_state(self):
154 # Check the object we are editing still exists (hasn't been deleted from the schematic)
155 # If it no longer exists then we just destroy the window and exit without saving
156 if self.object_id not in objects.schematic_objects.keys(): 156 ↛ 157line 156 didn't jump to line 157, because the condition on line 156 was never true
157 self.close_window()
158 else:
159 # Label the edit window
160 self.window.title("Textbox")
161 # Set the Initial UI state from the current object settings
162 self.text.set_value(objects.schematic_objects[self.object_id]["text"])
163 self.colour.set_value(objects.schematic_objects[self.object_id]["colour"])
164 self.background.set_value(objects.schematic_objects[self.object_id]["background"])
165 justify = objects.schematic_objects[self.object_id]["justify"]
166 font = objects.schematic_objects[self.object_id]["font"]
167 font_size = objects.schematic_objects[self.object_id]["fontsize"]
168 font_style = objects.schematic_objects[self.object_id]["fontstyle"]
169 border = objects.schematic_objects[self.object_id]["border"]
170 self.textstyle.set_values (font, font_size, font_style, border)
171 self.justify.set_value(justify)
172 # Justify the text and resize the font to match the initial selection
173 self.text.set_justification(justify)
174 self.text.set_font(font, font_size, font_style)
175 # Hide the validation error message
176 self.validation_error.pack_forget()
177 return()
179 def save_state(self, close_window:bool):
180 # Check the object we are editing still exists (hasn't been deleted from the schematic)
181 # If it no longer exists then we just destroy the window and exit without saving
182 if self.object_id not in objects.schematic_objects.keys(): 182 ↛ 183line 182 didn't jump to line 183, because the condition on line 182 was never true
183 self.close_window()
184 elif self.textstyle.validate(): 184 ↛ 204line 184 didn't jump to line 204, because the condition on line 184 was never false
185 # Copy the original object Configuration (elements get overwritten as required)
186 new_object_configuration = copy.deepcopy(objects.schematic_objects[self.object_id])
187 # Update the object coniguration elements from the current user selections
188 new_object_configuration["text"] = self.text.get_value()
189 new_object_configuration["colour"] = self.colour.get_value()
190 new_object_configuration["background"] = self.background.get_value()
191 new_object_configuration["justify"] = self.justify.get_value()
192 font, font_size, font_style, border = self.textstyle.get_values()
193 new_object_configuration["font"] = font
194 new_object_configuration["fontsize"] = font_size
195 new_object_configuration["fontstyle"] = font_style
196 new_object_configuration["border"] = border
197 # Save the updated configuration (and re-draw the object)
198 objects.update_object(self.object_id, new_object_configuration)
199 # Close window on "OK" or re-load UI for "apply"
200 if close_window: self.close_window()
201 else: self.load_state()
202 else:
203 # Display the validation error message
204 self.validation_error.pack(side=Tk.BOTTOM, before=self.controls.frame)
205 return()
207 def close_window(self):
208 # Prevent the dialog being closed if the colour chooser is still open as
209 # for some reason this doesn't get destroyed when the parent is destroyed
210 if not self.colour.is_open() and not self.background.is_open(): 210 ↛ exitline 210 didn't return from function 'close_window', because the condition on line 210 was never false
211 self.window.destroy()
212 del open_windows[self.object_id]
214#############################################################################################