Skip to content

Commit

Permalink
menu applet: Rework vector box (it was interfering with scrolling
Browse files Browse the repository at this point in the history
categories).

The vector box is now angle/distance calculations to determine whether
movement from a category to apps is within the range to maintain the
current category.
  • Loading branch information
mtwebster committed Dec 15, 2020
1 parent 721174e commit 4dc8c3a
Showing 1 changed file with 175 additions and 72 deletions.
247 changes: 175 additions & 72 deletions files/usr/share/cinnamon/applets/menu@cinnamon.org/applet.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ const RECENT_PLACES_ADDER = 4000;
* want to use it.
*/

function calc_angle(x, y) {
if (x == 0 || y == 0) {
return 0;
}

let r = Math.atan2(y, x) * (180 / Math.PI);
return r;
}

class VisibleChildIterator {
constructor(container) {
this.container = container;
Expand Down Expand Up @@ -910,6 +919,8 @@ class CategoryButton extends SimpleMenuItem {
this.icon.visible = false;

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

this.actor_motion_id = this.actor.connect("motion-event", Lang.bind(applet, this.applet._categoryMotionEvent));
}
}

Expand Down Expand Up @@ -1455,11 +1466,11 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
this.closeContextMenu(false);
this._previousVisibleIndex = null;

this._disableVectorMask();
this._clearAllSelections(true);
this._scrollToButton(null, this.applicationsScrollBox);
this._scrollToButton(null, this.categoriesScrollBox);
this._scrollToButton(null, this.favoritesScrollBox);
this.destroyVectorBox();
}
}

Expand Down Expand Up @@ -2163,7 +2174,6 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
button.isHovered = true;
this._clearPrevCatSelection(button.actor);
this._select_category(button.categoryId);
this.makeVectorBox(button.actor);
} else {
this._previousVisibleIndex = parent._vis_iter.getVisibleIndex(button.actor);

Expand Down Expand Up @@ -2235,106 +2245,199 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
}
}

/* Category Box
* _____
* | /|T
* | / |
* | /__|__________pointer Y
* | |\ |
* | | \ |
* |_|__\|B
* |
* |
* |pointer X
*/

/*
* The vectorBox overlays the the categoriesBox to aid in navigation from categories to apps
* by preventing misselections. It is set to the same size as the categoriesOverlayBox and
* categoriesBox.
* The vector mask activates on any motion from a category button. At this point, all
* category buttons are made non-reactive.
*
* The actor is a quadrilateral that we turn into a triangle by setting the A and B vertices to
* the same position. The size and origin of the vectorBox are calculated in _getVectorInfo().
* Using those properties, the bounding box is sized as (w, h) and the triangle is defined as
* follows:
* _____
* | /|D
* | / | AB: (mx, my)
* | A/ | C: (w, h)
* | B\ | D: (w, 0)
* | \ |
* |____\|C
* The starting point and two corners of the category box are taken, and two angles are
* calculated to intersect with the right box corners. If a movement is within those two
* angles, the current position is made the last position and used on the next interval.
*
* In this manner the left vertex of the triangle follows the mouse and category-switching
* is disabled as long as the pointer stays in bounds.
*
* If the poll interval is made too large, category switching will become sluggish. Polling
* stops when there is no movement.
*/

_getVectorInfo() {
static DEBUG_VMASK = false;
static VECTOR_VERTEX_COLOR = Clutter.Color.from_string("white")[1];

static POLL_INTERVAL = 20;
static MIN_MOVEMENT = 2; // Movement smaller than this disables the mask.

_getNewVectorInfo() {
let [mx, my, mask] = global.get_pointer();
let [bx, by] = this.categoriesScrollBox.get_transformed_position();

// The allocation is the only thing that works here - the 'height'
// property (and natural height) are the size of the entire scrollable
// area (the inner categoriesBox), which is weird...
let alloc = this.categoriesScrollBox.get_allocation_box();
let bw = alloc.x2 - alloc.x1;
let bh = alloc.y2 - alloc.y1;

if (mx < 0 || mx > bx + bw || my < 0 || my > by + bh) {
return null;
let x_dist = bx + bw - mx;
let y_dist = my - by;

// Calculate their angle from 3 o'clock.
let top_angle = calc_angle(x_dist, y_dist);
y_dist -= bh;
let bottom_angle = calc_angle(x_dist, y_dist);

let debug_actor = null;

if (CinnamonMenuApplet.DEBUG_VMASK) {
debug_actor = new St.Polygon({
ulc_x: mx, ulc_y: my,
llc_x: mx, llc_y: my,
urc_x: bx + bw, urc_y: by,
lrc_x: bx + bw, lrc_y: by + bh,
debug: true
});

global.stage.add_actor(debug_actor);
}

return { mx: mx,
my: my,
bx: bx,
by: by,
bw: bw,
bh: bh };
return {
start_x: mx,
start_y: my,
bx: bx,
by1: by,
by2: by + bh,
bw: bw,
bh: bh,
top_angle: top_angle,
bottom_angle: bottom_angle,
debug_actor: debug_actor
};
}

makeVectorBox(actor) {
this.destroyVectorBox(actor);
let vi = this._getVectorInfo();
if (!vi)
return;
_updateVectorInfo(mx, my) {
let bx = this.vector_mask_info.bx;
let by = this.vector_mask_info.by1;
let bw = this.vector_mask_info.bw;
let bh = this.vector_mask_info.bh;

if (this.vectorBox) {
this.vectorBox.visible = true;
} else {
this.vectorBox = new St.Polygon({ debug: false, reactive: true });
global.stage.add_actor(this.vectorBox);
let x_dist = bx + bw - mx;
let y_dist = my - by;

this.vectorBox.connect("leave-event", Lang.bind(this, this.destroyVectorBox));
this.vectorBox.connect("motion-event", Lang.bind(this, this.maybeUpdateVectorBox));
}
// Calculate their angle from 3 o'clock.
let top_angle = calc_angle(x_dist, y_dist);
y_dist -= bh;

let bottom_angle = calc_angle(x_dist, y_dist);

Object.assign(this.vectorBox, { width: vi.bw, height: vi.bh,
ulc_x: vi.mx, ulc_y: vi.my,
llc_x: vi.mx, llc_y: vi.my,
urc_x: vi.bx + vi.bw, urc_y: vi.by,
lrc_x: vi.bx + vi.bw, lrc_y: vi.by + vi.bh });
// Padding moves the saved x position slightly left, this makes the mask
// more forgiving of random small movement when starting to choose an
// app button.
this.vector_mask_info.start_x = mx;
this.vector_mask_info.start_y = my;
this.vector_mask_info.top_angle = top_angle;
this.vector_mask_info.bottom_angle = bottom_angle;

this.actor_motion_id = actor.connect("motion-event", Lang.bind(this, this.maybeUpdateVectorBox));
this.current_motion_actor = actor;
if (CinnamonMenuApplet.DEBUG_VMASK) {
this.vector_mask_info.debug_actor.ulc_x = mx;
this.vector_mask_info.debug_actor.llc_x = mx;
this.vector_mask_info.debug_actor.ulc_y = my;
this.vector_mask_info.debug_actor.llc_y = my;
}
}

maybeUpdateVectorBox() {
if (this.vector_update_loop) {
Mainloop.source_remove(this.vector_update_loop);
this.vector_update_loop = 0;
_keepMaskActive() {
let ret = false;
let ignored = false;
let angle = 0;

let [mx, my, mask] = global.get_pointer();

// Check for out of range entirely.
if (mx >= this.vector_mask_info.bx + this.vector_mask_info.bw ||
my < this.vector_mask_info.by1 ||
my > this.vector_mask_info.by2) {
return false;
}
this.vector_update_loop = Mainloop.timeout_add(50, Lang.bind(this, this.updateVectorBox));

let x_dist = mx - this.vector_mask_info.start_x;
let y_dist = this.vector_mask_info.start_y - my;

if (Math.abs(Math.hypot(x_dist, y_dist)) < CinnamonMenuApplet.MIN_MOVEMENT) {
return false;;
}

angle = calc_angle(x_dist, y_dist);

ret = angle <= this.vector_mask_info.top_angle &&
angle >= this.vector_mask_info.bottom_angle;

this._updateVectorInfo(mx, my);

if (CinnamonMenuApplet.DEBUG_VMASK) {
log(`${this.vector_mask_info.top_angle} <---${angle}---> ${this.vector_mask_info.bottom_angle} - Continue? ${ret}`);
}

return ret;
}

updateVectorBox(actor) {
if (!this.current_motion_actor)
return;
let vi = this._getVectorInfo();
if (vi) {
this.vectorBox.ulc_x = vi.mx;
this.vectorBox.llc_x = vi.mx;
this.vectorBox.queue_repaint();
} else {
this.destroyVectorBox(actor);
_enableVectorMask(actor) {
this._disableVectorMask();

this.vector_mask_info = this._getNewVectorInfo(actor);

// While the mask is active, disable category buttons.
this._setCategoryButtonsReactive(false);

this.vector_update_loop = Mainloop.timeout_add(CinnamonMenuApplet.POLL_INTERVAL, Lang.bind(this, this._maskPollTimeout));
}

_maskPollTimeout() {
if (this._keepMaskActive()) {
return GLib.SOURCE_CONTINUE;
}
this.vector_update_loop = 0;
return false;

this._disableVectorMask();
return GLib.SOURCE_REMOVE;
}

destroyVectorBox(actor) {
if (!this.vectorBox)
return;
_categoryMotionEvent(actor, event) {
// Always keep the mask engaged - motion-events on the category buttons
// trigger this.

if (this.vector_update_loop == 0) {
this._enableVectorMask(actor);
}

if (this.vector_update_loop) {
return Clutter.EVENT_PROPAGATE;
}

_disableVectorMask() {
if (this.vector_update_loop > 0) {
this._setCategoryButtonsReactive(true);
Mainloop.source_remove(this.vector_update_loop);
this.vector_update_loop = 0;

if (CinnamonMenuApplet.DEBUG_VMASK) {
this.vector_mask_info.debug_actor.destroy();
}
}
}

if (this.actor_motion_id > 0 && this.current_motion_actor != null) {
this.current_motion_actor.disconnect(this.actor_motion_id);
this.actor_motion_id = 0;
this.current_motion_actor = null;
this.vectorBox.visible = false;
_setCategoryButtonsReactive(active) {
for (let i = 0; i < this._categoryButtons.length; i++) {
this._categoryButtons[i].actor.reactive = active;
}
}

Expand Down Expand Up @@ -2640,7 +2743,7 @@ class CinnamonMenuApplet extends Applet.TextIconApplet {
this._activeActor = null;
this.vectorBox = null;
this.actor_motion_id = 0;
this.vector_update_loop = null;
this.vector_update_loop = 0;
this.current_motion_actor = null;

this.menu.actor.width = this.popup_width * global.ui_scale;
Expand Down

0 comments on commit 4dc8c3a

Please sign in to comment.