Skip to content

Commit bdfc130

Browse files
feat: add Date and Time widgets (#501)
1 parent 70ddd9a commit bdfc130

22 files changed

Lines changed: 3667 additions & 15 deletions

.github/copilot-instructions.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# pyTermTk Copilot Instructions
2+
3+
## Project Overview
4+
5+
pyTermTk is a Text-based User Interface (TUI) library for Python inspired by Qt5, GTK, and tkinter APIs. It creates cross-platform terminal applications with rich widgets, layouts, and an event-driven architecture.
6+
7+
## Architecture
8+
9+
### Core Structure
10+
- **`libs/pyTermTk/TermTk/`** - Main library code organized into logical modules:
11+
- `TTkCore/` - Core functionality (signals, colors, canvas, configuration)
12+
- `TTkWidgets/` - All UI widgets inherit from `TTkWidget` base class
13+
- `TTkLayouts/` - Layout managers (GridLayout, HBoxLayout, VBoxLayout)
14+
- `TTkGui/` - GUI components (drag & drop, application management)
15+
- `TTkTemplates/` - Mixin classes for event handling (`TKeyEvents`, `TMouseEvents`, `TDragEvents`)
16+
17+
### Widget Inheritance Pattern
18+
All widgets follow this pattern:
19+
```python
20+
class TTkMyWidget(TTkWidget): # Or TTkContainer for composite widgets
21+
# Class-level styling
22+
classStyle = {
23+
'default': {'color': TTkColor.fg("#dddd88"), 'borderColor': TTkColor.RST},
24+
'hover': {'color': TTkColor.fg("#ffffff"), 'borderColor': TTkColor.BOLD},
25+
'focus': {'borderColor': TTkColor.fg("#ffff00")},
26+
'disabled': {'color': TTkColor.fg('#888888')}
27+
}
28+
29+
# Signal declarations
30+
mySignal: pyTTkSignal
31+
32+
__slots__ = ('_private_vars',) # Always use slots for performance
33+
34+
def __init__(self, **kwargs):
35+
self.mySignal = pyTTkSignal(int) # Define signals in __init__
36+
super().__init__(**kwargs|{'size': (w, h)}) # Merge kwargs, set default size
37+
```
38+
39+
### Signal-Slot System (Qt-inspired)
40+
Use type-safe signal-slot patterns:
41+
```python
42+
# Define signals with types
43+
signal = pyTTkSignal(int)
44+
# Define slots with decorators
45+
@pyTTkSlot(int)
46+
def my_slot(value: int):
47+
pass
48+
# Connect them
49+
signal.connect(my_slot)
50+
```
51+
52+
### Event Handling
53+
Widgets handle events by overriding template methods:
54+
- `keyEvent()`, `mousePressEvent()`, `paintEvent()` - Core events
55+
- `focusInEvent()`, `focusOutEvent()` - Focus management
56+
- `dropEvent()`, `dragEnterEvent()` - Drag & drop
57+
- Always return `True` if event is handled, `False` to propagate
58+
59+
## Development Workflows
60+
61+
### Testing
62+
- **Unit tests**: `pytest tests/pytest/` (run via Makefile: `make test`)
63+
- **Performance tests**: `tests/timeit/` - Contains signal/slot benchmarks and optimization tests
64+
- **Manual tests**: `tests/t.*/` - Interactive UI tests
65+
- **CI**: Tests run on Python 3.9-3.14 with flake8 linting
66+
67+
### Build & Deploy
68+
- **Local build**: `pip install -e libs/pyTermTk` (uses `pip`)
69+
- **Documentation**: `make doc` (Sphinx-based, outputs to `docs/source/_build/html/`)
70+
- **Apps deployment**: Individual apps in `apps/` have their own `pyproject.toml`
71+
72+
### Running Examples
73+
- **Demo**: `python demo/demo.py -f`
74+
- **Designer**: `pip install -e apps/ttkDesigner ; ttkDesigner`
75+
- **Individual tests**: `python tests/t.ui/test.ui.036.datetime.01.py`
76+
77+
## Project-Specific Patterns
78+
79+
### Widget State Management
80+
Many widgets use internal state classes (see `datetime_date_form.py`):
81+
```python
82+
class _TTkWidgetState:
83+
__slots__ = ('_data', 'signal_name')
84+
def __init__(self):
85+
self.signal_name = pyTTkSignal()
86+
```
87+
88+
### File Organization
89+
- One widget per file in `TTkWidgets/`
90+
- Use `__all__ = ['ClassName']` exports
91+
- Import from `TermTk.TTkCore`, `TermTk.TTkWidgets` etc (not relative imports)
92+
- Apps in `apps/` are self-contained with `pyproject.toml`
93+
94+
### Color & Theming
95+
Use `TTkColor` constants and theme system:
96+
```python
97+
TTkColor.fg("#ffffff") + TTkColor.bg("#000044") + TTkColor.BOLD
98+
style = self.currentStyle() # Get theme-aware colors
99+
```
100+
101+
### Cross-Platform Considerations
102+
- Platform-specific code in `TTkCore/drivers/`
103+
- Terminal compatibility testing in `tests/ansi.images.json`
104+
- HTML5 export capabilities via `tools/webExporter/`
105+
106+
### Documentation & Docstrings
107+
Use **Sphinx-compatible docstring format** with Epytext-style field lists:
108+
```python
109+
def setGeometry(self, x: int, y: int, width: int, height: int):
110+
''' Resize and move the widget
111+
112+
:param x: the horizontal position
113+
:type x: int
114+
:param y: the vertical position
115+
:type y: int
116+
:param width: the new width
117+
:type width: int
118+
:param height: the new height
119+
:type height: int
120+
'''
121+
122+
# For class/module docstrings include ASCII art examples:
123+
class TTkButton(TTkWidget):
124+
''' TTkButton:
125+
126+
Border = True
127+
::
128+
129+
┌────────┐
130+
│ Text │
131+
╘════════╛
132+
133+
Demo: `formwidgets.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/demo/showcase/formwidgets.py>`_
134+
'''
135+
136+
# For signals, document parameters:
137+
toggled:pyTTkSignal
138+
'''
139+
This signal is emitted whenever the button state changes if checkeable,
140+
i.e., whenever the user checks or unchecks it.
141+
142+
:param checked: True if checked otherwise False
143+
:type checked: bool
144+
'''
145+
```
146+
147+
**Key conventions**:
148+
- Use single quotes `'''` for docstrings
149+
- Include ASCII art for visual widgets showing borders/layout
150+
- Link to demo files with full GitHub URLs
151+
- Use `:py:class:` for cross-references to other classes
152+
- Document all parameters with `:param name:` and `:type name:`
153+
- Include `:return:` and `:rtype:` for non-void methods
154+
- Signal docstrings document emitted parameters, not the signal itself
155+
156+
## Apps Ecosystem
157+
The project includes several full applications demonstrating patterns:
158+
- **ttkDesigner** - Visual UI designer (like Qt Designer)
159+
- **ttkode** - Code editor with syntax highlighting
160+
- **dumbPaintTool** - ASCII art editor
161+
- **tlogg** - Log file viewer
162+
163+
### Testing App Integration
164+
Apps use the main library via path manipulation:
165+
```python
166+
sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk'))
167+
import TermTk as ttk
168+
```
169+
170+
## Key Integration Points
171+
172+
### Layout System
173+
Use Qt-like layout managers:
174+
```python
175+
layout = TTkGridLayout()
176+
layout.addWidget(widget, row, col, rowspan, colspan)
177+
container.setLayout(layout)
178+
```
179+
180+
### Focus & Input Handling
181+
Set focus policy and handle keyboard navigation:
182+
```python
183+
self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus)
184+
```
185+
186+
When implementing new widgets, study existing patterns in `TTkWidgets/` and ensure signal-slot integration follows the established type-safe patterns shown in `tests/pytest/test_004_signals_slots.py`.

demo/demo.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from showcase.dndtabs import demoDnDTabs
5555
from showcase.sigmask import demoSigmask
5656
from showcase.apptemplate import demoAppTemplate
57+
from showcase.datetime import demoDateTimePicker
5758

5859
def stupidPythonHighlighter(txt):
5960
def _colorize(regex, txt, color):
@@ -181,9 +182,10 @@ def demoShowcase(root=None, border=True):
181182

182183
listMenu.addItem(f"Pickers")
183184
tabPickers = ttk.TTkTabWidget(parent=mainFrame, border=False, visible=False)
184-
tabPickers.addTab(demoFilePicker(), " File Picker ", 'showcase/filepicker.py')
185-
tabPickers.addTab(demoColorPicker(), " Color Picker ", 'showcase/colorpicker.py')
186-
tabPickers.addTab(demoTextPicker(), " Text Picker ", 'showcase/textpicker.py')
185+
tabPickers.addTab(demoFilePicker(), " File Picker ", 'showcase/filepicker.py')
186+
tabPickers.addTab(demoColorPicker(), " Color Picker ", 'showcase/colorpicker.py')
187+
tabPickers.addTab(demoTextPicker(), " Text Picker ", 'showcase/textpicker.py')
188+
tabPickers.addTab(demoDateTimePicker(), " DateTime Picker ", 'showcase/datetime.py')
187189
tabPickers.addMenu("sources", ttk.TTkK.RIGHT, tabPickers).menuButtonClicked.connect(lambda _menuButton : showSource(_menuButton.data().currentData()))
188190

189191
listMenu.addItem(f"Graphs")

demo/showcase/datetime.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env python3
2+
3+
# MIT License
4+
#
5+
# Copyright (c) 2025 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the "Software"), to deal
9+
# in the Software without restriction, including without limitation the rights
10+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
# copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in all
15+
# copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
# SOFTWARE.
24+
25+
import sys, os, argparse
26+
import datetime
27+
28+
sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk'))
29+
import TermTk as ttk
30+
31+
32+
def demoDateTimePicker(root=None):
33+
frame = ttk.TTkFrame(parent=root, border=False)
34+
35+
frameDateForm = ttk.TTkFrame(parent=frame, pos=(0,0), size=(24,10), title='Date Form', titleAlign=ttk.TTkK.Alignment.RIGHT_ALIGN, border=True)
36+
dateFormWidget = ttk.TTkDateForm(parent=frameDateForm, pos=(1,0))
37+
38+
frameDateTime = ttk.TTkFrame(parent=frame, pos=(24,0), size=(26,3), title='DateTime widget', titleAlign=ttk.TTkK.Alignment.LEFT_ALIGN, border=True)
39+
datetimeWidget = ttk.TTkDateTime(parent=frameDateTime, pos=(1,0))
40+
41+
frameDate = ttk.TTkFrame(parent=frame, pos=(24,3), size=(17,3), title='Date', titleAlign=ttk.TTkK.Alignment.LEFT_ALIGN, border=True)
42+
dateWidget = ttk.TTkDate(parent=frameDate, pos=(1,0))
43+
44+
frameTime = ttk.TTkFrame(parent=frame, pos=(24,6), size=(12,3), title='Time', titleAlign=ttk.TTkK.Alignment.LEFT_ALIGN, border=True)
45+
timeWidget = ttk.TTkTime(parent=frameTime, pos=(1,0))
46+
47+
ttk.pyTTkSlot(datetime.datetime)
48+
def _changedDatetime(dt:datetime.datetime):
49+
dateWidget.setDate(dt.date())
50+
timeWidget.setTime(dt.time())
51+
dateFormWidget.setDate(dt.date())
52+
53+
ttk.pyTTkSlot(datetime.time)
54+
def _changedTime(time:datetime.time):
55+
dt = datetimeWidget.datetime()
56+
new_dt = datetime.datetime.combine(dt.date(),time)
57+
datetimeWidget.setDatetime(new_dt)
58+
59+
ttk.pyTTkSlot(datetime.date)
60+
def _changedDate(date:datetime.date):
61+
dt = datetimeWidget.datetime()
62+
new_dt = datetime.datetime.combine(date,dt.time())
63+
datetimeWidget.setDatetime(new_dt)
64+
dateFormWidget.setDate(date)
65+
dateWidget.setDate(date)
66+
67+
timeWidget.timeChanged.connect(_changedTime)
68+
dateWidget.dateChanged.connect(_changedDate)
69+
dateFormWidget.dateChanged.connect(_changedDate)
70+
datetimeWidget.datetimeChanged.connect(_changedDatetime)
71+
72+
return frame
73+
74+
def main():
75+
parser = argparse.ArgumentParser()
76+
parser.add_argument('-f', help='Full Screen', action='store_true')
77+
args = parser.parse_args()
78+
79+
root = ttk.TTk(mouseTrack=True)
80+
if args.f:
81+
root.setLayout(ttk.TTkGridLayout())
82+
winColor1 = root
83+
else:
84+
winColor1 = ttk.TTkWindow(parent=root,pos = (0,0), size=(52,14), title="Test DateTime Picker", border=True, layout=ttk.TTkGridLayout())
85+
86+
demoDateTimePicker(winColor1)
87+
88+
root.mainloop()
89+
90+
if __name__ == "__main__":
91+
main()

libs/pyTermTk/TermTk/TTkAbstract/abstractscrollarea.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def setHorizontalScrollBarPolicy(self, policy):
167167
self._horizontalScrollBarPolicy = policy
168168
self._viewportChanged()
169169

170-
def viewport(self):
170+
def viewport(self) -> TTkAbstractScrollViewInterface:
171171
return self._viewport
172172

173173
def update(self, repaint=True, updateLayout=False, updateParent=False):

libs/pyTermTk/TermTk/TTkCore/canvas.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,6 @@ def drawBoxTitle(self, pos, size, text, align=TTkK.CENTER_ALIGN, color=TTkColor.
298298
x,y = pos
299299
w,h = size
300300
if w < 4: return
301-
gg = TTkCfg.theme.grid[grid]
302301

303302
if text.termWidth() > w-4:
304303
text = text.substring(to=w-4)
@@ -307,12 +306,12 @@ def drawBoxTitle(self, pos, size, text, align=TTkK.CENTER_ALIGN, color=TTkColor.
307306
elif align == TTkK.LEFT_ALIGN:
308307
l=1
309308
else:
310-
l = w-2-text.termWidth()
309+
l = w-3-text.termWidth()
311310
l+=x
312311
r = l+text.termWidth()+1
313312

314-
self._set(y,l, gg[0x0B], color)
315-
self._set(y,r, gg[0x08], color)
313+
self._set(y,l, '╸', color)
314+
self._set(y,r, '╺', color)
316315
self.drawText(pos=(l+1,y),text=text,color=colorText)
317316

318317

libs/pyTermTk/TermTk/TTkTemplates/mouseevents.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,15 @@ def enterEvent(self, evt:TTkMouseEvent) -> bool:
127127
'''
128128
This event handler, can be reimplemented in a subclass to receive mouse enter events for the widget.
129129
130-
.. note:: Reimplement this function to handle this event
130+
.. note::
131+
132+
This handler reimplementation require the `super()` method to be called.
133+
134+
.. code:: python
135+
136+
def enterEvent(self, evt):
137+
TTkLog.debug('Enter Event')
138+
return super().enterEvent(evt)
131139
132140
:param evt: The mouse event
133141
:type evt: :py:class:`TTkMouseEvent`
@@ -141,7 +149,15 @@ def leaveEvent(self, evt:TTkMouseEvent) -> bool:
141149
'''
142150
This event handler, can be reimplemented in a subclass to receive mouse leave events for the widget.
143151
144-
.. note:: Reimplement this function to handle this event
152+
.. note::
153+
154+
This handler reimplementation require the `super()` method to be called.
155+
156+
.. code:: python
157+
158+
def leaveEvent(self, evt):
159+
TTkLog.debug('Leave Event')
160+
return super().leaveEvent(evt)
145161
146162
:param evt: The mouse event
147163
:type evt: :py:class:`TTkMouseEvent`

0 commit comments

Comments
 (0)