mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2025-12-06 06:36:15 +00:00
Merge pull request #896 from eric-handley/feat/improve-emoji-selector
Improve >emoji selector with category drawers
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
64
Widgets/NIconTabButton.qml
Normal file
64
Widgets/NIconTabButton.qml
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user