/* * Copyright (c) 2021-2022 joshua stein * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include "amend.h" #include "browser.h" #include "committer.h" #include "diff.h" #include "editor.h" #include "focusable.h" #include "patch.h" #include "repo.h" #include "settings.h" #include "tetab.h" #include "util.h" #define DIFF_BUTTON_FONT geneva #define DIFF_BUTTON_FONT_SIZE 12 #define PADDING 10 bool browser_close(struct focusable *focusable); void browser_idle(struct focusable *focusable, EventRecord *event); void browser_update_menu(struct browser *browser); void browser_update(struct focusable *focusable, EventRecord *event); void browser_patch_ldefs(void); void browser_show_amendment(struct browser *browser, struct repo_amendment *amendment); void browser_mouse_down(struct focusable *focusable, EventRecord *event); bool browser_handle_menu(struct focusable *focusable, short menu, short item); void browser_add_files(struct browser *browser); void browser_filter_amendments(struct browser *browser); void browser_discard_changes(struct browser *browser); void browser_edit_amendment(struct browser *browser); Pattern fill_pattern; Handle amendment_list_ldef_h = NULL; Handle file_list_ldef_h = NULL; void browser_idle(struct focusable *focusable, EventRecord *event) { struct browser *browser = (struct browser *)focusable->cookie; switch (browser->state) { case BROWSER_STATE_IDLE: if (browser->need_refresh) { browser->need_refresh = false; browser->state = BROWSER_STATE_UPDATE_AMENDMENT_LIST; } break; case BROWSER_STATE_ADD_FILE: if (repo_add_file(browser->repo) == NULL) browser->state = BROWSER_STATE_IDLE; else browser->state = BROWSER_STATE_UPDATE_FILE_LIST; break; case BROWSER_STATE_UPDATE_FILE_LIST: browser_add_files(browser); browser->state = BROWSER_STATE_IDLE; break; case BROWSER_STATE_UPDATE_AMENDMENT_LIST: browser_filter_amendments(browser); browser->state = BROWSER_STATE_IDLE; break; case BROWSER_STATE_OPEN_COMMITTER: committer_init(browser); browser->state = BROWSER_STATE_WAITING_FOR_COMMITTER; break; case BROWSER_STATE_WAITING_FOR_COMMITTER: break; case BROWSER_STATE_DISCARD_CHANGES: browser_discard_changes(browser); browser->state = BROWSER_STATE_IDLE; break; case BROWSER_STATE_EXPORT_AMENDMENT: browser_export_amendment(browser); browser->state = BROWSER_STATE_IDLE; break; case BROWSER_STATE_APPLY_DIFF: browser_apply_diff(browser); browser->state = BROWSER_STATE_IDLE; break; case BROWSER_STATE_EDIT_AMENDMENT: browser_edit_amendment(browser); browser->state = BROWSER_STATE_IDLE; break; } } struct browser * browser_init(struct repo *repo) { char title[256], filename[256], *justfilename; struct browser *browser; struct focusable *focusable; Rect bounds = { 0 }, te_bounds = { 0 }; Rect data_bounds = { 0, 0, 0, 1 }; /* tlbr */ Point cell_size = { 0, 0 }; Cell cell = { 0 }; short width, height; browser = xmalloczero(sizeof(struct browser), "browser"); browser->state = BROWSER_STATE_IDLE; browser->repo = repo; GetIndPattern(&fill_pattern, sysPatListID, 22); /* main window */ width = screenBits.bounds.right - screenBits.bounds.left - PADDING; if (width > 540) width = 540; height = screenBits.bounds.bottom - screenBits.bounds.top - PADDING - (GetMBarHeight() * 2); if (height > 350) height = 350; center_in_screen(width, height, true, &bounds); memcpy(filename, browser->repo->bile->filename, sizeof(filename)); PtoCstr(filename); justfilename = strrchr(filename, ':'); if (justfilename == NULL) justfilename = (char *)&filename; else justfilename++; snprintf(title, sizeof(title), "%s: %s", PROGRAM_NAME, justfilename); CtoPstr(title); browser->win = NewWindow(0L, &bounds, title, false, noGrowDocProc, (WindowPtr)-1L, true, 0); if (!browser->win) err(1, "Can't create window"); SetPort(browser->win); browser_patch_ldefs(); /* file list */ bounds.top = bounds.left = PADDING; bounds.bottom = browser->win->portRect.bottom - (browser->win->portRect.bottom * 0.6) - 2 - 20 - PADDING - PADDING; bounds.right = 100; cell_size.v = FontHeight(FILE_LIST_FONT, FILE_LIST_FONT_SIZE); browser->file_list = LNew(&bounds, &data_bounds, cell_size, FILE_LDEF_ID, browser->win, true, true, false, true); if (!browser->file_list) err(1, "Can't create file list"); LAddColumn(1, 0, browser->file_list); (*(browser->file_list))->selFlags = lNoNilHilite; /* diff button */ bounds.top = bounds.bottom + PADDING; bounds.bottom = bounds.top + 20; bounds.right += SCROLLBAR_WIDTH; TextFont(DIFF_BUTTON_FONT); TextFace(0); TextSize(DIFF_BUTTON_FONT_SIZE); browser->diff_button = NewControl(browser->win, &bounds, "\pGenerate Diff", true, 1, 1, 1, pushButProc | useWFont, 0L); /* amendment list */ bounds.top = bounds.left = PADDING; bounds.left = bounds.right + PADDING; bounds.right = browser->win->portRect.right - SCROLLBAR_WIDTH - PADDING; cell_size.v = (FontHeight(AMENDMENT_LIST_FONT, AMENDMENT_LIST_FONT_SIZE) * 2) + 2; browser->amendment_list = LNew(&bounds, &data_bounds, cell_size, AMENDMENT_LDEF_ID, browser->win, true, true, false, true); if (!browser->amendment_list) err(1, "Can't create amendment list"); LAddColumn(1, 0, browser->amendment_list); (*(browser->amendment_list))->selFlags = lOnlyOne; /* diff text */ bounds.top = (*browser->amendment_list)->rView.bottom + PADDING; bounds.left = PADDING; bounds.right = browser->win->portRect.right - SCROLLBAR_WIDTH - PADDING; bounds.bottom = browser->win->portRect.bottom - PADDING; te_bounds = bounds; InsetRect(&te_bounds, 2, 2); browser->diff_te = TEStylNew(&te_bounds, &bounds); TEAutoView(true, browser->diff_te); TETabEnable(browser->diff_te); (*(browser->diff_te))->caretHook = NullCaretHook; TEActivate(browser->diff_te); /* scrollbar for diff text */ bounds.right = browser->win->portRect.right - PADDING; bounds.left = bounds.right - SCROLLBAR_WIDTH; bounds.bottom++; bounds.top--; browser->diff_scroller = NewControl(browser->win, &bounds, "\p", true, 1, 1, 1, scrollBarProc, 0L); browser_update_menu(browser); browser_add_files(browser); UpdateScrollbarForTE(browser->win, browser->diff_scroller, browser->diff_te, true); focusable = xmalloczero(sizeof(struct focusable), "focusable"); focusable->cookie = browser; focusable->win = browser->win; focusable->idle = browser_idle; focusable->update = browser_update; focusable->mouse_down = browser_mouse_down; focusable->menu = browser_handle_menu; focusable->close = browser_close; focusable_add(focusable); progress(NULL); SetCursor(&arrow); return browser; } bool browser_close(struct focusable *focusable) { struct browser *browser = (struct browser *)focusable->cookie; if (browser->committer) browser_close_committer(browser); if (browser->repo) repo_close(browser->repo); TEDispose(browser->diff_te); DisposeWindow(browser->win); xfree(&browser); menu_defaults(); return true; } void browser_close_committer(struct browser *browser) { struct focusable *f; if (browser->committer) { f = focusable_find(browser->committer->win); if (f) focusable_close(f); } } void browser_add_files(struct browser *browser) { Cell cell = { 0, 0 }; short i; LDoDraw(false, browser->file_list); LDelRow(0, 0, browser->file_list); browser_show_amendment(browser, NULL); LAddRow(1, cell.v, browser->file_list); /* will become "[All Files]" */ LSetCell(NULL, sizeof(Ptr), cell, browser->file_list); LSetSelect(true, cell, browser->file_list); cell.v++; /* fill in files */ for (i = 0; i < browser->repo->nfiles; i++) { LAddRow(1, cell.v, browser->file_list); LSetCell(&browser->repo->files[i], sizeof(Ptr), cell, browser->file_list); cell.v++; } LDoDraw(true, browser->file_list); browser_filter_amendments(browser); InvalRect(&browser->win->portRect); } short browser_is_all_files_selected(struct browser *browser) { Cell cell = { 0 }; if (browser->repo->nfiles == 0) return 0; /* * "All Files" is always cell.v=0, so if v=0 is selected or nothing * is, select all files */ if (LGetSelect(false, &cell, browser->file_list) == true || LGetSelect(true, &cell, browser->file_list) == false) return 1; return 0; } short browser_selected_file_ids(struct browser *browser, short **selected_files) { Cell cell = { 0 }; short nselected_files = 0, i; if (browser->repo->nfiles == 0) { *selected_files = NULL; return 0; } *selected_files = xcalloc(browser->repo->nfiles, sizeof(short), "selected_files"); if (browser_is_all_files_selected(browser)) { nselected_files = browser->repo->nfiles; for (i = 0; i < browser->repo->nfiles; i++) (*selected_files)[i] = browser->repo->files[i]->id; } else { cell.v = 0; while (LGetSelect(true, &cell, browser->file_list)) { (*selected_files)[nselected_files] = browser->repo->files[cell.v - 1]->id; nselected_files++; LNextCell(true, true, &cell, browser->file_list); } } return nselected_files; } void browser_filter_amendments(struct browser *browser) { Cell cell = { 0 }; struct repo_amendment *amendment; short i, j, k, add = 0; short *selected_files = NULL; short nselected_files = 0; LDoDraw(false, browser->amendment_list); LDelRow(0, 0, browser->amendment_list); browser_show_amendment(browser, NULL); /* fill in amendments for selected files */ nselected_files = browser_selected_file_ids(browser, &selected_files); cell.v = 0; for (i = browser->repo->namendments - 1; i >= 0; i--) { add = 0; amendment = browser->repo->amendments[i]; if (amendment == NULL) continue; for (j = 0; j < amendment->nfiles; j++) { for (k = 0; k < nselected_files; k++) { if (selected_files[k] == amendment->file_ids[j]) { add = 1; break; } } if (add) break; } if (!add) continue; LAddRow(1, cell.v, browser->amendment_list); LSetCell(&(browser->repo->amendments[i]), sizeof(Ptr), cell, browser->amendment_list); cell.v++; } if (browser->repo->unloaded_amendments) { LAddRow(1, cell.v, browser->amendment_list); LSetCell(NULL, 0, cell, browser->amendment_list); } LDoDraw(true, browser->amendment_list); InvalRect(&(*(browser->amendment_list))->rView); if (selected_files) xfree(&selected_files); } void browser_show_amendment(struct browser *browser, struct repo_amendment *amendment) { if (amendment == NULL) { TESetText("", 0, browser->diff_te); HLock(browser->diff_te); InvalRect(&(*(browser->diff_te))->viewRect); HUnlock(browser->diff_te); } else { SetCursor(*(GetCursor(watchCursor))); repo_show_diff_text(browser->repo, amendment, browser->diff_te); SetCursor(&arrow); } UpdateScrollbarForTE(browser->win, browser->diff_scroller, browser->diff_te, true); browser_update_menu(browser); } void browser_discard_changes(struct browser *browser) { char buf[256], filename[256]; struct repo_file *file; short *selected = NULL; short nselected = 0, i, error; SFReply reply; nselected = browser_selected_file_ids(browser, &selected); for (i = 0; i < nselected; i++) { file = repo_file_with_id(browser->repo, selected[i]); snprintf(buf, sizeof(buf), "Save %s:", file->filename); CtoPstr(buf); strlcpy(filename, file->filename, sizeof(filename)); CtoPstr(filename); SFPutFile(centered_sfput_dialog(), buf, filename, NULL, &reply); if (!reply.good) break; error = repo_checkout_file(browser->repo, file, reply.vRefNum, reply.fName); if (error) break; } } void browser_export_amendment(struct browser *browser) { Cell selected = { 0 }; struct repo_amendment *amendment; short len; SFReply reply; char filename[255]; if (LGetSelect(true, &selected, browser->amendment_list) == false) return; len = sizeof(Ptr); LGetCell(&amendment, &len, selected, browser->amendment_list); snprintf(filename, sizeof(filename), "amendment_%d.diff", amendment->id); CtoPstr(filename); SFPutFile(centered_sfput_dialog(), "\pSave as:", filename, NULL, &reply); if (!reply.good) return; repo_export_amendment(browser->repo, amendment, reply.vRefNum, reply.fName); } void browser_apply_diff(struct browser *browser) { SFReply reply; SFGetFile(centered_sfget_dialog(), NULL, NULL, -1, NULL, NULL, &reply); if (!reply.good) return; patch_process(browser->repo, reply.fName, reply.vRefNum); } void browser_edit_amendment(struct browser *browser) { Cell selected = { 0 }; struct repo_amendment *amendment; short len; if (LGetSelect(true, &selected, browser->amendment_list) == false) return; len = sizeof(Ptr); LGetCell(&amendment, &len, selected, browser->amendment_list); editor_init(browser, amendment); } void browser_patch_ldefs(void) { /* dynamically patch LDEFs to point to our custom functions */ if (file_list_ldef_h == NULL || *file_list_ldef_h == NULL || ((tCodeStub *)*file_list_ldef_h)->addr == 0) { file_list_ldef_h = xGetResource('LDEF', FILE_LDEF_ID); HLock(file_list_ldef_h); ((tCodeStub *)*file_list_ldef_h)->addr = &file_list_ldef; } if (amendment_list_ldef_h == NULL || *amendment_list_ldef_h == NULL || ((tCodeStub *)*amendment_list_ldef_h)->addr == 0) { amendment_list_ldef_h = xGetResource('LDEF', AMENDMENT_LDEF_ID); HLock(amendment_list_ldef_h); ((tCodeStub *)*amendment_list_ldef_h)->addr = &amendment_list_ldef; } } void browser_update_menu(struct browser *browser) { TERec *diff; Cell cell = { 0, 0 }; TextFont(systemFont); TextFace(0); TextSize(12); HLock(browser->diff_te); diff = *(browser->diff_te); DisableItem(edit_menu, EDIT_MENU_CUT_ID); if (diff->selStart == diff->selEnd) DisableItem(edit_menu, EDIT_MENU_COPY_ID); else EnableItem(edit_menu, EDIT_MENU_COPY_ID); DisableItem(edit_menu, EDIT_MENU_PASTE_ID); if (diff->nLines == 0) DisableItem(edit_menu, EDIT_MENU_SELECT_ALL_ID); else EnableItem(edit_menu, EDIT_MENU_SELECT_ALL_ID); HUnlock(browser->diff_te); if (browser->repo->nfiles == 0) HiliteControl(browser->diff_button, 255); else HiliteControl(browser->diff_button, 0); if (!browser->committer) { EnableItem(repo_menu, REPO_MENU_ADD_FILE_ID); EnableItem(repo_menu, REPO_MENU_DISCARD_CHANGES_ID); #if 0 EnableItem(repo_menu, REPO_MENU_APPLY_DIFF_ID); #endif } if (LGetSelect(true, &cell, browser->amendment_list)) { EnableItem(amendment_menu, AMENDMENT_MENU_EDIT_ID); EnableItem(amendment_menu, AMENDMENT_MENU_EXPORT_ID); } else { DisableItem(amendment_menu, AMENDMENT_MENU_EDIT_ID); DisableItem(amendment_menu, AMENDMENT_MENU_EXPORT_ID); } } void browser_update(struct focusable *focusable, EventRecord *event) { struct browser *browser = (struct browser *)focusable->cookie; Rect r; short what = -1; Handle t; if (event != NULL) what = event->what; switch (what) { case -1: case updateEvt: FillRect(&browser->win->portRect, fill_pattern); r = (*(browser->diff_te))->viewRect; FillRect(&r, white); TEUpdate(&r, browser->diff_te); InsetRect(&r, -1, -1); FrameRect(&r); /* * Under heavy memory pressure, List Manager seems to close our * custom resources and then re-open them, clearing the custom * addrs we set during setup. Make sure they're still alive before * doing an update or we'll jump to NULL. */ browser_patch_ldefs(); r = (*(browser->file_list))->rView; InsetRect(&r, -1, -1); FillRect(&r, white); FrameRect(&r); LUpdate(browser->win->visRgn, browser->file_list); r = (*(browser->amendment_list))->rView; InsetRect(&r, -1, -1); FillRect(&r, white); FrameRect(&r); LUpdate(browser->win->visRgn, browser->amendment_list); TextFont(DIFF_BUTTON_FONT); TextFace(0); TextSize(DIFF_BUTTON_FONT_SIZE); UpdtControl(browser->win, browser->win->visRgn); browser_update_menu(browser); break; } } void browser_mouse_down(struct focusable *focusable, EventRecord *event) { Cell selected = { 0 }, now = { 0 }; Point p; ControlHandle control; Rect r; struct browser *browser = (struct browser *)focusable->cookie; struct repo_amendment *amendment = NULL; short *selected_files = NULL, *now_selected_files = NULL; short nselected = 0, nnow_selected = 0; short val, adj, was_selected, i, data_len; p = event->where; GlobalToLocal(&p); /* is it in diff text? */ r = (*(browser->diff_te))->viewRect; if (PtInRect(p, &r)) { TEClick(p, ((event->modifiers & shiftKey) != 0), browser->diff_te); browser_update_menu(browser); return; } /* is it in file list? */ r = (*(browser->file_list))->rView; r.right += SCROLLBAR_WIDTH; if (PtInRect(p, &r)) { /* store what is selected now */ nselected = browser_selected_file_ids(browser, &selected_files); /* possibly highlight a new cell */ LClick(p, event->modifiers, browser->file_list); nnow_selected = browser_selected_file_ids(browser, &now_selected_files); if (nnow_selected == 0) { /* can't select nothing, select 'all files' */ selected.v = 0; LSetSelect(true, selected, browser->file_list); if (nselected != browser->repo->nfiles) browser_filter_amendments(browser); } else if (nselected != nnow_selected) { browser_filter_amendments(browser); } else { for (i = 0; i < nselected; i++) { if (selected_files[i] != now_selected_files[i]) { browser_filter_amendments(browser); break; } } } if (selected_files) xfree(&selected_files); if (now_selected_files) xfree(&now_selected_files); return; } /* is it in amendment list? */ r = (*(browser->amendment_list))->rView; r.right += SCROLLBAR_WIDTH; if (PtInRect(p, &r)) { if (browser->repo == NULL) return; /* store what is selected now */ was_selected = LGetSelect(true, &selected, browser->amendment_list); /* possibly highlight a new cell */ LClick(p, event->modifiers, browser->amendment_list); if (LGetSelect(true, &now, browser->amendment_list) == false) { if (was_selected) browser_show_amendment(browser, NULL); } else if (!was_selected || selected.v != now.v) { if (was_selected) LSetSelect(false, selected, browser->amendment_list); LSetSelect(true, now, browser->amendment_list); /* in a filtered list, amendments[now.v] won't be accurate */ data_len = sizeof(Ptr); LGetCell(&amendment, &data_len, now, browser->amendment_list); if (amendment == NULL && browser->repo->unloaded_amendments) { repo_load_amendments(browser->repo, true); browser_filter_amendments(browser); progress(NULL); } else browser_show_amendment(browser, amendment); } return; } switch (FindControl(p, browser->win, &control)) { case inButton: TextFont(DIFF_BUTTON_FONT); TextFace(0); TextSize(DIFF_BUTTON_FONT_SIZE); if (TrackControl(control, p, 0L) && control == browser->diff_button) browser->state = BROWSER_STATE_OPEN_COMMITTER; break; case inUpButton: case inDownButton: case inPageUp: case inPageDown: if (control == browser->diff_scroller) SetTrackControlTE(browser->diff_te); else break; TrackControl(control, p, TrackMouseDownInControl); break; case inThumb: val = GetCtlValue(control); if (TrackControl(control, p, 0L) == 0) break; adj = val - GetCtlValue(control); if (adj != 0) { val -= adj; if (control == browser->diff_scroller) TEScroll(0, adj * TEGetHeight(0, 0, browser->diff_te), browser->diff_te); SetCtlValue(control, val); } break; } } bool browser_handle_menu(struct focusable *focusable, short menu, short item) { struct browser *browser = (struct browser *)focusable->cookie; switch (menu) { case EDIT_MENU_ID: switch (item) { case EDIT_MENU_COPY_ID: TECopy(browser->diff_te); return true; case EDIT_MENU_SELECT_ALL_ID: TESetSelect(0, 1024 * 32, browser->diff_te); return true; } break; case REPO_MENU_ID: switch (item) { case REPO_MENU_ADD_FILE_ID: browser->state = BROWSER_STATE_ADD_FILE; return true; case REPO_MENU_DISCARD_CHANGES_ID: browser->state = BROWSER_STATE_DISCARD_CHANGES; return true; case REPO_MENU_APPLY_DIFF_ID: browser->state = BROWSER_STATE_APPLY_DIFF; return true; } break; case AMENDMENT_MENU_ID: switch (item) { case AMENDMENT_MENU_EDIT_ID: browser->state = BROWSER_STATE_EDIT_AMENDMENT; return true; case AMENDMENT_MENU_EXPORT_ID: browser->state = BROWSER_STATE_EXPORT_AMENDMENT; return true; } break; } return false; }