source: flexograder/gui/exam_layout_editor.py@ bc820cf

Last change on this file since bc820cf was bc820cf, checked in by Enrico Schwass <ennoausberlin@…>, 2 months ago

add KILE_EXAM.json - save session after layouting

  • Property mode set to 100644
File size: 12.5 KB
Line 
1# gui/layout_dialog.py
2import tkinter as tk
3from tkinter import ttk, messagebox, simpledialog
4
5
6class ExamLayoutDialog(tk.Toplevel):
7 """Dialog for arranging Exam layout: pages on the right, question pool on the left."""
8
9 def __init__(self, parent, exam):
10 super().__init__(parent)
11 self.parent = parent
12 self.exam = exam
13
14 # Ensure we have a layout object
15 self.layout = getattr(exam, "layout", None)
16 if self.layout is None:
17 from builder.exam import ExamLayout
18 self.layout = ExamLayout()
19
20 self.title(f"Exam Layout — {exam.title}")
21 self.geometry("900x600")
22 self.configure(padx=10, pady=10)
23
24 self.create_widgets()
25 self.refresh_all()
26
27 # Make modal
28 self.transient(parent)
29 self.grab_set()
30 self.wait_visibility()
31 self.focus_set()
32
33 # ─────────────────────────────────────────────────────────────
34 # GUI construction (pure grid layout)
35 # ─────────────────────────────────────────────────────────────
36 def create_widgets(self):
37 # 3 main vertical rows: top section, page section, footer
38 self.rowconfigure(0, weight=1)
39 self.rowconfigure(1, weight=0)
40 self.rowconfigure(2, weight=0)
41 self.columnconfigure(0, weight=1)
42
43 # ─────────────────────────────
44 # TOP SECTION (Questions | Mid | Pages)
45 # ─────────────────────────────
46 top = ttk.Frame(self)
47 top.grid(row=0, column=0, sticky="nsew", pady=(0, 8))
48 top.columnconfigure(0, weight=1) # left
49 top.columnconfigure(1, weight=0) # mid
50 top.columnconfigure(2, weight=1) # right
51 top.rowconfigure(1, weight=1)
52
53 # LEFT — Question pool
54 ttk.Label(top, text="Exam Questions").grid(row=0, column=0, sticky="w")
55 self.questions_list = tk.Listbox(top, selectmode="extended", exportselection=False)
56 self.questions_list.grid(row=1, column=0, sticky="nsew", padx=(0, 6))
57 qbtns = ttk.Frame(top)
58 qbtns.grid(row=2, column=0, sticky="ew", pady=(5, 0))
59 ttk.Button(qbtns, text="Add Question", command=self.add_question).pack(side="left")
60 ttk.Button(qbtns, text="Delete", command=self.delete_question).pack(side="left")
61
62 # MIDDLE — Assignment controls
63 mid = ttk.Frame(top)
64 mid.grid(row=1, column=1, sticky="ns", padx=4)
65 ttk.Button(mid, text="→ Add to Page", command=self.add_selected_to_page).pack(pady=10)
66 ttk.Button(mid, text="← Remove", command=self.remove_selected_from_page).pack(pady=10)
67
68 # RIGHT — Pages
69 ttk.Label(top, text="Exam Pages").grid(row=0, column=2, sticky="w")
70 self.pages_list = tk.Listbox(top, exportselection=False)
71 self.pages_list.grid(row=1, column=2, sticky="nsew", padx=(6, 0))
72 self.pages_list.bind("<<ListboxSelect>>", self.on_page_selected)
73
74 pagebtns = ttk.Frame(top)
75 pagebtns.grid(row=2, column=2, sticky="ew", pady=(5, 0))
76 ttk.Button(pagebtns, text="Add Page", command=self.add_page).pack(side="left")
77 ttk.Button(pagebtns, text="Rename", command=self.rename_page).pack(side="left")
78 ttk.Button(pagebtns, text="Delete", command=self.delete_page).pack(side="left")
79 ttk.Button(pagebtns, text="↑", command=self.move_page_up).pack(side="right")
80 ttk.Button(pagebtns, text="↓", command=self.move_page_down).pack(side="right")
81
82 # ─────────────────────────────
83 # SECOND SECTION — Page contents
84 # ─────────────────────────────
85 page_frame = ttk.LabelFrame(self, text="Questions on Selected Page")
86 page_frame.grid(row=1, column=0, sticky="nsew")
87 page_frame.columnconfigure(0, weight=1)
88 page_frame.rowconfigure(0, weight=1)
89
90 self.page_questions = tk.Listbox(page_frame, height=8, exportselection=False)
91 self.page_questions.grid(row=0, column=0, sticky="nsew", padx=2, pady=2)
92
93 pqbtns = ttk.Frame(page_frame)
94 pqbtns.grid(row=1, column=0, sticky="ew", pady=(4, 0))
95 ttk.Button(pqbtns, text="Move Up", command=self.move_q_up).pack(side="left")
96 ttk.Button(pqbtns, text="Move Down", command=self.move_q_down).pack(side="left")
97
98 # ─────────────────────────────
99 # FOOTER — OK / Cancel
100 # ─────────────────────────────
101 footer = ttk.Frame(self)
102 footer.grid(row=2, column=0, sticky="ew", pady=(10, 0))
103 ttk.Button(footer, text="OK", command=self.on_ok).pack(side="right", padx=5)
104 ttk.Button(footer, text="Cancel", command=self.destroy).pack(side="right", padx=5)
105
106 # ─────────────────────────────────────────────────────────────
107 # Refresh
108 # ─────────────────────────────────────────────────────────────
109 def refresh_all(self):
110 self.pages_list.delete(0, tk.END)
111 for p in self.layout.pages:
112 self.pages_list.insert(tk.END, p.title)
113
114 self.questions_list.delete(0, tk.END)
115 for q in self.exam.questions:
116 self.questions_list.insert(tk.END, q.text[:50])
117
118 self.page_questions.delete(0, tk.END)
119
120 def refresh_page_questions(self, page):
121 self.page_questions.delete(0, tk.END)
122 for qid in page.question_ids:
123 q = self.exam.get_question_by_id(qid)
124 if q:
125 self.page_questions.insert(tk.END, f"{qid}{q.text[:50]}")
126
127 # ─────────────────────────────────────────────────────────────
128 # Page operations
129 # ─────────────────────────────────────────────────────────────
130 def add_page(self):
131 name = simpledialog.askstring("New Page", "Enter page title:", parent=self)
132 if not name:
133 return
134 self.layout.add_page(name)
135 self.refresh_all()
136
137 def rename_page(self):
138 idx = self.pages_list.curselection()
139 if not idx:
140 return
141 page = self.layout.pages[idx[0]]
142 new_name = simpledialog.askstring("Rename Page", "New title:", initialvalue=page.title, parent=self)
143 if new_name:
144 page.title = new_name
145 self.refresh_all()
146 self.pages_list.selection_set(idx[0])
147
148 def delete_page(self):
149 idx = self.pages_list.curselection()
150 if not idx:
151 return
152 del self.layout.pages[idx[0]]
153 self.refresh_all()
154
155 def move_page_up(self):
156 idx = self.pages_list.curselection()
157 if not idx or idx[0] == 0:
158 return
159 i = idx[0]
160 pages = self.layout.pages
161 pages[i - 1], pages[i] = pages[i], pages[i - 1]
162 self.refresh_all()
163 self.pages_list.selection_set(i - 1)
164
165 def move_page_down(self):
166 idx = self.pages_list.curselection()
167 if not idx or idx[0] == len(self.layout.pages) - 1:
168 return
169 i = idx[0]
170 pages = self.layout.pages
171 pages[i + 1], pages[i] = pages[i], pages[i + 1]
172 self.refresh_all()
173 self.pages_list.selection_set(i + 1)
174
175 # ─────────────────────────────────────────────────────────────
176 # Question assignment
177 # ─────────────────────────────────────────────────────────────
178 def add_selected_to_page(self):
179 idx_page = self.pages_list.curselection()
180 if not idx_page:
181 messagebox.showinfo("Select Page", "Please select a page first.")
182 return
183 page = self.layout.pages[idx_page[0]]
184 for i in self.questions_list.curselection():
185 q = self.exam.questions[i]
186 if q.flexo_id not in page.question_ids:
187 page.question_ids.append(q.flexo_id)
188 self.refresh_page_questions(page)
189
190 def remove_selected_from_page(self):
191 idx_page = self.pages_list.curselection()
192 if not idx_page:
193 return
194 page = self.layout.pages[idx_page[0]]
195 for i in reversed(self.page_questions.curselection()):
196 del page.question_ids[i]
197 self.refresh_page_questions(page)
198
199 def on_page_selected(self, event=None):
200 idx = self.pages_list.curselection()
201 if not idx:
202 self.page_questions.delete(0, tk.END)
203 return
204 page = self.layout.pages[idx[0]]
205 self.refresh_page_questions(page)
206
207 # ─────────────────────────────────────────────────────────────
208 # Question management
209 # ─────────────────────────────────────────────────────────────
210 def add_question(self):
211 messagebox.showinfo("Info", "Use the main builder to add questions to the exam.")
212
213 def delete_question(self):
214 idxs = list(self.questions_list.curselection())
215 if not idxs:
216 return
217 if not messagebox.askyesno("Confirm", "Remove selected questions from exam?"):
218 return
219 qids = [self.exam.questions[i].flexo_id for i in idxs]
220 for qid in qids:
221 self.exam.remove_question(qid)
222 for p in self.layout.pages:
223 if qid in p.question_ids:
224 p.question_ids.remove(qid)
225 self.refresh_all()
226
227 # ─────────────────────────────────────────────────────────────
228 # Move question within page
229 # ─────────────────────────────────────────────────────────────
230 def move_q_up(self):
231 """Move selected question one position up within the current page."""
232 idx = self.page_questions.curselection()
233 if not idx:
234 return
235 i = idx[0]
236 page_idx = self.pages_list.curselection()
237 if not page_idx:
238 return
239 page = self.layout.pages[page_idx[0]]
240 if i == 0:
241 return
242 page.question_ids[i - 1], page.question_ids[i] = page.question_ids[i], page.question_ids[i - 1]
243 self.refresh_page_questions(page)
244 self.page_questions.selection_clear(0, tk.END)
245 self.page_questions.selection_set(i - 1)
246
247 def move_q_down(self):
248 """Move selected question one position down within the current page."""
249 idx = self.page_questions.curselection()
250 if not idx:
251 return
252 i = idx[0]
253 page_idx = self.pages_list.curselection()
254 if not page_idx:
255 return
256 page = self.layout.pages[page_idx[0]]
257 if i == len(page.question_ids) - 1:
258 return
259 page.question_ids[i + 1], page.question_ids[i] = page.question_ids[i], page.question_ids[i + 1]
260 self.refresh_page_questions(page)
261 self.page_questions.selection_clear(0, tk.END)
262 self.page_questions.selection_set(i + 1)
263
264 # ─────────────────────────────────────────────────────────────
265 # Finalize
266 # ─────────────────────────────────────────────────────────────
267 def on_ok(self):
268 self.exam.layout = self.layout
269 self.destroy()
Note: See TracBrowser for help on using the repository browser.