Merge pull request #896 from eric-handley/feat/improve-emoji-selector

Improve >emoji selector with category drawers
This commit is contained in:
Lysec
2025-11-29 08:06:25 +01:00
committed by GitHub
5 changed files with 12680 additions and 119 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -61,6 +61,9 @@ SmartPanel {
if (searchText.startsWith(">clip") || searchText.startsWith(">calc")) {
return false;
}
if (activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
return true;
}
return Settings.data.appLauncher.viewMode === "grid";
}
@@ -77,7 +80,14 @@ SmartPanel {
// They are not coming from SmartPanelWindow as they are consumed by the search field before reaching the panel.
// They are instead being forwared from the search field NTextInput below.
function onTabPressed() {
selectNextWrapped();
// In emoji browsing mode, Tab navigates between categories
if (activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
var currentIndex = emojiPlugin.categories.indexOf(emojiPlugin.selectedCategory);
var nextIndex = (currentIndex + 1) % emojiPlugin.categories.length;
emojiPlugin.selectCategory(emojiPlugin.categories[nextIndex]);
} else {
selectNextWrapped();
}
}
function onBackTabPressed() {
@@ -616,6 +626,33 @@ SmartPanel {
}
}
// Emoji category tabs (shown when in browsing mode)
NTabBar {
id: emojiCategoryTabs
visible: root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode
Layout.fillWidth: true
currentIndex: {
if (visible && emojiPlugin.categories) {
return emojiPlugin.categories.indexOf(emojiPlugin.selectedCategory);
}
return 0;
}
Repeater {
model: emojiPlugin.categories
NIconTabButton {
required property string modelData
required property int index
icon: emojiPlugin.categoryIcons[modelData] || "star"
tabIndex: index
checked: emojiCategoryTabs.currentIndex === index
onClicked: {
emojiPlugin.selectCategory(modelData);
}
}
}
}
Loader {
id: resultsViewLoader
Layout.fillWidth: true
@@ -774,8 +811,8 @@ SmartPanel {
NText {
id: emojiDisplay
anchors.centerIn: parent
visible: modelData.emojiChar ? true : (!imagePreview.visible && !iconLoader.visible)
text: modelData.emojiChar ? modelData.emojiChar : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?")
visible: modelData.emojiChar || (!imagePreview.visible && !iconLoader.visible)
text: modelData.emojiChar ? modelData.emojiChar : modelData.name.charAt(0).toUpperCase()
pointSize: modelData.emojiChar ? Style.fontSizeXXXL : Style.fontSizeXXL // Larger font for emojis
font.weight: Style.fontWeightBold
color: modelData.emojiChar ? Color.mOnSurface : Color.mOnPrimary // Different color for emojis
@@ -877,8 +914,18 @@ SmartPanel {
width: parent.width
height: parent.height
cellWidth: gridCellSize + Style.marginXXS
cellHeight: gridCellSize + Style.marginXXS
cellWidth: {
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
return parent.width / 5;
}
return gridCellSize + Style.marginXXS;
}
cellHeight: {
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
return (parent.width / 5) * 1.2;
}
return gridCellSize + Style.marginXXS;
}
model: results
cacheBuffer: resultsGrid.height * 2
keyNavigationEnabled: false
@@ -888,9 +935,14 @@ SmartPanel {
onWidthChanged: {
// Update gridColumns based on actual GridView width
// This ensures navigation works correctly regardless of panel size
const actualCols = Math.floor(width / cellWidth);
if (actualCols > 0 && actualCols !== root.gridColumns) {
root.gridColumns = actualCols;
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
// Always 5 columns for emoji browsing mode
root.gridColumns = 5;
} else {
const actualCols = Math.floor(width / cellWidth);
if (actualCols > 0 && actualCols !== root.gridColumns) {
root.gridColumns = actualCols;
}
}
}
@@ -903,6 +955,16 @@ SmartPanel {
onModelChanged: {}
// Update gridColumns when entering/exiting emoji browsing mode
Connections {
target: emojiPlugin
function onIsBrowsingModeChanged() {
if (emojiPlugin.isBrowsingMode) {
root.gridColumns = 5;
}
}
}
// Handle scrolling to show selected item when it changes
Connections {
target: root
@@ -934,8 +996,18 @@ SmartPanel {
property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex)
property string appId: (modelData && modelData.appId) ? String(modelData.appId) : ""
width: gridCellSize
height: gridCellSize
width: {
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
return resultsGrid.width / 5;
}
return gridCellSize;
}
height: {
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
return (resultsGrid.width / 5) * 1.2;
}
return gridCellSize;
}
radius: Style.radiusM
color: gridEntry.isSelected ? Color.mHover : Color.mSurface
@@ -948,13 +1020,28 @@ SmartPanel {
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginM
anchors.margins: {
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
return 4;
}
return Style.marginM;
}
spacing: Style.marginS
// Icon badge or Image preview or Emoji
Rectangle {
Layout.preferredWidth: badgeSize * 1.5
Layout.preferredHeight: badgeSize * 1.5
Layout.preferredWidth: {
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode && modelData.emojiChar) {
return gridEntry.width - 8;
}
return badgeSize * 1.5;
}
Layout.preferredHeight: {
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode && modelData.emojiChar) {
return gridEntry.width - 8;
}
return badgeSize * 1.5;
}
Layout.alignment: Qt.AlignHCenter
radius: Style.radiusM
color: Color.mSurfaceVariant
@@ -1016,9 +1103,17 @@ SmartPanel {
NText {
id: gridEmojiDisplay
anchors.centerIn: parent
visible: modelData.emojiChar ? true : (!gridImagePreview.visible && !gridIconLoader.visible)
text: modelData.emojiChar ? modelData.emojiChar : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?")
pointSize: modelData.emojiChar ? Style.fontSizeXXL : Style.fontSizeXL
visible: modelData.emojiChar || (!gridImagePreview.visible && !gridIconLoader.visible)
text: modelData.emojiChar ? modelData.emojiChar : modelData.name.charAt(0).toUpperCase()
pointSize: {
if (modelData.emojiChar) {
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
return Math.max(Style.fontSizeL, gridEntry.width * 0.4);
}
return Style.fontSizeXXL * 2;
}
return Style.fontSizeXL;
}
font.weight: Style.fontWeightBold
color: modelData.emojiChar ? Color.mOnSurface : Color.mOnPrimary
}
@@ -1027,13 +1122,20 @@ SmartPanel {
// Text content
NText {
text: modelData.name || "Unknown"
pointSize: Style.fontSizeS
pointSize: {
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode && modelData.emojiChar) {
return Style.fontSizeS;
}
return Style.fontSizeS;
}
font.weight: Style.fontWeightSemiBold
color: gridEntry.isSelected ? Color.mOnHover : Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
Layout.maximumWidth: gridCellSize - Style.marginM * 2
Layout.maximumWidth: gridEntry.width - 8
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.NoWrap
maximumLineCount: 1
}
}
@@ -1068,10 +1170,16 @@ SmartPanel {
NText {
Layout.fillWidth: true
text: {
if (results.length === 0)
return searchText ? "No results" : "";
const prefix = activePlugin?.name ? `${activePlugin.name}: ` : "";
return prefix + `${results.length} result${results.length !== 1 ? 's' : ''}`;
if (results.length === 0) {
if (searchText) {
return "No results";
} else if (activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode && emojiPlugin.selectedCategory === "recent") {
return "No recently used emoji";
}
return "";
}
var prefix = activePlugin && activePlugin.name ? activePlugin.name + ": " : "";
return prefix + results.length + " result" + (results.length !== 1 ? 's' : '');
}
pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant

View File

@@ -11,13 +11,30 @@ Item {
property var launcher: null
property bool handleSearch: false
property string selectedCategory: "recent"
property bool isBrowsingMode: false
property var categoryIcons: ({
"recent": "clock",
"people": "user",
"animals": "paw",
"nature": "leaf",
"food": "apple",
"activity": "run",
"travel": "plane",
"objects": "home",
"symbols": "star",
"flags": "flag"
})
property var categories: ["recent", "people", "animals", "nature", "food", "activity", "travel", "objects", "symbols", "flags"]
// Force update results when emoji service loads
Connections {
target: EmojiService
function onLoadedChanged() {
if (EmojiService.loaded && root.launcher) {
// Update launcher results to refresh the UI
root.launcher?.updateResults();
root.launcher.updateResults();
}
}
}
@@ -27,6 +44,18 @@ Item {
Logger.i("EmojiPlugin", "Initialized");
}
function selectCategory(category) {
selectedCategory = category;
if (launcher) {
launcher.updateResults();
}
}
function onOpened() {
// Always reset to "recent" category when opening
selectedCategory = "recent";
}
// Check if this plugin handles the command
function handleCommand(searchText) {
return searchText.startsWith(">emoji");
@@ -65,15 +94,23 @@ Item {
];
}
const query = searchText.slice(6).trim();
const emojis = EmojiService.search(query);
return emojis.map(formatEmojiEntry);
var query = searchText.slice(6).trim();
if (query === "") {
isBrowsingMode = true;
var emojis = EmojiService.getEmojisByCategory(selectedCategory);
return emojis.map(formatEmojiEntry);
} else {
isBrowsingMode = false;
var emojis = EmojiService.search(query);
return emojis.map(formatEmojiEntry);
}
}
// Format an emoji entry for the results list
function formatEmojiEntry(emoji) {
let title = emoji.name;
let description = (emoji.keywords || []).join(", ");
let description = emoji.keywords.join(", ");
if (emoji.category) {
description += " • Category: " + emoji.category;

View File

@@ -33,9 +33,9 @@ Singleton {
const results = emojis.filter(emoji => {
for (let term of terms) {
const emojiMatch = emoji.emoji.toLowerCase().includes(term);
const nameMatch = (emoji.name || "").toLowerCase().includes(term);
const keywordMatch = (emoji.keywords || []).some(kw => kw.toLowerCase().includes(term));
const categoryMatch = (emoji.category || "").toLowerCase().includes(term);
const nameMatch = emoji.name.toLowerCase().includes(term);
const keywordMatch = emoji.keywords.some(kw => kw.toLowerCase().includes(term));
const categoryMatch = emoji.category.toLowerCase().includes(term);
if (!emojiMatch && !nameMatch && !keywordMatch && !categoryMatch) {
return false;
@@ -47,26 +47,69 @@ Singleton {
return results;
}
// Get popular emojis sorted by usage count
function _getPopularEmojis(limit) {
// Create array of emojis with their usage counts
const emojisWithUsage = emojis.map(emoji => {
return {
emoji: emoji,
usageCount: usageCounts[emoji.emoji] || 0
};
});
var emojisWithUsage = emojis.map(function(emoji) {
return {
emoji: emoji,
usageCount: usageCounts[emoji.emoji] || 0
};
}).filter(function(item) {
return item.usageCount > 0;
});
// Sort by usage count (descending), then by name
emojisWithUsage.sort((a, b) => {
if (b.usageCount !== a.usageCount) {
return b.usageCount - a.usageCount;
}
return (a.emoji.name || "").localeCompare(b.emoji.name || "");
});
emojisWithUsage.sort(function(a, b) {
if (b.usageCount !== a.usageCount) {
return b.usageCount - a.usageCount;
}
return a.emoji.name.localeCompare(b.emoji.name);
});
// Return the emoji objects limited by the specified count
return emojisWithUsage.slice(0, limit).map(item => item.emoji);
return emojisWithUsage.slice(0, limit).map(function(item) {
return item.emoji;
});
}
function getCategoriesWithCounts() {
if (!loaded) {
return [];
}
var categoryCounts = {};
for (var i = 0; i < emojis.length; i++) {
var emoji = emojis[i];
var category = emoji.category || "other";
if (!categoryCounts[category]) {
categoryCounts[category] = 0;
}
categoryCounts[category]++;
}
var categories = [];
for (var cat in categoryCounts) {
categories.push({
name: cat,
count: categoryCounts[cat]
});
}
return categories;
}
function getEmojisByCategory(category) {
if (!loaded) {
return [];
}
if (category === "recent") {
return _getPopularEmojis(25);
}
return emojis.filter(function(emoji) {
return emoji.category === category;
});
}
// Record emoji usage

View File

@@ -0,0 +1,64 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
Rectangle {
id: root
// Public properties
property string icon: ""
property bool checked: false
property int tabIndex: 0
// Internal state
property bool isHovered: false
signal clicked
// Sizing
Layout.fillWidth: true
Layout.fillHeight: true
// Styling
radius: Style.radiusXS
color: root.checked ? Color.mPrimary : (root.isHovered ? Color.mHover : Color.mSurface)
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
NIcon {
id: tabIcon
anchors.centerIn: parent
icon: root.icon
pointSize: Style.fontSizeL
color: root.checked ? Color.mOnPrimary : root.isHovered ? Color.mOnHover : Color.mOnSurface
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.isHovered = true
onExited: root.isHovered = false
onClicked: {
root.clicked();
// Update parent NTabBar's currentIndex
if (root.parent && root.parent.parent && root.parent.parent.currentIndex !== undefined) {
root.parent.parent.currentIndex = root.tabIndex;
}
}
}
}