Skip to content

Commit 7d61d71

Browse files
committed
feat(editor): 添加课程表编辑的撤销和重做功能
- 实现了课程表编辑的撤销和重做功能 - 新增了撤销和重做按钮,并在界面上进行了展示 - 优化了课程表数据的保存和恢复逻辑 - 修复了一些与课程表编辑相关的bug
1 parent 835ebe5 commit 7d61d71

File tree

1 file changed

+154
-5
lines changed

1 file changed

+154
-5
lines changed

editor.py

Lines changed: 154 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,20 @@ def __init__(self, main_app):
6969
self._is_programmatic_tab_change = False
7070
self.is_dialog_open = False
7171

72+
# 撤销/重做功能
73+
self.undo_stack = []
74+
self.redo_stack = []
75+
self.undo_button = None
76+
self.redo_button = None
77+
7278
# 为临时开关创建BooleanVar
7379
self.auto_complete_var = tk.BooleanVar(value=self.main_app.config_handler.auto_complete_end_time)
7480
self.auto_calculate_var = tk.BooleanVar(value=self.main_app.config_handler.auto_calculate_next_course)
7581

7682
self._initialize_ui()
7783
self._create_schedule_selector()
7884
self._create_batch_operations_bar() # 添加批量操作按钮栏
85+
self.window.after(100, self._initial_state_capture) # 捕获初始状态
7986
except Exception as e:
8087
logger.log_error(e)
8188
raise
@@ -205,6 +212,7 @@ def _rename_schedule(self):
205212
self.schedule_combobox['values'] = list(self.main_app.schedule["schedules"].keys())
206213
self.schedule_combobox.set(new_name)
207214
messagebox.showinfo("成功", "课表已重命名")
215+
self._clear_history()
208216
except Exception as e:
209217
logger.log_error(e)
210218
messagebox.showerror("错误", f"重命名失败: {str(e)}")
@@ -245,6 +253,7 @@ def _copy_schedule(self):
245253
self.main_app.schedule["current_schedule"] = new_name
246254
self._update_ui_with_new_schedule()
247255
self._reset_modified_flag()
256+
self._clear_history()
248257

249258
def _add_new_schedule(self):
250259
"""添加新课表"""
@@ -271,6 +280,7 @@ def _add_new_schedule(self):
271280
self._update_ui_with_new_schedule()
272281
# 标记为未修改
273282
self._reset_modified_flag()
283+
self._clear_history()
274284
finally:
275285
self.is_dialog_open = False
276286

@@ -296,6 +306,7 @@ def _delete_schedule(self):
296306
self.schedule_combobox['values'] = list(self.main_app.schedule["schedules"].keys())
297307
self.schedule_combobox.set(new_schedule)
298308
self._on_schedule_change()
309+
self._clear_history()
299310
finally:
300311
self.is_dialog_open = False
301312

@@ -336,6 +347,7 @@ def _on_schedule_change(self, event=None):
336347
self.schedule_times[new_schedule] = []
337348
self._update_ui_with_new_schedule()
338349
self._reset_modified_flag()
350+
self._clear_history()
339351
finally:
340352
self.is_dialog_open = False
341353

@@ -414,8 +426,10 @@ def _on_tab_changed(self, event):
414426
if response is True: # Yes
415427
self._save_day(self.previous_tab_index)
416428
self._reset_modified_flag()
429+
self._clear_history()
417430
elif response is False: # No
418431
self._reset_modified_flag()
432+
self._clear_history()
419433
else: # Cancel
420434
self._is_programmatic_tab_change = True
421435
self.notebook.select(self.previous_tab_index)
@@ -425,6 +439,7 @@ def _on_tab_changed(self, event):
425439
try:
426440
self.create_day_ui(self.day_frames[new_tab_index], str(new_tab_index))
427441
self.previous_tab_index = new_tab_index
442+
self._clear_history()
428443
except tk.TclError:
429444
pass # 窗口关闭时可能会引发此错误
430445
def _on_close(self):
@@ -450,6 +465,115 @@ def _on_close(self):
450465
finally:
451466
self.is_dialog_open = False
452467

468+
def _initial_state_capture(self):
469+
"""捕获初始状态"""
470+
self._capture_state(initial=True)
471+
472+
def _capture_state(self, initial=False):
473+
"""捕获当前活动标签页的UI状态,并将其存入撤销栈"""
474+
current_tab_index = self.notebook.index(self.notebook.select())
475+
day_frame = self.day_frames[current_tab_index]
476+
477+
state = []
478+
# 按Y坐标排序以确保顺序一致
479+
visible_rows = sorted(
480+
[row for row in day_frame.winfo_children() if isinstance(row, tk.Frame) and hasattr(row, 'row_id')],
481+
key=lambda w: w.winfo_y()
482+
)
483+
484+
for row_frame in visible_rows:
485+
entries = [w for w in row_frame.winfo_children() if isinstance(w, tk.Entry)]
486+
if len(entries) >= 3:
487+
state.append({
488+
"start_time": entries[0].get(),
489+
"end_time": entries[1].get(),
490+
"name": entries[2].get()
491+
})
492+
493+
# 如果是初始状态,直接设置
494+
if initial:
495+
self.undo_stack.append(state)
496+
self._update_undo_redo_buttons()
497+
return
498+
499+
# 避免重复记录完全相同的状态
500+
if self.undo_stack and self.undo_stack[-1] == state:
501+
return
502+
503+
self.undo_stack.append(state)
504+
self.redo_stack.clear() # 任何新操作都会清空重做栈
505+
self._update_undo_redo_buttons()
506+
self.modified = True # 任何记录的操作都应标记为修改
507+
508+
def _restore_state(self, state):
509+
"""根据给定状态恢复UI"""
510+
current_tab_index = self.notebook.index(self.notebook.select())
511+
day_frame = self.day_frames[current_tab_index]
512+
513+
# 清除现有UI
514+
for widget in day_frame.winfo_children():
515+
widget.destroy()
516+
517+
# 根据状态重建UI
518+
for i, course in enumerate(state):
519+
self.add_course_row(day_frame, i, course, record_state=False)
520+
521+
# 重建“添加课程”按钮
522+
style = ttk.Style()
523+
style.configure("AddSchedule.TButton", font=("微软雅黑", 8), padding=5)
524+
btn_frame = tk.Frame(day_frame, bg="white")
525+
btn_frame.pack(pady=5)
526+
ttk.Button(btn_frame, text="添加课程",
527+
command=lambda: self.add_course_row(day_frame, len(state)),
528+
style="AddSchedule.TButton").pack(side=tk.LEFT, padx=2)
529+
530+
# 只有当恢复后的状态与初始状态不同时,才标记为已修改
531+
if self.undo_stack and state != self.undo_stack[0]:
532+
self.modified = True
533+
else:
534+
self.modified = False
535+
536+
def _undo(self):
537+
"""执行撤销操作"""
538+
if len(self.undo_stack) > 1:
539+
current_state = self.undo_stack.pop()
540+
self.redo_stack.append(current_state)
541+
542+
last_state = self.undo_stack[-1]
543+
self._restore_state(last_state)
544+
self._update_undo_redo_buttons()
545+
546+
# 检查是否回到了初始状态
547+
if self.undo_stack and last_state == self.undo_stack[0]:
548+
self.modified = False
549+
else:
550+
self.modified = True
551+
552+
def _redo(self):
553+
"""执行重做操作"""
554+
if self.redo_stack:
555+
state_to_restore = self.redo_stack.pop()
556+
self.undo_stack.append(state_to_restore)
557+
self._restore_state(state_to_restore)
558+
self._update_undo_redo_buttons()
559+
self.modified = True # 任何重做操作都意味着有修改
560+
561+
def _clear_history(self):
562+
"""清空撤销和重做历史"""
563+
self.undo_stack.clear()
564+
self.redo_stack.clear()
565+
# 捕获清空后的初始状态
566+
self.window.after(50, self._initial_state_capture)
567+
self._update_undo_redo_buttons()
568+
569+
def _update_undo_redo_buttons(self):
570+
"""更新撤销和重做按钮的状态"""
571+
if self.undo_button and self.redo_button:
572+
# 撤销按钮:当栈中有多于一个状态时(初始状态之外还有其他状态)才可点击
573+
self.undo_button.config(state="normal" if len(self.undo_stack) > 1 else "disabled")
574+
# 重做按钮:当重做栈不为空时可点击
575+
self.redo_button.config(state="normal" if self.redo_stack else "disabled")
576+
453577
def _create_batch_operations_bar(self) -> None:
454578
"""创建批量操作按钮栏"""
455579
style = ttk.Style()
@@ -469,6 +593,13 @@ def _create_batch_operations_bar(self) -> None:
469593
ttk.Button(batch_frame, text="☑ 全选",
470594
command=self._select_all,
471595
style="Small.TButton").pack(side=tk.LEFT, padx=4)
596+
597+
# 添加撤销和重做按钮
598+
self.undo_button = ttk.Button(batch_frame, text="↶", command=self._undo, style="Small.TButton", state="disabled", width=3)
599+
self.undo_button.pack(side=tk.LEFT, padx=(10, 2))
600+
601+
self.redo_button = ttk.Button(batch_frame, text="↷", command=self._redo, style="Small.TButton", state="disabled", width=3)
602+
self.redo_button.pack(side=tk.LEFT, padx=2)
472603

473604
# 批量操作按钮 (右侧)
474605
ttk.Button(batch_frame, text="导入课程",
@@ -556,7 +687,7 @@ def create_day_ui(self, frame, day):
556687

557688
# 绘制课程行
558689
for i, course in enumerate(courses_to_display):
559-
self.add_course_row(frame, i, course)
690+
self.add_course_row(frame, i, course, record_state=False)
560691

561692
# 更新课程名称建议
562693
self.all_courses = self._get_all_courses()
@@ -571,11 +702,13 @@ def create_day_ui(self, frame, day):
571702
# "添加课程"按钮
572703
# 使用 len(courses_to_display) 来确保索引正确
573704
ttk.Button(btn_frame, text="添加课程",
574-
command=lambda: self.add_course_row(frame, len(courses_to_display)),
705+
command=lambda: self.add_course_row(frame, len(courses_to_display), record_state=True),
575706
style="AddSchedule.TButton").pack(side=tk.LEFT, padx=2)
576707

577708

578-
def add_course_row(self, parent_frame, index, course=None):
709+
def add_course_row(self, parent_frame, index, course=None, record_state=True):
710+
if record_state and course is None: # 仅在手动添加新行时记录
711+
self._capture_state()
579712
row_frame = tk.Frame(parent_frame, bg="white", bd=0, relief=tk.FLAT)
580713
row_frame.pack(fill=tk.X, pady=4, padx=2)
581714
# 生成唯一且稳定的行ID
@@ -611,6 +744,7 @@ def show_start_time_picker():
611744
start_time_entry.delete(0, tk.END)
612745
start_time_entry.insert(0, picker.selected_time)
613746
calculate_end_time()
747+
self._capture_state()
614748

615749
ttk.Button(row_frame, text="🕒", command=show_start_time_picker,
616750
style="Editor.TButton").pack(side=tk.LEFT, padx=2)
@@ -628,6 +762,7 @@ def show_end_time_picker():
628762
if picker.selected_time:
629763
end_time_entry.delete(0, tk.END)
630764
end_time_entry.insert(0, picker.selected_time)
765+
self._capture_state()
631766

632767
ttk.Button(row_frame, text="🕒", command=show_end_time_picker,
633768
style="Editor.TButton").pack(side=tk.LEFT, padx=2)
@@ -637,6 +772,7 @@ def show_end_time_picker():
637772
if course and course["name"]:
638773
name_entry.insert(0, course["name"])
639774
name_entry.pack(side=tk.LEFT, padx=2, expand=True, fill=tk.X)
775+
name_entry.bind("<FocusOut>", lambda e: self._capture_state())
640776

641777
# 历史课程选择框
642778
history_var = tk.StringVar()
@@ -650,6 +786,7 @@ def on_history_select(event):
650786
if selected_course:
651787
name_entry.delete(0, tk.END)
652788
name_entry.insert(0, selected_course)
789+
self._capture_state()
653790

654791
history_combobox.bind("<<ComboboxSelected>>", on_history_select)
655792

@@ -776,13 +913,13 @@ def fix_time_format(time_str):
776913
end_time_entry.config(fg="red")
777914
return False
778915

779-
start_time_entry.bind("<FocusOut>", lambda e: [calculate_end_time(), validate_time()])
916+
start_time_entry.bind("<FocusOut>", lambda e: [calculate_end_time(), validate_time(), self._capture_state()])
780917

781918
# 仅在用户添加新行时(即 course is None)自动计算下一个课程时间
782919
if course is None:
783920
calculate_next_course_time()
784921

785-
end_time_entry.bind("<FocusOut>", lambda e: validate_time())
922+
end_time_entry.bind("<FocusOut>", lambda e: [validate_time(), self._capture_state()])
786923

787924
# 删除按钮
788925
ttk.Button(row_frame, text="×", command=lambda: self.delete_course_row(row_frame),
@@ -801,11 +938,14 @@ def move_down():
801938
style="Editor.TButton", width=2).pack(side=tk.RIGHT, padx=2)
802939

803940
def delete_course_row(self, row_frame):
941+
self._capture_state()
804942
row_frame.destroy()
805943
# 标记为已修改
806944
self.modified = True
945+
self._capture_state()
807946

808947
def move_course_row(self, row_frame, direction):
948+
self._capture_state()
809949
"""移动课程行位置 - 更安全的实现"""
810950
parent = row_frame.master
811951
if not parent.winfo_exists():
@@ -936,6 +1076,7 @@ def move_course_row(self, row_frame, direction):
9361076
except Exception as e:
9371077
logger.log_error(f"日志记录错误: {str(e)}")
9381078
self.modified = True
1079+
self._capture_state()
9391080

9401081
except Exception as e:
9411082
logger.log_error(f"移动行失败: {str(e)}")
@@ -987,6 +1128,10 @@ def move_course_row(self, row_frame, direction):
9871128

9881129

9891130
def _batch_delete(self):
1131+
if not self.selected_rows:
1132+
messagebox.showwarning("提示", "请先选中要删除的课程")
1133+
return
1134+
self._capture_state()
9901135
"""批量删除选中课程"""
9911136
if self.is_dialog_open:
9921137
return
@@ -1006,6 +1151,7 @@ def _batch_delete(self):
10061151

10071152
self.selected_rows.clear()
10081153
self.modified = True
1154+
self._capture_state()
10091155
finally:
10101156
self.is_dialog_open = False
10111157

@@ -1046,6 +1192,7 @@ def _copy_selected(self):
10461192
self.is_dialog_open = False
10471193

10481194
def _import_from_clipboard(self):
1195+
self._capture_state()
10491196
"""从剪贴板导入课程"""
10501197
if self.is_dialog_open:
10511198
return
@@ -1075,6 +1222,7 @@ def _import_from_clipboard(self):
10751222

10761223
messagebox.showinfo("成功", f"已导入{len(courses)}个课程")
10771224
self.modified = True
1225+
self._capture_state()
10781226

10791227
except json.JSONDecodeError:
10801228
messagebox.showerror("错误", "剪贴板中没有有效的课程数据")
@@ -1174,6 +1322,7 @@ def save(self, show_message=True):
11741322
selected_tab_index = self.notebook.index(self.notebook.select())
11751323
self._save_day(selected_tab_index)
11761324
self._reset_modified_flag()
1325+
self._clear_history()
11771326

11781327
if show_message:
11791328
messagebox.showinfo("成功", f"课表'{self.current_schedule}'已保存")

0 commit comments

Comments
 (0)