From 95aea608a073ed90164884cea0dae944ae53ffc4 Mon Sep 17 00:00:00 2001 From: fredcw <58893963+fredcw@users.noreply.github.com> Date: Mon, 3 Mar 2025 03:38:08 +0000 Subject: [PATCH 1/3] Add icon-menu appet --- .../cinnamon-sass/widgets/_startmenu.scss | 80 + .../applets/icon-menu@cinnamon.org/applet.js | 1818 ++++ .../icon-menu@cinnamon.org/appsview.js | 568 ++ .../icon-menu@cinnamon.org/categoriesview.js | 418 + .../icon-menu@cinnamon.org/contextmenu.js | 434 + .../applets/icon-menu@cinnamon.org/display.js | 317 + .../applets/icon-menu@cinnamon.org/emoji.js | 8028 +++++++++++++++++ .../icon-menu@cinnamon.org/metadata.json | 7 + .../settings-schema.json | 369 + .../applets/icon-menu@cinnamon.org/sidebar.js | 416 + .../icon-menu@cinnamon.org/stylesheet.css | 54 + .../applets/icon-menu@cinnamon.org/utils.js | 254 + 12 files changed, 12763 insertions(+) create mode 100644 files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/applet.js create mode 100644 files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/appsview.js create mode 100644 files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/categoriesview.js create mode 100644 files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/contextmenu.js create mode 100644 files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/display.js create mode 100644 files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/emoji.js create mode 100644 files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/metadata.json create mode 100755 files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/settings-schema.json create mode 100644 files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/sidebar.js create mode 100755 files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/stylesheet.css create mode 100755 files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/utils.js diff --git a/data/theme/cinnamon-sass/widgets/_startmenu.scss b/data/theme/cinnamon-sass/widgets/_startmenu.scss index c2e3cc5a44..0b9beedeff 100644 --- a/data/theme/cinnamon-sass/widgets/_startmenu.scss +++ b/data/theme/cinnamon-sass/widgets/_startmenu.scss @@ -94,3 +94,83 @@ $menu_outer_border_radius: $base_border_radius * 1.25; .menu-search-entry-icon { icon-size: $scalable_icon_size; } + +// gridmenu (icon-menu) + +.menu-category-button:highlighted, // Used to highlight category containing newly installed apps. +.menu-category-button-selected:highlighted { + font-weight: bold; +} + +.menu-applications-header-text { // shows folder name, "no recent files", error messages etc. + font-weight: bold; + font-size: 110%; +} + +.menu-applications-subheading { + font-weight: bold; + font-size: 100%; + padding-top: 0.3em; + padding-left: 0.3em; + //text-align: center; - known bug: causes label to jump about when resizing menu. +} + +.menu-applications-grid-box { + left-padding: 8px; + + .menu-application-button, + .menu-application-button-selected { + padding: 10px 2px; + } + + .menu-application-button-label { + text-align: center; + } +} + +.gridmenu-sidebar-box { + padding: 0px 6px; +} + +.gridmenu { + .menu-applications-inner-box { + padding: 0px; + } + + .menu-categories-box { + padding: 0px 6px; + } + + .menu-search-box, + .menu-search-box:ltr, + .menu-search-box:rtl { + min-width: 160px; + padding: 0px 8px; + } + + &.sidebar-left { + .gridmenu-sidebar-box { + padding-right: 16px; + } + } + + &.sidebar-right { + .gridmenu-sidebar-box { + padding-left: 8px; + } + } + + &.sidebar-left, + &.sidebar-right, + &.sidebar-bottom { + .gridmenu-middle-pane { + padding-bottom: 12px; + } + } + + &.sidebar-top .gridmenu-middle-pane { + padding-top: 12px; + } +} + + diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/applet.js new file mode 100644 index 0000000000..60f3b7467c --- /dev/null +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/applet.js @@ -0,0 +1,1818 @@ +const Gio = imports.gi.Gio; +const Gtk = imports.gi.Gtk; +const GLib = imports.gi.GLib; +const CMenu = imports.gi.CMenu; +const Clutter = imports.gi.Clutter; +const Cinnamon = imports.gi.Cinnamon; +const XApp = imports.gi.XApp; +const St = imports.gi.St; +const Meta = imports.gi.Meta; +const Main = imports.ui.main; +const Util = imports.misc.util; +const GnomeSession = imports.misc.gnomeSession; +const AppletManager = imports.ui.appletManager; +const {ScreenSaverProxy} = imports.misc.screenSaver; +const {PopupMenuManager} = imports.ui.popupMenu; +const {getAppFavorites} = imports.ui.appFavorites; +const {TextIconApplet, AllowedLayout, AppletPopupMenu, PopupResizeHandler} = imports.ui.applet; +const {SignalManager} = imports.misc.signalManager; +const {launch_all} = imports.ui.searchProviderManager; +const {AppletSettings} = imports.ui.settings; +const Mainloop = imports.mainloop; + +const {graphemeBaseChars, searchStr} = require('./utils'); +const {Display} = require('./display'); +const {EMOJI} = require('./emoji'); +const EMOJI_CODE = 0, EMOJI_NAME = 1, EMOJI_KEYWORDS = 2; +const REMEMBER_RECENT_KEY = 'remember-recent-files'; +const SEARCH_THRESHOLD = 0.45; +const SidebarPlacement = Object.freeze({TOP: 0, BOTTOM: 1, LEFT: 2, RIGHT: 3}); + +/* This graph shows the classes in which other classes are instantiated and how they are + * acessed. e.g. to call the update() method in categoriesView from contextMenu class use: + * this.appThis.display.categoriesView.update() + + ┌── class AppsView ───────┬── class AppButton + │ └── class Subheading + │ + ┌── class Display ────┼── class CategoriesView ──── class CategoryButton + │ │ + │ ├── class Sidebar ────────┬── class SidebarButton + │ │ └── class Separator +class │ │ +CinnamenuApplet ──┼ ├── class ContextMenu ─────── class ContextMenuItem + │ │ + │ └── class SearchView + │ + ├── class Apps + │ + └── class RecentApps + +*/ + +class CinnamenuApplet extends TextIconApplet { + constructor(metadata, orientation, panel_height, instance_id) { + super(orientation, panel_height, instance_id); + this.setAllowedLayout(AllowedLayout.BOTH); + this.privacy_settings = new Gio.Settings({schema_id: 'org.cinnamon.desktop.privacy'}); + this.appFavorites = getAppFavorites(); + this.currentCategory = 'all'; + this.recentManagerDefault = Gtk.RecentManager.get_default(); + this.orientation = orientation; + this.menuManager = new PopupMenuManager(this); + this.menu = new AppletPopupMenu(this, this.orientation); + this.menuManager.addMenu(this.menu); + this.signals = new SignalManager(null); + this.appSystem = Cinnamon.AppSystem.get_default(); + this._canUninstallApps = GLib.file_test("/usr/bin/cinnamon-remove-application", + GLib.FileTest.EXISTS); + this._pamacManagerAvailable = GLib.file_test("/usr/bin/pamac-manager", GLib.FileTest.EXISTS); + this.resizer = new PopupResizeHandler( + this.menu.actor, + () => this.orientation, + (w,h) => this.display.onMenuResized(w,h), + () => this.settings.customMenuWidth * global.ui_scale, + () => this.settings.customMenuHeight * global.ui_scale + ); + this.signals.connect(this.privacy_settings, 'changed::' + REMEMBER_RECENT_KEY, + () => this._onEnableRecentsChange()); + + const refreshDisplay = () => { + // TBD: For some reason the onEnable* settings callbacks get called several times per + // settings change. This is causing the start up category to reset, so throttling this + // function to 250ms prevents excess invocation. + if (!this.lastRenderTime) this.lastRenderTime = 0; + const now = Date.now(); + if ((now - this.lastRenderTime) <= 250) { + return; + } + this.lastRenderTime = now; + + this.display.destroy(); + this.menu.removeAll(); + this.display = new Display(this); + this.display.clearFocusedActors(); + } + + this.signals.connect(Main.themeManager, 'theme-set', () => { + this._updateIconAndLabel(); + Mainloop.timeout_add(0, () => { + refreshDisplay(); + return false; + }); + }); + this.iconTheme = Gtk.IconTheme.get_default(); + this.signals.connect(this.iconTheme, 'changed', () => this._updateIconAndLabel()); + this.signals.connect(this.appSystem, 'installed-changed', () => { + this.apps.installedChanged(); + refreshDisplay(); + }); + this.signals.connect(this.appFavorites, 'changed', () => { + if (this.display) {// Check if display is initialised + this.display.sidebar.populate(); + this.display.updateMenuSize(); + if (this.currentCategory === 'favorite_apps' && !this.searchActive) { + this.setActiveCategory(this.currentCategory); + } + } + }); + this.signals.connect( + this.menu, + 'open-state-changed', + this._onOpenStateToggled.bind(this) + ); + this.signals.connect(this.menu, + 'menu-animated-closed', + this._onMenuClosed.bind(this) + ); + this.apps = new Apps(this.appSystem); + this.screenSaverProxy = new ScreenSaverProxy(); + this.sessionManager = new GnomeSession.SessionManager(); + + const updateKeybinding = () => { + Main.keybindingManager.addHotKey( + 'overlay-key-' + this.instance_id, + this.settings.overlayKey, + () => { + if (Main.overview.visible || Main.expo.visible) return; + if (!this.isOpen) { + this.panel.peekPanel(); + } + this.menu.toggle_with_options(this.settings.enableAnimation); + } + ); + }; + + const updateActivateOnHover = () => { + const openMenu = () => { + if (!this._applet_context_menu.isOpen) { + this.menu.open(this.settings.enableAnimation); + } + }; + + if (this.signals.isConnected('enter-event', this.actor)) { + this.signals.disconnect('enter-event', this.actor); + this.signals.disconnect('leave-event', this.actor); + } + if (this.settings.activateOnHover) { + this.signals.connect(this.actor, 'enter-event', () => { + if (!this.menu.isOpen && !this.openMenuTimeoutId) { + this.openMenuTimeoutId = Mainloop.timeout_add(this.settings.hoverDelayMs, () => openMenu()); + } + }); + this.signals.connect(this.actor, 'leave-event', () => { + if (this.openMenuTimeoutId) { + Mainloop.source_remove(this.openMenuTimeoutId); + this.openMenuTimeoutId = null; + } + }); + } + }; + + this.settings = {}; + this.appletSettings = new AppletSettings(this.settings, __meta.uuid, this.instance_id); + [ + { key: 'categories', value: 'categories', cb: null }, + { key: 'custom-menu-height', value: 'customMenuHeight', cb: null }, + { key: 'custom-menu-width', value: 'customMenuWidth', cb: null }, + { key: 'recent-apps', value: 'recentApps', cb: null }, + { key: 'folder-categories', value: 'folderCategories', cb: null }, + + { key: 'description-placement', value: 'descriptionPlacement', cb: refreshDisplay }, + { key: 'show-sidebar', value: 'showSidebar', cb: refreshDisplay}, + { key: 'sidebar-placement', value: 'sidebarPlacement', cb: refreshDisplay }, + { key: 'sidebar-favorites', value: 'sidebarFavorites', cb: refreshDisplay }, + + { key: 'show-categories', value: 'showCategories', cb: refreshDisplay}, + { key: 'show-places-category', value: 'showPlaces', cb: null}, + { key: 'show-recents-category', value: 'showRecents', cb: this._onEnableRecentsChange }, + { key: 'show-favorite-apps-category', value: 'showFavAppsCategory', cb: null }, + { key: 'show-home-folder-category', value: 'showHomeFolder', cb: this._onShowHomeFolderChange}, + + { key: 'overlay-key', value: 'overlayKey', cb: updateKeybinding }, + { key: 'activate-on-hover', value: 'activateOnHover', cb: updateActivateOnHover }, + { key: 'hover-delay', value: 'hoverDelayMs', cb: updateActivateOnHover }, + { key: 'enable-animation', value: 'enableAnimation', cb: null }, + { key: 'open-on-category', value: 'openOnCategory', cb: null }, + + { key: 'category-click', value: 'categoryClick', cb: null }, + { key: 'enable-autoscroll', value: 'enableAutoScroll', cb: refreshDisplay }, + { key: 'show-hidden-files', value: 'showHiddenFiles', cb: null }, + + { key: 'enable-emoji-search', value: 'enableEmojiSearch', cb: null }, + { key: 'enable-home-folder-search', value: 'searchHomeFolder', cb: null }, + + { key: 'menu-icon-custom', value: 'menuIconCustom', cb: this._updateIconAndLabel }, + { key: 'menu-icon', value: 'menuIcon', cb: this._updateIconAndLabel }, + { key: 'menu-icon-size-custom', value: 'menuIconSizeCustom', cb: this._updateIconAndLabel }, + { key: 'menu-icon-size', value: 'menuIconSize', cb: this._updateIconAndLabel }, + { key: 'menu-label', value: 'menuLabel', cb: this._updateIconAndLabel }, + + { key: 'category-icon-size', value: 'categoryIconSize', cb: refreshDisplay }, + { key: 'apps-grid-icon-size', value: 'appsGridIconSize', cb: refreshDisplay }, + { key: 'sidebar-icon-size', value: 'sidebarIconSize', cb: refreshDisplay } + ].forEach(setting => this.appletSettings.bind( + setting.key, + setting.value, + setting.cb ? setting.cb.bind(this) : null ) + ); + + this.recentApps = new RecentApps(this); + this._onEnableRecentsChange(); + updateActivateOnHover(); + updateKeybinding(); + this.display = new Display(this); + this._updateIconAndLabel(); + } +//----------------TextIconApplet callbacks---------------- + on_orientation_changed(orientation) { + this.orientation = orientation; + if (this.orientation === St.Side.LEFT || this.orientation === St.Side.RIGHT) { + this.hide_applet_label(true); + } else { + this.hide_applet_label(false); + } + this._updateIconAndLabel(); + } + + on_applet_added_to_panel() { + this._onShowHomeFolderChange(); + } + + on_applet_removed_from_panel() { + Main.keybindingManager.removeHotKey('overlay-key-' + this.instance_id); + if (!this.appletSettings) { + return; + } + this.appletSettings.finalize(); + this.signals.disconnectAllSignals(); + this.display.destroy(); + this.menu.destroy(); + } + + on_applet_clicked() { + this.menu.toggle_with_options(this.settings.enableAnimation); + } + + _setStyle() { + // Override js/applet.js so _updateIconAndLabel doesn't have to fight with size changes + // from the panel configuration. This gets called any time set_applet_icon() variants are + // called. + + let icon_type = this._applet_icon.get_icon_type(); + let size; + + if (this.settings.menuIconSizeCustom) { + size = Math.max(Math.min(this.settings.menuIconSize, this.panel.height), 1); + } else { + size = this.getPanelIconSize(icon_type); + } + + if (icon_type === St.IconType.FULLCOLOR) { + this._applet_icon.set_style_class_name('applet-icon'); + } else { + this._applet_icon.set_style_class_name('system-status-icon'); + } + + this._applet_icon.set_icon_size(size); + } +//------------settings callbacks------------- + launchEditor() { + Util.spawnCommandLine('cinnamon-menu-editor'); + } + + _onEnableRecentsChange () { + const recentFilesEnabled = this.privacy_settings.get_boolean(REMEMBER_RECENT_KEY); + this.recentsEnabled = this.settings.showRecents && recentFilesEnabled; + }; + + _updateIconAndLabel() { + try { + if (this.settings.menuIconCustom) { + if (this.settings.menuIcon === '') { + this.set_applet_icon_name(''); + } else if (GLib.path_is_absolute(this.settings.menuIcon) && + GLib.file_test(this.settings.menuIcon, GLib.FileTest.EXISTS)) { + if (this.settings.menuIcon.includes('-symbolic')) { + this.set_applet_icon_symbolic_path(this.settings.menuIcon); + } else { + this.set_applet_icon_path(this.settings.menuIcon); + } + } else if (this.iconTheme.has_icon(this.settings.menuIcon)) { + if (this.settings.menuIcon.includes('-symbolic')) { + this.set_applet_icon_symbolic_name(this.settings.menuIcon); + } else { + this.set_applet_icon_name(this.settings.menuIcon); + } + } + } else { + const icon_name = global.settings.get_string('app-menu-icon-name'); + if (icon_name.search("-symbolic") != -1) { + this.set_applet_icon_symbolic_name(icon_name); + } + else { + this.set_applet_icon_name(icon_name); + } + } + } catch(e) { + global.logWarning('Gridmenu: Could not load icon file ' + this.settings.menuIcon + + ' for menu button'); + } + if (this.settings.menuIconCustom && this.settings.menuIcon === '' || + this.settings.menuIconSizeCustom && this.settings.menuIconSize === 0) { + this._applet_icon_box.hide(); + } else { + this._applet_icon_box.show(); + } + + if (this.orientation === St.Side.LEFT || this.orientation === St.Side.RIGHT) { + this.set_applet_label(''); + } else { + if (!this.settings.menuLabel) { + this.settings.menuLabel = ''; + } + const menuLabel = this.settings.menuLabel.substring(0, 45); + this.set_applet_label(menuLabel); + this.set_applet_tooltip(menuLabel); + } + } + + _onShowHomeFolderChange() { + const homePath = GLib.get_home_dir(); + if (this.settings.showHomeFolder) { + if (!this.getIsFolderCategory(homePath)) { + this.addFolderCategory(homePath); + } + } else { + if (this.getIsFolderCategory(homePath)) { + this.removeFolderCategory(homePath); + } + } + } +//================================================================== + addFavoriteAppToPos(add_id, pos_id) { + const pos = this.appFavorites._getIds().indexOf(pos_id); + if (pos >= 0) { //move + this.appFavorites.moveFavoriteToPos(add_id, pos); + } else { + this.appFavorites.addFavoriteAtPos(add_id, pos); + } + } + + updateAfterXappFavoriteFileChange() { + this.display.sidebar.populate(); + this.display.categoriesView.update();//in case fav files category needs adding/removing + this.display.updateMenuSize(); + if (this.currentCategory === 'favorite_files') { + this.setActiveCategory(this.currentCategory); + } + } + + xappGetIsFavoriteFile(uri) { + const favs = XApp.Favorites.get_default(); + return favs.find_by_uri(uri) !== null; + } + + xappAddFavoriteFile(uri) { + const favs = XApp.Favorites.get_default(); + favs.add(uri); + //xapp favs list doesn't update synchronously after adding fav so add small + //delay before updating menu. + Mainloop.timeout_add(100, this.updateAfterXappFavoriteFileChange.bind(this)); + } + + xappRemoveFavoriteFile(uri) { + const favs = XApp.Favorites.get_default(); + favs.remove(uri); + this.updateAfterXappFavoriteFileChange(); + } + + getIsFolderCategory(path) { + const index = this.settings.folderCategories.indexOf(path); + return index > -1; + } + + addFolderCategory(path) { + const folderCategories = this.settings.folderCategories.slice(); + folderCategories.push(path); + this.settings.folderCategories = folderCategories; + } + + removeFolderCategory(path) { + const folderCategories = this.settings.folderCategories.slice(); + const index = folderCategories.indexOf(path); + if (index != -1) { + folderCategories.splice(index, 1); + } + this.settings.folderCategories = folderCategories; + } + + _onOpenStateToggled(menu, open) { + if (global.settings.get_boolean('panel-edit-mode')) { + return false; + } + if (!open) { + return true; // this._onMenuClosed() is called on 'menu-animated-closed' signal to handle closing. + } + + if (this.openMenuTimeoutId) { + Mainloop.source_remove(this.openMenuTimeoutId); + this.openMenuTimeoutId = null; + } + + this.display.categoriesView.update();//in case menu editor or enabled category changes. + this.display.sidebar.populate();//in case fav files changed + this.display.sidebar.scrollToQuitButton();//ensure quit button is visible + + global.stage.set_key_focus(this.display.searchView.searchEntry); + if (this.currentCategory === 'places' && !this.settings.showPlaces || + this.currentCategory === 'recents' && !this.recentsEnabled || + this.currentCategory === 'favorite_apps' && !this.settings.showFavAppsCategory) { + this.currentCategory = 'all'; + } + let openOnCategory = this.currentCategory; + if (this.settings.openOnCategory === 4 || !this.settings.showCategories) { + openOnCategory = 'all'; + } else if (this.settings.openOnCategory === 1 && this.settings.showFavAppsCategory) { + openOnCategory = 'favorite_apps'; + } else if (this.settings.openOnCategory === 2 && this.recentsEnabled) { + openOnCategory = 'recents'; + } else if (this.settings.openOnCategory === 3 && this.settings.showPlaces) { + openOnCategory = 'places'; + } + + if (!this.resizer._size_restricted) { + this.display.updateMenuSize(); + } + this.setActiveCategory(openOnCategory); + + //Show panel when auto hide is on. + //this.panel.peekPanel(); //no longer works on cinnamon 5.4.x + + //center menu if applet in center zone of top or bottom panel + const appletDefinition = AppletManager.getAppletDefinition({applet_id: this.instance_id}); + if ((this.orientation === St.Side.BOTTOM || this.orientation === St.Side.TOP) && + appletDefinition.location_label === 'center') { + const monitor = Main.layoutManager.findMonitorForActor(this.menu.actor); + this.menu.shiftToPosition(Math.floor(monitor.width / 2) + monitor.x); + } + + //By default, current active category button will have focus. If categories are + //hidden, give focus to first app item. + if (!this.settings.showCategories) { + this.display.appsView.focusFirstItem(); + } + + return true; + } + + _onMenuClosed() { + if (this.searchActive) { + this._endSearchMode(); + } + this.display.clearFocusedActors(); + this.display.appsView.clearApps();//for quicker reopening of menu + } + + _onMenuKeyPress(actor, event) { + if (this.resizer.resizingInProgress) { + return Clutter.EVENT_STOP; + } + + const symbol = event.get_key_symbol(); + const keyCode = event.get_key_code(); + const modifierState = Cinnamon.get_event_state(event); + + /* check for a keybinding and quit early, otherwise we get a double hit + of the keybinding callback */ + const action = global.display.get_keybinding_action(keyCode, modifierState); + if (action === Meta.KeyBindingAction.CUSTOM) { + return Clutter.EVENT_PROPAGATE; + } + + const ctrlKey = modifierState === 4; + const shiftKey = modifierState === 1; + const altKey = modifierState === 8; + //const altgrKey = modifierState === 128; + const noModifiers = modifierState === 0; + + //Because Clutter.EVENT_PROPAGATE is returned on KEY_Left and KEY_Right, ignore duplicate + //event emitted by ibus. https://github.com/linuxmint/cinnamon-spices-applets/issues/3294 + if (!this.lastKeyEventTime) this.lastKeyEventTime = 0; + const now = Date.now(); + if ((symbol === Clutter.KEY_Left || symbol === Clutter.KEY_KP_Left + || symbol === Clutter.KEY_Right || symbol === Clutter.KEY_KP_Right) + && noModifiers && (now - this.lastKeyEventTime) <= 80) { + return Clutter.EVENT_PROPAGATE; + } + this.lastKeyEventTime = now; + + const contextMenuButtons = this.display.contextMenu.contextMenuButtons; + const appButtons = this.display.appsView.getActiveButtons(); + const sidebarButtons = this.display.sidebar.getButtons(); + const categoryButtons = this.display.categoriesView.buttons; + + const focusedContextMenuItemIndex = this.display.contextMenu.getCurrentlyFocusedMenuItem(); + let focusedAppItemIndex = appButtons.findIndex(button => button.has_focus); + const focusedSidebarItemIndex = sidebarButtons.findIndex(button => button.has_focus); + //When "activate categories on click" option is set, currentlyActiveCategoryIndex and + //focusedCategoryIndex may not be the same. + const focusedCategoryIndex = categoryButtons.findIndex(button => button.has_focus); + + let currentlyActiveCategoryIndex = categoryButtons.findIndex(button => + this.currentCategory === button.id); + if (currentlyActiveCategoryIndex < 0) { + currentlyActiveCategoryIndex = 0; + } + + const focusedContextMenuItemExists = focusedContextMenuItemIndex > -1; + let focusedAppItemExists = focusedAppItemIndex > -1; + const focusedSidebarItemExists = focusedSidebarItemIndex > -1; + const focusedCategoryExists = focusedCategoryIndex > -1; + + if (!focusedContextMenuItemExists && !focusedAppItemExists && + !focusedSidebarItemExists && !focusedCategoryExists) { + //todo: No focused item, ideally this shouldn't happen + if (appButtons[0]) { + appButtons[0].handleEnter(); + focusedAppItemIndex = 0; + focusedAppItemExists = true; + } + } + + const leaveCurrentlyFocusedItem = () => { + if (focusedContextMenuItemExists) { + contextMenuButtons[focusedContextMenuItemIndex].handleLeave(); + } else if (focusedAppItemExists) { + appButtons[focusedAppItemIndex].handleLeave(); + } else if (focusedSidebarItemExists) { + sidebarButtons[focusedSidebarItemIndex].handleLeave(); + } else if (focusedCategoryExists) { + categoryButtons[focusedCategoryIndex].removeFocusAndHover(); + } + }; + + let tabRight = () => { + if (focusedContextMenuItemExists) { + //effectively ignore keypress + contextMenuButtons[focusedContextMenuItemIndex].handleEnter(); + } else if (!this.searchActive && this.settings.showCategories && + (focusedSidebarItemExists || + focusedAppItemExists && !this.settings.showSidebar)) { + categoryButtons[currentlyActiveCategoryIndex].handleEnter(); + } else if (focusedAppItemExists && this.settings.showSidebar) { + sidebarButtons[0].handleEnter(); + } else { + appButtons[0].handleEnter(); + } + } + + let tabLeft = () => { + if (focusedContextMenuItemExists) { + //effectively ignore keypress + contextMenuButtons[focusedContextMenuItemIndex].handleEnter(); + } else if (focusedAppItemExists && !this.searchActive && + this.settings.showCategories) { + categoryButtons[currentlyActiveCategoryIndex].handleEnter(); + } else if (this.settings.showSidebar && (focusedCategoryExists || + focusedAppItemExists && (this.searchActive || !this.settings.showCategories))) { + sidebarButtons[0].handleEnter(); + } else { + appButtons[0].handleEnter(); + } + } + + if (St.Widget.get_default_direction() === St.TextDirection.RTL) { + [tabRight, tabLeft] = [tabLeft, tabRight]; + } + + const getNextSidebarItemIndex = () => { + if (focusedSidebarItemIndex < sidebarButtons.length - 1) { + return focusedSidebarItemIndex + 1; + } else { + return 0; + } + }; + + const getPreviousSidebarItemIndex = () => { + if (focusedSidebarItemIndex === 0) { + return sidebarButtons.length -1; + } else { + return focusedSidebarItemIndex - 1; + } + }; + + const leftNavigation = () => { + if (focusedContextMenuItemExists) { + contextMenuButtons[focusedContextMenuItemIndex].handleEnter();//effectively ignore + } else if (focusedAppItemExists) { + if (focusedAppItemIndex > 0) { + appButtons[focusedAppItemIndex - 1].handleEnter(); + } else { + appButtons[appButtons.length - 1].handleEnter(); + } + } else if (focusedSidebarItemExists) { + if (this.settings.sidebarPlacement === SidebarPlacement.LEFT || + this.settings.sidebarPlacement === SidebarPlacement.RIGHT) { + tabLeft(); + } else { + sidebarButtons[getPreviousSidebarItemIndex()].handleEnter(); + } + } else if (focusedCategoryExists) { + tabLeft(); + } + }; + + const rightNavigation = () => { + if (focusedContextMenuItemExists) { + contextMenuButtons[focusedContextMenuItemIndex].handleEnter();//effectively ignore keypress + } else if (focusedAppItemExists) { + if (appButtons[focusedAppItemIndex + 1]) { + appButtons[focusedAppItemIndex + 1].handleEnter(); + } else { + appButtons[0].handleEnter(); + } + } else if (focusedSidebarItemExists) { + if (this.settings.sidebarPlacement === SidebarPlacement.LEFT || + this.settings.sidebarPlacement === SidebarPlacement.RIGHT) { + tabRight();; + } else { + sidebarButtons[getNextSidebarItemIndex()].handleEnter(); + } + } else if (focusedCategoryExists) { + tabRight(); + } + }; + + const downNavigation = () => { + if (focusedContextMenuItemExists) { + let nextContextMenuItem = focusedContextMenuItemIndex + 1; + while (!contextMenuButtons[nextContextMenuItem] || + contextMenuButtons[nextContextMenuItem].action === null) { + nextContextMenuItem++; + if (nextContextMenuItem >= contextMenuButtons.length) { + nextContextMenuItem = 0; + } + } + contextMenuButtons[nextContextMenuItem].handleEnter(); + } else if (focusedAppItemExists) { + if (appButtons[focusedAppItemIndex + 1]) { + const column = appButtons[focusedAppItemIndex].actor.layout_column; + let next = focusedAppItemIndex + 1; + while (appButtons[next].actor.layout_column != column && appButtons[next + 1]) { + next++; + } + appButtons[next].handleEnter(); + } else { + appButtons[focusedAppItemIndex].handleEnter();//effectively no change + } + } else if (focusedSidebarItemExists) { + if (this.settings.sidebarPlacement === SidebarPlacement.TOP) { + tabRight(); + } else if (this.settings.sidebarPlacement === SidebarPlacement.BOTTOM) { + tabLeft(); + } else { + sidebarButtons[getNextSidebarItemIndex()].handleEnter(); + } + } else if (focusedCategoryExists) { + if (categoryButtons[focusedCategoryIndex + 1]) { + categoryButtons[focusedCategoryIndex + 1].handleEnter(); + } else { + categoryButtons[0].handleEnter(); + } + } + }; + + const upNavigation = () => { + if (focusedContextMenuItemExists) { + let previousContextMenuItem = focusedContextMenuItemIndex - 1; + while (!contextMenuButtons[previousContextMenuItem] || + contextMenuButtons[previousContextMenuItem].action === null) { + previousContextMenuItem--; + if (previousContextMenuItem < 0) { + previousContextMenuItem = contextMenuButtons.length -1; + } + } + contextMenuButtons[previousContextMenuItem].handleEnter(); + } else if (focusedAppItemExists) { + if (focusedAppItemIndex > 0) { + const column = appButtons[focusedAppItemIndex].actor.layout_column; + let previous = focusedAppItemIndex - 1; + while (appButtons[previous].actor.layout_column != column && previous > 0) { + previous--; + } + appButtons[previous].handleEnter(); + } else { + appButtons[0].handleEnter();//effectively no change + } + } else if (focusedSidebarItemExists) { + if (this.settings.sidebarPlacement === SidebarPlacement.TOP) { + tabLeft(); + } else if (this.settings.sidebarPlacement === SidebarPlacement.BOTTOM) { + tabRight(); + } else { + sidebarButtons[getPreviousSidebarItemIndex()].handleEnter(); + } + } else if (focusedCategoryExists) { + if (focusedCategoryIndex > 0) { + categoryButtons[focusedCategoryIndex - 1].handleEnter(); + } else { + categoryButtons[categoryButtons.length - 1].handleEnter(); + } + } + }; + + switch (true) { + case (symbol === Clutter.KEY_KP_Enter || symbol === Clutter.KP_Enter || + symbol === Clutter.KEY_Return) && ctrlKey: + case symbol === Clutter.KEY_Menu && noModifiers: + if (this.display.contextMenu.isOpen) { + this.display.contextMenu.close(); + } else if (focusedAppItemExists) { + appButtons[focusedAppItemIndex].openContextMenu(); + } else if (focusedSidebarItemExists) { + sidebarButtons[focusedSidebarItemIndex].openContextMenu(); + } else if (focusedCategoryExists) { + categoryButtons[focusedCategoryIndex].openContextMenu(); + } + return Clutter.EVENT_STOP; + case (symbol === Clutter.KP_Enter || symbol === Clutter.KEY_KP_Enter || + symbol === Clutter.KEY_Return) && noModifiers: + if (focusedContextMenuItemExists) { + contextMenuButtons[focusedContextMenuItemIndex].activate(); + } else if (focusedAppItemExists) { + appButtons[focusedAppItemIndex].activate(); + } else if (focusedSidebarItemExists) { + sidebarButtons[focusedSidebarItemIndex].activate(); + } else if (focusedCategoryExists) { + categoryButtons[focusedCategoryIndex].selectCategory(); + } + return Clutter.EVENT_STOP; + case symbol === Clutter.unicode_to_keysym("p".charCodeAt(0)) && ctrlKey: + if (focusedAppItemExists && appButtons[focusedAppItemIndex].app.isApplication) { + const desktop_file_path = appButtons[focusedAppItemIndex].app.desktop_file_path; + Util.spawn(['cinnamon-desktop-editor', '-mlauncher', '-o' + desktop_file_path]); + this.menu.close(); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE + case (symbol === Clutter.KEY_Up || symbol === Clutter.KEY_KP_Up) && noModifiers: + leaveCurrentlyFocusedItem(); + upNavigation(); + return Clutter.EVENT_STOP; + case (symbol === Clutter.KEY_Down || symbol === Clutter.KEY_KP_Down) && noModifiers: + leaveCurrentlyFocusedItem(); + downNavigation(); + return Clutter.EVENT_STOP; + case (symbol === Clutter.KEY_Right || symbol === Clutter.KEY_KP_Right) && noModifiers: + leaveCurrentlyFocusedItem(); + rightNavigation(); + return Clutter.EVENT_PROPAGATE; // so that left/right can also be used to + // navigate search entry + case (symbol === Clutter.KEY_Left || symbol === Clutter.KEY_KP_Left) && noModifiers: + leaveCurrentlyFocusedItem(); + leftNavigation(); + return Clutter.EVENT_PROPAGATE; // so that left/right can also be used to + // navigate search entry + case (symbol === Clutter.Tab || symbol === Clutter.KEY_Tab) && noModifiers: + leaveCurrentlyFocusedItem(); + if (St.Widget.get_default_direction() === St.TextDirection.RTL) { + //If direction is RTL then tabLeft() and tabRight() have already been swapped to + //account for layout change and so that arrow keys work correctly. But tab key is + //expected move to the left in RTL languages so swap again here. + tabLeft(); + } else { + tabRight(); + } + return Clutter.EVENT_STOP; + case (symbol === Clutter.KEY_ISO_Left_Tab || symbol === Clutter.ISO_Left_Tab || + (symbol === Clutter.Tab || symbol === Clutter.KEY_Tab) && shiftKey): + leaveCurrentlyFocusedItem(); + if (St.Widget.get_default_direction() === St.TextDirection.RTL) { + tabRight(); + } else { + tabLeft() + } + return Clutter.EVENT_STOP; + case symbol === Clutter.Tab && altKey: + this.menu.close();//Close menu as alt-tab is used for app-switcher in cinnamon + return Clutter.EVENT_STOP; + case (symbol === Clutter.Escape || symbol === Clutter.KEY_Escape) && noModifiers: + if (this.display.contextMenu.isOpen) { + this.display.contextMenu.close(); + } else { + this.menu.close(); + } + return Clutter.EVENT_STOP; + case symbol === Clutter.KEY_Page_Up && noModifiers: + leaveCurrentlyFocusedItem(); + if (focusedAppItemExists) { + appButtons[0].handleEnter(); + } else if (focusedSidebarItemExists) { + sidebarButtons[0].handleEnter(); + } else { + categoryButtons[0].handleEnter(); + } + return Clutter.EVENT_STOP; + case symbol === Clutter.KEY_Page_Down && noModifiers: + leaveCurrentlyFocusedItem(); + if (focusedAppItemExists) { + appButtons[appButtons.length - 1].handleEnter(); + } else if (focusedSidebarItemExists) { + sidebarButtons[sidebarButtons.length - 1].handleEnter(); + } else { + categoryButtons[categoryButtons.length - 1].handleEnter(); + } + return Clutter.EVENT_STOP; + default: + return Clutter.EVENT_PROPAGATE; + } + } + + getNumberOfItemsToFitColumns(minimumItems) { + //adjust number of items according to number of columns to make + //best use of available space. + const columns = this.display.appsView.getGridValues().columns; + return Math.ceil(minimumItems / columns) * columns; + } + + setActiveCategory(categoryId) { + // categoryId is one of 3 things: a special category (one of 'places', 'recents', + // 'favorite_files' or 'favorite_apps'), an application category id, + // or an absolute path used in folderview (must begin with a /) + this.currentCategory = categoryId; + this.display.categoriesView.setSelectedCategoryStyle(categoryId); + this.display.categoriesView.setCategoryFocus(categoryId); + this.display.appsView.buttonStoreCleanup(); + + switch (categoryId) { + case 'places': + this.display.appsView.populate(this.listPlaces()); + break; + case 'recents': + const maxItems = this.getNumberOfItemsToFitColumns(6); + const maxRecentApps = this.getNumberOfItemsToFitColumns(4); + + this.display.appsView.populate_init(); + const recentApps = this.listRecent_apps(maxRecentApps); + if (recentApps.length > 0) { + this.display.appsView.populate_add(recentApps,_('Applications')); + } + const recentDocs = this.listRecentByType('documents', maxItems); + if (recentDocs.length > 0) { + this.display.appsView.populate_add(recentDocs,_('Documents')); + } + const recentVids = this.listRecentByType('video', maxItems); + if (recentVids.length > 0) { + this.display.appsView.populate_add(recentVids,_('Videos')); + } + const recentPics = this.listRecentByType('image', maxItems); + if (recentPics.length > 0) { + this.display.appsView.populate_add(recentPics,_('Images')); + } + const recentAudio = this.listRecentByType('audio', maxItems); + if (recentAudio.length > 0) { + this.display.appsView.populate_add(recentAudio,_('Music')); + } + const totalItems = recentApps.length + recentDocs.length + recentVids.length + + recentPics.length + recentAudio.length; + if (totalItems > 0) { + this.display.appsView.populate_add(this.getClearRecentsButton()); + } + this.display.appsView.populate_finish(); + if (totalItems == 0) { + this.display.appsView.populate([], _('No recent Items')); + } + break; + case 'favorite_files': + this.display.appsView.populate(this.listFavoriteFiles()); + break; + case 'favorite_apps': + this.display.appsView.populate(this.listFavoriteApps()); + break; + default: + if (categoryId.startsWith('/')) {//folder view + const folderContents = this.listFolder(categoryId); + const headerText = folderContents.errorMsg? folderContents.errorMsg : categoryId; + this.display.appsView.populate(folderContents.results, headerText); + } else if (categoryId === 'all') { + this.display.appsView.populate_init(); + this.display.appsView.populate_add(this.apps.listApplications('allApps')); + this.display.appsView.populate_add( + this.apps.listApplications('allSettings'), _("Settings")); + this.display.appsView.populate_finish(); + } else {//other applications categories + this.display.appsView.populate(this.apps.listApplications(categoryId)); + } + } + } +//==============search============== + _onSearchTextChanged() { + const searchText = this.display.searchView.searchEntryText.get_text(); + + if (searchText.length === 0) {//search text deleted, cancel search mode + if (!this.searchActive) {//search mode already ended + return; + } + this._endSearchMode(); + this.setActiveCategory(this.currentCategory); + + //By default, current active category button will have focus. If categories are + //hidden, give focus to first app item. + if (!this.settings.showCategories) { + this.display.appsView.focusFirstItem(); + } + return; + } + + //---start search--- + + //Set a new search ID so that async search functions + //from a previous search can be aborted. + this.currentSearchId = Math.floor(Math.random() * 100000000); + + this.display.clearFocusedActors(); + if (!this.searchActive) {//set search mode + this.searchActive = true; + this.display.searchView.showAndConnectSecondaryIcon();//show edit-delete icon + this.display.categoriesView.buttons.forEach(button => button.disable()); + } + + // When doSearch() below is called by Meta.later_add, this.currentSearchId may have changed + // so store its current value in a const as the current lexical scope is preserved. + const currentSearchId = this.currentSearchId; + Meta.later_add(Meta.LaterType.IDLE, () => this._doSearch(searchText, currentSearchId)); + } + + _endSearchMode() { + this.searchActive = false; + this.display.searchView.hideAndDisconnectSecondaryIcon();//hide edit-delete icon + this.display.categoriesView.buttons.forEach(button => button.enable()); + this.display.searchView.searchEntry.set_text(''); + this.previousSearchPattern = ''; + } + + _doSearch(pattern_raw, thisSearchId) { + //this function has been called asynchronously meaning that a keypress may have changed the + //search query before this function is called. Check that this search is still valid. + if (!this.searchActive || thisSearchId !== this.currentSearchId) { + return; + } + + const pattern = graphemeBaseChars(pattern_raw).toLocaleUpperCase().trim(); + + //Don't repeat the same search. This can happen if a key and backspace are pressed in quick + //succession while a previous search is being carried out. + if (pattern_raw === this.previousSearchPattern) { + return; + } + this.previousSearchPattern = pattern_raw; + + let EMOJI_PREFIX = false; + let FILE_PREFIX = false; + if (pattern.length > 2) { + if (pattern.startsWith('E ')) { + EMOJI_PREFIX = true; + } + if (pattern.startsWith('F ')) { + FILE_PREFIX = true; + } + } + const PREFIX_USED = EMOJI_PREFIX || FILE_PREFIX; + + //======Begin search=========== + let applicationResults = []; + let fileResults = []; + let otherResults = []; + if (!PREFIX_USED) { + applicationResults = this.apps.searchApplications(pattern); + fileResults = this.searchFavoriteFiles(pattern) + .concat(this.recentsEnabled ? this.searchRecent(pattern) : []); + otherResults = this.settings.showPlaces ? this.searchPlaces(pattern) : []; + } + const emojiResults = []; + + //----- + + const showResults = () => {//sort and display all search results + if (!this.searchActive || thisSearchId != this.currentSearchId){ + return; //Search mode has ended or search string has changed + } + + // sort applicationResults[] + applicationResults.sort((a, b) => b.score - a.score); + + // sort fileResults[] + fileResults.sort((a, b) => b.score - a.score); // items with equal score are left in + // existing order + + if (fileResults.length > 25) { // remove poor results to save time. + fileResults.length = 25; + } + // Remove duplicate fileResults[]. eg. a fav file, a recent file and a folderfile might all + // be the same file. Prefer from highest to lowest: isFavoriteFile, isRecentFile, + // isFolderviewFile which is easy because fileResults[] should already be in this order. + for (let i = 0; i < fileResults.length -1; i++) { + const app = fileResults[i]; + if (app.isFavoriteFile || app.isRecentFile) { + for (let r = i + 1; r < fileResults.length; r++) { + const compareApp = fileResults[r]; + if ((compareApp.isRecentFile || compareApp.isFolderviewFile) + && compareApp.uri === app.uri) { + fileResults.splice(r, 1); + r--; + } + } + } + } + + // Limit applicationResults to 6 + applicationResults.length = Math.min(applicationResults.length, this.getNumberOfItemsToFitColumns(6)); + // Limit fileResults to 10 + fileResults.length = Math.min(fileResults.length, this.getNumberOfItemsToFitColumns(10)); + + // Display results + this.display.appsView.populate_init(calculatorResult); + if (applicationResults.length > 0) { + this.display.appsView.populate_add(applicationResults, _('Applications')); + } + if (fileResults.length > 0) { + this.display.appsView.populate_add(fileResults, _('Files')); + } + if (otherResults.length > 0) { + this.display.appsView.populate_add(otherResults, _('Other search results')); + } + if (emojiResults.length > 0) { + this.display.appsView.populate_add(emojiResults, _('Emoji')); + } + this.display.appsView.populate_finish(); + + //In case mouse is hovering a different item (thus selecting it) ensure first result + //is highlighted after drawing so that pressing return selects top result. + Meta.later_add(Meta.LaterType.IDLE, () => this.display.appsView.focusFirstItem()); + }; + + //=======search providers========== + //---calculator--- + let calculatorResult = null; + let ans = null; + const exp = pattern_raw.replace(/([a-zA-Z][a-zA-Z0-9_]*)/g, (match) => `Math.${match}`); + + try { + ans = eval?.(`"use strict"; ${exp}`); + } catch(e) { + const probablyMath = /[\(\)\+=/\*]/.test(exp); + if (probablyMath) { + calculatorResult = _("Calculator: ") + e.message; + } + } + + if ((typeof ans === 'number' || typeof ans === 'boolean' || typeof ans === 'bigint') + && ans != pattern_raw ) { + + let ans_str = ans.toString(); + //remove rounding error + if (typeof ans === 'number') { + if (ans > Number.MAX_SAFE_INTEGER || ans < Number.MIN_SAFE_INTEGER) { + // JS will show up to 21 digits of an integer (inaccurately) even though + // only 16 are significant, so show in exponential form instead. + ans_str = Number(ans.toPrecision(16)).toExponential(); + } else { + ans_str = Number(ans.toPrecision(16)).toString(); + } + } + + otherResults.push({ + isSearchResult: true, + name: ans_str, + description: _('Click to copy'), + deleteAfterUse: true, + icon: new St.Icon({ + icon_name: 'accessories-calculator', + icon_type: St.IconType.FULLCOLOR, + icon_size: this.settings.appsGridIconSize + }), + activate: () => { + const clipboard = St.Clipboard.get_default(); + clipboard.set_text(St.ClipboardType.CLIPBOARD, ans_str); + } + }); + calculatorResult = pattern_raw + " = " + ans_str; + } + + //---emoji search------ + if (pattern.length > 2 && this.settings.enableEmojiSearch && !PREFIX_USED || + EMOJI_PREFIX && pattern.length >= 4) { + let epattern = pattern; + if (EMOJI_PREFIX) { + epattern = pattern.substring(2); + } + + EMOJI.forEach(emoji => { + const nameScore = searchStr(epattern, emoji[EMOJI_NAME], true); + let keywordScore = searchStr(epattern, emoji[EMOJI_KEYWORDS], true); + keywordScore *= 0.95; //slightly lower priority for keyword match + const bestScore = Math.max(nameScore, keywordScore); + if (bestScore > SEARCH_THRESHOLD) { + emojiResults.push({ + name: emoji[EMOJI_NAME], + score: bestScore, + description: _('Click to copy'), + isSearchResult: true, + deleteAfterUse: true, + emoji: emoji[EMOJI_CODE], + activate: () => { + const clipboard = St.Clipboard.get_default(); + clipboard.set_text(St.ClipboardType.CLIPBOARD, emoji[EMOJI_CODE]); + } + }); + } + }); + + emojiResults.sort((a, b) => a.score < b.score); + if (emojiResults.length > 36) { + emojiResults.length = 36; + } + } + + //----home folder search-------- + Meta.later_add(Meta.LaterType.IDLE, () => { + if (!(pattern.length > 1 && this.settings.searchHomeFolder && !PREFIX_USED || + FILE_PREFIX && pattern.length >= 3)) { + return; + } + if (!this.searchActive || thisSearchId !== this.currentSearchId) { + return; + } + + let fpattern = pattern; + if (FILE_PREFIX) { + fpattern = pattern.substring(2); + } + // Call function searchNextDir() consecutively and asynchronously on each folder to be searched so + // that search can be interupted at any time. Starting with home folder, all folders to be + // searched are added to foldersToDo[]. Searching is cancelled when the search string has + // changed (thisSearchId !== this.currentSearchId). + + let updateInterval = 100;//update the results after the first 100ms even if search hasn't finished + const MAX_FOLDERS_TO_SEARCH = 50000; + const FOLLOW_SYMLINKS = false; + const FILE_SEARCH_DEBUG = false; + const results = []; + const foldersToDo = []; + foldersToDo.push(GLib.get_home_dir());// start search in home directory + let foldersSearched = 0; + let lastUpdateTime = Date.now(); + const total_timer=Date.now(); + + const searchNextDir = (thisSearchId) => { + const folder = foldersToDo.pop(); + + const dir = Gio.file_new_for_path(folder); + let enumerator; + let timer=Date.now(); + if (FILE_SEARCH_DEBUG) { + log("searching: " + folder); + } + + dir.enumerate_children_async('standard::name,standard::type,standard::is-symlink', + Gio.FileQueryInfoFlags.NONE, GLib.PRIORITY_DEFAULT, null, (source, result) => { + try { + enumerator = source.enumerate_children_finish(result); + } catch(e) { + global.logWarning('gridmenu file search:' + e.message); + } + if (!this.searchActive || thisSearchId !== this.currentSearchId) { + if (enumerator) { + enumerator.close(null); + } + return; + } + + //find matching files and folders in directory + if (enumerator) { + enumerator.next_files_async(1000, GLib.PRIORITY_DEFAULT, null, (source, result) => { + let fileInfos; + try { + fileInfos = source.next_files_finish(result); + } catch(e) { + global.logWarning('Cinnamenu file search:' + e.message); + } + if (!this.searchActive || thisSearchId !== this.currentSearchId) return; + if (fileInfos) { + fileInfos.forEach((fileInfo) => { + const filename = fileInfo.get_name(); + if (filename.startsWith(".")) { + return; // skip hidden files + } + const isDirectory = fileInfo.get_file_type() === Gio.FileType.DIRECTORY; + const filePath = folder + (folder === '/' ? '' : '/') + filename; + let matchScore = searchStr(fpattern, filename, true, true); + if (matchScore > 1) { //any word boundary match + const file = Gio.file_new_for_path(filePath); + const extraFileInfo = file.query_info('standard::icon,standard::content-type', + Gio.FileQueryInfoFlags.NONE, null); + matchScore -= 0.01; + //if file then treat as isFolderviewFile and if directory then treat as isPlace + const foundFile = { + name: filename, + score: matchScore * (fpattern.length > 2 ? 1 : 0.9), + gicon: extraFileInfo.get_icon(), + uri: file.get_uri(), + mimeType: extraFileInfo.get_content_type(), + description: filePath, + isPlace: isDirectory, + isDirectory: isDirectory, + isFolderviewFile: !isDirectory, + deleteAfterUse: true + }; + if (isDirectory) { + const defaultInfo = + Gio.AppInfo.get_default_for_type('inode/directory', false); + if (defaultInfo) { + foundFile.activate = () => { defaultInfo.launch([file], null); }; + } + } + results.push(foundFile); + } + + //Add subdirectories to foldersToDo[] + if (isDirectory && (!fileInfo.get_is_symlink() || FOLLOW_SYMLINKS) && + foldersSearched < MAX_FOLDERS_TO_SEARCH) { + foldersToDo.push(filePath); + } + });//end forEach + }//end if + + if (FILE_SEARCH_DEBUG) { + log("todo: " + foldersToDo.length + " done: " + foldersSearched + " time: " + (Date.now() - timer) + " : total " + (Date.now() - total_timer)); + } + //update display of results at intervals or when search completed + if (foldersToDo.length === 0 || Date.now() - lastUpdateTime > updateInterval) { + if (results.length > 0 && this.searchActive && + thisSearchId === this.currentSearchId) { + fileResults = fileResults.concat(results); + showResults(); + results.length = 0; + } + lastUpdateTime = Date.now(); + updateInterval *= 2;//progressively longer update intervals + } + + //continue search if not completed + if (foldersToDo.length > 0) { + foldersSearched++; + Meta.later_add(Meta.LaterType.IDLE, () => { searchNextDir(thisSearchId); }); + } + if (enumerator) { + enumerator.close(null); + } + }); //end of next_files_async + }//end if + });//end of enumerate_children_async + };// end searchNextDir() + + searchNextDir(this.currentSearchId); + }); + + ///----search providers-------- + Meta.later_add(Meta.LaterType.IDLE, () => { + if (!this.searchActive || thisSearchId !== this.currentSearchId) { + return; + } + launch_all(pattern, (provider, providerResults) => { + providerResults.forEach(providerResult => { + if (!providerResult) { + return; + } + providerResult.isSearchResult = true; + providerResult.name = providerResult.label.replace(/ : /g, ': '); + providerResult.activate = provider.on_result_selected; + providerResult.deleteAfterUse = true; + //providerResult.score = 0.2; + if (providerResult.icon) { + providerResult.icon.icon_size = this.settings.appsGridIconSize; + } else if (providerResult.icon_app) { + providerResult.icon = providerResult.icon_app.create_icon_texture( + this.settings.appsGridIconSize); + } else if (providerResult.icon_filename) { + providerResult.icon = new St.Icon({ + gicon: new Gio.FileIcon({ + file: Gio.file_new_for_path(providerResult.icon_filename)}), + icon_size: this.settings.appsGridIconSize + }); + } + }); + if (!this.searchActive || thisSearchId !== this.currentSearchId || + !providerResults || providerResults.length === 0) { + return; + } + otherResults = otherResults.concat(providerResults); + showResults(); + }); + }); + + showResults(); + return; + } + +/* Below are all functions creating arrays of app objects excluding _doSearch() and + * listApplications() which is in Apps class. Arrays of app objs are then passed + * to AppsView.populate() which creates AppButtons with .app as a property. + * + * app obj properties used: + * .name + * .description + * .id + * .uri + * .mimeType + * .icon + * .gicon + * .iconFactory() + * .score + * .desktop_file_path + * .isApplication + * .isPlace + * .isRecentFile + * .isClearRecentsButton + * .isFavoriteFile //Nemo favorites + * .isFolderviewFile + * .isDirectory + * .isBackButton + * .isSearchResult + * .deleteAfterUse + * .emoji + * .activate() + */ + + listFavoriteApps() { + const res = this.appFavorites.getFavorites(); + res.forEach(favApp => { + favApp.name = favApp.get_name(); + favApp.description = favApp.get_description(); + favApp.isApplication = true; + }); + return res; + } + + searchRecent(pattern) { + const res = []; + + this.listRecentByType('all', 100).forEach(recentItem => { + const score = searchStr(pattern, recentItem.name); + if (recentItem.name && score > SEARCH_THRESHOLD) { + recentItem.score = score; + res.push(recentItem); + } + }); + + return res; + } + + listRecent_apps(maxRecentItems) { + const res = []; + + this.recentApps.getApps(maxRecentItems).forEach(recentId => { + const app = this.apps.listApplications('all').find(app => app.id === recentId); + if (app) {//Check because app may have been uninstalled + res.push(app); + } + }); + + return res; + } + + listRecentByType(type, maxItems) { + //param "type" is one of all|documents|video|image|audio. + const res = []; + this.recentManagerDefault.get_items().forEach(recentInfo => { + if (type === 'documents' && ( recentInfo.get_mime_type().startsWith('video') || + recentInfo.get_mime_type().startsWith('image') || + recentInfo.get_mime_type().startsWith('audio') )) { + return; + } + if ((type === 'video' || type === 'image' || type === 'audio') && + !recentInfo.get_mime_type().startsWith(type)) { + return; + } + + const new_recent = { + name: recentInfo.get_display_name(), + gicon: recentInfo.get_gicon(), + uri: recentInfo.get_uri(), + mimeType: recentInfo.get_mime_type(), + description: recentInfo.get_uri_display(), + modifiedTime: recentInfo.get_modified(),//only used for sorting below + isRecentFile: true, + deleteAfterUse: true + }; + res.push(new_recent); + }); + res.sort((a, b) => a.modifiedTime < b.modifiedTime); + if (res.length > maxItems) { + res.length = maxItems; + } + return res; + } + + getClearRecentsButton() { + const res = []; + + const clearRecentsButton = + this.display.appsView.buttonStore.find(button => button.app.isClearRecentsButton); + if (clearRecentsButton) { + res.push(clearRecentsButton.app); + } else { + res.push({ + name: _('Clear List'), + description: '', + icon: new St.Icon({ + icon_name: 'edit-clear', + icon_type: St.IconType.SYMBOLIC, + icon_size: this.settings.appsGridIconSize + }), + isClearRecentsButton: true + }); + } + + return res; + } + + listPlaces() { + const res = []; + Main.placesManager.getAllPlaces().forEach(place => { + let selectedAppId = place.idDecoded.substr(place.idDecoded.indexOf(':') + 1); + const fileIndex = selectedAppId.indexOf('file:///'); + if (fileIndex !== -1) { + selectedAppId = selectedAppId.substr(fileIndex + 7); + } + if (selectedAppId === 'home' || selectedAppId === 'desktop' || selectedAppId === 'connect') { + selectedAppId = place.name; + } + place.isPlace = true; + place.description = selectedAppId; + place.activate = () => place.launch();//don't pass any params to launch() + if (place.id.startsWith('bookmark:')) { + place.uri = place.id.substr(9); + place.mimeType = 'inode/directory'; + place.isDirectory = true; + } + res.push(place); + }); + res.splice(2, 0, { + id: 'special:trash', + name: _('Trash'), + description: _('Trash'), + isPlace: true, + activate: () => Util.spawnCommandLine('xdg-open trash:'), + iconFactory: (size) => new St.Icon({ + icon_name: 'user-trash', + icon_type: St.IconType.FULLCOLOR, + icon_size: size + }) + }); + res.splice(2, 0, { + id: 'special:computer', + name: _('Computer'), + description: _('Computer'), + isPlace: true, + activate: () => Util.spawnCommandLine('xdg-open computer:'), + iconFactory: (size) => new St.Icon({ + icon_name: 'computer', + icon_type: St.IconType.FULLCOLOR, + icon_size: size + }) + }); + + return res; + } + + searchPlaces(pattern){ + const places = this.listPlaces(); + const res = []; + places.forEach(place => { + const score = searchStr(pattern, place.name); + if (score > SEARCH_THRESHOLD) { + place.score = score; + res.push(place); + } + }); + + return res; + } + + listFavoriteFiles() { + const res = []; + const favorite_infos = XApp.Favorites.get_default().get_favorites(null); + favorite_infos.forEach(info => { + const found = this.display.appsView.buttonStore.find(button => + button.app.isFavoriteFile && button.app.uri === info.uri); + if (found) { + res.push(found.app); + } else { + res.push({ + name: info.display_name, + description: Gio.File.new_for_uri(info.uri).get_path(), + gicon: Gio.content_type_get_icon(info.cached_mimetype), + isFavoriteFile: true, + mimeType: info.cached_mimetype, + uri: info.uri + }); + } + }); + + res.sort( (a, b) => a.name.localeCompare(b.name, + undefined, + {sensitivity: "base", ignorePunctuation: true})); + return res; + } + + searchFavoriteFiles(pattern) { + const favs = this.listFavoriteFiles(); + const res = []; + + favs.forEach(item => { + const score = searchStr(pattern, item.name); + if (item.name && score > SEARCH_THRESHOLD) { + item.score = score; + res.push(item); + } + }); + + return res; + } + + listFolder(folder) { + const res = []; + const dir = Gio.file_new_for_path(folder); + let enumerator; + let errorMsg = null; + try { + enumerator = dir.enumerate_children( + 'standard::name,standard::type,standard::icon,standard::content-type,standard::is-hidden', + 0, null); + } catch(e) {//folder access permission denied probably + errorMsg = e.message; + } + let next; + if (enumerator) { + next = enumerator.next_file(null); + } + while (next) { + const filename = next.get_name(); + if (this.settings.showHiddenFiles || !next.get_is_hidden()) { + let file = Gio.file_new_for_path(folder + (folder === '/' ? '' : '/') + filename); + const isDirectory = next.get_file_type() === Gio.FileType.DIRECTORY; + res.push({ + name: next.get_name(), + gicon: next.get_icon(), + uri: file.get_uri(), + mimeType: next.get_content_type(), + isDirectory: isDirectory, + description: '', + isFolderviewFile: !isDirectory, + deleteAfterUse: true + }); + file = null; + } + next = enumerator.next_file(null); + } + if (enumerator) { + enumerator.close(null); + } + + res.sort((a, b) => { + if (!a.isDirectory && b.isDirectory) return 1; + else if (a.isDirectory && !b.isDirectory) return -1; + else if (a.isDirectory && b.isDirectory && + a.name.startsWith('.') && !b.name.startsWith('.')) return 1; + else if (a.isDirectory && b.isDirectory && + !a.name.startsWith('.') && b.name.startsWith('.')) return -1; + else { + const nameA = a.name.toUpperCase(); + const nameB = b.name.toUpperCase(); + return (nameA > nameB) ? 1 : ( (nameA < nameB) ? -1 : 0 ); + } + }); + const parent = dir.get_parent(); + if (parent) {// Add back button + res.unshift({ + name: 'Back', + uri: parent.get_uri(), + icon: new St.Icon({ + icon_name: 'edit-undo-symbolic', + icon_type: St.IconType.SYMBOLIC, + icon_size: this.settings.appsGridIconSize + }), + mimeType: 'inode/directory', + isBackButton: true, + description: '', + deleteAfterUse: true + }); + } + + return {results: res, errorMsg: errorMsg}; + } +} + +class Apps {//This obj provides the .app objects for all the applications categories + constructor(appSystem) { + this._appsByCategory = {}; + this._dirs = []; + this._knownApps = []; + this._appsNeedRefresh = false; + this._newInstance = true; + this.appSystem = appSystem; + this._initAppCategories(); + } + + installedChanged() { + this._appsNeedRefresh = true; + } + + _initAppCategories() { + const apps_sort = arr => arr.sort((a, b) => a.name.localeCompare(b.name, undefined, + {sensitivity: "base", ignorePunctuation: true})); + this._dirs = []; + this._appsByCategory = {}; + const iter = this.appSystem.get_tree().get_root_directory().iter(); + let nextType; + while ((nextType = iter.next()) !== CMenu.TreeItemType.INVALID) { + if (nextType === CMenu.TreeItemType.DIRECTORY) { + const dir = iter.get_directory(); + if (dir.get_is_nodisplay()) { + continue; + } + const dirId = dir.get_menu_id(); + const foundApps = this._loadDirectory(dir); + if (foundApps.length > 0) { + apps_sort(foundApps); + this._appsByCategory[dirId] = foundApps; + dir.dirId = dirId; + this._dirs.push(dir); + } + } + } + + this._dirs.sort((a, b) => { + const prefCats = ['ADMINISTRATION', 'PREFERENCES']; + const prefIdA = prefCats.indexOf(a.dirId.toUpperCase()); + const prefIdB = prefCats.indexOf(b.dirId.toUpperCase()); + if (prefIdA < 0 && prefIdB >= 0) return -1; + if (prefIdA >= 0 && prefIdB < 0) return 1; + return a.get_name().localeCompare(b.get_name(), undefined, + {sensitivity: "base", ignorePunctuation: true}); + }); + + //create "All applications" categories + let all = []; + let allApps = []; + let allSettings = []; + Object.keys(this._appsByCategory).forEach(key => { + all = all.concat(this._appsByCategory[key]); + if (['Preferences','Administration'].includes(key)) { + allSettings = allSettings.concat(this._appsByCategory[key]); + } else { + allApps = allApps.concat(this._appsByCategory[key]); + } + }); + this._appsByCategory.all = Array.from(new Set(all));//remove duplicates + apps_sort(this._appsByCategory.all); + this._appsByCategory.allApps = Array.from(new Set(allApps));//remove duplicates + apps_sort(this._appsByCategory.allApps); + this._appsByCategory.allSettings = Array.from(new Set(allSettings));//remove duplicates + apps_sort(this._appsByCategory.allSettings); + //remove apps from allSettings if they're also in allApps + this._appsByCategory.allApps.forEach( app => { + const i = this._appsByCategory.allSettings.findIndex( a => a === app ); + if (i > -1) { + this._appsByCategory.allSettings.splice(i, 1); + } + }) + + this._appsNeedRefresh = false; + this._newInstance = false; + } + + _loadDirectory(dir) { + let foundApps = []; + const iter = dir.iter(); + let nextType; + while ((nextType = iter.next()) !== CMenu.TreeItemType.INVALID) { + if (nextType === CMenu.TreeItemType.ENTRY) { + const entry = iter.get_entry(); + const id = entry.get_desktop_file_id(); + const app = this.appSystem.lookup_app(id); + if (!app || app.get_nodisplay()) { + continue; + } + + foundApps.push(app); + app.name = app.get_name(); + app.description = app.get_description(); + app.isApplication = true; + app.id = id; + app.desktop_file_path = entry.get_desktop_file_path(); + + if (this._knownApps.indexOf(id) < 0) {//unknown app + if (!this._newInstance) { + app.newAppShouldHighlight = true; + } + this._knownApps.push(id); + } + } else if (nextType === CMenu.TreeItemType.DIRECTORY) { + const subDir = iter.get_directory(); + if (!subDir.get_is_nodisplay()) { + foundApps = foundApps.concat(this._loadDirectory(subDir)); + } + } + } + return foundApps; + } + + getDirs() { + if (this._appsNeedRefresh) { + this._initAppCategories(); + } + + return this._dirs; + } + + dirHasNewApp(dirId) { + const apps = this.listApplications(dirId); + const newAppIndex = apps.findIndex(app => !!app.newAppShouldHighlight); + return (newAppIndex >= 0); + } + + listApplications(categoryMenuId) { + if (this._appsNeedRefresh) { + this._initAppCategories(); + } + + return this._appsByCategory[categoryMenuId]; + } + + searchApplications(pattern) { + if (!pattern) { + return []; + } + + const res = []; + this.listApplications('all').forEach(app => { + const keywords = app.get_keywords() || ''; + //get and clean up the app ids. + let id = app.id.replace('.desktop', ''); + const idLastDot = id.lastIndexOf('.'); + if (idLastDot >= 0) { + id = id.substring(idLastDot + 1); + } + id = id.replace('cinnamon-settings-', ''); + + const nameScore = searchStr(pattern, app.name); + let desScore = searchStr(pattern, app.description); + desScore *= 0.95; //slightly lower priority for description match + let keywordScore = searchStr(pattern, keywords); + keywordScore *= 0.8; //lower priority for keyword match + const idScore = searchStr(pattern, id); + const bestMatchScore = Math.max(nameScore, desScore, keywordScore, idScore); + if (bestMatchScore > SEARCH_THRESHOLD) { + app.score = bestMatchScore; + res.push(app); + } + }); + + return res; + } +} + +class RecentApps {// Simple class to remember the last 20 used apps which are shown in the + // "recent" category + constructor(appThis) { + this.appThis = appThis; + } + + add(appId) { + const recentApps = this.appThis.settings.recentApps.slice(); + const duplicate = recentApps.indexOf(appId); + if (duplicate > -1) { + recentApps.splice(duplicate, 1); + } + recentApps.unshift(appId); + if (recentApps.length > 20) { + recentApps.length = 20; + } + this.appThis.settings.recentApps = recentApps; + } + + clear() { + this.appThis.settings.recentApps = []; + } + + getApps(max_count) { + return this.appThis.settings.recentApps.slice(0, max_count); + } +} + +function main(metadata, orientation, panel_height, instance_id) { + return new CinnamenuApplet(metadata, orientation, panel_height, instance_id); +} diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/appsview.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/appsview.js new file mode 100644 index 0000000000..435ce2bce7 --- /dev/null +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/appsview.js @@ -0,0 +1,568 @@ +const Gio = imports.gi.Gio; +const Gtk = imports.gi.Gtk; +const Clutter = imports.gi.Clutter; +const St = imports.gi.St; +const Atk = imports.gi.Atk; +const Main = imports.ui.main; +const Util = imports.misc.util; +const {SignalManager} = imports.misc.signalManager; +const {EllipsizeMode} = imports.gi.Pango; +const {DragMotionResult, makeDraggable} = imports.ui.dnd; + +const { + wordWrap, + getThumbnail_gicon, + showTooltip, + hideTooltipIfVisible, + scrollToButton +} = require('./utils'); +const DescriptionPlacement = Object.freeze({TOOLTIP: 0, UNDER: 1, NONE: 2}); + +class AppButton { + constructor(appThis, app) { + this.appThis = appThis; + this.app = app; + this.signals = new SignalManager(null); + + //----------ICON--------------------------------------------- + if (this.app.icon) { //isSearchResult(excl. emoji), isClearRecentsButton, isBackButton + this.icon = this.app.icon; + } else if (this.app.icon_filename) { //some of isSearchResult + const gicon = new Gio.FileIcon({file: Gio.file_new_for_path(this.app.icon_filename)}); + this.icon = new St.Icon({gicon: gicon, icon_size: this.appThis.settings.appsGridIconSize}); + } else if (this.app.gicon) { //isRecentFile, isFavoriteFile, + //isFolderviewFile/Directory, some of isSearchResult + let gicon = this.app.gicon; + if (!this.app.isSearchResult) { + gicon = getThumbnail_gicon(this.app.uri, this.app.mimeType) || gicon; + } + this.icon = new St.Icon({gicon: gicon, icon_size: this.appThis.settings.appsGridIconSize}); + } else if (this.app.emoji) {//emoji search result + this.icon = new St.Label({ style: 'color: white; font-size: ' + + (Math.round(this.appThis.settings.appsGridIconSize * 0.85)) + 'px;'}); + this.icon.get_clutter_text().set_text(this.app.emoji); + } else if (this.app.isApplication) {//isApplication + this.icon = this.app.create_icon_texture(this.appThis.settings.appsGridIconSize); + } else if (this.app.iconFactory) {//isPlace + this.icon = this.app.iconFactory(this.appThis.settings.appsGridIconSize); + if (!this.icon) { + this.icon = new St.Icon({icon_name: 'folder', icon_size: this.appThis.settings.appsGridIconSize}); + } + } + if (!this.icon) { + this.icon = new St.Icon({icon_name: 'dialog-error', icon_size: this.appThis.settings.appsGridIconSize}); + } + + //--------Label------------------------------------ + this.label = new St.Label({ style_class: 'menu-application-button-label' }); + if (this.app.isClearRecentsButton) { + this.label.style = 'font-weight: bold;'; + } + //set label text + let name = this.app.name.replace(/&/g, '&').replace(/'; + if (this.appThis.settings.descriptionPlacement === DescriptionPlacement.UNDER && description) { + markup += '\n' + description + ''; + } + const clutterText = this.label.get_clutter_text(); + clutterText.set_markup(markup); + clutterText.ellipsize = EllipsizeMode.END; + + //-------------actor--------------------- + this.actor = new St.BoxLayout({ + vertical: true, + reactive: true, + accessible_role: Atk.Role.MENU_ITEM + }); + + this.setGridButtonWidth(); + + if (this.icon && this.appThis.settings.appsGridIconSize > 0) { + this.actor.add(this.icon, { + x_fill: false, + y_fill: false, + x_align: St.Align.MIDDLE, + y_align: St.Align.MIDDLE + }); + } + this.actor.add(this.label, { + x_fill: false, + y_fill: false, + x_align: St.Align.MIDDLE, + y_align: St.Align.MIDDLE + }); + + this._setNewAppHighlightClass(); + + //----------dnd-------------- + + if (this.app.isApplication) { + //make apps drag targets (they only act as drag targets when currentCategory is favorite_apps) + this.actor._delegate = { + handleDragOver: (source) => { + if (source.isDraggableApp && source.id !== this.app.id && + this.appThis.currentCategory === 'favorite_apps') { + this._resetAllAppsOpacity(); + this.actor.set_opacity(40); + return DragMotionResult.MOVE_DROP; + } + return DragMotionResult.NO_DROP; }, + handleDragOut: () => { this.actor.set_opacity(255); }, + acceptDrop: (source) => { + if (source.isDraggableApp && source.id !== this.app.id && + this.appThis.currentCategory === 'favorite_apps') { + this.actor.set_opacity(255); + this.appThis.addFavoriteAppToPos(source.id, this.app.id); + return true; + } else { + this.actor.set_opacity(255); + return DragMotionResult.NO_DROP; + } } + }; + + // make apps draggable. + Object.assign(this.actor._delegate, { + getDragActorSource: () => this.actor, + _getDragActor: () => new Clutter.Clone({source: this.actor}), + getDragActor: () => new Clutter.Clone({source: this.icon}), + id: this.app.id, + get_app_id: () => this.app.id, //used when eg. dragging to panel launcher + isDraggableApp: true + }); + + this.draggable = makeDraggable(this.actor); + this.signals.connect(this.draggable, 'drag-begin', () => hideTooltipIfVisible()); + this.signals.connect(this.draggable, 'drag-end', () => this._resetAllAppsOpacity()); + } + + //make files and folders draggable + if (this.app.isFolderviewFile || this.app.isDirectory || this.app.isRecentFile ) { + this.actor._delegate = { + handleDragOver: (source) => { return DragMotionResult.NO_DROP; }, + handleDragOut: () => {}, + acceptDrop: (source) => { return DragMotionResult.NO_DROP; }, + getDragActorSource: () => this.actor, + _getDragActor: () => new Clutter.Clone({source: this.actor}), + getDragActor: () => new Clutter.Clone({source: this.icon}), + id: this.app.id, + get_app_id: () => this.app.id, //used when eg. dragging to panel launcher + isDraggableFile: true, + uri: this.app.uri + }; + + this.draggable = makeDraggable(this.actor); + this.signals.connect(this.draggable, 'drag-begin', () => hideTooltipIfVisible()); + } + + this.signals.connect(this.actor, 'button-release-event', this._handleButtonRelease.bind(this)); + this.signals.connect(this.actor, 'enter-event', this.handleEnter.bind(this)); + this.signals.connect(this.actor, 'leave-event', this.handleLeave.bind(this)); + } + + _setButtonStyleNormal() { + this.has_focus = false; + this.actor.set_style_class_name('menu-application-button'); + } + + _setButtonStyleSelected() { + this.has_focus = true; + this.actor.set_style_class_name('menu-application-button-selected'); + } + + _setNewAppHighlightClass() { + if (this.app.newAppShouldHighlight) { + if (!this.actor.has_style_pseudo_class('highlighted')) { + this.actor.add_style_pseudo_class('highlighted'); //'font-weight: bold;'; + } + } else { + if (this.actor.has_style_pseudo_class('highlighted')) { + this.actor.remove_style_pseudo_class('highlighted'); + } + } + } + + setGridButtonWidth() { + this.actor.width = this.appThis.display.appsView.getGridValues().columnWidth; + } + + handleEnter(actor, event) { + if (this.appThis.display.contextMenu.isOpen ) { + return Clutter.EVENT_PROPAGATE; + } + + if (event) {//mouse + this.appThis.display.clearFocusedActors(); + } else {//keyboard navigation + scrollToButton(this, this.appThis.settings.enableAnimation); + } + this._setButtonStyleSelected(); + + //------show tooltip + if (this.appThis.settings.descriptionPlacement != DescriptionPlacement.TOOLTIP) { + return Clutter.EVENT_STOP; + } + + let tooltipMarkup = '' + wordWrap(this.app.name) + ''; + if (this.app.description) { + tooltipMarkup += '\n' + wordWrap(this.app.description) + ''; + } + tooltipMarkup = tooltipMarkup.replace(/&/g, '&'); + + let [x, y] = this.actor.get_transformed_position(); + let {width, height} = this.actor; + + x += Math.floor(width / 2); + y += height + 8 * global.ui_scale; + + showTooltip(this.actor, x, y, true /*center tooltip on x*/, tooltipMarkup); + return Clutter.EVENT_STOP; + } + + handleLeave(actor, event) { + if (this.appThis.display.contextMenu.isOpen) { + return false; + } + this._setButtonStyleNormal(); + hideTooltipIfVisible(); + } + + _handleButtonRelease(actor, event) { + const button = event.get_button(); + if (button === Clutter.BUTTON_PRIMARY) { + if (this.appThis.display.contextMenu.isOpen) { + this.appThis.display.contextMenu.close(); + this.appThis.display.clearFocusedActors(); + this.handleEnter(); + } else { + this.activate(event); + } + return Clutter.EVENT_STOP; + } else if (button === Clutter.BUTTON_SECONDARY) { + if (this.appThis.display.contextMenu.isOpen) { + this.appThis.display.contextMenu.close(); + this.appThis.display.clearFocusedActors(); + this.handleEnter(); + return Clutter.EVENT_STOP; + } else { + if (this.app.isApplication || this.app.isFolderviewFile || + this.app.isDirectory || this.app.isFavoriteFile || + this.app.emoji || this.app.isRecentFile){ + this.openContextMenu(event); + } + return Clutter.EVENT_STOP; + } + } + return Clutter.EVENT_PROPAGATE; + } + + activate() { + if (this.app.isApplication) { + if (this.app.newAppShouldHighlight) { + this.app.newAppShouldHighlight = false; + this._setNewAppHighlightClass(); + } + this.appThis.recentApps.add(this.app.id); + this.app.open_new_window(-1); + this.appThis.menu.close(); + } else if ((this.app.isDirectory && !this.app.isPlace) || this.app.isBackButton) { + this.appThis.setActiveCategory(Gio.File.new_for_uri(this.app.uri).get_path()); + //don't menu.close() + } else if (this.app.isFolderviewFile || this.app.isRecentFile || + this.app.isFavoriteFile || (this.app.isDirectory && this.app.isPlace)) { + try { + Gio.app_info_launch_default_for_uri(this.app.uri, global.create_app_launch_context()); + this.appThis.menu.close(); + } catch (e) { + Main.notify(_('Error while opening file:'), e.message); + //don't menu.close() + } + } else if (this.app.isClearRecentsButton) { + this.appThis.recentApps.clear(); + this.appThis.recentManagerDefault.purge_items(); + this.appThis.setActiveCategory('recents'); + //don't menu.close + } else if (this.app.isSearchResult || this.app.isPlace) { + this.app.activate(this.app); + this.appThis.menu.close(); + } + } + + openContextMenu(e) { + this._setButtonStyleSelected(); + hideTooltipIfVisible(); + this.appThis.display.contextMenu.openAppContextMenu(this.app, e, this.actor); + } + + _resetAllAppsOpacity() { + this.appThis.display.appsView.applicationsGridLayout.get_children().forEach( + child => child.set_opacity(255)); + } + + destroy() { + this.signals.disconnectAllSignals(); + hideTooltipIfVisible(); + + this.label.destroy(); + if (this.icon) { + this.icon.destroy(); + } + this.actor.destroy(); + } +} + +class Subheading { + constructor(appThis, subheadingText) { + this.appThis = appThis; + this.subheadingText = subheadingText; + this.signals = new SignalManager(null); + this.subheading = new St.Label({ + text: subheadingText, + x_expand: true, + reactive: true, + accessible_role: Atk.Role.HEADING + }); + this.subheadingBox = new St.BoxLayout({ style_class: 'menu-applications-subheading' }); + this.subheadingBox.add(this.subheading, {}); + } + + destroy(){ + this.signals.disconnectAllSignals(); + this.subheading.destroy(); + this.subheadingBox.destroy(); + } +} + +/* Creates and populates the main applications view. Method populate_add() takes an array of app + * objects and creates AppButton objs with .app as a property. These AppButton objs are then stored + * in this.buttonStore[] array for later reuse otherwise new AppButton's would need to be created + * each time a category is clicked on. Only the .actor property of the AppButton is used to populate + * the actual GridLayout*/ +class AppsView { + constructor(appThis) { + this.appThis = appThis; + this.buttonStore = []; + this.appsViewSignals = new SignalManager(null); + + this.applicationsGridBox = new St.Bin({ + style_class: 'menu-applications-grid-box', + x_fill: true, + y_fill: true + }); + this.applicationsGridLayout = new Clutter.Actor({ layout_manager: new Clutter.GridLayout() }); + this.applicationsGridBox.set_child(this.applicationsGridLayout); + this.headerText = new St.Label({ style_class: 'menu-applications-header-text' }); + this.applicationsBoxWrapper = new St.BoxLayout({ + style_class: 'menu-applications-inner-box', + vertical: true + }); + + this.applicationsBoxWrapper.add(this.headerText, { x_fill: false, x_align: St.Align.MIDDLE }); + this.applicationsBoxWrapper.add(this.applicationsGridBox, {}); + this.applicationsScrollBox = new St.ScrollView( + { style_class: 'vfade menu-applications-scrollbox' }); + const vscrollApplications = this.applicationsScrollBox.get_vscroll_bar(); + this.appsViewSignals.connect(vscrollApplications, 'scroll-start', + () => { this.appThis.menu.passEvents = true; }); + this.appsViewSignals.connect(vscrollApplications, 'scroll-stop', + () => { this.appThis.menu.passEvents = false; }); + this.applicationsScrollBox.add_actor(this.applicationsBoxWrapper); + this.applicationsScrollBox.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); + this.applicationsScrollBox.set_clip_to_allocation(true); + this.applicationsScrollBox.set_auto_scrolling(this.appThis.settings.enableAutoScroll); + this.applicationsScrollBox.set_mouse_scrolling(true); + } + + populate(appList, headerText = null) { + //too many actors in applicationsGridBox causes display errors, don't know why. Plus, it takes a long time + if (appList.length > 1000) { + appList.length = 1000; //truncate array + headerText = _('Too many entries - showing first 1000 entries only'); + } + this.populate_init(headerText); + this.populate_add(appList, null); + this.populate_finish(); + } + + populate_init(headerText = null) { + this.clearApps(); + this.applicationsScrollBox.vscroll.adjustment.set_value(0);//scroll to top + + if (headerText) { + this.headerText.set_text(headerText); + this.headerText.show(); + } else { + this.headerText.hide(); + } + + this.column = 0; + this.rownum = 0; + + this.subheadings = []; + } + + populate_add(appList, subheadingText = null) { + const gridLayout = this.applicationsGridLayout.layout_manager; + + if (subheadingText) { + if (this.column !== 0) { + this.column = 0; + this.rownum++; + } + + const subheading = new Subheading(this.appThis, subheadingText); + gridLayout.attach( + subheading.subheadingBox, + this.column, + this.rownum, + this.getGridValues().columns, + 1 + ); + this.rownum++; + + subheading.subheadingBox.show(); + this.subheadings.push(subheading); + } + + appList.forEach(app => { + let appButton = this.buttonStore.find(button => button.app === app); + + if (!appButton) { + appButton = new AppButton(this.appThis, app); + this.buttonStore.push(appButton); + } + appButton.setGridButtonWidth();// In case menu has been resized. + gridLayout.attach(appButton.actor, this.column, this.rownum, 1, 1); + appButton.actor.layout_column = this.column;//used for key navigation + this.column++; + + if (this.column > this.getGridValues().columns - 1) { + this.column = 0; + this.rownum++; + } + + appButton._setButtonStyleNormal(); + }); + } + + populate_finish() { + this.currentGridViewColumnCount = this.getGridValues().columns; + } + + focusFirstItem() { + const buttons = this.getActiveButtons(); + if (buttons[0] && !buttons[0].has_focus) { + this.appThis.display.clearFocusedActors(); + buttons[0].handleEnter(); + } + } + + resizeGrid() { + this.applicationsGridBox.hide();//for performance + + const newcolumnCount = this.getGridValues().columns; + if (this.currentGridViewColumnCount === newcolumnCount) { + //Number of columns are the same so just adjust button widths only. + this.applicationsGridLayout.get_children().forEach(actor => { + if (actor.has_style_class_name('menu-application-button') || + actor.has_style_class_name('menu-application-button-selected')) { + actor.width = this.getGridValues().columnWidth; + } + }); + } else {//Rearrange buttons to fit new number of columns. + this.applicationsGridBox.hide();// + const buttons = this.applicationsGridLayout.get_children(); + this.applicationsGridLayout.remove_all_children(); + let column = 0; + let rownum = 0; + const gridLayout = this.applicationsGridLayout.layout_manager; + const newColumnWidth = this.getGridValues().columnWidth; + buttons.forEach(actor => { + if (actor.has_style_class_name('menu-application-button') || + actor.has_style_class_name('menu-application-button-selected')) { + actor.width = newColumnWidth; + gridLayout.attach(actor, column, rownum, 1, 1); + actor.layout_column = column;//used for key navigation + column++; + if (column > newcolumnCount - 1) { + column = 0; + rownum++; + } + } else { //subheading label + if (column !== 0) { + column = 0; + rownum++; + } + gridLayout.attach(actor, column, rownum, newcolumnCount, 1); + rownum++; + } + }); + this.applicationsGridBox.show(); + } + + this.applicationsGridBox.show(); + this.currentGridViewColumnCount = newcolumnCount; + } + + getGridValues() { + const gridBoxUsableWidth = this.appThis.display.currentGridBoxUsableWidth; + const minColumnWidth = Math.max(140, this.appThis.settings.appsGridIconSize * 1.2); + const columns = Math.floor(gridBoxUsableWidth / (minColumnWidth * global.ui_scale)); + const columnWidth = Math.floor(gridBoxUsableWidth / columns); + + return {columnWidth: columnWidth, columns: columns}; + } + + getActiveButtons() { + const activeButtons = []; + this.applicationsGridLayout.get_children().forEach(child => { + const foundButton = this.buttonStore.find(button => button.actor === child); + if (foundButton) { + activeButtons.push(foundButton); + } + }); + return activeButtons; + } + + clearApps() { + this.clearAppsViewFocusedActors(); + + //destroy subheading labels + if(this.subheadings){ + this.subheadings.forEach(subheading => subheading.destroy()); + } + this.subheadings = []; + + this.applicationsGridLayout.remove_all_children(); + } + + clearAppsViewFocusedActors() { + this.getActiveButtons().forEach(button => { if (button.has_focus) button.handleLeave(); }); + } + + buttonStoreCleanup() { + //delete all buttons which won't be reused + const buttonStore = this.buttonStore.filter(button => { + if (button.app.deleteAfterUse) { + button.destroy(); + return false; + } else { + return true; + } }); + this.buttonStore = buttonStore; + } + + destroy() { + this.appsViewSignals.disconnectAllSignals(); + this.headerText.destroy(); + this.applicationsGridBox.destroy(); + this.applicationsGridLayout.destroy(); + this.applicationsBoxWrapper.destroy(); + this.applicationsScrollBox.destroy(); + this.buttonStore.forEach(button => { if (button) button.destroy(); }); + this.buttonStore = []; + } +} + +module.exports = {AppsView}; diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/categoriesview.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/categoriesview.js new file mode 100644 index 0000000000..10da378776 --- /dev/null +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/categoriesview.js @@ -0,0 +1,418 @@ +const Gtk = imports.gi.Gtk; +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; +const Atk = imports.gi.Atk; +const Clutter = imports.gi.Clutter; +const XApp = imports.gi.XApp; +const St = imports.gi.St; +const {SignalManager} = imports.misc.signalManager; +const {DragMotionResult, makeDraggable} = imports.ui.dnd; + +const {scrollToButton} = require('./utils'); + +let buttonTimeoutId = null; +function clearButtonTimeout() { + if (buttonTimeoutId) { + clearTimeout(buttonTimeoutId); + buttonTimeoutId = null; + } +} + +class CategoryButton { + constructor(appThis, category_id, category_name, icon_name, gicon) { + this.appThis = appThis; + this.signals = new SignalManager(null); + this.disabled = false; + //Note: When option "Activate categories on click" is on, then the this.has_focus === true category + //is the one that has keyboard or mouse focus and is not necessarily the same as the currently + //selected category (this.appThis.currentCategory) + this.has_focus = false; + this.id = category_id; + this.actor = new St.BoxLayout({ + style_class: 'menu-category-button', + reactive: true, + accessible_role: Atk.Role.MENU_ITEM + }); + + //----icon + if (icon_name) { + this.icon = new St.Icon({ + icon_name: icon_name, + icon_type: St.IconType.FULLCOLOR, + icon_size: this.appThis.settings.categoryIconSize + }); + } else { + this.icon = new St.Icon({ + gicon: gicon, + icon_type: St.IconType.FULLCOLOR, + icon_size: this.appThis.settings.categoryIconSize + }); + } + if (this.appThis.settings.categoryIconSize > 0) { + this.actor.add(this.icon, {x_fill: false, y_fill: false, y_align: St.Align.MIDDLE}); + } + + //---label + this.category_name = category_name ? category_name : '';//is this needed? + this.label = new St.Label({ + text: this.category_name, + style_class: 'menu-category-button-label' + }); + this.actor.add(this.label, {x_fill: false, y_fill: false, y_align: St.Align.MIDDLE}); + + //---dnd + this.actor._delegate = { + handleDragOver: (source) => { + if (!source.isDraggableCategory || source.id === this.id || this.appThis.searchActive) { + return DragMotionResult.NO_DROP; + } + this.appThis.display.categoriesView.resetAllCategoriesOpacity(); + this.actor.set_opacity(50); + return DragMotionResult.MOVE_DROP; + }, + acceptDrop: (source) => { + if (!source.isDraggableCategory || source.id === this.id || this.appThis.searchActive) { + this.appThis.display.categoriesView.resetAllCategoriesOpacity(); + return DragMotionResult.NO_DROP; + } + //move category to new position + const categories = this.appThis.settings.categories.slice(); + const oldIndex = categories.indexOf(source.id); + const newIndex = categories.indexOf(this.id); + categories.splice(oldIndex, 1); + categories.splice(newIndex, 0, source.id); + this.appThis.settings.categories = categories; + this.appThis.display.categoriesView.resetAllCategoriesOpacity(); + this.appThis.display.categoriesView.update(); + this.appThis.setActiveCategory(this.appThis.currentCategory); + return true; + }, + getDragActorSource: () => this.actor, + _getDragActor: () => new Clutter.Clone({source: this.actor}), + getDragActor: () => new Clutter.Clone({source: this.icon}), + isDraggableCategory: true, + id: this.id + }; + this.draggable = makeDraggable(this.actor); + + // Connect signals + this.signals.connect(this.draggable, 'drag-begin', () => this.actor.set_opacity(51)); + this.signals.connect(this.draggable, 'drag-cancelled', () => this.actor.set_opacity(255)); + this.signals.connect(this.draggable, 'drag-end', () => + this.appThis.display.categoriesView.resetAllCategoriesOpacity()); + + this.signals.connect(this.actor, 'enter-event', this.handleEnter.bind(this)); + this.signals.connect(this.actor, 'leave-event', this.handleMouseLeave.bind(this)); + this.signals.connect(this.actor, 'button-release-event', this._handleButtonRelease.bind(this)); + } + + setHighlight(on) { + if (on) { + if (!this.actor.has_style_pseudo_class('highlighted')) { + this.actor.add_style_pseudo_class('highlighted'); //'font-weight: bold;'; + } + } else { + if (this.actor.has_style_pseudo_class('highlighted')) { + this.actor.remove_style_pseudo_class('highlighted'); + } + } + } + + setButtonStyleNormal() { + this.actor.set_style_class_name('menu-category-button'); + this.icon.set_opacity(255);//undo changes made in _setButtonStyleGreyed() + } + + setButtonStyleSelected() { + this.actor.set_style_class_name('menu-category-button-selected'); + } + + _setButtonStyleGreyed() { + this.actor.set_style_class_name('menu-category-button-greyed'); + if (this.icon) { + let icon_opacity = this.icon.get_theme_node().get_double('opacity'); + icon_opacity = Math.min(Math.max(0, icon_opacity), 1); + if (icon_opacity) { // Don't set opacity to 0 if not defined + this.icon.set_opacity(icon_opacity * 255); + } + } + } + + selectCategory() { + this.appThis.setActiveCategory(this.id); + } + + handleEnter(actor, event) { + //this method handles mouse and key events + if (this.has_focus || this.disabled || this.appThis.display.contextMenu.isOpen) { + return Clutter.EVENT_PROPAGATE; + } + //When "activate categories on click" is off, don't enter this button if mouse is moving + //quickly towards appviews, i.e. badAngle === true. + if (event && !this.appThis.settings.categoryClick && this.appThis.display.badAngle) { + clearButtonTimeout(); + //badAngle now but check again in a short while + buttonTimeoutId = setTimeout( + () => { + this.appThis.display.updateMouseTracking(); + this.handleEnter(actor, event); + }, + this.appThis.display.TRACKING_TIME + ); + return Clutter.EVENT_PROPAGATE; + } + + this.appThis.display.categoriesView.allButtonsRemoveFocusAndHover(); + this.has_focus = true; + + if (event) {//mouse + this.appThis.display.clearFocusedActors(); + } else {//keypress + scrollToButton(this, this.appThis.settings.enableAnimation); + } + + // No need to continue if current category is already selected + if (this.id === this.appThis.currentCategory) { + return Clutter.EVENT_PROPAGATE; + } + clearButtonTimeout(); + if (this.appThis.settings.categoryClick) { + this.actor.add_style_pseudo_class('hover'); + } else { + this.selectCategory(); + } + return Clutter.EVENT_PROPAGATE; + } + + handleMouseLeave(actor, event) { + if (this.disabled || this.appThis.display.contextMenu.isOpen) { + return false; + } + + this.removeFocusAndHover(); + + // return focus to currently active category + this.appThis.display.categoriesView.setCategoryFocus(this.appThis.currentCategory); + } + + removeFocusAndHover() { + this.has_focus = false; + + if (this.actor.has_style_pseudo_class('hover')) { + this.actor.remove_style_pseudo_class('hover'); + } + + clearButtonTimeout(); + } + + _handleButtonRelease(actor, event) { + if (this.appThis.display.contextMenu.isOpen) { + this.appThis.display.contextMenu.close(); + return Clutter.EVENT_STOP; + } + if (this.disabled) { + return Clutter.EVENT_STOP; + } + + const button = event.get_button(); + if (button === Clutter.BUTTON_PRIMARY && this.appThis.settings.categoryClick) { + this.selectCategory(); + return Clutter.EVENT_STOP; + } else if (button === Clutter.BUTTON_SECONDARY) { + if (this.actor.has_style_class_name('menu-category-button-hover')) { + //Remove focus from this category button before opening it's context menu. + //Todo: Ideally this button should retain focus style to indicate the button the + //context menu was opened on. + this.removeFocusAndHover(); + } + this.openContextMenu(event); + return Clutter.EVENT_STOP; + } + } + + openContextMenu(e) { + this.appThis.display.contextMenu.openCategoryContextMenu(this.id, e, this.actor); + } + + disable() { + this._setButtonStyleGreyed(); + this.disabled = true; + this.has_focus = false; + } + + enable() { + this.setButtonStyleNormal(); + this.disabled = false; + } + + destroy() { + this.signals.disconnectAllSignals(); + this.label.destroy(); + if (this.icon) { + this.icon.destroy(); + } + this.actor.destroy(); + } +} + +/* Creates the categories box and array of CategoryButtons (this.buttons[]). Updates the categories and + * populates the categoriesBox. */ +class CategoriesView { + constructor(appThis) { + this.appThis = appThis; + this.buttons = []; + + this.categoriesBox = new St.BoxLayout({ style_class: 'menu-categories-box', vertical: true }); + this.groupCategoriesWorkspacesWrapper = + new St.BoxLayout({/*style: 'max-width: 185px;',*/ vertical: true }); + this.groupCategoriesWorkspacesWrapper.add(this.categoriesBox, { }); + + this.groupCategoriesWorkspacesScrollBox = + new St.ScrollView({ style_class: 'vfade menu-categories-scrollbox' }); + this.groupCategoriesWorkspacesScrollBox.add_actor(this.groupCategoriesWorkspacesWrapper); + this.groupCategoriesWorkspacesScrollBox.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER); + this.groupCategoriesWorkspacesScrollBox.set_clip_to_allocation(true); + this.groupCategoriesWorkspacesScrollBox.set_auto_scrolling(this.appThis.settings.enableAutoScroll); + this.groupCategoriesWorkspacesScrollBox.set_mouse_scrolling(true); + if (!this.appThis.settings.showCategories) { + this.groupCategoriesWorkspacesScrollBox.width = 0; + } + } + + update() { + //Put all enabled categories into newButtons[] in default order by reusing the + //buttons in this.buttons[] or by creating new CategoryButton. + const newButtons = []; + + //Add 'All applications' + let button = this.buttons.find(button => button.id === 'all'); + if (!button) { + button = new CategoryButton(this.appThis, 'all', _('All applications'), 'cinnamon-all-applications', null); + } + newButtons.push(button); + + //Add other app categories + this.appThis.apps.getDirs().forEach(dir => { + let button = this.buttons.find(button => button.id === dir.dirId); + if (!button) { + button = new CategoryButton(this.appThis, dir.dirId, dir.get_name(), null, dir.get_icon()); + } + //highlight category if it contains a new app + button.setHighlight(this.appThis.apps.dirHasNewApp(dir.dirId)); + newButtons.push(button); + }); + + //Add special categories + const enableFavFiles = XApp.Favorites.get_default().get_favorites(null).length > 0; + [ + [enableFavFiles, 'favorite_files', _('Favorites'), 'xapp-user-favorites'], + [this.appThis.settings.showPlaces, 'places', _('Places'), 'folder'], + [this.appThis.recentsEnabled, 'recents', _('Recent'), 'document-open-recent'], + [this.appThis.settings.showFavAppsCategory, 'favorite_apps', _('Favorite apps'), 'emblem-favorite'] + ].forEach(param => { + if (param[0]) { + let button = this.buttons.find(button => button.id === param[1]); + if (!button) { + button = new CategoryButton(this.appThis, param[1], param[2], param[3], null); + } + newButtons.push(button); + } + }); + + //Add folder categories + const folderCategories = this.appThis.settings.folderCategories.slice(); + let folderCategoriesChanged = false; + folderCategories.forEach((folder, index) => { + let button = this.buttons.find(button => button.id === folder); + if (button) { + newButtons.push(button); + } else { + const file = Gio.file_new_for_path(folder); + try {//In case folder no longer exists. + const fileInfo = file.query_info('standard::icon', Gio.FileQueryInfoFlags.NONE, null); + const gicon = fileInfo.get_icon(); + let displayName = file.get_basename(); + if (displayName.length > 19) { + displayName = displayName.slice(0,17) + '...'; + } + button = new CategoryButton(this.appThis, folder, displayName, null, gicon); + newButtons.push(button); + } catch(e) { + log("gridmenu:Error creating folder category: " + folder + " ...skipping."); + //remove this error causing element from the array. + folderCategories.splice(index, 1); + folderCategoriesChanged = true; + } + } + }); + if (folderCategoriesChanged) this.appThis.settings.folderCategories = folderCategories; + + //set user category order to default if none already + if (this.appThis.settings.categories.length === 0) { + this.appThis.settings.categories = newButtons.map(button => button.id); + } + + //add new categories to end of user category order if not already included + newButtons.forEach(newButton => { + if (this.appThis.settings.categories.indexOf(newButton.id) === -1) { + this.appThis.settings.categories.push(newButton.id); + } + }); + + //set this.buttons[] to newButtons[] in user prefered order + this.buttons = []; + this.appThis.settings.categories.forEach(buttonId => { + const foundButton = newButtons.find(newButton => newButton.id === buttonId); + if (foundButton && !this.buttons.find(button => button.id === buttonId)) { + this.buttons.push(foundButton); + } + }); + + //replace user button order to remove unused ids. + if (this.appThis.settings.categories.length > this.buttons.length) { + this.appThis.settings.categories = this.buttons.map(button => button.id); + } + + //populate categoriesBox with buttons + this.categoriesBox.remove_all_children(); + this.buttons.forEach(button => this.categoriesBox.add_actor(button.actor)); + } + + setSelectedCategoryStyle(categoryId) { + this.buttons.forEach(categoryButton => { + if (categoryButton.id === categoryId) { + categoryButton.setButtonStyleSelected(); + } else { + categoryButton.setButtonStyleNormal(); + } + }); + } + + setCategoryFocus(categoryId) { + this.buttons.forEach(categoryButton => { + if (categoryButton.id === categoryId) { + categoryButton.has_focus = true; + } else { + categoryButton.has_focus = false; + } + }); + } + + allButtonsRemoveFocusAndHover() { + this.buttons.forEach(button => button.removeFocusAndHover()); + } + + resetAllCategoriesOpacity() { + this.buttons.forEach(button => button.actor.set_opacity(255)); + } + + destroy() { + this.buttons.forEach(button => button.destroy()); + this.buttons = []; + this.categoriesBox.destroy(); + this.groupCategoriesWorkspacesWrapper.destroy(); + this.groupCategoriesWorkspacesScrollBox.destroy(); + } +} + +module.exports = {CategoriesView}; diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/contextmenu.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/contextmenu.js new file mode 100644 index 0000000000..3e5106f1d5 --- /dev/null +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/contextmenu.js @@ -0,0 +1,434 @@ +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const St = imports.gi.St; +const Clutter = imports.gi.Clutter; +const Main = imports.ui.main; +const {PopupBaseMenuItem, PopupMenu, PopupSeparatorMenuItem} = imports.ui.popupMenu; +const {getUserDesktopDir, changeModeGFile} = imports.misc.fileUtils; +const {SignalManager} = imports.misc.signalManager; +const {spawnCommandLine} = imports.misc.util; + +const {MODABLE, MODED} = require('./emoji'); + +class ContextMenuItem extends PopupBaseMenuItem { + constructor(appThis, label, iconName, action, insensitive = false) { + super({focusOnHover: false}); + this.appThis = appThis; + if (iconName) { + const icon = new St.Icon({ style_class: 'popup-menu-icon', icon_name: iconName, + icon_type: St.IconType.SYMBOLIC}); + this.addActor(icon, {span: 0}); + } + this.addActor(new St.Label({text: label})); + + this.signals = new SignalManager(null); + this.action = action; + if (this.action === null && !insensitive) {//"Open with" item + this.actor.add_style_class_name('popup-subtitle-menu-item'); + } else if (insensitive) {//greyed out item + this.actor.add_style_pseudo_class('insensitive'); + } + this.signals.connect(this.actor, 'enter-event', this.handleEnter.bind(this)); + this.signals.connect(this.actor, 'leave-event', this.handleLeave.bind(this)); + } + + handleEnter(actor, e) { + if (this.action === null) { + return Clutter.EVENT_STOP; + } + this.has_focus = true; + this.actor.add_style_pseudo_class('hover'); + this.actor.add_style_pseudo_class('active'); + return Clutter.EVENT_STOP; + } + + handleLeave(actor, e) { + this.has_focus = false; + this.actor.remove_style_pseudo_class('hover'); + this.actor.remove_style_pseudo_class('active'); + return Clutter.EVENT_STOP; + } + + activate(event) { + if (!this.action || event && event.get_button() !== Clutter.BUTTON_PRIMARY) { + return Clutter.EVENT_STOP; + } + this.action(); + return Clutter.EVENT_STOP; + } + + destroy() { + this.signals.disconnectAllSignals(); + PopupBaseMenuItem.prototype.destroy.call(this); + } +} + +class ContextMenu { + constructor(appThis) { + this.appThis = appThis; + this.menu = new PopupMenu(this.appThis.actor /*,St.Side.TOP*/); + this.menu.actor.hide(); + this.contextMenuBox = new St.BoxLayout({ style_class: '', vertical: true, reactive: true }); + this.contextMenuBox.add_actor(this.menu.actor); + + this.contextMenuButtons = []; + this.isOpen = false; + } + + openAppContextMenu(app, e, buttonActor) { + //e is used to position context menu at mouse coords. If keypress opens menu then + //e is undefined and buttonActor position is used instead. + this.contextMenuButtons.forEach(button => button.destroy()); + this.contextMenuButtons = []; + + //------populate menu + if (app.isApplication) { + this._populateContextMenu_apps(app); + } else if (app.isFolderviewFile || app.isDirectory || + app.isRecentFile || app.isFavoriteFile) { + if (!this._populateContextMenu_files(app)) { + return; + } + } else if (app.isSearchResult && app.emoji) { + const i = MODABLE.indexOf(app.emoji);//Find if emoji is in list of emoji that can have + //skin tone modifiers. + if (i < 0) { + return; + } + const addMenuItem = (char, text) => { + const newEmoji = MODED[i].replace(/\u{1F3FB}/ug, char); //replace light skin tone character in + // MODED[i] with skin tone option. + const item = new ContextMenuItem(this.appThis, newEmoji + ' ' + text, null, + () => { + const clipboard = St.Clipboard.get_default(); + clipboard.set_text(St.ClipboardType.CLIPBOARD, newEmoji); + this.appThis.menu.close(); + } + ); + this.menu.addMenuItem(item); + this.contextMenuButtons.push(item); + }; + addMenuItem('\u{1F3FB}', _('light skin tone')); + addMenuItem('\u{1F3FC}', _('medium-light skin tone')); + addMenuItem('\u{1F3FD}', _('medium skin tone')); + addMenuItem('\u{1F3FE}', _('medium-dark skin tone')); + addMenuItem('\u{1F3FF}', _('dark skin tone')); + } else { + return; + } + this._showMenu(e, buttonActor); + } + + openCategoryContextMenu(categoryId, e, buttonActor) { + //e is used to position context menu at mouse coords. If keypress opens menu then + //e is undefined and buttonActor position is used instead. + this.contextMenuButtons.forEach(button => button.destroy()); + this.contextMenuButtons = []; + + //------populate menu + const addMenuItem = (item) => { + this.menu.addMenuItem(item); + this.contextMenuButtons.push(item); + }; + if (categoryId.startsWith('/')) { + addMenuItem(new ContextMenuItem(this.appThis, _('Remove category'), 'user-trash', + () => { + if (categoryId === GLib.get_home_dir()) { + this.appThis.settings.showHomeFolder = false; + this.appThis._onShowHomeFolderChange(); + } else { + this.appThis.removeFolderCategory(categoryId); + } + this.appThis.display.categoriesView.update(); + this.close(); + } + )); + this.menu.addMenuItem(new PopupSeparatorMenuItem(this.appThis)); + } + addMenuItem(new ContextMenuItem(this.appThis, _('Reset category order'), 'edit-undo-symbolic', + () => { + this.appThis.settings.categories = []; + this.appThis.display.categoriesView.update(); + this.close(); + } + )); + + this._showMenu(e, buttonActor); + } + + _showMenu(e, buttonActor) { + //----Position and open context menu---- + this.isOpen = true; + this.appThis.resizer.inhibit_resizing = true; + + const monitor = Main.layoutManager.findMonitorForActor(this.menu.actor); + let mx, my; + if (e) { + [mx, my] = e.get_coords(); //get mouse position + } else {//activated by keypress, no e supplied + [mx, my] = buttonActor.get_transformed_position(); + mx += 20; + my += 20; + } + if (mx > monitor.x + monitor.width - this.menu.actor.width) { + mx -= this.menu.actor.width; + } + if (my > monitor.y + monitor.height - this.menu.actor.height - 40/*allow for panel*/) { + my -= this.menu.actor.height; + } + + let [cx, cy] = this.contextMenuBox.get_transformed_position(); + + this.menu.actor.set_anchor_point(Math.round(cx - mx), Math.round(cy - my)); + + //This context menu doesn't have an St.Side and so produces errors in .xsession-errors. + //Enable animation here for the sole reason that it spams .xsession-errors less. Can't add an + //St.Side because in some themes it looks like it should be attached to a panel but isn't. + //Ideally, a proper floating popup menu should be coded. + this.menu.open(true); + return; + } + + _populateContextMenu_apps(app) { + const addMenuItem = (item) => { + this.menu.addMenuItem(item); + this.contextMenuButtons.push(item); + }; + + //Run with NVIDIA GPU + if (Main.gpu_offload_supported) { + addMenuItem( new ContextMenuItem(this.appThis, _('Run with NVIDIA GPU'), 'cpu', + () => { + try { + app.launch_offloaded(0, [], -1); + } catch (e) { + logError(e, 'Could not launch app with dedicated gpu: '); + } + this.appThis.menu.close(); + } + )); + } + + //Add to panel + addMenuItem(new ContextMenuItem(this.appThis, _('Add to panel'), 'list-add', + () => { + if (!Main.AppletManager.get_role_provider_exists(Main.AppletManager.Roles.PANEL_LAUNCHER)) { + const new_applet_id = global.settings.get_int('next-applet-id'); + global.settings.set_int('next-applet-id', (new_applet_id + 1)); + const enabled_applets = global.settings.get_strv('enabled-applets'); + enabled_applets.push('panel1:right:0:panel-launchers@cinnamon.org:' + new_applet_id); + global.settings.set_strv('enabled-applets', enabled_applets); + } + const launcherApplet = + Main.AppletManager.get_role_provider(Main.AppletManager.Roles.PANEL_LAUNCHER); + if (launcherApplet) { + launcherApplet.acceptNewLauncher(app.id); + } + this.close(); + } + )); + + //Add to desktop + const userDesktopPath = getUserDesktopDir(); + if (userDesktopPath) { + addMenuItem( new ContextMenuItem(this.appThis, _('Add to desktop'), 'computer', + () => { + const file = Gio.file_new_for_path(app.get_app_info().get_filename()); + const destFile = Gio.file_new_for_path(userDesktopPath + '/' + file.get_basename()); + try { + file.copy( destFile, 0, null, null); + changeModeGFile(destFile, 755); + } catch(e) { + global.logError('gridmenu: Error creating desktop file', e); + } + this.close(); + } + )); + } + + //add/remove favorite + if (this.appThis.appFavorites.isFavorite(app.id)) { + addMenuItem( new ContextMenuItem(this.appThis, _('Remove from favorites'), 'starred', + () => { + this.appThis.appFavorites.removeFavorite(app.id); + this.close(); + } + )); + } else { + addMenuItem( new ContextMenuItem(this.appThis, _('Add to favorites'), 'non-starred', + () => { + this.appThis.appFavorites.addFavorite(app.id); + this.close(); + } + )); + } + + //uninstall (Mint only) + if (this.appThis._canUninstallApps) { + addMenuItem( new ContextMenuItem(this.appThis, _('Uninstall'), 'edit-delete', + () => { + spawnCommandLine("/usr/bin/cinnamon-remove-application '" + + app.get_app_info().get_filename() + "'"); + this.appThis.menu.close(); + } + )); + } + + //show app info + if (this.appThis._pamacManagerAvailable) { + addMenuItem( new ContextMenuItem(this.appThis, _('App Info'), 'dialog-information', + () => { spawnCommandLine("/usr/bin/pamac-manager --details-id=" + app.id); + this.appThis.menu.close(); } )); + } + } + + _populateContextMenu_files(app) { + const addMenuItem = (item) => { + this.menu.addMenuItem(item); + this.contextMenuButtons.push(item); + }; + + const hasLocalPath = (file) => (file.is_native() && file.get_path() != null); + const file = Gio.File.new_for_uri(app.uri); + const fileExists = file.query_exists(null); + if (!fileExists && !app.isFavoriteFile) { + Main.notify(_('This file is no longer available'),''); + return false; //no context menu + } + //Note: a file can be an isFavoriteFile and also not exist so continue below and add option to + //remove from favorites. + + //Open with... + if (fileExists) { + addMenuItem( new ContextMenuItem(this.appThis, _('Open with'), null, null)); + const defaultInfo = Gio.AppInfo.get_default_for_type(app.mimeType, !hasLocalPath(file)); + if (defaultInfo) { + addMenuItem( new ContextMenuItem(this.appThis, defaultInfo.get_display_name(), null, + () => { + defaultInfo.launch([file], null); + this.appThis.menu.close(); + } + )); + } + Gio.AppInfo.get_all_for_type(app.mimeType).forEach(info => { + if (!hasLocalPath(file) || !info.supports_uris() || info.equal(defaultInfo)) { + return; + } + addMenuItem( new ContextMenuItem(this.appThis, info.get_display_name(), null, + () => { + info.launch([file], null); + this.appThis.menu.close(); + } + )); + }); + addMenuItem( new ContextMenuItem(this.appThis, _('Other application...'), null, + () => { + spawnCommandLine('nemo-open-with ' + app.uri); + this.appThis.menu.close(); + } + )); + } + + //add/remove favorite + this.menu.addMenuItem(new PopupSeparatorMenuItem(this.appThis)); + if (this.appThis.xappGetIsFavoriteFile(app.uri)) { //favorite + addMenuItem( new ContextMenuItem(this.appThis, _('Remove from favorites'), 'starred', + () => { + this.appThis.xappRemoveFavoriteFile(app.uri); + this.close(); + } + )); + } else { + addMenuItem( new ContextMenuItem(this.appThis, _('Add to favorites'), 'non-starred', + () => { + this.appThis.xappAddFavoriteFile(app.uri); + this.close(); + } + )); + } + + //Add folder as category + if (app.isDirectory && this.appThis.settings.showCategories) { + const path = Gio.file_new_for_uri(app.uri).get_path(); + if (!this.appThis.getIsFolderCategory(path)) { + this.menu.addMenuItem(new PopupSeparatorMenuItem(this.appThis)); + addMenuItem(new ContextMenuItem(this.appThis, _('Add folder as category'), 'list-add', + () => { + if (path === GLib.get_home_dir()) { + this.appThis.settings.showHomeFolder = true; + } + this.appThis.addFolderCategory(path); + this.appThis.display.categoriesView.update(); + this.close(); + } + )); + } + } + + //Open containing folder + const folder = file.get_parent(); + if (app.isRecentFile || app.isFavoriteFile || app.isFolderviewFile) { + this.menu.addMenuItem(new PopupSeparatorMenuItem(this.appThis)); + addMenuItem(new ContextMenuItem(this.appThis, _('Open containing folder'), 'go-jump', + () => { + const fileBrowser = Gio.AppInfo.get_default_for_type('inode/directory', true); + fileBrowser.launch([folder], null); + this.appThis.menu.close(); + } + )); + } + + //Move to trash + if (!app.isFavoriteFile) { + this.menu.addMenuItem(new PopupSeparatorMenuItem(this.appThis)); + + const fileInfo = file.query_info('access::can-trash', Gio.FileQueryInfoFlags.NONE, null); + const canTrash = fileInfo.get_attribute_boolean('access::can-trash'); + if (canTrash) { + addMenuItem(new ContextMenuItem(this.appThis, _('Move to trash'), 'user-trash', + () => { + const file = Gio.File.new_for_uri(app.uri); + try { + file.trash(null); + } catch (e) { + Main.notify(_('Error while moving file to trash:'), e.message); + } + this.appThis.setActiveCategory(this.appThis.currentCategory); + this.close(); + } + )); + } else {//show insensitive item + addMenuItem( new ContextMenuItem(this.appThis, _('Move to trash'), 'user-trash', + null, true /*insensitive*/)); + } + } + return true; //success. + } + + getCurrentlyFocusedMenuItem() { + if (!this.isOpen) { + return -1; + } + + let focusedButton = this.contextMenuButtons.findIndex(button => button.has_focus); + if (focusedButton < 0) { + focusedButton = 0; + } + return focusedButton; + } + + close() { + this.menu.close(); + this.isOpen = false; + this.appThis.resizer.inhibit_resizing = false; + } + + destroy() { + this.contextMenuButtons.forEach(button => button.destroy()); + this.contextMenuButtons = null; + //this.menu.destroy(); //causes errors in .xsession-errors?? + this.contextMenuBox.destroy(); + } +} + +module.exports = {ContextMenu}; diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/display.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/display.js new file mode 100644 index 0000000000..baef65768e --- /dev/null +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/display.js @@ -0,0 +1,317 @@ +const St = imports.gi.St; +const {SignalManager} = imports.misc.signalManager; +const {PopupMenuSection} = imports.ui.popupMenu; +const {ContextMenu} = require('./contextmenu'); +const {AppsView} = require('./appsview'); +const {CategoriesView} = require('./categoriesview'); +const {Sidebar} = require('./sidebar'); +const SidebarPlacement = Object.freeze({TOP: 0, BOTTOM: 1, LEFT: 2, RIGHT: 3}); + +class Display { + constructor (appThis) { + this.appThis = appThis; + this.displaySignals = new SignalManager(null); + const sidebarPlacement = this.appThis.settings.showSidebar ? + this.appThis.settings.sidebarPlacement : SidebarPlacement.BOTTOM; + switch (sidebarPlacement) { + case SidebarPlacement.TOP: + this.appThis.menu.setCustomStyleClass('menu-background gridmenu sidebar-top'); + break; + case SidebarPlacement.LEFT: + this.appThis.menu.setCustomStyleClass('menu-background gridmenu sidebar-left'); + break; + case SidebarPlacement.BOTTOM: + this.appThis.menu.setCustomStyleClass('menu-background gridmenu sidebar-bottom'); + break; + case SidebarPlacement.RIGHT: + this.appThis.menu.setCustomStyleClass('menu-background gridmenu sidebar-right'); + break; + } + this.sidebar = new Sidebar(this.appThis); + + //==================bottomPane (may also be at the top)================ + this.searchView = new SearchView(this.appThis); + this.displaySignals.connect( + this.searchView.searchEntryText, + 'text-changed', + (...args) => this.appThis._onSearchTextChanged(...args) + ); + this.displaySignals.connect( + this.searchView.searchEntryText, + 'key-press-event', + (...args) => this.appThis._onMenuKeyPress(...args) + ); + this.bottomPane = new St.BoxLayout({}); + if (sidebarPlacement === SidebarPlacement.TOP || sidebarPlacement === SidebarPlacement.BOTTOM) { + this.bottomPane.add(this.sidebar.sidebarOuterBox, { + expand: false, + x_fill: false, + y_fill: false, + x_align: St.Align.START, + y_align: St.Align.MIDDLE + }); + } + this.bottomPane.add(this.searchView.searchBox, { + expand: true, + x_fill: true, + y_fill: false, + x_align: St.Align.END, + y_align: St.Align.MIDDLE + }); + + //=================middlePane====================== + this.appsView = new AppsView(this.appThis); + this.categoriesView = new CategoriesView(this.appThis); + this.middlePane = new St.BoxLayout({style_class: 'gridmenu-middle-pane'}); + if (sidebarPlacement === SidebarPlacement.LEFT) { + this.middlePane.add(this.sidebar.sidebarOuterBox, { + expand: false, + x_fill: false, + y_fill: false, + x_align: St.Align.START, + y_align: St.Align.MIDDLE + }); + } + this.middlePane.add(this.categoriesView.groupCategoriesWorkspacesScrollBox, { + x_fill: false, + y_fill: false, + x_align: St.Align.START, + y_align: St.Align.START + }); + this.middlePane.add(this.appsView.applicationsScrollBox, { + x_fill: false, + y_fill: false, + x_align: St.Align.START, + y_align: St.Align.START, + expand: false + }); + if (sidebarPlacement === SidebarPlacement.RIGHT) { + this.middlePane.add(this.sidebar.sidebarOuterBox, { + expand: false, + x_fill: false, + y_fill: false, + x_align: St.Align.START, + y_align: St.Align.MIDDLE + }); + } + + //=============mainBox================ + //set style: 'spacing: 0px' so that extra space is not added to mainBox when contextMenuBox is + //added. Only happens with themes that have set a spacing value on this class. + this.mainBox = new St.BoxLayout({ + style_class: 'menu-applications-outer-box', + style: 'spacing: 0px;', + vertical: true, + reactive: true, + show_on_set_parent: false + }); + this.mainBox.add_style_class_name('menu-applications-box'); //this is to support old themes + if (sidebarPlacement === SidebarPlacement.TOP && this.appThis.settings.showSidebar) { + this.mainBox.add(this.bottomPane); + } + this.mainBox.add_actor(this.middlePane); + if (sidebarPlacement !== SidebarPlacement.TOP || !this.appThis.settings.showSidebar) { + this.mainBox.add(this.bottomPane); + } + + this.contextMenu = new ContextMenu(this.appThis); + // Note: The context menu is added to the stage by adding it to mainBox with it's height + // set to 0. contextMenuBox is then positioned at mouse coords and above siblings. + this.contextMenu.contextMenuBox.height = 0; + this.mainBox.add(this.contextMenu.contextMenuBox, { + expand: false, + x_fill: false, + x_align: St.Align.START, + y_align: St.Align.MIDDLE + }); + + //=============menu================ + const section = new PopupMenuSection(); + section.actor.add_actor(this.mainBox); + this.appThis.menu.addMenuItem(section); + + //if a blank part of the menu was clicked on, close context menu + this.displaySignals.connect(this.mainBox, 'button-release-event',() => { + if (this.contextMenu.isOpen) { + this.contextMenu.close(); + } + }); + + //monitor mouse motion to prevent category mis-selection + this.categoriesView.categoriesBox.set_reactive(true); + this.displaySignals.connect(this.categoriesView.categoriesBox, 'motion-event', + () => this.updateMouseTracking()); + + this.appsView.applicationsGridBox.show(); + + this.mainBox.show(); + } + + updateMouseTracking() { + this.TRACKING_TIME = 70; //ms + //keep track of mouse motion to prevent misselection of another category button when moving mouse + //pointer from selected category button to app button by calculating angle of pointer movement + let [x, y] = global.get_pointer(); + if (!this.mTrack) { + this.mTrack = []; + } + //compare current position with oldest position in last 0.1 seconds. + this.mTrack.push({time: Date.now(), x: x, y: y});//push current position onto array + //remove positions older than TRACKING_TIME ago + while (this.mTrack[0].time + this.TRACKING_TIME < Date.now()) { + this.mTrack.shift(); + } + const dx = x - this.mTrack[0].x; + const dy = Math.abs(y - this.mTrack[0].y); + + const tan = dx / dy; + if (this.mainBox.get_direction() === St.TextDirection.LTR) { + this.badAngle = isFinite(tan) && tan > 0.3; + } else { + this.badAngle = isFinite(tan) && tan < -0.3; + } + } + + clearFocusedActors() { + if (this.contextMenu.isOpen) { + this.contextMenu.close(); + } + this.appsView.clearAppsViewFocusedActors(); + this.sidebar.clearSidebarFocusedActors(); + this.categoriesView.allButtonsRemoveFocusAndHover(); + } + + onMenuResized(userWidth, userHeight){ //resizing callback + this.updateMenuSize(userWidth, userHeight); + this.appsView.resizeGrid(); + } + + updateMenuSize(newWidth, newHeight) { + //if newWidth & newHeight are not supplied, use current settings values. + if (!newWidth) { + newWidth = this.appThis.settings.customMenuWidth * global.ui_scale, + newHeight= this.appThis.settings.customMenuHeight * global.ui_scale + } + + //----------height-------- + //Note: the stored menu height value is middlePane + bottomPane which is smaller than the + //menu's actual height. CategoriesView and sidebar height are not automatically + //set because ScrollBox.set_policy Gtk.PolicyType.NEVER pushes other items off the menu + let appsHeight = newHeight - this.bottomPane.height; + appsHeight = Math.max(appsHeight, 200);//set minimum height + + //---set middlePane actors to appsHeight + this.appsView.applicationsScrollBox.height = appsHeight; + this.categoriesView.groupCategoriesWorkspacesScrollBox.height = appsHeight; + + if (this.appThis.settings.showSidebar) { + //find sidebarOuterBox vertical padding + const themeNode = this.sidebar.sidebarOuterBox.get_theme_node(); + const verticalPadding = Math.max(themeNode.get_length('padding-top') + + themeNode.get_length('padding-bottom'), + themeNode.get_length('padding') * 2); + + //set sidebarScrollBox height + this.sidebar.sidebarScrollBox.set_height(-1);//undo previous set_height() + this.sidebar.sidebarScrollBox.set_height(Math.min(appsHeight - verticalPadding, + this.sidebar.sidebarScrollBox.height)); + } + + //------------width------------- + //Note: the stored menu width value is less than the menu's actual width because it doesn't + //include the outer menuBox padding, margin, etc. appsView width is not set automatically + //because I don't know how to determine it's available width in order to calculate number + //of columns to use in Clutter.GridLayout + + //find minimum width for categoriesView + sidebar (if present) + let leftSideWidth = this.categoriesView.groupCategoriesWorkspacesScrollBox.width; + if (this.appThis.settings.sidebarPlacement === SidebarPlacement.LEFT || + this.appThis.settings.sidebarPlacement === SidebarPlacement.RIGHT) { + leftSideWidth += this.sidebar.sidebarOuterBox.width; + } + + //find minimum width of bottomPane + this.searchView.searchEntry.width = 5; //Set to something small so that it gets set to its + //minimum value. + let bottomPaneMinWidth = 0; + if ((this.appThis.settings.sidebarPlacement === SidebarPlacement.TOP || + this.appThis.settings.sidebarPlacement === SidebarPlacement.BOTTOM) && + this.appThis.settings.showSidebar) { + bottomPaneMinWidth = this.bottomPane.width; + } + + //find minimum menu width + const minWidthForAppsView = 200; + const minMenuWidth = Math.max(leftSideWidth + minWidthForAppsView, bottomPaneMinWidth); + + //---set applicationsGridBox width. + const menuWidth = Math.max(minMenuWidth, newWidth); + const appsBoxWidth = Math.floor(menuWidth - leftSideWidth); + this.appsView.applicationsGridBox.width = appsBoxWidth; + const gridBoxNode = this.appsView.applicationsGridBox.get_theme_node(); + const gridBoxLRPadding = gridBoxNode.get_padding(St.Side.LEFT) + gridBoxNode.get_padding(St.Side.RIGHT); + this.currentGridBoxUsableWidth = appsBoxWidth - gridBoxLRPadding; + + //Don't change settings while resizing to avoid excessive disk writes. + if (!this.appThis.resizer.resizingInProgress) { + this.appThis.settings.customMenuHeight = newHeight / global.ui_scale; + this.appThis.settings.customMenuWidth = menuWidth / global.ui_scale; + } + } + + destroy() { + this.displaySignals.disconnectAllSignals(); + this.searchView.destroy(); + this.searchView = null; + this.appsView.destroy(); + this.appsView = null; + this.sidebar.destroy(); + this.sidebar = null; + this.categoriesView.destroy(); + this.categoriesView = null; + this.contextMenu.destroy(); + this.contextMenu = null; + this.bottomPane.destroy(); + this.middlePane.destroy(); + this.mainBox.destroy(); + } +} + +class SearchView { + constructor(appThis) { + this.appThis = appThis; + this.searchInactiveIcon = new St.Icon({ + style_class: 'menu-search-entry-icon', + icon_name: 'edit-find' + }); + this.searchActiveIcon = new St.Icon({ + style_class: 'menu-search-entry-icon', + icon_name: 'edit-clear' + }); + this.searchEntry = new St.Entry({ name: 'menu-search-entry', track_hover: true, can_focus: true}); + this.searchEntryText = this.searchEntry.clutter_text; + this.searchEntry.set_primary_icon(this.searchInactiveIcon); + this.searchBox = new St.BoxLayout({ style_class: 'menu-search-box' }); + this.searchBox.add(this.searchEntry, { expand: true }); + } + + showAndConnectSecondaryIcon() { + this.searchEntry.set_secondary_icon(this.searchActiveIcon); + this.appThis.signals.connect(this.searchEntry, 'secondary-icon-clicked', () => { //todo + this.searchEntryText.set_text('');}); + } + + hideAndDisconnectSecondaryIcon() { + this.searchEntry.set_secondary_icon(null); + this.appThis.signals.disconnect('secondary-icon-clicked', this.searchEntry); + } + + destroy() { + this.searchInactiveIcon.destroy(); + this.searchActiveIcon.destroy(); + this.searchEntry.destroy(); + this.searchBox.destroy(); + } +} + +module.exports = {Display}; diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/emoji.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/emoji.js new file mode 100644 index 0000000000..355bb05f12 --- /dev/null +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/emoji.js @@ -0,0 +1,8028 @@ +//This list is taken from https://unicode.org/emoji/charts/emoji-list.html +//Emoji List, v16.0 + +//Additional keywords are from emojilib (https://github.com/muan/emojilib) License: MIT + +const EMOJI = [ +// Smileys & Emotion +[ +'😀', +'grinning face', +'face grin smile happy joy :D' +],[ +'😃', +'grinning face with big eyes', +'face mouth open smile happy joy haha :D :) funny' +],[ +'😄', +'grinning face with smiling eyes', +'eye face mouth open smile happy joy funny haha laugh like :D :)' +],[ +'😁', +'beaming face with smiling eyes', +'eye face grin smile happy joy kawaii' +],[ +'😆', +'grinning squinting face', +'face laugh mouth satisfied smile happy joy lol haha glad XD' +],[ +'😅', +'grinning face with sweat', +'cold face open smile sweat hot happy laugh relief' +],[ +'🤣', +'rolling on the floor laughing', +'face floor laugh rofl rolling rotfl laughing lol haha' +],[ +'😂', +'face with tears of joy', +'face joy laugh tear cry tears weep happy happytears haha' +],[ +'🙂', +'slightly smiling face', +'face smile' +],[ +'🙃', +'upside-down face', +'face upside-down upside down face flipped silly smile' +],[ +'🫠', +'melting face', +'disappear dissolve liquid melt' +],[ +'😉', +'winking face', +'face wink happy mischievous secret ;) smile eye' +],[ +'😊', +'smiling face with smiling eyes', +'blush eye face smile happy flushed crush embarrassed shy joy' +],[ +'😇', +'smiling face with halo', +'angel face fantasy halo innocent heaven' +],[ +'🥰', +'smiling face with hearts', +'adore crush hearts in love face love like affection valentines infatuation' +],[ +'😍', +'smiling face with heart-eyes', +'eye face love smile smiling face with heart eyes like affection valentines infatuation crush heart' +],[ +'🤩', +'star-struck', +'eyes face grinning star starry-eyed star struck smile starry' +],[ +'😘', +'face blowing a kiss', +'face kiss love like affection valentines infatuation' +],[ +'😗', +'kissing face', +'face kiss love like 3 valentines infatuation' +],[ +'☺️', +'smiling face', +'face outlined relaxed smile blush massage happiness' +],[ +'😚', +'kissing face with closed eyes', +'closed eye face kiss love like affection valentines infatuation' +],[ +'😙', +'kissing face with smiling eyes', +'eye face kiss smile affection valentines infatuation' +],[ +'🥲', +'smiling face with tear', +'grateful proud relieved smiling tear touched sad cry pretend' +],[ +'😋', +'face savoring food', +'delicious face savouring smile yum happy joy tongue silly yummy nom' +],[ +'😛', +'face with tongue', +'face tongue prank childish playful mischievous smile' +],[ +'😜', +'winking face with tongue', +'eye face joke tongue wink prank childish playful mischievous smile' +],[ +'🤪', +'zany face', +'eye goofy large small face crazy' +],[ +'😝', +'squinting face with tongue', +'eye face horrible taste tongue prank playful mischievous smile' +],[ +'🤑', +'money-mouth face', +'face money mouth money mouth face rich dollar' +],[ +'🤗', +'hugging face', +'face hug hugging smile' +],[ +'🤭', +'face with hand over mouth', +'whoops shock sudden realization surprise face' +],[ +'🫢', +'face with open eyes and hand over mouth', +'whoops shock sudden realization surprise face' +],[ +'🫣', +'face with peeking eye', +'captivated peep stare' +],[ +'🤫', +'shushing face', +'quiet shush face shhh' +],[ +'🤔', +'thinking face', +'face thinking hmmm think consider' +],[ +'🫡', +'saluting face', +'ok salute sunny troops yes' +],[ +'🤐', +'zipper-mouth face', +'face mouth zipper zipper mouth face sealed secret' +],[ +'🤨', +'face with raised eyebrow', +'distrust skeptic disapproval disbelief mild surprise scepticism face surprise' +],[ +'😐', +'neutral face', +'deadpan face meh neutral indifference :|' +],[ +'😑', +'expressionless face', +'expressionless face inexpressive meh unexpressive indifferent - - deadpan' +],[ +'😶', +'face without mouth', +'face mouth quiet silent hellokitty' +],[ +'🫥', +'dotted line face', +'depressed disappear hide introvert invisible' +],[ +'😶‍🌫️', +'face in clouds', +'absentminded face in the fog head in clouds' +],[ +'😏', +'smirking face', +'face smirk smile mean prank smug sarcasm' +],[ +'😒', +'unamused face', +'face unamused unhappy indifference bored straight face serious sarcasm unimpressed skeptical dubious side eye' +],[ +'🙄', +'face with rolling eyes', +'eyeroll eyes face rolling frustrated' +],[ +'😬', +'grimacing face', +'face grimace teeth' +],[ +'😮‍💨', +'face exhaling', +'exhale gasp groan relief whisper whistle' +],[ +'🤥', +'lying face', +'face lie pinocchio' +],[ +'🫨', +'shaking face', +'earthquake face shaking shock vibrate loud fear double' +],[ +'🙂‍↔️', +'head shaking horizontally', +'head shaking horizontally no face' +],[ +'🙂‍↕️', +'head shaking vertically', +'head shaking vertically yes nod face agree' +],[ +'😌', +'relieved face', +'face relieved relaxed phew massage happiness' +],[ +'😔', +'pensive face', +'dejected face pensive sad depressed upset' +],[ +'😪', +'sleepy face', +'face sleep tired rest nap' +],[ +'🤤', +'drooling face', +'drooling face' +],[ +'😴', +'sleeping face', +'bed bedtime face good goodnight nap night sleep sleeping tired whatever yawn zzz' +],[ +'🫩', +'face with bags under eyes', +'bags bored exhausted eyes face fatigued late sleepy tired weary' +],[ +'😷', +'face with medical mask', +'cold doctor face mask sick ill disease' +],[ +'🤒', +'face with thermometer', +'face ill sick thermometer temperature cold fever' +],[ +'🤕', +'face with head-bandage', +'bandage face hurt injury face with head bandage injured clumsy' +],[ +'🤢', +'nauseated face', +'face nauseated vomit gross green sick throw up ill' +],[ +'🤮', +'face vomiting', +'sick vomit face' +],[ +'🤧', +'sneezing face', +'face gesundheit sneeze sick allergy' +],[ +'🥵', +'hot face', +'feverish heat stroke hot red-faced sweating face heat red' +],[ +'🥶', +'cold face', +'blue-faced cold freezing frostbite icicles face blue frozen' +],[ +'🥴', +'woozy face', +'dizzy intoxicated tipsy uneven eyes wavy mouth face wavy' +],[ +'😵', +'knocked-out face', +'dead face knocked out dizzy face spent unconscious xox dizzy' +],[ +'😵‍💫', +'face with spiral eyes', +'dizzy hypnotized spiral trouble whoa' +],[ +'🤯', +'exploding head', +'mind blown shocked face mind blown' +],[ +'🤠', +'cowboy hat face', +'cowboy cowgirl face hat' +],[ +'🥳', +'partying face', +'celebration hat horn party face woohoo' +],[ +'🥸', +'disguised face', +'disguise face glasses incognito nose pretent brows moustache' +],[ +'😎', +'smiling face with sunglasses', +'bright cool face sun sunglasses smile summer beach sunglass' +],[ +'🤓', +'nerd face', +'face geek nerd nerdy dork' +],[ +'🧐', +'face with monocle', +'stuffy wealthy face' +],[ +'😕', +'confused face', +'confused face meh indifference huh weird hmmm :/' +],[ +'🫤', +'face with diagonal mouth', +'disappointed meh skeptical unsure :/' +],[ +'😟', +'worried face', +'face worried concern nervous :(' +],[ +'🙁', +'slightly frowning face', +'face frown frowning disappointed sad upset' +],[ +'☹️', +'frowning face', +'face frown sad upset' +],[ +'😮', +'face with open mouth', +'face mouth open sympathy surprise impressed wow whoa :O' +],[ +'😯', +'hushed face', +'face hushed stunned surprised woo shh' +],[ +'😲', +'astonished face', +'astonished face shocked totally xox surprised poisoned' +],[ +'😳', +'flushed face', +'dazed face flushed blush shy flattered' +],[ +'🥺', +'pleading face', +'begging mercy puppy eyes face' +],[ +'🥹', +'face holding back tears', +'angry cry proud resist sad' +],[ +'😦', +'frowning face with open mouth', +'face frown mouth open aw what' +],[ +'😧', +'anguished face', +'anguished face stunned nervous' +],[ +'😨', +'fearful face', +'face fear fearful scared terrified nervous oops huh' +],[ +'😰', +'anxious face with sweat', +'blue cold face rushed sweat nervous' +],[ +'😥', +'sad but relieved face', +'disappointed face relieved whew phew sweat nervous' +],[ +'😢', +'crying face', +'cry face sad tear tears depressed upset' +],[ +'😭', +'loudly crying face', +'cry face sad sob tear tears upset depressed' +],[ +'😱', +'face screaming in fear', +'face fear munch scared scream omg' +],[ +'😖', +'confounded face', +'confounded face confused sick unwell oops :S' +],[ +'😣', +'persevering face', +'face persevere sick no upset oops' +],[ +'😞', +'disappointed face', +'disappointed face sad upset depressed :(' +],[ +'😓', +'downcast face with sweat', +'cold face sweat hot sad tired exercise' +],[ +'😩', +'weary face', +'face tired weary sleepy sad frustrated upset' +],[ +'😫', +'tired face', +'face tired sick whine upset frustrated' +],[ +'🥱', +'yawning face', +'bored tired yawn sleepy' +],[ +'😤', +'face with steam from nose', +'face triumph won gas phew proud pride' +],[ +'😡', +'enraged face', +'angry enraged face mad pouting rage red hate despise' +],[ +'😠', +'angry face', +'angry face mad annoyed frustrated' +],[ +'🤬', +'face with symbols on mouth', +'swearing cursing face cussing profanity expletive' +],[ +'😈', +'smiling face with horns', +'face fairy tale fantasy horns smile devil' +],[ +'👿', +'angry face with horns', +'demon devil face fantasy imp angry horns' +],[ +'💀', +'skull', +'death face fairy tale monster dead skeleton creepy' +],[ +'☠️', +'skull and crossbones', +'crossbones death face monster skull poison danger deadly scary pirate evil' +],[ +'💩', +'pile of poo', +'dung face monster poo poop hankey shitface fail turd shit' +],[ +'🤡', +'clown face', +'clown face' +],[ +'👹', +'ogre', +'creature face fairy tale fantasy monster troll red mask halloween scary creepy devil demon japanese' +],[ +'👺', +'goblin', +'creature face fairy tale fantasy monster red evil mask scary creepy japanese' +],[ +'👻', +'ghost', +'creature face fairy tale fantasy monster halloween spooky scary' +],[ +'👽', +'alien', +'creature extraterrestrial face fantasy ufo UFO paul weird outer space' +],[ +'👾', +'alien monster', +'alien creature extraterrestrial face monster ufo game arcade play' +],[ +'🤖', +'robot', +'face monster computer machine bot' +],[ +'😺', +'grinning cat', +'cat face grinning mouth open smile animal cats happy' +],[ +'😸', +'grinning cat with smiling eyes', +'cat eye face grin smile animal cats' +],[ +'😹', +'cat with tears of joy', +'cat face joy tear animal cats haha happy tears' +],[ +'😻', +'smiling cat with heart-eyes', +'cat eye face heart love smile smiling cat with heart eyes animal like affection cats valentines' +],[ +'😼', +'cat with wry smile', +'cat face ironic smile wry animal cats smirk' +],[ +'😽', +'kissing cat', +'cat eye face kiss animal cats' +],[ +'🙀', +'weary cat', +'cat face oh surprised weary animal cats munch scared scream' +],[ +'😿', +'crying cat', +'cat cry face sad tear animal tears weep cats upset' +],[ +'😾', +'pouting cat', +'cat face pouting animal cats' +],[ +'🙈', +'see-no-evil monkey', +'evil face forbidden monkey see see no evil monkey animal nature haha' +],[ +'🙉', +'hear-no-evil monkey', +'evil face forbidden hear monkey hear no evil monkey animal nature' +],[ +'🙊', +'speak-no-evil monkey', +'evil face forbidden monkey speak speak no evil monkey animal nature omg' +],[ +'💌', +'love letter', +'heart letter love mail email like affection envelope valentines' +],[ +'💘', +'heart with arrow', +'arrow cupid love like heart affection valentines' +],[ +'💝', +'heart with ribbon', +'ribbon valentine love valentines' +],[ +'💖', +'sparkling heart', +'excited sparkle love like affection valentines' +],[ +'💗', +'growing heart', +'excited growing nervous pulse like love affection valentines pink' +],[ +'💓', +'beating heart', +'beating heartbeat pulsating love like affection valentines pink heart' +],[ +'💞', +'revolving hearts', +'revolving love like affection valentines' +],[ +'💕', +'two hearts', +'love like affection valentines heart' +],[ +'💟', +'heart decoration', +'heart purple-square love like' +],[ +'❣️', +'heart exclamation', +'exclamation mark punctuation decoration love' +],[ +'💔', +'broken heart', +'break broken sad sorry heart heartbreak' +],[ +'❤️‍🔥', +'heart on fire', +'burn love lust sacred heart' +],[ +'❤️‍🩹', +'mending heart', +'healthier improving mending recovering recuperating well' +],[ +'❤️', +'red heart', +'heart love like valentines' +],[ +'🩷', +'pink heart', +'cute heart like love pink friendship affection valentines' +],[ +'🧡', +'orange heart', +'orange love like affection valentines' +],[ +'💛', +'yellow heart', +'yellow love like affection valentines' +],[ +'💚', +'green heart', +'green love like affection valentines' +],[ +'💙', +'blue heart', +'blue love like affection valentines' +],[ +'🩵', +'light blue heart', +'cyan heart light blue teal love like affection valentines' +],[ +'💜', +'purple heart', +'purple love like affection valentines' +],[ +'🤎', +'brown heart', +'brown heart coffee love like' +],[ +'🖤', +'black heart', +'black evil wicked love like' +],[ +'🩶', +'grey heart', +'grey heart silver slate love like affection valentines' +],[ +'🤍', +'white heart', +'heart white pure love like' +],[ +'💋', +'kiss mark', +'kiss lips face love like affection valentines' +],[ +'💯', +'hundred points', +'100 full hundred score perfect numbers century exam quiz test pass' +],[ +'💢', +'anger symbol', +'angry comic mad' +],[ +'💥', +'collision', +'boom comic bomb explode explosion blown' +],[ +'💫', +'dizzy', +'comic star sparkle shoot magic' +],[ +'💦', +'sweat droplets', +'comic splashing sweat water drip oops' +],[ +'💨', +'dashing away', +'comic dash running wind air fast shoo fart smoke puff' +],[ +'🕳', +'hole', +'embarrassing' +],[ +'💬', +'speech balloon', +'balloon bubble comic dialog speech words message talk chatting' +],[ +'👁️‍🗨️', +'eye in speech bubble', +'eye speech bubble witness info' +],[ +'🗨️', +'left speech bubble', +'dialog speech words message talk chatting' +],[ +'🗯️', +'right anger bubble', +'angry balloon bubble mad caption speech thinking' +],[ +'💭', +'thought balloon', +'balloon bubble comic thought cloud speech thinking dream' +],[ +'💤', +'zzz', +'comic sleep sleepy tired dream' +], + + + +// People & Body +[ +'👋', +'waving hand', +'hand wave waving hands gesture goodbye solong farewell hello hi palm' +],[ +'🤚', +'raised back of hand', +'backhand raised fingers' +],[ +'🖐', +'hand with fingers splayed', +'finger hand splayed fingers palm' +],[ +'✋', +'raised hand', +'hand high 5 high five fingers stop highfive palm ban' +],[ +'🖖', +'vulcan salute', +'finger hand spock vulcan fingers star trek' +],[ +'🫱', +'rightwards hand', +'right' +],[ +'🫲', +'leftwards hand', +'left' +],[ +'🫳', +'palm down hand', +'dismiss drop shoo' +],[ +'🫴', +'palm up hand', +'beckon catch come offer' +],[ +'🫷', +'leftwards pushing hand', +'high five leftward pushing hand refuse reject stop wait palm' +],[ +'🫸', +'rightwards pushing hand', +'high five rightward pushing hand refuse reject stop wait palm' +],[ +'👌', +'OK hand', +'hand OK ok hand fingers limbs perfect ok okay' +],[ +'🤌', +'pinched fingers', +'fingers hand gesture interrogation pinched sarcastic size tiny small' +],[ +'🤏', +'pinching hand', +'small amount tiny small size' +],[ +'✌', +'victory hand', +'hand v victory fingers ohyeah peace two' +],[ +'🤞', +'crossed fingers', +'cross finger hand luck good lucky' +],[ +'🫰', +'hand with index finger and thumb crossed', +'expensive heart love money snap cash' +],[ +'🤟', +'love-you gesture', +'hand ILY love you gesture fingers gesture' +],[ +'🤘', +'sign of the horns', +'finger hand horns rock-on fingers evil eye sign of horns rock on' +],[ +'🤙', +'call me hand', +'call hand hands gesture' +],[ +'👈', +'backhand index pointing left', +'backhand finger hand index point direction fingers left' +],[ +'👉', +'backhand index pointing right', +'backhand finger hand index point fingers direction right' +],[ +'👆', +'backhand index pointing up', +'backhand finger hand point up fingers direction' +],[ +'🖕', +'middle finger', +'finger hand fingers rude middle flipping' +],[ +'👇', +'backhand index pointing down', +'backhand down finger hand point fingers direction' +],[ +'☝', +'index pointing up', +'finger hand index point up fingers direction' +],[ +'🫵', +'index pointing at the viewer', +'finger index point you' +],[ +'👍', +'thumbs up', +'+1 hand thumb up thumbsup yes awesome good agree accept cool like' +],[ +'👎', +'thumbs down', +'-1 down hand thumb thumbsdown no dislike' +],[ +'✊', +'raised fist', +'clenched fist hand punch fingers grasp' +],[ +'👊', +'oncoming fist', +'clenched fist hand punch angry violence hit attack' +],[ +'🤛', +'left-facing fist', +'fist leftwards left facing fist hand fistbump' +],[ +'🤜', +'right-facing fist', +'fist rightwards right facing fist hand fistbump' +],[ +'👏', +'clapping hands', +'clap hand hands praise applause congrats yay' +],[ +'🙌', +'raising hands', +'celebration gesture hand hooray raised yea hands' +],[ +'🫶', +'heart hands', +'love' +],[ +'👐', +'open hands', +'hand open fingers butterfly hands' +],[ +'🤲', +'palms up together', +'prayer cupped hands hands gesture cupped' +],[ +'🤝', +'handshake', +'agreement hand meeting shake' +],[ +'🙏', +'folded hands', +'ask hand high 5 high five please pray thanks hope wish namaste highfive' +],[ +'✍', +'writing hand', +'hand write lower left ballpoint pen stationery compose' +],[ +'💅', +'nail polish', +'care cosmetics manicure nail polish beauty finger fashion' +],[ +'🤳', +'selfie', +'camera phone' +],[ +'💪', +'flexed biceps', +'biceps comic flex muscle arm hand summer strong' +],[ +'🦾', +'mechanical arm', +'accessibility prosthetic' +],[ +'🦿', +'mechanical leg', +'accessibility prosthetic' +],[ +'🦵', +'leg', +'kick limb' +],[ +'🦶', +'foot', +'kick stomp' +],[ +'👂', +'ear', +'body face hear sound listen' +],[ +'🦻', +'ear with hearing aid', +'accessibility hard of hearing' +],[ +'👃', +'nose', +'body smell sniff' +],[ +'🧠', +'brain', +'intelligent smart' +],[ +'🫀', +'anatomical heart', +'anatomical cardiology heart organ pulse health heartbeat' +],[ +'🫁', +'lungs', +'breath exhalation inhalation organ respiration breathe' +],[ +'🦷', +'tooth', +'dentist teeth' +],[ +'🦴', +'bone', +'skeleton' +],[ +'👀', +'eyes', +'eye face look watch stalk peek see' +],[ +'👁️', +'eye', +'body face look see watch stare' +],[ +'👅', +'tongue', +'body mouth playful' +],[ +'👄', +'mouth', +'lips kiss' +],[ +'🫦', +'biting lip', +'anxious fear flirting nervous uncomfortable worried sexy' +],[ +'👶', +'baby', +'young child boy girl toddler' +],[ +'🧒', +'child', +'gender-neutral unspecified gender young' +],[ +'👦', +'boy', +'young man male guy teenager' +],[ +'👧', +'girl', +'Virgo young zodiac female woman teenager' +],[ +'🧑', +'person', +'adult gender-neutral unspecified gender' +],[ +'👱', +'person: blond hair', +'blond blond-haired person hair person blond hair hairstyle' +],[ +'👨', +'man', +'adult mustache father dad guy classy sir moustache' +],[ +'🧔', +'person: beard', +'beard person bewhiskered man beard' +],[ +'🧔‍♂️', +'man: beard', +'beard man' +],[ +'🧔‍♀️', +'woman: beard', +'beard woman' +],[ +'👨‍🦰', +'man: red hair', +'adult man red hair man red hair hairstyle' +],[ +'👨‍🦱', +'man: curly hair', +'adult curly hair man man curly hair hairstyle' +],[ +'👨‍🦳', +'man: white hair', +'adult man white hair man white hair old elder' +],[ +'👨‍🦲', +'man: bald', +'adult bald man man bald hairless' +],[ +'👩', +'woman', +'adult female girls lady' +],[ +'👩‍🦰', +'woman: red hair', +'adult red hair woman woman red hair hairstyle' +],[ +'🧑‍🦰', +'person: red hair', +'adult gender-neutral person red hair unspecified gender person red hair hairstyle' +],[ +'👩‍🦱', +'woman: curly hair', +'adult curly hair woman woman curly hair hairstyle' +],[ +'🧑‍🦱', +'person: curly hair', +'adult curly hair gender-neutral person unspecified gender person curly hair hairstyle' +],[ +'👩‍🦳', +'woman: white hair', +'adult white hair woman woman white hair old elder' +],[ +'🧑‍🦳', +'person: white hair', +'adult gender-neutral person unspecified gender white hair person white hair elder old' +],[ +'👩‍🦲', +'woman: bald', +'adult bald woman woman bald hairless' +],[ +'🧑‍🦲', +'person: bald', +'adult bald gender-neutral person unspecified gender person bald hairless' +],[ +'👱‍♀️', +'woman: blond hair', +'blond-haired woman blonde hair woman woman blond hair female girl person' +],[ +'👱‍♂️', +'man: blond hair', +'blond blond-haired man hair man man blond hair male boy blonde guy person' +],[ +'🧓', +'older person', +'adult gender-neutral old unspecified gender human elder senior' +],[ +'👴', +'old man', +'adult man old human male men elder senior' +],[ +'👵', +'old woman', +'adult old woman human female women lady elder senior' +],[ +'🙍', +'person frowning', +'frown gesture worried' +],[ +'🙍‍♂️', +'man frowning', +'frowning gesture man male boy sad depressed discouraged unhappy' +],[ +'🙍‍♀️', +'woman frowning', +'frowning gesture woman female girl sad depressed discouraged unhappy' +],[ +'🙎', +'person pouting', +'gesture pouting upset' +],[ +'🙎‍♂️', +'man pouting', +'gesture man pouting male boy' +],[ +'🙎‍♀️', +'woman pouting', +'gesture pouting woman female girl' +],[ +'🙅', +'person gesturing NO', +'forbidden gesture hand prohibited person gesturing no decline' +],[ +'🙅‍♂️', +'man gesturing NO', +'forbidden gesture hand man prohibited man gesturing no male boy nope' +],[ +'🙅‍♀️', +'woman gesturing NO', +'forbidden gesture hand prohibited woman woman gesturing no female girl nope' +],[ +'🙆', +'person gesturing OK', +'gesture hand OK person gesturing ok agree' +],[ +'🙆‍♂️', +'man gesturing OK', +'gesture hand man OK man gesturing ok men boy male blue human' +],[ +'🙆‍♀️', +'woman gesturing OK', +'gesture hand OK woman woman gesturing ok women girl female pink human' +],[ +'💁', +'person tipping hand', +'hand help information sassy tipping' +],[ +'💁‍♂️', +'man tipping hand', +'man sassy tipping hand male boy human information' +],[ +'💁‍♀️', +'woman tipping hand', +'sassy tipping hand woman female girl human information' +],[ +'🙋', +'person raising hand', +'gesture hand happy raised question' +],[ +'🙋‍♂️', +'man raising hand', +'gesture man raising hand male boy' +],[ +'🙋‍♀️', +'woman raising hand', +'gesture raising hand woman female girl' +],[ +'🧏', +'deaf person', +'accessibility deaf ear hear' +],[ +'🧏‍♂️', +'deaf man', +'deaf man accessibility' +],[ +'🧏‍♀️', +'deaf woman', +'deaf woman accessibility' +],[ +'🙇', +'person bowing', +'apology bow gesture sorry respectiful' +],[ +'🙇‍♂️', +'man bowing', +'apology bowing favor gesture man sorry male boy' +],[ +'🙇‍♀️', +'woman bowing', +'apology bowing favor gesture sorry woman female girl' +],[ +'🤦', +'person facepalming', +'disbelief exasperation face palm facepalm disappointed slap hand forehead' +],[ +'🤦‍♂️', +'man facepalming', +'disbelief exasperation face palm facepalm disappointed slap hand forehead man male boy' +],[ +'🤦‍♀️', +'woman facepalming', +'disbelief exasperation face palm facepalm disappointed slap hand forehead woman female girl' +],[ +'🤷', +'person shrugging', +'doubt ignorance indifference shrug regardless' +],[ +'🤷‍♂️', +'man shrugging', +'doubt ignorance indifference man shrug male boy confused indifferent' +],[ +'🤷‍♀️', +'woman shrugging', +'doubt ignorance indifference shrug woman female girl confused indifferent' +],[ +'🧑‍⚕️', +'health worker', +'doctor healthcare nurse therapist hospital' +],[ +'👨‍⚕️', +'man health worker', +'doctor healthcare man nurse therapist human' +],[ +'👩‍⚕️', +'woman health worker', +'doctor healthcare nurse therapist woman human' +],[ +'🧑‍🎓', +'student', +'graduate learn' +],[ +'👨‍🎓', +'man student', +'graduate man student human' +],[ +'👩‍🎓', +'woman student', +'graduate student woman human' +],[ +'🧑‍🏫', +'teacher', +'instructor professor' +],[ +'👨‍🏫', +'man teacher', +'instructor man professor teacher human' +],[ +'👩‍🏫', +'woman teacher', +'instructor professor teacher woman human' +],[ +'🧑‍⚖️', +'judge', +'justice scales law' +],[ +'👨‍⚖️', +'man judge', +'judge justice man scales court human' +],[ +'👩‍⚖️', +'woman judge', +'judge justice scales woman court human' +],[ +'🧑‍🌾', +'farmer', +'gardener rancher crops' +],[ +'👨‍🌾', +'man farmer', +'farmer gardener man rancher human' +],[ +'👩‍🌾', +'woman farmer', +'farmer gardener rancher woman human' +],[ +'🧑‍🍳', +'cook', +'chef food kitchen culinary' +],[ +'👨‍🍳', +'man cook', +'chef cook man human' +],[ +'👩‍🍳', +'woman cook', +'chef cook woman human' +],[ +'🧑‍🔧', +'mechanic', +'electrician plumber tradesperson worker technician' +],[ +'👨‍🔧', +'man mechanic', +'electrician man mechanic plumber tradesperson human wrench' +],[ +'👩‍🔧', +'woman mechanic', +'electrician mechanic plumber tradesperson woman human wrench' +],[ +'🧑‍🏭', +'factory worker', +'assembly factory industrial worker labor' +],[ +'👨‍🏭', +'man factory worker', +'assembly factory industrial man worker human' +],[ +'👩‍🏭', +'woman factory worker', +'assembly factory industrial woman worker human' +],[ +'🧑‍💼', +'office worker', +'architect business manager white-collar' +],[ +'👨‍💼', +'man office worker', +'architect business man manager white-collar human' +],[ +'👩‍💼', +'woman office worker', +'architect business manager white-collar woman human' +],[ +'🧑‍🔬', +'scientist', +'biologist chemist engineer physicist chemistry' +],[ +'👨‍🔬', +'man scientist', +'biologist chemist engineer man physicist scientist human' +],[ +'👩‍🔬', +'woman scientist', +'biologist chemist engineer physicist scientist woman human' +],[ +'🧑‍💻', +'technologist', +'coder developer inventor software computer' +],[ +'👨‍💻', +'man technologist', +'coder developer inventor man software technologist engineer programmer human laptop computer' +],[ +'👩‍💻', +'woman technologist', +'coder developer inventor software technologist woman engineer programmer human laptop computer' +],[ +'🧑‍🎤', +'singer', +'actor entertainer rock star song artist performer' +],[ +'👨‍🎤', +'man singer', +'actor entertainer man rock singer star rockstar human' +],[ +'👩‍🎤', +'woman singer', +'actor entertainer rock singer star woman rockstar human' +],[ +'🧑‍🎨', +'artist', +'palette painting draw creativity' +],[ +'👨‍🎨', +'man artist', +'artist man palette painter human' +],[ +'👩‍🎨', +'woman artist', +'artist palette woman painter human' +],[ +'🧑‍✈️', +'pilot', +'plane fly airplane' +],[ +'👨‍✈️', +'man pilot', +'man pilot plane aviator human' +],[ +'👩‍✈️', +'woman pilot', +'pilot plane woman aviator human' +],[ +'🧑‍🚀', +'astronaut', +'rocket outerspace' +],[ +'👨‍🚀', +'man astronaut', +'astronaut man rocket space human' +],[ +'👩‍🚀', +'woman astronaut', +'astronaut rocket woman space human' +],[ +'🧑‍🚒', +'firefighter', +'firetruck fire' +],[ +'👨‍🚒', +'man firefighter', +'firefighter firetruck man fireman human' +],[ +'👩‍🚒', +'woman firefighter', +'firefighter firetruck woman fireman human' +],[ +'👮', +'police officer', +'cop officer police' +],[ +'👮‍♂️', +'man police officer', +'cop man officer police law legal enforcement arrest 911' +],[ +'👮‍♀️', +'woman police officer', +'cop officer police woman law legal enforcement arrest 911 female' +],[ +'🕵', +'detective', +'sleuth spy human' +],[ +'🕵️‍♂️', +'man detective', +'detective man sleuth spy crime' +],[ +'🕵️‍♀️', +'woman detective', +'detective sleuth spy woman human female' +],[ +'💂', +'guard', +'protect' +],[ +'💂‍♂️', +'man guard', +'guard man uk gb british male guy royal' +],[ +'💂‍♀️', +'woman guard', +'guard woman uk gb british female royal' +],[ +'🥷', +'ninja', +'fighter hidden stealth ninjutsu skills japanese' +],[ +'👷', +'construction worker', +'construction hat worker labor build' +],[ +'👷‍♂️', +'man construction worker', +'construction man worker male human wip guy build labor' +],[ +'👷‍♀️', +'woman construction worker', +'construction woman worker female human wip build labor' +],[ +'🫅', +'person with crown', +'monarch noble regal royalty king queen' +],[ +'🤴', +'prince', +'boy man male crown royal king' +],[ +'👸', +'princess', +'fairy tale fantasy girl woman female blond crown royal queen' +],[ +'👳', +'person wearing turban', +'turban headdress' +],[ +'👳‍♂️', +'man wearing turban', +'man turban male indian hinduism arabs' +],[ +'👳‍♀️', +'woman wearing turban', +'turban woman female indian hinduism arabs' +],[ +'👲', +'person with skullcap', +'cap gua pi mao hat person skullcap man with skullcap male boy chinese' +],[ +'🧕', +'woman with headscarf', +'headscarf hijab mantilla tichel bandana head kerchief female' +],[ +'🤵', +'person in tuxedo', +'groom person tuxedo man in tuxedo couple marriage wedding' +],[ +'🤵‍♂️', +'man in tuxedo', +'man tuxedo formal fashion' +],[ +'🤵‍♀️', +'woman in tuxedo', +'tuxedo woman formal fashion' +],[ +'👰', +'person with veil', +'bride person veil wedding bride with veil couple marriage woman' +],[ +'👰‍♂️', +'man with veil', +'man veil wedding marriage' +],[ +'👰‍♀️', +'woman with veil', +'veil woman wedding marriage' +],[ +'🤰', +'pregnant woman', +'belly full pregnant woman baby' +],[ +'🫃', +'pregnant man', +'belly full pregnant' +],[ +'🫄', +'pregnant person', +'belly full pregnant' +],[ +'🤱', +'breast-feeding', +'baby breast nursing breast feeding' +],[ +'👩‍🍼', +'woman feeding baby', +'baby feeding nursing woman birth food' +],[ +'👨‍🍼', +'man feeding baby', +'baby feeding man nursing birth food' +],[ +'🧑‍🍼', +'person feeding baby', +'baby feeding nursing person birth food' +],[ +'👼', +'baby angel', +'angel baby face fairy tale fantasy heaven wings halo' +],[ +'🎅', +'Santa Claus', +'celebration Christmas claus father santa santa claus festival man male xmas father christmas' +],[ +'🤶', +'Mrs. Claus', +'celebration Christmas claus mother Mrs. mrs claus woman female xmas mother christmas' +],[ +'🧑‍🎄', +'mx claus', +'Claus, christmas christmas' +],[ +'🦸', +'superhero', +'good hero heroine superpower marvel' +],[ +'🦸‍♂️', +'man superhero', +'good hero man superpower male superpowers' +],[ +'🦸‍♀️', +'woman superhero', +'good hero heroine superpower woman female superpowers' +],[ +'🦹', +'supervillain', +'criminal evil superpower villain marvel' +],[ +'🦹‍♂️', +'man supervillain', +'criminal evil man superpower villain male bad hero superpowers' +],[ +'🦹‍♀️', +'woman supervillain', +'criminal evil superpower villain woman female bad heroine superpowers' +],[ +'🧙', +'mage', +'sorcerer sorceress witch wizard magic' +],[ +'🧙‍♂️', +'man mage', +'sorcerer wizard man male mage' +],[ +'🧙‍♀️', +'woman mage', +'sorceress witch woman female mage' +],[ +'🧚', +'fairy', +'Oberon Puck Titania wings magical' +],[ +'🧚‍♂️', +'man fairy', +'Oberon Puck man male' +],[ +'🧚‍♀️', +'woman fairy', +'Titania woman female' +],[ +'🧛', +'vampire', +'Dracula undead blood twilight' +],[ +'🧛‍♂️', +'man vampire', +'Dracula undead man male dracula' +],[ +'🧛‍♀️', +'woman vampire', +'undead woman female' +],[ +'🧜', +'merperson', +'mermaid merman merwoman sea' +],[ +'🧜‍♂️', +'merman', +'Triton man male triton' +],[ +'🧜‍♀️', +'mermaid', +'merwoman woman female ariel' +],[ +'🧝', +'elf', +'magical LOTR style' +],[ +'🧝‍♂️', +'man elf', +'magical man male' +],[ +'🧝‍♀️', +'woman elf', +'magical woman female' +],[ +'🧞', +'genie', +'djinn (non-human color) magical wishes' +],[ +'🧞‍♂️', +'man genie', +'djinn man male' +],[ +'🧞‍♀️', +'woman genie', +'djinn woman female' +],[ +'🧟', +'zombie', +'undead walking dead (non-human color) dead' +],[ +'🧟‍♂️', +'man zombie', +'undead walking dead man male dracula' +],[ +'🧟‍♀️', +'woman zombie', +'undead walking dead woman female' +],[ +'🧌', +'troll', +'fairy tale fantasy monster ogre' +],[ +'💆', +'person getting massage', +'face massage salon relax' +],[ +'💆‍♂️', +'man getting massage', +'face man massage male boy head' +],[ +'💆‍♀️', +'woman getting massage', +'face massage woman female girl head' +],[ +'💇', +'person getting haircut', +'barber beauty haircut parlor hairstyle' +],[ +'💇‍♂️', +'man getting haircut', +'haircut man male boy' +],[ +'💇‍♀️', +'woman getting haircut', +'haircut woman female girl' +],[ +'🚶', +'person walking', +'hike walking move human feet steps' +],[ +'🚶‍♂️', +'man walking', +'hike man walking move human feet steps' +],[ +'🚶‍♀️', +'woman walking', +'hike walking woman move human feet steps female' +],[ +'🚶‍➡️', +'person walking facing right', +'hike man walking move human feet steps' +],[ +'🚶‍♂️‍➡️', +'man walking facing right', +'hike man walking move human feet steps' +],[ +'🚶‍♀️‍➡️', +'woman walking facing right', +'hike walking woman move human feet steps female' +],[ +'🧍', +'person standing', +'stand standing still' +],[ +'🧍‍♂️', +'man standing', +'man standing still' +],[ +'🧍‍♀️', +'woman standing', +'standing woman still' +],[ +'🧎', +'person kneeling', +'person kneeling pray respectful' +],[ +'🧎‍♂️', +'man kneeling', +'kneeling man pray respectful' +],[ +'🧎‍♀️', +'woman kneeling', +'kneeling woman respectful pray' +],[ +'🧎‍➡️', +'person kneeling facing right', +'person kneeling pray respectful' +],[ +'🧎‍♀️‍➡️', +'woman kneeling facing right', +'kneeling woman respectful pray' +],[ +'🧎‍♂️‍➡️', +'man kneeling facing right', +'kneeling man pray respectful' +],[ +'🧑‍🦯', +'person with white cane', +'accessibility blind person with probing cane' +],[ +'🧑‍🦯‍➡️', +'person with white cane facing right', +'accessibility blind person with probing cane' +],[ +'👨‍🦯', +'man with white cane', +'accessibility blind man with probing cane' +],[ +'👨‍🦯‍➡️', +'man with white cane facing right', +'accessibility blind man with probing cane' +],[ +'👩‍🦯', +'woman with white cane', +'accessibility blind woman with probing cane' +],[ +'👩‍🦯‍➡️', +'woman with white cane facing right', +'accessibility blind woman with probing cane' +],[ +'🧑‍🦼', +'person in motorized wheelchair', +'accessibility wheelchair disability' +],[ +'🧑‍🦼‍➡️', +'person in motorized wheelchair facing right', +'accessibility wheelchair disability' +],[ +'👨‍🦼', +'man in motorized wheelchair', +'accessibility man wheelchair disability' +],[ +'👨‍🦼‍➡️', +'man in motorized wheelchair facing right', +'accessibility man wheelchair disability' +],[ +'👩‍🦼', +'woman in motorized wheelchair', +'accessibility wheelchair woman disability' +],[ +'👩‍🦼‍➡️', +'woman in motorized wheelchair facing right', +'accessibility wheelchair woman disability' +],[ +'🧑‍🦽', +'person in manual wheelchair', +'accessibility wheelchair disability' +],[ +'🧑‍🦽‍➡️', +'person in manual wheelchair facing right', +'accessibility wheelchair disability' +],[ +'👨‍🦽', +'man in manual wheelchair', +'accessibility man wheelchair disability' +],[ +'👨‍🦽‍➡️', +'man in manual wheelchair facing right', +'accessibility man wheelchair disability' +],[ +'👩‍🦽', +'woman in manual wheelchair', +'accessibility wheelchair woman disability' +],[ +'👩‍🦽‍➡️', +'woman in manual wheelchair facing right', +'accessibility wheelchair woman disability' +],[ +'🏃', +'person running', +'marathon racing running sprint move fast exercise race' +],[ +'🏃‍♂️', +'man running', +'man marathon racing running sprint move fast exercise race male' +],[ +'🏃‍♀️', +'woman running', +'marathon racing running sprint woman move fast exercise race female' +],[ +'🏃‍➡️', +'person running facing right', +'marathon running sprint move fast exercise race' +],[ +'🏃‍♀️‍➡️', +'woman running facing right', +'marathon racing running sprint woman move fast exercise race female' +],[ +'🏃‍♂️‍➡️', +'man running facing right', +'man marathon racing running sprint move fast exercise race male' +],[ +'💃', +'woman dancing', +'dance dancing woman female girl fun' +],[ +'🕺', +'man dancing', +'dance dancing man male boy fun dancer' +],[ +'🕴', +'person in suit levitating', +'business person suit man in suit levitating levitate hover jump' +],[ +'👯', +'people with bunny ears', +'bunny ear dancer partying perform costume' +],[ +'👯‍♂️', +'men with bunny ears', +'bunny ear dancer men partying male bunny boys' +],[ +'👯‍♀️', +'women with bunny ears', +'bunny ear dancer partying women female bunny girls' +],[ +'🧖', +'person in steamy room', +'sauna steam room hamam steambath relax spa' +],[ +'🧖‍♂️', +'man in steamy room', +'sauna steam room male man spa steamroom' +],[ +'🧖‍♀️', +'woman in steamy room', +'sauna steam room female woman spa steamroom' +],[ +'🧗', +'person climbing', +'climber sport' +],[ +'🧗‍♂️', +'man climbing', +'climber sports hobby man male rock' +],[ +'🧗‍♀️', +'woman climbing', +'climber sports hobby woman female rock' +],[ +'🤺', +'person fencing', +'fencer fencing sword sports' +],[ +'🏇', +'horse racing', +'horse jockey racehorse racing animal betting competition gambling luck' +],[ +'⛷️', +'skier', +'ski snow sports winter' +],[ +'🏂', +'snowboarder', +'ski snow snowboard sports winter' +],[ +'🏌', +'person golfing', +'ball golf sports business' +],[ +'🏌️‍♂️', +'man golfing', +'golf man sport' +],[ +'🏌️‍♀️', +'woman golfing', +'golf woman sports business female' +],[ +'🏄', +'person surfing', +'surfing sport sea' +],[ +'🏄‍♂️', +'man surfing', +'man surfing sports ocean sea summer beach' +],[ +'🏄‍♀️', +'woman surfing', +'surfing woman sports ocean sea summer beach female' +],[ +'🚣', +'person rowing boat', +'boat rowboat sport move' +],[ +'🚣‍♂️', +'man rowing boat', +'boat man rowboat sports hobby water ship' +],[ +'🚣‍♀️', +'woman rowing boat', +'boat rowboat woman sports hobby water ship female' +],[ +'🏊', +'person swimming', +'swim sport pool' +],[ +'🏊‍♂️', +'man swimming', +'man swim sports exercise human athlete water summer' +],[ +'🏊‍♀️', +'woman swimming', +'swim woman sports exercise human athlete water summer female' +],[ +'⛹', +'person bouncing ball', +'ball sports human' +],[ +'⛹️‍♂️', +'man bouncing ball', +'ball man sport' +],[ +'⛹️‍♀️', +'woman bouncing ball', +'ball woman sports human female' +],[ +'🏋', +'person lifting weights', +'lifter weight sports training exercise' +],[ +'🏋️‍♂️', +'man lifting weights', +'man weight lifter sport' +],[ +'🏋️‍♀️', +'woman lifting weights', +'weight lifter woman sports training exercise female' +],[ +'🚴', +'person biking', +'bicycle biking cyclist sport move' +],[ +'🚴‍♂️', +'man biking', +'bicycle biking cyclist man sports bike exercise hipster' +],[ +'🚴‍♀️', +'woman biking', +'bicycle biking cyclist woman sports bike exercise hipster female' +],[ +'🚵', +'person mountain biking', +'bicycle bicyclist bike cyclist mountain sport move' +],[ +'🚵‍♂️', +'man mountain biking', +'bicycle bike cyclist man mountain transportation sports human race' +],[ +'🚵‍♀️', +'woman mountain biking', +'bicycle bike biking cyclist mountain woman transportation sports human race female' +],[ +'🤸', +'person cartwheeling', +'cartwheel gymnastics sport gymnastic' +],[ +'🤸‍♂️', +'man cartwheeling', +'cartwheel gymnastics man' +],[ +'🤸‍♀️', +'woman cartwheeling', +'cartwheel gymnastics woman' +],[ +'🤼', +'people wrestling', +'wrestle wrestler sport' +],[ +'🤼‍♂️', +'men wrestling', +'men wrestle sports wrestlers' +],[ +'🤼‍♀️', +'women wrestling', +'women wrestle sports wrestlers' +],[ +'🤽', +'person playing water polo', +'polo water sport' +],[ +'🤽‍♂️', +'man playing water polo', +'man water polo sports pool' +],[ +'🤽‍♀️', +'woman playing water polo', +'water polo woman sports pool' +],[ +'🤾', +'person playing handball', +'ball handball sport' +],[ +'🤾‍♂️', +'man playing handball', +'handball man sports' +],[ +'🤾‍♀️', +'woman playing handball', +'handball woman sports' +],[ +'🤹', +'person juggling', +'balance juggle multitask skill performance' +],[ +'🤹‍♂️', +'man juggling', +'juggling man multitask juggle balance skill' +],[ +'🤹‍♀️', +'woman juggling', +'juggling multitask woman juggle balance skill' +],[ +'🧘', +'person in lotus position', +'meditation yoga serenity meditate' +],[ +'🧘‍♂️', +'man in lotus position', +'meditation yoga man male serenity zen mindfulness' +],[ +'🧘‍♀️', +'woman in lotus position', +'meditation yoga woman female serenity zen mindfulness' +],[ +'🛀', +'person taking bath', +'bath bathtub clean shower bathroom' +],[ +'🛌', +'person in bed', +'hotel sleep bed rest' +],[ +'🧑‍🤝‍🧑', +'people holding hands', +'couple hand hold holding hands person friendship' +],[ +'👭', +'women holding hands', +'couple hand holding hands women pair friendship love like female people human' +],[ +'👫', +'woman and man holding hands', +'couple hand hold holding hands man woman pair people human love date dating like affection valentines marriage' +],[ +'👬', +'men holding hands', +'couple Gemini holding hands man men twins zodiac pair love like bromance friendship people human' +],[ +'💏', +'kiss', +'couple pair valentines love like dating marriage' +],[ +'👩‍❤️‍💋‍👨', +'kiss: woman, man', +'couple kiss man woman kiss woman man love' +],[ +'👨‍❤️‍💋‍👨', +'kiss: man, man', +'couple kiss man kiss man man pair valentines love like dating marriage' +],[ +'👩‍❤️‍💋‍👩', +'kiss: woman, woman', +'couple kiss woman kiss woman woman pair valentines love like dating marriage' +],[ +'💑', +'couple with heart', +'couple love pair like affection human dating valentines marriage' +],[ +'👩‍❤️‍👨', +'couple with heart: woman, man', +'couple couple with heart love man woman couple with heart woman man' +],[ +'👨‍❤️‍👨', +'couple with heart: man, man', +'couple couple with heart love man couple with heart man man pair like affection human dating valentines marriage' +],[ +'👩‍❤️‍👩', +'couple with heart: woman, woman', +'couple couple with heart love woman couple with heart woman woman pair like affection human dating valentines marriage' +],[ +'👨‍👩‍👦', +'family: man, woman, boy', +'home parents children mom dad father mother man women boy people human' +],[ +'👨‍👩‍👧', +'family: man, woman, girl', +'home parents children mom dad father mother man women girl people human' +],[ +'👨‍👩‍👧‍👦', +'family: man, woman, girl, boy', +'home parents children mom dad father mother man women girl boy people human' +],[ +'👨‍👩‍👦‍👦', +'family: man, woman, boy, boy', +'home parents children mom dad father mother man women boy people human' +],[ +'👨‍👩‍👧‍👧', +'family: man, woman, girl, girl', +'home parents children mom dad father mother man women girl people human' +],[ +'👨‍👨‍👦', +'family: man, man, boy', +'home parents children dad father man boy people human' +],[ +'👨‍👨‍👧', +'family: man, man, girl', +'home parents children dad father man girl people human' +],[ +'👨‍👨‍👧‍👦', +'family: man, man, girl, boy', +'home parents children dad father man girl boy people human' +],[ +'👨‍👨‍👦‍👦', +'family: man, man, boy, boy', +'home parents children dad father man boy people human' +],[ +'👨‍👨‍👧‍👧', +'family: man, man, girl, girl', +'home parents children dad father man girl people human' +],[ +'👩‍👩‍👦', +'family: woman, woman, boy', +'home parents children mom mother women boy people human' +],[ +'👩‍👩‍👧', +'family: woman, woman, girl', +'home parents children mom mother women girl people human' +],[ +'👩‍👩‍👧‍👦', +'family: woman, woman, girl, boy', +'home parents children mom mother women girl boy people human' +],[ +'👩‍👩‍👦‍👦', +'family: woman, woman, boy, boy', +'home parents children mom mother women boy people human' +],[ +'👩‍👩‍👧‍👧', +'family: woman, woman, girl, girl', +'home parents children mom mother women girl people human' +],[ +'👨‍👦', +'family: man, boy', +'home parents children dad father man boy people human' +],[ +'👨‍👦‍👦', +'family: man, boy, boy', +'home parents children dad father man boy people human' +],[ +'👨‍👧', +'family: man, girl', +'home parents children dad father man girl people human' +],[ +'👨‍👧‍👦', +'family: man, girl, boy', +'home parents children dad father man girl boy people human' +],[ +'👨‍👧‍👧', +'family: man, girl, girl', +'home parents children dad father man girl people human' +],[ +'👩‍👦', +'family: woman, boy', +'home parents children mom mother women boy people human' +],[ +'👩‍👦‍👦', +'family: woman, boy, boy', +'home parents children mom mother women boy people human' +],[ +'👩‍👧', +'family: woman, girl', +'home parents children mom mother women girl people human' +],[ +'👩‍👧‍👦', +'family: woman, girl, boy', +'home parents children mom mother women girl boy people human' +],[ +'👩‍👧‍👧', +'family: woman, girl, girl', +'home parents children mom mother women girl people human' +],[ +'🗣️', +'speaking head', +'face head silhouette speak speaking user person human sing say talk' +],[ +'👤', +'bust in silhouette', +'bust silhouette user person human' +],[ +'👥', +'busts in silhouette', +'bust silhouette user person human group team' +],[ +'🫂', +'people hugging', +'goodbye hello hug thanks care' +],[ +'👪', +'family', +'home parents children mom dad father mother man women girl boy people human' +],[ +'🧑‍🧑‍🧒', +'family: adult, adult, child', +'home parents children mom dad father mother man women girl boy people human' +],[ +'🧑‍🧑‍🧒‍🧒', +'family: adult, adult, child, child', +'home parents children mom dad father mother man women girl boy people human' +],[ +'🧑‍🧒', +'family: adult, child', +'home parents children mom dad father mother man women girl boy people human' +],[ +'🧑‍🧒‍🧒', +'family: adult, child, child', +'home parents children mom dad father mother man women girl boy people human' +],[ +'👣', +'footprints', +'barefoot clothing footprint print feet tracking walking beach' +],[ +'🫆', +'fingerprint', +'clue crime detective fingerprint forensics identity mystery print safety trace' +],[ +'🦰', +'red hair', +'ginger redhead' +],[ +'🦱', +'curly hair', +'afro curly ringlets' +],[ +'🦳', +'white hair', +'gray hair old white' +],[ +'🦲', +'bald', +'chemotherapy hairless no hair shaven' +], + + + +// Animals & Nature +[ +'🐵', +'monkey face', +'face monkey animal nature circus' +],[ +'🐒', +'monkey', +'animal nature banana circus' +],[ +'🦍', +'gorilla', +'animal nature circus' +],[ +'🦧', +'orangutan', +'ape animal' +],[ +'🐶', +'dog face', +'dog face pet animal friend nature woof puppy faithful' +],[ +'🐕', +'dog', +'pet animal nature friend doge faithful' +],[ +'🦮', +'guide dog', +'accessibility blind guide animal' +],[ +'🐕‍🦺', +'service dog', +'accessibility assistance dog service blind animal' +],[ +'🐩', +'poodle', +'dog animal 101 nature pet' +],[ +'🐺', +'wolf', +'face animal nature wild' +],[ +'🦊', +'fox', +'face animal nature' +],[ +'🦝', +'raccoon', +'curious sly animal nature' +],[ +'🐱', +'cat face', +'cat face pet animal meow nature kitten' +],[ +'🐈', +'cat', +'pet animal meow cats' +],[ +'🐈‍⬛', +'black cat', +'black cat unlucky superstition luck' +],[ +'🦁', +'lion', +'face Leo zodiac animal nature' +],[ +'🐯', +'tiger face', +'face tiger animal cat danger wild nature roar' +],[ +'🐅', +'tiger', +'animal nature roar' +],[ +'🐆', +'leopard', +'animal nature' +],[ +'🐴', +'horse face', +'face horse animal brown nature' +],[ +'🫎', +'moose', +'animal antlers elk mammal moose canada sweden' +],[ +'🫏', +'donkey', +'animal ass burro donkey mammal mule stubborn' +],[ +'🐎', +'horse', +'equestrian racehorse racing animal gamble luck' +],[ +'🦄', +'unicorn', +'face animal nature mystical' +],[ +'🦓', +'zebra', +'stripe animal nature stripes safari' +],[ +'🦌', +'deer', +'animal nature horns venison' +],[ +'🦬', +'bison', +'buffalo herd wisent ox' +],[ +'🐮', +'cow face', +'cow face beef ox animal nature moo milk' +],[ +'🐂', +'ox', +'bull Taurus zodiac animal cow beef' +],[ +'🐃', +'water buffalo', +'buffalo water animal nature ox cow' +],[ +'🐄', +'cow', +'beef ox animal nature moo milk' +],[ +'🐷', +'pig face', +'face pig animal oink nature' +],[ +'🐖', +'pig', +'sow animal nature' +],[ +'🐗', +'boar', +'pig animal nature' +],[ +'🐽', +'pig nose', +'face nose pig animal oink' +],[ +'🐏', +'ram', +'Aries male sheep zodiac animal nature' +],[ +'🐑', +'ewe', +'female sheep animal nature wool shipit' +],[ +'🐐', +'goat', +'Capricorn zodiac animal nature' +],[ +'🐪', +'camel', +'dromedary hump animal hot desert' +],[ +'🐫', +'two-hump camel', +'bactrian camel hump two hump camel animal nature hot desert' +],[ +'🦙', +'llama', +'alpaca guanaco vicuña wool animal nature' +],[ +'🦒', +'giraffe', +'spots animal nature safari' +],[ +'🐘', +'elephant', +'animal nature nose th circus' +],[ +'🦣', +'mammoth', +'extinction large tusk woolly elephant tusks' +],[ +'🦏', +'rhinoceros', +'animal nature horn' +],[ +'🦛', +'hippopotamus', +'hippo animal nature' +],[ +'🐭', +'mouse face', +'face mouse animal nature cheese wedge rodent' +],[ +'🐁', +'mouse', +'animal nature rodent' +],[ +'🐀', +'rat', +'animal mouse rodent' +],[ +'🐹', +'hamster', +'face pet animal nature' +],[ +'🐰', +'rabbit face', +'bunny face pet rabbit animal nature spring magic' +],[ +'🐇', +'rabbit', +'bunny pet animal nature magic spring' +],[ +'🐿️', +'chipmunk', +'squirrel animal nature rodent' +],[ +'🦫', +'beaver', +'dam animal rodent' +],[ +'🦔', +'hedgehog', +'spiny animal nature' +],[ +'🦇', +'bat', +'vampire animal nature blind' +],[ +'🐻', +'bear', +'face animal nature wild' +],[ +'🐻‍❄️', +'polar bear', +'arctic bear white animal' +],[ +'🐨', +'koala', +'bear animal nature' +],[ +'🐼', +'panda', +'face animal nature' +],[ +'🦥', +'sloth', +'lazy slow animal' +],[ +'🦦', +'otter', +'fishing playful animal' +],[ +'🦨', +'skunk', +'stink animal' +],[ +'🦘', +'kangaroo', +'Australia joey jump marsupial animal nature australia hop' +],[ +'🦡', +'badger', +'honey badger pester animal nature honey' +],[ +'🐾', +'paw prints', +'feet paw print animal tracking footprints dog cat pet' +],[ +'🦃', +'turkey', +'bird animal' +],[ +'🐔', +'chicken', +'bird animal cluck nature' +],[ +'🐓', +'rooster', +'bird animal nature chicken' +],[ +'🐣', +'hatching chick', +'baby bird chick hatching animal chicken egg born' +],[ +'🐤', +'baby chick', +'baby bird chick animal chicken' +],[ +'🐥', +'front-facing baby chick', +'baby bird chick front facing baby chick animal chicken' +],[ +'🐦', +'bird', +'animal nature fly tweet spring' +],[ +'🐧', +'penguin', +'bird animal nature' +],[ +'🕊️', +'dove', +'bird fly peace animal' +],[ +'🦅', +'eagle', +'bird animal nature' +],[ +'🦆', +'duck', +'bird animal nature mallard' +],[ +'🦢', +'swan', +'bird cygnet ugly duckling animal nature' +],[ +'🦉', +'owl', +'bird wise animal nature hoot' +],[ +'🦤', +'dodo', +'extinction large Mauritius animal bird' +],[ +'🪶', +'feather', +'bird flight light plumage fly' +],[ +'🦩', +'flamingo', +'flamboyant tropical animal' +],[ +'🦚', +'peacock', +'bird ostentatious peahen proud animal nature' +],[ +'🦜', +'parrot', +'bird pirate talk animal nature' +],[ +'🪽', +'wing', +'angelic aviation bird flying mythology wing' +],[ +'🐦‍⬛', +'black bird', +'bird black crow raven rook' +],[ +'🪿', +'goose', +'bird fowl wild goose honk silly' +],[ +'🐦‍🔥', +'phoenix', +'fantasy firebird phoenix rebirth reincarnation' +],[ +'🐸', +'frog', +'face animal nature croak toad' +],[ +'🐊', +'crocodile', +'animal nature reptile lizard alligator' +],[ +'🐢', +'turtle', +'terrapin tortoise animal slow nature' +],[ +'🦎', +'lizard', +'reptile animal nature' +],[ +'🐍', +'snake', +'bearer Ophiuchus serpent zodiac animal evil nature hiss python' +],[ +'🐲', +'dragon face', +'dragon face fairy tale animal myth nature chinese green' +],[ +'🐉', +'dragon', +'fairy tale animal myth nature chinese green' +],[ +'🦕', +'sauropod', +'brachiosaurus brontosaurus diplodocus animal nature dinosaur extinct' +],[ +'🦖', +'T-Rex', +'Tyrannosaurus Rex t rex animal nature dinosaur tyrannosaurus extinct' +],[ +'🐳', +'spouting whale', +'face spouting whale animal nature sea ocean' +],[ +'🐋', +'whale', +'animal nature sea ocean' +],[ +'🐬', +'dolphin', +'flipper animal nature fish sea ocean fins beach' +],[ +'🦭', +'seal', +'sea Lion animal creature sea' +],[ +'🐟', +'fish', +'Pisces zodiac animal food nature' +],[ +'🐠', +'tropical fish', +'fish tropical animal swim ocean beach nemo' +],[ +'🐡', +'blowfish', +'fish animal nature food sea ocean' +],[ +'🦈', +'shark', +'fish animal nature sea ocean jaws fins beach' +],[ +'🐙', +'octopus', +'animal creature ocean sea nature beach' +],[ +'🐚', +'spiral shell', +'shell spiral nature sea beach' +],[ +'🪸', +'coral', +'ocean sea reef' +],[ +'🪼', +'jellyfish', +'burn invertebrate jellyfish marine ouch stinger tentacles' +],[ +'🐌', +'snail', +'slow animal shell' +],[ +'🦋', +'butterfly', +'insect pretty animal nature caterpillar' +],[ +'🐛', +'bug', +'insect animal nature worm' +],[ +'🐜', +'ant', +'insect animal nature bug' +],[ +'🐝', +'honeybee', +'bee insect animal nature bug spring honey' +],[ +'🪲', +'beetle', +'bug insect' +],[ +'🐞', +'lady beetle', +'beetle insect ladybird ladybug animal nature' +],[ +'🦗', +'cricket', +'grasshopper Orthoptera animal chirp' +],[ +'🪳', +'cockroach', +'insect pest roach pests' +],[ +'🕷️', +'spider', +'insect animal arachnid' +],[ +'🕸️', +'spider web', +'spider web animal insect arachnid silk' +],[ +'🦂', +'scorpion', +'scorpio Scorpio zodiac animal arachnid' +],[ +'🦟', +'mosquito', +'disease fever malaria pest virus animal nature insect' +],[ +'🪰', +'fly', +'disease maggot pest rotting insect' +],[ +'🪱', +'worm', +'annelid earthworm parasite animal' +],[ +'🦠', +'microbe', +'amoeba bacteria virus germs' +],[ +'💐', +'bouquet', +'flower flowers nature spring' +],[ +'🌸', +'cherry blossom', +'blossom cherry flower nature plant spring' +],[ +'💮', +'white flower', +'flower japanese spring' +],[ +'🪷', +'lotus', +'Buddhism flower Hinduism India purity Vietnam' +],[ +'🏵️', +'rosette', +'plant flower decoration military' +],[ +'🌹', +'rose', +'flower flowers valentines love spring' +],[ +'🥀', +'wilted flower', +'flower wilted plant nature' +],[ +'🌺', +'hibiscus', +'flower plant vegetable flowers beach' +],[ +'🌻', +'sunflower', +'flower sun nature plant fall' +],[ +'🌼', +'blossom', +'blossom flower nature flowers yellow' +],[ +'🌷', +'tulip', +'flower flowers plant nature summer spring blossom' +],[ +'🪻', +'hyacinth', +'bluebonnet flower hyacinth lavender lupine snapdragon blossom springtime' +],[ +'🌱', +'seedling', +'young plant nature grass lawn spring' +],[ +'🪴', +'potted plant', +'boring grow house nurturing plant useless greenery' +],[ +'🌲', +'evergreen tree', +'tree plant nature' +],[ +'🌳', +'deciduous tree', +'deciduous shedding tree plant nature' +],[ +'🌴', +'palm tree', +'palm tree plant vegetable nature summer beach mojito tropical' +],[ +'🌵', +'cactus', +'plant vegetable nature' +],[ +'🌾', +'sheaf of rice', +'ear grain rice nature plant' +],[ +'🌿', +'herb', +'leaf vegetable plant medicine weed grass lawn' +],[ +'☘️', +'shamrock', +'plant vegetable nature irish clover' +],[ +'🍀', +'four leaf clover', +'4 clover four four-leaf clover leaf vegetable plant nature lucky irish' +],[ +'🍁', +'maple leaf', +'falling leaf maple nature plant vegetable ca fall' +],[ +'🍂', +'fallen leaf', +'falling leaf nature plant vegetable leaves' +],[ +'🍃', +'leaf fluttering in wind', +'blow flutter leaf wind nature plant tree vegetable grass lawn spring' +],[ +'🪹', +'empty nest', +'nesting birds' +],[ +'🪺', +'nest with eggs', +'nesting birds' +],[ +'🍄', +'mushroom', +'mushroom toadstool fungus' +],[ +'🪾', +'leafless tree', +'bare barren branches dead drought leafless tree trunk winter wood' +], + + + +// Food & Drink +[ +'🍇', +'grapes', +'fruit grape food wine' +],[ +'🍈', +'melon', +'fruit nature food' +],[ +'🍉', +'watermelon', +'fruit food picnic summer' +],[ +'🍊', +'tangerine', +'fruit orange food nature' +],[ +'🍋', +'lemon', +'citrus fruit nature' +],[ +'🍋‍🟩', +'lime', +'citrus fruit lime tropical' +],[ +'🍌', +'banana', +'fruit food monkey' +],[ +'🍍', +'pineapple', +'fruit nature food' +],[ +'🥭', +'mango', +'fruit tropical food' +],[ +'🍎', +'red apple', +'apple fruit red mac school' +],[ +'🍏', +'green apple', +'apple fruit green nature' +],[ +'🍐', +'pear', +'fruit nature food' +],[ +'🍑', +'peach', +'fruit nature food' +],[ +'🍒', +'cherries', +'berries cherry fruit red food' +],[ +'🍓', +'strawberry', +'berry fruit food nature' +],[ +'🫐', +'blueberries', +'berry bilberry blue blueberry fruit' +],[ +'🥝', +'kiwi fruit', +'food fruit kiwi' +],[ +'🍅', +'tomato', +'fruit vegetable nature food' +],[ +'🫒', +'olive', +'food fruit' +],[ +'🥥', +'coconut', +'palm piña colada fruit nature food' +],[ +'🥑', +'avocado', +'food fruit' +],[ +'🍆', +'eggplant', +'aubergine vegetable nature food' +],[ +'🥔', +'potato', +'food vegetable tuber vegatable starch' +],[ +'🥕', +'carrot', +'food vegetable orange' +],[ +'🌽', +'ear of corn', +'corn ear maize maze food vegetable plant' +],[ +'🌶️', +'hot pepper', +'hot pepper food spicy chilli chili' +],[ +'🫑', +'bell pepper', +'capsicum pepper vegetable fruit plant' +],[ +'🥒', +'cucumber', +'food pickle vegetable fruit' +],[ +'🥬', +'leafy green', +'bok choy cabbage kale lettuce food vegetable plant' +],[ +'🥦', +'broccoli', +'wild cabbage fruit food vegetable' +],[ +'🧄', +'garlic', +'flavoring food spice cook' +],[ +'🧅', +'onion', +'flavoring cook food spice' +],[ +'🥜', +'peanuts', +'food nut peanut vegetable' +],[ +'🫘', +'beans', +'food kidney legume' +],[ +'🌰', +'chestnut', +'plant food squirrel' +],[ +'🫚', +'ginger root', +'beer ginger root spice flavour cooking' +],[ +'🫛', +'pea pod', +'beans edamame legume pea pod vegetable green' +],[ +'🍄‍🟫', +'brown mushroom', +'brown food fungi fungus mushroom nature pizza portobello shiitake shroom spore sprout toppings truffle vegetable vegetarian veggie' +],[ +'🫜', +'root vegetable', +'beet food garden radish root salad turnip vegetable vegetarian' +],[ +'🍞', +'bread', +'loaf food wheat breakfast toast' +],[ +'🥐', +'croissant', +'bread breakfast food french roll' +],[ +'🥖', +'baguette bread', +'baguette bread food french' +],[ +'🫓', +'flatbread', +'arepa lavash naan pita flour food' +],[ +'🥨', +'pretzel', +'twisted convoluted food bread' +],[ +'🥯', +'bagel', +'bakery breakfast schmear food bread' +],[ +'🥞', +'pancakes', +'breakfast crêpe food hotcake pancake flapjacks hotcakes' +],[ +'🧇', +'waffle', +'breakfast indecisive iron food' +],[ +'🧀', +'cheese wedge', +'cheese food chadder' +],[ +'🍖', +'meat on bone', +'bone meat good food drumstick' +],[ +'🍗', +'poultry leg', +'bone chicken drumstick leg poultry food meat bird turkey' +],[ +'🥩', +'cut of meat', +'chop lambchop porkchop steak food cow meat cut' +],[ +'🥓', +'bacon', +'breakfast food meat pork pig' +],[ +'🍔', +'hamburger', +'burger meat fast food beef cheeseburger mcdonalds burger king' +],[ +'🍟', +'french fries', +'french fries chips snack fast food' +],[ +'🍕', +'pizza', +'cheese slice food party' +],[ +'🌭', +'hot dog', +'frankfurter hotdog sausage food' +],[ +'🥪', +'sandwich', +'bread food lunch' +],[ +'🌮', +'taco', +'mexican food' +],[ +'🌯', +'burrito', +'mexican wrap food' +],[ +'🫔', +'tamale', +'mexican wrapped food masa' +],[ +'🥙', +'stuffed flatbread', +'falafel flatbread food gyro kebab stuffed' +],[ +'🧆', +'falafel', +'chickpea meatball food' +],[ +'🥚', +'egg', +'breakfast food chicken' +],[ +'🍳', +'cooking', +'breakfast egg frying pan food kitchen' +],[ +'🥘', +'shallow pan of food', +'casserole food paella pan shallow cooking' +],[ +'🍲', +'pot of food', +'pot stew food meat soup' +],[ +'🫕', +'fondue', +'cheese chocolate melted pot Swiss food' +],[ +'🥣', +'bowl with spoon', +'breakfast cereal congee oatmeal porridge food' +],[ +'🥗', +'green salad', +'food green salad healthy lettuce' +],[ +'🍿', +'popcorn', +'food movie theater films snack' +],[ +'🧈', +'butter', +'dairy food cook' +],[ +'🧂', +'salt', +'condiment shaker' +],[ +'🥫', +'canned food', +'can food soup' +],[ +'🍱', +'bento box', +'bento box food japanese' +],[ +'🍘', +'rice cracker', +'cracker rice food japanese' +],[ +'🍙', +'rice ball', +'ball Japanese rice food japanese' +],[ +'🍚', +'cooked rice', +'cooked rice food china asian' +],[ +'🍛', +'curry rice', +'curry rice food spicy hot indian' +],[ +'🍜', +'steaming bowl', +'bowl noodle ramen steaming food japanese chopsticks' +],[ +'🍝', +'spaghetti', +'pasta food italian noodle' +],[ +'🍠', +'roasted sweet potato', +'potato roasted sweet food nature' +],[ +'🍢', +'oden', +'kebab seafood skewer stick food japanese' +],[ +'🍣', +'sushi', +'food fish japanese rice' +],[ +'🍤', +'fried shrimp', +'fried prawn shrimp tempura food animal appetizer summer' +],[ +'🍥', +'fish cake with swirl', +'cake fish pastry swirl food japan sea beach narutomaki pink kamaboko surimi ramen' +],[ +'🥮', +'moon cake', +'autumn festival yuèbǐng food' +],[ +'🍡', +'dango', +'dessert Japanese skewer stick sweet food japanese barbecue meat' +],[ +'🥟', +'dumpling', +'empanada gyōza jiaozi pierogi potsticker food' +],[ +'🥠', +'fortune cookie', +'prophecy food' +],[ +'🥡', +'takeout box', +'oyster pail food leftovers' +],[ +'🦀', +'crab', +'Cancer zodiac animal crustacean' +],[ +'🦞', +'lobster', +'bisque claws seafood animal nature' +],[ +'🦐', +'shrimp', +'food shellfish small animal ocean nature seafood' +],[ +'🦑', +'squid', +'food molusc animal nature ocean sea' +],[ +'🦪', +'oyster', +'diving pearl food' +],[ +'🍦', +'soft ice cream', +'cream dessert ice icecream soft sweet food hot summer' +],[ +'🍧', +'shaved ice', +'dessert ice shaved sweet hot summer' +],[ +'🍨', +'ice cream', +'cream dessert ice sweet food hot' +],[ +'🍩', +'doughnut', +'breakfast dessert donut sweet food snack' +],[ +'🍪', +'cookie', +'dessert sweet food snack oreo chocolate' +],[ +'🎂', +'birthday cake', +'birthday cake celebration dessert pastry sweet food' +],[ +'🍰', +'shortcake', +'cake dessert pastry slice sweet food' +],[ +'🧁', +'cupcake', +'bakery sweet food dessert' +],[ +'🥧', +'pie', +'filling pastry fruit meat food dessert' +],[ +'🍫', +'chocolate bar', +'bar chocolate dessert sweet food snack' +],[ +'🍬', +'candy', +'dessert sweet snack lolly' +],[ +'🍭', +'lollipop', +'candy dessert sweet food snack' +],[ +'🍮', +'custard', +'dessert pudding sweet food' +],[ +'🍯', +'honey pot', +'honey honeypot pot sweet bees kitchen' +],[ +'🍼', +'baby bottle', +'baby bottle drink milk food container' +],[ +'🥛', +'glass of milk', +'drink glass milk beverage cow' +],[ +'☕', +'hot beverage', +'beverage coffee drink hot steaming tea caffeine latte espresso' +],[ +'🫖', +'teapot', +'drink pot tea hot' +],[ +'🍵', +'teacup without handle', +'beverage cup drink tea teacup bowl breakfast green british' +],[ +'🍶', +'sake', +'bar beverage bottle cup drink wine drunk japanese alcohol booze' +],[ +'🍾', +'bottle with popping cork', +'bar bottle cork drink popping wine celebration' +],[ +'🍷', +'wine glass', +'bar beverage drink glass wine drunk alcohol booze' +],[ +'🍸', +'cocktail glass', +'bar cocktail drink glass drunk alcohol beverage booze mojito' +],[ +'🍹', +'tropical drink', +'bar drink tropical beverage cocktail summer beach alcohol booze mojito' +],[ +'🍺', +'beer mug', +'bar beer drink mug relax beverage drunk party pub summer alcohol booze' +],[ +'🍻', +'clinking beer mugs', +'bar beer clink drink mug relax beverage drunk party pub summer alcohol booze' +],[ +'🥂', +'clinking glasses', +'celebrate clink drink glass beverage party alcohol cheers wine champagne toast' +],[ +'🥃', +'tumbler glass', +'glass liquor shot tumbler whisky drink beverage drunk alcohol booze bourbon scotch' +],[ +'🫗', +'pouring liquid', +'drink empty glass spill' +],[ +'🥤', +'cup with straw', +'juice soda malt soft drink water drink' +],[ +'🧋', +'bubble tea', +'bubble milk pearl tea taiwan boba milk tea straw' +],[ +'🧃', +'beverage box', +'beverage box juice straw sweet drink' +],[ +'🧉', +'mate', +'drink tea beverage' +],[ +'🧊', +'ice', +'cold ice cube iceberg water' +],[ +'🥢', +'chopsticks', +'hashi jeotgarak kuaizi food' +],[ +'🍽️', +'fork and knife with plate', +'cooking fork knife plate food eat meal lunch dinner restaurant' +],[ +'🍴', +'fork and knife', +'cooking cutlery fork knife kitchen' +],[ +'🥄', +'spoon', +'tableware cutlery kitchen' +],[ +'🔪', +'kitchen knife', +'cooking hocho knife tool weapon blade cutlery kitchen' +],[ +'🫙', +'jar', +'condiment container empty sauce store' +],[ +'🏺', +'amphora', +'Aquarius cooking drink jug zodiac vase jar' +], + + + +// Travel & Places +[ +'🌍', +'globe showing Europe-Africa', +'Africa earth Europe globe world globe showing europe africa international' +],[ +'🌎', +'globe showing Americas', +'Americas earth globe world globe showing americas USA international' +],[ +'🌏', +'globe showing Asia-Australia', +'Asia Australia earth globe world globe showing asia australia east international' +],[ +'🌐', +'globe with meridians', +'earth globe meridians world international internet interweb i18n' +],[ +'🗺️', +'world map', +'map world location direction' +],[ +'🗾', +'map of Japan', +'Japan map map of japan nation country japanese asia' +],[ +'🧭', +'compass', +'magnetic navigation orienteering' +],[ +'🏔️', +'snow-capped mountain', +'cold mountain snow snow capped mountain photo nature environment winter' +],[ +'⛰️', +'mountain', +'photo nature environment' +],[ +'🌋', +'volcano', +'eruption mountain photo nature disaster' +],[ +'🗻', +'mount fuji', +'fuji mountain photo nature japanese' +],[ +'🏕️', +'camping', +'photo outdoors tent' +],[ +'☂️', +'beach with umbrella', +'beach umbrella weather summer sunny sand mojito' +],[ +'🏜️', +'desert', +'photo warm saharah' +],[ +'🏝️', +'desert island', +'desert island photo tropical mojito' +],[ +'🏞️', +'national park', +'park photo environment nature' +],[ +'🏟️', +'stadium', +'photo place sports concert venue' +],[ +'🏛️', +'classical building', +'classical art culture history' +],[ +'🏗️', +'building construction', +'construction wip working progress' +],[ +'🧱', +'brick', +'bricks clay mortar wall' +],[ +'🪨', +'rock', +'boulder heavy solid stone' +],[ +'🪵', +'wood', +'log lumber timber nature trunk' +],[ +'🛖', +'hut', +'house roundhouse yurt structure' +],[ +'🏘️', +'houses', +'buildings photo' +],[ +'🏚️', +'derelict house', +'derelict house abandon evict broken building' +],[ +'🏠', +'house', +'home building' +],[ +'🏡', +'house with garden', +'garden home house plant nature' +],[ +'🏢', +'office building', +'building bureau work' +],[ +'🏣', +'Japanese post office', +'Japanese post japanese post office building envelope communication' +],[ +'🏤', +'post office', +'European post building email' +],[ +'🏥', +'hospital', +'doctor medicine building health surgery' +],[ +'🏦', +'bank', +'building money sales cash business enterprise' +],[ +'🏨', +'hotel', +'building accomodation checkin' +],[ +'🏩', +'love hotel', +'hotel love like affection dating' +],[ +'🏪', +'convenience store', +'convenience store building shopping groceries' +],[ +'🏫', +'school', +'building student education learn teach' +],[ +'🏬', +'department store', +'department store building shopping mall' +],[ +'🏭', +'factory', +'building industry pollution smoke' +],[ +'🏯', +'Japanese castle', +'castle Japanese japanese castle photo building' +],[ +'🏰', +'castle', +'European building royalty history' +],[ +'💒', +'wedding', +'chapel romance love like affection couple marriage bride groom' +],[ +'🗼', +'Tokyo tower', +'Tokyo tower tokyo tower photo japanese' +],[ +'🗽', +'Statue of Liberty', +'liberty statue statue of liberty american newyork' +],[ +'⛪', +'church', +'Christian cross religion building christ' +],[ +'🕌', +'mosque', +'islam Muslim religion worship minaret' +],[ +'🛕', +'hindu temple', +'hindu temple religion' +],[ +'🕍', +'synagogue', +'Jew Jewish religion temple judaism worship jewish' +],[ +'⛩️', +'shinto shrine', +'religion shinto shrine temple japan kyoto' +],[ +'🕋', +'kaaba', +'islam Muslim religion mecca mosque' +],[ +'⛲', +'fountain', +'photo summer water fresh' +],[ +'⛺', +'tent', +'camping photo outdoors' +],[ +'🌁', +'foggy', +'fog photo mountain' +],[ +'🌃', +'night with stars', +'night star evening city downtown' +],[ +'🏙️', +'cityscape', +'city photo night life urban' +],[ +'🌄', +'sunrise over mountains', +'morning mountain sun sunrise view vacation photo' +],[ +'🌅', +'sunrise', +'morning sun view vacation photo' +],[ +'🌆', +'cityscape at dusk', +'city dusk evening landscape sunset photo sky buildings' +],[ +'🌇', +'sunset', +'dusk sun photo good morning dawn' +],[ +'🌉', +'bridge at night', +'bridge night photo sanfrancisco' +],[ +'♨️', +'hot springs', +'hot hotsprings springs steaming bath warm relax' +],[ +'🎠', +'carousel horse', +'carousel horse photo carnival' +],[ +'🛝', +'playground slide', +'amusement park play' +],[ +'🎡', +'ferris wheel', +'amusement park ferris wheel photo carnival londoneye' +],[ +'🎢', +'roller coaster', +'amusement park coaster roller carnival playground photo fun' +],[ +'💈', +'barber pole', +'barber haircut pole hair salon style' +],[ +'🎪', +'circus tent', +'circus tent festival carnival party' +],[ +'🚂', +'locomotive', +'engine railway steam train transportation vehicle' +],[ +'🚃', +'railway car', +'car electric railway train tram trolleybus transportation vehicle' +],[ +'🚄', +'high-speed train', +'railway shinkansen speed train high speed train transportation vehicle' +],[ +'🚅', +'bullet train', +'bullet railway shinkansen speed train transportation vehicle fast public travel' +],[ +'🚆', +'train', +'railway transportation vehicle' +],[ +'🚇', +'metro', +'subway transportation blue-square mrt underground tube' +],[ +'🚈', +'light rail', +'railway transportation vehicle' +],[ +'🚉', +'station', +'railway train transportation vehicle public' +],[ +'🚊', +'tram', +'trolleybus transportation vehicle' +],[ +'🚝', +'monorail', +'vehicle transportation' +],[ +'🚞', +'mountain railway', +'car mountain railway transportation vehicle' +],[ +'🚋', +'tram car', +'car tram trolleybus transportation vehicle carriage public travel' +],[ +'🚌', +'bus', +'vehicle car transportation' +],[ +'🚍', +'oncoming bus', +'bus oncoming vehicle transportation' +],[ +'🚎', +'trolleybus', +'bus tram trolley bart transportation vehicle' +],[ +'🚐', +'minibus', +'bus vehicle car transportation' +],[ +'🚑', +'ambulance', +'vehicle health 911 hospital' +],[ +'🚒', +'fire engine', +'engine fire truck transportation cars vehicle' +],[ +'🚓', +'police car', +'car patrol police vehicle cars transportation law legal enforcement' +],[ +'🚔', +'oncoming police car', +'car oncoming police vehicle law legal enforcement 911' +],[ +'🚕', +'taxi', +'vehicle uber cars transportation' +],[ +'🚖', +'oncoming taxi', +'oncoming taxi vehicle cars uber' +],[ +'🚗', +'automobile', +'car red transportation vehicle' +],[ +'🚘', +'oncoming automobile', +'automobile car oncoming vehicle transportation' +],[ +'🚙', +'sport utility vehicle', +'recreational sport utility transportation vehicle' +],[ +'🛻', +'pickup truck', +'pick-up pickup truck car transportation' +],[ +'🚚', +'delivery truck', +'delivery truck cars transportation' +],[ +'🚛', +'articulated lorry', +'lorry semi truck vehicle cars transportation express' +],[ +'🚜', +'tractor', +'vehicle car farming agriculture' +],[ +'🏎️', +'racing car', +'car racing sports race fast formula f1' +],[ +'🏍️', +'motorcycle', +'racing race sports fast' +],[ +'🛵', +'motor scooter', +'motor scooter vehicle vespa sasha' +],[ +'🦽', +'manual wheelchair', +'accessibility' +],[ +'🦼', +'motorized wheelchair', +'accessibility' +],[ +'🛺', +'auto rickshaw', +'tuk tuk move transportation' +],[ +'🚲', +'bicycle', +'bike sports exercise hipster' +],[ +'🛴', +'kick scooter', +'kick scooter vehicle razor' +],[ +'🛹', +'skateboard', +'board' +],[ +'🛼', +'roller skate', +'roller skate footwear sports' +],[ +'🚏', +'bus stop', +'bus busstop stop transportation wait' +],[ +'🛣️', +'motorway', +'highway road cupertino interstate' +],[ +'🛤️', +'railway track', +'railway train transportation' +],[ +'🛢️', +'oil drum', +'drum oil barrell' +],[ +'⛽', +'fuel pump', +'diesel fuel fuelpump gas pump station gas station petroleum' +],[ +'🛞', +'wheel', +'circle tire turn' +],[ +'🚨', +'police car light', +'beacon car light police revolving ambulance 911 emergency alert error pinged law legal' +],[ +'🚥', +'horizontal traffic light', +'light signal traffic transportation' +],[ +'🚦', +'vertical traffic light', +'light signal traffic transportation driving' +],[ +'🛑', +'stop sign', +'octagonal sign stop' +],[ +'🚧', +'construction', +'barrier wip progress caution warning' +],[ +'⚓', +'anchor', +'ship tool ferry sea boat' +],[ +'🛟', +'ring buoy', +'float life preserver saver rescue safety' +],[ +'⛵', +'sailboat', +'boat resort sea yacht ship summer transportation water sailing' +],[ +'🛶', +'canoe', +'boat paddle water ship' +],[ +'🚤', +'speedboat', +'boat ship transportation vehicle summer' +],[ +'🛳️', +'passenger ship', +'passenger ship yacht cruise ferry' +],[ +'⛴️', +'ferry', +'boat passenger ship yacht' +],[ +'🛥️', +'motor boat', +'boat motorboat ship' +],[ +'🚢', +'ship', +'boat passenger transportation titanic deploy' +],[ +'✈️', +'airplane', +'aeroplane vehicle transportation flight fly' +],[ +'🛩️', +'small airplane', +'aeroplane airplane flight transportation fly vehicle' +],[ +'🛫', +'airplane departure', +'aeroplane airplane check-in departure departures airport flight landing' +],[ +'🛬', +'airplane arrival', +'aeroplane airplane arrivals arriving landing airport flight boarding' +],[ +'🪂', +'parachute', +'hang-glide parasail skydive fly glide' +],[ +'💺', +'seat', +'chair sit airplane transport bus flight fly' +],[ +'🚁', +'helicopter', +'vehicle transportation fly' +],[ +'🚟', +'suspension railway', +'railway suspension vehicle transportation' +],[ +'🚠', +'mountain cableway', +'cable gondola mountain transportation vehicle ski' +],[ +'🚡', +'aerial tramway', +'aerial cable car gondola tramway transportation vehicle ski' +],[ +'🛰️', +'satellite', +'space communication gps orbit spaceflight NASA ISS' +],[ +'🚀', +'rocket', +'space launch ship staffmode NASA outer space fly' +],[ +'🛸', +'flying saucer', +'UFO transportation vehicle ufo' +],[ +'🛎️', +'bellhop bell', +'bell bellhop hotel service' +],[ +'🧳', +'luggage', +'packing travel' +],[ +'⌛', +'hourglass done', +'sand timer time clock oldschool limit exam quiz test' +],[ +'⏳', +'hourglass not done', +'hourglass sand timer oldschool time countdown' +],[ +'⌚', +'watch', +'clock time accessories' +],[ +'⏰', +'alarm clock', +'alarm clock time wake' +],[ +'⏱️', +'stopwatch', +'clock time deadline' +],[ +'⏲️', +'timer clock', +'clock timer alarm' +],[ +'🕰️', +'mantelpiece clock', +'clock time' +],[ +'🕛', +'twelve o’clock', +'00 12 12:00 clock o’clock twelve twelve o clock time noon midnight midday late early schedule' +],[ +'🕧', +'twelve-thirty', +'12 12:30 clock thirty twelve twelve thirty time late early schedule' +],[ +'🕐', +'one o’clock', +'00 1 1:00 clock o’clock one one o clock time late early schedule' +],[ +'🕜', +'one-thirty', +'1 1:30 clock one thirty one thirty time late early schedule' +],[ +'🕑', +'two o’clock', +'00 2 2:00 clock o’clock two two o clock time late early schedule' +],[ +'🕝', +'two-thirty', +'2 2:30 clock thirty two two thirty time late early schedule' +],[ +'🕒', +'three o’clock', +'00 3 3:00 clock o’clock three three o clock time late early schedule' +],[ +'🕞', +'three-thirty', +'3 3:30 clock thirty three three thirty time late early schedule' +],[ +'🕓', +'four o’clock', +'00 4 4:00 clock four o’clock four o clock time late early schedule' +],[ +'🕟', +'four-thirty', +'4 4:30 clock four thirty four thirty time late early schedule' +],[ +'🕔', +'five o’clock', +'00 5 5:00 clock five o’clock five o clock time late early schedule' +],[ +'🕠', +'five-thirty', +'5 5:30 clock five thirty five thirty time late early schedule' +],[ +'🕕', +'six o’clock', +'00 6 6:00 clock o’clock six six o clock time late early schedule dawn dusk' +],[ +'🕡', +'six-thirty', +'6 6:30 clock six thirty six thirty time late early schedule' +],[ +'🕖', +'seven o’clock', +'00 7 7:00 clock o’clock seven seven o clock time late early schedule' +],[ +'🕢', +'seven-thirty', +'7 7:30 clock seven thirty seven thirty time late early schedule' +],[ +'🕗', +'eight o’clock', +'00 8 8:00 clock eight o’clock eight o clock time late early schedule' +],[ +'🕣', +'eight-thirty', +'8 8:30 clock eight thirty eight thirty time late early schedule' +],[ +'🕘', +'nine o’clock', +'00 9 9:00 clock nine o’clock nine o clock time late early schedule' +],[ +'🕤', +'nine-thirty', +'9 9:30 clock nine thirty nine thirty time late early schedule' +],[ +'🕙', +'ten o’clock', +'00 10 10:00 clock o’clock ten ten o clock time late early schedule' +],[ +'🕥', +'ten-thirty', +'10 10:30 clock ten thirty ten thirty time late early schedule' +],[ +'🕚', +'eleven o’clock', +'00 11 11:00 clock eleven o’clock eleven o clock time late early schedule' +],[ +'🕦', +'eleven-thirty', +'11 11:30 clock eleven thirty eleven thirty time late early schedule' +],[ +'🌑', +'new moon', +'dark moon nature twilight planet space night evening sleep' +],[ +'🌒', +'waxing crescent moon', +'crescent moon waxing nature twilight planet space night evening sleep' +],[ +'🌓', +'first quarter moon', +'moon quarter nature twilight planet space night evening sleep' +],[ +'🌔', +'waxing gibbous moon', +'gibbous moon waxing nature night sky gray twilight planet space evening sleep' +],[ +'🌕', +'full moon', +'full moon nature yellow twilight planet space night evening sleep' +],[ +'🌖', +'waning gibbous moon', +'gibbous moon waning nature twilight planet space night evening sleep waxing gibbous moon' +],[ +'🌗', +'last quarter moon', +'moon quarter nature twilight planet space night evening sleep' +],[ +'🌘', +'waning crescent moon', +'crescent moon waning nature twilight planet space night evening sleep' +],[ +'🌙', +'crescent moon', +'crescent moon night sleep sky evening magic' +],[ +'🌚', +'new moon face', +'face moon nature twilight planet space night evening sleep' +],[ +'🌛', +'first quarter moon face', +'face moon quarter nature twilight planet space night evening sleep' +],[ +'🌜', +'last quarter moon face', +'face moon quarter nature twilight planet space night evening sleep' +],[ +'🌡️', +'thermometer', +'weather temperature hot cold' +],[ +'☀️', +'sun', +'bright rays sunny weather nature brightness summer beach spring' +],[ +'🌝', +'full moon face', +'bright face full moon nature twilight planet space night evening sleep' +],[ +'🌞', +'sun with face', +'bright face sun nature morning sky' +],[ +'🪐', +'ringed planet', +'saturn saturnine outerspace' +],[ +'⭐', +'star', +'night yellow' +],[ +'🌟', +'glowing star', +'glittery glow shining sparkle star night awesome good magic' +],[ +'🌠', +'shooting star', +'falling shooting star night photo' +],[ +'🌌', +'milky way', +'space photo stars' +],[ +'☁️', +'cloud', +'weather sky' +],[ +'⛅', +'sun behind cloud', +'cloud sun weather nature cloudy morning fall spring' +],[ +'⛈️', +'cloud with lightning and rain', +'cloud rain thunder weather lightning' +],[ +'🌤️', +'sun behind small cloud', +'cloud sun weather' +],[ +'🌥️', +'sun behind large cloud', +'cloud sun weather' +],[ +'🌦️', +'sun behind rain cloud', +'cloud rain sun weather' +],[ +'🌧️', +'cloud with rain', +'cloud rain weather' +],[ +'🌨️', +'cloud with snow', +'cloud cold snow weather' +],[ +'🌩️', +'cloud with lightning', +'cloud lightning weather thunder' +],[ +'🌪️', +'tornado', +'cloud whirlwind weather cyclone twister' +],[ +'🌫️', +'fog', +'cloud weather' +],[ +'🌫️', +'wind face', +'blow cloud face wind gust air' +],[ +'🌀', +'cyclone', +'dizzy hurricane twister typhoon weather swirl blue cloud vortex spiral whirlpool spin tornado' +],[ +'🌈', +'rainbow', +'rain nature happy unicorn face photo sky spring' +],[ +'🌂', +'closed umbrella', +'clothing rain umbrella weather drizzle' +],[ +'☂️', +'umbrella', +'clothing rain weather spring' +],[ +'☔', +'umbrella with rain drops', +'clothing drop rain umbrella rainy weather spring' +],[ +'⛱️', +'umbrella on ground', +'rain sun umbrella weather summer' +],[ +'⚡', +'high voltage', +'danger electric lightning voltage zap thunder weather lightning bolt fast' +],[ +'❄️', +'snowflake', +'cold snow winter season weather christmas xmas' +],[ +'☃️', +'snowman', +'cold snow winter season weather christmas xmas frozen' +],[ +'⛄', +'snowman without snow', +'cold snow snowman winter season weather christmas xmas frozen without snow' +],[ +'☄️', +'comet', +'space' +],[ +'🔥', +'fire', +'flame tool hot cook' +],[ +'💧', +'droplet', +'cold comic drop sweat water drip faucet spring' +],[ +'🌊', +'water wave', +'ocean water wave sea nature tsunami disaster' +], + + + +// Activities +[ +'🎃', +'jack-o-lantern', +'celebration halloween jack lantern jack o lantern light pumpkin creepy fall' +],[ +'🎄', +'Christmas tree', +'celebration Christmas tree christmas tree festival vacation december xmas' +],[ +'🎆', +'fireworks', +'celebration photo festival carnival congratulations' +],[ +'🎇', +'sparkler', +'celebration fireworks sparkle stars night shine' +],[ +'🧨', +'firecracker', +'dynamite explosive fireworks boom explode explosion' +],[ +'✨', +'sparkles', +'* sparkle star stars shine shiny cool awesome good magic' +],[ +'🎈', +'balloon', +'celebration party birthday circus' +],[ +'🎉', +'party popper', +'celebration party popper tada congratulations birthday magic circus' +],[ +'🎊', +'confetti ball', +'ball celebration confetti festival party birthday circus' +],[ +'🎋', +'tanabata tree', +'banner celebration Japanese tree plant nature branch summer' +],[ +'🎍', +'pine decoration', +'bamboo celebration Japanese pine plant nature vegetable panda' +],[ +'🎎', +'Japanese dolls', +'celebration doll festival Japanese japanese dolls japanese toy kimono' +],[ +'🎏', +'carp streamer', +'carp celebration streamer fish japanese koinobori banner' +],[ +'🎐', +'wind chime', +'bell celebration chime wind nature ding spring' +],[ +'🎑', +'moon viewing ceremony', +'celebration ceremony moon photo japan asia tsukimi' +],[ +'🧧', +'red envelope', +'gift good luck hóngbāo lai see money' +],[ +'🎀', +'ribbon', +'celebration decoration pink girl bowtie' +],[ +'🎁', +'wrapped gift', +'box celebration gift present wrapped birthday christmas xmas' +],[ +'🎗️', +'reminder ribbon', +'celebration reminder ribbon sports cause support awareness' +],[ +'🎟️', +'admission tickets', +'admission ticket sports concert entrance' +],[ +'🎫', +'ticket', +'admission event concert pass' +],[ +'🎖️', +'military medal', +'celebration medal military award winning army' +],[ +'🏆', +'trophy', +'prize win award contest place ftw ceremony' +],[ +'🏅', +'sports medal', +'medal award winning' +],[ +'🥇', +'1st place medal', +'first gold medal award winning' +],[ +'🥈', +'2nd place medal', +'medal second silver award' +],[ +'🥉', +'3rd place medal', +'bronze medal third award' +],[ +'⚽', +'soccer ball', +'ball football soccer sports' +],[ +'⚾', +'baseball', +'ball sports balls' +],[ +'🥎', +'softball', +'ball glove underarm sports balls' +],[ +'🏀', +'basketball', +'ball hoop sports balls NBA' +],[ +'🏐', +'volleyball', +'ball game sports balls' +],[ +'🏈', +'american football', +'american ball football sports balls NFL' +],[ +'🏉', +'rugby football', +'ball football rugby sports team' +],[ +'🎾', +'tennis', +'ball racquet sports balls green' +],[ +'🥏', +'flying disc', +'ultimate sports frisbee' +],[ +'🎳', +'bowling', +'ball game sports fun play' +],[ +'🏏', +'cricket game', +'ball bat game sports' +],[ +'🏑', +'field hockey', +'ball field game hockey stick sports' +],[ +'🏒', +'ice hockey', +'game hockey ice puck stick sports' +],[ +'🥍', +'lacrosse', +'ball goal stick sports' +],[ +'🏓', +'ping pong', +'ball bat game paddle table tennis sports pingpong' +],[ +'🏸', +'badminton', +'birdie game racquet shuttlecock sports' +],[ +'🥊', +'boxing glove', +'boxing glove sports fighting' +],[ +'🥋', +'martial arts uniform', +'judo karate martial arts taekwondo uniform' +],[ +'🥅', +'goal net', +'goal net sports' +],[ +'⛳', +'flag in hole', +'golf hole sports business flag summer' +],[ +'⛸️', +'ice skate', +'ice skate sports' +],[ +'🎣', +'fishing pole', +'fish pole food hobby summer' +],[ +'🤿', +'diving mask', +'diving scuba snorkeling sport ocean' +],[ +'🎽', +'running shirt', +'athletics running sash shirt play pageant' +],[ +'🎿', +'skis', +'ski snow sports winter cold' +],[ +'🛷', +'sled', +'sledge sleigh luge toboggan' +],[ +'🥌', +'curling stone', +'game rock sports' +],[ +'🎯', +'bullseye', +'dart direct hit game hit target play bar' +],[ +'🪀', +'yo-yo', +'fluctuate toy yo yo' +],[ +'🪁', +'kite', +'fly soar wind' +],[ +'🔫', +'water pistol', +'gun handgun pistol revolver tool water weapon violence' +],[ +'🎱', +'pool 8 ball', +'8 ball billiard eight game pool hobby luck magic' +],[ +'🔮', +'crystal ball', +'ball crystal fairy tale fantasy fortune tool disco party magic circus fortune teller' +],[ +'🪄', +'magic wand', +'magic witch wizard supernature power' +],[ +'🎮', +'video game', +'controller game play console PS4' +],[ +'🕹️', +'joystick', +'game video game play' +],[ +'🎰', +'slot machine', +'game slot bet gamble vegas fruit machine luck casino' +],[ +'🎲', +'game die', +'dice die game random tabletop play luck' +],[ +'🧩', +'puzzle piece', +'clue interlocking jigsaw piece puzzle' +],[ +'🧸', +'teddy bear', +'plaything plush stuffed toy' +],[ +'🪅', +'piñata', +'celebration party pinata mexico candy' +],[ +'🪩', +'mirror ball', +'dance disco glitter party' +],[ +'🪆', +'nesting dolls', +'doll nesting russia matryoshka toy' +],[ +'♠️', +'spade suit', +'card game poker cards suits magic' +],[ +'♥️', +'heart suit', +'card game poker cards magic suits' +],[ +'♦️', +'diamond suit', +'card game poker cards magic suits' +],[ +'♣️', +'club suit', +'card game poker cards magic suits' +],[ +'♟️', +'chess pawn', +'chess dupe expendable' +],[ +'🃏', +'joker', +'card game wildcard poker cards play magic' +],[ +'🀄', +'mahjong red dragon', +'game mahjong red play chinese kanji' +],[ +'🎴', +'flower playing cards', +'card flower game Japanese playing sunset red' +],[ +'🎭', +'performing arts', +'art mask performing theater theatre acting drama' +],[ +'🖼️', +'framed picture', +'art frame museum painting picture photography' +],[ +'🎨', +'artist palette', +'art museum painting palette design paint draw colors' +],[ +'🧵', +'thread', +'needle sewing spool string' +],[ +'🪡', +'sewing needle', +'embroidery needle sewing stitches sutures tailoring' +],[ +'🧶', +'yarn', +'ball crochet knit' +],[ +'🪢', +'knot', +'rope tangled tie twine twist scout' +], + + + +// Objects +[ +'👓', +'glasses', +'clothing eye eyeglasses eyewear fashion accessories eyesight nerdy dork geek' +],[ +'🕶️', +'sunglasses', +'dark eye eyewear glasses face cool accessories' +],[ +'🥽', +'goggles', +'eye protection swimming welding eyes protection safety' +],[ +'🥼', +'lab coat', +'doctor experiment scientist chemist' +],[ +'🦺', +'safety vest', +'emergency safety vest protection' +],[ +'👔', +'necktie', +'clothing tie shirt suitup formal fashion cloth business' +],[ +'👕', +'t-shirt', +'clothing shirt tshirt t shirt fashion cloth casual tee' +],[ +'👖', +'jeans', +'clothing pants trousers fashion shopping' +],[ +'🧣', +'scarf', +'neck winter clothes' +],[ +'🧤', +'gloves', +'hand hands winter clothes' +],[ +'🧥', +'coat', +'jacket' +],[ +'🧦', +'socks', +'stocking stockings clothes' +],[ +'👗', +'dress', +'clothing clothes fashion shopping' +],[ +'👘', +'kimono', +'clothing dress fashion women female japanese' +],[ +'🥻', +'sari', +'clothing dress' +],[ +'🩱', +'one-piece swimsuit', +'bathing suit one piece swimsuit fashion' +],[ +'🩲', +'briefs', +'bathing suit one-piece swimsuit underwear clothing' +],[ +'🩳', +'shorts', +'bathing suit pants underwear clothing' +],[ +'👙', +'bikini', +'clothing swim swimming female woman girl fashion beach summer' +],[ +'👚', +'woman’s clothes', +'clothing woman woman s clothes fashion shopping bags female' +],[ +'🪭', +'folding hand fan', +'cooling dance flutter folding hand fan hot shy buchaechum nihon buyō maranao' +],[ +'👛', +'purse', +'clothing coin fashion accessories money sales shopping' +],[ +'👜', +'handbag', +'bag clothing purse fashion accessory accessories shopping' +],[ +'👝', +'clutch bag', +'bag clothing pouch accessories shopping' +],[ +'🛍️', +'shopping bags', +'bag hotel shopping mall buy purchase' +],[ +'🎒', +'backpack', +'bag rucksack satchel school student education' +],[ +'🩴', +'thong sandal', +'beach sandals sandals thong sandals thongs zōri footwear summer' +],[ +'👞', +'man’s shoe', +'clothing man shoe man s shoe fashion male' +],[ +'👟', +'running shoe', +'athletic clothing shoe sneaker shoes sports sneakers' +],[ +'🥾', +'hiking boot', +'backpacking boot camping hiking' +],[ +'🥿', +'flat shoe', +'ballet flat slip-on slipper ballet' +],[ +'👠', +'high-heeled shoe', +'clothing heel shoe woman high heeled shoe fashion shoes female pumps stiletto' +],[ +'👡', +'woman’s sandal', +'clothing sandal shoe woman woman s sandal shoes fashion flip flops' +],[ +'🩰', +'ballet shoes', +'ballet dance' +],[ +'👢', +'woman’s boot', +'boot clothing shoe woman woman s boot shoes fashion' +],[ +'🪮', +'hair pick', +'afro comb hair pick curly' +],[ +'👑', +'crown', +'clothing king queen kod leader royalty lord' +],[ +'👒', +'woman’s hat', +'clothing hat woman woman s hat fashion accessories female lady spring' +],[ +'🎩', +'top hat', +'clothing hat top tophat magic gentleman classy circus' +],[ +'🎓', +'graduation cap', +'cap celebration clothing graduation hat school college degree university legal learn education' +],[ +'🧢', +'billed cap', +'baseball cap cap baseball' +],[ +'🪖', +'military helmet', +'army helmet military soldier warrior protection' +],[ +'⛑️', +'rescue worker’s helmet', +'aid cross face hat helmet rescue worker s helmet construction build' +],[ +'📿', +'prayer beads', +'beads clothing necklace prayer religion dhikr religious' +],[ +'💄', +'lipstick', +'cosmetics makeup female girl fashion woman' +],[ +'💍', +'ring', +'diamond wedding propose marriage valentines fashion jewelry gem engagement' +],[ +'💎', +'gem stone', +'diamond gem jewel blue ruby jewelry' +],[ +'🔇', +'muted speaker', +'mute quiet silent speaker sound volume silence' +],[ +'🔈', +'speaker low volume', +'soft sound volume silence broadcast' +],[ +'🔉', +'speaker medium volume', +'medium volume speaker broadcast' +],[ +'🔊', +'speaker high volume', +'loud volume noise noisy speaker broadcast' +],[ +'📢', +'loudspeaker', +'loud public address volume sound' +],[ +'📣', +'megaphone', +'cheering sound speaker volume' +],[ +'📯', +'postal horn', +'horn post postal instrument music' +],[ +'🔔', +'bell', +'sound notification christmas xmas chime' +],[ +'🔕', +'bell with slash', +'bell forbidden mute quiet silent sound volume' +],[ +'🎼', +'musical score', +'music score treble clef compose' +],[ +'🎵', +'musical note', +'music note score tone sound' +],[ +'🎶', +'musical notes', +'music note notes score' +],[ +'🎙️', +'studio microphone', +'mic microphone music studio sing recording artist talkshow' +],[ +'🎚️', +'level slider', +'level music slider scale' +],[ +'🎛️', +'control knobs', +'control knobs music dial' +],[ +'🎤', +'microphone', +'karaoke mic sound music PA sing talkshow' +],[ +'🎧', +'headphone', +'earbud music score gadgets' +],[ +'📻', +'radio', +'video communication music podcast program' +],[ +'🎷', +'saxophone', +'instrument music sax jazz blues' +],[ +'🪗', +'accordion', +'accordian concertina squeeze box music' +],[ +'🎸', +'guitar', +'instrument music' +],[ +'🎹', +'musical keyboard', +'instrument keyboard music piano compose' +],[ +'🎺', +'trumpet', +'instrument music brass' +],[ +'🎻', +'violin', +'instrument music orchestra symphony' +],[ +'🪕', +'banjo', +'music stringed instructment' +],[ +'🥁', +'drum', +'drumsticks music instrument snare' +],[ +'🪘', +'long drum', +'beat conga drum rhythm music' +],[ +'🪇', +'maracas', +'instrument maracas music percussion rattle shake' +],[ +'🪈', +'flute', +'fife flute music pipe recorder woodwind' +],[ +'🪉', +'harp', +'cupid harp instrument love music orchestra' +],[ +'📱', +'mobile phone', +'cell mobile phone telephone technology apple gadgets dial' +],[ +'📲', +'mobile phone with arrow', +'arrow cell mobile phone receive iphone incoming' +],[ +'☎️', +'telephone', +'phone technology communication dial' +],[ +'📞', +'telephone receiver', +'phone receiver telephone technology communication dial' +],[ +'📟', +'pager', +'bbcall oldschool 90s' +],[ +'📠', +'fax machine', +'fax communication technology' +],[ +'🔋', +'battery', +'power energy sustain' +],[ +'🪫', +'low battery', +'electronic power low energy drained empty' +],[ +'🔌', +'electric plug', +'electric electricity plug charger power' +],[ +'💻', +'laptop', +'computer pc personal technology screen display monitor' +],[ +'🖥️', +'desktop computer', +'computer desktop technology computing screen' +],[ +'🖨️', +'printer', +'computer paper ink' +],[ +'⌨️', +'keyboard', +'computer technology type input text' +],[ +'🖱️', +'computer mouse', +'computer click' +],[ +'🖲️', +'trackball', +'computer technology trackpad' +],[ +'💽', +'computer disk', +'computer disk minidisk optical technology record data 90s' +],[ +'💾', +'floppy disk', +'computer disk floppy oldschool technology save 90s 80s' +],[ +'💿', +'optical disk', +'cd computer disk optical technology dvd disc 90s' +],[ +'📀', +'dvd', +'blu-ray computer disk optical cd disc' +],[ +'🧮', +'abacus', +'calculation' +],[ +'🎥', +'movie camera', +'camera cinema movie film record' +],[ +'🎞️', +'film frames', +'cinema film frames movie' +],[ +'📽️', +'film projector', +'cinema film movie projector video tape record' +],[ +'🎬', +'clapper board', +'clapper movie film record' +],[ +'📺', +'television', +'tv video technology program oldschool show' +],[ +'📷', +'camera', +'video gadgets photography' +],[ +'📸', +'camera with flash', +'camera flash video photography gadgets' +],[ +'📹', +'video camera', +'camera video film record' +],[ +'📼', +'videocassette', +'tape vhs video record oldschool 90s 80s' +],[ +'🔍', +'magnifying glass tilted left', +'glass magnifying search tool zoom find detective' +],[ +'🔎', +'magnifying glass tilted right', +'glass magnifying search tool zoom find detective' +],[ +'🕯️', +'candle', +'light fire wax' +],[ +'💡', +'light bulb', +'bulb comic electric idea light electricity' +],[ +'🔦', +'flashlight', +'electric light tool torch dark camping sight night' +],[ +'🏮', +'red paper lantern', +'bar lantern light red paper halloween spooky' +],[ +'🪔', +'diya lamp', +'diya lamp oil lighting' +],[ +'📔', +'notebook with decorative cover', +'book cover decorated notebook classroom notes record paper study' +],[ +'📕', +'closed book', +'book closed read library knowledge textbook learn' +],[ +'📖', +'open book', +'book open read library knowledge literature learn study' +],[ +'📗', +'green book', +'book green read library knowledge study' +],[ +'📘', +'blue book', +'blue book read library knowledge learn study' +],[ +'📙', +'orange book', +'book orange read library knowledge textbook study' +],[ +'📚', +'books', +'book literature library study' +],[ +'📓', +'notebook', +'stationery record notes paper study' +],[ +'📒', +'ledger', +'notebook notes paper' +],[ +'📃', +'page with curl', +'curl document page documents office paper' +],[ +'📜', +'scroll', +'paper documents ancient history' +],[ +'📄', +'page facing up', +'document page documents office paper information' +],[ +'📰', +'newspaper', +'news paper press headline' +],[ +'🗞️', +'rolled-up newspaper', +'news newspaper paper rolled rolled up newspaper press headline' +],[ +'📑', +'bookmark tabs', +'bookmark mark marker tabs favorite save order tidy' +],[ +'🔖', +'bookmark', +'mark favorite label save' +],[ +'🏷️', +'label', +'sale tag' +],[ +'💰', +'money bag', +'bag dollar money moneybag payment coins sale' +],[ +'🪙', +'coin', +'gold metal money silver treasure currency' +],[ +'💴', +'yen banknote', +'banknote bill currency money note yen sales japanese dollar' +],[ +'💵', +'dollar banknote', +'banknote bill currency dollar money note sales' +],[ +'💶', +'euro banknote', +'banknote bill currency euro money note sales dollar' +],[ +'💷', +'pound banknote', +'banknote bill currency money note pound british sterling sales bills uk england' +],[ +'💸', +'money with wings', +'banknote bill fly money wings dollar bills payment sale' +],[ +'💳', +'credit card', +'card credit money sales dollar bill payment shopping' +],[ +'🧾', +'receipt', +'accounting bookkeeping evidence proof expenses' +],[ +'💹', +'chart increasing with yen', +'chart graph growth money yen green-square presentation stats' +],[ +'✉️', +'envelope', +'email letter postal inbox communication' +],[ +'📧', +'e-mail', +'email letter mail e mail communication inbox' +],[ +'📨', +'incoming envelope', +'e-mail email envelope incoming letter receive inbox' +],[ +'📩', +'envelope with arrow', +'arrow e-mail email envelope outgoing communication' +],[ +'📤', +'outbox tray', +'box letter mail outbox sent tray inbox email' +],[ +'📥', +'inbox tray', +'box inbox letter mail receive tray email documents' +],[ +'📦', +'package', +'box parcel mail gift cardboard moving' +],[ +'📫', +'closed mailbox with raised flag', +'closed mail mailbox postbox email inbox communication' +],[ +'📪', +'closed mailbox with lowered flag', +'closed lowered mail mailbox postbox email communication inbox' +],[ +'📬', +'open mailbox with raised flag', +'mail mailbox open postbox email inbox communication' +],[ +'📭', +'open mailbox with lowered flag', +'lowered mail mailbox open postbox email inbox' +],[ +'📮', +'postbox', +'mail mailbox email letter envelope' +],[ +'🗳️', +'ballot box with ballot', +'ballot box election vote' +],[ +'✏️', +'pencil', +'stationery write paper writing school study' +],[ +'✒️', +'black nib', +'nib pen stationery writing write' +],[ +'🖋️', +'fountain pen', +'fountain pen stationery writing write' +],[ +'🖊️', +'pen', +'ballpoint stationery writing write' +],[ +'🖌️', +'paintbrush', +'painting drawing creativity art' +],[ +'🖍️', +'crayon', +'drawing creativity' +],[ +'📝', +'memo', +'pencil write documents stationery paper writing legal exam quiz test study compose' +],[ +'💼', +'briefcase', +'business documents work law legal job career' +],[ +'📁', +'file folder', +'file folder documents business office' +],[ +'📂', +'open file folder', +'file folder open documents load' +],[ +'🗂️', +'card index dividers', +'card dividers index organizing business stationery' +],[ +'📅', +'calendar', +'date schedule' +],[ +'📆', +'tear-off calendar', +'calendar tear off calendar schedule date planning' +],[ +'🗒️', +'spiral notepad', +'note pad spiral memo stationery' +],[ +'🗓️', +'spiral calendar', +'calendar pad spiral date schedule planning' +],[ +'📇', +'card index', +'card index rolodex business stationery' +],[ +'📈', +'chart increasing', +'chart graph growth trend upward presentation stats recovery business economics money sales good success' +],[ +'📉', +'chart decreasing', +'chart down graph trend presentation stats recession business economics money sales bad failure' +],[ +'📊', +'bar chart', +'bar chart graph presentation stats' +],[ +'📋', +'clipboard', +'stationery documents' +],[ +'📌', +'pushpin', +'pin stationery mark here' +],[ +'📍', +'round pushpin', +'pin pushpin stationery location map here' +],[ +'📎', +'paperclip', +'documents stationery' +],[ +'🖇️', +'linked paperclips', +'link paperclip documents stationery' +],[ +'📏', +'straight ruler', +'ruler straight edge stationery calculate length math school drawing architect sketch' +],[ +'📐', +'triangular ruler', +'ruler set triangle stationery math architect sketch' +],[ +'✂️', +'scissors', +'cutting tool stationery cut' +],[ +'🗃️', +'card file box', +'box card file business stationery' +],[ +'🗄️', +'file cabinet', +'cabinet file filing organizing' +],[ +'🗑️', +'wastebasket', +'bin trash rubbish garbage toss' +],[ +'🔒', +'locked', +'closed security password padlock' +],[ +'🔓', +'unlocked', +'lock open unlock privacy security' +],[ +'🔏', +'locked with pen', +'ink lock nib pen privacy security secret' +],[ +'🔐', +'locked with key', +'closed key lock secure security privacy' +],[ +'🔑', +'key', +'lock password door' +],[ +'🗝️', +'old key', +'clue key lock old door password' +],[ +'🔨', +'hammer', +'tool tools build create' +],[ +'🪓', +'axe', +'chop hatchet split wood tool cut' +],[ +'⛏️', +'pick', +'mining tool tools dig' +],[ +'⚒️', +'hammer and pick', +'hammer pick tool tools build create' +],[ +'🛠️', +'hammer and wrench', +'hammer spanner tool wrench tools build create' +],[ +'🗡️', +'dagger', +'knife weapon' +],[ +'⚔️', +'crossed swords', +'crossed swords weapon' +],[ +'💣', +'bomb', +'comic boom explode explosion terrorism' +],[ +'🪃', +'boomerang', +'australia rebound repercussion weapon' +],[ +'🏹', +'bow and arrow', +'archer arrow bow Sagittarius zodiac sports' +],[ +'🛡️', +'shield', +'weapon protection security' +],[ +'🪚', +'carpentry saw', +'carpenter lumber saw tool cut chop' +],[ +'🔧', +'wrench', +'spanner tool tools diy ikea fix maintainer' +],[ +'🪛', +'screwdriver', +'screw tool tools' +],[ +'🔩', +'nut and bolt', +'bolt nut tool handy tools fix' +],[ +'⚙️', +'gear', +'cog cogwheel tool' +],[ +'🗜️', +'clamp', +'compress tool vice' +],[ +'⚖️', +'balance scale', +'balance justice Libra scale zodiac law fairness weight' +],[ +'🦯', +'white cane', +'accessibility blind probing cane' +],[ +'🔗', +'link', +'chain rings url hyperlink' +],[ +'⛓️‍💥', +'broken chain', +'breaking broken chain cuffs freedom' +],[ +'⛓️', +'chains', +'chain lock arrest' +],[ +'🪝', +'hook', +'catch crook curve ensnare selling point tools' +],[ +'🧰', +'toolbox', +'chest mechanic tool tools diy fix maintainer' +],[ +'🧲', +'magnet', +'attraction horseshoe magnetic' +],[ +'🪜', +'ladder', +'climb rung step tools' +],[ +'🪏', +'shovel', +'bury dig garden hole plant scoop shovel snow spade' +],[ +'⚗️', +'alembic', +'chemistry tool distilling science experiment' +],[ +'🧪', +'test tube', +'chemist chemistry experiment lab science' +],[ +'🧫', +'petri dish', +'bacteria biologist biology culture lab' +],[ +'🧬', +'dna', +'biologist evolution gene genetics life' +],[ +'🔬', +'microscope', +'science tool laboratory experiment zoomin study' +],[ +'🔭', +'telescope', +'science tool stars space zoom astronomy' +],[ +'📡', +'satellite antenna', +'antenna dish satellite communication future radio space' +],[ +'💉', +'syringe', +'medicine needle shot sick health hospital drugs blood doctor nurse' +],[ +'🩸', +'drop of blood', +'bleed blood donation injury medicine menstruation period hurt harm wound' +],[ +'💊', +'pill', +'doctor medicine sick health pharmacy drug' +],[ +'🩹', +'adhesive bandage', +'bandage heal' +],[ +'🩼', +'crutch', +'cane disability hurt mobility aid stick' +],[ +'🩺', +'stethoscope', +'doctor heart medicine health' +],[ +'🩻', +'x-ray', +'bones doctor medical skeleton' +],[ +'🚪', +'door', +'house entry exit' +],[ +'🛗', +'elevator', +'accessibility hoist lift' +],[ +'🪞', +'mirror', +'reflection reflector speculum' +],[ +'🪟', +'window', +'frame fresh air opening transparent view scenery' +],[ +'🛏️', +'bed', +'hotel sleep rest' +],[ +'🛋️', +'couch and lamp', +'couch hotel lamp read chill' +],[ +'🪑', +'chair', +'seat sit furniture' +],[ +'🚽', +'toilet', +'restroom wc washroom bathroom potty' +],[ +'🪠', +'plunger', +'force cup plumber suction toilet' +],[ +'🚿', +'shower', +'water clean bathroom' +],[ +'🛁', +'bathtub', +'bath clean shower bathroom' +],[ +'🪤', +'mouse trap', +'bait mousetrap snare trap cheese' +],[ +'🪒', +'razor', +'sharp shave cut' +],[ +'🧴', +'lotion bottle', +'lotion moisturizer shampoo sunscreen' +],[ +'🧷', +'safety pin', +'diaper punk rock' +],[ +'🧹', +'broom', +'cleaning sweeping witch' +],[ +'🧺', +'basket', +'farming laundry picnic' +],[ +'🧻', +'roll of paper', +'paper towels toilet paper roll' +],[ +'🪣', +'bucket', +'cask pail vat water container' +],[ +'🧼', +'soap', +'bar bathing cleaning lather soapdish' +],[ +'🫧', +'bubbles', +'burp clean soap underwater' +],[ +'🪥', +'toothbrush', +'bathroom brush clean dental hygiene teeth' +],[ +'🧽', +'sponge', +'absorbing cleaning porous' +],[ +'🧯', +'fire extinguisher', +'extinguish fire quench' +],[ +'🛒', +'shopping cart', +'cart shopping trolley' +],[ +'🚬', +'cigarette', +'smoking kills tobacco joint smoke' +],[ +'⚰️', +'coffin', +'death vampire dead die rip graveyard cemetery casket funeral box' +],[ +'🪦', +'headstone', +'cemetery grave graveyard tombstone death rip' +],[ +'⚱️', +'funeral urn', +'ashes death funeral urn dead die rip' +],[ +'🧿', +'nazar amulet', +'bead charm evil-eye nazar talisman' +],[ +'🪬', +'hamsa', +'amulet Fatima hand Mary Miriam protection' +],[ +'🗿', +'moai', +'face moyai statue rock easter island' +],[ +'🪧', +'placard', +'demonstration picket protest sign announcement' +],[ +'🪪', +'identification card', +'credentials ID license security' +], + + + +// Symbols +[ +'🏧', +'ATM sign', +'atm automated bank teller atm sign money sales cash blue-square payment' +],[ +'🚮', +'litter in bin sign', +'litter litter bin blue-square sign human info' +],[ +'🚰', +'potable water', +'drinking potable water blue-square liquid restroom cleaning faucet' +],[ +'♿', +'wheelchair symbol', +'access blue-square disabled accessibility' +],[ +'🚹', +'men’s room', +'lavatory man restroom wc men s room toilet blue-square gender male' +],[ +'🚺', +'women’s room', +'lavatory restroom wc woman women s room purple-square female toilet loo gender' +],[ +'🚻', +'restroom', +'lavatory WC blue-square toilet refresh wc gender' +],[ +'🚼', +'baby symbol', +'baby changing orange-square child' +],[ +'🚾', +'water closet', +'closet lavatory restroom water wc toilet blue-square' +],[ +'🛂', +'passport control', +'control passport custom blue-square' +],[ +'🛃', +'customs', +'passport border blue-square' +],[ +'🛄', +'baggage claim', +'baggage claim blue-square airport transport' +],[ +'🛅', +'left luggage', +'baggage locker luggage blue-square travel' +],[ +'⚠️', +'warning', +'exclamation wip alert error problem issue' +],[ +'🚸', +'children crossing', +'child crossing pedestrian traffic school warning danger sign driving yellow-diamond' +],[ +'⛔', +'no entry', +'entry forbidden no not prohibited traffic limit security privacy bad denied stop circle' +],[ +'🚫', +'prohibited', +'entry forbidden no not forbid stop limit denied disallow circle' +],[ +'🚳', +'no bicycles', +'bicycle bike forbidden no prohibited cyclist circle' +],[ +'🚭', +'no smoking', +'forbidden no not prohibited smoking cigarette blue-square smell smoke' +],[ +'🚯', +'no littering', +'forbidden litter no not prohibited trash bin garbage circle' +],[ +'🚱', +'non-potable water', +'non-drinking non-potable water non potable water drink faucet tap circle' +],[ +'🚷', +'no pedestrians', +'forbidden no not pedestrian prohibited rules crossing walking circle' +],[ +'📵', +'no mobile phones', +'cell forbidden mobile no phone iphone mute circle' +],[ +'🔞', +'no one under eighteen', +'18 age restriction eighteen prohibited underage drink pub night minor circle' +],[ +'☢️', +'radioactive', +'sign nuclear danger' +],[ +'☣️', +'biohazard', +'sign danger' +],[ +'⬆️', +'up arrow', +'arrow cardinal direction north blue-square continue top' +],[ +'↗️', +'up-right arrow', +'arrow direction intercardinal northeast up right arrow blue-square point diagonal' +],[ +'➡️', +'right arrow', +'arrow cardinal direction east blue-square next' +],[ +'↘️', +'down-right arrow', +'arrow direction intercardinal southeast down right arrow blue-square diagonal' +],[ +'⬇️', +'down arrow', +'arrow cardinal direction down south blue-square bottom' +],[ +'↙️', +'down-left arrow', +'arrow direction intercardinal southwest down left arrow blue-square diagonal' +],[ +'⬅️', +'left arrow', +'arrow cardinal direction west blue-square previous back' +],[ +'↖️', +'up-left arrow', +'arrow direction intercardinal northwest up left arrow blue-square point diagonal' +],[ +'↕️', +'up-down arrow', +'arrow up down arrow blue-square direction way vertical' +],[ +'↔️', +'left-right arrow', +'arrow left right arrow shape direction horizontal sideways' +],[ +'↩️', +'right arrow curving left', +'arrow back return blue-square undo enter' +],[ +'↪️', +'left arrow curving right', +'arrow blue-square return rotate direction' +],[ +'⤴️', +'right arrow curving up', +'arrow blue-square direction top' +],[ +'⤵️', +'right arrow curving down', +'arrow down blue-square direction bottom' +],[ +'🔃', +'clockwise vertical arrows', +'arrow clockwise reload sync cycle round repeat' +],[ +'🔄', +'counterclockwise arrows button', +'anticlockwise arrow counterclockwise withershins blue-square sync cycle' +],[ +'🔙', +'BACK arrow', +'arrow back back arrow words return' +],[ +'🔚', +'END arrow', +'arrow end end arrow words' +],[ +'🔛', +'ON! arrow', +'arrow mark on on arrow words' +],[ +'🔜', +'SOON arrow', +'arrow soon soon arrow words' +],[ +'🔝', +'TOP arrow', +'arrow top up top arrow words blue-square' +],[ +'🛐', +'place of worship', +'religion worship church temple prayer' +],[ +'⚛️', +'atom symbol', +'atheist atom science physics chemistry' +],[ +'🕉️', +'om', +'Hindu religion hinduism buddhism sikhism jainism' +],[ +'✡️', +'star of David', +'David Jew Jewish religion star star of david judaism' +],[ +'☸️', +'wheel of dharma', +'Buddhist dharma religion wheel hinduism buddhism sikhism jainism' +],[ +'☯️', +'yin yang', +'religion tao taoist yang yin balance' +],[ +'✝️', +'latin cross', +'Christian cross religion christianity' +],[ +'☦️', +'orthodox cross', +'Christian cross religion suppedaneum' +],[ +'☪️', +'star and crescent', +'islam Muslim religion' +],[ +'☮️', +'peace symbol', +'peace hippie' +],[ +'🕎', +'menorah', +'candelabrum candlestick religion hanukkah candles jewish' +],[ +'🔯', +'dotted six-pointed star', +'fortune star dotted six pointed star purple-square religion jewish hexagram' +],[ +'🪯', +'khanda', +'khanda religion Sikh' +],[ +'♈', +'Aries', +'ram zodiac aries sign purple-square astrology' +],[ +'♉', +'Taurus', +'bull ox zodiac taurus purple-square sign astrology' +],[ +'♊', +'Gemini', +'twins zodiac gemini sign purple-square astrology' +],[ +'♋', +'Cancer', +'crab zodiac cancer sign purple-square astrology' +],[ +'♌', +'Leo', +'lion zodiac leo sign purple-square astrology' +],[ +'♍', +'Virgo', +'zodiac virgo sign purple-square astrology' +],[ +'♎', +'Libra', +'balance justice scales zodiac libra sign purple-square astrology' +],[ +'♏', +'Scorpio', +'scorpion scorpius zodiac scorpio sign purple-square astrology' +],[ +'♐', +'Sagittarius', +'archer zodiac sagittarius sign purple-square astrology' +],[ +'♑', +'Capricorn', +'goat zodiac capricorn sign purple-square astrology' +],[ +'♒', +'Aquarius', +'bearer water zodiac aquarius sign purple-square astrology' +],[ +'♓', +'Pisces', +'fish zodiac pisces purple-square sign astrology' +],[ +'⛎', +'Ophiuchus', +'bearer serpent snake zodiac ophiuchus sign purple-square constellation astrology' +],[ +'🔀', +'shuffle tracks button', +'arrow crossed blue-square shuffle music random' +],[ +'🔁', +'repeat button', +'arrow clockwise repeat loop record' +],[ +'🔂', +'repeat single button', +'arrow clockwise once blue-square loop' +],[ +'▶️', +'play button', +'arrow play right triangle blue-square direction' +],[ +'⏩', +'fast-forward button', +'arrow double fast forward fast forward button blue-square play speed continue' +],[ +'⏭️', +'next track button', +'arrow next scene next track triangle forward next blue-square' +],[ +'⏭️', +'play or pause button', +'arrow pause play right triangle blue-square' +],[ +'◀️', +'reverse button', +'arrow left reverse triangle blue-square direction' +],[ +'⏪', +'fast reverse button', +'arrow double rewind play blue-square' +],[ +'⏮️', +'last track button', +'arrow previous scene previous track triangle backward' +],[ +'🔼', +'upwards button', +'arrow button red blue-square triangle direction point forward top' +],[ +'⏫', +'fast up button', +'arrow double blue-square direction top' +],[ +'🔽', +'downwards button', +'arrow button down red blue-square direction bottom' +],[ +'⏬', +'fast down button', +'arrow double down blue-square direction bottom' +],[ +'⏸️', +'pause button', +'bar double pause vertical blue-square' +],[ +'⏹️', +'stop button', +'square stop blue-square' +],[ +'⏺️', +'record button', +'circle record blue-square' +],[ +'⏏️', +'eject button', +'eject blue-square' +],[ +'🎦', +'cinema', +'camera film movie blue-square record curtain stage theater' +],[ +'🔅', +'dim button', +'brightness dim low sun afternoon warm summer' +],[ +'🔆', +'bright button', +'bright brightness sun light' +],[ +'📶', +'antenna bars', +'antenna bar cell mobile phone blue-square reception internet connection wifi bluetooth bars' +],[ +'🛜', +'wireless', +'computer internet network wi-fi wifi wireless connection' +],[ +'📳', +'vibration mode', +'cell mobile mode phone telephone vibration orange-square' +],[ +'📴', +'mobile phone off', +'cell mobile off phone telephone mute orange-square silence quiet' +],[ +'♀️', +'female sign', +'woman women lady girl' +],[ +'♂️', +'male sign', +'man boy men' +],[ +'⚧️', +'transgender symbol', +'transgender lgbtq' +],[ +'✖️', +'multiply', +'× cancel multiplication sign x multiplication sign math calculation' +],[ +'➕', +'plus', +'+ math sign plus sign calculation addition more increase' +],[ +'➖', +'minus', +'- − math sign minus sign calculation subtract less' +],[ +'➗', +'divide', +'÷ division math sign division sign calculation' +],[ +'🟰', +'heavy equals sign', +'equality math equation' +],[ +'♾️', +'infinity', +'forever unbounded universal' +],[ +'‼️', +'double exclamation mark', +'! !! bangbang exclamation mark surprise' +],[ +'⁉️', +'exclamation question mark', +'! !? ? exclamation interrobang mark punctuation question wat surprise' +],[ +'❓', +'red question mark', +'? mark punctuation question question mark doubt confused' +],[ +'❔', +'white question mark', +'? mark outlined punctuation question doubts gray huh confused' +],[ +'❕', +'white exclamation mark', +'! exclamation mark outlined punctuation surprise gray wow warning' +],[ +'❗', +'red exclamation mark', +'! exclamation mark punctuation exclamation mark heavy exclamation mark danger surprise wow warning' +],[ +'〰️', +'wavy dash', +'dash punctuation wavy draw line moustache mustache squiggle scribble' +],[ +'💱', +'currency exchange', +'bank currency exchange money sales dollar travel' +],[ +'💲', +'heavy dollar sign', +'currency dollar money sales payment buck' +],[ +'⚕️', +'medical symbol', +'aesculapius medicine staff health hospital' +],[ +'♻️', +'recycling symbol', +'recycle arrow environment garbage trash' +],[ +'⚜️', +'fleur-de-lis', +'fleur de lis decorative scout' +],[ +'🔱', +'trident emblem', +'anchor emblem ship tool trident weapon spear' +],[ +'📛', +'name badge', +'badge name fire forbid' +],[ +'🔰', +'Japanese symbol for beginner', +'beginner chevron Japanese leaf japanese symbol for beginner badge shield' +],[ +'⭕', +'hollow red circle', +'circle large o red round' +],[ +'✅', +'check mark button', +'✓ button check mark green-square ok agree vote election answer tick' +],[ +'☑️', +'check box with check', +'✓ box check ok agree confirm black-square vote election yes tick' +],[ +'✔️', +'check mark', +'✓ check mark ok nike answer yes tick' +],[ +'❌', +'cross mark', +'× cancel cross mark multiplication multiply x no delete remove red' +],[ +'❎', +'cross mark button', +'× mark square x green-square no deny' +],[ +'➰', +'curly loop', +'curl loop scribble draw shape squiggle' +],[ +'➿', +'double curly loop', +'curl double loop tape cassette' +],[ +'〽️', +'part alternation mark', +'mark part graph presentation stats business economics bad' +],[ +'✳️', +'eight-spoked asterisk', +'* asterisk eight spoked asterisk star sparkle green-square' +],[ +'✴️', +'eight-pointed star', +'* star eight pointed star orange-square shape polygon' +],[ +'❇️', +'sparkle', +'* stars green-square awesome good fireworks' +],[ +'©️', +'copyright', +'c ip license circle law legal' +],[ +'®️', +'registered', +'r alphabet circle' +],[ +'™️', +'trade mark', +'mark tm trademark brand law legal' +],[ +'🫟', +'splatter', +'drip holi ink liquid mess paint spill stain' +],[ +'#️⃣', +'keycap: #', +'keycap keycap symbol blue-square twitter' +],[ +'*️⃣', +'keycap: *', +'keycap keycap star' +],[ +'0️⃣', +'keycap: 0', +'keycap keycap 0 0 numbers blue-square null' +],[ +'1️⃣', +'keycap: 1', +'keycap keycap 1 blue-square numbers 1' +],[ +'2️⃣', +'keycap: 2', +'keycap keycap 2 numbers 2 prime blue-square' +],[ +'3️⃣', +'keycap: 3', +'keycap keycap 3 3 numbers prime blue-square' +],[ +'4️⃣', +'keycap: 4', +'keycap keycap 4 4 numbers blue-square' +],[ +'5️⃣', +'keycap: 5', +'keycap keycap 5 5 numbers blue-square prime' +],[ +'6️⃣', +'keycap: 6', +'keycap keycap 6 6 numbers blue-square' +],[ +'7️⃣', +'keycap: 7', +'keycap keycap 7 7 numbers blue-square prime' +],[ +'8️⃣', +'keycap: 8', +'keycap keycap 8 8 blue-square numbers' +],[ +'9️⃣', +'keycap: 9', +'keycap keycap 9 blue-square numbers 9' +],[ +'🔟', +'keycap: 10', +'keycap keycap 10 numbers 10 blue-square' +],[ +'🔠', +'input latin uppercase', +'ABCD input latin letters uppercase alphabet words blue-square' +],[ +'🔡', +'input latin lowercase', +'abcd input latin letters lowercase blue-square alphabet' +],[ +'🔢', +'input numbers', +'1234 input numbers blue-square' +],[ +'🔣', +'input symbols', +'〒♪&% input blue-square music note ampersand percent glyphs characters' +],[ +'🔤', +'input latin letters', +'abc alphabet input latin letters blue-square' +],[ +'🅰️', +'A button (blood type)', +'a blood type a button red-square alphabet letter' +],[ +'🆎', +'AB button (blood type)', +'ab blood type ab button red-square alphabet' +],[ +'🅱️', +'B button (blood type)', +'b blood type b button red-square alphabet letter' +],[ +'🆑', +'CL button', +'cl cl button alphabet words red-square' +],[ +'🆒', +'COOL button', +'cool cool button words blue-square' +],[ +'🆓', +'FREE button', +'free free button blue-square words' +],[ +'ℹ️', +'information', +'i blue-square alphabet letter' +],[ +'🆔', +'ID button', +'id identity id button purple-square words' +],[ +'Ⓜ️', +'circled M', +'circle m circled m alphabet blue-circle letter' +],[ +'🆕', +'NEW button', +'new new button blue-square words start' +],[ +'🆖', +'NG button', +'ng ng button blue-square words shape icon' +],[ +'🅾️', +'O button (blood type)', +'blood type o o button alphabet red-square letter' +],[ +'🆗', +'OK button', +'OK ok button good agree yes blue-square' +],[ +'🅿️', +'P button', +'parking p button cars blue-square alphabet letter' +],[ +'🆘', +'SOS button', +'help sos sos button red-square words emergency 911' +],[ +'🆙', +'UP! button', +'mark up up button blue-square above high' +],[ +'🆚', +'VS button', +'versus vs vs button words orange-square' +],[ +'🈁', +'Japanese “here” button', +'“here” Japanese katakana ココ japanese here button blue-square here japanese destination' +],[ +'🈂️', +'Japanese “service charge” button', +'“service charge” Japanese katakana サ japanese service charge button japanese blue-square' +],[ +'🈷️', +'Japanese “monthly amount” button', +'“monthly amount” ideograph Japanese 月 japanese monthly amount button chinese month moon japanese orange-square kanji' +],[ +'🈶', +'Japanese “not free of charge” button', +'“not free of charge” ideograph Japanese 有 japanese not free of charge button orange-square chinese have kanji' +],[ +'🈯', +'Japanese “reserved” button', +'“reserved” ideograph Japanese 指 japanese reserved button chinese point green-square kanji' +],[ +'🉐', +'Japanese “bargain” button', +'“bargain” ideograph Japanese 得 japanese bargain button chinese kanji obtain get circle' +],[ +'🈹', +'Japanese “discount” button', +'“discount” ideograph Japanese 割 japanese discount button cut divide chinese kanji pink-square' +],[ +'🈚', +'Japanese “free of charge” button', +'“free of charge” ideograph Japanese 無 japanese free of charge button nothing chinese kanji japanese orange-square' +],[ +'🈲', +'Japanese “prohibited” button', +'“prohibited” ideograph Japanese 禁 japanese prohibited button kanji japanese chinese forbidden limit restricted red-square' +],[ +'🉑', +'Japanese “acceptable” button', +'“acceptable” ideograph Japanese 可 japanese acceptable button ok good chinese kanji agree yes orange-circle' +],[ +'🈸', +'Japanese “application” button', +'“application” ideograph Japanese 申 japanese application button chinese japanese kanji orange-square' +],[ +'🈴', +'Japanese “passing grade” button', +'“passing grade” ideograph Japanese 合 japanese passing grade button japanese chinese join kanji red-square' +],[ +'🈳', +'Japanese “vacancy” button', +'“vacancy” ideograph Japanese 空 japanese vacancy button kanji japanese chinese empty sky blue-square' +],[ +'㊗️', +'Japanese “congratulations” button', +'“congratulations” ideograph Japanese 祝 japanese congratulations button chinese kanji japanese red-circle' +],[ +'㊙️', +'Japanese “secret” button', +'“secret” ideograph Japanese 秘 japanese secret button privacy chinese sshh kanji red-circle' +],[ +'🈺', +'Japanese “open for business” button', +'“open for business” ideograph Japanese 営 japanese open for business button japanese opening hours orange-square' +],[ +'🈵', +'Japanese “no vacancy” button', +'“no vacancy” ideograph Japanese 満 japanese no vacancy button full chinese japanese red-square kanji' +],[ +'🔴', +'red circle', +'circle geometric red shape error danger' +],[ +'🟠', +'orange circle', +'circle orange round' +],[ +'🟡', +'yellow circle', +'circle yellow round' +],[ +'🟢', +'green circle', +'circle green round' +],[ +'🔵', +'blue circle', +'blue circle geometric shape icon button' +],[ +'🟣', +'purple circle', +'circle purple round' +],[ +'🟤', +'brown circle', +'brown circle round' +],[ +'⚫', +'black circle', +'circle geometric shape button round' +],[ +'⚪', +'white circle', +'circle geometric shape round' +],[ +'🟥', +'red square', +'red square' +],[ +'🟧', +'orange square', +'orange square' +],[ +'🟨', +'yellow square', +'square yellow' +],[ +'🟩', +'green square', +'green square' +],[ +'🟦', +'blue square', +'blue square' +],[ +'🟪', +'purple square', +'purple square' +],[ +'🟫', +'brown square', +'brown square' +],[ +'⬛', +'black large square', +'geometric square shape icon button' +],[ +'⬜', +'white large square', +'geometric square shape icon stone button' +],[ +'◼️', +'black medium square', +'geometric square shape button icon' +],[ +'◻️', +'white medium square', +'geometric square shape stone icon' +],[ +'◾', +'black medium-small square', +'geometric square black medium small square icon shape button' +],[ +'◽', +'white medium-small square', +'geometric square white medium small square shape stone icon button' +],[ +'▪️', +'black small square', +'geometric square shape icon' +],[ +'▫️', +'white small square', +'geometric square shape icon' +],[ +'🔶', +'large orange diamond', +'diamond geometric orange shape jewel gem' +],[ +'🔷', +'large blue diamond', +'blue diamond geometric shape jewel gem' +],[ +'🔸', +'small orange diamond', +'diamond geometric orange shape jewel gem' +],[ +'🔹', +'small blue diamond', +'blue diamond geometric shape jewel gem' +],[ +'🔺', +'red triangle pointed up', +'geometric red shape direction up top' +],[ +'🔻', +'red triangle pointed down', +'down geometric red shape direction bottom' +],[ +'💠', +'diamond with a dot', +'comic diamond geometric inside jewel blue gem crystal fancy' +],[ +'🔘', +'radio button', +'button geometric radio input old music circle' +],[ +'🔳', +'white square button', +'button geometric outlined square shape input' +],[ +'🔲', +'black square button', +'button geometric square shape input frame' +], + + + +// Flags +[ +'🏁', +'chequered flag', +'checkered chequered racing contest finishline race gokart' +],[ +'🚩', +'triangular flag', +'post mark milestone place' +],[ +'🎌', +'crossed flags', +'celebration cross crossed Japanese japanese nation country border' +],[ +'🏴', +'black flag', +'waving pirate' +],[ +'🏳️', +'white flag', +'waving losing loser lost surrender give up fail' +],[ +'🏳️‍🌈', +'rainbow flag', +'pride rainbow flag gay lgbt glbt queer homosexual lesbian bisexual transgender' +],[ +'🏳️‍⚧️', +'transgender flag', +'flag light blue pink transgender white lgbtq' +],[ +'🏴‍☠️', +'pirate flag', +'Jolly Roger pirate plunder treasure skull crossbones flag banner' +],[ +'🇦🇨', +'flag: Ascension Island', +'flag flag ascension island' +],[ +'🇦🇩', +'flag: Andorra', +'flag flag andorra ad nation country banner' +],[ +'🇦🇪', +'flag: United Arab Emirates', +'flag flag united arab emirates united arab emirates nation country banner' +],[ +'🇦🇫', +'flag: Afghanistan', +'flag flag afghanistan af nation country banner' +],[ +'🇦🇬', +'flag: Antigua & Barbuda', +'flag flag antigua barbuda antigua barbuda nation country banner' +],[ +'🇦🇮', +'flag: Anguilla', +'flag flag anguilla ai nation country banner' +],[ +'🇦🇱', +'flag: Albania', +'flag flag albania al nation country banner' +],[ +'🇦🇲', +'flag: Armenia', +'flag flag armenia am nation country banner' +],[ +'🇦🇴', +'flag: Angola', +'flag flag angola ao nation country banner' +],[ +'🇦🇶', +'flag: Antarctica', +'flag flag antarctica aq nation country banner' +],[ +'🇦🇷', +'flag: Argentina', +'flag flag argentina ar nation country banner' +],[ +'🇦🇸', +'flag: American Samoa', +'flag flag american samoa american ws nation country banner' +],[ +'🇦🇹', +'flag: Austria', +'flag flag austria at nation country banner' +],[ +'🇦🇺', +'flag: Australia', +'flag flag australia au nation country banner' +],[ +'🇦🇼', +'flag: Aruba', +'flag flag aruba aw nation country banner' +],[ +'🇦🇽', +'flag: Åland Islands', +'flag flag aland islands Åland islands nation country banner' +],[ +'🇦🇿', +'flag: Azerbaijan', +'flag flag azerbaijan az nation country banner' +],[ +'🇧🇦', +'flag: Bosnia & Herzegovina', +'flag flag bosnia herzegovina bosnia herzegovina nation country banner' +],[ +'🇧🇧', +'flag: Barbados', +'flag flag barbados bb nation country banner' +],[ +'🇧🇩', +'flag: Bangladesh', +'flag flag bangladesh bd nation country banner' +],[ +'🇧🇪', +'flag: Belgium', +'flag flag belgium be nation country banner' +],[ +'🇧🇫', +'flag: Burkina Faso', +'flag flag burkina faso burkina faso nation country banner' +],[ +'🇧🇬', +'flag: Bulgaria', +'flag flag bulgaria bg nation country banner' +],[ +'🇧🇭', +'flag: Bahrain', +'flag flag bahrain bh nation country banner' +],[ +'🇧🇮', +'flag: Burundi', +'flag flag burundi bi nation country banner' +],[ +'🇧🇯', +'flag: Benin', +'flag flag benin bj nation country banner' +],[ +'🇧🇱', +'flag: St. Barthélemy', +'flag flag st barthelemy saint barthélemy nation country banner' +],[ +'🇧🇲', +'flag: Bermuda', +'flag flag bermuda bm nation country banner' +],[ +'🇧🇳', +'flag: Brunei', +'flag flag brunei bn darussalam nation country banner' +],[ +'🇧🇴', +'flag: Bolivia', +'flag flag bolivia bo nation country banner' +],[ +'🇧🇶', +'flag: Caribbean Netherlands', +'flag flag caribbean netherlands bonaire nation country banner' +],[ +'🇧🇷', +'flag: Brazil', +'flag flag brazil br nation country banner' +],[ +'🇧🇸', +'flag: Bahamas', +'flag flag bahamas bs nation country banner' +],[ +'🇧🇹', +'flag: Bhutan', +'flag flag bhutan bt nation country banner' +],[ +'🇧🇻', +'flag: Bouvet Island', +'flag flag bouvet island norway' +],[ +'🇧🇼', +'flag: Botswana', +'flag flag botswana bw nation country banner' +],[ +'🇧🇾', +'flag: Belarus', +'flag flag belarus by nation country banner' +],[ +'🇧🇿', +'flag: Belize', +'flag flag belize bz nation country banner' +],[ +'🇨🇦', +'flag: Canada', +'flag flag canada ca nation country banner' +],[ +'🇨🇨', +'flag: Cocos (Keeling) Islands', +'flag flag cocos islands cocos keeling islands nation country banner' +],[ +'🇨🇩', +'flag: Congo - Kinshasa', +'flag flag congo kinshasa congo democratic republic nation country banner' +],[ +'🇨🇫', +'flag: Central African Republic', +'flag flag central african republic central african republic nation country banner' +],[ +'🇨🇬', +'flag: Congo - Brazzaville', +'flag flag congo brazzaville congo nation country banner' +],[ +'🇨🇭', +'flag: Switzerland', +'flag flag switzerland ch nation country banner' +],[ +'🇨🇮', +'flag: Côte d’Ivoire', +'flag flag cote d ivoire ivory coast nation country banner' +],[ +'🇨🇰', +'flag: Cook Islands', +'flag flag cook islands cook islands nation country banner' +],[ +'🇨🇱', +'flag: Chile', +'flag flag chile nation country banner' +],[ +'🇨🇲', +'flag: Cameroon', +'flag flag cameroon cm nation country banner' +],[ +'🇨🇳', +'flag: China', +'flag flag china china chinese prc country nation banner' +],[ +'🇨🇴', +'flag: Colombia', +'flag flag colombia co nation country banner' +],[ +'🇨🇵', +'flag: Clipperton Island', +'flag flag clipperton island' +],[ +'🇨🇶', +'flag: Sark', +'flag flag sark' +],[ +'🇨🇷', +'flag: Costa Rica', +'flag flag costa rica costa rica nation country banner' +],[ +'🇨🇺', +'flag: Cuba', +'flag flag cuba cu nation country banner' +],[ +'🇨🇻', +'flag: Cape Verde', +'flag flag cape verde cabo verde nation country banner' +],[ +'🇨🇼', +'flag: Curaçao', +'flag flag curacao curaçao nation country banner' +],[ +'🇨🇽', +'flag: Christmas Island', +'flag flag christmas island christmas island nation country banner' +],[ +'🇨🇾', +'flag: Cyprus', +'flag flag cyprus cy nation country banner' +],[ +'🇨🇿', +'flag: Czechia', +'flag flag czechia cz nation country banner' +],[ +'🇩🇪', +'flag: Germany', +'flag flag germany german nation country banner' +],[ +'🇩🇬', +'flag: Diego Garcia', +'flag flag diego garcia' +],[ +'🇩🇯', +'flag: Djibouti', +'flag flag djibouti dj nation country banner' +],[ +'🇩🇰', +'flag: Denmark', +'flag flag denmark dk nation country banner' +],[ +'🇩🇲', +'flag: Dominica', +'flag flag dominica dm nation country banner' +],[ +'🇩🇴', +'flag: Dominican Republic', +'flag flag dominican republic dominican republic nation country banner' +],[ +'🇩🇿', +'flag: Algeria', +'flag flag algeria dz nation country banner' +],[ +'🇪🇦', +'flag: Ceuta & Melilla', +'flag flag ceuta melilla' +],[ +'🇪🇨', +'flag: Ecuador', +'flag flag ecuador ec nation country banner' +],[ +'🇪🇪', +'flag: Estonia', +'flag flag estonia ee nation country banner' +],[ +'🇪🇬', +'flag: Egypt', +'flag flag egypt eg nation country banner' +],[ +'🇪🇭', +'flag: Western Sahara', +'flag flag western sahara western sahara nation country banner' +],[ +'🇪🇷', +'flag: Eritrea', +'flag flag eritrea er nation country banner' +],[ +'🇪🇸', +'flag: Spain', +'flag flag spain spain nation country banner' +],[ +'🇪🇹', +'flag: Ethiopia', +'flag flag ethiopia et nation country banner' +],[ +'🇪🇺', +'flag: European Union', +'flag flag european union european union banner' +],[ +'🇫🇮', +'flag: Finland', +'flag flag finland fi nation country banner' +],[ +'🇫🇯', +'flag: Fiji', +'flag flag fiji fj nation country banner' +],[ +'🇫🇰', +'flag: Falkland Islands', +'flag flag falkland islands falkland islands malvinas nation country banner' +],[ +'🇫🇲', +'flag: Micronesia', +'flag flag micronesia micronesia federated states nation country banner' +],[ +'🇫🇴', +'flag: Faroe Islands', +'flag flag faroe islands faroe islands nation country banner' +],[ +'🇫🇷', +'flag: France', +'flag flag france banner nation france french country' +],[ +'🇬🇦', +'flag: Gabon', +'flag flag gabon ga nation country banner' +],[ +'🇬🇧', +'flag: United Kingdom', +'flag flag united kingdom united kingdom great britain northern ireland nation country banner british UK english england union jack' +],[ +'🇬🇩', +'flag: Grenada', +'flag flag grenada gd nation country banner' +],[ +'🇬🇪', +'flag: Georgia', +'flag flag georgia ge nation country banner' +],[ +'🇬🇫', +'flag: French Guiana', +'flag flag french guiana french guiana nation country banner' +],[ +'🇬🇬', +'flag: Guernsey', +'flag flag guernsey gg nation country banner' +],[ +'🇬🇭', +'flag: Ghana', +'flag flag ghana gh nation country banner' +],[ +'🇬🇮', +'flag: Gibraltar', +'flag flag gibraltar gi nation country banner' +],[ +'🇬🇱', +'flag: Greenland', +'flag flag greenland gl nation country banner' +],[ +'🇬🇲', +'flag: Gambia', +'flag flag gambia gm nation country banner' +],[ +'🇬🇳', +'flag: Guinea', +'flag flag guinea gn nation country banner' +],[ +'🇬🇵', +'flag: Guadeloupe', +'flag flag guadeloupe gp nation country banner' +],[ +'🇬🇶', +'flag: Equatorial Guinea', +'flag flag equatorial guinea equatorial gn nation country banner' +],[ +'🇬🇷', +'flag: Greece', +'flag flag greece gr nation country banner' +],[ +'🇬🇸', +'flag: South Georgia & South Sandwich Islands', +'flag flag south georgia south sandwich islands south georgia sandwich islands nation country banner' +],[ +'🇬🇹', +'flag: Guatemala', +'flag flag guatemala gt nation country banner' +],[ +'🇬🇺', +'flag: Guam', +'flag flag guam gu nation country banner' +],[ +'🇬🇼', +'flag: Guinea-Bissau', +'flag flag guinea bissau gw bissau nation country banner' +],[ +'🇬🇾', +'flag: Guyana', +'flag flag guyana gy nation country banner' +],[ +'🇭🇰', +'flag: Hong Kong SAR China', +'flag flag hong kong sar china hong kong nation country banner' +],[ +'🇭🇲', +'flag: Heard & McDonald Islands', +'flag flag heard mcdonald islands' +],[ +'🇭🇳', +'flag: Honduras', +'flag flag honduras hn nation country banner' +],[ +'🇭🇷', +'flag: Croatia', +'flag flag croatia hr nation country banner' +],[ +'🇭🇹', +'flag: Haiti', +'flag flag haiti ht nation country banner' +],[ +'🇭🇺', +'flag: Hungary', +'flag flag hungary hu nation country banner' +],[ +'🇮🇨', +'flag: Canary Islands', +'flag flag canary islands canary islands nation country banner' +],[ +'🇮🇩', +'flag: Indonesia', +'flag flag indonesia nation country banner' +],[ +'🇮🇪', +'flag: Ireland', +'flag flag ireland ie nation country banner' +],[ +'🇮🇱', +'flag: Israel', +'flag flag israel il nation country banner' +],[ +'🇮🇲', +'flag: Isle of Man', +'flag flag isle of man isle man nation country banner' +],[ +'🇮🇳', +'flag: India', +'flag flag india in nation country banner' +],[ +'🇮🇴', +'flag: British Indian Ocean Territory', +'flag flag british indian ocean territory british indian ocean territory nation country banner' +],[ +'🇮🇶', +'flag: Iraq', +'flag flag iraq iq nation country banner' +],[ +'🇮🇷', +'flag: Iran', +'flag flag iran iran islamic republic nation country banner' +],[ +'🇮🇸', +'flag: Iceland', +'flag flag iceland is nation country banner' +],[ +'🇮🇹', +'flag: Italy', +'flag flag italy italy nation country banner' +],[ +'🇯🇪', +'flag: Jersey', +'flag flag jersey je nation country banner' +],[ +'🇯🇲', +'flag: Jamaica', +'flag flag jamaica jm nation country banner' +],[ +'🇯🇴', +'flag: Jordan', +'flag flag jordan jo nation country banner' +],[ +'🇯🇵', +'flag: Japan', +'flag flag japan japanese nation country banner' +],[ +'🇰🇪', +'flag: Kenya', +'flag flag kenya ke nation country banner' +],[ +'🇰🇬', +'flag: Kyrgyzstan', +'flag flag kyrgyzstan kg nation country banner' +],[ +'🇰🇭', +'flag: Cambodia', +'flag flag cambodia kh nation country banner' +],[ +'🇰🇮', +'flag: Kiribati', +'flag flag kiribati ki nation country banner' +],[ +'🇰🇲', +'flag: Comoros', +'flag flag comoros km nation country banner' +],[ +'🇰🇳', +'flag: St. Kitts & Nevis', +'flag flag st kitts nevis saint kitts nevis nation country banner' +],[ +'🇰🇵', +'flag: North Korea', +'flag flag north korea north korea nation country banner' +],[ +'🇰🇷', +'flag: South Korea', +'flag flag south korea south korea nation country banner' +],[ +'🇰🇼', +'flag: Kuwait', +'flag flag kuwait kw nation country banner' +],[ +'🇰🇾', +'flag: Cayman Islands', +'flag flag cayman islands cayman islands nation country banner' +],[ +'🇰🇿', +'flag: Kazakhstan', +'flag flag kazakhstan kz nation country banner' +],[ +'🇱🇦', +'flag: Laos', +'flag flag laos lao democratic republic nation country banner' +],[ +'🇱🇧', +'flag: Lebanon', +'flag flag lebanon lb nation country banner' +],[ +'🇱🇨', +'flag: St. Lucia', +'flag flag st lucia saint lucia nation country banner' +],[ +'🇱🇮', +'flag: Liechtenstein', +'flag flag liechtenstein li nation country banner' +],[ +'🇱🇰', +'flag: Sri Lanka', +'flag flag sri lanka sri lanka nation country banner' +],[ +'🇱🇷', +'flag: Liberia', +'flag flag liberia lr nation country banner' +],[ +'🇱🇸', +'flag: Lesotho', +'flag flag lesotho ls nation country banner' +],[ +'🇱🇹', +'flag: Lithuania', +'flag flag lithuania lt nation country banner' +],[ +'🇱🇺', +'flag: Luxembourg', +'flag flag luxembourg lu nation country banner' +],[ +'🇱🇻', +'flag: Latvia', +'flag flag latvia lv nation country banner' +],[ +'🇱🇾', +'flag: Libya', +'flag flag libya ly nation country banner' +],[ +'🇲🇦', +'flag: Morocco', +'flag flag morocco ma nation country banner' +],[ +'🇲🇨', +'flag: Monaco', +'flag flag monaco mc nation country banner' +],[ +'🇲🇩', +'flag: Moldova', +'flag flag moldova moldova republic nation country banner' +],[ +'🇲🇪', +'flag: Montenegro', +'flag flag montenegro me nation country banner' +],[ +'🇲🇫', +'flag: St. Martin', +'flag flag st martin' +],[ +'🇲🇬', +'flag: Madagascar', +'flag flag madagascar mg nation country banner' +],[ +'🇲🇭', +'flag: Marshall Islands', +'flag flag marshall islands marshall islands nation country banner' +],[ +'🇲🇰', +'flag: North Macedonia', +'flag flag north macedonia macedonia nation country banner' +],[ +'🇲🇱', +'flag: Mali', +'flag flag mali ml nation country banner' +],[ +'🇲🇲', +'flag: Myanmar (Burma)', +'flag flag myanmar mm nation country banner' +],[ +'🇲🇳', +'flag: Mongolia', +'flag flag mongolia mn nation country banner' +],[ +'🇲🇴', +'flag: Macao SAR China', +'flag flag macao sar china macao nation country banner' +],[ +'🇲🇵', +'flag: Northern Mariana Islands', +'flag flag northern mariana islands northern mariana islands nation country banner' +],[ +'🇲🇶', +'flag: Martinique', +'flag flag martinique mq nation country banner' +],[ +'🇲🇷', +'flag: Mauritania', +'flag flag mauritania mr nation country banner' +],[ +'🇲🇸', +'flag: Montserrat', +'flag flag montserrat ms nation country banner' +],[ +'🇲🇹', +'flag: Malta', +'flag flag malta mt nation country banner' +],[ +'🇲🇺', +'flag: Mauritius', +'flag flag mauritius mu nation country banner' +],[ +'🇲🇻', +'flag: Maldives', +'flag flag maldives mv nation country banner' +],[ +'🇲🇼', +'flag: Malawi', +'flag flag malawi mw nation country banner' +],[ +'🇲🇽', +'flag: Mexico', +'flag flag mexico mx nation country banner' +],[ +'🇲🇾', +'flag: Malaysia', +'flag flag malaysia my nation country banner' +],[ +'🇲🇿', +'flag: Mozambique', +'flag flag mozambique mz nation country banner' +],[ +'🇳🇦', +'flag: Namibia', +'flag flag namibia na nation country banner' +],[ +'🇳🇨', +'flag: New Caledonia', +'flag flag new caledonia new caledonia nation country banner' +],[ +'🇳🇪', +'flag: Niger', +'flag flag niger ne nation country banner' +],[ +'🇳🇫', +'flag: Norfolk Island', +'flag flag norfolk island norfolk island nation country banner' +],[ +'🇳🇬', +'flag: Nigeria', +'flag flag nigeria nation country banner' +],[ +'🇳🇮', +'flag: Nicaragua', +'flag flag nicaragua ni nation country banner' +],[ +'🇳🇱', +'flag: Netherlands', +'flag flag netherlands nl nation country banner' +],[ +'🇳🇴', +'flag: Norway', +'flag flag norway no nation country banner' +],[ +'🇳🇵', +'flag: Nepal', +'flag flag nepal np nation country banner' +],[ +'🇳🇷', +'flag: Nauru', +'flag flag nauru nr nation country banner' +],[ +'🇳🇺', +'flag: Niue', +'flag flag niue nu nation country banner' +],[ +'🇳🇿', +'flag: New Zealand', +'flag flag new zealand new zealand nation country banner' +],[ +'🇴🇲', +'flag: Oman', +'flag flag oman om symbol nation country banner' +],[ +'🇵🇦', +'flag: Panama', +'flag flag panama pa nation country banner' +],[ +'🇵🇪', +'flag: Peru', +'flag flag peru pe nation country banner' +],[ +'🇵🇫', +'flag: French Polynesia', +'flag flag french polynesia french polynesia nation country banner' +],[ +'🇵🇬', +'flag: Papua New Guinea', +'flag flag papua new guinea papua new guinea nation country banner' +],[ +'🇵🇭', +'flag: Philippines', +'flag flag philippines ph nation country banner' +],[ +'🇵🇰', +'flag: Pakistan', +'flag flag pakistan pk nation country banner' +],[ +'🇵🇱', +'flag: Poland', +'flag flag poland pl nation country banner' +],[ +'🇵🇲', +'flag: St. Pierre & Miquelon', +'flag flag st pierre miquelon saint pierre miquelon nation country banner' +],[ +'🇵🇳', +'flag: Pitcairn Islands', +'flag flag pitcairn islands pitcairn nation country banner' +],[ +'🇵🇷', +'flag: Puerto Rico', +'flag flag puerto rico puerto rico nation country banner' +],[ +'🇵🇸', +'flag: Palestinian Territories', +'flag flag palestinian territories palestine palestinian territories nation country banner' +],[ +'🇵🇹', +'flag: Portugal', +'flag flag portugal pt nation country banner' +],[ +'🇵🇼', +'flag: Palau', +'flag flag palau pw nation country banner' +],[ +'🇵🇾', +'flag: Paraguay', +'flag flag paraguay py nation country banner' +],[ +'🇶🇦', +'flag: Qatar', +'flag flag qatar qa nation country banner' +],[ +'🇷🇪', +'flag: Réunion', +'flag flag reunion réunion nation country banner' +],[ +'🇷🇴', +'flag: Romania', +'flag flag romania ro nation country banner' +],[ +'🇷🇸', +'flag: Serbia', +'flag flag serbia rs nation country banner' +],[ +'🇷🇺', +'flag: Russia', +'flag flag russia russian federation nation country banner' +],[ +'🇷🇼', +'flag: Rwanda', +'flag flag rwanda rw nation country banner' +],[ +'🇸🇦', +'flag: Saudi Arabia', +'flag flag saudi arabia nation country banner' +],[ +'🇸🇧', +'flag: Solomon Islands', +'flag flag solomon islands solomon islands nation country banner' +],[ +'🇸🇨', +'flag: Seychelles', +'flag flag seychelles sc nation country banner' +],[ +'🇸🇩', +'flag: Sudan', +'flag flag sudan sd nation country banner' +],[ +'🇸🇪', +'flag: Sweden', +'flag flag sweden se nation country banner' +],[ +'🇸🇬', +'flag: Singapore', +'flag flag singapore sg nation country banner' +],[ +'🇸🇭', +'flag: St. Helena', +'flag flag st helena saint helena ascension tristan cunha nation country banner' +],[ +'🇸🇮', +'flag: Slovenia', +'flag flag slovenia si nation country banner' +],[ +'🇸🇯', +'flag: Svalbard & Jan Mayen', +'flag flag svalbard jan mayen' +],[ +'🇸🇰', +'flag: Slovakia', +'flag flag slovakia sk nation country banner' +],[ +'🇸🇱', +'flag: Sierra Leone', +'flag flag sierra leone sierra leone nation country banner' +],[ +'🇸🇲', +'flag: San Marino', +'flag flag san marino san marino nation country banner' +],[ +'🇸🇳', +'flag: Senegal', +'flag flag senegal sn nation country banner' +],[ +'🇸🇴', +'flag: Somalia', +'flag flag somalia so nation country banner' +],[ +'🇸🇷', +'flag: Suriname', +'flag flag suriname sr nation country banner' +],[ +'🇸🇸', +'flag: South Sudan', +'flag flag south sudan south sd nation country banner' +],[ +'🇸🇹', +'flag: São Tomé & Príncipe', +'flag flag sao tome principe sao tome principe nation country banner' +],[ +'🇸🇻', +'flag: El Salvador', +'flag flag el salvador el salvador nation country banner' +],[ +'🇸🇽', +'flag: Sint Maarten', +'flag flag sint maarten sint maarten dutch nation country banner' +],[ +'🇸🇾', +'flag: Syria', +'flag flag syria syrian arab republic nation country banner' +],[ +'🇸🇿', +'flag: Eswatini', +'flag flag eswatini sz nation country banner' +],[ +'🇹🇦', +'flag: Tristan da Cunha', +'flag flag tristan da cunha' +],[ +'🇹🇨', +'flag: Turks & Caicos Islands', +'flag flag turks caicos islands turks caicos islands nation country banner' +],[ +'🇹🇩', +'flag: Chad', +'flag flag chad td nation country banner' +],[ +'🇹🇫', +'flag: French Southern Territories', +'flag flag french southern territories french southern territories nation country banner' +],[ +'🇹🇬', +'flag: Togo', +'flag flag togo tg nation country banner' +],[ +'🇹🇭', +'flag: Thailand', +'flag flag thailand th nation country banner' +],[ +'🇹🇯', +'flag: Tajikistan', +'flag flag tajikistan tj nation country banner' +],[ +'🇹🇰', +'flag: Tokelau', +'flag flag tokelau tk nation country banner' +],[ +'🇹🇱', +'flag: Timor-Leste', +'flag flag timor leste timor leste nation country banner' +],[ +'🇹🇲', +'flag: Turkmenistan', +'flag flag turkmenistan nation country banner' +],[ +'🇹🇳', +'flag: Tunisia', +'flag flag tunisia tn nation country banner' +],[ +'🇹🇴', +'flag: Tonga', +'flag flag tonga to nation country banner' +],[ +'🇹🇷', +'flag: Turkey', +'flag flag turkey turkey nation country banner' +],[ +'🇹🇹', +'flag: Trinidad & Tobago', +'flag flag trinidad tobago trinidad tobago nation country banner' +],[ +'🇹🇻', +'flag: Tuvalu', +'flag flag tuvalu nation country banner' +],[ +'🇹🇼', +'flag: Taiwan', +'flag flag taiwan tw nation country banner' +],[ +'🇹🇿', +'flag: Tanzania', +'flag flag tanzania tanzania united republic nation country banner' +],[ +'🇺🇦', +'flag: Ukraine', +'flag flag ukraine ua nation country banner' +],[ +'🇺🇬', +'flag: Uganda', +'flag flag uganda ug nation country banner' +],[ +'🇺🇲', +'flag: U.S. Outlying Islands', +'flag flag u s outlying islands' +],[ +'🇺🇳', +'flag: United Nations', +'flag flag united nations un banner' +],[ +'🇺🇸', +'flag: United States', +'flag flag united states united states america nation country banner' +],[ +'🇺🇾', +'flag: Uruguay', +'flag flag uruguay uy nation country banner' +],[ +'🇺🇿', +'flag: Uzbekistan', +'flag flag uzbekistan uz nation country banner' +],[ +'🇻🇦', +'flag: Vatican City', +'flag flag vatican city vatican city nation country banner' +],[ +'🇻🇨', +'flag: St. Vincent & Grenadines', +'flag flag st vincent grenadines saint vincent grenadines nation country banner' +],[ +'🇻🇪', +'flag: Venezuela', +'flag flag venezuela ve bolivarian republic nation country banner' +],[ +'🇻🇬', +'flag: British Virgin Islands', +'flag flag british virgin islands british virgin islands bvi nation country banner' +],[ +'🇻🇮', +'flag: U.S. Virgin Islands', +'flag flag u s virgin islands virgin islands us nation country banner' +],[ +'🇻🇳', +'flag: Vietnam', +'flag flag vietnam viet nam nation country banner' +],[ +'🇻🇺', +'flag: Vanuatu', +'flag flag vanuatu vu nation country banner' +],[ +'🇼🇫', +'flag: Wallis & Futuna', +'flag flag wallis futuna wallis futuna nation country banner' +],[ +'🇼🇸', +'flag: Samoa', +'flag flag samoa ws nation country banner' +],[ +'🇽🇰', +'flag: Kosovo', +'flag flag kosovo xk nation country banner' +],[ +'🇾🇪', +'flag: Yemen', +'flag flag yemen ye nation country banner' +],[ +'🇾🇹', +'flag: Mayotte', +'flag flag mayotte yt nation country banner' +],[ +'🇿🇦', +'flag: South Africa', +'flag flag south africa south africa nation country banner' +],[ +'🇿🇲', +'flag: Zambia', +'flag flag zambia zm nation country banner' +],[ +'🇿🇼', +'flag: Zimbabwe', +'flag flag zimbabwe zw nation country banner' +],[ +'🏴󠁧󠁢󠁥󠁮󠁧󠁿', +'flag: England', +'flag flag england english' +],[ +'🏴󠁧󠁢󠁳󠁣󠁴󠁿', +'flag: Scotland', +'flag flag scotland scottish' +],[ +'🏴󠁧󠁢󠁷󠁬󠁳󠁿', +'flag: Wales', +'flag flag wales welsh' +] +]; + +//this list is taken from https://unicode.org/emoji/charts/full-emoji-modifiers.html +//Full Emoji Modifier Sequences, v15.1 + +const MODED = [ +'👋🏻', +'🤚🏻', +'🖐🏻', +'✋🏻', +'🖖🏻', +'🫱🏻', +'🫲🏻', +'🫳🏻', +'🫴🏻', +'🫷🏻', +'🫸🏻', +'👌🏻', +'🤌🏻', +'🤏🏻', +'✌🏻', +'🤞🏻', +'🫰🏻', +'🤟🏻', +'🤘🏻', +'🤙🏻', +'👈🏻', +'👉🏻', +'👆🏻', +'🖕🏻', +'👇🏻', +'☝🏻', +'🫵🏻', +'👍🏻', +'👎🏻', +'✊🏻', +'👊🏻', +'🤛🏻', +'🤜🏻', +'👏🏻', +'🙌🏻', +'🫶🏻', +'👐🏻', +'🤲🏻', +'🤝🏻', +'🙏🏻', +'✍🏻', +'💅🏻', +'🤳🏻', +'💪🏻', +'🦵🏻', +'🦶🏻', +'👂🏻', +'🦻🏻', +'👃🏻', +'👶🏻', +'🧒🏻', +'👦🏻', +'👧🏻', +'🧑🏻', +'👱🏻', +'👨🏻', +'🧔🏻', +'🧔🏻‍♂️', +'🧔🏻‍♀️', +'👨🏻‍🦰', +'👨🏻‍🦱', +'👨🏻‍🦳', +'👨🏻‍🦲', +'👩🏻', +'👩🏻‍🦰', +'🧑🏻‍🦰', +'👩🏻‍🦱', +'🧑🏻‍🦱', +'👩🏻‍🦳', +'🧑🏻‍🦳', +'👩🏻‍🦲', +'🧑🏻‍🦲', +'👱🏻‍♀️', +'👱🏻‍♂️', +'🧓🏻', +'👴🏻', +'👵🏻', +'🙍🏻', +'🙍🏻‍♂️', +'🙍🏻‍♀️', +'🙎🏻', +'🙎🏻‍♂️', +'🙎🏻‍♀️', +'🙅🏻', +'🙅🏻‍♂️', +'🙅🏻‍♀️', +'🙆🏻', +'🙆🏻‍♂️', +'🙆🏻‍♀️', +'💁🏻', +'💁🏻‍♂️', +'💁🏻‍♀️', +'🙋🏻', +'🙋🏻‍♂️', +'🙋🏻‍♀️', +'🧏🏻', +'🧏🏻‍♂️', +'🧏🏻‍♀️', +'🙇🏻', +'🙇🏻‍♂️', +'🙇🏻‍♀️', +'🤦🏻', +'🤦🏻‍♂️', +'🤦🏻‍♀️', +'🤷🏻', +'🤷🏻‍♂️', +'🤷🏻‍♀️', +'🧑🏻‍⚕️', +'👨🏻‍⚕️', +'👩🏻‍⚕️', +'🧑🏻‍🎓', +'👨🏻‍🎓', +'👩🏻‍🎓', +'🧑🏻‍🏫', +'👨🏻‍🏫', +'👩🏻‍🏫', +'🧑🏻‍⚖️', +'👨🏻‍⚖️', +'👩🏻‍⚖️', +'🧑🏻‍🌾', +'👨🏻‍🌾', +'👩🏻‍🌾', +'🧑🏻‍🍳', +'👨🏻‍🍳', +'👩🏻‍🍳', +'🧑🏻‍🔧', +'👨🏻‍🔧', +'👩🏻‍🔧', +'🧑🏻‍🏭', +'👨🏻‍🏭', +'👩🏻‍🏭', +'🧑🏻‍💼', +'👨🏻‍💼', +'👩🏻‍💼', +'🧑🏻‍🔬', +'👨🏻‍🔬', +'👩🏻‍🔬', +'🧑🏻‍💻', +'👨🏻‍💻', +'👩🏻‍💻', +'🧑🏻‍🎤', +'👨🏻‍🎤', +'👩🏻‍🎤', +'🧑🏻‍🎨', +'👨🏻‍🎨', +'👩🏻‍🎨', +'🧑🏻‍✈️', +'👨🏻‍✈️', +'👩🏻‍✈️', +'🧑🏻‍🚀', +'👨🏻‍🚀', +'👩🏻‍🚀', +'🧑🏻‍🚒', +'👨🏻‍🚒', +'👩🏻‍🚒', +'👮🏻', +'👮🏻‍♂️', +'👮🏻‍♀️', +'🕵🏻', +'🕵🏻‍♂️', +'🕵🏻‍♀️', +'💂🏻', +'💂🏻‍♂️', +'💂🏻‍♀️', +'🥷🏻', +'👷🏻', +'👷🏻‍♂️', +'👷🏻‍♀️', +'🫅🏻', +'🤴🏻', +'👸🏻', +'👳🏻', +'👳🏻‍♂️', +'👳🏻‍♀️', +'👲🏻', +'🧕🏻', +'🤵🏻', +'🤵🏻‍♂️', +'🤵🏻‍♀️', +'👰🏻', +'👰🏻‍♂️', +'👰🏻‍♀️', +'🤰🏻', +'🫃🏻', +'🫄🏻', +'🤱🏻', +'👩🏻‍🍼', +'👨🏻‍🍼', +'🧑🏻‍🍼', +'👼🏻', +'🎅🏻', +'🤶🏻', +'🧑🏻‍🎄', +'🦸🏻', +'🦸🏻‍♂️', +'🦸🏻‍♀️', +'🦹🏻', +'🦹🏻‍♂️', +'🦹🏻‍♀️', +'🧙🏻', +'🧙🏻‍♂️', +'🧙🏻‍♀️', +'🧚🏻', +'🧚🏻‍♂️', +'🧚🏻‍♀️', +'🧛🏻', +'🧛🏻‍♂️', +'🧛🏻‍♀️', +'🧜🏻', +'🧜🏻‍♂️', +'🧜🏻‍♀️', +'🧝🏻', +'🧝🏻‍♂️', +'🧝🏻‍♀️', +'💆🏻', +'💆🏻‍♂️', +'💆🏻‍♀️', +'💇🏻', +'💇🏻‍♂️', +'💇🏻‍♀️', +'🚶🏻', +'🚶🏻‍♂️', +'🚶🏻‍♀️', +'🚶🏻‍➡️', +'🚶🏻‍♀️‍➡️', +'🚶🏻‍♂️‍➡️', +'🧍🏻', +'🧍🏻‍♂️', +'🧍🏻‍♀️', +'🧎🏻', +'🧎🏻‍♂️', +'🧎🏻‍♀️', +'🧎🏻‍➡️', +'🧎🏻‍♀️‍➡️', +'🧎🏻‍♂️‍➡️', +'🧑🏻‍🦯', +'🧑🏻‍🦯‍➡️', +'👨🏻‍🦯', +'👨🏻‍🦯‍➡️', +'👩🏻‍🦯', +'👩🏻‍🦯‍➡️', +'🧑🏻‍🦼', +'🧑🏻‍🦼‍➡️', +'👨🏻‍🦼', +'👨🏻‍🦼‍➡️', +'👩🏻‍🦼', +'👩🏻‍🦼‍➡️', +'🧑🏻‍🦽', +'🧑🏻‍🦽‍➡️', +'👨🏻‍🦽', +'👨🏻‍🦽‍➡️', +'👩🏻‍🦽', +'👩🏻‍🦽‍➡️', +'🏃🏻', +'🏃🏻‍♂️', +'🏃🏻‍♀️', +'🏃🏻‍➡️', +'🏃🏻‍♀️‍➡️', +'🏃🏻‍♂️‍➡️', +'💃🏻', +'🕺🏻', +'🕴🏻', +'🧖🏻', +'🧖🏻‍♂️', +'🧖🏻‍♀️', +'🧗🏻', +'🧗🏻‍♂️', +'🧗🏻‍♀️', +'🏇🏻', +'🏂🏻', +'🏌🏻', +'🏌🏻‍♂️', +'🏌🏻‍♀️', +'🏄🏻', +'🏄🏻‍♂️', +'🏄🏻‍♀️', +'🚣🏻', +'🚣🏻‍♂️', +'🚣🏻‍♀️', +'🏊🏻', +'🏊🏻‍♂️', +'🏊🏻‍♀️', +'⛹🏻', +'⛹🏻‍♂️', +'⛹🏻‍♀️', +'🏋🏻', +'🏋🏻‍♂️', +'🏋🏻‍♀️', +'🚴🏻', +'🚴🏻‍♂️', +'🚴🏻‍♀️', +'🚵🏻', +'🚵🏻‍♂️', +'🚵🏻‍♀️', +'🤸🏻', +'🤸🏻‍♂️', +'🤸🏻‍♀️', +'🤽🏻', +'🤽🏻‍♂️', +'🤽🏻‍♀️', +'🤾🏻', +'🤾🏻‍♂️', +'🤾🏻‍♀️', +'🤹🏻', +'🤹🏻‍♂️', +'🤹🏻‍♀️', +'🧘🏻', +'🧘🏻‍♂️', +'🧘🏻‍♀️', +'🛀🏻', +'🛌🏻', +'🧑🏻‍🤝‍🧑🏻', +'👭🏻', +'👫🏻', +'👬🏻', +'💏🏻', +'👩🏻‍❤️‍💋‍👨🏻', +'👨🏻‍❤️‍💋‍👨🏻', +'👩🏻‍❤️‍💋‍👩🏻', +'💑🏻', +'👩🏻‍❤️‍👨🏻', +'👨🏻‍❤️‍👨🏻', +'👩🏻‍❤️‍👩🏻' + +]; + +// MODED is the emojis with the skin tone modifier \u{1F3FB} included +// create MODABLE[], the same emoji but with the skin tone modifiers removed. +const MODABLE = []; +for (let i = 0; i < MODED.length; i++) { + MODABLE[i] = MODED[i].replace(/\u{1F3FB}/ug, ''); +} + +module.exports = {EMOJI, MODED, MODABLE}; diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/metadata.json b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/metadata.json new file mode 100644 index 0000000000..f830fa725e --- /dev/null +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/metadata.json @@ -0,0 +1,7 @@ +{ + "uuid": "icon-menu@cinnamon.org", + "name": "Icon Menu", + "description": "A grid layout applications menu.", + "icon": "applications-other", + "max-instances": -1 +} diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/settings-schema.json b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/settings-schema.json new file mode 100755 index 0000000000..a0d0d37d9f --- /dev/null +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/settings-schema.json @@ -0,0 +1,369 @@ +{ + "layout":{ + "type":"layout", + "pages":[ + "layout", + "behaviour", + "search", + "appearance" + ], + "layout":{ + "type":"page", + "title":"Layout", + "sections":[ + "menu-layout", + "menu-content" + ] + }, + "behaviour":{ + "type":"page", + "title":"Behavior", + "sections":[ + "panel-behave", + "menu-behave" + ] + }, + "search":{ + "type":"page", + "title":"Search", + "sections":[ + "search-search" + ] + }, + "appearance":{ + "type":"page", + "title":"Appearance", + "sections":[ + "panel-appear", + "menu-icons" + ] + }, + "menu-layout":{ + "type":"section", + "title":"Layout", + "keys":[ + "show-sidebar", + "sidebar-placement", + "sidebar-favorites", + "description-placement" + ] + }, + "menu-content":{ + "type":"section", + "title":"Content", + "keys":[ + "show-categories", + "show-places-category", + "show-recents-category", + "show-favorite-apps-category", + "show-home-folder-category", + "menu-editor-button" + ] + }, + "panel-behave":{ + "type":"section", + "title":"Panel", + "keys":[ + "overlay-key", + "activate-on-hover", + "hover-delay", + "enable-animation" + ] + }, + "menu-behave":{ + "type":"section", + "title":"Menu", + "keys":[ + "category-click", + "open-on-category", + "enable-autoscroll", + "show-hidden-files" + ] + }, + "search-search":{ + "type":"section", + "title":"Search", + "keys":[ + "enable-emoji-search", + "enable-home-folder-search", + "search-help" + ] + }, + "panel-appear":{ + "type":"section", + "title":"Panel", + "keys":[ + "menu-icon-custom", + "menu-icon", + "menu-icon-size-custom", + "menu-icon-size", + "menu-label" + ] + }, + "menu-icons":{ + "type":"section", + "title":"Menu", + "keys":[ + "category-icon-size", + "apps-grid-icon-size", + "sidebar-icon-size" + ] + } + }, + + "categories":{ + "type":"generic", + "default":[] + }, + "custom-menu-width":{ + "type":"generic", + "default":800 + }, + "custom-menu-height":{ + "type":"generic", + "default":530 + }, + "recent-apps":{ + "type":"generic", + "default":[] + }, + "folder-categories":{ + "type":"generic", + "default":[] + }, + + "show-sidebar":{ + "type":"switch", + "default":true, + "description":"Show sidebar", + "tooltip":"Show the sidebar in the menu" + }, + "sidebar-placement":{ + "dependency":"show-sidebar", + "type":"combobox", + "default":1, + "description":"Sidebar location", + "options":{ + "Top":0, + "Bottom":1, + "Left":2, + "Right":3 + }, + "tooltip":"Choose where to show the sidebar" + }, + "sidebar-favorites":{ + "dependency":"show-sidebar", + "type":"combobox", + "default":3, + "description":"Show favorites on sidebar", + "options":{ + "None":0, + "Apps only":1, + "Files only":2, + "Apps and files":3 + }, + "tooltip":"Show your favorite apps and file icons on the sidebar" + }, + "description-placement":{ + "type":"combobox", + "default":0, + "description":"Application description placement", + "options":{ + "Tooltips":0, + "Under titles":1, + "None":2 + }, + "tooltip":"Choose where to show application descriptions" + }, + + "show-categories":{ + "type":"switch", + "default":true, + "description":"Show categories", + "tooltip":"Show list of application categories" + }, + "show-places-category":{ + "dependency":"show-categories", + "type":"switch", + "default":true, + "description":"Show bookmarks and places", + "tooltip":"Show bookmarks and places category in the menu" + }, + "show-recents-category":{ + "dependency":"show-categories", + "type":"switch", + "default":true, + "description":"Show recent items", + "tooltip":"Show recent items category in the menu" + }, + "show-favorite-apps-category":{ + "dependency":"show-categories", + "type":"switch", + "default":false, + "description":"Show favorite apps category", + "tooltip":"Show your favorite apps category in the menu" + }, + "show-home-folder-category":{ + "dependency":"show-categories", + "type":"checkbox", + "default":true, + "description":"Show home folder", + "tooltip":"Show home folder category in the menu" + }, + "menu-editor-button":{ + "type":"button", + "description":"Open the menu editor", + "callback":"launchEditor" + }, + + "overlay-key":{ + "type":"keybinding", + "description":"Keyboard shortcut to open and close the menu", + "default":"Super_L::Super_R" + }, + "activate-on-hover":{ + "type":"checkbox", + "default":false, + "description":"Open menu on hover", + "tooltip":"Open the menu when I move my mouse over the panel icon" + }, + "hover-delay":{ + "dependency":"activate-on-hover", + "type":"spinbutton", + "default":50, + "min":0, + "max":1000, + "step":50, + "units":"milliseconds", + "description":"Menu hover delay" + }, + "enable-animation":{ + "type":"checkbox", + "default":false, + "description":"Use menu animations", + "tooltip":"Animate the menu on open and close" + }, + + "category-click":{ + "dependency":"show-categories", + "type":"checkbox", + "default":true, + "description":"Activate categories on click", + "tooltip":"Activate categories on click instead of on hover" + }, + "open-on-category":{ + "dependency":"show-categories", + "type":"combobox", + "default":0, + "description":"Open menu on category:", + "tooltip":"Always open the menu with this category selected", + "options":{ + "Last used category":0, + "Favorite apps":1, + "Recent":2, + "Places":3, + "All applications":4 + } + }, + "enable-autoscroll":{ + "type":"checkbox", + "default":true, + "description":"Enable autoscrolling", + "tooltip":"Autoscroll the application list on mouse hover" + }, + "show-hidden-files":{ + "dependency":"show-categories", + "type":"checkbox", + "default":false, + "description":"Show hidden files", + "tooltip":"Show hidden files in folder view" + }, + + "enable-emoji-search":{ + "type":"checkbox", + "default":false, + "description":"Emoji search", + "tooltip":"Show emojis in search results" + }, + "enable-home-folder-search":{ + "type":"checkbox", + "default":false, + "description":"Search home folder", + "tooltip":"When searching, also search my home folder for files" + }, + "search-help":{ + "type":"label", + "description":"\nNote: Search options can still be used if you have not enabled them here by prefixing your search term.\nFor example: to search for an emoji, type 'e heart' and use prefix 'f ' for file search.\n" + }, + + "menu-icon-custom":{ + "type":"checkbox", + "default":false, + "description":"Use a custom icon", + "tooltip":"Use a custom icon in the panel" + }, + "menu-icon":{ + "type":"iconfilechooser", + "default":"cinnamon-symbolic", + "description":"Panel icon", + "dependency":"menu-icon-custom", + "default_icon" : "cinnamon-symbolic", + "tooltip":"Select an icon to show in the panel." + }, + "menu-icon-size-custom":{ + "type":"checkbox", + "default":false, + "description":"Use a custom icon size", + "tooltip":"Use a custom size for the panel icon" + }, + "menu-icon-size":{ + "type":"spinbutton", + "default":24, + "min":0, + "max":60, + "step":1, + "units":"", + "description":"Panel icon size (pixels)", + "dependency":"menu-icon-size-custom", + "tooltip":"Choose a custom size for the panel icon" + }, + "menu-label":{ + "type":"entry", + "default":"Menu", + "description":"Panel text", + "tooltip":"Text to show beside the panel icon" + }, + + "category-icon-size":{ + "dependency":"show-categories", + "type":"spinbutton", + "default":24, + "min":0, + "max":64, + "step":1, + "units":"", + "description":"Category icon size (pixels)", + "tooltip":"Category icon size (pixels)" + }, + "apps-grid-icon-size":{ + "type":"spinbutton", + "default":64, + "min":0, + "max":128, + "step":1, + "units":"", + "description":"Applications grid icon size (pixels)", + "tooltip":"Applications grid icon size (pixels)" + }, + "sidebar-icon-size":{ + "dependency":"show-sidebar", + "type":"spinbutton", + "default":32, + "min":6, + "max":64, + "step":1, + "units":"", + "description":"Sidebar icon size (pixels)", + "tooltip":"Sidebar icon size (pixels)" + } +} diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/sidebar.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/sidebar.js new file mode 100644 index 0000000000..1d0ac61475 --- /dev/null +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/sidebar.js @@ -0,0 +1,416 @@ +const Gio = imports.gi.Gio; +const Gtk = imports.gi.Gtk; +const Atk = imports.gi.Atk; +const Clutter = imports.gi.Clutter; +const Util = imports.misc.util; +const St = imports.gi.St; +const Main = imports.ui.main; +const {SignalManager} = imports.misc.signalManager; +const {DragMotionResult, makeDraggable} = imports.ui.dnd; + +const { + wordWrap, + getThumbnail_gicon, + showTooltip, + hideTooltipIfVisible, + scrollToButton +} = require('./utils'); +const SidebarPlacement = Object.freeze({ TOP: 0, BOTTOM: 1, LEFT: 2, RIGHT: 3}); +const DescriptionPlacement = Object.freeze({TOOLTIP: 0, UNDER: 1, NONE: 2}); + +class SidebarButton { + constructor(appThis, icon, app, name, description, callback) { + //super({ hover: false, activate: false }); + this.appThis = appThis; + this.signals = new SignalManager(null); + this.app = app; + this.name = name; + this.description = description; + this.callback = callback; + this.actor = new St.BoxLayout({ + style_class: 'menu-favorites-button', + reactive: true, + accessible_role: Atk.Role.MENU_ITEM + }); + + this.has_focus = false; + if (icon) { + this.icon = icon; + this.actor.add_actor(this.icon); + } + + //----------dnd-------------- + this.actor._delegate = { //make all sidebar items a drag target for apps and files + handleDragOver: (source) => { + if (source.isDraggableApp) { + if (this.app && this.app.isApplication && source.id !== this.app.id) { + this.actor.set_opacity(40); + } + return DragMotionResult.MOVE_DROP; + } else if (source.isDraggableFile) { + return DragMotionResult.MOVE_DROP; + } + return DragMotionResult.NO_DROP; + }, + handleDragOut: () => { + if (this.app && this.app.isApplication) { + this.actor.set_opacity(255); + } + }, + acceptDrop: (source) => { + if (source.isDraggableApp) { + if (this.app && this.app.isApplication && source.id !== this.app.id) { + this.actor.set_opacity(255); + this.appThis.addFavoriteAppToPos(source.id, this.app.id); + return true; + } else if (!(this.app && this.app.isApplication)) { + this.appThis.appFavorites.addFavorite(source.id); + return true; + } + return DragMotionResult.NO_DROP + } else if (source.isDraggableFile){ + this.appThis.xappAddFavoriteFile(source.uri); + return true; + } + }, + }; + + if (this.app && this.app.isApplication) { //make sidebar apps draggable + Object.assign(this.actor._delegate, { + getDragActorSource: () => this.actor, + _getDragActor: () => new Clutter.Clone({source: this.actor}), + getDragActor: () => new Clutter.Clone({source: this.icon}), + id: this.app.id, + isDraggableApp: true + }); + + this.draggable = makeDraggable(this.actor); + this.signals.connect(this.draggable, 'drag-begin', () => hideTooltipIfVisible()); + } + + this.signals.connect(this.actor, 'enter-event', this.handleEnter.bind(this)); + this.signals.connect(this.actor, 'leave-event', this.handleLeave.bind(this)); + this.signals.connect(this.actor, 'button-release-event', this._handleButtonRelease.bind(this)); + } + + _handleButtonRelease(actor, e) { + if (this.appThis.display.contextMenu.isOpen) { + this.appThis.display.contextMenu.close(); + this.appThis.display.clearFocusedActors(); + this.handleEnter(); + return Clutter.EVENT_STOP; + } + + const button = e.get_button(); + if (button === Clutter.BUTTON_PRIMARY) { + this.activate(); + return Clutter.EVENT_STOP; + } else if (button === Clutter.BUTTON_SECONDARY) { + if (this.app != null) { + this.openContextMenu(e); + } + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + } + + activate() { + if (this.callback) { + this.callback(); + } else if (this.app.isApplication) { + this.appThis.recentApps.add(this.app.id); + this.app.open_new_window(-1); + this.appThis.menu.close(); + } else if (this.app.isFavoriteFile) { + try { + Gio.app_info_launch_default_for_uri(this.app.uri, global.create_app_launch_context()); + this.appThis.menu.close(); + } catch (e) { + Main.notify(_('Error while opening file:'), e.message); + } + } + } + + openContextMenu(e) { + hideTooltipIfVisible(); + this.appThis.display.contextMenu.openAppContextMenu(this.app, e, this.actor); + } + + handleEnter(actor, event) { + if (this.appThis.display.contextMenu.isOpen) { + return true; + } + + if (event) {//mouse event + this.appThis.display.clearFocusedActors(); + } else {//key nav + scrollToButton(this, this.appThis.settings.enableAnimation); + } + + this.has_focus = true; + this.actor.add_style_pseudo_class('hover'); + + //show tooltip + if (this.appThis.settings.descriptionPlacement === DescriptionPlacement.NONE) { + return Clutter.EVENT_STOP; + } + let [x, y] = this.actor.get_transformed_position(); + x += this.actor.width + 2 * global.ui_scale; + y += this.actor.height + 6 * global.ui_scale; + let text = `${this.name}`; + if (this.description) { + text += '\n' + wordWrap(this.description) + ''; + } + text = text.replace(/&/g, '&'); + showTooltip(this.actor, x, y, false /*don't center tooltip on x*/, text); + return Clutter.EVENT_STOP; + } + + handleLeave() { + if (this.appThis.display.contextMenu.isOpen) { + return true; + } + this.has_focus = false; + this.actor.remove_style_pseudo_class('hover'); + hideTooltipIfVisible(); + return true; + } + + destroy() { + this.signals.disconnectAllSignals(); + + if (this.icon) { + this.icon.destroy(); + } + this.actor.destroy(); + } +} + +class Separator { //creates a faint line (St.BoxLayout) used to separate items on the sidebar + constructor (appThis) { + this.appThis = appThis; + this.separator = new St.BoxLayout({x_expand: false, y_expand: false}); + + const getThemeForegroundColor = () => { + return this.appThis.menu.actor.get_theme_node().get_foreground_color().to_string().substring(0, 7); + } + + let width = this.appThis.settings.sidebarIconSize + 8; + let height = 2; + if (this.appThis.settings.sidebarPlacement === SidebarPlacement.TOP || + this.appThis.settings.sidebarPlacement === SidebarPlacement.BOTTOM) { + [width, height] = [height, width]; + } + this.separator.style = `width: ${width}px; height: ${height}px; background-color: ${ + getThemeForegroundColor()}; margin: 1px; border: 0px; border-radius: 10px; `; + this.separator.set_opacity(35); + } + + destroy() { + this.separator.destroy(); + } +} + +//Creates the sidebar. Creates SidebarButtons and populates the sidebar. +class Sidebar { + constructor (appThis) { + this.appThis = appThis; + this.items = []; + this.innerBox = new St.BoxLayout({ + vertical: (this.appThis.settings.sidebarPlacement === SidebarPlacement.LEFT + || this.appThis.settings.sidebarPlacement === SidebarPlacement.RIGHT) + }); + + this.sidebarScrollBox = new St.ScrollView({ y_align: St.Align.MIDDLE, style_class: 'vfade gridmenu-sidebar-scrollbox' }); + this.sidebarScrollBox.add_actor(this.innerBox); + this.sidebarScrollBox.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER); + this.sidebarScrollBox.set_clip_to_allocation(true); + this.sidebarScrollBox.set_auto_scrolling(this.appThis.settings.enableAutoScroll); + this.sidebarScrollBox.set_mouse_scrolling(true); + this.sidebarOuterBox = new St.BoxLayout({style_class: 'gridmenu-sidebar-box'}); + this.sidebarOuterBox.add(this.sidebarScrollBox, { }); + if (!this.appThis.settings.showSidebar) { + this.sidebarScrollBox.width = 0; + this.sidebarScrollBox.height = 0; + } + } + + populate () { + this.innerBox.remove_all_children(); + this.items.forEach(item => item.destroy()); + this.items = []; + this.separator1Position = null; + this.separator2Position = null; + if (this.separator1) { + this.separator1.destroy(); + this.separator1 = null; + } + if (this.separator2) { + this.separator2.destroy(); + this.separator2 = null; + } + //----add session buttons to this.items[] + const newSidebarIcon = (iconName) => { + return new St.Icon({ + icon_name: iconName, + icon_size: this.appThis.settings.sidebarIconSize, + icon_type: this.appThis.settings.sidebarIconSize <= 24 ? + St.IconType.SYMBOLIC : St.IconType.FULLCOLOR + }); + }; + this.items.push(new SidebarButton( + this.appThis, + newSidebarIcon('system-shutdown'), + null, + _('Quit'), + _('Shutdown the computer'), + () => { + this.appThis.menu.close(); + this.appThis.sessionManager.ShutdownRemote(); + } + )); + this.items.push(new SidebarButton( + this.appThis, newSidebarIcon('system-log-out'), + null, + _('Logout'), + _('Leave the session'), + () => { + this.appThis.menu.close(); + this.appThis.sessionManager.LogoutRemote(0); + } + )); + this.items.push(new SidebarButton( + this.appThis, + newSidebarIcon('system-lock-screen'), + null, _('Lock screen'), + _('Lock the screen'), + () => { + const screensaver_settings = new Gio.Settings({ + schema_id: 'org.cinnamon.desktop.screensaver' }); + const screensaver_dialog = Gio.file_new_for_path('/usr/bin/cinnamon-screensaver-command'); + if (screensaver_dialog.query_exists(null)) { + if (screensaver_settings.get_boolean('ask-for-away-message')) { + Util.spawnCommandLine('cinnamon-screensaver-lock-dialog'); + } else { + Util.spawnCommandLine('cinnamon-screensaver-command --lock');// + } + } else { + this.screenSaverProxy.LockRemote(''); + } + this.appThis.menu.close(); + } + )); + //----add favorite apps to this.items[] + if (this.appThis.settings.sidebarFavorites === 1 //Apps only + || this.appThis.settings.sidebarFavorites === 3) { // Apps and files + this.appThis.listFavoriteApps().forEach(fav => { + if (!this.separator1Position) { + this.separator1Position = this.items.length; + } + this.items.push(new SidebarButton( + this.appThis, + fav.create_icon_texture(this.appThis.settings.sidebarIconSize), + fav, + fav.name, + fav.description, + null + )); + }); + } + //----add favorite files to this.items[] + if (this.appThis.settings.sidebarFavorites === 2 //Files only + || this.appThis.settings.sidebarFavorites === 3) { // Apps and files + this.appThis.listFavoriteFiles().forEach(fav => { + if (!this.separator2Position) { + this.separator2Position = this.items.length; + } + let gicon = getThumbnail_gicon(fav.uri, fav.mimeType) || fav.gicon; + this.items.push(new SidebarButton( + this.appThis, + new St.Icon({ gicon: gicon, icon_size: this.appThis.settings.sidebarIconSize}), + fav, + fav.name, + fav.description, + null + )); + }); + } + //----change order of all items depending on buttons placement + const reverseOrder = this.appThis.settings.sidebarPlacement === SidebarPlacement.LEFT || + this.appThis.settings.sidebarPlacement === SidebarPlacement.RIGHT; + if (reverseOrder) { + this.items.reverse(); + } + + if (this.separator1Position) { + this.separator1 = new Separator(this.appThis); + } + if (this.separator2Position) { + this.separator2 = new Separator(this.appThis); + } + + //----populate box with items[] + for (let i = 0; i < this.items.length; i++) { + if (this.separator1Position && + ((reverseOrder && i == this.items.length - this.separator1Position) || + (!reverseOrder && i === this.separator1Position))){ + this.innerBox.add(this.separator1.separator, { + x_fill: false, + y_fill: false, + x_align: St.Align.MIDDLE, + y_align: St.Align.MIDDLE + }); + } + if (this.separator2Position && + ((reverseOrder && i == this.items.length - this.separator2Position) || + (!reverseOrder && i === this.separator2Position))){ + this.innerBox.add(this.separator2.separator, { + x_fill: false, + y_fill: false, + x_align: St.Align.MIDDLE, + y_align: St.Align.MIDDLE + }); + } + this.innerBox.add(this.items[i].actor, { + x_fill: false, + y_fill: false, + x_align: St.Align.MIDDLE, + y_align: St.Align.MIDDLE + }); + } + + return; + } + + scrollToQuitButton() { + scrollToButton(this.items[this.items.length - 1], false); + } + + getButtons() { + return this.items; + } + + clearSidebarFocusedActors() { + const foundItem = this.items.findIndex(button => button.has_focus); + if (foundItem > -1) { + this.items[foundItem].handleLeave(); + } + } + + destroy() { + this.items.forEach(item => item.destroy()); + this.items = null; + if (this.separator1) { + this.separator1.destroy(); + } + if (this.separator2) { + this.separator2.destroy(); + } + this.innerBox.destroy(); + this.sidebarScrollBox.destroy(); + this.sidebarOuterBox.destroy(); + } +} + +module.exports = {Sidebar}; diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/stylesheet.css b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/stylesheet.css new file mode 100755 index 0000000000..85639504e4 --- /dev/null +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/stylesheet.css @@ -0,0 +1,54 @@ +/*css selectors used by gridmenu (icon-menu) +┌──.menu-background .gridmenu .sidebar-top .sidebar-bottom .sidebar-left .sidebar-right────── +│┌──.menu-applications-outer-box .menu-applications-box────────────────────────────────────────────────────────────────────────── +││┌──.gridmenu-middle-pane─────────────────────────────────────────────────────────────────────────────────────────── +│││┌──.gridmenu-sidebar-box─────────────────┐┌──.menu-categories-scrollbox .vfade──┐┌──.menu-applications-scrollbox .vfade─────── +││││┌──.gridmenu-sidebar-scrollbox .vfade──┐││┌──.menu-categories-box─────────────┐││┌──.menu-applications-inner-box────────── +│││││ ││││ ││││┌──.menu-applications-header-text───┐ +│││││ ││││ ││││└───────────────────────────────────┘ +││││└─────── ││││ ││││┌──.menu-applications-grid-box────────┐ +│││└─────── ││││ ││││└─────────────────────────────────────┘ +││└────────────────── +││┌──.menu-search-box───────── +│││┌──#menu-search-entry────────── +*/ + +.menu-category-button:hover { /*Only used when "activate categories on click" option is set.*/ + background-color: rgba(128,128,128,0.2); + border-radius: 4px; + border-image: none; +} + +.menu-category-button:highlighted, /*Used to highlight category containing newly installed apps.*/ +.menu-category-button-selected:highlighted { + font-weight: bold; +} + +.gridmenu .menu-applications-inner-box { + padding-left: 0px; + padding-right: 0px; +} + +.menu-applications-grid-box .menu-application-button, +.menu-applications-grid-box .menu-application-button-selected { + padding: 10px 2px; +} + +.menu-applications-grid-box .menu-application-button-label, +.menu-applications-grid-box .menu-application-button-label:ltr, +.menu-applications-grid-box .menu-application-button-label:rtl { + text-align: center; + padding-left: 0px; + padding-right: 0px; +} + +.gridmenu .menu-categories-box { + padding-left: 6px; + padding-right: 6px; +} + +.gridmenu .menu-search-box, .gridmenu .menu-search-box:ltr, .gridmenu .menu-search-box:rtl { + min-width: 160px; + padding: 0px 8px; +} + diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/utils.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/utils.js new file mode 100755 index 0000000000..07e6117238 --- /dev/null +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/utils.js @@ -0,0 +1,254 @@ +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; +const Mainloop = imports.mainloop; +const St = imports.gi.St; +const Main = imports.ui.main; +const ByteArray = imports.byteArray; +const {addTween} = imports.ui.tweener; + +const wordWrap = text => text.match( /.{1,80}(\s|$|-|=|\+|_|&|\\)|\S+?(\s|$|-|=|\+|_|&|\\)/g ).join('\n'); + +const graphemeBaseChars = s => +//decompose and remove discritics (blocks: Combining Diacritical Marks, +//Combining Diacritical Marks Extended and Combining Diacritical Marks Supplement) + s.normalize('NFKD').replace(/[\u0300-\u036F\u1AB0-\u1AFF\u1DC0-\u1DFF]/g, ""); + +function escapeRegExp(str) { + // from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + return str.replace(/[-\/.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +//=========================================================== + +const getThumbnail_gicon = (uri, mimeType) => { + //Note: this function doesn't check if thumbnail is up to date. + const file = Gio.File.new_for_uri(uri); + if (!file.query_exists(null)) {//check because it's possible for isFavoriteFile's to not exist. + return null; + } + // + const isImage = ['image/jpeg', 'image/png', 'image/svg+xml', 'image/tiff', 'image/bmp', + 'image/gif'].includes(mimeType); + const fileSize = file.query_info('standard::size', Gio.FileQueryInfoFlags.NONE, null).get_size(); + + //----Get thumbnail from cache + if (!(isImage && fileSize < 50000)) {//Don't bother with thumbnail cache if file is a + //small image, quicker to just create icon from file itself and avoids + //possible out of date cached thumbnail. + const ba = ByteArray.fromString(uri, 'UTF-8'); + const md5 = GLib.Checksum.new(GLib.ChecksumType.MD5); + md5.update(ba); + const thumbDir = GLib.get_user_cache_dir() + '/thumbnails/'; + const thumbName = md5.get_string() + '.png'; + const thumbPathNormal = thumbDir + 'normal/' + thumbName; + const thumbPathLarge = thumbDir + 'large/' + thumbName; + if (GLib.file_test(thumbPathNormal, GLib.FileTest.EXISTS)) { + return new Gio.FileIcon({ file: Gio.file_new_for_path(thumbPathNormal) }); + } + if (GLib.file_test(thumbPathLarge, GLib.FileTest.EXISTS)) { + return new Gio.FileIcon({ file: Gio.file_new_for_path(thumbPathLarge) }); + } + } + + //----No cached thumbnail available so make icon from image. + if (isImage && fileSize < 30000000) {//don't read image files > 30MB + return new Gio.FileIcon({ file: file }); + } + + //----No thumbnail + return null; +}; + +//============================================================ + +let onlyOneTooltip = null; +const showTooltip = (actor, xpos, ypos, center_on_xpos, text) => { + if (onlyOneTooltip) { + global.logWarning("gridmenu: Previous tooltip still exists...removing..."); + onlyOneTooltip.destroy(); + onlyOneTooltip = null; + } + onlyOneTooltip = new NewTooltip (actor, xpos, ypos, center_on_xpos, text); +}; + +const hideTooltipIfVisible = () => { + if (onlyOneTooltip) { + onlyOneTooltip.destroy(); + onlyOneTooltip = null; + } +}; + +class NewTooltip { + constructor(actor, xpos, ypos, center_on_xpos /*boolean*/, text) { + this.actor = actor; + this.xpos = xpos; + this.ypos = ypos; + this.center_on_xpos = center_on_xpos; + this.text = text; + if (this.text && this.text !== '') { + this.showTimer = Mainloop.timeout_add(250, () => this.show()); + } + } + + show() { + this.showTimer = null; + + this.tooltip = new St.Label({ + name: 'Tooltip' + }); + this.tooltip.show_on_set_parent = true; + Main.uiGroup.add_actor(this.tooltip); + this.tooltip.get_clutter_text().set_markup(this.text); + this.tooltip.set_style('text-align: left;'); + + let tooltipWidth = this.tooltip.get_allocation_box().x2 - this.tooltip.get_allocation_box().x1; + let tooltipHeight = this.tooltip.get_allocation_box().y2 - this.tooltip.get_allocation_box().y1; + let monitor = Main.layoutManager.findMonitorForActor(this.actor); + let tooltipLeft = this.xpos; + let tooltipTop = this.ypos; + if (this.center_on_xpos) { + tooltipLeft -= Math.floor(tooltipWidth / 3); + } + tooltipLeft = Math.max(tooltipLeft, monitor.x); + tooltipLeft = Math.min(tooltipLeft, monitor.x + monitor.width - tooltipWidth); + tooltipTop = Math.max(tooltipTop, monitor.y); + tooltipTop = Math.min(tooltipTop, monitor.y + monitor.height - tooltipHeight); + + this.tooltip.set_position(tooltipLeft, tooltipTop); + this.tooltip.raise_top(); + this.tooltip.show(); + } + + destroy() { + if (this.showTimer) { + Mainloop.source_remove(this.showTimer); + this.showTimer = null; + } + if (this.tooltip) { + this.tooltip.destroy(); + this.tooltip = null; + } + } +} + +//=================================================== +const searchStrPart = (q, str, noFuzzySearch, noSubStringSearch) => { + if (!str || !q) { + return 0; //match score = 0 + } + + const str2 = graphemeBaseChars(str).toLocaleUpperCase(); + //q is already graphemeBaseChars().toLocaleUpperCase() in _doSearch() + let score = 0, bigrams_score = 0; + + if (new RegExp('\\b'+escapeRegExp(q)).test(str2)) { //match substring from beginning of words + const foundPosition = str2.indexOf(q); + score = (foundPosition === 0) ? 1.21 : 1.2;//slightly higher score if from beginning + } else if (!noSubStringSearch && str2.indexOf(q) !== -1) { //else match substring + score = 1.1; + } else if (!noFuzzySearch){ //else fuzzy match + //find longest substring of str2 made up of letters from q + const found = str2.match(new RegExp('[' + escapeRegExp(q) + ']+','g')); + let length = 0; + let longest; + if (found) { + for(let i=0; i < found.length; i++){ + if(found[i].length > length){ + length = found[i].length; + longest = found[i]; + } + } + } + if (longest) { + //get a score for similarity by counting 2 letter pairs (bigrams) that match + if (q.length >= 2) { + const max_bigrams = q.length -1; + let found_bigrams = 0; + for (let qi = 0; qi < max_bigrams; qi++ ) { + if (longest.indexOf(q[qi] + q[qi + 1]) >= 0) { + found_bigrams++; + } + } + bigrams_score = Math.min(found_bigrams / max_bigrams, 1); + } else { + bigrams_score = 1; + } + + //return a fuzzy match score of between 0 and 1. + score = Math.min(longest.length / q.length, 1.0) * bigrams_score; + } + } + + return score; +}; + +const searchStr = (q, str, noFuzzySearch = false, noSubStringSearch = false) => { + const separatorIndex = q.indexOf(" "); + if (separatorIndex < 1) { + return searchStrPart(q, str, noFuzzySearch, noSubStringSearch); + } + + //There are two search terms separated by a space. + const part1Score = searchStrPart(q.slice(0, separatorIndex), str, noFuzzySearch, noSubStringSearch); + const part2Score = searchStrPart(q.slice(separatorIndex + 1), str, noFuzzySearch, noSubStringSearch); + const avgScore = (part1Score + part2Score) / 2.0; + + return avgScore; +}; + +var scrollToButton = (button, enableAnimation) => { + let scrollBox = button.actor.get_parent(); + let i = 0; + while (!(scrollBox instanceof St.ScrollView)) { + i++; + if (i > 10 || !scrollBox) { + global.logWarning('gridmenu: Unable to find scrollbox for' + button.actor.toString()); + return false; + } + scrollBox = scrollBox.get_parent(); + } + + const adjustment = scrollBox.vscroll.adjustment; + let [value, lower, upper, stepIncrement, pageIncrement, pageSize] = adjustment.get_values(); + + let offset = 0; + const vfade = scrollBox.get_effect('fade');//this always seems to return null? + if (vfade) { + offset = vfade.vfade_offset; + } + + const box = button.actor.get_allocation_box(); + const y1 = box.y1, y2 = box.y2; + const PADDING_ALLOWANCE = 20; //In case button parent(s) have padding meaning y1 won't go to 0 + if (y1 < value + offset) { + if (y1 < PADDING_ALLOWANCE) { + value = 0; + } else { + value = Math.max(0, y1 - offset); + } + } else if (y2 > value + pageSize - offset) { + if (y2 > upper - offset - PADDING_ALLOWANCE) { + value = upper - pageSize; + } else { + value = Math.min(upper, y2 + offset - pageSize); + } + } else { + return false; + } + + if (enableAnimation) { + addTween(adjustment, {value: value, time: 0.1, transition: 'easeOutQuad'}); + } else { + adjustment.set_value(value); + } +} + +module.exports = { + wordWrap, + graphemeBaseChars, + getThumbnail_gicon, + showTooltip, + hideTooltipIfVisible, + searchStr, + scrollToButton +}; From c7343d12452811f3d6ac1298bd0b14d0f612263d Mon Sep 17 00:00:00 2001 From: fredcw <58893963+fredcw@users.noreply.github.com> Date: Wed, 4 Jun 2025 18:26:07 +0100 Subject: [PATCH 2/3] fix minor UI bug --- .../applets/icon-menu@cinnamon.org/applet.js | 12 +++++++----- .../applets/icon-menu@cinnamon.org/display.js | 11 ++++++----- .../applets/icon-menu@cinnamon.org/sidebar.js | 4 ---- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/applet.js index 60f3b7467c..b1c3302b92 100644 --- a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/applet.js @@ -409,10 +409,10 @@ class CinnamenuApplet extends TextIconApplet { _onOpenStateToggled(menu, open) { if (global.settings.get_boolean('panel-edit-mode')) { - return false; + return; } if (!open) { - return true; // this._onMenuClosed() is called on 'menu-animated-closed' signal to handle closing. + return; // this._onMenuClosed() is called on 'menu-animated-closed' signal to handle closing. } if (this.openMenuTimeoutId) { @@ -421,8 +421,10 @@ class CinnamenuApplet extends TextIconApplet { } this.display.categoriesView.update();//in case menu editor or enabled category changes. - this.display.sidebar.populate();//in case fav files changed - this.display.sidebar.scrollToQuitButton();//ensure quit button is visible + if (this.settings.showSidebar) { + this.display.sidebar.populate();//in case fav files changed + this.display.sidebar.scrollToQuitButton();//ensure quit button is visible + } global.stage.set_key_focus(this.display.searchView.searchEntry); if (this.currentCategory === 'places' && !this.settings.showPlaces || @@ -463,7 +465,7 @@ class CinnamenuApplet extends TextIconApplet { this.display.appsView.focusFirstItem(); } - return true; + return; } _onMenuClosed() { diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/display.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/display.js index baef65768e..ff27b0eb63 100644 --- a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/display.js +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/display.js @@ -42,7 +42,8 @@ class Display { (...args) => this.appThis._onMenuKeyPress(...args) ); this.bottomPane = new St.BoxLayout({}); - if (sidebarPlacement === SidebarPlacement.TOP || sidebarPlacement === SidebarPlacement.BOTTOM) { + if (this.appThis.settings.showSidebar && (sidebarPlacement === SidebarPlacement.TOP || + sidebarPlacement === SidebarPlacement.BOTTOM)) { this.bottomPane.add(this.sidebar.sidebarOuterBox, { expand: false, x_fill: false, @@ -63,7 +64,7 @@ class Display { this.appsView = new AppsView(this.appThis); this.categoriesView = new CategoriesView(this.appThis); this.middlePane = new St.BoxLayout({style_class: 'gridmenu-middle-pane'}); - if (sidebarPlacement === SidebarPlacement.LEFT) { + if (this.appThis.settings.showSidebar && sidebarPlacement === SidebarPlacement.LEFT) { this.middlePane.add(this.sidebar.sidebarOuterBox, { expand: false, x_fill: false, @@ -85,7 +86,7 @@ class Display { y_align: St.Align.START, expand: false }); - if (sidebarPlacement === SidebarPlacement.RIGHT) { + if (this.appThis.settings.showSidebar && sidebarPlacement === SidebarPlacement.RIGHT) { this.middlePane.add(this.sidebar.sidebarOuterBox, { expand: false, x_fill: false, @@ -225,8 +226,8 @@ class Display { //find minimum width for categoriesView + sidebar (if present) let leftSideWidth = this.categoriesView.groupCategoriesWorkspacesScrollBox.width; - if (this.appThis.settings.sidebarPlacement === SidebarPlacement.LEFT || - this.appThis.settings.sidebarPlacement === SidebarPlacement.RIGHT) { + if (this.appThis.settings.showSidebar && (this.appThis.settings.sidebarPlacement === SidebarPlacement.LEFT || + this.appThis.settings.sidebarPlacement === SidebarPlacement.RIGHT)) { leftSideWidth += this.sidebar.sidebarOuterBox.width; } diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/sidebar.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/sidebar.js index 1d0ac61475..465055526d 100644 --- a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/sidebar.js +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/sidebar.js @@ -230,10 +230,6 @@ class Sidebar { this.sidebarScrollBox.set_mouse_scrolling(true); this.sidebarOuterBox = new St.BoxLayout({style_class: 'gridmenu-sidebar-box'}); this.sidebarOuterBox.add(this.sidebarScrollBox, { }); - if (!this.appThis.settings.showSidebar) { - this.sidebarScrollBox.width = 0; - this.sidebarScrollBox.height = 0; - } } populate () { From 7e66e056470917cd6e58cfa9248f501f782a41c1 Mon Sep 17 00:00:00 2001 From: fredcw <58893963+fredcw@users.noreply.github.com> Date: Tue, 24 Jun 2025 12:17:52 +0100 Subject: [PATCH 3/3] Minor fixes --- .../applets/icon-menu@cinnamon.org/applet.js | 36 ++++--------------- .../icon-menu@cinnamon.org/categoriesview.js | 2 +- .../icon-menu@cinnamon.org/contextmenu.js | 24 +++++++++---- .../applets/icon-menu@cinnamon.org/display.js | 1 - .../applets/icon-menu@cinnamon.org/sidebar.js | 3 +- .../icon-menu@cinnamon.org/stylesheet.css | 8 +++++ 6 files changed, 35 insertions(+), 39 deletions(-) diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/applet.js index b1c3302b92..0f4a12bb82 100644 --- a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/applet.js @@ -125,6 +125,7 @@ class CinnamenuApplet extends TextIconApplet { 'menu-animated-closed', this._onMenuClosed.bind(this) ); + this.signals.connect(XApp.Favorites.get_default(), 'changed', () => this._onXappFavoritesChange()); this.apps = new Apps(this.appSystem); this.screenSaverProxy = new ScreenSaverProxy(); this.sessionManager = new GnomeSession.SessionManager(); @@ -133,9 +134,9 @@ class CinnamenuApplet extends TextIconApplet { Main.keybindingManager.addHotKey( 'overlay-key-' + this.instance_id, this.settings.overlayKey, - () => { + () => { if (Main.overview.visible || Main.expo.visible) return; - if (!this.isOpen) { + if (!this.menu.isOpen) { this.panel.peekPanel(); } this.menu.toggle_with_options(this.settings.enableAnimation); @@ -359,7 +360,9 @@ class CinnamenuApplet extends TextIconApplet { } } - updateAfterXappFavoriteFileChange() { + _onXappFavoritesChange() { + if (!this.menu.isOpen) return; + this.display.sidebar.populate(); this.display.categoriesView.update();//in case fav files category needs adding/removing this.display.updateMenuSize(); @@ -368,25 +371,6 @@ class CinnamenuApplet extends TextIconApplet { } } - xappGetIsFavoriteFile(uri) { - const favs = XApp.Favorites.get_default(); - return favs.find_by_uri(uri) !== null; - } - - xappAddFavoriteFile(uri) { - const favs = XApp.Favorites.get_default(); - favs.add(uri); - //xapp favs list doesn't update synchronously after adding fav so add small - //delay before updating menu. - Mainloop.timeout_add(100, this.updateAfterXappFavoriteFileChange.bind(this)); - } - - xappRemoveFavoriteFile(uri) { - const favs = XApp.Favorites.get_default(); - favs.remove(uri); - this.updateAfterXappFavoriteFileChange(); - } - getIsFolderCategory(path) { const index = this.settings.folderCategories.indexOf(path); return index > -1; @@ -750,14 +734,6 @@ class CinnamenuApplet extends TextIconApplet { categoryButtons[focusedCategoryIndex].selectCategory(); } return Clutter.EVENT_STOP; - case symbol === Clutter.unicode_to_keysym("p".charCodeAt(0)) && ctrlKey: - if (focusedAppItemExists && appButtons[focusedAppItemIndex].app.isApplication) { - const desktop_file_path = appButtons[focusedAppItemIndex].app.desktop_file_path; - Util.spawn(['cinnamon-desktop-editor', '-mlauncher', '-o' + desktop_file_path]); - this.menu.close(); - return Clutter.EVENT_STOP; - } - return Clutter.EVENT_PROPAGATE case (symbol === Clutter.KEY_Up || symbol === Clutter.KEY_KP_Up) && noModifiers: leaveCurrentlyFocusedItem(); upNavigation(); diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/categoriesview.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/categoriesview.js index 10da378776..67986ba5b6 100644 --- a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/categoriesview.js +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/categoriesview.js @@ -338,7 +338,7 @@ class CategoriesView { button = new CategoryButton(this.appThis, folder, displayName, null, gicon); newButtons.push(button); } catch(e) { - log("gridmenu:Error creating folder category: " + folder + " ...skipping."); + global.log("gridmenu: Error creating folder category: " + folder + " ...skipping."); //remove this error causing element from the array. folderCategories.splice(index, 1); folderCategoriesChanged = true; diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/contextmenu.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/contextmenu.js index 3e5106f1d5..170756c688 100644 --- a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/contextmenu.js +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/contextmenu.js @@ -2,6 +2,7 @@ const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const St = imports.gi.St; const Clutter = imports.gi.Clutter; +const XApp = imports.gi.XApp; const Main = imports.ui.main; const {PopupBaseMenuItem, PopupMenu, PopupSeparatorMenuItem} = imports.ui.popupMenu; const {getUserDesktopDir, changeModeGFile} = imports.misc.fileUtils; @@ -239,7 +240,7 @@ class ContextMenu { file.copy( destFile, 0, null, null); changeModeGFile(destFile, 755); } catch(e) { - global.logError('gridmenu: Error creating desktop file', e); + global.logError('gridmenu: Error creating desktop file', e.message); } this.close(); } @@ -277,9 +278,20 @@ class ContextMenu { //show app info if (this.appThis._pamacManagerAvailable) { addMenuItem( new ContextMenuItem(this.appThis, _('App Info'), 'dialog-information', - () => { spawnCommandLine("/usr/bin/pamac-manager --details-id=" + app.id); - this.appThis.menu.close(); } )); + () => { + spawnCommandLine("/usr/bin/pamac-manager --details-id=" + app.id); + this.appThis.menu.close(); + } + )); } + + //Properties + addMenuItem( new ContextMenuItem(this.appThis, _('Properties'), 'document-properties-symbolic', + () => { + spawnCommandLine('cinnamon-desktop-editor -mlauncher -o ' + app.desktop_file_path); + this.appThis.menu.close(); + } + )); } _populateContextMenu_files(app) { @@ -331,17 +343,17 @@ class ContextMenu { //add/remove favorite this.menu.addMenuItem(new PopupSeparatorMenuItem(this.appThis)); - if (this.appThis.xappGetIsFavoriteFile(app.uri)) { //favorite + if (XApp.Favorites.get_default().find_by_uri(app.uri) !== null) { //favorite addMenuItem( new ContextMenuItem(this.appThis, _('Remove from favorites'), 'starred', () => { - this.appThis.xappRemoveFavoriteFile(app.uri); + XApp.Favorites.get_default().remove(app.uri); this.close(); } )); } else { addMenuItem( new ContextMenuItem(this.appThis, _('Add to favorites'), 'non-starred', () => { - this.appThis.xappAddFavoriteFile(app.uri); + XApp.Favorites.get_default().add(app.uri); this.close(); } )); diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/display.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/display.js index ff27b0eb63..eab370bde7 100644 --- a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/display.js +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/display.js @@ -106,7 +106,6 @@ class Display { reactive: true, show_on_set_parent: false }); - this.mainBox.add_style_class_name('menu-applications-box'); //this is to support old themes if (sidebarPlacement === SidebarPlacement.TOP && this.appThis.settings.showSidebar) { this.mainBox.add(this.bottomPane); } diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/sidebar.js b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/sidebar.js index 465055526d..a872ac7fe2 100644 --- a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/sidebar.js +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/sidebar.js @@ -4,6 +4,7 @@ const Atk = imports.gi.Atk; const Clutter = imports.gi.Clutter; const Util = imports.misc.util; const St = imports.gi.St; +const XApp = imports.gi.XApp; const Main = imports.ui.main; const {SignalManager} = imports.misc.signalManager; const {DragMotionResult, makeDraggable} = imports.ui.dnd; @@ -69,7 +70,7 @@ class SidebarButton { } return DragMotionResult.NO_DROP } else if (source.isDraggableFile){ - this.appThis.xappAddFavoriteFile(source.uri); + XApp.Favorites.get_default().add(source.uri); return true; } }, diff --git a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/stylesheet.css b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/stylesheet.css index 85639504e4..c7481b1fb8 100755 --- a/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/stylesheet.css +++ b/files/usr/share/cinnamon/applets/icon-menu@cinnamon.org/stylesheet.css @@ -13,6 +13,14 @@ │││┌──#menu-search-entry────────── */ +/* The reason that some style css is in this file rather than in cinnamon's default theme is + * so that the user theme does not override the more specific selectors here with less specific + * selectors. For instance, ".gridmenu .menu-categories-box" in the default theme would have no + * effect if ".menu-categories-box" is defined in the user theme. By putting it here, it still + * takes effect, unless ".gridmenu .menu-categories-box" specifically, is overridden in the + * user theme. + */ + .menu-category-button:hover { /*Only used when "activate categories on click" option is set.*/ background-color: rgba(128,128,128,0.2); border-radius: 4px;