-
Notifications
You must be signed in to change notification settings - Fork 0
/
webster123pro-v1.1.0.py
1792 lines (1448 loc) · 69.9 KB
/
webster123pro-v1.1.0.py
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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Required Imports
import pandas as pd
import sqlite3
import os
import tkinter as tk
from tkinter import simpledialog, filedialog, messagebox, Menu, font, Label, Toplevel
import webbrowser
import datetime
from bs4 import BeautifulSoup
import requests
import re
import re
import unicodedata
# Define the path to the logo
#logo_path = "c:/websterpy/.venv/webster-logo-web.png"
global_df = None # Global variable to store the DataFrame
current_selection = None
clipboard_data = None
shift_start_row = None # Variable to store the starting row for shift-click selection
db_file = None # No default database file name
# Define the column order
columns = [
"siteId", "folderPath", "ourUrl", "ourTitle", "ourContent", "Extra1", "Extra2", "topMenu",
"ourHeader", "ourFooter", "styleSheet", "scriptsUrl", "fileExtension", "ourMeta", "shareImageUrl",
"Website", "websiteUrl", "Icon", "topHtml", "headTag", "ourShareButton", "useLinkBox", "directoryMode", "frontPage"
]
# Define initial values
initial_data = {
"ourUrl": ["double-clk-to-edit"],
"folderPath": ["c:\\webster123"],
"fileExtension": ["html"],
"scriptsUrl": [""],
"ourTitle": [""],
"ourContent": [f"""<center><h2>Congratulations!</h2><p>You've published your first page. <br>Change it to your html to get started.<br>This is Webster123pro-with-Swami v1.0.8<br>Visit <a href="https://webster123.com/">Webster123.com</a> for instructions.</p></center>"""],
"Extra1": [""],
"Extra2": [""],
"siteId": ["My Site"],
"topMenu": [""],
"ourHeader": [f"""<img src="https://webster123.com/webster-logo-web.jpg">"""],
"ourFooter": [""],
"directoryMode": ["False"],
"shareImageUrl": [""],
"ourMeta": [""],
"Website": [""],
"websiteUrl": [""],
"styleSheet": [""],
"Icon": [""],
"topHtml": [""],
"headTag": [""],
"ourShareButton": [""],
"useLinkBox": ["False"],
"frontPage": ["False"],
}
# Add 20 extra empty rows
for _ in range(20):
for key in initial_data.keys():
initial_data[key].append("")
# Column configuration
column_config = {
"ourUrl": {"width": 220, "instructions": "Words with a dash between them, no special characters"},
"folderPath": {"width": 100, "instructions": "The folder on your local where you wish to store the pages you create. Like C:\\webster123"},
"fileExtension": {"width": 100, "instructions": "html or php"},
"ourTitle": {"width": 100, "instructions": "The title of your web page."},
"ourContent": {"width": 100, "instructions": "Html content"},
"Extra1": {"width": 100, "instructions": "Extra Html content"},
"Extra2": {"width": 100, "instructions": "Extra Html content"},
"siteId": {"width": 100, "instructions": "Your site Id, Which site is this?"},
"topMenu": {"width": 100, "instructions": "Our menu entries are Anchor links stacked on top of each other."},
"ourHeader": {"width": 100, "instructions": "Html for the header of the website."},
"ourFooter": {"width": 100, "instructions": "Html for the Footer of our site."},
"directoryMode": {"width": 100, "instructions": "False and we produce a url like example.html. True and we create a folder example/ and put an index page in it.."},
"shareImageUrl": {"width": 100, "instructions": "The url of your share image"},
"ourMeta": {"width": 100, "instructions": "The meta Description of your page."},
"Website": {"width": 100, "instructions": "yoursite.com"},
"websiteUrl": {"width": 100, "instructions": "Website URL. Must have trailing slash '/', like https://yoursite.com/"},
"styleSheet": {"width": 100, "instructions": "The url of your stylesheet file. On your local drive it can look like file:///c:/Stylesheets/mystylesheet.css This way you can work with a stylesheet on your drive. When you publish the page on the internet, you can change it to something like https://mysite.com/mystylesheet.css"},
"Icon": {"width": 100, "instructions": "The website icon, usually 100x100px"},
"topHtml": {"width": 100, "instructions": "Inserted after <html>"},
"headTag": {"width": 100, "instructions": "Inserted after <head>"},
"ourShareButton": {"width": 100, "instructions": "AddtoAny Share Button. Leave blank to not use."},
"useLinkBox": {"width": 100, "instructions": "If True, a Link To This Page Box will be added"},
"scriptsUrl": {"width": 100, "instructions": "The url of your java script file. On your local drive it can look like file:///c:/Scriptsfolder/myscript.js This way you can work with a script on your drive. When you publish the page on the internet, you can change it to something like https://mysite.com/myscript.js"},
}
color_config = {
#darktheme
'cell_bg': '#0B161D',
'cell_fg': 'white',
'header_bg': '#181818',
'header_fg': 'white',
'row_number_bg': '#181818',
'row_number_fg': 'white',
'highlight_bg': '#41525D',
'highlight_fg': 'white',
'line_color': 'lightblue',
'highlight_line_color': 'lightblue',
'menu_bg': '#F8F8F8',
'menu_fg': 'white',
'frame_bg': '#181818',
'progress_text_bg': '#0B161D',
'progress_text_fg': 'white',
'button_bg': 'lightgrey',
'button_fg': '#181818',
'menu_bar_bg': '#0B161D',
'menu_bar_fg': 'white',
'logo_frame_bg': '#181818',
'clear_button_frame_bg': '#181818',
'cursor_color_edit_cell': 'white'
}
global_df = pd.DataFrame([["1", "2"], ["3", "4"]], columns=["A", "B"])
# Add visual settings
def set_visual_settings(root):
root.configure(bg="black") # Background color of the main window
def set_font_settings():
return font.Font(family="Arial", size=10, weight="normal", slant="roman")
# Undo/Redo Manager
class UndoRedoManager:
def __init__(self):
self.history = []
self.redo_stack = []
self.current_state = None
def save_state(self, state):
if self.current_state is not None:
self.history.append(self.current_state)
self.current_state = state.copy()
self.redo_stack.clear()
def undo(self):
if self.history:
self.redo_stack.append(self.current_state)
self.current_state = self.history.pop()
return self.current_state
return None
def redo(self):
if self.redo_stack:
self.history.append(self.current_state)
self.current_state = self.redo_stack.pop()
return self.current_state
return None
undo_manager = UndoRedoManager()
#main menu
class SimpleTable(tk.Canvas):
def right_click_menu(self, event, row):
menu = Menu(self, tearoff=0)
menu.add_command(label="Insert Row", command=lambda: self.insert_row(row))
menu.add_command(label="Delete Row", command=lambda: self.delete_row(row))
menu.add_command(label="Copy Selection", command=self.copy_selection)
menu.add_command(label="5. Publish Selected Rows", command=self.publish_selected_rows)
menu.add_command(label="6. View Selected Rows", command=self.view_selected_rows)
menu.post(event.x_root, event.y_root)
def publish_selected_rows(self):
global current_selection
if current_selection is not None:
row1, col1, row2, col2 = current_selection
rows_to_publish = list(range(min(row1, row2) + 1, max(row1, row2) + 2))
result = publish_rows_to_disk(",".join(map(str, rows_to_publish)))
def view_selected_rows(self):
global current_selection
if current_selection is not None:
row1, col1, row2, col2 = current_selection
rows_to_view = list(range(min(row1, row2) + 1, max(row1, row2) + 2))
result = view_html_pages(",".join(map(str, rows_to_view)))
#copy here
def __init__(self, parent, rows=10, cols=5, font_size=12, color_config=None):
super().__init__(parent)
self.parent = parent
self.rows = rows
self.cols = cols
self.cells = {}
self.headers = []
self.row_numbers = []
self.header_height = 30
self.row_number_width = 50
self.font_size = font_size
self.font = font.Font(family="Arial", size=10, weight="normal", slant="roman")
self.cell_height = self.font.metrics("linespace") + 10
# Set default color configuration if none is provided
self.color_config = color_config if color_config else {
'cell_bg': 'black',
'cell_fg': 'white',
'header_bg': 'gray',
'header_fg': 'white',
'row_number_bg': 'gray',
'row_number_fg': 'white',
'highlight_bg': 'lightyellow',
'highlight_fg': 'black',
'line_color': 'white',
'highlight_line_color': 'red',
'menu_bg': '#dcdcdc',
'menu_fg': 'black',
'frame_bg': '#f0f0f0',
'progress_text_bg': 'white',
'progress_text_fg': 'black',
'button_bg': 'lightgrey',
'button_fg': 'black'
}
self.selection_rects = []
self.start_row = None
self.edit_window = None # Track the edit window
self.clipboard_data = None # To store single cell value for pasting
self.header_canvas = tk.Canvas(parent, height=self.header_height, bg=self.color_config['header_bg'])
self.row_number_canvas = tk.Canvas(parent, width=self.row_number_width, bg=self.color_config['row_number_bg'])
self.header_canvas.grid(row=0, column=1, sticky='ew')
self.row_number_canvas.grid(row=1, column=0, sticky='ns')
self.grid(row=1, column=1, sticky='nsew')
self.create_widgets()
self.bind_shortcuts()
self.find_start_row = 0 # Initialize find_start_row
self.find_text = "" # Initialize find_text
self.find_replace_dialog = None # Initialize find_replace_dialog
self.xview_moveto(0)
self.yview_moveto(0)
self.bind("<Configure>", self.update_scroll_region)
self.config(xscrollcommand=self.xview_handler)
self.config(yscrollcommand=self.yview_handler)
self.dragged_col = None
self.drag_start_x = None
self.drag_current_x = None # Initialize drag_current_x
def bind_shortcuts(self):
self.parent.bind("<Control-c>", self.copy_selection)
self.parent.bind("<Control-x>", self.cut_selection)
self.parent.bind("<Control-v>", self.paste_selection)
self.parent.bind("<Control-z>", self.undo)
self.parent.bind("<Control-y>", self.redo)
def on_table_drag(self, event):
global current_selection
x, y = self.canvasx(event.x), self.canvasy(event.y)
row, col = int(y // self.cell_height), self.get_col_at_x(x - self.row_number_width)
if col is None or row >= self.rows: # Add check to prevent accessing non-existing rows
return # If col is None or row is out of bounds, exit the method to avoid the error
if row >= 0 and col >= 0 and current_selection is not None:
self.clear_selection()
row1, col1, row2, col2 = current_selection
current_selection = (row1, col1, row, col)
self.highlight_rectangle(row1, col1, row, col)
self.scroll_if_needed(event)
def create_widgets(self):
self.config(bg=self.color_config['cell_bg'], highlightthickness=0)
self.bind("<Button-1>", self.on_table_click)
self.bind("<B1-Motion>", self.on_table_drag)
self.bind("<ButtonRelease-1>", self.on_drag_end)
self.bind("<Double-1>", self.on_table_double_click)
self.bind("<Button-3>", self.on_table_right_click)
self.bind_all("<MouseWheel>", self.on_mouse_wheel)
self.row_number_canvas.bind("<Button-3>", self.on_row_number_right_click)
self.row_number_canvas.bind("<B1-Motion>", self.on_row_number_drag)
self.header_canvas.bind("<Button-1>", self.on_header_left_click)
self.header_canvas.bind("<Button-3>", self.on_header_right_click)
self.header_canvas.bind("<B1-Motion>", self.on_header_drag)
self.header_canvas.bind("<ButtonRelease-1>", self.on_drag_release)
def cut_selection(self):
self.copy_selection()
self.delete_selection(current_selection[0], current_selection[1])
def create_cell(self, row, col, value):
col_name = self.headers[col]
cell_config = column_config.get(col_name, {"width": 100, "instructions": ""})
cell_width = cell_config["width"]
x0 = self.row_number_width + sum(column_config.get(self.headers[c], {"width": 100})["width"] for c in range(col))
y0 = row * self.cell_height
x1 = x0 + cell_width
y1 = y0 + self.cell_height
rect = self.create_rectangle(x0, y0, x1, y1, fill=self.color_config['cell_bg'], outline=self.color_config['line_color'])
# Calculate the number of characters that fit in the cell width
char_width = self.font.measure("A")
max_chars = cell_width // char_width
display_value = str(value)[:max_chars] if value else ""
text = self.create_text(x0 + 5, y0 + 5, anchor="nw", text=display_value, width=cell_width - 10, font=self.font, fill=self.color_config['cell_fg'], tag=f"cell_{row}_{col}")
self.cells[(row, col)] = (rect, text)
def create_row_numbers(self):
self.row_number_canvas.delete("all")
self.row_numbers.clear()
if self.rows == 0:
return
for row in range(self.rows):
x0 = 0
y0 = row * self.cell_height
x1 = x0 + self.row_number_width
y1 = y0 + self.cell_height
rect = self.row_number_canvas.create_rectangle(x0, y0, x1, y1, fill=self.color_config['row_number_bg'], outline=self.color_config['line_color'])
text = self.row_number_canvas.create_text(x0 + 5, y0 + 5, anchor="nw", text=str(row + 1), font=self.font, fill=self.color_config['row_number_fg'])
self.row_numbers.append((rect, text))
self.row_number_canvas.tag_bind(rect, "<Button-1>", lambda event, r=row: self.on_row_number_click(event, r))
self.row_number_canvas.tag_bind(text, "<Button-1>", lambda event, r=row: self.on_row_number_click(event, r))
def on_row_number_click(self, event, row):
if self.rows == 0:
return
global shift_start_row
if shift_start_row is None or not event.state & 0x1: # If shift key is not pressed
shift_start_row = row
self.clear_selection()
self.highlight_rows(shift_start_row, row)
global current_selection
current_selection = (min(shift_start_row, row), 0, max(shift_start_row, row), self.cols - 1)
def on_row_number_drag(self, event):
if self.rows == 0:
return
global current_selection
y = self.row_number_canvas.canvasy(event.y)
row = int(y // self.cell_height)
if row >= 0 and current_selection is not None:
self.clear_selection()
row1, _, row2, _ = current_selection
current_selection = (row1, 0, row, self.cols - 1)
self.highlight_rows(min(row1, row), max(row1, row))
self.scroll_if_needed(event)
def highlight_rows(self, start_row, end_row):
for row in range(min(start_row, end_row), max(start_row, end_row) + 1):
for col in range(self.cols):
self.highlight_cell(row, col)
def populate_table(self):
for row in range(self.rows):
for col in range(self.cols):
self.create_cell(row, col, "")
self.create_headers()
self.create_row_numbers()
def on_table_click(self, event):
global current_selection
try:
x, y = self.canvasx(event.x), self.canvasy(event.y)
row, col = int(y // self.cell_height), self.get_col_at_x(x - self.row_number_width)
if row >= 0 and col is not None and col >= 0:
self.clear_selection()
self.highlight_cell(row, col)
current_selection = (row, col, row, col)
except Exception as e:
print(f"Error in on_table_click: {e}")
def rearrange_headers(self, source_col, target_col):
global global_df
if source_col != target_col:
# Rearrange headers
self.headers.insert(target_col, self.headers.pop(source_col))
# Rearrange data in the DataFrame
cols = list(global_df.columns)
cols.insert(target_col, cols.pop(source_col))
global_df = global_df[cols]
# Update the table display
self.load_data(global_df.values.tolist(), self.headers)
self.update_table_headers()
def load_data(self, data, column_names):
self.rows = len(data)
self.cols = len(data[0]) if self.rows > 0 else 0
self.headers = column_names
self.cells.clear()
self.delete("all")
self.populate_table()
for row, row_data in enumerate(data):
for col, value in enumerate(row_data):
self.create_cell(row, col, value)
self.create_headers()
self.create_row_numbers()
self.update_scroll_region()
self.xview_moveto(0) # Ensure initial alignment
self.yview_moveto(0)
def insert_column(self, col):
# Capture current scroll position
current_xview = self.xview()
new_header = simpledialog.askstring("Insert Column", "Enter new header name:")
if new_header:
self.headers.insert(col, new_header)
global global_df
global_df.insert(col, new_header, "")
# Update the table display
self.load_data(global_df.values.tolist(), self.headers)
# Restore scroll position
self.xview_moveto(current_xview[0])
def delete_column(self, col):
# Capture current scroll position
current_xview = self.xview()
confirm = messagebox.askyesno("Delete Column", f"Are you sure you want to delete '{self.headers[col]}'?")
if confirm:
header_name = self.headers.pop(col)
global global_df
global_df.drop(columns=[header_name], inplace=True)
# Update the table display
self.load_data(global_df.values.tolist(), self.headers)
messagebox.showinfo("Column Deleted", f"Deleted column and variable '{header_name}'.")
# Restore scroll position
self.xview_moveto(current_xview[0])
def create_headers(self):
self.header_canvas.delete("all")
x_offset = 0
for col, header in enumerate(self.headers):
cell_config = column_config.get(header, {"width": 100, "instructions": ""})
cell_width = cell_config["width"]
x0 = x_offset
y0 = 0
x1 = x0 + cell_width
y1 = y0 + self.header_height
rect = self.header_canvas.create_rectangle(x0, y0, x1, y1, fill=self.color_config['header_bg'], outline=self.color_config['line_color'])
text = self.header_canvas.create_text(x0 + 5, y0 + 5, anchor="nw", text=header, font=self.font, fill=self.color_config['header_fg'])
self.header_canvas.tag_bind(rect, "<Button-1>", lambda event, c=col: self.start_drag(event, c))
self.header_canvas.tag_bind(text, "<Button-1>", lambda event, c=col: self.start_drag(event, c))
x_offset += cell_width
def start_drag(self, event, col):
self.dragged_col = col
self.drag_start_x = event.x
self.drag_current_x = event.x # Initialize drag_current_x
def on_header_drag(self, event):
if self.dragged_col is not None:
self.drag_current_x = event.x # Update drag_current_x with the current event x position
def on_drag_release(self, event):
if self.dragged_col is not None and self.drag_start_x != self.drag_current_x:
target_col = self.get_col_at_x(self.header_canvas.canvasx(event.x))
if target_col is not None:
self.rearrange_headers(self.dragged_col, target_col)
self.dragged_col = None
self.drag_start_x = None
self.drag_current_x = None # Reset drag_current_x
def get_col_at_x(self, x):
x_offset = 0
for col, header in enumerate(self.headers):
cell_config = column_config.get(header, {"width": 100, "instructions": ""})
cell_width = cell_config["width"]
if x_offset <= x < x_offset + cell_width:
return col
x_offset += cell_width
return None
def update_table_headers(self):
self.create_headers()
self.update_scroll_region()
def update_table_cells(self):
new_cells = {}
for (row, col), (rect, text) in self.cells.items():
new_col = self.headers.index(self.headers[col])
new_cells[(row, new_col)] = (rect, text)
self.cells = new_cells
self.load_data(global_df.values.tolist(), self.headers)
def scroll_if_needed(self, event):
x, y = self.canvasx(event.x), self.canvasy(event.y)
scroll_speed = 2 # Adjust this value to control the scrolling speed
# Scroll vertically if needed
if y < self.canvasy(0) + self.cell_height:
self.yview_scroll(-1, "units")
self.row_number_canvas.yview_scroll(-1, "units")
elif y > self.canvasy(self.winfo_height()) - self.cell_height:
self.yview_scroll(1, "units")
self.row_number_canvas.yview_scroll(1, "units")
# Scroll horizontally if needed, but ensure it doesn't scroll past the first column
if x < self.canvasx(0) + self.row_number_width:
self.xview_moveto(0) # Ensure we don't scroll past the first column
self.header_canvas.xview_moveto(0)
elif x > self.canvasx(self.winfo_width()) - self.row_number_width:
self.xview_scroll(1, "units")
self.header_canvas.xview_scroll(1, "units")
self.update_idletasks()
def on_drag_end(self, event):
self.start_row = None
def on_table_double_click(self, event):
x, y = self.canvasx(event.x), self.canvasy(event.y)
row, col = int(y // self.cell_height), self.get_col_at_x(x - self.row_number_width)
if row >= 0 and col >= 0:
self.edit_cell(row, col)
def on_table_right_click(self, event):
x, y = self.canvasx(event.x), self.canvasy(event.y)
row, col = int(y // self.cell_height), self.get_col_at_x(x - self.row_number_width)
if row >= 0 and col >= 0:
if (row, col) not in self.cells:
return
if not self.is_cell_selected(row, col):
self.clear_selection()
self.highlight_cell(row, col)
global current_selection
current_selection = (row, col, row, col)
self.show_cell_context_menu(event, row, col)
def is_cell_selected(self, row, col):
global current_selection
if current_selection is None:
return False
row1, col1, row2, col2 = current_selection
return row1 <= row <= row2 and col1 <= col <= col2
def on_row_number_right_click(self, event):
if self.rows == 0:
return
y = self.row_number_canvas.canvasy(event.y)
row = int(y // self.cell_height)
if row >= 0:
if not self.is_row_selected(row):
self.clear_selection()
self.highlight_rows(row, row)
global current_selection
current_selection = (row, 0, row, self.cols - 1)
self.right_click_menu(event, row)
def on_mouse_wheel(self, event):
if event.state & 0x1: # If Shift key is pressed
self.xview_scroll(-1 * int((event.delta / 120)), "units")
else:
self.yview_scroll(-1 * int((event.delta / 120)), "units")
self.update_scroll_region()
def highlight_cell(self, row, col):
if (row, col) in self.cells: # Add boundary check
self.itemconfig(self.cells[(row, col)][0], fill=self.color_config['highlight_bg'])
self.itemconfig(self.cells[(row, col)][1], fill=self.color_config['highlight_fg'], font=self.font)
def clear_selection(self):
for rect in self.selection_rects:
self.delete(rect)
self.selection_rects.clear()
for (row, col), (rect, text) in self.cells.items():
self.itemconfig(rect, fill=self.color_config['cell_bg'])
self.itemconfig(text, fill=self.color_config['cell_fg'], font=self.font)
def highlight_rectangle(self, row1, col1, row2, col2):
for row in range(min(row1, row2), max(row1, row2) + 1):
for col in range(min(col1, col2), max(col1, col2) + 1):
if (row, col) in self.cells: # Add boundary check
self.highlight_cell(row, col)
cell_config = column_config.get(self.headers[col], {"width": 100, "instructions": ""})
cell_width = cell_config["width"]
x0 = self.row_number_width + sum(column_config.get(self.headers[c], {"width": 100})["width"] for c in range(col))
y0 = row * self.cell_height
x1 = x0 + cell_width
y1 = y0 + self.cell_height
rect = self.create_rectangle(x0, y0, x1, y1, outline=self.color_config['highlight_line_color'], width=2)
self.selection_rects.append(rect)
def show_cell_context_menu(self, event, row, col):
menu = tk.Menu(self, tearoff=0)
menu.add_command(label="Copy Selection", command=self.copy_selection)
menu.add_command(label="Paste", command=self.paste_selection)
menu.add_command(label="Paste List", command=self.paste_list_selection) # New option for pasting lists
menu.add_command(label="Delete Selection", command=lambda: self.delete_selection(row, col))
menu.post(event.x_root, event.y_root)
def insert_row(self, row):
# Capture current scroll position
current_xview = self.xview()
current_yview = self.yview()
new_row = [""] * self.cols
global global_df
global_df = pd.concat([global_df.iloc[:row], pd.DataFrame([new_row], columns=global_df.columns), global_df.iloc[row:]]).reset_index(drop=True)
undo_manager.save_state(global_df.copy())
self.load_data(global_df.values.tolist(), self.headers)
# Restore scroll position
self.xview_moveto(current_xview[0])
self.yview_moveto(current_yview[0])
# Update scroll region and synchronize view
self.update_scroll_region()
def delete_row(self, row):
# Capture current scroll position
current_xview = self.xview()
current_yview = self.yview()
global global_df
global_df = global_df.drop(global_df.index[row]).reset_index(drop=True)
self.add_empty_row() # Add an empty row at the bottom
undo_manager.save_state(global_df.copy())
self.load_data(global_df.values.tolist(), self.headers)
# Restore scroll position
self.xview_moveto(current_xview[0])
self.yview_moveto(current_yview[0])
# Update scroll region and synchronize view
self.update_scroll_region()
def add_empty_row(self):
global global_df
new_row = pd.DataFrame([[""] * self.cols], columns=global_df.columns)
global_df = pd.concat([global_df, new_row], ignore_index=True)
self.load_data(global_df.values.tolist(), self.headers)
def delete_selection(self, row, col):
# Capture current scroll position
current_xview = self.xview()
current_yview = self.yview()
global global_df, current_selection
if current_selection is not None:
row1, col1, row2, col2 = current_selection
global_df.iloc[min(row1, row2):max(row1, row2) + 1, min(col1, col2):max(col1, col2) + 1] = ""
# Replace NaN values with empty strings
global_df.fillna("", inplace=True)
undo_manager.save_state(global_df.copy())
self.load_data(global_df.values.tolist(), self.headers)
# Restore scroll position
self.xview_moveto(current_xview[0])
self.yview_moveto(current_yview[0])
# Update scroll region and synchronize view
self.update_scroll_region()
def copy_selection(self):
global global_df, clipboard_data, current_selection
if current_selection is not None:
row1, col1, row2, col2 = current_selection
if row1 == row2 and col1 == col2:
# Copy a single cell
clipboard_data = global_df.iat[row1, col1]
else:
# Copy a range of cells
clipboard_data = global_df.iloc[min(row1, row2):max(row1, row2) + 1, min(col1, col2):max(col1, col2) + 1].copy()
def paste_selection(self):
# Capture current scroll position
current_xview = self.xview()
current_yview = self.yview()
global global_df, clipboard_data, current_selection
if clipboard_data is not None and current_selection is not None:
row1, col1, row2, col2 = current_selection
if isinstance(clipboard_data, pd.DataFrame):
rows_to_paste = clipboard_data.shape[0]
cols_to_paste = clipboard_data.shape[1]
if row1 + rows_to_paste > self.rows or col1 + cols_to_paste > self.cols:
messagebox.showerror("Paste Error", "Not enough space to paste the selection")
return
for i in range(rows_to_paste):
for j in range(cols_to_paste):
global_df.iat[row1 + i, col1 + j] = clipboard_data.iat[i, j] if clipboard_data.iat[i, j] is not None else ""
else:
for row in range(min(row1, row2), max(row1, row2) + 1):
for col in range(min(col1, col2), max(col1, col2) + 1):
global_df.iat[row, col] = clipboard_data if clipboard_data is not None else ""
undo_manager.save_state(global_df.copy())
self.load_data(global_df.values.tolist(), self.headers)
# Restore scroll position
self.xview_moveto(current_xview[0])
self.yview_moveto(current_yview[0])
# Update scroll region and synchronize view
self.update_scroll_region()
def paste_list_selection(self):
# Capture current scroll position
current_xview = self.xview()
current_yview = self.yview()
global global_df, clipboard_data, current_selection
# Retrieve the clipboard data
try:
clipboard_data = self.clipboard_get()
except tk.TclError:
clipboard_data = None
if clipboard_data and current_selection:
row1, col1, row2, col2 = current_selection
# Split the clipboard data by lines to handle vertical pasting
lines = clipboard_data.splitlines()
# Check if pasting the lines will overflow the table rows
if row1 + len(lines) > self.rows:
messagebox.showerror("Paste Error", "Not enough space to paste the selection")
return
for i, line in enumerate(lines):
global_df.iat[row1 + i, col1] = line
undo_manager.save_state(global_df.copy())
self.load_data(global_df.values.tolist(), self.headers)
# Restore scroll position
self.xview_moveto(current_xview[0])
self.yview_moveto(current_yview[0])
# Update scroll region and synchronize view
self.update_scroll_region()
def undo(self, event=None):
global global_df
state = undo_manager.undo()
if state is not None:
global_df = state.copy()
update_table()
def redo(self, event=None):
global global_df
state = undo_manager.redo()
if state is not None:
global_df = state.copy()
update_table()
def undo_text(self, event, text_widget):
try:
text_widget.edit_undo()
except tk.TclError:
pass
return "break"
def redo_text(self, event, text_widget):
try:
text_widget.edit_redo()
except tk.TclError:
pass
return "break"
def edit_cell(self, row, col):
if (row, col) not in self.cells:
return
full_text = global_df.iat[row, col] if not pd.isna(global_df.iat[row, col]) else "" # Handle NaN values
col_name = self.headers[col]
cell_config = column_config.get(col_name, {"width": 100, "instructions": ""})
instructions = cell_config.get("instructions", "")
edit_window = tk.Toplevel(self)
edit_window.title(f"Edit Cell ({row, col})")
# Bind the close event to a custom handler
edit_window.protocol("WM_DELETE_WINDOW", lambda window=edit_window: self.on_edit_window_close(window))
if instructions:
instruction_label = tk.Label(edit_window, text=instructions, wraplength=400, justify="left")
instruction_label.pack(fill="x", padx=10, pady=10)
text_frame = tk.Frame(edit_window, padx=10, pady=10)
text_frame.pack(fill="both", expand=True)
text_font = font.Font(family="Arial", size=10) # Set font size here
cursor_color = color_config['cursor_color_edit_cell']
text = tk.Text(text_frame, wrap="word", undo=True, font=text_font, insertbackground=cursor_color, bg=self.color_config['cell_bg'], fg=self.color_config['cell_fg']) # Enable undo for the text widget
text.insert("1.0", full_text)
text.edit_reset() # Reset the undo stack after inserting initial text
text.pack(fill="both", expand=True, side="left")
scrollbar = tk.Scrollbar(text_frame, command=text.yview)
scrollbar.pack(side="right", fill="y")
text.config(yscrollcommand=scrollbar.set)
text.bind_all("<MouseWheel>", lambda event: text.yview_scroll(-1 * int((event.delta / 120)), "units"))
text.focus_set()
def save_edit():
new_text = text.get("1.0", tk.END).strip()
self.itemconfig(self.cells[(row, col)][1], text=new_text[:cell_config["width"] // self.font.measure("A")]) # Update with truncated text
global_df.iat[row, col] = new_text # Update the global DataFrame with full text
undo_manager.save_state(global_df.copy()) # Save the state for undo
edit_window.destroy()
self.update_scroll_region() # Update the scroll region after editing
save_button = tk.Button(edit_window, text="Save", command=save_edit)
save_button.pack(pady=10)
cancel_button = tk.Button(edit_window, text="Cancel", command=lambda window=edit_window: self.on_edit_window_close(window))
cancel_button.pack(pady=10)
# Bind the keyboard shortcuts
text.bind("<Control-c>", lambda event: self.copy_to_clipboard(event, text))
text.bind("<Control-x>", lambda event: self.cut_to_clipboard(event, text))
text.bind("<Control-v>", lambda event: self.paste_from_clipboard(event, text))
text.bind("<Control-z>", lambda event: self.undo_text(event, text))
text.bind("<Control-y>", lambda event: self.redo_text(event, text))
text.focus_set()
def on_edit_window_close(self, window):
if window is not None:
window.destroy()
self.update_scroll_region()
def copy_to_clipboard(self, event, text_widget):
try:
text_widget.clipboard_clear()
selected_text = text_widget.get(tk.SEL_FIRST, tk.SEL_LAST)
text_widget.clipboard_append(selected_text)
except tk.TclError:
pass
return "break"
def cut_to_clipboard(self, event, text_widget):
self.copy_to_clipboard(event, text_widget)
try:
text_widget.delete(tk.SEL_FIRST, tk.SEL_LAST)
except tk.TclError:
pass
return "break"
def paste_from_clipboard(self, event, text_widget):
try:
clipboard_text = text_widget.clipboard_get()
if text_widget.tag_ranges(tk.SEL):
text_widget.delete(tk.SEL_FIRST, tk.SEL_LAST)
text_widget.insert(tk.INSERT, clipboard_text)
except tk.TclError:
pass
return "break"
def redo_text(self, event, text_widget):
try:
text_widget.edit_redo()
except tk.TclError:
pass
return "break"
def update_scroll_region(self, event=None):
self.config(scrollregion=self.bbox("all"))
self.header_canvas.config(scrollregion=self.header_canvas.bbox("all"))
self.row_number_canvas.config(scrollregion=self.row_number_canvas.bbox("all"))
# Synchronize the header and row number views with the table view
if self.xview() is not None:
self.header_canvas.xview_moveto(self.xview()[0])
if self.yview() is not None:
self.row_number_canvas.yview_moveto(self.yview()[0])
# Ensure headers and row numbers are always visible
self.tag_lower("header")
self.tag_lower("row_num")
self.parent.update()
# Re-bind scroll events
self.bind_all("<MouseWheel>", self.on_mouse_wheel)
def initial_view(self):
self.xview_moveto(0)
self.yview_moveto(0)
def xview_handler(self, *args):
self.xview(*args)
self.header_canvas.xview(*args)
self.update_scroll_region()
def yview_handler(self, *args):
self.yview(*args)
self.row_number_canvas.yview(*args)
self.update_scroll_region()
def on_header_left_click(self, event):
x = self.header_canvas.canvasx(event.x)
col = self.get_col_at_x(x)
if col is not None and col >= 0:
self.highlight_column(col)
def highlight_column(self, col):
self.clear_selection()
for row in range(self.rows):
self.highlight_cell(row, col)
def on_header_right_click(self, event):
x = self.header_canvas.canvasx(event.x)
col = self.get_col_at_x(x)
if col >= 0:
self.show_header_context_menu(event, col)
def show_header_context_menu(self, event, col):
menu = tk.Menu(self, tearoff=0)
menu.add_command(label="Insert Column", command=lambda: self.insert_column(col))
menu.add_command(label="Delete Column", command=lambda: self.delete_column(col))
menu.add_command(label="Find and Replace", command=lambda: self.show_find_replace_dialog(col))
menu.post(event.x_root, event.y_root)
def show_find_replace_dialog(self, col):
if self.find_replace_dialog is not None and self.find_replace_dialog.winfo_exists():
self.find_replace_dialog.lift()
return
self.find_replace_dialog = tk.Toplevel(self)
self.find_replace_dialog.title(f"Find and Replace in Column '{self.headers[col]}'")
tk.Label(self.find_replace_dialog, text=f"Column: {self.headers[col]}").grid(row=0, column=0, columnspan=2, pady=5)
tk.Label(self.find_replace_dialog, text="Find:").grid(row=1, column=0, pady=5, sticky="e")
find_entry = tk.Entry(self.find_replace_dialog, width=30)
find_entry.grid(row=1, column=1, pady=5, padx=5)
tk.Label(self.find_replace_dialog, text="Replace:").grid(row=2, column=0, pady=5, sticky="e")
replace_entry = tk.Entry(self.find_replace_dialog, width=30)
replace_entry.grid(row=2, column=1, pady=5, padx=5)
def find_next():
find_text = find_entry.get()
if find_text:
self.find_next_in_column(col, find_text)