Skip to content

Commit e163eee

Browse files
Add Separator and Treeview widgets with regression tests
- Implemented the Separator widget, allowing for horizontal and vertical dividers. - Created the Treeview widget for hierarchical and flat table representations. - Added regression tests for both Separator and Treeview widgets to ensure functionality. - Included tests for various scenarios including insertion, selection, and configuration of both widgets. - Added Notebook, PanedWindow, and LabelFrame tests to ensure comprehensive coverage of widget functionalities.
1 parent 4672ed5 commit e163eee

16 files changed

Lines changed: 1893 additions & 0 deletions

docs/LabelFrame.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# LabelFrame Widget
2+
3+
The **LabelFrame** widget is a frame with a titled border — wraps Tk's `ttk::labelframe`. Visually a [`Frame`](Frame.md) whose top edge is broken by a small title label, useful for grouping related controls in a settings dialog. _New in v1.9._
4+
5+
---
6+
7+
### Constructor
8+
9+
```php
10+
new LabelFrame(string $parentId, array $options = [])
11+
```
12+
13+
| Parameter | Type | Description |
14+
|-------------|----------|------------------------------------------|
15+
| `$parentId` | `string` | `getId()` of the parent widget. |
16+
| `$options` | `array` | Configuration — see below. |
17+
18+
| Option | Type | Description |
19+
|--------------|----------|--------------------------------------------------------------|
20+
| `text` | `string` | Title shown along the top border. Optional. |
21+
| `labelanchor`| `string` | Title position: `'nw'`, `'n'`, `'ne'`, `'w'`, `'e'`, `'sw'`, `'s'`, `'se'`. |
22+
| `padding` | `int`/`string` | Inner padding around children. |
23+
| `borderwidth`| `int` | Border thickness. |
24+
| `relief` | `string` | `'flat'`, `'raised'`, `'sunken'`, `'groove'`, `'ridge'`. |
25+
26+
---
27+
28+
### Examples
29+
30+
**Grouping related inputs:**
31+
```php
32+
use PhpGui\Widget\{LabelFrame, Label, Input};
33+
34+
$net = new LabelFrame($window->getId(), ['text' => 'Network']);
35+
$net->pack(['fill' => 'x', 'padx' => 8, 'pady' => 8]);
36+
37+
(new Label($net->getId(), ['text' => 'Hostname:']))->pack(['anchor' => 'w']);
38+
(new Input($net->getId()))->pack(['fill' => 'x']);
39+
40+
(new Label($net->getId(), ['text' => 'Port:']))->pack(['anchor' => 'w']);
41+
(new Input($net->getId()))->pack(['fill' => 'x']);
42+
```
43+
44+
**Updating the title at runtime:**
45+
```php
46+
$box = new LabelFrame($window->getId(), ['text' => 'Loading...']);
47+
$box->pack(['fill' => 'both', 'expand' => 1]);
48+
// later...
49+
$box->setText('Results (12 rows)');
50+
```
51+
52+
---
53+
54+
### Methods
55+
56+
| Method | Signature | Description |
57+
|---------------|------------------------|------------------------------------------------------|
58+
| `setText()` | `(string $text): void` | Update the title shown along the top border. |
59+
| `getText()` | `(): string` | Read the current title. |
60+
| `pack/place/grid` | `(array $opts = []): void` | Inherited. |
61+
| `destroy()` | `(): void` | Removes the frame and all its children. |
62+
63+
---
64+
65+
### Notes
66+
67+
- Children are placed inside a LabelFrame the same way as a regular [`Frame`](Frame.md): construct them with `$labelFrame->getId()` as the parent.
68+
- The title is run through the safe-quoting helper, so user-supplied (e.g. translated) titles cannot inject Tcl.
69+
- Construct without `text` for a plain bordered frame — useful when you want the visual border but no caption.

docs/Notebook.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Notebook Widget
2+
3+
The **Notebook** widget is a tabbed container — each tab hosts a child widget that becomes visible when its tab is selected. Wraps Tk's `ttk::notebook`. _New in v1.9._
4+
5+
Pages must be **children of the notebook itself**. The typical pattern is to make each page a `Frame` parented to the notebook, populate the frame, then call `addTab()`.
6+
7+
---
8+
9+
### Constructor
10+
11+
```php
12+
new Notebook(string $parentId, array $options = [])
13+
```
14+
15+
| Parameter | Type | Description |
16+
|-------------|----------|------------------------------------------|
17+
| `$parentId` | `string` | `getId()` of the parent widget. |
18+
| `$options` | `array` | Tk options forwarded to `ttk::notebook`. |
19+
20+
---
21+
22+
### Examples
23+
24+
**Two simple tabs:**
25+
```php
26+
use PhpGui\Widget\{Notebook, Frame, Label};
27+
28+
$nb = new Notebook($window->getId());
29+
$nb->pack(['fill' => 'both', 'expand' => 1]);
30+
31+
// Page 1
32+
$general = new Frame($nb->getId());
33+
(new Label($general->getId(), ['text' => 'General settings here']))
34+
->pack(['padx' => 20, 'pady' => 20]);
35+
$nb->addTab($general, 'General');
36+
37+
// Page 2
38+
$advanced = new Frame($nb->getId());
39+
(new Label($advanced->getId(), ['text' => 'Advanced settings here']))
40+
->pack(['padx' => 20, 'pady' => 20]);
41+
$nb->addTab($advanced, 'Advanced');
42+
```
43+
44+
**Reacting to tab changes:**
45+
```php
46+
$nb->onTabChange(function (int $idx) use ($status) {
47+
$status->setText("now on tab {$idx}");
48+
});
49+
```
50+
51+
**Disabling a tab:**
52+
```php
53+
$nb->setTabState(1, 'disabled'); // tab 1 greyed out, can't be selected
54+
$nb->setTabState(2, 'hidden'); // tab 2 not shown at all
55+
```
56+
57+
---
58+
59+
### Methods
60+
61+
| Method | Signature | Description |
62+
|---------------------------|----------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
63+
| `addTab()` | `(AbstractWidget $page, string $title, array $options = []): void` | Attach `$page` as a new tab. Throws if `$page` isn't a child of this Notebook. |
64+
| `selectTab()` | `(int $index): void` | Switch to the tab at 0-based `$index`. `OutOfRangeException` on bad index. |
65+
| `selectPage()` | `(AbstractWidget $page): void` | Switch to the tab whose page is `$page`. `InvalidArgumentException` if not in this notebook. |
66+
| `getSelectedIndex()` | `(): int` | Active tab index, or `-1` if the notebook is empty. |
67+
| `getSelectedPage()` | `(): ?AbstractWidget` | Active page widget, or `null`. |
68+
| `getTabCount()` | `(): int` | Total number of tabs. |
69+
| `removeTab()` | `(int $index): void` | Detach the tab at `$index`. Page widget is **not** destroyed — caller can re-attach or destroy explicitly. |
70+
| `setTabTitle()` | `(int $index, string $title): void` | Update an existing tab's title. |
71+
| `getTabTitle()` | `(int $index): string` | Read an existing tab's title. |
72+
| `setTabState()` | `(int $index, string $state): void` | `'normal'`, `'disabled'`, or `'hidden'`. Other values throw. |
73+
| `onTabChange()` | `(callable $h): void` | `$h(int $index)` fires on `<<NotebookTabChanged>>` — user click and programmatic `selectTab()` alike. |
74+
| `pack/place/grid` | `(array $opts = []): void` | Inherited. |
75+
| `destroy()` | `(): void` | Removes the notebook and frees the tab-change callback. |
76+
77+
#### `addTab()` options
78+
79+
The third arg accepts tab-level Tk options:
80+
81+
| Key | Description |
82+
|-----------|------------------------------------------------------------|
83+
| `state` | `'normal'`, `'disabled'`, `'hidden'` |
84+
| `image` | Tk image name to show alongside the text |
85+
| `compound`| Image+text layout (`'left'`, `'right'`, `'top'`, `'none'`) |
86+
| `underline` | Index of the character to underline (for keyboard mnemonic) |
87+
| `padding` | Internal padding around the tab title |
88+
| `sticky` | How the page widget fills its slot |
89+
90+
---
91+
92+
### Notes
93+
94+
- The framework checks at `addTab()` time that the page widget's parent is actually this notebook. Without that guard, Tk would create the tab and silently render an empty pane (the page widget lives elsewhere in the widget tree).
95+
- `removeTab()` calls Tk's `forget`, which detaches the tab but leaves the page widget alive. Call `destroy()` on the page yourself if you want it gone.
96+
- The `<<NotebookTabChanged>>` virtual event needs Tk's event loop to dispatch — in `Application::run()` this happens automatically; in tests, call `evalTcl('update')` after `selectTab()` if you need the handler to fire synchronously.
97+
- Tab titles are run through the safe-quoting helper, so user-supplied (e.g. translated) titles cannot inject Tcl.

docs/PanedWindow.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# PanedWindow Widget
2+
3+
The **PanedWindow** widget is a resizable split container — wraps Tk's `ttk::panedwindow`. Each pane hosts a child widget; the user drags the divider between panes to resize them. _New in v1.9._
4+
5+
Like [`Notebook`](Notebook.md), each pane must be a **child of the PanedWindow itself**. The typical pattern is to make each pane a `Frame` parented to the PanedWindow, populate the frame, then call `addPane()`.
6+
7+
---
8+
9+
### Constructor
10+
11+
```php
12+
new PanedWindow(string $parentId, array $options = [])
13+
```
14+
15+
| Parameter | Type | Description |
16+
|-------------|----------|-----------------------------------------------------------------------------------|
17+
| `$parentId` | `string` | `getId()` of the parent widget. |
18+
| `$options` | `array` | Configuration — see below. |
19+
20+
| Option | Type | Description |
21+
|----------|----------|----------------------------------------------------------------------|
22+
| `orient` | `string` | `'horizontal'` (default) or `'vertical'`. Other values throw. |
23+
24+
Any other option is forwarded to `ttk::panedwindow` (run through safe-quoting).
25+
26+
---
27+
28+
### Examples
29+
30+
**Sidebar + main content (3:1 split):**
31+
```php
32+
use PhpGui\Widget\{PanedWindow, Frame};
33+
34+
$split = new PanedWindow($window->getId(), ['orient' => 'horizontal']);
35+
$split->pack(['fill' => 'both', 'expand' => 1]);
36+
37+
$sidebar = new Frame($split->getId());
38+
$content = new Frame($split->getId());
39+
40+
$split->addPane($sidebar, ['weight' => 1]);
41+
$split->addPane($content, ['weight' => 3]);
42+
```
43+
44+
**Stacked vertical panes:**
45+
```php
46+
$split = new PanedWindow($window->getId(), ['orient' => 'vertical']);
47+
$split->pack(['fill' => 'both', 'expand' => 1]);
48+
49+
$top = new Frame($split->getId());
50+
$bottom = new Frame($split->getId());
51+
$split->addPane($top, ['weight' => 2]);
52+
$split->addPane($bottom, ['weight' => 1]);
53+
54+
// Programmatically move the divider 200px down from the top.
55+
$split->setSashPosition(0, 200);
56+
```
57+
58+
---
59+
60+
### Methods
61+
62+
| Method | Signature | Description |
63+
|-----------------------|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
64+
| `addPane()` | `(AbstractWidget $child, array $options = []): void` | Attach `$child` as a new pane. Throws if `$child` isn't a child of this PanedWindow. |
65+
| `removePane()` | `(int $index): void` | Detach the pane at `$index`. The child widget is **not** destroyed. |
66+
| `getPaneCount()` | `(): int` | Total number of panes. |
67+
| `getPane()` | `(int $index): ?AbstractWidget` | Pane widget at `$index`, or `null` if out of range. |
68+
| `configurePane()` | `(int $index, array $options): void` | Update pane-level options (e.g. `weight`). |
69+
| `setSashPosition()` | `(int $index, int $position): void` | Move the divider at `$index` to absolute pixel position `$position`. |
70+
| `getSashPosition()` | `(int $index): int` | Read the divider position. |
71+
| `pack/place/grid` | `(array $opts = []): void` | Inherited. |
72+
| `destroy()` | `(): void` | Removes the PanedWindow. |
73+
74+
#### `addPane()` and `configurePane()` options
75+
76+
| Key | Description |
77+
|----------|------------------------------------------------------------------|
78+
| `weight` | `int` — share of leftover space when the parent resizes. |
79+
80+
---
81+
82+
### Notes
83+
84+
- The framework checks at `addPane()` time that the child's parent is actually this PanedWindow. Without this guard, Tk would silently render an empty pane.
85+
- `removePane()` calls Tk's `forget`, which detaches the pane but leaves the child widget alive. Call `destroy()` on the child yourself if you want it gone.
86+
- Sash indices are 0-based: index 0 is the divider after the first pane, index 1 is the divider between the second and third pane, and so on.

docs/Separator.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Separator Widget
2+
3+
The **Separator** widget is a thin horizontal or vertical divider line — wraps Tk's `ttk::separator`. _New in v1.9._
4+
5+
---
6+
7+
### Constructor
8+
9+
```php
10+
new Separator(string $parentId, array $options = [])
11+
```
12+
13+
| Parameter | Type | Description |
14+
|-------------|----------|------------------------------------------|
15+
| `$parentId` | `string` | `getId()` of the parent widget. |
16+
| `$options` | `array` | Configuration — see below. |
17+
18+
| Option | Type | Description |
19+
|----------|----------|----------------------------------------------------------------------|
20+
| `orient` | `string` | `'horizontal'` (default) or `'vertical'`. Other values throw. |
21+
22+
---
23+
24+
### Examples
25+
26+
**Horizontal divider between sections:**
27+
```php
28+
use PhpGui\Widget\Separator;
29+
30+
$header->pack(['fill' => 'x']);
31+
32+
(new Separator($window->getId()))->pack(['fill' => 'x', 'pady' => 6]);
33+
34+
$body->pack(['fill' => 'both', 'expand' => 1]);
35+
```
36+
37+
**Vertical divider in a horizontal toolbar:**
38+
```php
39+
$toolbar = new Frame($window->getId());
40+
$toolbar->pack(['fill' => 'x']);
41+
42+
$openBtn->pack(['side' => 'left']);
43+
$saveBtn->pack(['side' => 'left']);
44+
45+
(new Separator($toolbar->getId(), ['orient' => 'vertical']))
46+
->pack(['side' => 'left', 'fill' => 'y', 'padx' => 4]);
47+
48+
$cutBtn->pack(['side' => 'left']);
49+
$copyBtn->pack(['side' => 'left']);
50+
```
51+
52+
---
53+
54+
### Methods
55+
56+
The widget is purely visual — there is no per-widget API beyond what `AbstractWidget` provides (`pack`, `place`, `grid`, `destroy`).
57+
58+
---
59+
60+
### Notes
61+
62+
- For a horizontal separator inside a horizontally-packing container, `pack(['fill' => 'x'])` makes it span the available width.
63+
- For a vertical separator inside a horizontally-packing container, `pack(['side' => 'left', 'fill' => 'y'])` is the typical incantation.

0 commit comments

Comments
 (0)