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