diff --git a/README.md b/README.md index b57ca09..d639f41 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Extracts and organizes documents from FileCabinet CS (Thomson Reuters) using GUI | `fccs_export.py` | Step 2: Automate FCCS GUI to export all drawers | | `fccs_reorganize.py` | Step 3: Parse filenames and rebuild folder structure | | `fccs_verify.py` | Step 4 (optional): Compare manifests against exported files | +| `fccs_dump_controls.py` | Utility: Dump control identifiers of an on-screen dialog | ## Setup Per Engagement @@ -36,12 +37,12 @@ python fccs_scan.py Reads the restored FCCS backup directory and writes all drawer IDs (subfolder names) to `drawer_ids.txt`. It also: -- **Flags prefix clashes** — if one drawer ID is a prefix of another (e.g. `02218` and `02218A`), searching them in FCCS can pop up a selection box that breaks automated navigation. These clashes are reported, and **every drawer in each clash group** is auto-seeded into `ignore.txt` for review. +- **Flags prefix clashes** — if one drawer ID is a prefix of another (e.g. `02218` and `02218A`), searching the **base** ID in FCCS pops up a selection box that breaks plain automated navigation. These clashes are reported, and the base (shorter) IDs are auto-seeded into `ignore.txt`. The longer, more-specific IDs (`02218A`) search fine and export normally; the base IDs are handled by the separate clash-export script. - **Reports ignored drawers** — any IDs listed in `ignore.txt` that exist in this backup are shown as ones the export will skip. -**Ignoring drawers:** The scan creates `ignore.txt` (at `ignore_file`, default `C:\Migration\ignore.txt`) if it doesn't exist and pre-fills it with every drawer in each clash group — searching those in FCCS can show a selection box that stalls the automation, so they're skipped by default. Open the file and: +**Ignoring drawers:** The scan creates `ignore.txt` (at `ignore_file`, default `C:\Migration\ignore.txt`) if it doesn't exist and pre-fills it with the clash base IDs — searching those in FCCS shows a selection box that stalls the plain export, so they're skipped by the main export. Open the file and: -- **Delete or comment out** any clashing drawer you actually want exported (then handle it manually in FCCS). +- **Delete or comment out** any clash base you'd rather handle fully by hand. - **Add** any other drawers to skip (e.g. password-protected folders), one ID per line. Re-running the scan never overwrites your edits — it only appends newly-discovered clashes. `drawer_ids.txt` stays a full inventory; the export skips anything active in the ignore list. Lines starting with `#` are comments. diff --git a/__pycache__/fccs_scan.cpython-313.pyc b/__pycache__/fccs_scan.cpython-313.pyc index ec8de81..69beba4 100644 Binary files a/__pycache__/fccs_scan.cpython-313.pyc and b/__pycache__/fccs_scan.cpython-313.pyc differ diff --git a/fccs_dump_controls.py b/fccs_dump_controls.py new file mode 100644 index 0000000..5448b51 --- /dev/null +++ b/fccs_dump_controls.py @@ -0,0 +1,95 @@ +""" +Utility: Dump control identifiers of on-screen windows. + +Use this to discover the control identifiers of a dialog we don't have mapped +yet — e.g. the drawer-selection box that FCCS shows when you search a base ID +that clashes with a longer one (02218 -> 02218A). + +USAGE +----- +1. In FCCS, get the target screen on-screen (e.g. type the clashing base ID and + click Go so the selection box is showing). +2. From another cmd window (with the venv active), run: + + python fccs_dump_controls.py + + This lists every visible top-level window and writes the full control + identifiers of each to control_dump.txt. + + To narrow it down, filter by title or class: + + python fccs_dump_controls.py --title Select + python fccs_dump_controls.py --class #32770 + +3. Open control_dump.txt, find the selection box, and copy its identifiers + into the references folder (like references/send_to_file_dialog.txt). + +Notes: + - Run from 32-bit Python (same as the rest of the tool). + - Running this from a separate cmd window means the FCCS dialog stays open; + this script does not need focus. +""" + +import argparse +import sys + +from pywinauto import Desktop + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--title", default=None, + help="only dump windows whose title contains this text") + ap.add_argument("--class", dest="cls", default=None, + help="only dump windows with this exact class name") + ap.add_argument("--out", default="control_dump.txt", + help="output file (default: control_dump.txt)") + args = ap.parse_args() + + targets = [] + for w in Desktop(backend="win32").windows(): + try: + if not w.is_visible(): + continue + title = w.window_text() + cls = w.class_name() + except Exception: + continue + if args.title and args.title.lower() not in title.lower(): + continue + if args.cls and args.cls != cls: + continue + targets.append(w) + + print(f"Found {len(targets)} matching visible window(s):") + for w in targets: + print(f" handle={w.handle} class={w.class_name()!r} " + f"title={w.window_text()!r}") + + if not targets: + print("Nothing to dump. Is the target window visible? " + "Try without filters, or adjust --title/--class.") + return + + with open(args.out, "w", encoding="utf-8") as f: + old_stdout = sys.stdout + sys.stdout = f + try: + for w in targets: + print("=" * 72) + print(f"WINDOW handle={w.handle} class={w.class_name()!r} " + f"title={w.window_text()!r}") + print("=" * 72) + try: + w.print_control_identifiers() + except Exception as e: + print(f" (could not dump this window: {e})") + print() + finally: + sys.stdout = old_stdout + + print(f"\nControl identifiers written to: {args.out}") + + +if __name__ == "__main__": + main() diff --git a/fccs_scan.py b/fccs_scan.py index 1299714..988231d 100644 --- a/fccs_scan.py +++ b/fccs_scan.py @@ -19,45 +19,40 @@ _IGNORE_HEADER = [ "#", "# Add password-protected or otherwise-excluded drawers here.", "#", - "# The IDs auto-added below are prefix clashes: searching any of them in", - "# FCCS can pop a selection box that breaks the automation, so ALL drawers", - "# in each clash group are skipped by default. DELETE or comment out any", - "# you actually DO want exported (then handle them manually), keep the rest.", + "# The IDs auto-added below are prefix clashes: searching the base ID in", + "# FCCS pops a selection box that breaks the plain export. Only the base", + "# (shorter) ID is listed — the longer, more-specific IDs export normally.", + "# The base IDs are exported separately by fccs_export_clashes.py, which", + "# handles the selection box. DELETE or comment out any you'd rather skip.", "", ] def update_ignore_file(ignore_file, clashes, log): - """Create ignore.txt if missing and seed it with clash-group IDs. + """Create ignore.txt if missing and seed it with clash base (prefix) IDs. - Every drawer involved in a clash (both the prefix and each longer ID that - matches it) is added, since any of them can trip the FCCS selection box. - Never overwrites existing entries — only appends IDs not already present, - and de-dupes so no ID is written twice. Returns the list of IDs added. + Only the shorter (prefix) ID of each clash is added — that's the one that + triggers the FCCS selection box and needs special handling. The longer, + more-specific IDs (e.g. '02218A') search fine and export normally. + Never overwrites existing entries — only appends prefixes not already + present. Returns the list of IDs added. """ existing = set(load_lines(ignore_file)) file_exists = os.path.exists(ignore_file) - seen = set(existing) # never re-add anything already listed - blocks = [] # (comment, [new_ids]) to append, in clash order - for short, matches in clashes: - group = [short] + list(matches) - new_ids = [g for g in group if g not in seen] - if not new_ids: - continue - seen.update(new_ids) - blocks.append((f"# clash group: {', '.join(group)}", new_ids)) + to_add = [(short, matches) for short, matches in clashes + if short not in existing] # Existing file already covers every clash — leave it untouched. - if file_exists and not blocks: + if file_exists and not to_add: return [] lines = [] if not file_exists: lines.extend(_IGNORE_HEADER) - for comment, new_ids in blocks: - lines.append(comment) - lines.extend(new_ids) + for short, matches in to_add: + lines.append(f"# clashes with: {', '.join(matches)}") + lines.append(short) mode = "a" if file_exists else "w" with open(ignore_file, mode, encoding="utf-8") as f: @@ -65,11 +60,11 @@ def update_ignore_file(ignore_file, clashes, log): f.write("\n") # separate the new block from prior content f.write("\n".join(lines) + "\n") - added = [i for _, ids in blocks for i in ids] + added = [short for short, _ in to_add] if not file_exists: log(f"Created ignore file: {ignore_file}") if added: - log(f"Added {len(added)} clash-group ID(s) to ignore list: " + log(f"Added {len(added)} clash base ID(s) to ignore list: " f"{', '.join(added)}") return added