Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion quicktile/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ def main(): # type: () -> None
winman.screen.force_update()

for arg in args:
commands.commands.call(arg, winman)
commands.commands.call_multiple(arg, winman)
while gtk.events_pending(): # pylint: disable=no-member
gtk.main_iteration() # pylint: disable=no-member
elif not opts.show_args and not opts.show_binds:
Expand Down
148 changes: 84 additions & 64 deletions quicktile/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ def get_window_meta(window, state, winman):
# FIXME: Make calls to win.get_* lazy in case --debug
# wasn't passed.
logging.debug("Operating on window %r with title \"%s\" "
"and geometry %r",
window, window.get_name(),
window.get_geometry())
"and geometry %r",
window, window.get_name(),
window.get_geometry())

monitor_id, monitor_geom = winman.get_monitor(window)
use_area, use_rect = winman.workarea.get(monitor_geom)
Expand All @@ -68,16 +68,16 @@ def get_window_meta(window, state, winman):
# comprehensive exception catcher.
if not use_rect:
logging.debug("Received a worthless value for largest "
"rectangular subset of desktop (%r). Doing "
"nothing.", use_rect)
"rectangular subset of desktop (%r). Doing "
"nothing.", use_rect)
return False

state.update({
"monitor_id": monitor_id,
"monitor_geom": monitor_geom,
"usable_region": use_area,
"usable_rect": use_rect,
})
})
return True

def add(self, name, *p_args, **p_kwargs):
Expand All @@ -103,10 +103,10 @@ def decorate(func): # type: (CommandCB) -> CommandCB
@wraps(func)
# pylint: disable=missing-docstring
def wrapper(winman, # type: WindowManager
window=None, # type: wnck.Window
*args,
**kwargs
): # type: (...) -> None
window=None, # type: wnck.Window
*args,
**kwargs
): # type: (...) -> None

window = window or winman.screen.get_active_window()

Expand All @@ -121,7 +121,7 @@ def wrapper(winman, # type: WindowManager

# Bail out early on None or things like the desktop window
if not (windowless or self.get_window_meta(
window, state, winman)):
window, state, winman)):
logging.debug("No window and windowless=False")
return None

Expand All @@ -140,8 +140,8 @@ def wrapper(winman, # type: WindowManager

if not func.__doc__:
raise AssertionError("All commands must have a docstring: "
"%r" % func)
help_str = func.__doc__.strip().split('\n')[0].split('. ')[0]
"%r" % func)
help_str = func.__doc__.strip().split('\n')[0].split('. ')[0]
self.help[name] = help_str.strip('.')

# Return the unwrapped function so decorators can be stacked
Expand All @@ -168,13 +168,13 @@ def decorate(func):
return decorate

def call(self, command, winman, *args, **kwargs):
"""Check if the command is valid and execute it."""
# type: (str, WindowManager, *Any, **Any) -> bool
"""Resolve a textual positioning command and execute it."""

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ssokolow@monolith quicktile [master] % ./run_tests.sh 
[...]
quicktile/commands.py:172: error: misplaced type annotation

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! That's done.

cmd = self.commands.get(command, None)

if cmd:
logging.debug("Executing command '%s' with arguments %r, %r",
command, args, kwargs)
command, args, kwargs)
cmd(winman, *args, **kwargs)

# TODO: Allow commands to report success or failure
Expand All @@ -183,15 +183,30 @@ def call(self, command, winman, *args, **kwargs):
logging.error("Unrecognized command: %s", command)
return False

def call_multiple(self, command, winman, *args, **kwargs):
"""Resolve a textual positioning command and execute it.
Accepts a comma seperated list as the command."""
# type: (str, WindowManager, *Any, **Any) -> bool

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll probably need another day or two.

If I'm seeing List[str] turn into str in type signatures without any explanation (whether it's a correction or a mistake), it suggests that, as a responsible project maintainer, I need to do a full audit before merging to make sure I wouldn't be letting in any new bugs.

cmds = []
success = True
if ',' in command:
cmds = [i.strip() for i in command.split(',')]
for cmd in cmds:
success = self.call(cmd, winman, *args, **kwargs)
else:
return self.call(command, winman, *args, **kwargs)

return success


#: The instance of L{CommandRegistry} to be used in 99.9% of use cases.
commands = CommandRegistry()

def cycle_dimensions(winman, # type: WindowManager
win, # type: Any # TODO: Consistent Window type
state, # type: Dict[str, Any]
*dimensions # type: Any
): # type: (...) -> Optional[gtk.gdk.Rectangle]
win, # type: Any # TODO: Consistent Window type
state, # type: Dict[str, Any]
*dimensions # type: Any
): # type: (...) -> Optional[gtk.gdk.Rectangle]
# type: (WindowManager, Any, Dict[str, Any], *Tuple[...]) ->
# TODO: Standardize on what kind of window object to pass around
"""Cycle the active window through a list of positions and shapes.
Expand Down Expand Up @@ -223,7 +238,7 @@ def cycle_dimensions(winman, # type: WindowManager
return None

logging.debug("Selected preset sequence resolves to these monitor-relative"
" pixel dimensions:\n\t%r", dims)
" pixel dimensions:\n\t%r", dims)

try:
cmd_idx, pos = winman.get_property('_QUICKTILE_CYCLE_POS', win)[2]
Expand All @@ -236,11 +251,11 @@ def cycle_dimensions(winman, # type: WindowManager
pos = 0

winman.set_property('_QUICKTILE_CYCLE_POS',
(state.get('cmd_idx', 0), pos), win)
(state.get('cmd_idx', 0), pos), win)
result = gtk.gdk.Rectangle(*dims[pos])

logging.debug("Target preset is %s relative to monitor %s",
result, clip_box)
result, clip_box)
result.x += clip_box.x
result.y += clip_box.y

Expand All @@ -250,25 +265,25 @@ def cycle_dimensions(winman, # type: WindowManager
if not usable_region.rect_in(result) == gtk.gdk.OVERLAP_RECTANGLE_IN:
result = result.intersect(state['usable_rect'])
logging.debug("Result exceeds usable (non-rectangular) region of "
"desktop. (overlapped a non-fullwidth panel?) Reducing "
"to within largest usable rectangle: %s",
state['usable_rect'])
"desktop. (overlapped a non-fullwidth panel?) Reducing "
"to within largest usable rectangle: %s",
state['usable_rect'])

logging.debug("Calling reposition() with default gravity and dimensions "
"%r", tuple(result))
winman.reposition(win, result)
logging.debug("Calling reposition() with default gravity and dimensions "
"%r", tuple(result))
winman.reposition(win, result)
return result

@commands.add('monitor-switch', force_wrap=True)
@commands.add('monitor-next', 1)
@commands.add('monitor-prev', -1)
def cycle_monitors(winman, # type: WindowManager
win, # type: wnck.Window
state, # type: Dict[str, Any]
step=1, # type: int
force_wrap=False, # type: bool
n_monitors=None # type: Optional[int]
): # type: (...) -> None
win, # type: wnck.Window
state, # type: Dict[str, Any]
step=1, # type: int
force_wrap=False, # type: bool
n_monitors=None # type: Optional[int]
): # type: (...) -> None
"""Cycle the active window between monitors while preserving position.

@todo 1.0.0: Remove C{monitor-switch} in favor of C{monitor-next}
Expand All @@ -278,12 +293,12 @@ def cycle_monitors(winman, # type: WindowManager
n_monitors = n_monitors or winman.gdk_screen.get_n_monitors()

new_mon_id = clamp_idx(mon_id + step, n_monitors,
state['config'].getboolean('general', 'MovementsWrap') or
force_wrap)
state['config'].getboolean('general', 'MovementsWrap') or
force_wrap)

new_mon_geom = winman.gdk_screen.get_monitor_geometry(new_mon_id)
logging.debug("Moving window to monitor %s, which has geometry %s",
new_mon_id, new_mon_geom)
new_mon_id, new_mon_geom)

winman.reposition(win, None, new_mon_geom, keep_maximize=True)

Expand All @@ -305,48 +320,53 @@ def cycle_monitors_all(winman, win, state, step=1, force_wrap=False):

# pylint: disable=no-member
MOVE_TO_COMMANDS = {
'move-to-top-left': [wnck.WINDOW_GRAVITY_NORTHWEST,
wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y],
'move-to-top': [wnck.WINDOW_GRAVITY_NORTH, wnck.WINDOW_CHANGE_Y],
'move-to-top-right': [wnck.WINDOW_GRAVITY_NORTHEAST,
wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y],
'move-to-left': [wnck.WINDOW_GRAVITY_WEST, wnck.WINDOW_CHANGE_X],
'move-to-center': [wnck.WINDOW_GRAVITY_CENTER,
wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y],
'move-to-right': [wnck.WINDOW_GRAVITY_EAST, wnck.WINDOW_CHANGE_X],
'move-to-bottom-left': [wnck.WINDOW_GRAVITY_SOUTHWEST,
wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y],
'move-to-bottom': [wnck.WINDOW_GRAVITY_SOUTH, wnck.WINDOW_CHANGE_Y],
'move-to-bottom-right': [wnck.WINDOW_GRAVITY_SOUTHEAST,
wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y],
}
'move-to-top-left': [wnck.WINDOW_GRAVITY_NORTHWEST,
wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y],
'move-to-top': [wnck.WINDOW_GRAVITY_NORTH, wnck.WINDOW_CHANGE_Y],
'move-to-top-right': [wnck.WINDOW_GRAVITY_NORTHEAST,
wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y],
'move-to-left': [wnck.WINDOW_GRAVITY_WEST, wnck.WINDOW_CHANGE_X],
'move-to-center': [wnck.WINDOW_GRAVITY_CENTER,
wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y],
'move-to-right': [wnck.WINDOW_GRAVITY_EAST, wnck.WINDOW_CHANGE_X],
'move-to-bottom-left': [wnck.WINDOW_GRAVITY_SOUTHWEST,
wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y],
'move-to-bottom': [wnck.WINDOW_GRAVITY_SOUTH, wnck.WINDOW_CHANGE_Y],
'move-to-bottom-right': [wnck.WINDOW_GRAVITY_SOUTHEAST,
wnck.WINDOW_CHANGE_X | wnck.WINDOW_CHANGE_Y],
}

@commands.add_many(MOVE_TO_COMMANDS)
def move_to_position(winman, # type: WindowManager
win, # type: Any # TODO: Make this specific
state, # type: Dict[str, Any]
gravity, # type: Any # TODO: Make this specific
gravity_mask # type: wnck.WindowMoveResizeMask
): # type: (...) -> None # TODO: Decide on a return type
win, # type: Any # TODO: Make this specific
state, # type: Dict[str, Any]
gravity, # type: Any # TODO: Make this specific
gravity_mask # type: wnck.WindowMoveResizeMask
): # type: (...) -> None # TODO: Decide on a return type
"""Move window to a position on the screen, preserving its dimensions."""
use_rect = state['usable_rect']

grav_x, grav_y = GRAVITY[gravity]
dims = (int(use_rect.width * grav_x), int(use_rect.height * grav_y), 0, 0)
result = gtk.gdk.Rectangle(*dims)
logging.debug("Calling reposition() with %r gravity and dimensions %r",
gravity, tuple(result))
gravity, tuple(result))

# pylint: disable=no-member
winman.reposition(win, result, use_rect, gravity=gravity,
geometry_mask=gravity_mask)

@commands.add('bordered-set', True)
@commands.add('bordered-unset', False)
@commands.add('bordered')
def toggle_decorated(winman, win, state): # pylint: disable=unused-argument
def toggle_decorated(winman, win, state, decoration=None): # pylint: disable=unused-argument
# type: (WindowManager, wnck.Window, Any) -> None
"""Toggle window decoration state on the active window."""
win = gtk.gdk.window_foreign_new(win.get_xid())
win.set_decorations(not win.get_decorations())
if decoration is not None:
win.set_decorations(decoration)
else:
win.set_decorations(not win.get_decorations())

@commands.add('show-desktop', windowless=True)
def toggle_desktop(winman, win, state): # pylint: disable=unused-argument
Expand All @@ -358,9 +378,9 @@ def toggle_desktop(winman, win, state): # pylint: disable=unused-argument
@commands.add('all-desktops', 'pin', 'is_pinned')
@commands.add('fullscreen', 'set_fullscreen', 'is_fullscreen', True)
@commands.add('vertical-maximize', 'maximize_vertically',
'is_maximized_vertically')
'is_maximized_vertically')
@commands.add('horizontal-maximize', 'maximize_horizontally',
'is_maximized_horizontally')
'is_maximized_horizontally')
@commands.add('maximize', 'maximize', 'is_maximized')
@commands.add('minimize', 'minimize', 'is_minimized')
@commands.add('always-above', 'make_above', 'is_above')
Expand Down Expand Up @@ -411,7 +431,7 @@ def workspace_go(winman, win, state, motion): # pylint: disable=W0613
# type: (WindowManager, wnck.Window, Any, wnck.MotionDirection) -> None
"""Switch the active workspace (next/prev wrap around)"""
target = winman.get_workspace(None, motion,
wrap_around=state['config'].getboolean('general', 'MovementsWrap'))
wrap_around=state['config'].getboolean('general', 'MovementsWrap'))
if not target:
logging.debug("Couldn't get the active workspace.")
return
Expand All @@ -430,7 +450,7 @@ def workspace_send_window(winman, win, state, motion):
# type: (WindowManager, wnck.Window, Any, wnck.MotionDirection) -> None
"""Move the active window to another workspace (next/prev wrap around)"""
target = winman.get_workspace(win, motion,
wrap_around=state['config'].getboolean('general', 'MovementsWrap'))
wrap_around=state['config'].getboolean('general', 'MovementsWrap'))
if not target:
return # It's either pinned, on no workspaces, or there is no match

Expand Down
4 changes: 2 additions & 2 deletions quicktile/dbus_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ def doCommand(self, command): # type: (str) -> bool
"""Execute a QuickTile tiling command

@todo 1.0.0: Expose a proper, introspectable D-Bus API"""
return self.commands.call(command, self.winman)
# FIXME: self.commands.call always returns None
return self.commands.call_multiple(command, self.winman)
# FIXME: self.commands.call_multiple always returns None

def init(commands, # type: CommandRegistry
winman # type: WindowManager
Expand Down
2 changes: 1 addition & 1 deletion quicktile/keybinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def init(modmask, # type: Optional[str]
def call(func=func):
"""Closure to resolve `func` and call it on a
`WindowManager` instance"""
commands.call(func, winman)
commands.call_multiple(func, winman)

keybinder.bind(modmask + key, call)
return keybinder