add dnd interactions

This commit is contained in:
Tom Beckmann 2014-04-29 21:35:49 +02:00
parent 84355bbdca
commit 2977609308
11 changed files with 947 additions and 472 deletions

View File

@ -38,11 +38,22 @@ namespace Gala
public interface WindowManager : Meta.Plugin
{
/**
* Window stacking changed. You can read the plugin's stacking_order property
* to restack window clones.
*/
public signal void windows_restacked ();
public abstract Clutter.Actor ui_group { get; protected set; }
public abstract Clutter.Stage stage { get; protected set; }
public abstract Clutter.Actor window_group { get; protected set; }
public abstract Clutter.Actor top_window_group { get; protected set; }
public abstract Meta.BackgroundGroup background_group { get; protected set; }
/*
* Provides a hashtable of windows' stable sequences and their stacking order
* counted from 0 up.
*/
public abstract HashTable<int,int> window_stacking_order { get; protected set; }
public abstract void begin_modal ();
public abstract void end_modal ();

View File

@ -47,7 +47,6 @@ namespace Gala
}
destroy_all_children ();
print ("DESTROYING!!!! \n");
var settings = BackgroundSettings.get_default ().schema;

View File

@ -25,19 +25,15 @@ namespace Gala
public class DragDropAction : Clutter.Action
{
public DragDropActionType drag_type { get; construct; }
public string drag_id { get; construct; }
public Clutter.Actor handle { get; private set; }
Clutter.Actor? hovered = null;
bool clicked = false;
bool dragging = false;
/**
* A drag has been started. You have to connect to this signal
* @return A ClutterActor that serves as handle
* A drag has been started. You have to connect to this signal and
* return an actor that is transformed during the drag operation.
*
* @param x The global x coordinate where the action was activated
* @param y The global y coordinate where the action was activated
* @return A ClutterActor that serves as handle
*/
public signal Clutter.Actor drag_begin ();
public signal Clutter.Actor drag_begin (float x, float y);
/**
* A drag has been canceled. You may want to consider cleaning up
@ -59,13 +55,55 @@ namespace Gala
*/
public signal void crossed (bool hovered);
/**
* Emitted on the source when a destination is crossed.
*
* @param destination The destination actor that has been crossed
* @param hovered Whether the actor is now hovered or has just been left
*/
public signal void destination_crossed (Clutter.Actor destination, bool hovered);
/**
* The source has been clicked, but the movement was not larger than
* the drag threshold. Useful if the source is also activable.
*/
public signal void actor_clicked ();
/**
* The type of the action
*/
public DragDropActionType drag_type { get; construct; }
/**
* The unique id given to this drag-drop-group
*/
public string drag_id { get; construct; }
public Clutter.Actor handle { get; private set; }
/**
* Indicates whether a drag action is currently active
*/
public bool dragging { get; private set; default = false; }
/**
* Allow checking the parents of reactive children if they are valid destinations
* if the child is none
*/
public bool allow_bubbling { get; set; default = true; }
Clutter.Actor? hovered = null;
bool clicked = false;
bool actor_was_reactive = false;
float last_x;
float last_y;
/**
* Create a new DragDropAction
*
* @param type The type of this actor
* @param id An ID that marks which sources can be dragged on
* which destinations. It has to be the same for all actors that
* should be compatible with each other.
* @param id An ID that marks which sources can be dragged on
* which destinations. It has to be the same for all actors that
* should be compatible with each other.
*/
public DragDropAction (DragDropActionType type, string id)
{
@ -95,7 +133,6 @@ namespace Gala
{
if (drag_type == DragDropActionType.SOURCE) {
actor.button_press_event.disconnect (source_clicked);
actor.motion_event.disconnect (source_motion);
}
}
@ -103,38 +140,72 @@ namespace Gala
{
if (drag_type == DragDropActionType.SOURCE) {
actor.button_press_event.connect (source_clicked);
actor.motion_event.connect (source_motion);
}
}
void emit_crossed (Clutter.Actor destination, bool hovered)
{
get_drag_drop_action (destination).crossed (hovered);
destination_crossed (destination, hovered);
}
bool source_clicked (Clutter.ButtonEvent event)
{
if (event.button != 1)
return false;
clicked = true;
return true;
}
bool source_motion (Clutter.MotionEvent event)
{
if (!clicked)
return false;
handle = drag_begin ();
if (handle == null) {
critical ("No handle has been returned by the started signal, aborting drag.");
return false;
}
dragging = true;
clicked = false;
actor.get_stage ().captured_event.connect (follow_move);
clicked = true;
last_x = event.x;
last_y = event.y;
return true;
}
bool follow_move (Clutter.Event event)
{
// still determining if we actually want to start a drag action
if (!dragging) {
switch (event.get_type ()) {
case Clutter.EventType.MOTION:
float x, y;
event.get_coords (out x, out y);
var drag_threshold = Clutter.Settings.get_default ().dnd_drag_threshold;
if (Math.fabsf (last_x - x) > drag_threshold || Math.fabsf (last_y - y) > drag_threshold) {
handle = drag_begin (x, y);
if (handle == null) {
actor.get_stage ().captured_event.disconnect (follow_move);
critical ("No handle has been returned by the started signal, aborting drag.");
return false;
}
actor_was_reactive = handle.reactive;
handle.reactive = false;
clicked = false;
dragging = true;
}
return true;
case Clutter.EventType.BUTTON_RELEASE:
float x, y, ex, ey;
event.get_coords (out ex, out ey);
actor.get_transformed_position (out x, out y);
// release has happened within bounds of actor
if (x < ex && x + actor.width > ex && y < ey && y + actor.height > ey) {
actor_clicked ();
}
actor.get_stage ().captured_event.disconnect (follow_move);
clicked = false;
dragging = false;
return true;
default:
return true;
}
}
switch (event.get_type ()) {
case Clutter.EventType.KEY_PRESS:
if (event.get_key_code () == Clutter.Key.Escape) {
@ -144,25 +215,44 @@ namespace Gala
case Clutter.EventType.MOTION:
float x, y;
event.get_coords (out x, out y);
handle.x = x;
handle.y = y;
handle.x -= last_x - x;
handle.y -= last_y - y;
last_x = x;
last_y = y;
var stage = actor.get_stage ();
var actor = actor.get_stage ().get_actor_at_pos (Clutter.PickMode.REACTIVE, (int)x, (int)y);
DragDropAction action = null;
if (actor == null || (action = get_drag_drop_action (actor)) == null) {
// if we're allowed to bubble and we this actor is not a destination, check its parents
if (actor != null && (action = get_drag_drop_action (actor)) == null && allow_bubbling) {
while ((actor = actor.get_parent ()) != stage) {
if ((action = get_drag_drop_action (actor)) != null)
break;
}
}
// didn't change, no need to do anything
if (actor == hovered)
return true;
if (action == null) {
// apparently we left ours if we had one before
if (hovered != null) {
get_drag_drop_action (hovered).crossed (false);
emit_crossed (hovered, false);
hovered = null;
}
return true;
}
// signal the previous one that we left it
if (hovered != null) {
get_drag_drop_action (hovered).crossed (false);
emit_crossed (hovered, false);
}
// tell the new one that it is hovered
hovered = actor;
action.crossed (true);
emit_crossed (hovered, true);
return true;
case Clutter.EventType.BUTTON_RELEASE:
@ -212,16 +302,22 @@ namespace Gala
actor.get_stage ().captured_event.disconnect (follow_move);
}
drag_canceled ();
dragging = false;
actor.reactive = actor_was_reactive;
drag_canceled ();
}
void finish ()
{
// make sure they reset the style or whatever they changed when hovered
get_drag_drop_action (hovered).crossed (false);
emit_crossed (hovered, false);
actor.get_stage ().captured_event.disconnect (follow_move);
dragging = false;
actor.reactive = actor_was_reactive;
drag_end (hovered);
}
}

View File

@ -106,5 +106,390 @@ namespace Gala
var xregion = X.Fixes.create_region (display.get_xdisplay (), rects);
Util.set_stage_input_region (screen, xregion);
}
/**
* Code ported from KWin present windows effect
* https://projects.kde.org/projects/kde/kde-workspace/repository/revisions/master/entry/kwin/effects/presentwindows/presentwindows.cpp
**/
// constants, mainly for natural expo
static const int GAPS = 10;
static const int MAX_TRANSLATIONS = 100000;
static const int ACCURACY = 20;
// some math utilities
static int squared_distance (Gdk.Point a, Gdk.Point b)
{
var k1 = b.x - a.x;
var k2 = b.y - a.y;
return k1*k1 + k2*k2;
}
static bool rect_is_overlapping_any (Meta.Rectangle rect, Meta.Rectangle[] rects, Meta.Rectangle border)
{
if (!border.contains_rect (rect))
return true;
foreach (var comp in rects) {
if (comp == rect)
continue;
if (rect.overlap (comp))
return true;
}
return false;
}
static Meta.Rectangle rect_adjusted (Meta.Rectangle rect, int dx1, int dy1, int dx2, int dy2)
{
return {rect.x + dx1, rect.y + dy1, rect.width + (-dx1 + dx2), rect.height + (-dy1 + dy2)};
}
static Gdk.Point rect_center (Meta.Rectangle rect)
{
return {rect.x + rect.width / 2, rect.y + rect.height / 2};
}
public struct TilableWindow
{
Meta.Rectangle rect;
void *id;
}
public static List<TilableWindow?> calculate_grid_placement (Meta.Rectangle area,
List<TilableWindow?> windows)
{
uint window_count = windows.length ();
int columns = (int)Math.ceil (Math.sqrt (window_count));
int rows = (int)Math.ceil (window_count / (double)columns);
// Assign slots
int slot_width = area.width / columns;
int slot_height = area.height / rows;
TilableWindow?[] taken_slots = {};
taken_slots.resize (rows * columns);
// precalculate all slot centers
Gdk.Point[] slot_centers = {};
slot_centers.resize (rows * columns);
for (int x = 0; x < columns; x++) {
for (int y = 0; y < rows; y++) {
slot_centers[x + y * columns] = {area.x + slot_width * x + slot_width / 2,
area.y + slot_height * y + slot_height / 2};
}
}
// Assign each window to the closest available slot
var tmplist = windows.copy ();
while (tmplist.length () > 0) {
unowned List<TilableWindow?> link = tmplist.nth (0);
var window = link.data;
var rect = window.rect;
var slot_candidate = -1;
var slot_candidate_distance = int.MAX;
var pos = rect_center (rect);
// all slots
for (int i = 0; i < columns * rows; i++) {
if (i > window_count - 1)
break;
var dist = squared_distance (pos, slot_centers[i]);
if (dist < slot_candidate_distance) {
// window is interested in this slot
var occupier = taken_slots[i];
if (occupier == window)
continue;
if (occupier == null || dist < squared_distance (rect_center (occupier.rect), slot_centers[i])) {
// either nobody lives here, or we're better - takeover the slot if it's our best
slot_candidate = i;
slot_candidate_distance = dist;
}
}
}
if (slot_candidate == -1)
continue;
if (taken_slots[slot_candidate] != null)
tmplist.prepend (taken_slots[slot_candidate]);
tmplist.remove_link (link);
taken_slots[slot_candidate] = window;
}
var result = new List<TilableWindow?> ();
// see how many windows we have on the last row
int left_over = (int)window_count - columns * (rows - 1);
for (int slot = 0; slot < columns * rows; slot++) {
var window = taken_slots[slot];
// some slots might be empty
if (window == null)
continue;
var rect = window.rect;
// Work out where the slot is
Meta.Rectangle target = {area.x + (slot % columns) * slot_width,
area.y + (slot / columns) * slot_height,
slot_width,
slot_height};
target = rect_adjusted (target, 10, 10, -10, -10);
float scale;
if (target.width / (double)rect.width < target.height / (double)rect.height) {
// Center vertically
scale = target.width / (float)rect.width;
target.y += (target.height - (int)(rect.height * scale)) / 2;
target.height = (int)Math.floorf (rect.height * scale);
} else {
// Center horizontally
scale = target.height / (float)rect.height;
target.x += (target.width - (int)(rect.width * scale)) / 2;
target.width = (int)Math.floorf (rect.width * scale);
}
// Don't scale the windows too much
if (scale > 1.0) {
scale = 1.0f;
target = {rect_center (target).x - (int)Math.floorf (rect.width * scale) / 2,
rect_center (target).y - (int)Math.floorf (rect.height * scale) / 2,
(int)Math.floorf (scale * rect.width),
(int)Math.floorf (scale * rect.height)};
}
// put the last row in the center, if necessary
if (left_over != columns && slot >= columns * (rows - 1))
target.x += (columns - left_over) * slot_width / 2;
result.prepend ({ target, window.id });
}
result.reverse ();
return result;
}
/*public List<Meta.Rectangle?> natural_placement (Meta.Rectangle area, List<Meta.Rectangle?> windows)
{
Meta.Rectangle bounds = {area.x, area.y, area.width, area.height};
var window_count = windows.length ();
var direction = 0;
int[] directions = new int[window_count];
Meta.Rectangle[] rects = new Meta.Rectangle[window_count];
for (int i = 0; i < window_count; i++) {
// save rectangles into 4-dimensional arrays representing two corners of the rectangular: [left_x, top_y, right_x, bottom_y]
var rect = clones.nth_data (i);
rect = rect_adjusted(rect, -GAPS, -GAPS, GAPS, GAPS);
rects[i] = rect;
bounds = bounds.union (rect);
// This is used when the window is on the edge of the screen to try to use as much screen real estate as possible.
directions[i] = direction;
direction++;
if (direction == 4)
direction = 0;
}
var loop_counter = 0;
var overlap = false;
do {
overlap = false;
for (var i = 0; i < rects.length; i++) {
for (var j = 0; j < rects.length; j++) {
if (i == j)
continue;
var rect = rects[i];
var comp = rects[j];
if (!rect.overlap (comp))
continue;
loop_counter ++;
overlap = true;
// Determine pushing direction
Gdk.Point i_center = rect_center (rect);
Gdk.Point j_center = rect_center (comp);
Gdk.Point diff = {j_center.x - i_center.x, j_center.y - i_center.y};
// Prevent dividing by zero and non-movement
if (diff.x == 0 && diff.y == 0)
diff.x = 1;
// Approximate a vector of between 10px and 20px in magnitude in the same direction
var length = Math.sqrtf (diff.x * diff.x + diff.y * diff.y);
diff.x = (int)Math.floorf (diff.x * ACCURACY / length);
diff.y = (int)Math.floorf (diff.y * ACCURACY / length);
// Move both windows apart
rect.x += -diff.x;
rect.y += -diff.y;
comp.x += diff.x;
comp.y += diff.y;
// Try to keep the bounding rect the same aspect as the screen so that more
// screen real estate is utilised. We do this by splitting the screen into nine
// equal sections, if the window center is in any of the corner sections pull the
// window towards the outer corner. If it is in any of the other edge sections
// alternate between each corner on that edge. We don't want to determine it
// randomly as it will not produce consistant locations when using the filter.
// Only move one window so we don't cause large amounts of unnecessary zooming
// in some situations. We need to do this even when expanding later just in case
// all windows are the same size.
// (We are using an old bounding rect for this, hopefully it doesn't matter)
var x_section = (int)Math.roundf ((rect.x - bounds.x) / (bounds.width / 3.0f));
var y_section = (int)Math.roundf ((comp.y - bounds.y) / (bounds.height / 3.0f));
i_center = rect_center (rect);
diff.x = 0;
diff.y = 0;
if (x_section != 1 || y_section != 1) { // Remove this if you want the center to pull as well
if (x_section == 1)
x_section = (directions[i] / 2 == 1 ? 2 : 0);
if (y_section == 1)
y_section = (directions[i] % 2 == 1 ? 2 : 0);
}
if (x_section == 0 && y_section == 0) {
diff.x = bounds.x - i_center.x;
diff.y = bounds.y - i_center.y;
}
if (x_section == 2 && y_section == 0) {
diff.x = bounds.x + bounds.width - i_center.x;
diff.y = bounds.y - i_center.y;
}
if (x_section == 2 && y_section == 2) {
diff.x = bounds.x + bounds.width - i_center.x;
diff.y = bounds.y + bounds.height - i_center.y;
}
if (x_section == 0 && y_section == 2) {
diff.x = bounds.x - i_center.x;
diff.y = bounds.y + bounds.height - i_center.y;
}
if (diff.x != 0 || diff.y != 0) {
length = Math.sqrtf (diff.x * diff.x + diff.y * diff.y);
diff.x *= (int)Math.floorf (ACCURACY / length / 2.0f);
diff.y *= (int)Math.floorf (ACCURACY / length / 2.0f);
rect.x += diff.x;
rect.y += diff.y;
}
// Update bounding rect
bounds = bounds.union(rect);
bounds = bounds.union(comp);
//we took copies from the rects from our list so we need to reassign them
rects[i] = rect;
rects[j] = comp;
}
}
} while (overlap && loop_counter < MAX_TRANSLATIONS);
// Work out scaling by getting the most top-left and most bottom-right window coords.
float scale = Math.fminf (Math.fminf (area.width / (float)bounds.width, area.height / (float)bounds.height), 1.0f);
// Make bounding rect fill the screen size for later steps
bounds.x = (int)Math.floorf (bounds.x - (area.width - bounds.width * scale) / 2);
bounds.y = (int)Math.floorf (bounds.y - (area.height - bounds.height * scale) / 2);
bounds.width = (int)Math.floorf (area.width / scale);
bounds.height = (int)Math.floorf (area.height / scale);
// Move all windows back onto the screen and set their scale
var index = 0;
foreach (var rect in rects) {
rect = {(int)Math.floorf ((rect.x - bounds.x) * scale + area.x),
(int)Math.floorf ((rect.y - bounds.y) * scale + area.y),
(int)Math.floorf (rect.width * scale),
(int)Math.floorf (rect.height * scale)};
rects[index] = rect;
index++;
}
// fill gaps by enlarging windows
bool moved = false;
Meta.Rectangle border = area;
do {
moved = false;
index = 0;
foreach (var rect in rects) {
int width_diff = ACCURACY;
int height_diff = (int)Math.floorf ((((rect.width + width_diff) - rect.height) /
(float)rect.width) * rect.height);
int x_diff = width_diff / 2;
int y_diff = height_diff / 2;
//top right
Meta.Rectangle old = rect;
rect = {rect.x + x_diff, rect.y - y_diff - height_diff, rect.width + width_diff, rect.height + width_diff};
if (rect_is_overlapping_any (rect, rects, border))
rect = old;
else
moved = true;
//bottom right
old = rect;
rect = {rect.x + x_diff, rect.y + y_diff, rect.width + width_diff, rect.height + width_diff};
if (rect_is_overlapping_any (rect, rects, border))
rect = old;
else
moved = true;
//bottom left
old = rect;
rect = {rect.x - x_diff, rect.y + y_diff, rect.width + width_diff, rect.height + width_diff};
if (rect_is_overlapping_any (rect, rects, border))
rect = old;
else
moved = true;
//top left
old = rect;
rect = {rect.x - x_diff, rect.y - y_diff - height_diff, rect.width + width_diff, rect.height + width_diff};
if (rect_is_overlapping_any (rect, rects, border))
rect = old;
else
moved = true;
rects[index] = rect;
index++;
}
} while (moved);
var result = new List<Meta.Rectangle?> ();
index = 0;
foreach (var rect in rects) {
var window_rect = clones.nth_data (index);
rect = rect_adjusted (rect, GAPS, GAPS, -GAPS, -GAPS);
scale = rect.width / (float)window_rect.width;
if (scale > 2.0 || (scale > 1.0 && (window_rect.width > 300 || window_rect.height > 300))) {
scale = (window_rect.width > 300 || window_rect.height > 300) ? 1.0f : 2.0f;
rect = {rect_center (rect).x - (int)Math.floorf (window_rect.width * scale) / 2,
rect_center (rect).y - (int)Math.floorf (window_rect.height * scale) / 2,
(int)Math.floorf (window_rect.width * scale),
(int)Math.floorf (window_rect.height * scale)};
}
result.prepend (rect);
index++;
}
result.reverse ();
return result;
}*/
}
}

View File

@ -54,6 +54,7 @@ gala_VALASOURCES = \
Widgets/WorkspaceView.vala \
Widgets/MultitaskingView/IconGroup.vala \
Widgets/MultitaskingView/MultitaskingView.vala \
Widgets/MultitaskingView/TiledWindowContainer.vala \
Widgets/MultitaskingView/Workspace.vala \
$(NULL)

View File

@ -4,8 +4,6 @@ namespace Gala
{
public class IconGroup : Actor
{
List<Meta.Window> windows;
const int SIZE = 64;
static const int PLUS_SIZE = 8;
@ -13,8 +11,14 @@ namespace Gala
public signal void selected ();
public IconGroup ()
public Meta.Workspace workspace { get; construct; }
List<Meta.Window> windows;
public IconGroup (Meta.Workspace workspace)
{
Object (workspace: workspace);
clear ();
width = SIZE;
@ -47,6 +51,12 @@ namespace Gala
redraw ();
}
public void remove_window (Meta.Window window)
{
windows.remove (window);
redraw ();
}
public void redraw ()
{
content.invalidate ();

View File

@ -57,7 +57,7 @@ namespace Gala
{
float x = 0;
WorkspaceClone? active = null;
var active_index = screen.get_active_workspace ().index ();;
var active_index = screen.get_active_workspace ().index ();
foreach (var child in workspaces.get_children ()) {
var workspace_clone = child as WorkspaceClone;
@ -80,7 +80,7 @@ namespace Gala
void add_workspace (int num)
{
var workspace = new WorkspaceClone (screen.get_workspace_by_index (num));
var workspace = new WorkspaceClone (screen.get_workspace_by_index (num), wm);
workspace.window_selected.connect (window_selected);
workspace.selected.connect (activate_workspace);

View File

@ -0,0 +1,296 @@
using Clutter;
using Meta;
namespace Gala
{
public class TiledWindow : Actor
{
public signal void selected ();
public signal void request_reposition ();
public Meta.Window window { get; construct; }
DragDropAction drag_action;
Clone? clone = null;
Actor prev_parent = null;
int prev_index = -1;
public TiledWindow (Meta.Window window)
{
Object (window: window);
reactive = true;
load_clone ();
window.unmanaged.connect (unmanaged);
drag_action = new DragDropAction (DragDropActionType.SOURCE, "multitaskingview-window");
drag_action.drag_begin.connect (drag_begin);
drag_action.destination_crossed.connect (drag_destination_crossed);
drag_action.drag_end.connect (drag_end);
drag_action.drag_canceled.connect (drag_canceled);
drag_action.actor_clicked.connect (() => { selected (); });
add_action (drag_action);
}
~TiledWindow ()
{
window.unmanaged.disconnect (unmanaged);
}
public void load_clone ()
{
var actor = window.get_compositor_private () as WindowActor;
if (actor == null) {
Idle.add (() => {
if (window.get_compositor_private () != null)
load_clone ();
return false;
});
return;
}
clone = new Clone (actor);
clone.add_constraint (new BindConstraint (this, BindCoordinate.SIZE, 0));
add_child (clone);
set_easing_duration (250);
set_easing_mode (AnimationMode.EASE_OUT_QUAD);
set_position (actor.x, actor.y);
request_reposition ();
}
public void transition_to_original_state ()
{
var actor = window.get_compositor_private () as Actor;
set_easing_mode (AnimationMode.EASE_IN_OUT_CUBIC);
set_easing_duration (300);
set_size (actor.width, actor.height);
set_position (actor.x, actor.y);
}
void unmanaged ()
{
if (drag_action.dragging)
drag_action.cancel ();
if (clone != null)
clone.destroy ();
destroy ();
}
Actor drag_begin (float click_x, float click_y)
{
float abs_x, abs_y;
get_transformed_position (out abs_x, out abs_y);
prev_parent = get_parent ();
prev_index = prev_parent.get_children ().index (this);
reparent (get_stage ());
save_easing_state ();
set_easing_duration (200);
set_easing_mode (AnimationMode.EASE_IN_OUT_CUBIC);
set_scale (0.4, 0.4);
restore_easing_state ();
request_reposition ();
save_easing_state ();
set_easing_duration (0);
set_position (abs_x, abs_y);
set_pivot_point ((click_x - abs_x) / width, (click_y - abs_y) / height);
return this;
}
void drag_destination_crossed (Actor destination, bool hovered)
{
if (!(destination is IconGroup))
return;
var scale = hovered ? 0.2 : 0.4;
var opacity = hovered ? 100 : 255;
var mode = hovered ? AnimationMode.EASE_IN_OUT_BACK : AnimationMode.EASE_OUT_ELASTIC;
save_easing_state ();
set_easing_mode (mode);
set_easing_duration (300);
set_scale (scale, scale);
set_easing_mode (AnimationMode.LINEAR);
set_opacity (opacity);
restore_easing_state ();
}
void drag_end (Actor destination)
{
Meta.Workspace workspace = null;
if (destination is IconGroup) {
workspace = (destination as IconGroup).workspace;
} else if (destination is FramedBackground) {
workspace = (destination.get_parent () as WorkspaceClone).workspace;
}
if (workspace != null && workspace != window.get_workspace ()) {
window.change_workspace (workspace);
unmanaged ();
} else {
// if we're dropped at the place where we came from interpret as cancel
drag_canceled ();
}
}
void drag_canceled ()
{
get_parent ().remove_child (this);
prev_parent.insert_child_at_index (this, prev_index);
save_easing_state ();
set_easing_duration (400);
set_easing_mode (AnimationMode.EASE_OUT_BOUNCE);
set_scale (1, 1);
request_reposition ();
restore_easing_state ();
restore_easing_state ();
opacity = 255;
}
}
public class TiledWorkspaceContainer : Actor
{
public signal void window_selected (Meta.Window window);
public int padding_top { get; set; }
public int padding_left { get; set; }
public int padding_right { get; set; }
public int padding_bottom { get; set; }
HashTable<int,int> _stacking_order;
public HashTable<int,int> stacking_order {
get {
return _stacking_order;
}
set {
_stacking_order = value;
restack ();
}
}
public TiledWorkspaceContainer (HashTable<int,int> stacking_order)
{
_stacking_order = stacking_order;
}
public void add_window (Meta.Window window, bool reflow_windows = true)
{
var new_window = new TiledWindow (window);
var new_seq = stacking_order.get ((int)window.get_stable_sequence ());
new_window.selected.connect (window_selected_cb);
new_window.destroy.connect (window_destroyed);
new_window.request_reposition.connect (reflow);
var children = get_children ();
var added = false;
foreach (var child in children) {
if (stacking_order.get ((int)(child as TiledWindow).window.get_stable_sequence ()) < new_seq) {
insert_child_below (new_window, child);
added = true;
break;
}
}
// top most or no other children
if (!added)
add_child (new_window);
if (reflow_windows)
reflow ();
}
void window_selected_cb (TiledWindow tiled)
{
window_selected (tiled.window);
}
void window_destroyed (Actor actor)
{
var window = actor as TiledWindow;
window.destroy.disconnect (window_destroyed);
window.selected.disconnect (window_selected_cb);
Idle.add (() => {
reflow ();
return false;
});
}
public void restack ()
{
// FIXME there is certainly a way to do this in less than n^2 steps
foreach (var child1 in get_children ()) {
var i = 0;
foreach (var child2 in get_children ()) {
int index1 = stacking_order.get ((int)(child1 as TiledWindow).window.get_stable_sequence ());
int index2 = stacking_order.get ((int)(child2 as TiledWindow).window.get_stable_sequence ());
if (index1 < index2) {
set_child_at_index (child1, i);
i++;
break;
}
}
}
}
public void reflow ()
{
var windows = new List<InternalUtils.TilableWindow?> ();
foreach (var child in get_children ()) {
var window = child as TiledWindow;
windows.prepend ({ window.window.get_outer_rect (), window });
}
windows.reverse ();
if (windows.length () < 1)
return;
Meta.Rectangle area = {
padding_left,
padding_top,
(int)width - padding_left - padding_right,
(int)height - padding_top - padding_bottom
};
var result = InternalUtils.calculate_grid_placement (area, windows);
foreach (var tilable in result) {
var window = (TiledWindow)tilable.id;
window.set_position (tilable.rect.x, tilable.rect.y);
window.set_size (tilable.rect.width, tilable.rect.height);
}
}
public void transition_to_original_state ()
{
foreach (var child in get_children ()) {
var clone = child as TiledWindow;
clone.transition_to_original_state ();
}
}
}
}

View File

@ -77,19 +77,27 @@ namespace Gala
public class WorkspaceClone : Clutter.Actor
{
public static const int BOTTOM_OFFSET = 100;
const int TOP_OFFSET = 20;
const int HOVER_ACTIVATE_DELAY = 400;
public WindowManager wm { get; construct; }
public Meta.Workspace workspace { get; construct set; }
public BackgroundManager background { get; private set; }
public IconGroup icon_group { get; private set; }
public TiledWorkspaceContainer window_container { get; private set; }
public signal void window_selected (Meta.Window window);
public signal void selected (bool close_view);
const int TOP_OFFSET = 20;
public static const int BOTTOM_OFFSET = 100;
uint hover_activate_timeout = 0;
public WorkspaceClone (Meta.Workspace workspace)
public WorkspaceClone (Meta.Workspace workspace, WindowManager wm)
{
Object (workspace: workspace);
Object (workspace: workspace, wm: wm);
var screen = workspace.get_screen ();
var monitor_geometry = screen.get_monitor_geometry (screen.get_primary_monitor ());
background = new FramedBackground (workspace.get_screen ());
background.reactive = true;
@ -98,12 +106,40 @@ namespace Gala
return false;
});
icon_group = new IconGroup ();
window_container = new TiledWorkspaceContainer (wm.window_stacking_order);
window_container.window_selected.connect ((w) => { window_selected (w); });
window_container.width = monitor_geometry.width;
window_container.height = monitor_geometry.height;
wm.windows_restacked.connect (() => {
window_container.stacking_order = wm.window_stacking_order;
});
icon_group = new IconGroup (workspace);
icon_group.selected.connect (() => {
selected (false);
});
var screen = workspace.get_screen ();
var icons_drop_action = new DragDropAction (DragDropActionType.DESTINATION, "multitaskingview-window");
icon_group.add_action (icons_drop_action);
var background_drop_action = new DragDropAction (DragDropActionType.DESTINATION, "multitaskingview-window");
background.add_action (background_drop_action);
background_drop_action.crossed.connect ((hovered) => {
if (!hovered && hover_activate_timeout != 0) {
Source.remove (hover_activate_timeout);
hover_activate_timeout = 0;
return;
}
if (hovered && hover_activate_timeout == 0) {
hover_activate_timeout = Timeout.add (HOVER_ACTIVATE_DELAY, () => {
selected (false);
hover_activate_timeout = 0;
return false;
});
}
});
screen.window_left_monitor.connect ((monitor, window) => {
if (monitor == screen.get_primary_monitor ())
remove_window (window);
@ -117,22 +153,26 @@ namespace Gala
workspace.window_added.connect (add_window);
add_child (background);
add_child (window_container);
}
private void add_window (Meta.Window window)
{
if (window.window_type != Meta.WindowType.NORMAL)
return;
foreach (var child in window_container.get_children ())
if ((child as TiledWindow).window == window)
return;
window_container.add_window (window);
icon_group.add_window (window);
}
private void remove_window (Meta.Window window)
{
var window_actor = window.get_compositor_private ();
foreach (var child in get_children ()) {
if (child is Clone && (child as Clone).source == window_actor) {
child.destroy ();
break;
}
}
icon_group.remove_window (window);
// TODO ?
}
private void shrink_rectangle (ref Meta.Rectangle rect, int amount)
@ -164,51 +204,31 @@ namespace Gala
icon_group.clear ();
var unsorted_windows = workspace.list_windows ();
var used_windows = new SList<Meta.Window> ();
foreach (var window in unsorted_windows) {
// TODO this can be optimized
window_container.destroy_all_children ();
window_container.padding_top = TOP_OFFSET;
window_container.padding_left =
window_container.padding_right = (int)(monitor.width - monitor.width * scale) / 2;
window_container.padding_bottom = BOTTOM_OFFSET;
var windows = workspace.list_windows ();
foreach (var window in windows) {
if (window.window_type == Meta.WindowType.NORMAL) {
used_windows.append (window);
window_container.add_window (window);
icon_group.add_window (window, true);
}
}
icon_group.redraw ();
var windows = display.sort_windows_by_stacking (used_windows);
var clones = new List<WindowThumb> ();
foreach (var window in windows) {
var window_actor = window.get_compositor_private () as Meta.WindowActor;
var clone = new WindowThumb (window, false);
clone.selected.connect (() => {
window_selected (clone.window);
});
clone.set_position (window_actor.x, window_actor.y);
add_child (clone);
clones.append (clone);
}
if (clones.length () > 0)
WindowOverview.grid_placement (area, clones, place_window);
}
void place_window (Actor window, Meta.Rectangle rect)
{
window.animate (AnimationMode.EASE_OUT_CUBIC, 250,
x: rect.x + 0.0f, y: rect.y + 0.0f, width: rect.width + 0.0f, height: rect.height + 0.0f);
(window as WindowThumb).place_children (rect.width, rect.height);
window_container.restack ();
window_container.reflow ();
}
public void close ()
{
background.animate (AnimationMode.EASE_IN_OUT_CUBIC, 300, scale_x: 1.0f, scale_y: 1.0f);
foreach (var child in get_children ()) {
if (child is WindowThumb)
(child as WindowThumb).close (true, false);
}
window_container.transition_to_original_state ();
}
~Workspace ()

View File

@ -31,6 +31,10 @@ namespace Gala
public class WindowOverview : Actor
{
const int BORDER = 10;
const int TOP_GAP = 20;
const int BOTTOM_GAP = 100;
WindowManager wm;
Screen screen;
@ -81,54 +85,6 @@ namespace Gala
return true;
}
/**
* Code ported from KWin present windows effect
* https://projects.kde.org/projects/kde/kde-workspace/repository/revisions/master/entry/kwin/effects/presentwindows/presentwindows.cpp
**/
//constants, mainly for natural expo
const int GAPS = 10;
const int MAX_TRANSLATIONS = 100000;
const int ACCURACY = 20;
const int BORDER = 10;
const int TOP_GAP = 20;
const int BOTTOM_GAP = 100;
//some math utilities
static int squared_distance (Gdk.Point a, Gdk.Point b)
{
var k1 = b.x - a.x;
var k2 = b.y - a.y;
return k1*k1 + k2*k2;
}
static bool rect_is_overlapping_any (Meta.Rectangle rect, Meta.Rectangle[] rects, Meta.Rectangle border)
{
if (!border.contains_rect (rect))
return true;
foreach (var comp in rects) {
if (comp == rect)
continue;
if (rect.overlap (comp))
return true;
}
return false;
}
static Meta.Rectangle rect_adjusted (Meta.Rectangle rect, int dx1, int dy1, int dx2, int dy2)
{
return {rect.x + dx1, rect.y + dy1, rect.width + (-dx1 + dx2), rect.height + (-dy1 + dy2)};
}
static Gdk.Point rect_center (Meta.Rectangle rect)
{
return {rect.x + rect.width / 2, rect.y + rect.height / 2};
}
void calculate_places (List<Actor> windows)
{
var clones = windows.copy ();
@ -137,20 +93,21 @@ namespace Gala
(int)(b as WindowThumb).window.get_stable_sequence ();
});
//sort windows by monitor
List<Actor>[] monitors = {};
// sort windows by monitor
List<InternalUtils.TilableWindow?>[] monitors = {};
monitors.resize (screen.get_n_monitors ());
foreach (var clone in clones) {
// we had some crashes here so there's a reasonable suspicion
// that get_monitor() could be larger than get_n_monitors()
var index = (clone as WindowThumb).window.get_monitor ();
var thumb = clone as WindowThumb;
var index = thumb.window.get_monitor ();
if (index >= screen.get_n_monitors ()) {
critical ("Window '%s' has a monitor assigned that does not actually exists",
(clone as WindowThumb).window.get_title ());
index = screen.get_n_monitors () - 1;
}
monitors[index].append (clone);
monitors[index].append ({ thumb.window.get_outer_rect (), thumb });
}
for (var i = 0; i < screen.get_n_monitors (); i++) {
@ -164,340 +121,21 @@ namespace Gala
(int)Math.floorf (geom.width - BORDER * 2),
(int)Math.floorf (geom.height - BOTTOM_GAP)};
if (BehaviorSettings.get_default ().schema.get_enum ("window-overview-type") == WindowOverviewType.GRID)
/*TODO if (BehaviorSettings.get_default ().schema.get_enum ("window-overview-type") == WindowOverviewType.GRID)
grid_placement (area, monitors[i], place_window);
else
natural_placement (area, monitors[i]);
}
}
natural_placement (area, monitors[i]);*/
public static void grid_placement (Meta.Rectangle area, List<Actor> clones, WindowPlacer place)
{
int columns = (int)Math.ceil (Math.sqrt (clones.length ()));
int rows = (int)Math.ceil (clones.length () / (double)columns);
// Assign slots
int slot_width = area.width / columns;
int slot_height = area.height / rows;
WindowThumb[] taken_slots = {};
taken_slots.resize (rows * columns);
// precalculate all slot centers
Gdk.Point[] slot_centers = {};
slot_centers.resize (rows * columns);
for (int x = 0; x < columns; x++) {
for (int y = 0; y < rows; y++) {
slot_centers[x + y * columns] = {area.x + slot_width * x + slot_width / 2,
area.y + slot_height * y + slot_height / 2};
var result = InternalUtils.calculate_grid_placement (area, monitors[i]);
foreach (var window in result) {
place_window ((WindowThumb)window.id, window.rect);
}
}
// Assign each window to the closest available slot
var tmplist = clones.copy ();
var window_count = tmplist.length ();
while (tmplist.length () > 0) {
var window = tmplist.nth_data (0) as WindowThumb;
var rect = window.window.get_outer_rect ();
var slot_candidate = -1;
var slot_candidate_distance = int.MAX;
var pos = rect_center (rect);
// all slots
for (int i = 0; i < columns * rows; i++) {
if (i > window_count - 1)
break;
var dist = squared_distance (pos, slot_centers[i]);
if (dist < slot_candidate_distance) {
// window is interested in this slot
WindowThumb occupier = taken_slots[i];
if (occupier == window)
continue;
if (occupier == null || dist < squared_distance (rect_center (occupier.window.get_outer_rect ()), slot_centers[i])) {
// either nobody lives here, or we're better - takeover the slot if it's our best
slot_candidate = i;
slot_candidate_distance = dist;
}
}
}
if (slot_candidate == -1)
continue;
if (taken_slots[slot_candidate] != null)
tmplist.prepend (taken_slots[slot_candidate]);
tmplist.remove_all (window);
taken_slots[slot_candidate] = window;
}
//see how many windows we have on the last row
int left_over = (int)clones.length () - columns * (rows - 1);
for (int slot = 0; slot < columns * rows; slot++) {
var window = taken_slots[slot];
// some slots might be empty
if (window == null)
continue;
var rect = window.window.get_outer_rect ();
// Work out where the slot is
Meta.Rectangle target = {area.x + (slot % columns) * slot_width,
area.y + (slot / columns) * slot_height,
slot_width,
slot_height};
target = rect_adjusted (target, 10, 10, -10, -10);
float scale;
if (target.width / (double)rect.width < target.height / (double)rect.height) {
// Center vertically
scale = target.width / (float)rect.width;
target.y += (target.height - (int)(rect.height * scale)) / 2;
target.height = (int)Math.floorf (rect.height * scale);
} else {
// Center horizontally
scale = target.height / (float)window.height;
target.x += (target.width - (int)(rect.width * scale)) / 2;
target.width = (int)Math.floorf (rect.width * scale);
}
// Don't scale the windows too much
if (scale > 1.0) {
scale = 1.0f;
target = {rect_center (target).x - (int)Math.floorf (rect.width * scale) / 2,
rect_center (target).y - (int)Math.floorf (rect.height * scale) / 2,
(int)Math.floorf (scale * rect.width),
(int)Math.floorf (scale * rect.height)};
}
//put the last row in the center, if necessary
if (left_over != columns && slot >= columns * (rows - 1))
target.x += (columns - left_over) * slot_width / 2;
place (window, target);
}
}
void natural_placement (Meta.Rectangle area, List<Actor> clones)
{
Meta.Rectangle bounds = {area.x, area.y, area.width, area.height};
var direction = 0;
int[] directions = new int[clones.length ()];
Meta.Rectangle[] rects = new Meta.Rectangle[clones.length ()];
for (int i = 0; i < clones.length (); i++) {
// save rectangles into 4-dimensional arrays representing two corners of the rectangular: [left_x, top_y, right_x, bottom_y]
var rect = (clones.nth_data (i) as WindowThumb).window.get_outer_rect ();
rect = rect_adjusted(rect, -GAPS, -GAPS, GAPS, GAPS);
rects[i] = rect;
bounds = bounds.union (rect);
// This is used when the window is on the edge of the screen to try to use as much screen real estate as possible.
directions[i] = direction;
direction++;
if (direction == 4)
direction = 0;
}
var loop_counter = 0;
var overlap = false;
do {
overlap = false;
for (var i = 0; i < rects.length; i++) {
for (var j = 0; j < rects.length; j++) {
if (i == j)
continue;
var rect = rects[i];
var comp = rects[j];
if (!rect.overlap (comp))
continue;
loop_counter ++;
overlap = true;
// Determine pushing direction
Gdk.Point i_center = rect_center (rect);
Gdk.Point j_center = rect_center (comp);
Gdk.Point diff = {j_center.x - i_center.x, j_center.y - i_center.y};
// Prevent dividing by zero and non-movement
if (diff.x == 0 && diff.y == 0)
diff.x = 1;
// Approximate a vector of between 10px and 20px in magnitude in the same direction
var length = Math.sqrtf (diff.x * diff.x + diff.y * diff.y);
diff.x = (int)Math.floorf (diff.x * ACCURACY / length);
diff.y = (int)Math.floorf (diff.y * ACCURACY / length);
// Move both windows apart
rect.x += -diff.x;
rect.y += -diff.y;
comp.x += diff.x;
comp.y += diff.y;
// Try to keep the bounding rect the same aspect as the screen so that more
// screen real estate is utilised. We do this by splitting the screen into nine
// equal sections, if the window center is in any of the corner sections pull the
// window towards the outer corner. If it is in any of the other edge sections
// alternate between each corner on that edge. We don't want to determine it
// randomly as it will not produce consistant locations when using the filter.
// Only move one window so we don't cause large amounts of unnecessary zooming
// in some situations. We need to do this even when expanding later just in case
// all windows are the same size.
// (We are using an old bounding rect for this, hopefully it doesn't matter)
var x_section = (int)Math.roundf ((rect.x - bounds.x) / (bounds.width / 3.0f));
var y_section = (int)Math.roundf ((comp.y - bounds.y) / (bounds.height / 3.0f));
i_center = rect_center (rect);
diff.x = 0;
diff.y = 0;
if (x_section != 1 || y_section != 1) { // Remove this if you want the center to pull as well
if (x_section == 1)
x_section = (directions[i] / 2 == 1 ? 2 : 0);
if (y_section == 1)
y_section = (directions[i] % 2 == 1 ? 2 : 0);
}
if (x_section == 0 && y_section == 0) {
diff.x = bounds.x - i_center.x;
diff.y = bounds.y - i_center.y;
}
if (x_section == 2 && y_section == 0) {
diff.x = bounds.x + bounds.width - i_center.x;
diff.y = bounds.y - i_center.y;
}
if (x_section == 2 && y_section == 2) {
diff.x = bounds.x + bounds.width - i_center.x;
diff.y = bounds.y + bounds.height - i_center.y;
}
if (x_section == 0 && y_section == 2) {
diff.x = bounds.x - i_center.x;
diff.y = bounds.y + bounds.height - i_center.y;
}
if (diff.x != 0 || diff.y != 0) {
length = Math.sqrtf (diff.x * diff.x + diff.y * diff.y);
diff.x *= (int)Math.floorf (ACCURACY / length / 2.0f);
diff.y *= (int)Math.floorf (ACCURACY / length / 2.0f);
rect.x += diff.x;
rect.y += diff.y;
}
// Update bounding rect
bounds = bounds.union(rect);
bounds = bounds.union(comp);
//we took copies from the rects from our list so we need to reassign them
rects[i] = rect;
rects[j] = comp;
}
}
} while (overlap && loop_counter < MAX_TRANSLATIONS);
// Work out scaling by getting the most top-left and most bottom-right window coords.
float scale = Math.fminf (Math.fminf (area.width / (float)bounds.width, area.height / (float)bounds.height), 1.0f);
// Make bounding rect fill the screen size for later steps
bounds.x = (int)Math.floorf (bounds.x - (area.width - bounds.width * scale) / 2);
bounds.y = (int)Math.floorf (bounds.y - (area.height - bounds.height * scale) / 2);
bounds.width = (int)Math.floorf (area.width / scale);
bounds.height = (int)Math.floorf (area.height / scale);
// Move all windows back onto the screen and set their scale
var index = 0;
foreach (var rect in rects) {
rect = {(int)Math.floorf ((rect.x - bounds.x) * scale + area.x),
(int)Math.floorf ((rect.y - bounds.y) * scale + area.y),
(int)Math.floorf (rect.width * scale),
(int)Math.floorf (rect.height * scale)};
rects[index] = rect;
index++;
}
// fill gaps by enlarging windows
bool moved = false;
Meta.Rectangle border = area;
do {
moved = false;
index = 0;
foreach (var rect in rects) {
int width_diff = ACCURACY;
int height_diff = (int)Math.floorf ((((rect.width + width_diff) - rect.height) /
(float)rect.width) * rect.height);
int x_diff = width_diff / 2;
int y_diff = height_diff / 2;
//top right
Meta.Rectangle old = rect;
rect = {rect.x + x_diff, rect.y - y_diff - height_diff, rect.width + width_diff, rect.height + width_diff};
if (rect_is_overlapping_any (rect, rects, border))
rect = old;
else
moved = true;
//bottom right
old = rect;
rect = {rect.x + x_diff, rect.y + y_diff, rect.width + width_diff, rect.height + width_diff};
if (rect_is_overlapping_any (rect, rects, border))
rect = old;
else
moved = true;
//bottom left
old = rect;
rect = {rect.x - x_diff, rect.y + y_diff, rect.width + width_diff, rect.height + width_diff};
if (rect_is_overlapping_any (rect, rects, border))
rect = old;
else
moved = true;
//top left
old = rect;
rect = {rect.x - x_diff, rect.y - y_diff - height_diff, rect.width + width_diff, rect.height + width_diff};
if (rect_is_overlapping_any (rect, rects, border))
rect = old;
else
moved = true;
rects[index] = rect;
index++;
}
} while (moved);
index = 0;
foreach (var rect in rects) {
var window = clones.nth_data (index) as WindowThumb;
var window_rect = window.window.get_outer_rect ();
rect = rect_adjusted(rect, GAPS, GAPS, -GAPS, -GAPS);
scale = rect.width / (float)window_rect.width;
if (scale > 2.0 || (scale > 1.0 && (window_rect.width > 300 || window_rect.height > 300))) {
scale = (window_rect.width > 300 || window_rect.height > 300) ? 1.0f : 2.0f;
rect = {rect_center (rect).x - (int)Math.floorf (window_rect.width * scale) / 2,
rect_center (rect).y - (int)Math.floorf (window_rect.height * scale) / 2,
(int)Math.floorf (window_rect.width * scale),
(int)Math.floorf (window_rect.height * scale)};
}
place_window (window, rect);
index++;
}
}
// animate a window to the given position
void place_window (Actor actor, Meta.Rectangle rect)
void place_window (WindowThumb clone, Meta.Rectangle rect)
{
var clone = actor as WindowThumb;
var fscale = rect.width / clone.width;
//animate the windows and icons to the calculated positions

View File

@ -21,11 +21,13 @@ namespace Gala
{
public class WindowManagerGala : Meta.Plugin, WindowManager
{
public Clutter.Actor ui_group { get; protected set; }
public Clutter.Stage stage { get; protected set; }
public Clutter.Actor window_group { get; protected set; }
public Clutter.Actor top_window_group { get; protected set; }
public Meta.BackgroundGroup background_group { get; protected set; }
public HashTable<int,int> window_stacking_order { get; protected set; }
Meta.PluginInfo info;
@ -124,6 +126,10 @@ namespace Gala
stage.remove_child (top_window_group);
ui_group.add_child (top_window_group);
window_stacking_order = new HashTable<int,int> (null, null);
update_stacking_order ();
screen.restacked.connect (update_stacking_order);
/*keybindings*/
screen.get_display ().add_keybinding ("switch-to-workspace-first", KeybindingSettings.get_default ().schema, 0, () => {
@ -246,6 +252,19 @@ namespace Gala
return false;
}
void update_stacking_order ()
{
window_stacking_order.remove_all ();
var i = 0;
foreach (var window in Compositor.get_window_actors (get_screen ())) {
var seq = window.get_meta_window ().get_stable_sequence ();
window_stacking_order.set ((int)window.get_meta_window ().get_stable_sequence (), i++);
}
windows_restacked ();
}
void configure_hotcorners ()
{
var geometry = get_screen ().get_monitor_geometry (get_screen ().get_primary_monitor ());