|
|
@@ -0,0 +1,288 @@
|
|
|
+#include <gtk-3.0/gtk/gtk.h>
|
|
|
+#include <stdbool.h>
|
|
|
+#include <math.h>
|
|
|
+#include <stdio.h>
|
|
|
+
|
|
|
+#define NUM_TRACKS 8
|
|
|
+#define NUM_SAMPLES 15
|
|
|
+#define TRACK_THICKNESS 45.0
|
|
|
+#define SAMPLE_THRESHOLD (TRACK_THICKNESS * 0.8)
|
|
|
+#define TEST_TIME_LIMIT 60 // 60 seconds
|
|
|
+#define EXIT_DELAY_MS 2000 // 2 seconds delay before auto-exit
|
|
|
+
|
|
|
+typedef struct {
|
|
|
+ double x, y;
|
|
|
+ bool active;
|
|
|
+} SamplePoint;
|
|
|
+
|
|
|
+typedef struct {
|
|
|
+ SamplePoint samples[NUM_SAMPLES];
|
|
|
+ bool completed;
|
|
|
+ double x1, y1, x2, y2;
|
|
|
+} Track;
|
|
|
+
|
|
|
+static cairo_surface_t *surface = NULL;
|
|
|
+static double last_x = 0;
|
|
|
+static double last_y = 0;
|
|
|
+static Track tracks[NUM_TRACKS];
|
|
|
+
|
|
|
+static bool test_passed = false;
|
|
|
+static bool test_failed = false;
|
|
|
+static int time_remaining = TEST_TIME_LIMIT;
|
|
|
+static guint timer_id = 0;
|
|
|
+
|
|
|
+static void redraw_all(GtkWidget *widget);
|
|
|
+
|
|
|
+/* Auto-exit callback */
|
|
|
+static gboolean auto_exit_callback(gpointer data) {
|
|
|
+ gtk_main_quit();
|
|
|
+ return FALSE;
|
|
|
+}
|
|
|
+
|
|
|
+/* Handle test end: print result and schedule exit */
|
|
|
+static void handle_test_end(GtkWidget *widget, bool passed) {
|
|
|
+ if (passed) {
|
|
|
+ test_passed = true;
|
|
|
+ printf("PASS\n");
|
|
|
+ } else {
|
|
|
+ test_failed = true;
|
|
|
+ printf("FAIL\n");
|
|
|
+ }
|
|
|
+ fflush(stdout); // Ensure output is printed immediately
|
|
|
+
|
|
|
+ if (timer_id > 0) {
|
|
|
+ g_source_remove(timer_id);
|
|
|
+ timer_id = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Schedule window close after a short delay
|
|
|
+ g_timeout_add(EXIT_DELAY_MS, auto_exit_callback, NULL);
|
|
|
+ redraw_all(widget);
|
|
|
+}
|
|
|
+
|
|
|
+static void init_tracks(GtkWidget *widget) {
|
|
|
+ int w = gtk_widget_get_allocated_width(widget);
|
|
|
+ int h = gtk_widget_get_allocated_height(widget);
|
|
|
+ if (w <= 0 || h <= 0) return;
|
|
|
+
|
|
|
+ // Use explicit double casting to avoid narrowing conversion warnings
|
|
|
+ double dw = (double)w;
|
|
|
+ double dh = (double)h;
|
|
|
+
|
|
|
+ double coords[NUM_TRACKS][4] = {
|
|
|
+ {0.0, 0.0, dw, 0.0}, // 0: Top
|
|
|
+ {0.0, dh, dw, dh}, // 1: Bottom
|
|
|
+ {0.0, 0.0, 0.0, dh}, // 2: Left
|
|
|
+ {dw, 0.0, dw, dh}, // 3: Right
|
|
|
+ {0.0, dh/2.0, dw, dh/2.0}, // 4: H-Mid
|
|
|
+ {dw/2.0, 0.0, dw/2.0, dh}, // 5: V-Mid
|
|
|
+ {0.0, 0.0, dw, dh}, // 6: D1
|
|
|
+ {dw, 0.0, 0.0, dh} // 7: D2
|
|
|
+ };
|
|
|
+
|
|
|
+ for (int t = 0; t < NUM_TRACKS; t++) {
|
|
|
+ tracks[t].x1 = coords[t][0]; tracks[t].y1 = coords[t][1];
|
|
|
+ tracks[t].x2 = coords[t][2]; tracks[t].y2 = coords[t][3];
|
|
|
+ tracks[t].completed = false;
|
|
|
+ for (int i = 0; i < NUM_SAMPLES; i++) {
|
|
|
+ double ratio = (double)i / (NUM_SAMPLES - 1);
|
|
|
+ tracks[t].samples[i].x = tracks[t].x1 + (tracks[t].x2 - tracks[t].x1) * ratio;
|
|
|
+ tracks[t].samples[i].y = tracks[t].y1 + (tracks[t].y2 - tracks[t].y1) * ratio;
|
|
|
+ tracks[t].samples[i].active = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ test_passed = false;
|
|
|
+ test_failed = false;
|
|
|
+ time_remaining = TEST_TIME_LIMIT;
|
|
|
+}
|
|
|
+
|
|
|
+static gboolean timer_callback(gpointer data) {
|
|
|
+ if (test_passed || test_failed) return FALSE;
|
|
|
+
|
|
|
+ time_remaining--;
|
|
|
+ if (time_remaining <= 0) {
|
|
|
+ handle_test_end(GTK_WIDGET(data), false);
|
|
|
+ return FALSE;
|
|
|
+ }
|
|
|
+
|
|
|
+ redraw_all(GTK_WIDGET(data));
|
|
|
+ return TRUE;
|
|
|
+}
|
|
|
+
|
|
|
+static void draw_background_tracks(GtkWidget *widget, cairo_t *cr) {
|
|
|
+ cairo_set_line_width(cr, TRACK_THICKNESS);
|
|
|
+ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
|
|
+
|
|
|
+ for (int t = 0; t < NUM_TRACKS; t++) {
|
|
|
+ if (tracks[t].completed) cairo_set_source_rgba(cr, 0.2, 0.4, 1.0, 0.4);
|
|
|
+ else cairo_set_source_rgba(cr, 0.8, 0.8, 0.8, 0.4);
|
|
|
+
|
|
|
+ cairo_move_to(cr, tracks[t].x1, tracks[t].y1);
|
|
|
+ cairo_line_to(cr, tracks[t].x2, tracks[t].y2);
|
|
|
+ cairo_stroke(cr);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+static void redraw_all(GtkWidget *widget) {
|
|
|
+ if (!surface) return;
|
|
|
+ cairo_t *cr = cairo_create(surface);
|
|
|
+ cairo_set_source_rgb(cr, 1, 1, 1);
|
|
|
+ cairo_paint(cr);
|
|
|
+ draw_background_tracks(widget, cr);
|
|
|
+
|
|
|
+ cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
|
|
|
+
|
|
|
+ // Draw Timer
|
|
|
+ char time_buf[32];
|
|
|
+ snprintf(time_buf, sizeof(time_buf), "Time: %ds", time_remaining);
|
|
|
+ cairo_set_font_size(cr, 20.0);
|
|
|
+ cairo_set_source_rgb(cr, time_remaining < 10 ? 1.0 : 0.0, 0, 0);
|
|
|
+ cairo_move_to(cr, gtk_widget_get_allocated_width(widget) - 120, 40);
|
|
|
+ cairo_show_text(cr, time_buf);
|
|
|
+
|
|
|
+ // Draw Status
|
|
|
+ cairo_set_font_size(cr, 24.0);
|
|
|
+ if (test_passed) {
|
|
|
+ cairo_set_source_rgb(cr, 0, 0.6, 0);
|
|
|
+ cairo_move_to(cr, 30, 50);
|
|
|
+ cairo_show_text(cr, "PASS: Closing in 2s...");
|
|
|
+ } else if (test_failed) {
|
|
|
+ cairo_set_source_rgb(cr, 0.8, 0, 0);
|
|
|
+ cairo_move_to(cr, 30, 50);
|
|
|
+ cairo_show_text(cr, "FAIL: Time Out! Closing...");
|
|
|
+ } else {
|
|
|
+ cairo_set_source_rgb(cr, 0.4, 0.4, 0.4);
|
|
|
+ cairo_move_to(cr, 30, 50);
|
|
|
+ int done = 0;
|
|
|
+ for(int i=0; i<NUM_TRACKS; i++) if(tracks[i].completed) done++;
|
|
|
+ char buf[64];
|
|
|
+ snprintf(buf, sizeof(buf), "Progress: %d/%d tracks finished", done, NUM_TRACKS);
|
|
|
+ cairo_show_text(cr, buf);
|
|
|
+ }
|
|
|
+ cairo_destroy(cr);
|
|
|
+ gtk_widget_queue_draw(widget);
|
|
|
+}
|
|
|
+
|
|
|
+static void check_touch_logic(GtkWidget *widget, gdouble x, gdouble y) {
|
|
|
+ if (test_passed || test_failed) return;
|
|
|
+
|
|
|
+ bool state_changed = false;
|
|
|
+ for (int t = 0; t < NUM_TRACKS; t++) {
|
|
|
+ if (tracks[t].completed) continue;
|
|
|
+
|
|
|
+ int active_count = 0;
|
|
|
+ for (int s = 0; s < NUM_SAMPLES; s++) {
|
|
|
+ if (!tracks[t].samples[s].active) {
|
|
|
+ double dx = x - tracks[t].samples[s].x;
|
|
|
+ double dy = y - tracks[t].samples[s].y;
|
|
|
+ if (sqrt(dx*dx + dy*dy) < SAMPLE_THRESHOLD) {
|
|
|
+ tracks[t].samples[s].active = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (tracks[t].samples[s].active) active_count++;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (active_count == NUM_SAMPLES) {
|
|
|
+ tracks[t].completed = true;
|
|
|
+ state_changed = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (state_changed) {
|
|
|
+ bool all_done = true;
|
|
|
+ for(int i=0; i<NUM_TRACKS; i++) if(!tracks[i].completed) all_done = false;
|
|
|
+ if (all_done) {
|
|
|
+ handle_test_end(widget, true);
|
|
|
+ } else {
|
|
|
+ redraw_all(widget);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+static void draw_line(GtkWidget *widget, gdouble x, gdouble y) {
|
|
|
+ if (test_passed || test_failed) return;
|
|
|
+
|
|
|
+ cairo_t *cr = cairo_create(surface);
|
|
|
+ cairo_set_source_rgb(cr, 0, 0, 0);
|
|
|
+ cairo_set_line_width(cr, TRACK_THICKNESS);
|
|
|
+ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
|
|
+ cairo_move_to(cr, last_x, last_y);
|
|
|
+ cairo_line_to(cr, x, y);
|
|
|
+ cairo_stroke(cr);
|
|
|
+ cairo_destroy(cr);
|
|
|
+
|
|
|
+ double offset = TRACK_THICKNESS / 2.0 + 5.0;
|
|
|
+ double min_x = fmin(last_x, x) - offset, max_x = fmax(last_x, x) + offset;
|
|
|
+ double min_y = fmin(last_y, y) - offset, max_y = fmax(last_y, y) + offset;
|
|
|
+ gtk_widget_queue_draw_area(widget, (int)min_x, (int)min_y, (int)(max_x - min_x), (int)(max_y - min_y));
|
|
|
+
|
|
|
+ last_x = x; last_y = y;
|
|
|
+ check_touch_logic(widget, x, y);
|
|
|
+}
|
|
|
+
|
|
|
+static gboolean configure_event_cb(GtkWidget *widget, GdkEventConfigure *event, gpointer data) {
|
|
|
+ if (surface) cairo_surface_destroy(surface);
|
|
|
+ surface = gdk_window_create_similar_surface(gtk_widget_get_window(widget), CAIRO_CONTENT_COLOR,
|
|
|
+ gtk_widget_get_allocated_width(widget),
|
|
|
+ gtk_widget_get_allocated_height(widget));
|
|
|
+ init_tracks(widget);
|
|
|
+ if (timer_id > 0) g_source_remove(timer_id);
|
|
|
+ timer_id = g_timeout_add_seconds(1, timer_callback, widget);
|
|
|
+ redraw_all(widget);
|
|
|
+ return TRUE;
|
|
|
+}
|
|
|
+
|
|
|
+static gboolean draw_cb(GtkWidget *widget, cairo_t *cr, gpointer data) {
|
|
|
+ cairo_set_source_surface(cr, surface, 0, 0);
|
|
|
+ cairo_paint(cr);
|
|
|
+ return FALSE;
|
|
|
+}
|
|
|
+
|
|
|
+static gboolean button_press_event_cb(GtkWidget *widget, GdkEventButton *event, gpointer data) {
|
|
|
+ if (surface == NULL || test_passed || test_failed) return FALSE;
|
|
|
+ if (event->button == GDK_BUTTON_PRIMARY) {
|
|
|
+ last_x = event->x; last_y = event->y;
|
|
|
+ check_touch_logic(widget, event->x, event->y);
|
|
|
+ } else if (event->button == GDK_BUTTON_SECONDARY) {
|
|
|
+ init_tracks(widget);
|
|
|
+ if (timer_id > 0) g_source_remove(timer_id);
|
|
|
+ timer_id = g_timeout_add_seconds(1, timer_callback, widget);
|
|
|
+ redraw_all(widget);
|
|
|
+ }
|
|
|
+ return TRUE;
|
|
|
+}
|
|
|
+
|
|
|
+static gboolean motion_notify_event_cb(GtkWidget *widget, GdkEventMotion *event, gpointer data) {
|
|
|
+ if (surface == NULL || test_passed || test_failed) return FALSE;
|
|
|
+ if (event->state & GDK_BUTTON1_MASK) draw_line(widget, event->x, event->y);
|
|
|
+ return TRUE;
|
|
|
+}
|
|
|
+
|
|
|
+static void close_window(void) {
|
|
|
+ if (timer_id > 0) g_source_remove(timer_id);
|
|
|
+ if (surface) cairo_surface_destroy(surface);
|
|
|
+ gtk_main_quit();
|
|
|
+}
|
|
|
+
|
|
|
+int main(int argc, char *argv[]) {
|
|
|
+ GtkWidget *window, *frame, *da;
|
|
|
+ gtk_init(&argc, &argv);
|
|
|
+ window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
|
|
+ gtk_window_set_title(GTK_WINDOW(window), "TP Test - Final Version");
|
|
|
+ g_signal_connect(window, "destroy", G_CALLBACK(close_window), NULL);
|
|
|
+ gtk_container_set_border_width(GTK_CONTAINER(window), 8);
|
|
|
+ frame = gtk_frame_new(NULL);
|
|
|
+ gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
|
|
|
+ gtk_container_add(GTK_CONTAINER(window), frame);
|
|
|
+ da = gtk_drawing_area_new();
|
|
|
+ gtk_widget_set_size_request(da, 800, 600);
|
|
|
+ gtk_container_add(GTK_CONTAINER(frame), da);
|
|
|
+ g_signal_connect(da, "draw", G_CALLBACK(draw_cb), NULL);
|
|
|
+ g_signal_connect(da, "configure-event", G_CALLBACK(configure_event_cb), NULL);
|
|
|
+ g_signal_connect(da, "motion-notify-event", G_CALLBACK(motion_notify_event_cb), NULL);
|
|
|
+ g_signal_connect(da, "button-press-event", G_CALLBACK(button_press_event_cb), NULL);
|
|
|
+ gtk_widget_set_events(da, gtk_widget_get_events(da) | GDK_BUTTON_PRESS_MASK | GDK_POINTER_MOTION_MASK);
|
|
|
+ gtk_widget_show_all(window);
|
|
|
+ gtk_main();
|
|
|
+ return 0;
|
|
|
+}
|