diff --git a/profiles.json b/profiles.json index cc7d2ea..ade7ee0 100644 --- a/profiles.json +++ b/profiles.json @@ -143,8 +143,8 @@ { "name": "DSP", "paths": [ - "C:\\__temp\\Metrics\\attuale\\REP\\Projects\\DSP", - "C:\\__temp\\Metrics\\attuale\\REP\\Projects\\DspAlgorithms" + "C:\\src\\____GitProjects\\SXXXXXXX_PyUcc\\__UCC\\Metrics\\_25_11\\REP\\Projects\\DSP", + "C:\\src\\____GitProjects\\SXXXXXXX_PyUcc\\__UCC\\Metrics\\_25_11\\REP\\Projects\\DspAlgorithms" ], "languages": [ "C", diff --git a/pyucc/gui/profile_manager.py b/pyucc/gui/profile_manager.py index cdbc4d9..c16478b 100644 --- a/pyucc/gui/profile_manager.py +++ b/pyucc/gui/profile_manager.py @@ -62,6 +62,8 @@ class ProfileManager(tk.Toplevel): self.title("Profile Manager") self.geometry("1100x700") self.on_change = on_change + # currently loaded profile dict (or None for new) + self._loaded_profile = None self.profiles = profiles_cfg.load_profiles() @@ -97,7 +99,7 @@ class ProfileManager(tk.Toplevel): self.ext_frame.grid(row=0, column=1, sticky="nsew") # Paths listbox + scrollbar inside its frame - self.paths_listbox = tk.Listbox(self.paths_frame, height=12) + self.paths_listbox = tk.Listbox(self.paths_frame, height=12, exportselection=False) self.paths_listbox.grid(row=0, column=0, sticky="nsew") paths_vsb = ttk.Scrollbar( self.paths_frame, orient="vertical", command=self.paths_listbox.yview @@ -141,7 +143,7 @@ class ProfileManager(tk.Toplevel): ).grid(row=1, column=1, padx=6, pady=4) # Extensions listbox + scrollbar inside its frame - self.ext_listbox = tk.Listbox(self.ext_frame, selectmode="extended", height=12) + self.ext_listbox = tk.Listbox(self.ext_frame, selectmode="extended", height=12, exportselection=False) self.ext_listbox.grid(row=0, column=0, sticky="nsew") ext_vsb = ttk.Scrollbar( self.ext_frame, orient="vertical", command=self.ext_listbox.yview @@ -215,10 +217,16 @@ class ProfileManager(tk.Toplevel): ttk.Button(btn_frame, text="Delete", command=self._delete).grid( row=0, column=3, padx=4 ) - ttk.Button(btn_frame, text="Close", command=self.destroy).grid( + ttk.Button(btn_frame, text="Close", command=self._on_close).grid( row=0, column=4, padx=4 ) + # handle window close (X) the same way as Close button + try: + self.protocol("WM_DELETE_WINDOW", self._on_close) + except Exception: + pass + right.columnconfigure(1, weight=1) # If no profiles exist, prefill form with sensible defaults (widgets are ready) @@ -247,36 +255,25 @@ class ProfileManager(tk.Toplevel): if f: self._insert_path_display("end", str(Path(f))) - def _populate_ext_listbox(self): - # Populate extension listbox with LANGUAGE_EXTENSIONS entries - try: - from ..config.languages import LANGUAGE_EXTENSIONS - except Exception: - return - self.ext_listbox.delete(0, "end") - keys = sorted(LANGUAGE_EXTENSIONS.keys(), key=lambda s: s.lower()) - for k in keys: - exts = LANGUAGE_EXTENSIONS.get(k) or [] - exts_label = f" ({', '.join(exts)})" if exts else "" - self.ext_listbox.insert("end", f"{k}{exts_label}") + def _insert_path_display(self, index, path_str: str): + # choose a prefix depending on whether path is dir/file/unknown + p = Path(path_str) + if p.exists(): + prefix = "[D] " if p.is_dir() else "[F] " + else: + prefix = "[?] " + display = f"{prefix}{path_str}" + self.paths_listbox.insert(index, display) - def _clear_ext_selection(self): - self.ext_listbox.selection_clear(0, "end") - - def _edit_selected_path(self): - sel = self.paths_listbox.curselection() - if not sel: - return - idx = sel[0] - current = self.paths_listbox.get(idx) - # strip prefix for editing - stripped = self._strip_display_prefix(current) - new = simpledialog.askstring( - "Edit Path", "Path:", initialvalue=stripped, parent=self - ) - if new: - self.paths_listbox.delete(idx) - self._insert_path_display(idx, new) + def _strip_display_prefix(self, display_text: str) -> str: + # remove known prefixes like [D] , [F] , [?] if present + if ( + display_text.startswith("[D] ") + or display_text.startswith("[F] ") + or display_text.startswith("[?] ") + ): + return display_text[4:] + return display_text def _remove_selected_path(self): sel = list(self.paths_listbox.curselection()) @@ -319,25 +316,107 @@ class ProfileManager(tk.Toplevel): for i in [min(size - 1, x + 1) for x in sel]: self.paths_listbox.selection_set(i) - def _insert_path_display(self, index, path_str: str): - # choose a prefix depending on whether path is dir/file/unknown - p = Path(path_str) - if p.exists(): - prefix = "[D] " if p.is_dir() else "[F] " - else: - prefix = "[?] " - display = f"{prefix}{path_str}" - self.paths_listbox.insert(index, display) + def _populate_ext_listbox(self): + # Populate extension listbox with LANGUAGE_EXTENSIONS entries + try: + from ..config.languages import LANGUAGE_EXTENSIONS + except Exception: + return + self.ext_listbox.delete(0, "end") + keys = sorted(LANGUAGE_EXTENSIONS.keys(), key=lambda s: s.lower()) + for k in keys: + exts = LANGUAGE_EXTENSIONS.get(k) or [] + exts_label = f" ({', '.join(exts)})" if exts else "" + self.ext_listbox.insert("end", f"{k}{exts_label}") - def _strip_display_prefix(self, display_text: str) -> str: - # remove known prefixes like [D] , [F] , [?] if present - if ( - display_text.startswith("[D] ") - or display_text.startswith("[F] ") - or display_text.startswith("[?] ") - ): - return display_text[4:] - return display_text + def _clear_ext_selection(self): + self.ext_listbox.selection_clear(0, "end") + + def _edit_selected_path(self): + sel = self.paths_listbox.curselection() + if not sel: + return + idx = sel[0] + current = self.paths_listbox.get(idx) + # strip prefix for editing + stripped = self._strip_display_prefix(current) + new = self._ask_edit_path(stripped) + if new: + self.paths_listbox.delete(idx) + self._insert_path_display(idx, new) + + def _ask_edit_path(self, initial: str) -> str: + """Open a small modal dialog with a larger entry widget for editing paths. + + Returns the edited string, or None if cancelled. + """ + dlg = tk.Toplevel(self) + dlg.title("Edit Path") + dlg.transient(self) + dlg.grab_set() + + frm = ttk.Frame(dlg) + frm.grid(row=0, column=0, padx=8, pady=8, sticky="nsew") + ttk.Label(frm, text="Path:").grid(row=0, column=0, columnspan=2, sticky="w") + val = tk.StringVar(value=initial) + entry = ttk.Entry(frm, textvariable=val, width=90) + entry.grid(row=1, column=0, sticky="ew", pady=(6, 0)) + + def _on_browse(): + d = filedialog.askdirectory(parent=dlg) + if d: + val.set(str(Path(d))) + entry.focus_set() + entry.selection_range(0, 'end') + + browse_btn = ttk.Button(frm, text="Browse...", command=_on_browse) + browse_btn.grid(row=1, column=1, padx=(6, 0), pady=(6, 0)) + + btns = ttk.Frame(frm) + btns.grid(row=2, column=0, columnspan=2, pady=(10, 0), sticky="e") + + ok_pressed = {"ok": False} + + def _on_ok(): + ok_pressed["ok"] = True + try: + dlg.destroy() + except Exception: + pass + + def _on_cancel(): + try: + dlg.destroy() + except Exception: + pass + + ttk.Button(btns, text="OK", command=_on_ok).grid(row=0, column=0, padx=6) + ttk.Button(btns, text="Cancel", command=_on_cancel).grid(row=0, column=1) + + frm.columnconfigure(0, weight=1) + # center dialog above parent (ProfileManager) + try: + dlg.update_idletasks() + pwx = self.winfo_rootx() + pwy = self.winfo_rooty() + pww = self.winfo_width() + pwh = self.winfo_height() + dw = dlg.winfo_width() + dh = dlg.winfo_height() + cx = pwx + max(0, (pww - dw) // 2) + cy = pwy + max(0, (pwh - dh) // 2) + dlg.geometry(f"+{cx}+{cy}") + except Exception: + pass + + entry.focus_set() + entry.selection_range(0, 'end') + self.wait_window(dlg) + + if ok_pressed["ok"]: + return val.get().strip() + return None + def _get_paths_from_listbox(self) -> List[str]: out = [] @@ -356,6 +435,8 @@ class ProfileManager(tk.Toplevel): def _load_profile(self, pr): self.name_var.set(pr.get("name", "")) + # remember loaded profile so we can detect unsaved changes on close + self._loaded_profile = dict(pr) # Load only new-style 'paths' list (no legacy compatibility) self.paths_listbox.delete(0, "end") paths = pr.get("paths") or [] @@ -396,6 +477,8 @@ class ProfileManager(tk.Toplevel): self.ignore_text.delete("1.0", "end") # insert the DEFAULT_IGNORES joined by commas self.ignore_text.insert("1.0", ",".join(self.DEFAULT_IGNORES)) + # new form — no loaded profile + self._loaded_profile = None def _apply_default_ignores(self): """Merge current ignore patterns with the DEFAULT_IGNORES and @@ -472,6 +555,11 @@ class ProfileManager(tk.Toplevel): messagebox.showinfo("Saved", f"Profile '{name}' saved.") if self.on_change: self.on_change() + # mark this as the currently loaded profile + try: + self._loaded_profile = dict(profile) + except Exception: + self._loaded_profile = None def _delete(self): name = self.name_var.get().strip() @@ -487,3 +575,71 @@ class ProfileManager(tk.Toplevel): self._new() if self.on_change: self.on_change() + self._loaded_profile = None + + def _on_close(self): + """Handle window close: if form differs from loaded profile, ask to save.""" + # build current form representation + try: + cur_name = self.name_var.get().strip() + cur_paths = self._get_paths_from_listbox() + cur_langs = [] + try: + sel = [self.ext_listbox.get(i) for i in self.ext_listbox.curselection()] + for s in sel: + lang_name = s.split(" (", 1)[0] + if lang_name and lang_name not in cur_langs: + cur_langs.append(lang_name) + except Exception: + pass + raw_custom = self.custom_text.get("1.0", "end").strip() + raw_custom = raw_custom.replace("\n", ",") + custom = [c.strip() for c in raw_custom.split(",") if c.strip()] + cur_langs.extend([c for c in custom if c]) + raw_ignore = self.ignore_text.get("1.0", "end").strip() + raw_ignore = raw_ignore.replace("\n", ",") + cur_ignore = [s.strip() for s in raw_ignore.split(",") if s.strip()] + cur_profile = { + "name": cur_name, + "paths": cur_paths, + "languages": cur_langs, + "ignore": cur_ignore, + } + except Exception: + cur_profile = None + + changed = False + if self._loaded_profile is None and any([v for v in (cur_name, cur_paths, cur_langs, cur_ignore) if v]): + changed = True + elif self._loaded_profile is not None and cur_profile is not None: + # compare simple dicts + # note: do not attempt deep-normalization beyond this + if ( + self._loaded_profile.get("name") != cur_profile.get("name") + or self._loaded_profile.get("paths") != cur_profile.get("paths") + or self._loaded_profile.get("languages") != cur_profile.get("languages") + or self._loaded_profile.get("ignore") != cur_profile.get("ignore") + ): + changed = True + + if changed: + ans = messagebox.askyesnocancel( + "Unsaved Changes", "Save changes to profile before closing?" + ) + # True => save, False => discard and close, None => cancel + if ans is None: + return + if ans: + try: + self._save() + except Exception: + # if save fails, don't close + return + + try: + self.destroy() + except Exception: + try: + super().destroy() + except Exception: + pass diff --git a/pyucc/gui/topbar.py b/pyucc/gui/topbar.py index 208f81b..71f342e 100644 --- a/pyucc/gui/topbar.py +++ b/pyucc/gui/topbar.py @@ -101,7 +101,26 @@ class TopBar(ttk.Frame): def _open_manager(self): def _refresh(): + # remember current selection, reload profiles and restore selection + cur = self.profile_var.get() self._load_profiles() + vals = list(self.profile_cb["values"]) if self.profile_cb["values"] else [] + if cur and cur in vals: + # keep same selection and refresh derived UI + self.profile_var.set(cur) + self._on_profile_selected() + elif vals: + # if there is at least one profile, ensure UI reflects a valid selection + # keep existing selection if already set, otherwise pick first + if not self.profile_var.get(): + self.profile_var.set(vals[0]) + self._on_profile_selected() + else: + # no profiles available + self.profile_var.set("") + self.current_profile = None + self.path_var.set("") + self.project_type_var.set("-") pm = ProfileManager(self.master, on_change=_refresh) pm.grab_set() diff --git a/pyucc/utils/report_generator.py b/pyucc/utils/report_generator.py index eec2feb..96111dc 100644 --- a/pyucc/utils/report_generator.py +++ b/pyucc/utils/report_generator.py @@ -316,8 +316,49 @@ def generate_differ_report(result, profile_config, baseline_id, output_path): m = counts.get("modified", 0) u = counts.get("unmodified", 0) - cd = pair.get("countings_delta") or {} - md = pair.get("metrics_delta") or {} + # Prefer explicit deltas computed by the differ, but if missing + # (e.g. added or deleted files where only one side exists) fall + # back to using the current or baseline values so the compact + # table shows meaningful numbers for new/deleted files. + cd = pair.get("countings_delta") + if cd is None: + cur_cnt = pair.get("current_countings") or {} + base_cnt = pair.get("baseline_countings") or {} + if cur_cnt and not base_cnt: + cd = { + "physical_lines": cur_cnt.get("physical_lines", 0), + "code_lines": cur_cnt.get("code_lines", 0), + "comment_lines": cur_cnt.get("comment_lines", 0), + "blank_lines": cur_cnt.get("blank_lines", 0), + } + elif base_cnt and not cur_cnt: + cd = { + "physical_lines": -base_cnt.get("physical_lines", 0), + "code_lines": -base_cnt.get("code_lines", 0), + "comment_lines": -base_cnt.get("comment_lines", 0), + "blank_lines": -base_cnt.get("blank_lines", 0), + } + else: + cd = {} + + md = pair.get("metrics_delta") + if md is None: + cur_m = pair.get("current_metrics") or {} + base_m = pair.get("baseline_metrics") or {} + if cur_m and not base_m: + md = { + "func_count": cur_m.get("func_count", 0), + "avg_cc": cur_m.get("avg_cc", 0.0), + "mi": cur_m.get("mi", 0.0), + } + elif base_m and not cur_m: + md = { + "func_count": -base_m.get("func_count", 0), + "avg_cc": -base_m.get("avg_cc", 0.0), + "mi": -base_m.get("mi", 0.0), + } + else: + md = {} def fmt_int(v): try: diff --git a/todo.md b/todo.md index fb9197c..c6d7e97 100644 --- a/todo.md +++ b/todo.md @@ -28,4 +28,8 @@ - [x] colorare la tabella - [x] salvare il file delle diff in automatico - [x] mettere le hint per spiegare i vari parametri cosa sono -- [x] ordinare le righe selezionando la colonna sia in ordine screscente che descrescente \ No newline at end of file +- [x] ordinare le righe selezionando la colonna sia in ordine screscente che descrescente +- [ ] aggiornare profilo percorso quando premo su salva +- [x] non perdere la selezione sulle estensioni quando modifico il profilo +- [ ] verificare con dsp 10 e 11 le differenze con quelle di UCC intorno ai 70000 linee di codice 708 linee di codice modificato +- [ ] verificare se i commenti vengo conteggiati oppure no, nei file c++, c, h ecc