Skip to content

Commit 80527f1

Browse files
dsblankGaryGriffin
authored andcommitted
GrampyScript: fix histogram IndexError, add editor conveniences
Histogram chart's bucketing computed the bucket index from the raw value instead of its offset from min_val, so any dataset with negative numbers (or just not zero-anchored) could index past the end of the buckets list. Bucket count and index math now match, with the top edge (value == max_val) clamped into the last bucket. Editor additions: - Pasted (or loaded) tab characters are converted to 4 spaces. - Enter carries over the current line's indentation, adding a level after a trailing ':' and removing one after return/pass/break/ continue/raise. - Tab with a selection indents the touched lines; Shift+Tab dedents (with or without a selection). - Ctrl+/ toggles '#' comments on the current line or selection. - The filename label shows a '*' prefix while there are unsaved changes. Also updates a few example scripts to use the new row(person, ...) display shorthand.
1 parent 6cfeb01 commit 80527f1

4 files changed

Lines changed: 159 additions & 25 deletions

File tree

GrampyScript/GrampyScript.py

Lines changed: 154 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ def init(self):
305305
self.liststore = None
306306
self.text_length = 0
307307
self.chart_data = None
308+
self.converting_tabs = False
308309
self.gui.WIDGET = self.build_gui()
309310
self.gui.get_container_widget().remove(self.gui.textview)
310311
self.gui.get_container_widget().add(self.gui.WIDGET)
@@ -397,6 +398,8 @@ def build_gui(self):
397398
)
398399
self.ebuf = UndoableBuffer()
399400
self.editor_textview.set_buffer(self.ebuf)
401+
self.ebuf.connect_after("insert-text", self.on_insert_text_after)
402+
self.ebuf.connect("modified-changed", self.on_modified_changed)
400403
self.keyword_tag = self.ebuf.create_tag(
401404
"keyword", foreground="blue", weight=700
402405
)
@@ -500,8 +503,13 @@ def build_gui(self):
500503

501504
def update_filename_label(self):
502505
name = os.path.basename(self.last_filename) if self.last_filename else _("Untitled")
506+
if self.ebuf.get_modified():
507+
name = "*" + name
503508
self.filename_label.set_text(name)
504509

510+
def on_modified_changed(self, buffer):
511+
self.update_filename_label()
512+
505513
def check_unsaved_changes(self, proceed):
506514
"""
507515
If the script has unsaved changes, ask the user whether to save,
@@ -692,6 +700,21 @@ def on_buffer_changed(self, buffer):
692700
self.highlight_syntax()
693701
self.completion.on_buffer_changed()
694702

703+
def on_insert_text_after(self, buffer, text_iter, text, length):
704+
if "\t" not in text or self.converting_tabs:
705+
return
706+
self.converting_tabs = True
707+
try:
708+
end_offset = text_iter.get_offset()
709+
start_offset = end_offset - len(text)
710+
start = buffer.get_iter_at_offset(start_offset)
711+
end = buffer.get_iter_at_offset(end_offset)
712+
new_text = text.replace("\t", " ")
713+
buffer.delete(start, end)
714+
buffer.insert(buffer.get_iter_at_offset(start_offset), new_text)
715+
finally:
716+
self.converting_tabs = False
717+
695718
def highlight_syntax(self):
696719
start_iter = self.ebuf.get_start_iter()
697720
end_iter = self.ebuf.get_end_iter()
@@ -932,38 +955,147 @@ def on_editor_focus_out(self, widget, event):
932955
return False
933956

934957
def on_key_press(self, textview, event):
958+
keyval = event.keyval
959+
shift_tab = keyval == Gdk.KEY_ISO_Left_Tab or (
960+
keyval == Gdk.KEY_Tab and (event.state & Gdk.ModifierType.SHIFT_MASK)
961+
)
962+
963+
if shift_tab:
964+
self.dedent_selection()
965+
return True
966+
967+
if keyval == Gdk.KEY_Tab and self.ebuf.get_has_selection():
968+
self.indent_selection()
969+
return True
970+
935971
if self.completion.on_key_press(event):
936972
return True
937973

938-
if event.keyval == Gdk.KEY_Tab:
974+
if keyval == Gdk.KEY_Tab:
939975
# buffer = textview.get_buffer()
940976
iter_ = self.ebuf.get_iter_at_mark(self.ebuf.get_insert())
941977
self.ebuf.insert(iter_, " ") # Insert 4 spaces
942978
return True
943979

944-
elif event.keyval == Gdk.KEY_Return and (
945-
event.state & Gdk.ModifierType.MOD1_MASK
946-
):
980+
elif keyval == Gdk.KEY_Return and (event.state & Gdk.ModifierType.MOD1_MASK):
947981
self.apply_button.emit("clicked")
948982
return True
949983

950-
elif event.keyval == Gdk.KEY_c and (event.state & Gdk.ModifierType.MOD1_MASK):
984+
elif keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
985+
self.insert_auto_indent_newline()
986+
return True
987+
988+
elif keyval == Gdk.KEY_c and (event.state & Gdk.ModifierType.MOD1_MASK):
951989
self.copy_selected_text()
952990
return True
953991

954-
elif (Gdk.keyval_name(event.keyval) == "Z") and match_primary_mask(
992+
elif (Gdk.keyval_name(keyval) == "Z") and match_primary_mask(
955993
event.get_state(), Gdk.ModifierType.SHIFT_MASK
956994
):
957995
self.redo()
958996
return True
959-
elif (Gdk.keyval_name(event.keyval) == "z") and match_primary_mask(
997+
elif (Gdk.keyval_name(keyval) == "z") and match_primary_mask(
960998
event.get_state()
961999
):
9621000
self.undo()
9631001
return True
9641002

1003+
elif keyval == Gdk.KEY_slash and match_primary_mask(event.get_state()):
1004+
self.toggle_comment_selection()
1005+
return True
1006+
9651007
return False
9661008

1009+
def compute_indent_for_new_line(self, text_before_cursor):
1010+
stripped = text_before_cursor.rstrip()
1011+
indent = re.match(r"[ \t]*", text_before_cursor).group(0).replace("\t", " ")
1012+
if stripped.endswith(":"):
1013+
indent += " "
1014+
elif re.match(r"^[ \t]*(return|pass|break|continue|raise)\b", stripped):
1015+
if indent.endswith(" "):
1016+
indent = indent[:-4]
1017+
return indent
1018+
1019+
def insert_auto_indent_newline(self):
1020+
buf = self.ebuf
1021+
it = buf.get_iter_at_mark(buf.get_insert())
1022+
line_start = it.copy()
1023+
line_start.set_line_offset(0)
1024+
text_before_cursor = buf.get_text(line_start, it, True)
1025+
indent = self.compute_indent_for_new_line(text_before_cursor)
1026+
buf.insert_at_cursor("\n" + indent)
1027+
1028+
def selection_line_bounds(self):
1029+
buf = self.ebuf
1030+
if buf.get_has_selection():
1031+
sel_start, sel_end = buf.get_selection_bounds()
1032+
else:
1033+
it = buf.get_iter_at_mark(buf.get_insert())
1034+
sel_start = sel_end = it
1035+
start = buf.get_iter_at_line(sel_start.get_line())
1036+
end_line = sel_end.get_line()
1037+
if end_line > sel_start.get_line() and sel_end.get_line_offset() == 0:
1038+
# A drag-selection ending at column 0 of a line usually means
1039+
# the user didn't mean to touch that line.
1040+
end_line -= 1
1041+
end = buf.get_iter_at_line(end_line)
1042+
end.forward_to_line_end()
1043+
return start, end
1044+
1045+
def reindent_selection(self, transform):
1046+
buf = self.ebuf
1047+
start, end = self.selection_line_bounds()
1048+
start_offset = start.get_offset()
1049+
text = buf.get_text(start, end, True)
1050+
new_text = "\n".join(transform(line) for line in text.split("\n"))
1051+
if new_text == text:
1052+
return
1053+
buf.delete(start, end)
1054+
buf.insert(buf.get_iter_at_offset(start_offset), new_text)
1055+
new_start = buf.get_iter_at_offset(start_offset)
1056+
new_end = buf.get_iter_at_offset(start_offset + len(new_text))
1057+
buf.select_range(new_start, new_end)
1058+
1059+
def indent_selection(self):
1060+
self.reindent_selection(lambda line: " " + line)
1061+
1062+
def dedent_selection(self):
1063+
def dedent(line):
1064+
if line.startswith(" "):
1065+
return line[4:]
1066+
if line.startswith("\t"):
1067+
return line[1:]
1068+
return line.lstrip(" ")
1069+
1070+
self.reindent_selection(dedent)
1071+
1072+
def toggle_comment_selection(self):
1073+
buf = self.ebuf
1074+
start, end = self.selection_line_bounds()
1075+
text = buf.get_text(start, end, True)
1076+
code_lines = [line for line in text.split("\n") if line.strip()]
1077+
all_commented = bool(code_lines) and all(
1078+
line.lstrip().startswith("#") for line in code_lines
1079+
)
1080+
1081+
def comment(line):
1082+
if not line.strip():
1083+
return line
1084+
stripped = line.lstrip(" ")
1085+
indent = line[: len(line) - len(stripped)]
1086+
return indent + "# " + stripped
1087+
1088+
def uncomment(line):
1089+
stripped = line.lstrip(" ")
1090+
indent = line[: len(line) - len(stripped)]
1091+
if stripped.startswith("# "):
1092+
return indent + stripped[2:]
1093+
if stripped.startswith("#"):
1094+
return indent + stripped[1:]
1095+
return line
1096+
1097+
self.reindent_selection(uncomment if all_commented else comment)
1098+
9671099
def undo(self):
9681100
self.ebuf.undo()
9691101
self.text_length = len(self.get_text())
@@ -1120,25 +1252,30 @@ def on_draw(self, widget, cr):
11201252
min_val = min(data)
11211253
if max_val == min_val:
11221254
return
1123-
interval = (max_val - min_val) / self.chart_data[2]
1124-
buckets = [0] * (int(max_val / interval) + 1)
1255+
num_buckets = max(1, int(self.chart_data[2]))
1256+
interval = (max_val - min_val) / num_buckets
1257+
buckets = [0] * num_buckets
11251258
for value in data:
1126-
if value > max_val:
1127-
buckets[int(max_val / interval)] += 1
1128-
else:
1129-
buckets[int(value / interval)] += 1
1259+
# Bucket index is the value's offset from min_val, not
1260+
# the raw value -- otherwise negative or non-zero-based
1261+
# data lands outside the buckets list. Clamp the top
1262+
# edge (value == max_val) into the last bucket rather
1263+
# than one past it.
1264+
idx = int((value - min_val) / interval)
1265+
if idx >= num_buckets:
1266+
idx = num_buckets - 1
1267+
buckets[idx] += 1
11301268

11311269
labels = []
11321270
decimal_places = self.chart_data[3].get("decimal_places", 0)
11331271
format = "%0." + str(decimal_places) + "f"
1134-
for i in range(int(max_val / interval)):
1135-
begin = format % (i * interval)
1136-
end = format % ((i + 1) * interval)
1272+
for i in range(num_buckets):
1273+
begin = format % (min_val + i * interval)
1274+
end = format % (min_val + (i + 1) * interval)
11371275
if begin != end:
11381276
labels.append(begin + "-" + end)
11391277
else:
11401278
labels.append(begin)
1141-
labels.append(format % ((i + 1) * interval,))
11421279

11431280
# Draw a bar chart with values
11441281
bar_width = width / (len(buckets) * 1.5)

GrampyScript/scripts/01_list_people.gram.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66

77
for person in people():
88
row(
9-
person.gramps_id,
10-
person.name.first_name,
11-
person.surname.surname,
9+
person,
1210
person.gender,
11+
person.age,
1312
)

GrampyScript/scripts/07_csv_ready_report.gram.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@
55
Table tab's contents.
66
"""
77

8-
columns("ID", "Given Name", "Surname", "Gender", "Birth Year")
8+
columns("Person", "Gender", "Birth Year")
99

1010
for person in people():
1111
birth = person.birth
1212
birth_year = birth.get_date_object().get_year() if birth else ""
1313
row(
14-
person.gramps_id,
15-
person.name.first_name,
16-
person.surname.surname,
14+
person,
1715
person.gender,
1816
birth_year,
1917
)

GrampyScript/scripts/10_find_missing_birth_dates.gram.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66

77
for person in people():
88
if not person.birth:
9-
row(person.gramps_id, person.name.first_name, person.surname.surname)
9+
row(person)

0 commit comments

Comments
 (0)