Add docs to WindowManager interface, rework modal mode handling API and add functionality to control keybinding filtering

Add interface to properly replace core components.
This commit is contained in:
Tom Beckmann 2014-07-20 22:02:34 +02:00 committed by Rico Tzschichholz
parent 41de35783b
commit 1e684669fb
8 changed files with 340 additions and 77 deletions

View File

@ -1,6 +1,7 @@
include $(top_srcdir)/Makefile.common
libgala_doc_files = \
$(top_srcdir)/lib/ActivatableComponent.vala \
$(top_srcdir)/lib/Plugin.vala \
$(top_srcdir)/lib/Utils.vala \
$(top_srcdir)/lib/WindowManager.vala \

View File

@ -32,6 +32,7 @@ libgala_la_LIBADD = \
$(NULL)
libgala_la_VALASOURCES = \
ActivatableComponent.vala \
Plugin.vala \
Utils.vala \
WindowManager.vala \

View File

@ -29,23 +29,121 @@ namespace Gala
WINDOW_OVERVIEW_ALL
}
/**
* Function that should return true if the given shortcut should be blocked.
*/
public delegate bool KeybindingFilter (Meta.KeyBinding binding);
/**
* A minimal class mostly used to identify your call to {@link WindowManager.push_modal} and used
* to end your modal mode again with {@link WindowManager.pop_modal}
*/
public class ModalProxy : Object
{
/**
* A function which is called whenever a keybinding is pressed. If you supply a custom
* one you can filter out those that'd you like to be passed through and block all others.
* Defaults to blocking all.
* @see KeybindingFilter
*/
public KeybindingFilter? keybinding_filter { get; set; default = () => true; }
public ModalProxy ()
{
}
/**
* Small utility to allow all keybindings
*/
public void allow_all_keybindings ()
{
keybinding_filter = null;
}
}
public interface WindowManager : Meta.Plugin
{
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; }
/**
* If true all keybindings will be blocked while modal mode is active.
* This is the container you'll most likely want to add your component to. It wraps
* every other container listed in this interface and is a direct child of the stage.
*/
public abstract bool block_keybindings_in_modal { get; set; default = true; }
public abstract Clutter.Actor ui_group { get; protected set; }
public abstract void begin_modal ();
public abstract void end_modal ();
/**
* The stage of the window manager
*/
public abstract Clutter.Stage stage { get; protected set; }
/**
* A group containting all 'usual' windows
* @see top_window_group
*/
public abstract Clutter.Actor window_group { get; protected set; }
/**
* The top window group contains special windows that are always placed on top
* like fullscreen windows.
*/
public abstract Clutter.Actor top_window_group { get; protected set; }
/**
* The background group is a container for the background actors forming the wallpaper
*/
public abstract Meta.BackgroundGroup background_group { get; protected set; }
/**
* Enters the modal mode, which means that all events are directed to the stage instead
* of the windows. This is the only way to receive keyboard events besides shortcut listeners.
*
* @return a {@link ModalProxy} which is needed to end the modal mode again and provides some
* some basic control on the behavior of the window manager while it is in modal mode.
*/
public abstract ModalProxy push_modal ();
/**
* May exit the modal mode again, unless another component has called {@link push_modal}
*
* @param proxy The {@link ModalProxy} received from {@link push_modal}
*/
public abstract void pop_modal (ModalProxy proxy);
/**
* Returns whether the window manager is currently in modal mode.
* @see push_modal
*/
public abstract bool is_modal ();
/**
* Tests if a given {@link ModalProxy} is valid and may be popped. Should not be necessary
* to use this function in most cases, but it may be helpful for debugging. Gala catches
* invalid proxies as well and emits a warning in that case.
*
* @param proxy The {@link ModalProxy} to check
* @return Returns true if the prox is valid
*/
public abstract bool modal_proxy_valid (ModalProxy proxy);
/**
* Tells the window manager to perform the given action.
*
* @param type The type of action to perform
*/
public abstract void perform_action (ActionType type);
/**
* Moves the window to the workspace next to its current workspace in the given direction.
* Gala currently only supports LEFT and RIGHT.
*
* @param window The window to be moved
* @param direction The direction in which to move the window
*/
public abstract void move_window (Meta.Window? window, Meta.MotionDirection direction);
/**
* Switches to the next workspace in the given direction.
*
* @param direction The direction in which to switch
*/
public abstract void switch_to_next_workspace (Meta.MotionDirection direction);
}
}

View File

@ -199,6 +199,11 @@ namespace Gala
load_later_plugins.clear ();
}
public Plugin? get_plugin (string id)
{
return plugins.lookup (id);
}
/**
* Iterate over all plugins and grab their regions, update the regions
* array accordingly and emit the regions_changed signal.

View File

@ -25,7 +25,7 @@ namespace Gala
* preparing the wm, opening the components and holds containers for
* the icon groups, the WorkspaceClones and the MonitorClones.
*/
public class MultitaskingView : Actor
public class MultitaskingView : Actor, ActivatableComponent
{
const int HIDING_DURATION = 300;
const int SMOOTH_SCROLL_DELAY = 500;
@ -33,6 +33,7 @@ namespace Gala
public WindowManager wm { get; construct; }
Meta.Screen screen;
ModalProxy modal_proxy;
bool opened = false;
bool animating = false;
@ -391,12 +392,42 @@ namespace Gala
}
}
/**
* {@inheritDoc}
*/
public bool is_opened ()
{
return opened;
}
/**
* {@inheritDoc}
*/
public void open (Gee.HashMap<string,GLib.Value?>? hints = null)
{
if (opened)
return;
toggle ();
}
/**
* {@inheritDoc}
*/
public void close ()
{
if (!opened)
return;
toggle ();
}
/**
* Toggles the view open or closed. Takes care of all the wm related tasks, like
* starting the modal mode and hiding the WindowGroup. Finally tells all components
* to animate to their positions.
*/
public void toggle ()
void toggle ()
{
if (animating)
return;
@ -415,8 +446,8 @@ namespace Gala
}
if (opening) {
wm.begin_modal ();
wm.block_keybindings_in_modal = false;
modal_proxy = wm.push_modal ();
modal_proxy.keybinding_filter = keybinding_filter;
wm.background_group.hide ();
wm.window_group.hide ();
@ -464,8 +495,7 @@ namespace Gala
wm.window_group.show ();
wm.top_window_group.show ();
wm.block_keybindings_in_modal = true;
wm.end_modal ();
wm.pop_modal (modal_proxy);
animating = false;
@ -478,6 +508,19 @@ namespace Gala
});
}
}
bool keybinding_filter (KeyBinding binding)
{
var action = Prefs.get_keybinding_action (binding.get_name ());
switch (action) {
case KeyBindingAction.WORKSPACE_LEFT:
case KeyBindingAction.WORKSPACE_RIGHT:
case KeyBindingAction.SHOW_DESKTOP:
return false;
default:
return true;
}
}
}
}

View File

@ -29,7 +29,7 @@ namespace Gala
public delegate void WindowPlacer (Actor window, Meta.Rectangle rect);
public class WindowOverview : Actor
public class WindowOverview : Actor, ActivatableComponent
{
const int BORDER = 10;
const int TOP_GAP = 30;
@ -39,6 +39,7 @@ namespace Gala
Meta.Screen screen;
ModalProxy modal_proxy;
bool ready;
// the workspaces which we expose right now
@ -53,7 +54,7 @@ namespace Gala
{
screen = wm.get_screen ();
screen.workspace_switched.connect (() => close (false));
screen.workspace_switched.connect (close);
screen.restacked.connect (restack_windows);
visible = false;
@ -69,7 +70,7 @@ namespace Gala
public override bool key_press_event (Clutter.KeyEvent event)
{
if (event.keyval == Clutter.Key.Escape) {
close (true);
close ();
return true;
}
@ -79,27 +80,41 @@ namespace Gala
public override void key_focus_out ()
{
close (false);
close ();
}
public override bool button_release_event (Clutter.ButtonEvent event)
{
if (event.button == 1)
close (true);
close ();
return true;
}
public void open (bool animate = true, bool all_windows = false)
/**
* {@inheritDoc}
*/
public bool is_opened ()
{
return visible;
}
/**
* {@inheritDoc}
* You may specify 'all-windows' in hints to expose all windows
*/
public void open (Gee.HashMap<string,GLib.Value?>? hints = null)
{
if (!ready)
return;
if (visible) {
close (true);
close ();
return;
}
var all_windows = hints != null && hints.has_key ("all-windows");
var used_windows = new SList<Window> ();
workspaces = new List<Workspace> ();
@ -152,8 +167,8 @@ namespace Gala
grab_key_focus ();
wm.block_keybindings_in_modal = false;
wm.begin_modal ();
modal_proxy = wm.push_modal ();
modal_proxy.keybinding_filter = keybinding_filter;
visible = true;
@ -189,6 +204,13 @@ namespace Gala
ready = true;
}
bool keybinding_filter (KeyBinding binding)
{
var name = binding.get_name ();
return name != "expose-windows"
&& name != "expose-all-windows";
}
void restack_windows (Screen screen)
{
foreach (var child in get_children ())
@ -240,9 +262,9 @@ namespace Gala
{
if (window.get_workspace () == screen.get_active_workspace ()) {
window.activate (screen.get_display ().get_current_time ());
close (true);
close ();
} else {
close (true);
close ();
//wait for the animation to finish before switching
Timeout.add (400, () => {
window.get_workspace ().activate_with_focus (window, screen.get_display ().get_current_time ());
@ -251,7 +273,10 @@ namespace Gala
}
}
void close (bool animate)
/**
* {@inheritDoc}
*/
public void close ()
{
if (!visible || !ready)
return;
@ -264,21 +289,17 @@ namespace Gala
ready = false;
wm.end_modal ();
wm.block_keybindings_in_modal = true;
wm.pop_modal (modal_proxy);
foreach (var child in get_children ()) {
((WindowCloneContainer) child).close ();
}
if (animate) {
Clutter.Threads.Timeout.add (300, () => {
cleanup ();
return false;
});
} else
Clutter.Threads.Timeout.add (300, () => {
cleanup ();
return false;
});
}
void cleanup ()

View File

@ -38,6 +38,7 @@ namespace Gala
BindConstraint h_constraint;
bool closing = false;
ModalProxy modal_proxy;
//estimated value, if possible
float dock_width = 0.0f;
@ -167,7 +168,7 @@ namespace Gala
current_window = null;
}
wm.end_modal ();
wm.pop_modal (modal_proxy);
dock.animate (AnimationMode.EASE_OUT_CUBIC, 250, width:dest_width, opacity : 0).
completed.connect (() => {
@ -464,7 +465,7 @@ namespace Gala
});
closing = false;
wm.begin_modal ();
modal_proxy = wm.push_modal ();
bool backward = (binding.get_name () == "switch-windows-backward");

View File

@ -21,19 +21,36 @@ namespace Gala
{
public class WindowManagerGala : Meta.Plugin, WindowManager
{
/**
* {@inheritDoc}
*/
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 bool block_keybindings_in_modal { get; set; default = true; }
/**
* {@inheritDoc}
*/
public Clutter.Stage stage { get; protected set; }
/**
* {@inheritDoc}
*/
public Clutter.Actor window_group { get; protected set; }
/**
* {@inheritDoc}
*/
public Clutter.Actor top_window_group { get; protected set; }
/**
* {@inheritDoc}
*/
public Meta.BackgroundGroup background_group { get; protected set; }
Meta.PluginInfo info;
WindowSwitcher? winswitcher = null;
MultitaskingView? workspace_view = null;
WindowOverview? window_overview = null;
ActivatableComponent? workspace_view = null;
ActivatableComponent? window_overview = null;
// used to detect which corner was used to trigger an action
Clutter.Actor? last_hotcorner;
@ -41,7 +58,7 @@ namespace Gala
Window? moving; //place for the window that is being moved over
int modal_count = 0; //count of modal modes overlaying each other
Gee.LinkedList<ModalProxy> modal_stack = new Gee.LinkedList<ModalProxy> ();
Gee.HashSet<Meta.WindowActor> minimizing = new Gee.HashSet<Meta.WindowActor> ();
Gee.HashSet<Meta.WindowActor> maximizing = new Gee.HashSet<Meta.WindowActor> ();
@ -203,35 +220,50 @@ namespace Gala
if (plugin_manager.workspace_view_provider == null) {
workspace_view = new MultitaskingView (this);
ui_group.add_child (workspace_view);
KeyBinding.set_custom_handler ("show-desktop", () => {
workspace_view.toggle ();
});
ui_group.add_child ((Clutter.Actor) workspace_view);
} else if (plugin_manager.workspace_view_provider is ActivatableComponent) {
workspace_view = (ActivatableComponent) plugin_manager.get_plugin (plugin_manager.workspace_view_provider);
}
KeyBinding.set_custom_handler ("show-desktop", () => {
if (workspace_view.is_opened ())
workspace_view.close ();
else
workspace_view.open ();
});
if (plugin_manager.window_switcher_provider == null) {
winswitcher = new WindowSwitcher (this);
ui_group.add_child (winswitcher);
//FIXME we have to investigate this. Apparently alt-tab is now bound to switch-applications
// instead of windows, which we should probably handle too
KeyBinding.set_custom_handler ("switch-applications", winswitcher.handle_switch_windows);
KeyBinding.set_custom_handler ("switch-applications-backward", winswitcher.handle_switch_windows);
}
if (plugin_manager.window_overview_provider == null) {
window_overview = new WindowOverview (this);
ui_group.add_child (window_overview);
ui_group.add_child ((Clutter.Actor) window_overview);
screen.get_display ().add_keybinding ("expose-windows", KeybindingSettings.get_default ().schema, 0, () => {
window_overview.open (true);
});
screen.get_display ().add_keybinding ("expose-all-windows", KeybindingSettings.get_default ().schema, 0, () => {
window_overview.open (true, true);
});
} else if (plugin_manager.window_overview_provider is ActivatableComponent) {
window_overview = (ActivatableComponent) plugin_manager.get_plugin (plugin_manager.window_overview_provider);
}
screen.get_display ().add_keybinding ("expose-windows", KeybindingSettings.get_default ().schema, 0, () => {
if (window_overview.is_opened ())
window_overview.close ();
else
window_overview.open ();
});
screen.get_display ().add_keybinding ("expose-all-windows", KeybindingSettings.get_default ().schema, 0, () => {
if (window_overview.is_opened ())
window_overview.close ();
else {
var hints = new Gee.HashMap<string,GLib.Value?> ();
hints.@set ("all-windows", true);
window_overview.open (hints);
}
});
update_input_area ();
stage.show ();
@ -297,6 +329,9 @@ namespace Gala
switch_to_next_workspace (direction);
}
/**
* {@inheritDoc}
*/
public void switch_to_next_workspace (MotionDirection direction)
{
var screen = get_screen ();
@ -348,7 +383,7 @@ namespace Gala
}
}
if (modal_count > 0)
if (is_modal ())
InternalUtils.set_input_area (screen, InputArea.FULLSCREEN);
else
InternalUtils.set_input_area (screen, InputArea.DEFAULT);
@ -378,6 +413,9 @@ namespace Gala
screen.get_workspace_by_index (index).activate (screen.get_display ().get_current_time ());
}
/**
* {@inheritDoc}
*/
public void move_window (Window? window, MotionDirection direction)
{
if (window == null)
@ -403,42 +441,69 @@ namespace Gala
next.activate_with_focus (window, display.get_current_time ());
}
public new void begin_modal ()
/**
* {@inheritDoc}
*/
public ModalProxy push_modal ()
{
modal_count ++;
if (modal_count > 1)
return;
var proxy = new ModalProxy ();
modal_stack.offer_head (proxy);
// modal already active
if (modal_stack.size > 2)
return proxy;
var screen = get_screen ();
var display = screen.get_display ();
var time = screen.get_display ().get_current_time ();
update_input_area ();
#if HAS_MUTTER310
base.begin_modal (0, display.get_current_time ());
begin_modal (0, time);
#else
base.begin_modal (x_get_stage_window (Compositor.get_stage_for_screen (screen)), {}, 0, display.get_current_time ());
begin_modal (x_get_stage_window (Compositor.get_stage_for_screen (screen)), {}, 0, time);
#endif
Meta.Util.disable_unredirect_for_screen (screen);
return proxy;
}
public new void end_modal ()
/**
* {@inheritDoc}
*/
public void pop_modal (ModalProxy proxy)
{
modal_count --;
if (modal_count > 0)
if (!modal_stack.remove (proxy)) {
warning ("Attempted to remove a modal proxy that was not in the stack");
return;
}
if (modal_stack.size > 0)
return;
update_input_area ();
var screen = get_screen ();
base.end_modal (screen.get_display ().get_current_time ());
end_modal (screen.get_display ().get_current_time ());
Meta.Util.enable_unredirect_for_screen (screen);
}
/**
* {@inheritDoc}
*/
public bool is_modal ()
{
return (modal_count > 0);
return modal_stack.size > 0;
}
/**
* {@inheritDoc}
*/
public bool modal_proxy_valid (ModalProxy proxy)
{
return proxy in modal_stack;
}
public void get_current_cursor_position (out int x, out int y)
@ -459,6 +524,9 @@ namespace Gala
win.clear_effects ();*/
}
/**
* {@inheritDoc}
*/
public void perform_action (ActionType type)
{
var screen = get_screen ();
@ -467,7 +535,13 @@ namespace Gala
switch (type) {
case ActionType.SHOW_WORKSPACE_VIEW:
workspace_view.toggle ();
if (workspace_view == null)
break;
if (workspace_view.is_opened ())
workspace_view.close ();
else
workspace_view.open ();
break;
case ActionType.MAXIMIZE_CURRENT:
if (current == null || current.window_type != WindowType.NORMAL)
@ -518,10 +592,25 @@ namespace Gala
}
break;
case ActionType.WINDOW_OVERVIEW:
window_overview.open (true);
if (window_overview == null)
break;
if (window_overview.is_opened ())
window_overview.close ();
else
window_overview.open ();
break;
case ActionType.WINDOW_OVERVIEW_ALL:
window_overview.open (true, true);
if (window_overview == null)
break;
if (window_overview.is_opened ())
window_overview.close ();
else {
var hints = new Gee.HashMap<string,GLib.Value?> ();
hints.@set ("all-windows", true);
window_overview.open (hints);
}
break;
default:
warning ("Trying to run unknown action");
@ -1149,7 +1238,11 @@ namespace Gala
public override bool keybinding_filter (Meta.KeyBinding binding)
{
return block_keybindings_in_modal && modal_count > 0;
var modal_proxy = modal_stack.peek_head ();
return is_modal ()
&& modal_proxy.keybinding_filter != null
&& modal_proxy.keybinding_filter (binding);
}
#if HAS_MUTTER310