Home

Tuesday, January 25, 2022

Python 3 - Win10: Tkinter 1 - Word Processor - Text Widget - Font Styling 1

#Python 3 - Win10

Tkinter Word Processor

Text Widget Font Styling 1

Brief:

_______________________________________________________________
This script is a slice of my word processor project. I'm trying to get styling to function and feel good, or at least something like Microsoft 365 Word.
 
It feels like I'm groping around in the dark to complete the project. The built-in tkinter functions don't seem to be of use for acquired and affecting the data I want. Which I think means I don't understand how they are meant to be employed. The workings of tkinter are also a bit of a black box for me, which is frustrating to say the least.

 

Functionality:

_______________________________________________________________
Improvements:
  • N/A
Issues:
  • Continuation of previous tags
    • While typing quickly continuation of previous tags is lost
    • New text is briefly displayed as default before previous tags are applied
    • Note: this is not the case for text inserted within the existing domain of the tag

 

Demo:

_______________________________________________________________
Functionality Test:
  • General tag handling
    1. Tag / untag a range
    2. Fully tag a partially tagged range
    3. Untag a fully tagged range
  • Continue text
    • Get previous adjacent tags
    • If no adjacent text, step back through spaces and newlines until text is found
  • Bold text
  • Underline text
  • Overstrike text
  • Change font
    • Revert to previous font


 

 

Code Snippet: 

_______________________________________________________________ 
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import tkinter as tk


class Document(tk.Text):
	
	#@! Class Variables
	#================================================================================
	children     = []
	old_insert   = "1.0"
	carrot_moved = False


	#@! Constructor
	#================================================================================
	def __init__(self, *args, **kewargs):
		super().__init__(*args, **kewargs)
		# print(f"<{self.__class__.__name__}> object created <{id(self)}> ")
		Document.children.append(id(self))
		self.set_bindings()
		self.old_insert = "1.0"

		# Break bindings for special window hotkeys e.g. 'ctrl-i'
		self.tag_init()


	#@! Utility
	#================================================================================
	
	def get_sel_index(self, event=None) -> (str, str):
		
		try:
			sel_first = self.index(f"{tk.SEL}.first")
			sel_last  = self.index(f"{tk.SEL}.last")
		except:
			sel_first = self.index(f"{tk.INSERT}")
			sel_last  = self.index(f"{tk.INSERT} +1c")

		return sel_first, sel_last
	

	#@! Tags
	#================================================================================
	
	def tag_init(self):
		tag_list = {
			   'regular':{
				     '<b>' :{'foreground':'red'},
				     '<u>' :{'underline':1, 'underlinefg':'green'},
				     '<os>':{'overstrike':1, 'overstrikefg':'red'},
				     '<jl>':{'justify':'left'},
				     '<jc>':{'justify':'center'},
				     '<jr>':{'justify':'right'},
				     '<f>' :{'foreground':'red', 'font':'Helvetica 18 bold underline overstrike italic'},
				     },
			   }
		for tag in tag_list['regular']:
			self.tag_add(tag, "1.0", "1.2")
			self.tag_config(tag, tag_list['regular'][tag])

		# Broken
		# print(self.tag_config('<f>'))
	

	# Step 2 get only the ranges that intersect the selection
	def tag_ranges_in_sel(self, tag:str, start:str="", end:str="") -> [int]:
		"""Returns list of first index of intersection tag range"""
		# def overthinking_it(self, tag, start, end):
			# Q: What are we really after with all these functions?
			# A: 1 finding if there is a tag in the selected range
			# A: 2 finding if the full range IS tagged or NOT
			# A: 3 finding if part(s) of the selection range ARE tagged
			# Q: what built in tools are there to do that job?
			# A: tag_ranges()
			# Q: are they good enough?
			# A: maybe?
		
		# guard statement
		ranges = self.tag_ranges(tag)
		if () == ranges: return []

		if start == "":
			start, end = self.get_sel_index()

		intersections = []
		
		for index in range(0, len(ranges), 2):
			range_start = ranges[index]
			range_end   = ranges[index+1]
		 	
		 	# Q: We want to know if; range does or does NOT intersect selection.
			if self.compare(range_start, "<", end) and self.compare(range_end, ">", start):
				intersections.append(index) # print("range is IN selection")
			else:
				pass # print("range is OUTside selection")

		return intersections


	# Step 3 determine if tag range encompasses the selection
	def tag_range_match_sel(self, tag:str, range_indices:[], start:str="", end:str="") -> bool:
		"""Returns True if tag range matches selection"""
		
		# guard statement
		ranges = self.tag_ranges(tag)
		if () == ranges: return False
		
		if start == "":
			start, end = self.get_sel_index()

		if 1 == len(range_indices):
			index 		= range_indices[0]
			range_start = ranges[index]
			range_end 	= ranges[index+1]
			
			if self.compare(range_start, "<=", start) and self.compare(range_end, ">=", end):
				return True
		
		return False


	def check_tags(self, event=None):
		ranges = self.tag_ranges_in_sel('bold')
		self.tag_range_match_sel('bold', ranges)


	def bold(self, event=None) -> None:
		self.set_tag('<b>')
	

	def underline(self, event=None) -> None:
		self.set_tag('<u>')
	

	def overstrike(self, event=None) -> None:
		self.set_tag('<os>')
	

	def justify(self, event=None) -> None:
		self.set_tag('<jl>')
	

	def fon(self, event=None) -> None:
		self.set_tag('<f>')
		self.tag_raise('<f>')


	def set_tag(self, tag:str, event=None) -> None:
		sel_first, sel_last = self.get_sel_index()
		tag_detected = self.tag_ranges_in_sel(tag)
		part_tagged = self.tag_range_match_sel(tag, tag_detected)
		
		if not tag_detected or not part_tagged:
			self.tag_add(tag, sel_first, sel_last)
		else:
			self.tag_remove(tag, sel_first, sel_last)


	def carry_over(self, step):
		"""Continues a style"""
		index = self.index(f"{tk.INSERT} -{step}c")
		char = self.get(index)
		while '\n' == char or ' ' == char:
			if "1.0" == index:
				return
			index = self.index(f"{index} -1c")
			char = self.get(index)

		tags = self.tag_names(index)
		
		for tag in tags:
			self.tag_add(tag, index, tk.INSERT)


	def valid_over(self, event, step):
		# ISSUE: update is not fast enough, style only continues if typing is slow
		ignore = ['space', 'Left', 'Right', 'Up', 'Down', 'Control_L', 'Control_R', 'End', 'Home', 'Prior', 'Next']
		
		if event.keysym not in ignore:
			self.carry_over(step)


	#@! Status Info
	#================================================================================
	def carrot_move(self, event=None):
		new_insert = self.index(tk.INSERT)
		
		if self.old_insert != new_insert:
			self.old_insert = new_insert
			carrot_moved = True
			print("carrot_move", new_insert) # update index on release
		else:
			carrot_moved = False


	#@! Inputs
	#================================================================================
	
	def set_bindings(self):
		self.bind("<Key>", self.any_key_down)
		self.bind("<KeyRelease>", self.any_key_up)
		self.bind("<Control-b>", self.bold)
		self.bind("<Control-u>", self.underline)
		self.bind("<Control-f>", self.fon)
		self.bind("<Control-minus>", self.overstrike)
		self.bind("<Control-bracketright>", self.overstrike)


	def any_key_down(self, event=None):
		self.valid_over(event, 1)
	

	def any_key_up(self, event=None):
		self.carrot_move(event)
		self.valid_over(event, 2)
		


root = tk.Tk()
text = Document(root, height=4)
text.pack()

text.insert("1.0", "This is helvetica font\n", "font_hel")
text.insert("1.0", "This is terminal font\n", "font_term")
text.tag_config('font_hel', font='Helvetica 12')
text.tag_config('font_term', font='Terminal 12')

root.mainloop()


Credits and Resources:

Code formatted via: http://hilite.me/ : Styling: Python, monokai
 

-END-

No comments:

Post a Comment

Conduct: Be nice and don't advertise or plug without adding value to the conversation.