/* * gui/gui.c - GUI for eeshow * * Written 2016 by Werner Almesberger * Copyright 2016 by Werner Almesberger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. */ /* * Resources: * * http://zetcode.com/gfx/cairo/cairobackends/ * https://developer.gnome.org/gtk3/stable/gtk-migrating-2-to-3.html */ #define _GNU_SOURCE /* for asprintf */ #include #include #include #include #include #include #include "version.h" #include "misc/util.h" #include "misc/diag.h" #include "file/git-hist.h" #include "kicad/ext.h" #include "kicad/pl.h" #include "kicad/lib.h" #include "kicad/sch.h" #include "kicad/pro.h" #include "kicad/delta.h" #include "gui/aoi.h" #include "gui/input.h" #include "gui/common.h" #include "gui/icons.h" #include "gui/gui.h" /* ----- Helper functions -------------------------------------------------- */ struct gui_sheet *find_corresponding_sheet(struct gui_sheet *pick_from, struct gui_sheet *ref_in, const struct gui_sheet *ref) { struct gui_sheet *sheet, *plan_b; const char *title = ref->sch->title; /* plan A: try to find sheet with same name */ if (title) for (sheet = pick_from; sheet; sheet = sheet->next) if (sheet->sch->title && !strcmp(title, sheet->sch->title)) return sheet; /* plan B: use sheet in same position in sheet sequence */ plan_b = ref_in; for (sheet = pick_from; sheet; sheet = sheet->next) { if (plan_b == ref) return sheet; plan_b = plan_b->next; } /* plan C: just go to the top */ return pick_from; } /* ----- AoIs -------------------------------------------------------------- */ struct sheet_aoi_ctx { struct gui_ctx *gui_ctx; const struct sch_obj *obj; }; static void select_subsheet(void *user) { const struct sheet_aoi_ctx *aoi_ctx = user; struct gui_ctx *ctx = aoi_ctx->gui_ctx; const struct sch_obj *obj = aoi_ctx->obj; struct gui_sheet *sheet; if (!obj->u.sheet.sheet) return; if (!ctx->old_hist || ctx->diff_mode != diff_old) { for (sheet = ctx->new_hist->sheets; sheet; sheet = sheet->next) if (sheet->sch == obj->u.sheet.sheet) { go_to_sheet(ctx, sheet); return; } abort(); } for (sheet = ctx->old_hist->sheets; sheet; sheet = sheet->next) if (sheet->sch == obj->u.sheet.sheet) goto found; abort(); found: sheet = find_corresponding_sheet(ctx->new_hist->sheets, ctx->old_hist->sheets, sheet); go_to_sheet(ctx, sheet); } static void add_sheet_aoi(struct gui_ctx *ctx, struct gui_sheet *parent, const struct sch_obj *obj) { struct sheet_aoi_ctx *aoi_ctx = alloc_type(struct sheet_aoi_ctx); aoi_ctx->gui_ctx = ctx; aoi_ctx->obj = obj; struct aoi aoi = { .x = obj->x, .y = obj->y, .w = obj->u.sheet.w, .h = obj->u.sheet.h, .click = select_subsheet, .user = aoi_ctx, }; aoi_add(&parent->aois, &aoi); } /* ----- Load revisions ---------------------------------------------------- */ void mark_aois(struct gui_ctx *ctx, struct gui_sheet *sheet) { const struct sch_obj *obj; sheet->aois = NULL; for (obj = sheet->sch->objs; obj; obj = obj->next) switch (obj->type) { case sch_obj_sheet: add_sheet_aoi(ctx, sheet, obj); break; case sch_obj_glabel: add_glabel_aoi(sheet, obj); default: break; } } static struct gui_sheet *get_sheets(struct gui_ctx *ctx, struct gui_hist *hist, const struct sheet *sheets) { const struct sheet *sheet; struct gui_sheet *gui_sheets = NULL; struct gui_sheet **next = &gui_sheets; struct gui_sheet *new; for (sheet = sheets; sheet; sheet = sheet->next) { new = alloc_type(struct gui_sheet); new->sch = sheet; new->ctx = ctx; new->hist = hist; new->rendered = 0; *next = new; next = &new->next; } *next = NULL; return gui_sheets; } /* * Library caching: * * We reuse previous components if all libraries are identical * * Future optimizations: * - don't parse into single list of components, so that we can share * libraries that are the same, even if there are others that have changed. * - maybe put components into tree, so that they can be replaced individually * (this would also help to identify sheets that don't need parsing) * * Sheet caching: * * We reuse previous sheets if * - all libraries are identical (whether a given sheet uses them or not), * - they have no sub-sheets, and * - the objects IDs (hashes) are identical. * * Note that we only compare with the immediately preceding (newer) revision, * so branches and merges can disrupt caching. * * Possible optimizations: * - if we record which child sheets a sheet has, we could also clone it, * without having to parse it. However, this is somewhat complex and may * not save all that much time. * - we could record what libraries a sheet uses, and parse only if one of * these has changed (benefits scenarios with many library files), * - we could record what components a sheet uses, and parse only if one of * these has changed (benefits scenarios with few big libraries), * - we could postpone library lookups to render time. * - we could record IDs globally, which would help to avoid tripping over * branches and merges. */ static const struct sheet *parse_files(struct gui_hist *hist, const struct file_names *fn, bool recurse, struct gui_hist *prev) { char *rev = NULL; struct file pro_file, sch_file; struct file pl_file; const struct file *leader = NULL; unsigned libs_open, i; bool libs_cached = 0; bool ok; if (hist->vcs_hist && hist->vcs_hist->commit) rev = vcs_git_get_rev(hist->vcs_hist); if (fn->pro) { if (file_open_revision(&pro_file, rev, fn->pro, NULL)) { free(rev); rev = NULL; /* thus sch_file opens as with file_open */ fn = pro_parse_file(&pro_file, fn); if (!fn) return NULL; leader = &pro_file; } else { /* * If we happen to have a top sheet name, we may as * well try to use it. */ if (!fn->sch) { free(rev); return NULL; } } } sch_init(&hist->sch_ctx, recurse); ok = file_open_revision(&sch_file, rev, fn->sch, leader); free(rev); if (!ok) { sch_free(&hist->sch_ctx); return NULL; } if (!leader) leader = &sch_file; struct file lib_files[fn->n_libs]; lib_init(&hist->lib); for (libs_open = 0; libs_open != fn->n_libs; libs_open++) if (!file_open(lib_files + libs_open, fn->libs[libs_open], leader)) goto fail; if (fn->pl) { if (!file_open(&pl_file, fn->pl, leader)) goto fail; hist->pl = pl_parse(&pl_file); file_close(&pl_file); /* * We treat failing to parse the page layout as a "minor" * failure and don't reject the revision just because of it. */ } if (hist->vcs_hist) { hist->oids = alloc_type_n(void *, libs_open); hist->libs_open = libs_open; for (i = 0; i != libs_open; i++) hist->oids[i] = file_oid(lib_files + i); if (prev && prev->vcs_hist && prev->libs_open == libs_open) { for (i = 0; i != libs_open; i++) if (!file_oid_eq(hist->oids[i], prev->oids[i])) break; if (i == libs_open) { hist->lib.comps = prev->lib.comps; libs_cached = 1; } } } if (!libs_cached) for (i = 0; i != libs_open; i++) if (!lib_parse_file(&hist->lib, lib_files +i)) goto fail; if (!sch_parse(&hist->sch_ctx, &sch_file, &hist->lib, libs_cached ? &prev->sch_ctx : NULL)) goto fail; for (i = 0; i != libs_open; i++) file_close(lib_files + i); file_close(&sch_file); // @@@ close pro_file if (prev && prev->sheets && sheet_eq(prev->sch_ctx.sheets, hist->sch_ctx.sheets)) prev->identical = 1; /* * @@@ we have a major memory leak for the component library. * We should record parsed schematics and libraries separately, so * that we can clean them up, without having to rely on the history, * with - when sharing unchanged item - possibly many duplicate * pointers. */ return hist->sch_ctx.sheets; fail: while (libs_open--) file_close(lib_files + libs_open); sch_free(&hist->sch_ctx); lib_free(&hist->lib); file_close(&sch_file); // @@@ close pro_file return NULL; } struct add_hist_ctx { struct gui_ctx *ctx; const struct file_names *fn; bool recurse; unsigned limit; }; static void add_hist(void *user, struct hist *h) { struct add_hist_ctx *ahc = user; struct gui_ctx *ctx = ahc->ctx; struct gui_hist **anchor, *hist, *prev; const struct sheet *sch; unsigned age = 0; if (!ahc->limit) return; if (ahc->limit > 0) ahc->limit--; prev = NULL; for (anchor = &ctx->hist; *anchor; anchor = &(*anchor)->next) { prev = *anchor; age++; } hist = alloc_type(struct gui_hist); hist->ctx = ctx; hist->vcs_hist = h; hist->libs_open = 0; hist->identical = 0; hist->pl = NULL; sch = parse_files(hist, ahc->fn, ahc->recurse, prev); hist->sheets = sch ? get_sheets(ctx, hist, sch) : NULL; hist->age = age; hist->next = NULL; *anchor = hist; if (ctx->hist_size) progress_update(ctx); } static void get_revisions(struct gui_ctx *ctx, const struct file_names *fn, bool recurse, int limit) { struct add_hist_ctx add_hist_ctx = { .ctx = ctx, .fn = fn, .recurse = recurse, .limit = limit ? limit < 0 ? -limit : limit : -1, }; if (ctx->vcs_hist) hist_iterate(ctx->vcs_hist, add_hist, &add_hist_ctx); else add_hist(&add_hist_ctx, NULL); } /* ----- Retrieve and count history ---------------------------------------- */ static void count_history(void *user, struct hist *h) { struct gui_ctx *ctx = user; ctx->hist_size++; } static void get_history(struct gui_ctx *ctx, const char *sch_name, int limit) { if (!vcs_git_try(sch_name)) { ctx->vcs_hist = NULL; return; } ctx->vcs_hist = vcs_git_hist(sch_name); if (limit) ctx->hist_size = limit > 0 ? limit : -limit; else hist_iterate(ctx->vcs_hist, count_history, ctx); } /* ----- Initialization ---------------------------------------------------- */ int gui(const struct file_names *fn, bool recurse, int limit) { GtkWidget *window; char *title; struct gui_ctx ctx = { .scale = 1 / 16.0, .hist = NULL, .vcs_hist = NULL, .showing_history= 0, .sheet_overlays = NULL, .hist_overlays = NULL, .pop_overlays = NULL, .pop_underlays = NULL, .pop_origin = NULL, .glabel = NULL, .aois = NULL, .diff_mode = diff_delta, .old_hist = NULL, .hist_y_offset = 0, .hist_size = 0, }; window = gtk_window_new(GTK_WINDOW_TOPLEVEL); ctx.da = gtk_drawing_area_new(); gtk_container_add(GTK_CONTAINER(window), ctx.da); gtk_window_set_default_size(GTK_WINDOW(window), 640, 480); if (asprintf(&title, "eeshow (rev %s)", version)) {}; gtk_window_set_title(GTK_WINDOW(window), title); gtk_widget_set_events(ctx.da, GDK_EXPOSE | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK); input_setup(ctx.da); gtk_widget_show_all(window); get_history(&ctx, fn->pro ? fn->pro : fn->sch, limit); if (ctx.hist_size) setup_progress_bar(&ctx, window); get_revisions(&ctx, fn, recurse, limit); for (ctx.new_hist = ctx.hist; ctx.new_hist && !ctx.new_hist->sheets; ctx.new_hist = ctx.new_hist->next); if (!ctx.new_hist) fatal("no valid sheets\n"); g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL); icons_init(); sheet_setup(&ctx); render_setup(&ctx); // gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); go_to_sheet(&ctx, ctx.new_hist->sheets); gtk_widget_show_all(window); /* for performance testing, use -N-depth */ if (limit >= 0) gtk_main(); return 0; }