import QtQuick import QtQuick.Window import QtQuick.Templates as QQCT import Weave.Controls import Weave.Templates as T import "internal" T.Dropdown { id: root function _currentItemData() { return currentIndex >= 0 && delegateModel && delegateModel.items && currentIndex < delegateModel.count ? delegateModel.items.get(currentIndex).model : null } function _modelRoleValue(data, role, defaultValue, allowNoRole) { if (data && role) { if (Array.isArray(root.model)) { // the data is a JS string or object array. if (allowNoRole && (typeof data.modelData === "string" || data.modelData instanceof String)) { return data.modelData } else if (role && role.length > 0) { return data.modelData[role] } else { return defaultValue } } else if (role && role.length > 0) { return data[role] || defaultValue } } return defaultValue } function _iconName(data) { const icon = _modelRoleValue(data, iconRole) return icon && icon.indexOf(':') < 0 ? icon : undefined } function _iconSource(data) { const icon = _modelRoleValue(data, iconRole) return icon && icon.indexOf(':') >= 0 ? icon : undefined } function _thumbnail(data) { return _modelRoleValue(data, thumbnailRole) } function _avatarName(data) { return _modelRoleValue(data, avatarNameRole) } T.TableLayout.minimumWidth: contentItem.T.TableLayout.minimumWidth T.TableLayout.minimumHeight: contentItem.T.TableLayout.minimumHeight TableLayout.leftPadding: Math.max(T.TableLayout.leftLayoutPadding, _styleAttributes.paddingLeft) TableLayout.rightPadding: Math.max(T.TableLayout.rightLayoutPadding, _styleAttributes.paddingRight) TableLayout.topPadding: Math.max(T.TableLayout.topLayoutPadding, _styleAttributes.paddingTop) TableLayout.bottomPadding: root.bottomInset + Math.max(T.TableLayout.bottomLayoutPadding, _styleAttributes.paddingBottom) implicitWidth: implicitContentWidth + leftPadding + rightPadding implicitHeight: implicitContentHeight + topPadding + bottomPadding // If the contentItem is editable it will eat clicks, so we need external padding to prevent it overlapping // the indicator (i.e. the dropdown caret) and the indicators (feedback indicator icons). // Note the clickable area for opening the dropdown is anywhere not blocked by the text field, so the padding on the other // sides is internal to the contentItem text field so there isn't a clickable border. leftPadding: mirrored ? T.TableLayout.leftPadding + implicitIndicatorWidth : 0 rightPadding: mirrored ? 0 : T.TableLayout.rightPadding + implicitIndicatorWidth bottomInset: (root._messageLoader.active ? root._messageLoader.height : 0) font.family: Theme.generic.font.family // TODO: Theme constant for input fontFamily font.pixelSize: Theme.component.menu.fontSize font.weight: Theme.component.menu.fontWeight.default hoverEnabled: root.enabled selectTextByMouse: editable focusPolicy: editable || filterable ? Qt.TabFocus : Qt.StrongFocus opacity: root.enabled ? 1.0 : Theme.component.menu.opacity.disabled currentIndex: -1 imageStyle: (_avatarName(_currentItemData())?.length ?? 0) > 0 ? Dropdown.Avatar : (_thumbnail(_currentItemData())?.toString()?.length ?? 0) > 0 ? Dropdown.Thumbnail : Dropdown.Icon icon { name: (root._avatarName(_currentItemData()) || root._iconName(_currentItemData())) ?? "" source: (root._thumbnail(_currentItemData()) || root._iconSource(_currentItemData())) ?? "" color: Theme.component.menu.icon.fill width: Theme.component.menu.icon.width // TODO: Theme constant for dropdown.iconSize ? height: Theme.component.menu.icon.height // TODO: Theme constant for dropdown.iconSize ? } decoration: { if (imageStyle === Dropdown.Avatar) { return avatarComponent } else if (imageStyle === Dropdown.Thumbnail){ return thumbnailComponent } else { return iconComponent } } Component { id: iconComponent T.IconImage { T.TableLayout.itemStatus: status icon: root.icon color: Theme.component.menu.icon.fill sourceSize { width: Theme.component.menu.icon.width // TODO: Theme constant for dropdown.iconSize ? height: Theme.component.menu.icon.height // TODO: Theme constant for dropdown.iconSize ? } } } Component { id: thumbnailComponent Thumbnail { T.TableLayout.itemStatus: status icon: root.icon aspectRatio: Thumbnail.Aspect1_1 size: Thumbnail.ExtraExtraSmall } } Component { id: avatarComponent Avatar { icon: root.icon size: Avatar.Small } } T.SearchValidator { id: searchValidator enabled: root.filterable && root.activeFocus property string precompletionText property bool openingPopup property bool entryActivated onSearchTextChanged: { if (entryActivated) { // the text might change due to the user selecting an entry from the dropdown. // in this case, ignore the update, as it wasn't typed. entryActivated = false } else if (precompletionText === "") { precompletionText = searchText Qt.callLater(updateFilterText) } } function updateFilterText() { root.filterText = precompletionText precompletionText = "" Qt.callLater(searchValidator.maybeOpenPopup) } function resetFilter() { root.filterText = "" precompletionText = "" searchValidator.reset() } function maybeOpenPopup() { if (root.filterText !== "" && searchValidator.enabled) { openingPopup = true root.popup.open() } } property T.TextField field: T.TextField { id: searchHighlighter } } onDownChanged: { if (down) { if (searchValidator.openingPopup) { searchValidator.openingPopup = false } else { searchValidator.resetFilter() } } } onAccepted: searchValidator.resetFilter() onCurrentIndexChanged: searchValidator.resetFilter() Component.onCompleted: searchValidator.resetFilter() indicator: T.TableLayoutRow { x: root.mirrored ? root.leftPadding - width : root.width - root.rightPadding y: root.topPadding height: root.availableHeight - (root._messageLoader.active ? root._messageLoader.height : 0) horizontalItemFillMode: T.TableLayout.Preferred verticalItemFillMode: T.TableLayout.Preferred leftMargin: Theme.component.dropdown.icon.marginLeft rightMargin: Theme.component.dropdown.icon.marginRight topMargin: Theme.component.dropdown.icon.marginTop bottomMargin: Theme.component.dropdown.icon.marginBottom T.IconImage { name: root.down ? Theme.icon.medium("caret-up") : Theme.icon.medium("caret-down") color: root.down ? Theme.component.dropdown.icon.fill.active : Theme.component.dropdown.icon.fill.default } } property Item _messageLoader: TextInputMessageLoader { parent: root y: root.height - height width: root.width message: root.message feedback: root.feedback toolTip: root.feedbackToolTip visible: active active: root.message.length > 0 } contentItem: T.DropdownTextField { // The contentItem must be a TextInput derivative item for the editText property to work. id: textField T.TableLayout.minimumWidth: contentLayout.minimumWidth T.TableLayout.minimumHeight: contentLayout.minimumHeight implicitWidth: contentLayout.implicitWidth implicitHeight: Math.max(Theme.component.input.minHeight, contentLayout.implicitHeight) leftPadding: proxyText.x topPadding: proxyText.y rightPadding: contentLayout.width - proxyText.x - proxyText.width bottomPadding: contentLayout.height - proxyText.y - proxyText.height autoScroll: root.editable readOnly: (!root.filterable && (root.down || !root.editable)) || root.multiselect inputMethodHints: root.inputMethodHints validator: root.filterable ? searchValidator : root.validator selectByMouse: root.selectTextByMouse focus: true font: root.font color: Theme.component.menu.textColor selectionColor: root.feedback === Feedback.None ? Theme.component.input.backgroundColor.selected : Theme.component.input.error.backgroundColor.selected selectedTextColor: color placeholderTextColor: Theme.component.input.textColor // TODO: token for color of placeholder text verticalAlignment: Text.AlignVCenter text: root.multiselect ? root.multiselectText : root.displayStyle === T.Dropdown.IconOnly ? "" : root.editable ? root.editText : root.displayText TableLayoutRow { id: contentLayout rowItem: root width: textField.width height: textField.height leftMargin: root.mirrored ? 0 : root.T.TableLayout.leftPadding rightMargin: !root.mirrored ? 0 : root.T.TableLayout.rightPadding topMargin: root.T.TableLayout.topPadding bottomMargin: root.T.TableLayout.bottomPadding horizontalSpacing: 0 verticalSpacing: 0 verticalItemAlignment: TableLayout.AlignVCenter Repeater { model: root.tagContentModel } Item { T.TableLayout.canWrap: true T.TableLayout.minimumHeight: Theme.component.input.lineHeight } T.DecorationItemLoader { id: iconLoader T.TableLayout.verticalFillMode: T.TableLayout.Preferred T.TableLayout.topMargin: 0 T.TableLayout.bottomMargin: 0 visible: !root.multiselect && root.displayStyle !== T.Dropdown.TextOnly leftMargin: Theme.component.menu.icon.marginLeft rightMargin: Theme.component.menu.icon.marginRight topMargin: Theme.component.menu.icon.marginTop bottomMargin: Theme.component.menu.icon.marginBottom + (root._messageLoader.active ? root._messageLoader.height : 0) horizontalItemFillMode: TableLayout.Maximum verticalItemFillMode: TableLayout.Maximum verticalItemAlignment: TableLayout.AlignVCenter delegateItem: root sourceComponent: root.decoration } Text { id: proxyText T.TableLayout.horizontalFillMode: T.TableLayout.Preferred T.TableLayout.minimumWidth: Theme.semantic.size.touchTarget T.TableLayout.minimumHeight: Theme.component.input.lineHeight T.TableLayout.preferredWidth: Math.max(implicitWidth, root.largestTextWidth) T.TableLayout.preferredHeight: Theme.component.input.lineHeight T.TableLayout.visible: root.displayStyle !== T.Dropdown.IconOnly text: textField.displayText font: root.font lineHeight: Theme.component.input.lineHeight lineHeightMode: Text.FixedHeight opacity: 0 } T.TableLayoutLoader { id: warningLoader T.TableLayout.topMargin: 0 T.TableLayout.bottomMargin: 0 active: root.feedback !== Feedback.None && !indicatorsRow.visible && root.message.length === 0 visible: active sourceComponent: InputErrorIcon { id: inputErrorIcon MouseArea { id: hoverDetector anchors.fill: parent enabled: root.feedbackToolTip.text.length > 0 hoverEnabled: true } property Component defaultContent: ToolTipContent {} ToolTip.content: root.feedbackToolTip.content ? root.feedbackToolTip.content : defaultContent ToolTip.text: root.feedbackToolTip.text ToolTip.delay: root.feedbackToolTip.delay ToolTip.timeout: root.feedbackToolTip.timeout ToolTip.preferredAlignment: root.feedbackToolTip.preferredAlignment ToolTip.visible: hoverDetector.containsMouse || (ToolTip.toolTip.parent === inputErrorIcon && ToolTip.toolTip.hovered) || (root.feedbackToolTip.visible) } } T.TableLayoutRow { id: indicatorsRow T.TableLayout.topMargin: 0 T.TableLayout.bottomMargin: 0 visible: indicatorsRow.children.length > 0 verticalItemAlignment: TableLayout.AlignVCenter children: root.indicators } } } background: T.BackgroundItemLoader { delegateItem: root sourceComponent: TextInputBackground { style: root.style focused: root.activeFocus hovered: root.hovered error: root.feedback !== Feedback.None louder: error color: root._styleAttributes.backgroundColor } } headerDelegate: MenuSeparator { required property string section text: root.displayHeaderLabel ? section : "" dividerVisible: !ListView.view || y > ListView.view.originY width: ListView.view ? ListView.view.width : undefined } delegate: CheckDelegate { id: delegate required property var model required property int index enabled: !(root._modelRoleValue(model, root.disabledRole) ?? false) text: searchHighlighter.completion.highlightedText( root._modelRoleValue(model, root.textRole || "modelData", "", true) ?? "", root.filterText, root.filterable && root.filterText.length > 0 ? T.TextField.StartsWith : T.TextField.NoFilter, Qt.CaseInsensitive) font.weight: checked ? Theme.component.menu.fontWeight.selected : Theme.component.menu.fontWeight.default highlighted: model.index === root.keyNavigationIndex // don't use highlightedIndex if the keyNavigationIndex is < 0 icon { name: (root._avatarName(delegate.model) || root._iconName(delegate.model)) ?? "" source: (root._thumbnail(delegate.model) || root._iconSource(delegate.model)) ?? "" } indicator: T.IndicatorItemLoader { delegateItem: delegate leftMargin: Theme.component.menu.icon.marginLeft rightMargin: Theme.component.menu.icon.marginRight topMargin: Theme.component.menu.icon.marginTop bottomMargin: Theme.component.menu.icon.marginBottom horizontalItemAlignment: T.TableLayout.AlignHCenter verticalItemAlignment: T.TableLayout.AlignVCenter opacity: enabled && !delegate.T.TableLayout.moving ? 1.0 : Theme.component.menu.opacity.disabled visible: status === T.TableLayoutLoader.Ready active: root.multiselect || root.displayCheckmarks sourceComponent: CheckBoxIndicator { checkState: delegate.checkState pressed: delegate.pressed hovered: delegate.hovered visualFocus: delegate.visualFocus } } } popup: QQCT.Popup { id: dropdownPopup x: root.leftInset y: root.height - (root._messageLoader.active ? root._messageLoader.height : 0) width: root.popupWidthMode === T.Dropdown.FixedWidth ? root.width - root.leftInset - root.rightInset : implicitWidth implicitWidth: implicitContentWidth implicitHeight: implicitContentHeight margins: 0 onClosed: { if (root._multiselectReopen) { root._multiselectReopen = false dropdownPopup.open() } } background: DropdownPopupBackground { belowDropdown: dropdownPopup.y > 0 } contentItem: T.ListView { id: listView maximumVisibleHeight: 10 * (Theme.component.menu.lineHeight + Theme.component.menu.paddingTop + Theme.component.menu.paddingBottom) topMargin: Theme.component.menu.paddingTop bottomMargin: Theme.component.menu.paddingBottom section.property: root.headerRole section.delegate: root.headerDelegate model: root.delegateModel selectionModel: root.selectionModel keepCurrentItemVisible: root.keyNavigationIndex >= 0 font.family: Theme.component.table.fontFamily font.pixelSize: Theme.component.table.fontSize font.weight: Theme.component.table.fontWeight displayRole: root.textRole implicitTextWidthPolicy: root.popupWidthMode === T.Dropdown.FixedWidth ? T.TableView.ImplicitWidthFromVisibleItems : T.TableView.ImplicitWidthFromAllItems decorationDelegate: root.displayStyle === T.Dropdown.TextOnly ? null : decorationSelector filter: SearchFilter { role: root.textRole text: root.filterText } indicatorDelegate: T.IconImage { required property bool checked required property bool hovered required property bool highlighted color: { if (checked) { return Theme.component.dropdown.icon.fill.active } else if (enabled && (hovered || highlighted)) { return Theme.component.dropdown.icon.fill.default } else { return Theme.colorWithAlpha(Theme.component.dropdown.icon.fill.default, 0) } } name: Theme.icon.small("checkmark") } backgroundDelegate: Rectangle { required property bool pressed required property bool hovered required property bool highlighted color: { if (pressed) { return Theme.component.menu.backgroundColor.pressed } else if (enabled && (hovered || highlighted)) { return Theme.component.menu.backgroundColor.hover } else { return Theme.component.menu.backgroundColor.default } } } verticalScrollBar: ScrollBar { policy: root.scrollBarPolicy } } } T.DelegateSelection { id: decorationSelector T.Delegate { when: (data, index) => { if (Array.isArray(root.model)) { let element = root.model[index] if (typeof element === "string" || element instanceof String) { return false } else if (typeof element === "object" && element !== null && !Array.isArray(element)) { return (element[root.avatarNameRole]?.length ?? 0) > 0 } else { return false } } return (root._avatarName(data)?.length ?? 0) > 0 } Avatar { required icon size: Avatar.Small } } T.Delegate { when: (data, index) => { if (Array.isArray(root.model)) { let element = root.model[index] if (typeof element === "string" || element instanceof String) { return false } else if (typeof element === "object" && element !== null && !Array.isArray(element)) { return (element[root.thumbnailRole]?.toString()?.length ?? 0) > 0 } else { return false } } return (root._thumbnail(data)?.toString()?.length ?? 0) > 0 } Thumbnail { required icon aspectRatio: Thumbnail.Aspect1_1 size: Thumbnail.ExtraExtraSmall } } T.Delegate { T.IconImage { required icon color: Theme.component.menu.icon.fill sourceSize { width: Theme.component.menu.icon.width height: Theme.component.menu.icon.height } } } } property bool _multiselectReopen onActivated: { searchValidator.resetFilter() searchValidator.entryActivated = true if (root.multiselect && root.popup.opened) { _multiselectReopen = true } } }