ctkforge 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: ctkforge
3
+ Version: 0.1.0
4
+ Summary: CustomTkinter extension components and widgets
5
+ Author: Rafalus
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: customtkinter>=6.0.0
9
+ Requires-Dist: pillow>=12.0.0
10
+
11
+ # CTkForge
12
+
13
+ Custom extension components and widgets for [CustomTkinter](https://github.com/TomSchimansky/CustomTkinter).
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install ctkforge
19
+ ```
20
+
21
+ ## Features
22
+
23
+ - **[CTkPopup](docs/CTkPopup.md)**: A modern popup dialog widget for alerts, confirmations, and error messages. Includes ready-to-use types (info, warning, error, question, success), keyboard shortcuts (Enter to confirm, Escape to cancel), clean Windows 11 button styling, and automatically turns into a scrollable box for long error text. See [detailed documentation](docs/CTkPopup.md) or run [example_ctkpopup.py](examples/example_ctkpopup.py).
24
+
25
+ - **[CTkSpinbox](docs/CTkSpinbox.md)**: A number input widget with classic up and down arrow buttons (`▲`/`▼`). Supports mouse wheel scrolling, arrow key adjustments, minimum/maximum limits, custom steps, and matches CustomTkinter's colors and rounded corners. See [detailed documentation](docs/CTkSpinbox.md) or run [example_ctkspinbox.py](examples/example_ctkspinbox.py).
26
+
27
+ - **[CTkTable](docs/table.md)**: A fast, smooth data table widget built to handle massive datasets without slowing down. Features smart column sorting, row and cell selection, double-click cell editing, Excel-compatible copy-pasting (`Ctrl+C`), auto-fitting column widths, and optional alternating row colors. See [API reference](docs/table.md), [user guide](docs/table_user_guide.md), or run [demo_ctktable.py](examples/demo_ctktable.py).
28
+ - **Universal DPI `unscale` Helper**: Utility function to convert raw physical pixel dimensions from Tkinter (`winfo_*`) back to unscaled 100% baseline (96 DPI) values for CustomTkinter windows.
29
+ - **Resource Locator (`resource_path`)**: Robust asset path resolver that finds images, icons, audio, and data files across direct filesystem paths, the script entry directory (`sys.argv[0]`), and PyInstaller single-file bundle extractions (`sys._MEIPASS`).
30
+
31
+ ## Usage
32
+
33
+ ### 1. CTkPopup Dialogs
34
+
35
+ ```python
36
+ import customtkinter as ctk
37
+ from ctkforge import CTkPopup
38
+
39
+ app = ctk.CTk()
40
+
41
+ # Quick confirmation dialog (Enter for Yes, Escape for No)
42
+ popup = CTkPopup(
43
+ master=app,
44
+ title="Confirm Action",
45
+ message="Do you want to proceed with the update?",
46
+ type="question",
47
+ )
48
+
49
+ choice = popup.get()
50
+ if choice is True:
51
+ print("User clicked Yes")
52
+ else:
53
+ print("User clicked No or closed the dialog")
54
+
55
+ app.mainloop()
56
+ ```
57
+
58
+ *(See [example_ctkpopup.py](examples/example_ctkpopup.py) and [demo_ctkpopup.py](examples/demo_ctkpopup.py) for interactive demos).*
59
+
60
+ ### 2. CTkSpinbox
61
+
62
+ ```python
63
+ import customtkinter as ctk
64
+ from ctkforge import CTkSpinbox
65
+
66
+ app = ctk.CTk()
67
+
68
+ spinbox = CTkSpinbox(
69
+ app,
70
+ start_value=25,
71
+ min_value=1,
72
+ max_value=100,
73
+ step=1,
74
+ width=120,
75
+ height=38,
76
+ )
77
+ spinbox.pack(padx=20, pady=20)
78
+
79
+ app.mainloop()
80
+ ```
81
+
82
+ *(See [example_ctkspinbox.py](examples/example_ctkspinbox.py) for the standalone snippet, and [demo_ctkspinbox.py](examples/demo_ctkspinbox.py) for a complete app).*
83
+
84
+ ### 3. CTkTable Data Grid
85
+
86
+ ```python
87
+ import customtkinter as ctk
88
+ from ctkforge import CTkTable
89
+
90
+ app = ctk.CTk()
91
+ app.geometry("800x400")
92
+
93
+ headers = ["ID", "Employee Name", "Department", "Salary"]
94
+ data = [
95
+ ["101", "Ada Lovelace", "Research", "$125,000"],
96
+ ["102", "Alan Turing", "Data Systems", "$135,000"],
97
+ ["103", "Grace Hopper", "Engineering", "$130,000"],
98
+ ]
99
+
100
+ table = CTkTable(
101
+ app,
102
+ headers=headers,
103
+ values=data,
104
+ column_widths=[90, 220, 180, 140],
105
+ column_types={0: "int"},
106
+ selection_target="row",
107
+ selection_mode="extended",
108
+ editable=True,
109
+ alternate_row_color=("gray92", "gray14"),
110
+ )
111
+ table.pack(fill="both", expand=True, padx=20, pady=20)
112
+
113
+ app.mainloop()
114
+ ```
115
+
116
+ *(See [example_ctktable.py](examples/example_ctktable.py) and [demo_ctktable.py](examples/demo_ctktable.py) for interactive demos).*
117
+
118
+ ### 4. Universal DPI Unscaling (`unscale`)
119
+
120
+ Convert raw physical pixel values from Tkinter (`winfo_*`) back to unscaled 100% baseline (96 DPI) values to prevent double-scaling bugs when setting CustomTkinter geometry on High-DPI displays:
121
+
122
+ ```python
123
+ import customtkinter as ctk
124
+ from ctkforge import unscale
125
+
126
+ app = ctk.CTk()
127
+ app.update_idletasks()
128
+
129
+ # Unscale multiple physical dimensions simultaneously:
130
+ width, height = unscale(app, app.winfo_width(), app.winfo_height())
131
+
132
+ # Or unscale a single coordinate:
133
+ root_x = unscale(app, app.winfo_rootx())
134
+
135
+ # Set window geometry safely without High-DPI drift:
136
+ app.geometry(f"{width}x{height}")
137
+ ```
138
+
139
+ ### 5. Resource Path Resolution (`resource_path`)
140
+
141
+ Find files reliably in both development and PyInstaller single-file executables (`.exe`):
142
+
143
+ ```python
144
+ from ctkforge import resource_path
145
+
146
+ # Finds asset whether running from terminal, script folder, or PyInstaller bundle
147
+ icon_path = resource_path("assets/custom_icon.png")
148
+ ```
149
+
@@ -0,0 +1,139 @@
1
+ # CTkForge
2
+
3
+ Custom extension components and widgets for [CustomTkinter](https://github.com/TomSchimansky/CustomTkinter).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install ctkforge
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **[CTkPopup](docs/CTkPopup.md)**: A modern popup dialog widget for alerts, confirmations, and error messages. Includes ready-to-use types (info, warning, error, question, success), keyboard shortcuts (Enter to confirm, Escape to cancel), clean Windows 11 button styling, and automatically turns into a scrollable box for long error text. See [detailed documentation](docs/CTkPopup.md) or run [example_ctkpopup.py](examples/example_ctkpopup.py).
14
+
15
+ - **[CTkSpinbox](docs/CTkSpinbox.md)**: A number input widget with classic up and down arrow buttons (`▲`/`▼`). Supports mouse wheel scrolling, arrow key adjustments, minimum/maximum limits, custom steps, and matches CustomTkinter's colors and rounded corners. See [detailed documentation](docs/CTkSpinbox.md) or run [example_ctkspinbox.py](examples/example_ctkspinbox.py).
16
+
17
+ - **[CTkTable](docs/table.md)**: A fast, smooth data table widget built to handle massive datasets without slowing down. Features smart column sorting, row and cell selection, double-click cell editing, Excel-compatible copy-pasting (`Ctrl+C`), auto-fitting column widths, and optional alternating row colors. See [API reference](docs/table.md), [user guide](docs/table_user_guide.md), or run [demo_ctktable.py](examples/demo_ctktable.py).
18
+ - **Universal DPI `unscale` Helper**: Utility function to convert raw physical pixel dimensions from Tkinter (`winfo_*`) back to unscaled 100% baseline (96 DPI) values for CustomTkinter windows.
19
+ - **Resource Locator (`resource_path`)**: Robust asset path resolver that finds images, icons, audio, and data files across direct filesystem paths, the script entry directory (`sys.argv[0]`), and PyInstaller single-file bundle extractions (`sys._MEIPASS`).
20
+
21
+ ## Usage
22
+
23
+ ### 1. CTkPopup Dialogs
24
+
25
+ ```python
26
+ import customtkinter as ctk
27
+ from ctkforge import CTkPopup
28
+
29
+ app = ctk.CTk()
30
+
31
+ # Quick confirmation dialog (Enter for Yes, Escape for No)
32
+ popup = CTkPopup(
33
+ master=app,
34
+ title="Confirm Action",
35
+ message="Do you want to proceed with the update?",
36
+ type="question",
37
+ )
38
+
39
+ choice = popup.get()
40
+ if choice is True:
41
+ print("User clicked Yes")
42
+ else:
43
+ print("User clicked No or closed the dialog")
44
+
45
+ app.mainloop()
46
+ ```
47
+
48
+ *(See [example_ctkpopup.py](examples/example_ctkpopup.py) and [demo_ctkpopup.py](examples/demo_ctkpopup.py) for interactive demos).*
49
+
50
+ ### 2. CTkSpinbox
51
+
52
+ ```python
53
+ import customtkinter as ctk
54
+ from ctkforge import CTkSpinbox
55
+
56
+ app = ctk.CTk()
57
+
58
+ spinbox = CTkSpinbox(
59
+ app,
60
+ start_value=25,
61
+ min_value=1,
62
+ max_value=100,
63
+ step=1,
64
+ width=120,
65
+ height=38,
66
+ )
67
+ spinbox.pack(padx=20, pady=20)
68
+
69
+ app.mainloop()
70
+ ```
71
+
72
+ *(See [example_ctkspinbox.py](examples/example_ctkspinbox.py) for the standalone snippet, and [demo_ctkspinbox.py](examples/demo_ctkspinbox.py) for a complete app).*
73
+
74
+ ### 3. CTkTable Data Grid
75
+
76
+ ```python
77
+ import customtkinter as ctk
78
+ from ctkforge import CTkTable
79
+
80
+ app = ctk.CTk()
81
+ app.geometry("800x400")
82
+
83
+ headers = ["ID", "Employee Name", "Department", "Salary"]
84
+ data = [
85
+ ["101", "Ada Lovelace", "Research", "$125,000"],
86
+ ["102", "Alan Turing", "Data Systems", "$135,000"],
87
+ ["103", "Grace Hopper", "Engineering", "$130,000"],
88
+ ]
89
+
90
+ table = CTkTable(
91
+ app,
92
+ headers=headers,
93
+ values=data,
94
+ column_widths=[90, 220, 180, 140],
95
+ column_types={0: "int"},
96
+ selection_target="row",
97
+ selection_mode="extended",
98
+ editable=True,
99
+ alternate_row_color=("gray92", "gray14"),
100
+ )
101
+ table.pack(fill="both", expand=True, padx=20, pady=20)
102
+
103
+ app.mainloop()
104
+ ```
105
+
106
+ *(See [example_ctktable.py](examples/example_ctktable.py) and [demo_ctktable.py](examples/demo_ctktable.py) for interactive demos).*
107
+
108
+ ### 4. Universal DPI Unscaling (`unscale`)
109
+
110
+ Convert raw physical pixel values from Tkinter (`winfo_*`) back to unscaled 100% baseline (96 DPI) values to prevent double-scaling bugs when setting CustomTkinter geometry on High-DPI displays:
111
+
112
+ ```python
113
+ import customtkinter as ctk
114
+ from ctkforge import unscale
115
+
116
+ app = ctk.CTk()
117
+ app.update_idletasks()
118
+
119
+ # Unscale multiple physical dimensions simultaneously:
120
+ width, height = unscale(app, app.winfo_width(), app.winfo_height())
121
+
122
+ # Or unscale a single coordinate:
123
+ root_x = unscale(app, app.winfo_rootx())
124
+
125
+ # Set window geometry safely without High-DPI drift:
126
+ app.geometry(f"{width}x{height}")
127
+ ```
128
+
129
+ ### 5. Resource Path Resolution (`resource_path`)
130
+
131
+ Find files reliably in both development and PyInstaller single-file executables (`.exe`):
132
+
133
+ ```python
134
+ from ctkforge import resource_path
135
+
136
+ # Finds asset whether running from terminal, script folder, or PyInstaller bundle
137
+ icon_path = resource_path("assets/custom_icon.png")
138
+ ```
139
+
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ctkforge"
7
+ version = "0.1.0"
8
+ description = "CustomTkinter extension components and widgets"
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Rafalus" },
12
+ ]
13
+ requires-python = ">=3.10"
14
+ dependencies = [
15
+ "customtkinter>=6.0.0",
16
+ "pillow>=12.0.0",
17
+ ]
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["src"]
21
+
22
+ [tool.setuptools.package-data]
23
+ "*" = ["*.png"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,26 @@
1
+ """CustomTkinter extension components and widgets."""
2
+
3
+ from ctkforge.debug import (
4
+ DEBUG_BANNER,
5
+ enable_debug_mode,
6
+ is_debug_mode,
7
+ print_window_telemetry,
8
+ )
9
+ from ctkforge.geometry import scale, unscale
10
+ from ctkforge.popup import CTkPopup
11
+ from ctkforge.resources import resource_path
12
+ from ctkforge.spinbox import CTkSpinbox
13
+ from ctkforge.table import CTkTable
14
+
15
+ __all__ = [
16
+ "DEBUG_BANNER",
17
+ "CTkPopup",
18
+ "CTkSpinbox",
19
+ "CTkTable",
20
+ "enable_debug_mode",
21
+ "is_debug_mode",
22
+ "print_window_telemetry",
23
+ "resource_path",
24
+ "scale",
25
+ "unscale",
26
+ ]
@@ -0,0 +1,95 @@
1
+ """Runtime telemetry and debug tools for CustomTkinter windows."""
2
+
3
+ from typing import Any
4
+
5
+ from ctkforge.geometry import unscale
6
+
7
+ DEBUG_BANNER: str = " === DEBUG MODE IS ACTIVE ==="
8
+ _DEBUG_ACTIVE: bool = False
9
+
10
+ __all__ = [
11
+ "DEBUG_BANNER",
12
+ "enable_debug_mode",
13
+ "is_debug_mode",
14
+ "print_window_telemetry",
15
+ ]
16
+
17
+
18
+ def is_debug_mode() -> bool:
19
+ """Return whether debug mode is currently active across the application."""
20
+ return _DEBUG_ACTIVE
21
+
22
+
23
+ def print_window_telemetry(window: Any) -> None:
24
+ """Print size, DPI scaling, and baseline dimensions for the active window.
25
+
26
+ Args:
27
+ window: The active Tk or CustomTkinter window to inspect.
28
+ """
29
+ if not hasattr(window, "winfo_exists") or not window.winfo_exists():
30
+ return
31
+
32
+ window.update_idletasks()
33
+ width = window.winfo_width()
34
+ height = window.winfo_height()
35
+ base_w, base_h = unscale(window, width, height)
36
+ base_req_w, base_req_h = unscale(
37
+ window, window.winfo_reqwidth(), window.winfo_reqheight()
38
+ )
39
+ scaling = (
40
+ window._get_window_scaling()
41
+ if hasattr(window, "_get_window_scaling")
42
+ else getattr(window, "_get_widget_scaling", lambda: 1.0)()
43
+ )
44
+
45
+ # Resolve window title
46
+ title_text = (
47
+ window.title()
48
+ if callable(getattr(window, "title", None))
49
+ else getattr(window, "_title_text", "Untitled")
50
+ )
51
+
52
+ class_name = window.__class__.__name__
53
+
54
+ print(f"\n--- {class_name} Telemetry ---")
55
+ print(f"Title: {title_text}")
56
+ print(
57
+ f"Physical Size: {width}x{height} px @ {scaling:g} ({int(scaling * 100)}%) Scaling"
58
+ )
59
+ print(f"Baseline Size: {base_w}x{base_h} px (96 DPI)")
60
+ if hasattr(window, "_target_width"):
61
+ print(f"Target Width: {window._target_width} px")
62
+ print(
63
+ f"Req Size: {base_req_w}x{base_req_h} px (base) / {window.winfo_reqwidth()}x{window.winfo_reqheight()} px (phys)"
64
+ )
65
+ print("-" * (len(class_name) + 18) + "\n")
66
+
67
+
68
+ def enable_debug_mode(app: Any, banner: str = DEBUG_BANNER) -> None:
69
+ """Activate debug mode across the application, popups, and future windows.
70
+
71
+ This function:
72
+ 1. Sets the global debug state to active.
73
+ 2. Appends the debug banner to the root application window title.
74
+ 3. Binds <F12> globally so pressing F12 in any focused window prints its telemetry.
75
+
76
+ Args:
77
+ app: The root CTk or CTkToplevel application instance.
78
+ banner: Text appended to the window title to show debug is active.
79
+ """
80
+ global _DEBUG_ACTIVE
81
+ _DEBUG_ACTIVE = True
82
+
83
+ # Append banner to app title if not already present
84
+ if hasattr(app, "title") and callable(app.title):
85
+ current_title = app.title()
86
+ if banner not in current_title:
87
+ app.title(f"{current_title}{banner}")
88
+
89
+ def _handle_f12(event: Any) -> None:
90
+ target_window = event.widget.winfo_toplevel()
91
+ print_window_telemetry(target_window)
92
+
93
+ # Bind globally so whichever window has focus receives the F12 telemetry dump
94
+ if hasattr(app, "bind_all") and callable(app.bind_all):
95
+ app.bind_all("<F12>", _handle_f12)
@@ -0,0 +1,96 @@
1
+ """DPI-aware geometry, scaling, and coordinate conversion helpers for CustomTkinter."""
2
+
3
+ from typing import Any, overload
4
+
5
+ __all__ = ["scale", "unscale"]
6
+
7
+
8
+ def _get_scaling(target: Any) -> float:
9
+ """Safely extract active DPI scaling factor from a CTk widget or window.
10
+
11
+ CustomTkinter defines `_get_window_scaling()` on CTkBaseClass, so `hasattr`
12
+ returns True for all widgets. However, the underlying private `__window_scaling`
13
+ attribute only exists on root `CTk` windows. Calling it on child widgets raises
14
+ AttributeError. We inspect `_get_widget_scaling` first and safely catch
15
+ AttributeError if `_get_window_scaling` is called on a child widget.
16
+ """
17
+ for method_name in ("_get_widget_scaling", "_get_window_scaling"):
18
+ method = getattr(target, method_name, None)
19
+ if callable(method):
20
+ try:
21
+ val = method()
22
+ if isinstance(val, (int, float)) and val > 0:
23
+ return float(val)
24
+ except AttributeError:
25
+ continue
26
+ return 1.0
27
+
28
+
29
+ @overload
30
+ def unscale(target: Any, value: float, /) -> int: ...
31
+
32
+
33
+ @overload
34
+ def unscale(
35
+ target: Any,
36
+ value1: float,
37
+ value2: float,
38
+ /,
39
+ *values: float,
40
+ ) -> tuple[int, ...]: ...
41
+
42
+
43
+ def unscale(target: Any, *values: float) -> int | tuple[int, ...]:
44
+ """Convert physical (device hardware) pixel values to logical (96 DPI baseline) integers.
45
+
46
+ Args:
47
+ target: A CTk widget or window providing the active DPI scaling factor.
48
+ *values: One or more physical pixel values to convert to logical units.
49
+
50
+ Returns:
51
+ A single logical integer if one value was provided, or a tuple of logical integers if multiple.
52
+
53
+ Raises:
54
+ ValueError: If no values are passed.
55
+ """
56
+ if not values:
57
+ raise ValueError("unscale() requires at least one value to convert.")
58
+
59
+ scaling = _get_scaling(target)
60
+ result = tuple(round(v / scaling) if scaling else round(v) for v in values)
61
+ return result[0] if len(result) == 1 else result
62
+
63
+
64
+ @overload
65
+ def scale(target: Any, value: float, /) -> int: ...
66
+
67
+
68
+ @overload
69
+ def scale(
70
+ target: Any,
71
+ value1: float,
72
+ value2: float,
73
+ /,
74
+ *values: float,
75
+ ) -> tuple[int, ...]: ...
76
+
77
+
78
+ def scale(target: Any, *values: float) -> int | tuple[int, ...]:
79
+ """Convert logical (96 DPI baseline) pixel values to physical (device hardware) integers.
80
+
81
+ Args:
82
+ target: A CTk widget or window providing the active DPI scaling factor.
83
+ *values: One or more logical pixel values to convert to physical device units.
84
+
85
+ Returns:
86
+ A single physical integer if one value was provided, or a tuple of physical integers if multiple.
87
+
88
+ Raises:
89
+ ValueError: If no values are passed.
90
+ """
91
+ if not values:
92
+ raise ValueError("scale() requires at least one value to convert.")
93
+
94
+ scaling = _get_scaling(target)
95
+ result = tuple(round(v * scaling) if scaling else round(v) for v in values)
96
+ return result[0] if len(result) == 1 else result
@@ -0,0 +1,20 @@
1
+ """CTkPopup - A robust, modern, and DPI-aware dialog replacement for CTkMessagebox.
2
+
3
+ Features:
4
+ - 4 semantic button roles (primary, cancel, destructive, normal) with dedicated resting and hover colors
5
+ - Decoupled semantic return values (booleans, tokens)
6
+ - Hidden-staging window lifecycle preventing geometry jumps
7
+ - The Courier Rule: safe return values without widget destruction errors
8
+ - Automatic switching to copyable CTkTextbox for long logs and exceptions
9
+ - Option B Smart Fail-Safe icon resolution
10
+ """
11
+
12
+ from .constants import PRESETS, ROLE_COLORS
13
+ from .popup import CTkPopup
14
+
15
+ __version__ = "1.0.0"
16
+ __all__ = [
17
+ "PRESETS",
18
+ "ROLE_COLORS",
19
+ "CTkPopup",
20
+ ]