1 /*
2 * TilEm II
3 *
4 * Copyright (c) 2010-2011 Thibault Duponchelle
5 * Copyright (c) 2010-2012 Benjamin Moody
6 *
7 * This program is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 #ifdef HAVE_CONFIG_H
22 # include <config.h>
23 #endif
24
25 #include <stdio.h>
26 #include <gtk/gtk.h>
27 #include <glib/gstdio.h>
28 #include <ticalcs.h>
29 #include <tilem.h>
30
31 #include "gui.h"
32 #include "files.h"
33 #include "msgbox.h"
34
35 /* Set size hints for the toplevel window */
set_size_hints(GtkWidget * widget,gpointer data)36 static void set_size_hints(GtkWidget *widget, gpointer data)
37 {
38 TilemEmulatorWindow *ewin = data;
39
40 /* Don't use gtk_window_set_geometry_hints() (which would
41 appear to do what we want) because, in addition to setting
42 the hints we want, that function causes GTK+ to argue with
43 the window manager.
44
45 Instead, we call this function after the check-resize
46 signal (which is when GTK+ itself would normally set the
47 hints.)
48
49 FIXME: check that this works as desired on Win32/Quartz. */
50
51 if (gtk_widget_get_window(widget))
52 gdk_window_set_geometry_hints(gtk_widget_get_window(widget),
53 &ewin->geomhints,
54 ewin->geomhintmask);
55 }
56
window_state_changed(G_GNUC_UNUSED GtkWidget * w,GdkEventWindowState * event,gpointer data)57 static void window_state_changed(G_GNUC_UNUSED GtkWidget *w,
58 GdkEventWindowState *event, gpointer data)
59 {
60 TilemEmulatorWindow *ewin = data;
61 ewin->window_state = event->new_window_state;
62 }
63
window_maximized(TilemEmulatorWindow * ewin)64 static gboolean window_maximized(TilemEmulatorWindow *ewin)
65 {
66 return (ewin->window_state & (GDK_WINDOW_STATE_MAXIMIZED
67 | GDK_WINDOW_STATE_FULLSCREEN));
68 }
69
screen_repaint(GtkWidget * w,GdkEventExpose * ev G_GNUC_UNUSED,TilemEmulatorWindow * ewin)70 static gboolean screen_repaint(GtkWidget *w, GdkEventExpose *ev G_GNUC_UNUSED,
71 TilemEmulatorWindow *ewin)
72 {
73 GtkAllocation alloc;
74 GdkWindow *win;
75 GtkStyle *style;
76
77 gtk_widget_get_allocation(w, &alloc);
78
79 /* If image buffer is not the correct size, allocate a new one */
80
81 if (!ewin->lcd_image_buf
82 || alloc.width != ewin->lcd_image_width
83 || alloc.height != ewin->lcd_image_height) {
84 ewin->lcd_image_width = alloc.width;
85 ewin->lcd_image_height = alloc.height;
86 g_free(ewin->lcd_image_buf);
87 ewin->lcd_image_buf = g_new(byte, alloc.width * alloc.height);
88 }
89
90 /* Draw LCD contents into the image buffer */
91
92 g_mutex_lock(ewin->emu->lcd_mutex);
93 ewin->emu->lcd_update_pending = FALSE;
94 tilem_draw_lcd_image_indexed(ewin->emu->lcd_buffer,
95 ewin->lcd_image_buf,
96 alloc.width, alloc.height, alloc.width,
97 (ewin->lcd_smooth_scale
98 ? TILEM_SCALE_SMOOTH
99 : TILEM_SCALE_FAST));
100 g_mutex_unlock(ewin->emu->lcd_mutex);
101
102 /* Render buffer to the screen */
103
104 win = gtk_widget_get_window(w);
105 style = gtk_widget_get_style(w);
106 gdk_draw_indexed_image(win, style->fg_gc[GTK_STATE_NORMAL], 0, 0,
107 alloc.width, alloc.height, GDK_RGB_DITHER_NONE,
108 ewin->lcd_image_buf, alloc.width,
109 ewin->lcd_cmap);
110 return TRUE;
111 }
112
113 /* Set the color palette for drawing the emulated LCD. */
screen_restyle(GtkWidget * w,GtkStyle * oldstyle G_GNUC_UNUSED,TilemEmulatorWindow * ewin)114 static void screen_restyle(GtkWidget* w, GtkStyle* oldstyle G_GNUC_UNUSED,
115 TilemEmulatorWindow* ewin)
116 {
117 dword* palette;
118 GtkStyle *style;
119 int r_dark, g_dark, b_dark;
120 int r_light, g_light, b_light;
121 double gamma = 2.2;
122
123 if (!ewin->skin) {
124 /* no skin -> use standard GTK colors */
125
126 style = gtk_widget_get_style(w);
127
128 r_dark = style->text[GTK_STATE_NORMAL].red / 257;
129 g_dark = style->text[GTK_STATE_NORMAL].green / 257;
130 b_dark = style->text[GTK_STATE_NORMAL].blue / 257;
131
132 r_light = style->base[GTK_STATE_NORMAL].red / 257;
133 g_light = style->base[GTK_STATE_NORMAL].green / 257;
134 b_light = style->base[GTK_STATE_NORMAL].blue / 257;
135 }
136 else {
137 /* use skin colors */
138
139 r_dark = ((ewin->skin->lcd_black >> 16) & 0xff);
140 g_dark = ((ewin->skin->lcd_black >> 8) & 0xff);
141 b_dark = (ewin->skin->lcd_black & 0xff);
142
143 r_light = ((ewin->skin->lcd_white >> 16) & 0xff);
144 g_light = ((ewin->skin->lcd_white >> 8) & 0xff);
145 b_light = (ewin->skin->lcd_white & 0xff);
146 }
147
148 /* Generate a new palette, and convert it into GDK format */
149
150 if (ewin->lcd_cmap)
151 gdk_rgb_cmap_free(ewin->lcd_cmap);
152
153 palette = tilem_color_palette_new(r_light, g_light, b_light,
154 r_dark, g_dark, b_dark, gamma);
155 ewin->lcd_cmap = gdk_rgb_cmap_new(palette, 256);
156 tilem_free(palette);
157
158 gtk_widget_queue_draw(ewin->lcd);
159 }
160
skin_size_allocate(GtkWidget * widget,GtkAllocation * alloc,gpointer data)161 static void skin_size_allocate(GtkWidget *widget, GtkAllocation *alloc,
162 gpointer data)
163 {
164 TilemEmulatorWindow *ewin = data;
165 int rawwidth, rawheight, scaledwidth, scaledheight;
166 int lcdleft, lcdright, lcdtop, lcdbottom;
167 double rx, ry, r;
168
169 g_return_if_fail(ewin->skin != NULL);
170
171 rawwidth = gdk_pixbuf_get_width(ewin->skin->raw);
172 rawheight = gdk_pixbuf_get_height(ewin->skin->raw);
173
174 rx = (double) alloc->width / rawwidth;
175 ry = (double) alloc->height / rawheight;
176 r = MIN(rx, ry);
177
178 scaledwidth = rawwidth * r;
179 scaledheight = rawheight * r;
180
181 if (ewin->skin->width == scaledwidth
182 && ewin->skin->height == scaledheight)
183 return;
184
185 ewin->skin->width = scaledwidth;
186 ewin->skin->height = scaledheight;
187 ewin->skin->sx = ewin->skin->sy = 1.0 / r;
188
189 if (ewin->skin->image)
190 g_object_unref(ewin->skin->image);
191 ewin->skin->image = gdk_pixbuf_scale_simple(ewin->skin->raw,
192 scaledwidth,
193 scaledheight,
194 GDK_INTERP_BILINEAR);
195
196 gtk_image_set_from_pixbuf(GTK_IMAGE(ewin->background),
197 ewin->skin->image);
198
199 lcdleft = ewin->skin->lcd_pos.left * r + 0.5;
200 lcdright = ewin->skin->lcd_pos.right * r + 0.5;
201 lcdtop = ewin->skin->lcd_pos.top * r + 0.5;
202 lcdbottom = ewin->skin->lcd_pos.bottom * r + 0.5;
203
204 gtk_widget_set_size_request(ewin->lcd,
205 MAX(lcdright - lcdleft, 1),
206 MAX(lcdbottom - lcdtop, 1));
207
208 gtk_layout_move(GTK_LAYOUT(widget), ewin->lcd,
209 lcdleft, lcdtop);
210
211 ewin->zoom_factor = r / ewin->base_zoom;
212
213 if (ewin->zoom_factor <= 1.0)
214 ewin->zoom_factor = 1.0;
215 }
216
noskin_size_allocate(G_GNUC_UNUSED GtkWidget * widget,GtkAllocation * alloc,gpointer data)217 static void noskin_size_allocate(G_GNUC_UNUSED GtkWidget *widget,
218 GtkAllocation *alloc, gpointer data)
219 {
220 TilemEmulatorWindow *ewin = data;
221 int lcdwidth, lcdheight;
222
223 g_return_if_fail(ewin->emu->calc != NULL);
224
225 lcdwidth = ewin->emu->calc->hw.lcdwidth;
226 lcdheight = ewin->emu->calc->hw.lcdheight;
227
228 if (alloc->width > alloc->height)
229 ewin->zoom_factor = (gdouble) alloc->width / lcdwidth;
230 else
231 ewin->zoom_factor = (gdouble) alloc->height / lcdheight;
232
233 if (ewin->zoom_factor <= 1.0)
234 ewin->zoom_factor = 1.0;
235 }
236
237 /* Used when you load another skin */
redraw_screen(TilemEmulatorWindow * ewin)238 void redraw_screen(TilemEmulatorWindow *ewin)
239 {
240 GtkWidget *pImage;
241 GtkWidget *emuwin;
242 int lcdwidth, lcdheight;
243 int screenwidth, screenheight;
244 int minwidth, minheight, defwidth, defheight,
245 curwidth, curheight;
246 double sx, sy, s, a1, a2;
247 GError *err = NULL;
248
249 if (ewin->skin) {
250 skin_unload(ewin->skin);
251 g_free(ewin->skin);
252 }
253
254 if (ewin->skin_disabled || !ewin->skin_file_name) {
255 ewin->skin = NULL;
256 }
257 else {
258 ewin->skin = g_new0(SKIN_INFOS, 1);
259 if (skin_load(ewin->skin, ewin->skin_file_name, &err)) {
260 skin_unload(ewin->skin);
261 ewin->skin = NULL;
262 }
263 }
264
265 if (ewin->emu->calc) {
266 lcdwidth = ewin->emu->calc->hw.lcdwidth;
267 lcdheight = ewin->emu->calc->hw.lcdheight;
268 }
269 else {
270 lcdwidth = lcdheight = 1;
271 }
272
273 if (ewin->lcd)
274 gtk_widget_destroy(ewin->lcd);
275 if (ewin->background)
276 gtk_widget_destroy(ewin->background);
277 if (ewin->layout)
278 gtk_widget_destroy(ewin->layout);
279
280 /* create LCD widget */
281 ewin->lcd = gtk_drawing_area_new();
282 gtk_widget_set_double_buffered(ewin->lcd, FALSE);
283
284 /* create background image and layout */
285 if (ewin->skin) {
286 ewin->layout = gtk_layout_new(NULL, NULL);
287
288 pImage = gtk_image_new();
289 gtk_layout_put(GTK_LAYOUT(ewin->layout), pImage, 0, 0);
290 ewin->background = pImage;
291
292 screenwidth = (ewin->skin->lcd_pos.right
293 - ewin->skin->lcd_pos.left);
294 screenheight = (ewin->skin->lcd_pos.bottom
295 - ewin->skin->lcd_pos.top);
296
297 gtk_widget_set_size_request(ewin->lcd,
298 screenwidth, screenheight);
299 gtk_layout_put(GTK_LAYOUT(ewin->layout), ewin->lcd,
300 ewin->skin->lcd_pos.left,
301 ewin->skin->lcd_pos.top);
302
303 g_signal_connect(ewin->layout, "size-allocate",
304 G_CALLBACK(skin_size_allocate), ewin);
305
306 emuwin = ewin->layout;
307
308 defwidth = ewin->skin->width;
309 defheight = ewin->skin->height;
310
311 sx = (double) lcdwidth / screenwidth;
312 sy = (double) lcdheight / screenheight;
313 s = MAX(sx, sy);
314 minwidth = defwidth * s + 0.5;
315 minheight = defheight * s + 0.5;
316 ewin->base_zoom = s;
317 }
318 else {
319 ewin->layout = NULL;
320 ewin->background = NULL;
321
322 emuwin = ewin->lcd;
323
324 g_signal_connect(ewin->lcd, "size-allocate",
325 G_CALLBACK(noskin_size_allocate), ewin);
326
327 defwidth = minwidth = lcdwidth;
328 defheight = minheight = lcdheight;
329 ewin->base_zoom = 1.0;
330 }
331
332 curwidth = defwidth * ewin->base_zoom * ewin->zoom_factor + 0.5;
333 curheight = defheight * ewin->base_zoom * ewin->zoom_factor + 0.5;
334
335 gtk_widget_set_can_focus(emuwin, TRUE);
336
337 gtk_widget_add_events(emuwin, (GDK_BUTTON_PRESS_MASK
338 | GDK_BUTTON_RELEASE_MASK
339 | GDK_BUTTON1_MOTION_MASK
340 | GDK_POINTER_MOTION_HINT_MASK
341 | GDK_DROP_FINISHED
342 | GDK_DRAG_MOTION));
343
344 g_signal_connect(ewin->lcd, "expose-event",
345 G_CALLBACK(screen_repaint), ewin);
346 g_signal_connect(ewin->lcd, "style-set",
347 G_CALLBACK(screen_restyle), ewin);
348
349 g_signal_connect(emuwin, "button-press-event",
350 G_CALLBACK(mouse_press_event), ewin);
351 g_signal_connect(emuwin, "motion-notify-event",
352 G_CALLBACK(pointer_motion_event), ewin);
353 g_signal_connect(emuwin, "button-release-event",
354 G_CALLBACK(mouse_release_event), ewin);
355 g_signal_connect(emuwin, "popup-menu",
356 G_CALLBACK(popup_menu_event), ewin);
357
358 /* FIXME: this is rather broken; GTK_DEST_DEFAULT_ALL sends a
359 successful "finished" message to any drop that matches the
360 list of targets. Files/URIs we can't accept should be
361 rejected, and we shouldn't send the "finished" message
362 until it's actually finished. */
363 gtk_drag_dest_set(emuwin, GTK_DEST_DEFAULT_ALL,
364 NULL, 0, GDK_ACTION_COPY);
365 gtk_drag_dest_add_uri_targets(emuwin);
366 g_signal_connect(emuwin, "drag-data-received",
367 G_CALLBACK(drag_data_received), ewin);
368
369
370 /* Hint calculation assumes the emulator is the only thing in
371 the window; if other widgets are added, this will have to
372 change accordingly
373 */
374 ewin->geomhints.min_width = minwidth;
375 ewin->geomhints.min_height = minheight;
376 a1 = (double) minwidth / minheight;
377 a2 = (double) defwidth / defheight;
378 ewin->geomhints.min_aspect = MIN(a1, a2) - 0.0001;
379 ewin->geomhints.max_aspect = MAX(a1, a2) + 0.0001;
380 ewin->geomhintmask = (GDK_HINT_MIN_SIZE | GDK_HINT_ASPECT);
381
382 /* If the window is already realized, set the hints now, so
383 that the WM will see the new hints before we try to resize
384 the window */
385 set_size_hints(ewin->window, ewin);
386
387 gtk_widget_set_size_request(emuwin, minwidth, minheight);
388 gtk_container_add(GTK_CONTAINER(ewin->window), emuwin);
389
390 if (!window_maximized(ewin))
391 gtk_window_resize(GTK_WINDOW(ewin->window),
392 curwidth, curheight);
393
394 gtk_widget_show_all(emuwin);
395
396 if (err) {
397 messagebox01(GTK_WINDOW(ewin->window), GTK_MESSAGE_ERROR,
398 "Unable to load skin", "%s", err->message);
399 g_error_free(err);
400 }
401 }
402
window_destroy(G_GNUC_UNUSED GtkWidget * w,gpointer data)403 static void window_destroy(G_GNUC_UNUSED GtkWidget *w, gpointer data)
404 {
405 TilemEmulatorWindow *ewin = data;
406
407 if (!window_maximized(ewin))
408 tilem_config_set("settings",
409 "zoom/r", ewin->zoom_factor,
410 NULL);
411
412 ewin->window = ewin->layout = ewin->lcd = ewin->background = NULL;
413 }
414
tilem_emulator_window_new(TilemCalcEmulator * emu)415 TilemEmulatorWindow *tilem_emulator_window_new(TilemCalcEmulator *emu)
416 {
417 TilemEmulatorWindow *ewin;
418
419 g_return_val_if_fail(emu != NULL, NULL);
420
421 ewin = g_slice_new0(TilemEmulatorWindow);
422 ewin->emu = emu;
423
424 tilem_config_get("settings",
425 "skin_disabled/b", &ewin->skin_disabled,
426 "smooth_scaling/b=1", &ewin->lcd_smooth_scale,
427 "zoom/r=2.0", &ewin->zoom_factor,
428 NULL);
429
430 if (ewin->zoom_factor <= 1.0)
431 ewin->zoom_factor = 1.0;
432
433 /* Create the window */
434 ewin->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
435
436 g_signal_connect(ewin->window, "window-state-event",
437 G_CALLBACK(window_state_changed), ewin);
438 g_signal_connect(ewin->window, "destroy",
439 G_CALLBACK(window_destroy), ewin);
440
441 g_signal_connect_after(ewin->window, "check-resize",
442 G_CALLBACK(set_size_hints), ewin);
443
444 gtk_widget_add_events(ewin->window, (GDK_KEY_PRESS_MASK
445 | GDK_KEY_RELEASE_MASK));
446
447 g_signal_connect(ewin->window, "key-press-event",
448 G_CALLBACK(key_press_event), ewin);
449 g_signal_connect(ewin->window, "key-release-event",
450 G_CALLBACK(key_release_event), ewin);
451
452 build_menu(ewin);
453
454 tilem_emulator_window_calc_changed(ewin);
455
456 return ewin;
457 }
458
tilem_emulator_window_free(TilemEmulatorWindow * ewin)459 void tilem_emulator_window_free(TilemEmulatorWindow *ewin)
460 {
461 g_return_if_fail(ewin != NULL);
462
463 if (ewin->lcd)
464 gtk_widget_destroy(ewin->lcd);
465 if (ewin->background)
466 gtk_widget_destroy(ewin->background);
467 if (ewin->layout)
468 gtk_widget_destroy(ewin->layout);
469 if (ewin->window)
470 gtk_widget_destroy(ewin->window);
471 if (ewin->popup_menu)
472 gtk_widget_destroy(ewin->popup_menu);
473 if (ewin->actions)
474 g_object_unref(ewin->actions);
475
476 g_free(ewin->lcd_image_buf);
477
478 g_free(ewin->skin_file_name);
479 if (ewin->skin) {
480 skin_unload(ewin->skin);
481 g_free(ewin->skin);
482 }
483
484 if (ewin->lcd_cmap)
485 gdk_rgb_cmap_free(ewin->lcd_cmap);
486
487 g_slice_free(TilemEmulatorWindow, ewin);
488 }
489
tilem_emulator_window_set_skin(TilemEmulatorWindow * ewin,const char * filename)490 void tilem_emulator_window_set_skin(TilemEmulatorWindow *ewin,
491 const char *filename)
492 {
493 g_return_if_fail(ewin != NULL);
494
495 g_free(ewin->skin_file_name);
496 if (filename)
497 ewin->skin_file_name = g_strdup(filename);
498 else
499 ewin->skin_file_name = NULL;
500 redraw_screen(ewin);
501 }
502
503 /* Switch between skin and LCD-only mode */
tilem_emulator_window_set_skin_disabled(TilemEmulatorWindow * ewin,gboolean disabled)504 void tilem_emulator_window_set_skin_disabled(TilemEmulatorWindow *ewin,
505 gboolean disabled)
506 {
507 g_return_if_fail(ewin != NULL);
508
509 if (ewin->skin_disabled != !!disabled) {
510 ewin->skin_disabled = !!disabled;
511 redraw_screen(ewin);
512 }
513 }
514
tilem_emulator_window_calc_changed(TilemEmulatorWindow * ewin)515 void tilem_emulator_window_calc_changed(TilemEmulatorWindow *ewin)
516 {
517 const char *model;
518 char *name = NULL, *path;
519
520 g_return_if_fail(ewin != NULL);
521 g_return_if_fail(ewin->emu != NULL);
522
523 g_free(ewin->skin_file_name);
524 ewin->skin_file_name = NULL;
525
526 if (!ewin->emu->calc)
527 return;
528
529 model = ewin->emu->calc->hw.name;
530
531 tilem_config_get(model,
532 "skin/f", &name,
533 NULL);
534
535 if (!name)
536 name = g_strdup_printf("%s.skn", model);
537
538 if (!g_path_is_absolute(name)) {
539 path = get_shared_file_path("skins", name, NULL);
540 tilem_emulator_window_set_skin(ewin, path);
541 g_free(path);
542 }
543 else {
544 tilem_emulator_window_set_skin(ewin, name);
545 }
546
547 g_free(name);
548 }
549
tilem_emulator_window_refresh_lcd(TilemEmulatorWindow * ewin)550 void tilem_emulator_window_refresh_lcd(TilemEmulatorWindow *ewin)
551 {
552 g_return_if_fail(ewin != NULL);
553 if (ewin->lcd)
554 gtk_widget_queue_draw(ewin->lcd);
555 }
556
557
558
559
560 /* Display the lcd image into the terminal */
display_lcdimage_into_terminal(TilemEmulatorWindow * ewin)561 void display_lcdimage_into_terminal(TilemEmulatorWindow *ewin)
562 {
563
564 int width, height;
565 guchar* lcddata;
566 int x, y;
567 char c;
568 width = ewin->emu->calc->hw.lcdwidth;
569 height = ewin->emu->calc->hw.lcdheight;
570 FILE* lcd_content_file;
571 /* Alloc mmem */
572 lcddata = g_new(guchar, (width / 8) * height);
573
574 /* Get the lcd content using the function 's pointer from Benjamin's core */
575 (*ewin->emu->calc->hw.get_lcd)(ewin->emu->calc, lcddata);
576
577 /* Print a little demo just for fun;) */
578 printf("\n\n\n");
579 printf(" r rr r rrrrr rrr r rrrrr r r rr r rr r \n");
580 printf(" r r r r r r r r rr rr r r r r r r r \n");
581 printf(" r r r r r r r r r r r r r r r r r r \n");
582 printf("rrrrr r r r r r r rrrr r r r r r r r rrrrr rrrrr rrrrr \n");
583 printf(" r r r r r r r r r r rrr r r r r r r \n");
584 printf(" r r r r r r r r r r r r r r r r \n");
585 printf(" r rr r r rrr rrrrr rrrrr r r r rr r \n");
586 printf("\n(Here is just a sample...)\n\n");
587
588 /* Request user to know which char user wants */
589
590 printf("Which char to display FOR BLACK?\n");
591 scanf("%c", &c); /* Choose wich char for the black */
592
593 //printf("Which char to display FOR GRAY ?\n");
594 //scanf("%c", &b); /* Choose wich char for the black */
595
596 lcd_content_file = g_fopen("lcd_content.txt", "w");
597
598 printf("\n\n\n### LCD CONTENT ###\n\n\n\n");
599 for (y = 0; y < height; y++) {
600 for (x = 0; x < width; x++) {
601 /*printf("%d ", lcddata[y * width + x]); */
602 if (lcddata[(y * width + x) / 8] & (0x80 >> (x % 8))) {
603 printf("%c", c);
604 if(lcd_content_file != NULL)
605 fprintf(lcd_content_file,"%c", c);
606 } else {
607 printf("%c", ' ');
608 if(lcd_content_file != NULL)
609 fprintf(lcd_content_file,"%c", ' ');
610 }
611 }
612 printf("\n");
613 if(lcd_content_file != NULL)
614 fprintf(lcd_content_file,"%c", '\n');
615 }
616 if(lcd_content_file != NULL) {
617 fclose(lcd_content_file);
618 printf("\n### END ###\n\nSaved into lcd_content.txt\n");
619 }
620
621 }
622