| 1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
| 2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 |
| 3 | |
| 4 | import { ListView } from "../common/listview.slint" ; |
| 5 | import { StateLayer } from "components.slint" ; |
| 6 | import { MaterialPalette, Icons } from "styling.slint" ; |
| 7 | |
| 8 | component TableViewColumn inherits Rectangle { |
| 9 | in property <SortOrder> sort-order: SortOrder.unsorted; |
| 10 | |
| 11 | callback clicked <=> state-layer.clicked; |
| 12 | callback adjust-size(/* size */ length); |
| 13 | |
| 14 | state-layer := StateLayer { |
| 15 | background: MaterialPalette.accent-background; |
| 16 | checked-background: MaterialPalette.alternate-background; |
| 17 | ripple-color: MaterialPalette.accent-ripple; |
| 18 | has-ripple: true; |
| 19 | } |
| 20 | |
| 21 | HorizontalLayout { |
| 22 | padding-left: 16px; |
| 23 | padding-right: 16px; |
| 24 | spacing: 4px; |
| 25 | |
| 26 | HorizontalLayout { |
| 27 | @children |
| 28 | } |
| 29 | |
| 30 | Image { |
| 31 | visible: root.sort-order != SortOrder.unsorted; |
| 32 | width: 12px; |
| 33 | height: 12px; |
| 34 | y: (parent.height - self.height) / 2; |
| 35 | source: root.sort-order == SortOrder.ascending ? |
| 36 | Icons.arrow-upward : |
| 37 | Icons.arrow-downward; |
| 38 | colorize: MaterialPalette.control-foreground; |
| 39 | accessible-role: none; |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | // border |
| 44 | Rectangle { |
| 45 | y: parent.height - self.height; |
| 46 | width: 100%; |
| 47 | height: 1px; |
| 48 | background: MaterialPalette.border; |
| 49 | } |
| 50 | |
| 51 | Rectangle { |
| 52 | x: parent.width - 1px; |
| 53 | width: 1px; |
| 54 | background: movable-touch-area.has-hover ? MaterialPalette.border : transparent; |
| 55 | |
| 56 | animate background { duration: 250ms; } |
| 57 | |
| 58 | movable-touch-area := TouchArea { |
| 59 | width: 10px; |
| 60 | |
| 61 | moved => { |
| 62 | if (self.pressed) { |
| 63 | adjust_size(self.mouse-x - self.pressed-x); |
| 64 | } |
| 65 | } |
| 66 | mouse-cursor: ew-resize; |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | component TableViewCell inherits Rectangle { |
| 72 | clip: true; |
| 73 | |
| 74 | HorizontalLayout { |
| 75 | padding-left: 16px; |
| 76 | padding-right: 16px; |
| 77 | padding-top: 12px; |
| 78 | padding-bottom: 12px; |
| 79 | |
| 80 | @children |
| 81 | } |
| 82 | |
| 83 | // border |
| 84 | Rectangle { |
| 85 | y: parent.height - self.height; |
| 86 | width: 100%; |
| 87 | height: 1px; |
| 88 | background: MaterialPalette.border; |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | component TableViewRow inherits Rectangle { |
| 93 | in property <bool> selected; |
| 94 | out property <length> mouse-x <=> state-layer.mouse-x; |
| 95 | out property <length> mouse-y <=> state-layer.mouse-y; |
| 96 | |
| 97 | callback clicked <=> state-layer.clicked; |
| 98 | callback pointer-event <=> state-layer.pointer-event; |
| 99 | |
| 100 | min-height: max(42px, layout.min-height); |
| 101 | |
| 102 | state-layer := StateLayer { |
| 103 | checked: root.selected; |
| 104 | background: MaterialPalette.accent-background; |
| 105 | checked-background: MaterialPalette.control-background; |
| 106 | ripple-color: MaterialPalette.accent-ripple; |
| 107 | has-ripple: true; |
| 108 | } |
| 109 | |
| 110 | layout := HorizontalLayout { |
| 111 | @children |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | export component StandardTableView { |
| 116 | private property <length> item-height: scroll-view.viewport-height / rows.length; |
| 117 | private property <length> current-item-y: scroll-view.viewport-y + current-row * item-height; |
| 118 | private property <length> min-header-height: 42px; |
| 119 | |
| 120 | in property <[[StandardListViewItem]]> rows; |
| 121 | out property <int> current-sort-column: -1; |
| 122 | in-out property <[TableColumn]> columns; |
| 123 | in-out property <int> current-row: -1; |
| 124 | in property <bool> enabled <=> scroll-view.enabled; |
| 125 | out property <length> visible-width <=> scroll-view.visible-width; |
| 126 | out property <length> visible-height <=> scroll-view.visible-height; |
| 127 | in-out property <bool> has-focus <=> scroll-view.has-focus; |
| 128 | in-out property <length> viewport-width <=> scroll-view.viewport-width; |
| 129 | in-out property <length> viewport-height <=> scroll-view.viewport-height; |
| 130 | in-out property <length> viewport-x <=> scroll-view.viewport-x; |
| 131 | in-out property <length> viewport-y <=> scroll-view.viewport-y; |
| 132 | in property <ScrollBarPolicy> vertical-scrollbar-policy <=> scroll-view.vertical-scrollbar-policy; |
| 133 | in property <ScrollBarPolicy> horizontal-scrollbar-policy <=> scroll-view.horizontal-scrollbar-policy; |
| 134 | |
| 135 | callback sort-ascending(column: int); |
| 136 | callback sort-descending(column: int); |
| 137 | callback row-pointer-event(row: int, event: PointerEvent, position: Point); |
| 138 | callback current-row-changed(current-row: int); |
| 139 | |
| 140 | public function set-current-row(index: int) { |
| 141 | if(index < 0 || index >= rows.length) { |
| 142 | return; |
| 143 | } |
| 144 | |
| 145 | current-row = index; |
| 146 | current-row-changed(current-row); |
| 147 | |
| 148 | if(current-item-y < 0) { |
| 149 | scroll-view.viewport-y += 0 - current-item-y; |
| 150 | } |
| 151 | |
| 152 | if(current-item-y + item-height > scroll-view.visible-height) { |
| 153 | scroll-view.viewport-y -= current-item-y + item-height - scroll-view.visible-height; |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | function sort(index: int) { |
| 158 | if (root.current-sort-column != index) { |
| 159 | root.columns[root.current-sort-column].sort-order = SortOrder.unsorted; |
| 160 | } |
| 161 | |
| 162 | if(root.columns[index].sort-order == SortOrder.ascending) { |
| 163 | root.columns[index].sort-order = SortOrder.descending; |
| 164 | root.sort-descending(index); |
| 165 | } else { |
| 166 | root.columns[index].sort-order = SortOrder.ascending; |
| 167 | root.sort-ascending(index); |
| 168 | } |
| 169 | |
| 170 | root.current-sort-column = index; |
| 171 | } |
| 172 | |
| 173 | min-width: 400px; |
| 174 | min-height: 200px; |
| 175 | horizontal-stretch: 1; |
| 176 | vertical-stretch: 1; |
| 177 | forward-focus: focus-scope; |
| 178 | accessible-role: table; |
| 179 | |
| 180 | VerticalLayout { |
| 181 | Rectangle { |
| 182 | clip: true; |
| 183 | vertical-stretch: 0; |
| 184 | min-height: header-layout.min-height; |
| 185 | |
| 186 | header-layout := HorizontalLayout { |
| 187 | padding-right: 20px; |
| 188 | min-height: root.min-header-height; |
| 189 | vertical-stretch: 0; |
| 190 | |
| 191 | for column[index] in root.columns : TableViewColumn { |
| 192 | sort-order: column.sort-order; |
| 193 | horizontal-stretch: column.horizontal-stretch; |
| 194 | min-width: max(column.min-width, column.width); |
| 195 | preferred-width: self.min-width; |
| 196 | max-width: (index < columns.length && column.width >= 1px) ? max(column.min-width, column.width) : 100000px; |
| 197 | |
| 198 | Text { |
| 199 | vertical-alignment: center; |
| 200 | text: column.title; |
| 201 | font-weight: 900; |
| 202 | overflow: elide; |
| 203 | } |
| 204 | |
| 205 | clicked => { |
| 206 | root.sort(index); |
| 207 | } |
| 208 | |
| 209 | adjust-size(diff) => { |
| 210 | column.width = max(1px, self.width + diff); |
| 211 | } |
| 212 | } |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | scroll-view := ListView { |
| 217 | for row[idx] in root.rows : TableViewRow { |
| 218 | selected: idx == root.current-row; |
| 219 | |
| 220 | clicked => { |
| 221 | root.focus(); |
| 222 | root.set-current-row(idx); |
| 223 | } |
| 224 | |
| 225 | pointer-event(pe) => { |
| 226 | root.row-pointer-event(idx, pe, { |
| 227 | x: self.absolute-position.x + self.mouse-x - root.absolute-position.x, |
| 228 | y: self.absolute-position.y + self.mouse-y - root.absolute-position.y, |
| 229 | }); |
| 230 | } |
| 231 | |
| 232 | for cell[index] in row : TableViewCell { |
| 233 | private property <bool> has_inner_focus; |
| 234 | |
| 235 | horizontal-stretch: root.columns[index].horizontal-stretch; |
| 236 | min-width: max(columns[index].min-width, columns[index].width); |
| 237 | preferred-width: self.min-width; |
| 238 | max-width: (index < columns.length && columns[index].width >= 1px) ? max(columns[index].min-width, columns[index].width) : 100000px; |
| 239 | |
| 240 | Rectangle { |
| 241 | Text { |
| 242 | width: 100%; |
| 243 | height: 100%; |
| 244 | overflow: elide; |
| 245 | vertical-alignment: center; |
| 246 | text: cell.text; |
| 247 | } |
| 248 | } |
| 249 | } |
| 250 | } |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | focus-scope := FocusScope { |
| 255 | x: 0; |
| 256 | width: 0; // Do not react on clicks |
| 257 | |
| 258 | key-pressed(event) => { |
| 259 | if (event.text == Key.UpArrow) { |
| 260 | root.set-current-row(root.current-row - 1); |
| 261 | return accept; |
| 262 | } else if (event.text == Key.DownArrow) { |
| 263 | root.set-current-row(root.current-row + 1); |
| 264 | return accept; |
| 265 | } |
| 266 | |
| 267 | reject |
| 268 | } |
| 269 | } |
| 270 | } |
| 271 | |