Skip to content

Commit

Permalink
menu@cinnamon.org: Allow items to be re-ordered to better reflect
Browse files Browse the repository at this point in the history
the search outcome.

- The app buttons have always been sorted prior to having their
  actors added to their container. As a result, their original order
  within the app box can always be restored.
- When a search is made, an index is assigned to resulting items,
  corresponding to the position of the pattern within the search
  string. The four app search strings (name, keywords, description,
  id) are weighted, so name matches come before keywords, and so on.
- Places will always display after apps, and recents after places,
  maintaining their original respective positions.

Sorting is done simply by comparing the search indices for all
matching buttons:

Examples:
- Given two apps with with the names "Files" and "The File Manager"
  with "file" as the search string, "File" has a match value of 0
  (matched at the start of the name string) and "The File Manager"
  has an index of 4 (position 4), so "File" will appear before
  "The File Manager" in the search results.
- Given two apps with matches of "File" in the name for one, and
  "browse,file,manage" in the keywords of the second, the first
  will have a match value of 0 and the second will have a value of
  1007.

Taking the second example further, a match in the description would
have a value >= 2000, and the app id would be >= 3000.  Recents and
places share a 4000 base value - they will always appear after any
apps in the search results.

Ties are broken by alphabetic sorting of the app name (which will
always be unique).

Performance appears to be little affected when searching, and there
shouldn't be much imact at all on normal non-search operation of
the applet.
  • Loading branch information
mtwebster committed Sep 14, 2020
1 parent d9a4a3f commit a6738bd
Showing 1 changed file with 123 additions and 49 deletions.
172 changes: 123 additions & 49 deletions files/usr/share/cinnamon/applets/menu@cinnamon.org/applet.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ const RefreshFlags = Object.freeze({
});
const REFRESH_ALL_MASK = 0b11111;

const NO_MATCH = 99999;
const APP_MATCH_ADDERS = [
0, // name
1000, // keywords
2000, // desc
3000 // id
];
const RECENT_PLACES_ADDER = 4000;

/* VisibleChildIterator takes a container (boxlayout, etc.)
* and creates an array of its visible children and their index
* positions. We can then work through that list without
Expand Down Expand Up @@ -168,6 +177,8 @@ class SimpleMenuItem {
this.label = null;
this.icon = null;

this.matchIndex = NO_MATCH;

for (let prop in params)
this[prop] = params[prop];

Expand Down Expand Up @@ -529,6 +540,13 @@ class ApplicationButton extends GenericApplicationButton {
this._draggable = DND.makeDraggable(this.actor);
this._signals.connect(this._draggable, 'drag-end', Lang.bind(this, this._onDragEnd));
this.isDraggableApp = true;

this.searchStrings = [
Util.latinise(app.get_name().toLowerCase()),
app.get_keywords() ? Util.latinise(app.get_keywords().toLowerCase()) : "",
app.get_description() ? Util.latinise(app.get_description().toLowerCase()) : "",
app.get_id() ? Util.latinise(app.get_id().toLowerCase()) : ""
];
}

get_app_id() {
Expand Down Expand Up @@ -622,6 +640,10 @@ class PlaceButton extends SimpleMenuItem {
this.icon.visible = false;

this.addLabel(this.name, 'menu-application-button-label');

this.searchStrings = [
Util.latinise(place.name.toLowerCase())
];
}

activate() {
Expand Down Expand Up @@ -670,6 +692,10 @@ class RecentButton extends SimpleMenuItem {
this.icon.visible = false;

this.addLabel(this.name, 'menu-application-button-label');

this.searchStrings = [
Util.latinise(recent.name.toLowerCase())
];
}

activate() {
Expand Down Expand Up @@ -1085,6 +1111,8 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
this.lastSelectedCategory = null;
this.settings.bind("force-show-panel", "forceShowPanel");

this.orderDirty = false;

// We shouldn't need to call refreshAll() here... since we get a "icon-theme-changed" signal when CSD starts.
// The reason we do is in case the Cinnamon icon theme is the same as the one specificed in GTK itself (in .config)
// In that particular case we get no signal at all.
Expand Down Expand Up @@ -2586,6 +2614,22 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
}
}

_resetSortOrder() {
let pos = 0;

for (let i = 0; i < this._applicationsButtons.length; i++) {
this.applicationsBox.set_child_at_index(this._applicationsButtons[i].actor, pos++);
}

for (let i = 0; i < this._placesButtons.length; i++) {
this.applicationsBox.set_child_at_index(this._placesButtons[i].actor, pos++);
}

for (let i = 0; i < this._recentButtons.length; i++) {
this.applicationsBox.set_child_at_index(this._recentButtons[i].actor, pos++);
}
}

_select_category (name) {
if (name === this.lastSelectedCategory)
return;
Expand Down Expand Up @@ -2614,7 +2658,6 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
this.applicationsBox.set_width(width + 42); // The answer to life...
}


/**
* Reset the ApplicationsBox to a specific category or list of buttons.
* @param {String} category (optional) The button type or application category to be displayed.
Expand All @@ -2624,23 +2667,58 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
_displayButtons(category, buttons=[], autoCompletes=[]){
/* We only operate on SimpleMenuItems here. If any other menu item types
* are added, they should be managed independently. */
Util.each(this.applicationsBox.get_children(), c => {
let b = c._delegate;
if (!(b instanceof SimpleMenuItem))
return;

// destroy temporary buttons
if (b.type === 'transient' || b.type === 'search-provider') {
b.destroy();
return;
if (category) {
if (this.orderDirty) {
this._resetSortOrder();
this.orderDirty = false;
}

if (category) {
Util.each(this.applicationsBox.get_children(), c => {
let b = c._delegate;
if (!(b instanceof SimpleMenuItem))
return;

// destroy temporary buttons
if (b.type === 'transient' || b.type === 'search-provider') {
b.destroy();
return;
}

c.visible = b.type.includes(category) || b.type === 'app' && b.category.includes(category);
} else {
c.visible = buttons.includes(b);
});
} else {
this.orderDirty = true;

Util.each(this.applicationsBox.get_children(), c => {
let b = c._delegate;
if (!(b instanceof SimpleMenuItem))
return;

// destroy temporary buttons
if (b.type === 'transient' || b.type === 'search-provider' || b.type === 'search-result') {
b.destroy();
return;
}

c.visible = false;
});

buttons.sort((ba, bb) => {
if (ba.matchIndex < bb.matchIndex) {
return -1;
} else
if (bb.matchIndex < ba.matchIndex) {
return 1;
}

return ba.searchStrings < bb.searchStrings ? -1 : 1;
});

for (let i = 0; i < buttons.length; i++) {
this.applicationsBox.set_child_at_index(buttons[i].actor, i);
buttons[i].actor.visible = true;
}
});
}

// reset temporary button storage
this._transientButtons = [];
Expand Down Expand Up @@ -2728,50 +2806,48 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
}

_matchNames(buttons, pattern){
let res = [];
let exactMatch = null;
let ret = [];
let regexpPattern = new RegExp("\\b" + Util.escapeRegExp(pattern));

for (let i = 0; i < buttons.length; i++) {
let name = buttons[i].name;
let lowerName = name.toLowerCase();
if (lowerName.includes(pattern))
res.push(buttons[i]);
if (!exactMatch && lowerName === pattern)
exactMatch = buttons[i];
if (buttons[i].type == "recent-clear") {
continue;
}
let res = buttons[i].searchStrings[0].match(regexpPattern);
if (res) {
buttons[i].matchIndex = res.index + RECENT_PLACES_ADDER;
ret.push(buttons[i]);
} else {
buttons[i].matchIndex = NO_MATCH;
}
}
return [res, exactMatch];

return ret;
}

_listApplications(pattern){
if (!pattern)
return [[], null];
return [];

let res = [];
let exactMatch = null;
let apps = [];
let regexpPattern = new RegExp("\\b" + Util.escapeRegExp(pattern));

for (let i in this._applicationsButtons) {
let app = this._applicationsButtons[i].app;
let latinisedLowerName = Util.latinise(app.get_name().toLowerCase());
if (latinisedLowerName.match(regexpPattern) !== null) {
res.push(this._applicationsButtons[i]);
if (!exactMatch && latinisedLowerName === pattern)
exactMatch = this._applicationsButtons[i];
}
}
let button = this._applicationsButtons[i];

if (!exactMatch) {
for (let i in this._applicationsButtons) {
let app = this._applicationsButtons[i].app;
if ((Util.latinise(app.get_name().toLowerCase()).split(' ').some(word => word.startsWith(pattern))) || // match on app name
(app.get_keywords() && Util.latinise(app.get_keywords().toLowerCase()).split(';').some(keyword => keyword.startsWith(pattern))) || // match on keyword
(app.get_description() && Util.latinise(app.get_description().toLowerCase()).split(' ').some(word => word.startsWith(pattern))) || // match on description
(app.get_id() && Util.latinise(app.get_id().slice(0, -8).toLowerCase()).startsWith(pattern))) { // match on app ID
res.push(this._applicationsButtons[i]);
for (let j = 0; j < button.searchStrings.length; j++) {
let res = button.searchStrings[j].match(regexpPattern);
if (res) {
button.matchIndex = res.index + APP_MATCH_ADDERS[j];
apps.push(button);
break;
} else {
button.matchIndex = NO_MATCH;
}
}
}

return [res, exactMatch];
return apps;
}

_doSearch(rawPattern){
Expand All @@ -2784,15 +2860,13 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
this._previousTreeSelectedActor = null;
this._previousSelectedActor = null;

let [buttons, exactMatch] = this._listApplications(pattern);
let buttons = this._listApplications(pattern);

let result = this._matchNames(this._placesButtons, pattern);
buttons = buttons.concat(result[0]);
exactMatch = exactMatch || result[1];
buttons = buttons.concat(result);

result = this._matchNames(this._recentButtons, pattern);
buttons = buttons.concat(result[0]);
exactMatch = exactMatch || result[1];
buttons = buttons.concat(result);

var acResults = []; // search box autocompletion results
if (this.searchFilesystem) {
Expand All @@ -2804,7 +2878,7 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {

if (buttons.length || acResults.length) {
this.appBoxIter.reloadVisible();
let item_actor = exactMatch ? exactMatch.actor : this.appBoxIter.getFirstVisible();
let item_actor = this.appBoxIter.getFirstVisible();
this._selectedItemIndex = this.appBoxIter.getAbsoluteIndexOfChild(item_actor);
this._activeContainer = this.applicationsBox;
this._scrollToButton(item_actor._delegate);
Expand Down

0 comments on commit a6738bd

Please sign in to comment.