Skip to content

Instantly share code, notes, and snippets.

@skypce
Created February 15, 2015 00:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save skypce/37b311aa75d905cc93b8 to your computer and use it in GitHub Desktop.
Save skypce/37b311aa75d905cc93b8 to your computer and use it in GitHub Desktop.
//
// Copyright (C) 2012 Tom Beckmann, Rico Tzschichholz
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
using Meta;
namespace Gala
{
public class WorkspaceView : Clutter.Actor
{
static const float VIEW_HEIGHT = 160.0f;
static const float SCROLL_SPEED = 30.0f;
Gala.Plugin plugin;
Screen screen;
Clutter.Actor thumbnails;
Clutter.Actor scroll;
Clutter.Actor click_catcher; //invisible plane that catches clicks outside the view
bool animating; // delay closing the popup
bool wait_one_key_release; //called by shortcut, don't close it on first keyrelease
uint last_switch_time = 0;
Gtk.StyleContext background_style;
Gtk.EventBox background_style_widget;
Gtk.StyleContext thumbnails_style;
Gtk.EventBox thumbnails_style_widget;
public WorkspaceView (Gala.Plugin _plugin)
{
plugin = _plugin;
screen = plugin.get_screen ();
height = VIEW_HEIGHT;
reactive = true;
clip_to_allocation = true;
background_style_widget = new Gtk.EventBox ();
background_style_widget.show ();
background_style = background_style_widget.get_style_context ();
background_style.add_class ("gala-workspaces-background");
background_style.add_provider (Utils.get_default_style (), Gtk.STYLE_PROVIDER_PRIORITY_FALLBACK);
thumbnails_style_widget = new Gtk.EventBox ();
thumbnails_style_widget.show ();
thumbnails_style = thumbnails_style_widget.get_style_context ();
thumbnails_style.add_class ("thumbnails-boxes");
thumbnails_style.add_provider (Utils.get_default_style (), Gtk.STYLE_PROVIDER_PRIORITY_FALLBACK);
thumbnails = new Clutter.Actor ();
thumbnails.layout_manager = new Clutter.BoxLayout ();
(thumbnails.layout_manager as Clutter.BoxLayout).spacing = 12;
(thumbnails.layout_manager as Clutter.BoxLayout).homogeneous = true;
content = new Clutter.Canvas ();
(content as Clutter.Canvas).draw.connect (draw_background);
scroll = new Clutter.Actor ();
scroll.height = 12;
scroll.content = new Clutter.Canvas ();
(scroll.content as Clutter.Canvas).draw.connect (draw_scroll);
click_catcher = new Clutter.Actor ();
click_catcher.reactive = true;
click_catcher.button_release_event.connect ((e) => {
hide ();
return true;
});
Compositor.get_stage_for_screen (screen).add_child (click_catcher);
add_child (thumbnails);
add_child (scroll);
//place it somewhere low, so it won't slide down on first open
int swidth, sheight;
screen.get_size (out swidth, out sheight);
y = sheight;
screen.workspace_added.connect ((index) => {
create_workspace_thumb (screen.get_workspace_by_index (index));
});
Prefs.add_listener ((pref) => {
if (pref == Preference.DYNAMIC_WORKSPACES && Prefs.get_dynamic_workspaces ()) {
// if the last workspace has a window, we need to append a new workspace
if (Utils.get_n_windows (screen.get_workspaces ().nth_data (screen.get_n_workspaces () - 1)) > 0)
add_workspace ();
} else if ((pref == Preference.DYNAMIC_WORKSPACES ||
pref == Preference.NUM_WORKSPACES) &&
!Prefs.get_dynamic_workspaces ()) {
// only need to listen for the case when workspaces were removed.
// Any other case will be caught by the workspace_added signal.
// For some reason workspace_removed is not emitted, when changing the workspace number
if (Prefs.get_num_workspaces () < thumbnails.get_n_children ()) {
for (int i = Prefs.get_num_workspaces () - 1; i < thumbnails.get_n_children (); i++) {
(thumbnails.get_child_at_index (i) as WorkspaceThumb).closed ();
}
}
}
});
init_thumbnails ();
}
void init_thumbnails ()
{
foreach (var workspace in screen.get_workspaces ()) {
var thumb = new WorkspaceThumb (workspace);
thumb.clicked.connect (hide);
thumb.closed.connect (remove_workspace);
thumb.window_on_last.connect (add_workspace);
thumbnails.add_child (thumb);
}
//if there went something wrong, we need to get the system back rolling
if (Prefs.get_dynamic_workspaces ()
&& screen.n_workspaces == 1
&& Utils.get_n_windows (screen.get_workspaces ().first ().data) > 0)
add_workspace ();
}
bool draw_background (Cairo.Context cr)
{
cr.set_operator (Cairo.Operator.CLEAR);
cr.paint ();
cr.set_operator (Cairo.Operator.OVER);
background_style.render_background (cr, 0, 0, width, height);
background_style.render_frame (cr, 0, 0, width, height);
var pat = new Cairo.Pattern.for_surface (new Cairo.ImageSurface.from_png (Config.PKGDATADIR + "/texture.png"));
pat.set_extend (Cairo.Extend.REPEAT);
cr.set_source (pat);
cr.paint_with_alpha (0.6);
return false;
}
bool draw_scroll (Cairo.Context cr)
{
cr.set_operator (Cairo.Operator.CLEAR);
cr.paint ();
cr.set_operator (Cairo.Operator.OVER);
Granite.Drawing.Utilities.cairo_rounded_rectangle (cr, 4, 4, scroll.width-32, 4, 2);
cr.set_source_rgba (1, 1, 1, 0.8);
cr.fill ();
return false;
}
void add_workspace ()
{
var wp = screen.append_new_workspace (false, screen.get_display ().get_current_time ());
if (wp == null)
return;
}
void create_workspace_thumb (Meta.Workspace workspace)
{
var thumb = new WorkspaceThumb (workspace);
thumb.clicked.connect (hide);
thumb.closed.connect (remove_workspace);
thumb.window_on_last.connect (add_workspace);
thumbnails.insert_child_at_index (thumb, workspace.index ());
thumb.show ();
check_scrollbar ();
}
void remove_workspace (WorkspaceThumb thumb)
{
//if there's only one used left, remove the second one to avoid rather confusing workspace movement
if (thumb.workspace.index () == 0 && screen.n_workspaces == 2) {
return;
}
thumb.clicked.disconnect (hide);
thumb.closed.disconnect (remove_workspace);
thumb.window_on_last.disconnect (add_workspace);
var workspace = thumb.workspace;
//dont remove non existing workspaces
if (workspace != null && workspace.index () > -1) {
var screen = workspace.get_screen ();
screen.remove_workspace (workspace, screen.get_display ().get_current_time ());
}
thumb.workspace = null;
thumbnails.remove_child (thumb);
thumb.destroy ();
check_scrollbar ();
}
void check_scrollbar ()
{
scroll.visible = thumbnails.width > width;
if (scroll.visible) {
if (thumbnails.x + thumbnails.width < width)
thumbnails.x = width - thumbnails.width;
scroll.width = width / thumbnails.width * width;
scroll.y = height - 12;
(scroll.content as Clutter.Canvas).set_size ((int)scroll.width, 12);
} else {
thumbnails.animate (Clutter.AnimationMode.EASE_OUT_QUAD, 400, x : width / 2 - thumbnails.width / 2);
}
}
void switch_to_next_workspace (MotionDirection direction)
{
var display = screen.get_display ();
var old_index = screen.get_active_workspace_index ();
var neighbor = screen.get_active_workspace ().get_neighbor (direction);
neighbor.activate (display.get_current_time ());
// if we didnt switch, show a nudge-over animation. need to take the indices
// here since the changing only applies after the animation ends
if (old_index == 0 && direction == MotionDirection.LEFT ||
old_index == screen.n_workspaces - 1 && direction == MotionDirection.RIGHT) {
var dest = (direction == MotionDirection.LEFT ? 32.0f : -32.0f);
Compositor.get_window_group_for_screen (screen).animate (Clutter.AnimationMode.LINEAR, 100, x:dest);
Clutter.Threads.Timeout.add (210, () => {
Compositor.get_window_group_for_screen (screen).animate (Clutter.AnimationMode.LINEAR, 150, x:0.0f);
return false;
});
}
}
public override void key_focus_out ()
{
hide ();
}
public override bool key_press_event (Clutter.KeyEvent event)
{
var display = screen.get_display ();
var current_time = display.get_current_time_roundtrip ();
// Don't allow switching while another animation is still in progress to avoid visual disruptions
if (current_time < (last_switch_time + AnimationSettings.get_default ().workspace_switch_duration))
return false;
int switch_index = -1;
switch (event.keyval) {
case Clutter.Key.Left:
if ((event.modifier_state & Clutter.ModifierType.SHIFT_MASK) != 0)
plugin.move_window (display.get_focus_window (), MotionDirection.LEFT);
else
switch_to_next_workspace (MotionDirection.LEFT);
last_switch_time = current_time;
return false;
case Clutter.Key.Right:
if ((event.modifier_state & Clutter.ModifierType.SHIFT_MASK) != 0)
plugin.move_window (display.get_focus_window (), MotionDirection.RIGHT);
else
switch_to_next_workspace (MotionDirection.RIGHT);
last_switch_time = current_time;
return false;
case Clutter.Key.@1:
switch_index = 1;
break;
case Clutter.Key.@2:
switch_index = 2;
break;
case Clutter.Key.@3:
switch_index = 3;
break;
case Clutter.Key.@4:
switch_index = 4;
break;
case Clutter.Key.@5:
switch_index = 5;
break;
case Clutter.Key.@6:
switch_index = 6;
break;
case Clutter.Key.@7:
switch_index = 7;
break;
case Clutter.Key.@8:
switch_index = 8;
break;
case Clutter.Key.@9:
switch_index = 9;
break;
case Clutter.Key.@0:
switch_index = 10;
break;
//we have super+s as default combination, so we allow closing by pressing s
case Clutter.Key.s:
hide ();
break;
default:
break;
}
if (switch_index > 0 && switch_index <= screen.n_workspaces) {
screen.get_workspace_by_index (switch_index - 1).activate (current_time);
last_switch_time = current_time;
}
return true;
}
public override bool key_release_event (Clutter.KeyEvent event)
{
switch (event.keyval) {
case Clutter.Key.Alt_L:
case Clutter.Key.Alt_R:
case Clutter.Key.Control_L:
case Clutter.Key.Control_R:
case Clutter.Key.Super_L:
case Clutter.Key.Super_R:
case Clutter.Key.Escape:
case Clutter.Key.Return:
if (wait_one_key_release) {
wait_one_key_release = false;
return false;
}
hide ();
return true;
}
return false;
}
public override bool scroll_event (Clutter.ScrollEvent event)
{
switch (event.direction) {
case Clutter.ScrollDirection.DOWN:
case Clutter.ScrollDirection.RIGHT:
if (thumbnails.width + thumbnails.x > width)
thumbnails.x -= SCROLL_SPEED;
break;
case Clutter.ScrollDirection.UP:
case Clutter.ScrollDirection.LEFT:
if (thumbnails.x < 0)
thumbnails.x += SCROLL_SPEED;
break;
default:
return false;
}
scroll.x = Math.floorf (width / thumbnails.width * -thumbnails.x);
return false;
}
/*
* if shortcut, wait one key release before closing
*/
public new void show (bool shortcut = false)
{
if (visible) {
hide ();
return;
}
wait_one_key_release = shortcut;
var screen = plugin.get_screen ();
visible = true;
grab_key_focus ();
plugin.begin_modal ();
var area = screen.get_monitor_geometry (screen.get_primary_monitor ());
y = area.height + area.y;
x = area.x;
width = area.width;
(content as Clutter.Canvas).set_size ((int)width, (int)height);
thumbnails.get_children ().foreach ((thumb) => {
thumb.show ();
});
thumbnails.x = width / 2 - thumbnails.width / 2;
thumbnails.y = 15;
scroll.visible = thumbnails.width > width;
if (scroll.visible) {
scroll.y = height - 12;
scroll.x = 0.0f;
scroll.width = width / thumbnails.width * width;
thumbnails.x = 4.0f;
}
int swidth, sheight;
screen.get_size (out swidth, out sheight);
click_catcher.width = swidth;
click_catcher.height = sheight;
click_catcher.x = 0;
click_catcher.y = 0;
click_catcher.visible = true;
animating = true;
Timeout.add (50, () => {
animating = false;
return false;
}); //catch hot corner hiding problem
var wins = Compositor.get_window_group_for_screen (screen);
wins.detach_animation ();
wins.x = 0.0f;
animate (Clutter.AnimationMode.EASE_OUT_QUAD, 250, y : (area.height + area.y) - height);
wins.animate (Clutter.AnimationMode.EASE_OUT_QUAD, 250, y : -height + 1);
}
public new void hide ()
{
if (!visible || animating)
return;
float width, height;
plugin.get_screen ().get_size (out width, out height);
plugin.end_modal ();
plugin.update_input_area ();
animating = true;
animate (Clutter.AnimationMode.EASE_OUT_EXPO, 500, y : height).completed.connect (() => {
thumbnails.get_children ().foreach ((thumb) => {
thumb.hide ();
});
animating = false;
visible = false;
});
click_catcher.visible = false;
var wins = Compositor.get_window_group_for_screen (screen);
wins.detach_animation ();
wins.x = 0.0f;
wins.animate (Clutter.AnimationMode.EASE_OUT_EXPO, 500, y : 0.0f);
}
public void handle_switch_to_workspace (Meta.Display display, Meta.Screen screen, Meta.Window? window,
X.Event event, Meta.KeyBinding binding)
{
var direction = (binding.get_name () == "switch-to-workspace-left" ? MotionDirection.LEFT : MotionDirection.RIGHT);
switch_to_next_workspace (direction);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment