551 lines
14 KiB
JavaScript
551 lines
14 KiB
JavaScript
// dragAndDrop.js
|
||
// GPLv3
|
||
|
||
const DND = imports.ui.dnd;
|
||
const AppDisplay = imports.ui.appDisplay;
|
||
const Clutter = imports.gi.Clutter;
|
||
const St = imports.gi.St;
|
||
const Main = imports.ui.main;
|
||
const Mainloop = imports.mainloop;
|
||
|
||
const ExtensionUtils = imports.misc.extensionUtils;
|
||
const Me = ExtensionUtils.getCurrentExtension();
|
||
const Convenience = Me.imports.convenience;
|
||
const Extension = Me.imports.extension;
|
||
|
||
const CHANGE_PAGE_TIMEOUT = 400;
|
||
|
||
const Gettext = imports.gettext.domain('appfolders-manager');
|
||
const _ = Gettext.gettext;
|
||
|
||
//-------------------------------------------------
|
||
|
||
var OVERLAY_MANAGER;
|
||
|
||
/* This method is called by extension.js' enable function. It does code injections
|
||
* to AppDisplay.AppIcon, connecting it to DND-related signals.
|
||
*/
|
||
function initDND () {
|
||
OVERLAY_MANAGER = new OverlayManager();
|
||
}
|
||
|
||
//--------------------------------------------------------------
|
||
|
||
/* Amazing! A singleton! It allows easy (and safer?) access to general methods,
|
||
* managing other objects: it creates/updates/deletes all overlays (for folders,
|
||
* pages, creation, removing).
|
||
*/
|
||
class OverlayManager {
|
||
constructor () {
|
||
this.addActions = [];
|
||
this.removeAction = new FolderActionArea('remove');
|
||
this.createAction = new FolderActionArea('create');
|
||
this.upAction = new NavigationArea('up');
|
||
this.downAction = new NavigationArea('down');
|
||
|
||
this.next_drag_should_recompute = true;
|
||
this.current_width = 0;
|
||
}
|
||
|
||
on_drag_begin () {
|
||
this.ensurePopdowned();
|
||
this.ensureFolderOverlayActors();
|
||
this.updateFoldersVisibility();
|
||
this.updateState(true);
|
||
}
|
||
|
||
on_drag_end () {
|
||
// force to compute new positions if a drop occurs
|
||
this.next_drag_should_recompute = true;
|
||
this.updateState(false);
|
||
}
|
||
|
||
on_drag_cancelled () {
|
||
this.updateState(false);
|
||
}
|
||
|
||
updateArrowVisibility () {
|
||
let grid = Main.overview.viewSelector.appDisplay._views[1].view._grid;
|
||
if (grid.currentPage == 0) {
|
||
this.upAction.setActive(false);
|
||
} else {
|
||
this.upAction.setActive(true);
|
||
}
|
||
if (grid.currentPage == grid._nPages -1) {
|
||
this.downAction.setActive(false);
|
||
} else {
|
||
this.downAction.setActive(true);
|
||
}
|
||
this.upAction.show();
|
||
this.downAction.show();
|
||
}
|
||
|
||
updateState (isDragging) {
|
||
if (isDragging) {
|
||
this.removeAction.show();
|
||
if (this.openedFolder == null) {
|
||
this.removeAction.setActive(false);
|
||
} else {
|
||
this.removeAction.setActive(true);
|
||
}
|
||
this.createAction.show();
|
||
this.updateArrowVisibility();
|
||
} else {
|
||
this.hideAll();
|
||
}
|
||
}
|
||
|
||
hideAll () {
|
||
this.removeAction.hide();
|
||
this.createAction.hide();
|
||
this.upAction.hide();
|
||
this.downAction.hide();
|
||
this.hideAllFolders();
|
||
}
|
||
|
||
hideAllFolders () {
|
||
for (var i = 0; i < this.addActions.length; i++) {
|
||
this.addActions[i].hide();
|
||
}
|
||
}
|
||
|
||
updateActorsPositions () {
|
||
let monitor = Main.layoutManager.primaryMonitor;
|
||
this.topOfTheGrid = Main.overview.viewSelector.actor.get_parent().get_parent().get_allocation_box().y1;
|
||
let temp = Main.overview.viewSelector.appDisplay._views[1].view.actor.get_parent();
|
||
let bottomOfTheGrid = this.topOfTheGrid + temp.get_allocation_box().y2;
|
||
|
||
let _availHeight = bottomOfTheGrid - this.topOfTheGrid;
|
||
let _availWidth = Main.overview.viewSelector.appDisplay._views[1].view._grid.actor.width;
|
||
let sideMargin = (monitor.width - _availWidth) / 2;
|
||
|
||
let xMiddle = ( monitor.x + monitor.width ) / 2;
|
||
let yMiddle = ( monitor.y + monitor.height ) / 2;
|
||
|
||
// Positions of areas
|
||
this.removeAction.setPosition( xMiddle , bottomOfTheGrid );
|
||
this.createAction.setPosition( xMiddle, Main.overview._panelGhost.height );
|
||
this.upAction.setPosition( 0, Main.overview._panelGhost.height );
|
||
this.downAction.setPosition( 0, bottomOfTheGrid );
|
||
|
||
// Sizes of areas
|
||
this.removeAction.setSize(xMiddle, monitor.height - bottomOfTheGrid);
|
||
this.createAction.setSize(xMiddle, this.topOfTheGrid - Main.overview._panelGhost.height);
|
||
this.upAction.setSize(xMiddle, this.topOfTheGrid - Main.overview._panelGhost.height);
|
||
this.downAction.setSize(xMiddle, monitor.height - bottomOfTheGrid);
|
||
|
||
this.updateArrowVisibility();
|
||
}
|
||
|
||
ensureFolderOverlayActors () {
|
||
// A folder was opened, and just closed.
|
||
if (this.openedFolder != null) {
|
||
this.updateActorsPositions();
|
||
this.computeFolderOverlayActors();
|
||
this.next_drag_should_recompute = true;
|
||
return;
|
||
}
|
||
|
||
// The grid "moved" or the whole shit needs forced updating
|
||
let allAppsGrid = Main.overview.viewSelector.appDisplay._views[1].view._grid;
|
||
let new_width = allAppsGrid.actor.allocation.get_width();
|
||
if (new_width != this.current_width || this.next_drag_should_recompute) {
|
||
this.next_drag_should_recompute = false;
|
||
this.updateActorsPositions();
|
||
this.computeFolderOverlayActors();
|
||
}
|
||
}
|
||
|
||
computeFolderOverlayActors () {
|
||
let monitor = Main.layoutManager.primaryMonitor;
|
||
let xMiddle = ( monitor.x + monitor.width ) / 2;
|
||
let yMiddle = ( monitor.y + monitor.height ) / 2;
|
||
let allAppsGrid = Main.overview.viewSelector.appDisplay._views[1].view._grid;
|
||
|
||
let nItems = 0;
|
||
let indexes = [];
|
||
let folders = [];
|
||
let x, y;
|
||
|
||
Main.overview.viewSelector.appDisplay._views[1].view._allItems.forEach(function(icon) {
|
||
if (icon.actor.visible) {
|
||
if (icon instanceof AppDisplay.FolderIcon) {
|
||
indexes.push(nItems);
|
||
folders.push(icon);
|
||
}
|
||
nItems++;
|
||
}
|
||
});
|
||
|
||
this.current_width = allAppsGrid.actor.allocation.get_width();
|
||
let x_correction = (monitor.width - this.current_width)/2;
|
||
let availHeightPerPage = (allAppsGrid.actor.height)/(allAppsGrid._nPages);
|
||
|
||
for (var i = 0; i < this.addActions.length; i++) {
|
||
this.addActions[i].actor.destroy();
|
||
}
|
||
|
||
for (var i = 0; i < indexes.length; i++) {
|
||
let inPageIndex = indexes[i] % allAppsGrid._childrenPerPage;
|
||
let page = Math.floor(indexes[i] / allAppsGrid._childrenPerPage);
|
||
x = folders[i].actor.get_allocation_box().x1;
|
||
y = folders[i].actor.get_allocation_box().y1;
|
||
|
||
// Invalid coords (example: when dragging out of the folder) should
|
||
// not produce a visible overlay, a negative page number is an easy
|
||
// way to be sure it stays hidden.
|
||
if (x == 0) {
|
||
page = -1;
|
||
}
|
||
x = Math.floor(x + x_correction);
|
||
y = y + this.topOfTheGrid;
|
||
y = y - (page * availHeightPerPage);
|
||
|
||
this.addActions[i] = new FolderArea(folders[i].id, x, y, page);
|
||
}
|
||
}
|
||
|
||
updateFoldersVisibility () {
|
||
let appView = Main.overview.viewSelector.appDisplay._views[1].view;
|
||
for (var i = 0; i < this.addActions.length; i++) {
|
||
if ((this.addActions[i].page == appView._grid.currentPage) && (!appView._currentPopup)) {
|
||
this.addActions[i].show();
|
||
} else {
|
||
this.addActions[i].hide();
|
||
}
|
||
}
|
||
}
|
||
|
||
ensurePopdowned () {
|
||
let appView = Main.overview.viewSelector.appDisplay._views[1].view;
|
||
if (appView._currentPopup) {
|
||
this.openedFolder = appView._currentPopup._source.id;
|
||
appView._currentPopup.popdown();
|
||
} else {
|
||
this.openedFolder = null;
|
||
}
|
||
}
|
||
|
||
goToPage (nb) {
|
||
Main.overview.viewSelector.appDisplay._views[1].view.goToPage( nb );
|
||
this.updateArrowVisibility();
|
||
this.hideAllFolders();
|
||
this.updateFoldersVisibility(); //load folders of the new page
|
||
}
|
||
|
||
destroy () {
|
||
for (let i = 0; i > this.addActions.length; i++) {
|
||
this.addActions[i].destroy();
|
||
}
|
||
this.removeAction.destroy();
|
||
this.createAction.destroy();
|
||
this.upAction.destroy();
|
||
this.downAction.destroy();
|
||
//log('OverlayManager destroyed');
|
||
}
|
||
};
|
||
|
||
//-------------------------------------------------------
|
||
|
||
// Abstract overlay with very generic methods
|
||
class DroppableArea {
|
||
|
||
constructor (id) {
|
||
this.id = id;
|
||
this.styleClass = 'folderArea';
|
||
|
||
this.actor = new St.BoxLayout ({
|
||
width: 10,
|
||
height: 10,
|
||
visible: false,
|
||
});
|
||
this.actor._delegate = this;
|
||
|
||
this.lock = true;
|
||
this.use_frame = Convenience.getSettings('org.gnome.shell.extensions.appfolders-manager').get_boolean('debug');
|
||
}
|
||
|
||
setPosition (x, y) {
|
||
let monitor = Main.layoutManager.primaryMonitor;
|
||
this.actor.set_position(monitor.x + x, monitor.y + y);
|
||
}
|
||
|
||
setSize (w, h) {
|
||
this.actor.width = w;
|
||
this.actor.height = h;
|
||
}
|
||
|
||
hide () {
|
||
this.actor.visible = false;
|
||
this.lock = true;
|
||
}
|
||
|
||
show () {
|
||
this.actor.visible = true;
|
||
}
|
||
|
||
setActive (active) {
|
||
this._active = active;
|
||
if (this._active) {
|
||
this.actor.style_class = this.styleClass;
|
||
} else {
|
||
this.actor.style_class = 'insensitiveArea';
|
||
}
|
||
}
|
||
|
||
destroy () {
|
||
this.actor.destroy();
|
||
}
|
||
}
|
||
|
||
/* Overlay representing an "action". Actions can be creating a folder, or
|
||
* removing an app from a folder. These areas accept drop, and display a label.
|
||
*/
|
||
class FolderActionArea extends DroppableArea {
|
||
constructor (id) {
|
||
super(id);
|
||
|
||
let x, y, label;
|
||
|
||
switch (this.id) {
|
||
case 'create':
|
||
label = _("Create a new folder");
|
||
this.styleClass = 'shadowedAreaTop';
|
||
break;
|
||
case 'remove':
|
||
label = '';
|
||
this.styleClass = 'shadowedAreaBottom';
|
||
break;
|
||
default:
|
||
label = 'invalid id';
|
||
break;
|
||
}
|
||
if (this.use_frame) {
|
||
this.styleClass = 'framedArea';
|
||
}
|
||
this.actor.style_class = this.styleClass;
|
||
|
||
this.label = new St.Label({
|
||
text: label,
|
||
style_class: 'dropAreaLabel',
|
||
x_expand: true,
|
||
y_expand: true,
|
||
x_align: Clutter.ActorAlign.CENTER,
|
||
y_align: Clutter.ActorAlign.CENTER,
|
||
});
|
||
this.actor.add(this.label);
|
||
|
||
this.setPosition(10, 10);
|
||
Main.layoutManager.overviewGroup.add_actor(this.actor);
|
||
}
|
||
|
||
getRemoveLabel () {
|
||
let label;
|
||
if (OVERLAY_MANAGER.openedFolder == null) {
|
||
label = '…';
|
||
} else {
|
||
let folder_schema = Extension.folderSchema (OVERLAY_MANAGER.openedFolder);
|
||
label = folder_schema.get_string('name');
|
||
}
|
||
return (_("Remove from %s")).replace('%s', label);
|
||
}
|
||
|
||
setActive (active) {
|
||
super.setActive(active);
|
||
if (this.id == 'remove') {
|
||
this.label.text = this.getRemoveLabel();
|
||
}
|
||
}
|
||
|
||
handleDragOver (source, actor, x, y, time) {
|
||
if (source instanceof AppDisplay.AppIcon && this._active) {
|
||
return DND.DragMotionResult.MOVE_DROP;
|
||
}
|
||
Main.overview.endItemDrag(this);
|
||
return DND.DragMotionResult.NO_DROP;
|
||
}
|
||
|
||
acceptDrop (source, actor, x, y, time) {
|
||
if ((source instanceof AppDisplay.AppIcon) && (this.id == 'create')) {
|
||
Extension.createNewFolder(source);
|
||
Main.overview.endItemDrag(this);
|
||
return true;
|
||
}
|
||
if ((source instanceof AppDisplay.AppIcon) && (this.id == 'remove')) {
|
||
this.removeApp(source);
|
||
Main.overview.endItemDrag(this);
|
||
return true;
|
||
}
|
||
Main.overview.endItemDrag(this);
|
||
return false;
|
||
}
|
||
|
||
removeApp (source) {
|
||
let id = source.app.get_id();
|
||
Extension.removeFromFolder(id, OVERLAY_MANAGER.openedFolder);
|
||
OVERLAY_MANAGER.updateState(false);
|
||
Main.overview.viewSelector.appDisplay._views[1].view._redisplay();
|
||
}
|
||
|
||
destroy () {
|
||
this.label.destroy();
|
||
super.destroy();
|
||
}
|
||
};
|
||
|
||
/* Overlay reacting to hover, but isn't droppable. The goal is to go to an other
|
||
* page of the grid while dragging an app.
|
||
*/
|
||
class NavigationArea extends DroppableArea {
|
||
constructor (id) {
|
||
super(id);
|
||
|
||
let x, y, i;
|
||
switch (this.id) {
|
||
case 'up':
|
||
i = 'pan-up-symbolic';
|
||
this.styleClass = 'shadowedAreaTop';
|
||
break;
|
||
case 'down':
|
||
i = 'pan-down-symbolic';
|
||
this.styleClass = 'shadowedAreaBottom';
|
||
break;
|
||
default:
|
||
i = 'dialog-error-symbolic';
|
||
break;
|
||
}
|
||
if (this.use_frame) {
|
||
this.styleClass = 'framedArea';
|
||
}
|
||
this.actor.style_class = this.styleClass;
|
||
|
||
this.actor.add(new St.Icon({
|
||
icon_name: i,
|
||
icon_size: 24,
|
||
style_class: 'system-status-icon',
|
||
x_expand: true,
|
||
y_expand: true,
|
||
x_align: Clutter.ActorAlign.CENTER,
|
||
y_align: Clutter.ActorAlign.CENTER,
|
||
}));
|
||
|
||
this.setPosition(x, y);
|
||
Main.layoutManager.overviewGroup.add_actor(this.actor);
|
||
}
|
||
|
||
handleDragOver (source, actor, x, y, time) {
|
||
if (this.id == 'up' && this._active) {
|
||
this.pageUp();
|
||
return DND.DragMotionResult.CONTINUE;
|
||
}
|
||
|
||
if (this.id == 'down' && this._active) {
|
||
this.pageDown();
|
||
return DND.DragMotionResult.CONTINUE;
|
||
}
|
||
|
||
Main.overview.endItemDrag(this);
|
||
return DND.DragMotionResult.NO_DROP;
|
||
}
|
||
|
||
pageUp () {
|
||
if(this.lock && !this.timeoutSet) {
|
||
this._timeoutId = Mainloop.timeout_add(CHANGE_PAGE_TIMEOUT, this.unlock.bind(this));
|
||
this.timeoutSet = true;
|
||
}
|
||
if(!this.lock) {
|
||
let currentPage = Main.overview.viewSelector.appDisplay._views[1].view._grid.currentPage;
|
||
this.lock = true;
|
||
OVERLAY_MANAGER.goToPage(currentPage - 1);
|
||
}
|
||
}
|
||
|
||
pageDown () {
|
||
if(this.lock && !this.timeoutSet) {
|
||
this._timeoutId = Mainloop.timeout_add(CHANGE_PAGE_TIMEOUT, this.unlock.bind(this));
|
||
this.timeoutSet = true;
|
||
}
|
||
if(!this.lock) {
|
||
let currentPage = Main.overview.viewSelector.appDisplay._views[1].view._grid.currentPage;
|
||
this.lock = true;
|
||
OVERLAY_MANAGER.goToPage(currentPage + 1);
|
||
}
|
||
}
|
||
|
||
acceptDrop (source, actor, x, y, time) {
|
||
Main.overview.endItemDrag(this);
|
||
return false;
|
||
}
|
||
|
||
unlock () {
|
||
this.lock = false;
|
||
this.timeoutSet = false;
|
||
Mainloop.source_remove(this._timeoutId);
|
||
}
|
||
};
|
||
|
||
/* This overlay is the area upon a folder. Position and visibility of the actor
|
||
* is handled by exterior functions.
|
||
* "this.id" is the folder's id, a string, as written in the gsettings key.
|
||
* Dropping an app on this folder will add it to the folder
|
||
*/
|
||
class FolderArea extends DroppableArea {
|
||
constructor (id, asked_x, asked_y, page) {
|
||
super(id);
|
||
this.page = page;
|
||
|
||
let grid = Main.overview.viewSelector.appDisplay._views[1].view._grid;
|
||
this.actor.width = grid._getHItemSize();
|
||
this.actor.height = grid._getVItemSize();
|
||
|
||
if (this.use_frame) {
|
||
this.styleClass = 'framedArea';
|
||
this.actor.add(new St.Label({
|
||
text: this.id,
|
||
x_expand: true,
|
||
y_expand: true,
|
||
x_align: Clutter.ActorAlign.CENTER,
|
||
y_align: Clutter.ActorAlign.CENTER,
|
||
}));
|
||
} else {
|
||
this.styleClass = 'folderArea';
|
||
this.actor.add(new St.Icon({
|
||
icon_name: 'list-add-symbolic',
|
||
icon_size: 24,
|
||
style_class: 'system-status-icon',
|
||
x_expand: true,
|
||
y_expand: true,
|
||
x_align: Clutter.ActorAlign.CENTER,
|
||
y_align: Clutter.ActorAlign.CENTER,
|
||
}));
|
||
}
|
||
if (this.use_frame) {
|
||
this.styleClass = 'framedArea';
|
||
}
|
||
this.actor.style_class = this.styleClass;
|
||
|
||
this.setPosition(asked_x, asked_y);
|
||
Main.layoutManager.overviewGroup.add_actor(this.actor);
|
||
}
|
||
|
||
handleDragOver (source, actor, x, y, time) {
|
||
if (source instanceof AppDisplay.AppIcon) {
|
||
return DND.DragMotionResult.MOVE_DROP;
|
||
}
|
||
Main.overview.endItemDrag(this);
|
||
return DND.DragMotionResult.NO_DROP;
|
||
}
|
||
|
||
acceptDrop (source, actor, x, y, time) { //FIXME recharger la vue ou au minimum les miniatures des dossiers
|
||
if ((source instanceof AppDisplay.AppIcon) &&
|
||
!Extension.isInFolder(source.id, this.id)) {
|
||
Extension.addToFolder(source, this.id);
|
||
Main.overview.endItemDrag(this);
|
||
return true;
|
||
}
|
||
Main.overview.endItemDrag(this);
|
||
return false;
|
||
}
|
||
};
|
||
|