1# -*- coding: utf-8 -*- 2# 3# Caribou - text entry and UI navigation application 4# 5# Copyright (C) 2009 Eitan Isaacson <eitan@monotonous.org> 6# Copyright (C) 2010 Warp Networks S.L. 7# * Contributor: Daniel Baeyens <dbaeyens@warp.es> 8# 9# This program is free software; you can redistribute it and/or modify it 10# under the terms of the GNU Lesser General Public License as published by the 11# Free Software Foundation; either version 2.1 of the License, or (at your 12# option) any later version. 13# 14# This program is distributed in the hope that it will be useful, but WITHOUT 15# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License 17# for more details. 18# 19# You should have received a copy of the GNU Lesser General Public License 20# along with this program; if not, write to the Free Software Foundation, 21# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 22 23import gi 24gi.require_version('Clutter', '1.0') 25from gi.repository import Gtk 26from gi.repository import Gdk 27from gi.repository import GObject 28from gi.repository import Clutter 29from .antler_settings import AntlerSettings 30from math import sqrt 31import os 32import sys 33 34 35class AnimatedWindowBase(Gtk.Window, Clutter.Animatable): 36 __gproperties__ = { 37 'antler-window-x' : (GObject.TYPE_INT, 'Window position', 38 'Window X coordinate', 39 GObject.G_MININT, GObject.G_MAXINT, 0, 40 GObject.PARAM_READWRITE), 41 'antler-window-y' : (GObject.TYPE_INT, 'Window position', 42 'Window Y coordinate', 43 GObject.G_MININT, GObject.G_MAXINT, 0, 44 GObject.PARAM_READWRITE) 45 } 46 def __init__(self): 47 GObject.GObject.__init__(self, type=Gtk.WindowType.POPUP) 48 Clutter.init(None) 49 50 # animation 51 self._stage = Clutter.Stage.get_default() 52 self._move_animation = None 53 self._opacity_animation = None 54 55 def do_get_property(self, property): 56 if property.name == "antler-window-x": 57 return self.get_position()[0] 58 elif property.name == "antler-window-y": 59 return self.get_position()[1] 60 else: 61 raise AttributeError('unknown property %s' % property.name) 62 63 def do_set_property(self, property, value): 64 if property.name == "antler-window-x": 65 if value is not None: 66 self.move(value, self.get_position()[1]) 67 elif property.name == "antler-window-y": 68 if value is not None: 69 self.move(self.get_position()[0], value) 70 else: 71 raise AttributeError('unknown property %s' % property.name) 72 73 def do_animate_property(self, animation, prop_name, initial_value, 74 final_value, progress, gvalue): 75 if prop_name == "antler-window-x": 76 dx = int(initial_value * progress) 77 self.move(initial_value + dx, self.get_position()[1]) 78 return True 79 elif prop_name == "antler-window-y": 80 dy = int(initial_value * progress) 81 self.move(self.get_position()[0], initial_value + dy) 82 return True 83 if prop_name == "opacity": 84 opacity = initial_value + ((final_value - initial_value) * progress) 85 GObject.idle_add(lambda: self.set_opacity(opacity)) 86 return True 87 else: 88 return False 89 90 def animated_move(self, x, y, mode=Clutter.AnimationMode.EASE_OUT_CUBIC): 91 self._move_animation = Clutter.Animation(object=self, 92 mode=mode, 93 duration=250) 94 self._move_animation.bind("antler-window-x", x) 95 self._move_animation.bind("antler-window-y", y) 96 97 timeline = self._move_animation.get_timeline() 98 timeline.start() 99 100 return self._move_animation 101 102 def animated_opacity(self, opacity, mode=Clutter.AnimationMode.EASE_OUT_CUBIC): 103 if opacity == self.get_opacity(): 104 return None 105 if self._opacity_animation is not None: 106 if self._opacity_animation.has_property('opacity'): 107 timeline = self._opacity_animation.get_timeline() 108 timeline.pause() 109 self._opacity_animation.unbind_property('opacity') 110 111 self._opacity_animation = Clutter.Animation(object=self, mode=mode, 112 duration=100) 113 self._opacity_animation.bind("opacity", opacity) 114 115 timeline = self._opacity_animation.get_timeline() 116 timeline.start() 117 118 return self._opacity_animation 119 120 121class ProximityWindowBase(AnimatedWindowBase): 122 def __init__(self): 123 AnimatedWindowBase.__init__(self) 124 self._poll_tid = 0 125 settings = AntlerSettings() 126 self.max_distance = settings.max_distance.value 127 settings.max_distance.connect("value-changed", self._on_max_dist_changed) 128 min_alpha = settings.min_alpha 129 max_alpha = settings.max_alpha 130 min_alpha.connect("value-changed", 131 self._on_min_alpha_changed, max_alpha) 132 max_alpha.connect("value-changed", 133 self._on_max_alpha_changed, min_alpha) 134 self.connect('map-event', self._onmapped, settings) 135 136 def _on_max_dist_changed(self, setting, value): 137 self.max_distance = value 138 139 def _set_min_max_alpha(self, min_alpha, max_alpha): 140 if min_alpha > max_alpha: 141 min_alpha = max_alpha 142 self.max_alpha = max_alpha 143 self.min_alpha = min_alpha 144 if self.max_alpha != self.min_alpha: 145 if self._poll_tid == 0: 146 self._poll_tid = GObject.timeout_add(100, self._proximity_check) 147 elif self._poll_tid != 0: 148 GObject.source_remove(self._poll_tid) 149 150 def _onmapped(self, obj, event, settings): 151 if self.is_composited(): 152 self._set_min_max_alpha(settings.min_alpha.value, 153 settings.max_alpha.value) 154 self._proximity_check() 155 156 def _on_min_alpha_changed(self, setting, value, max_alpha): 157 self._set_min_max_alpha(value, max_alpha.value) 158 159 def _on_max_alpha_changed(self, setting, value, min_alpha): 160 self._set_min_max_alpha(min_alpha.value, value) 161 162 def _proximity_check(self): 163 px, py = self.get_pointer() 164 165 ww = self.get_allocated_width() 166 wh = self.get_allocated_height() 167 168 distance = self._get_distance_to_bbox(px, py, ww, wh) 169 170 opacity = (self.max_alpha - self.min_alpha) * \ 171 (1 - min(distance, self.max_distance)/self.max_distance) 172 opacity += self.min_alpha 173 174 self.animated_opacity(opacity) 175 176 if not self.props.visible: 177 self._poll_tid = 0 178 return False 179 180 return True 181 182 def _get_distance_to_bbox(self, px, py, bw, bh): 183 if px < 0: 184 x_distance = float(abs(px)) 185 elif px > bw: 186 x_distance = float(px - bw) 187 else: 188 x_distance = 0.0 189 190 if py < 0: 191 y_distance = float(abs(py)) 192 elif py > bh: 193 y_distance = float(py - bh) 194 else: 195 y_distance = 0.0 196 197 if y_distance == 0 and x_distance == 0: 198 return 0.0 199 elif y_distance != 0 and x_distance == 0: 200 return y_distance 201 elif y_distance == 0 and x_distance != 0: 202 return x_distance 203 else: 204 x2 = 0 if x_distance > 0 else bw 205 y2 = 0 if y_distance > 0 else bh 206 return sqrt((px - x2)**2 + (py - y2)**2) 207 208class AntlerWindow(ProximityWindowBase): 209 def __init__(self, keyboard_view_factory, placement=None, 210 min_alpha=1.0, max_alpha=1.0, max_distance=100): 211 ProximityWindowBase.__init__(self) 212 213 self.set_name("AntlerWindow") 214 215 ctx = self.get_style_context() 216 ctx.add_class("antler-keyboard-window") 217 218 settings = AntlerSettings() 219 settings.keyboard_type.connect('value-changed', self.on_kb_type_changed) 220 221 self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 222 self.add(self._vbox) 223 224 self.keyboard_view_factory = keyboard_view_factory 225 self.keyboard_view = keyboard_view_factory (keyboard_type=settings.keyboard_type.value) 226 227 self._vbox.pack_start(self.keyboard_view, True, True, 0) 228 229 self.connect("size-allocate", self.on_size_allocate) 230 231 self._cursor_location = Rectangle() 232 self._entry_location = Rectangle() 233 self.placement = placement or \ 234 AntlerWindowPlacement() 235 236 def on_kb_type_changed(self, setting, value): 237 self._vbox.remove(self.keyboard_view) 238 self.resize(1, 1) 239 self.keyboard_view = self.keyboard_view_factory (value) 240 self._vbox.pack_start(self.keyboard_view, True, True, 0) 241 self.keyboard_view.show_all() 242 243 def on_size_allocate(self, widget, allocation): 244 self._update_position() 245 246 def destroy(self): 247 self.keyboard.destroy() 248 super(Gtk.Window, self).destroy() 249 250 def set_cursor_location(self, x, y, w, h): 251 self._cursor_location = Rectangle(x, y, w, h) 252 self._update_position() 253 254 def set_entry_location(self, x, y, w, h): 255 self._entry_location = Rectangle(x, y, w, h) 256 self._update_position() 257 258 def set_placement(self, placement): 259 self.placement = placement 260 self._update_position() 261 262 def _get_root_bbox(self): 263 root_window = Gdk.get_default_root_window() 264 args = root_window.get_geometry() 265 266 root_bbox = Rectangle(*args) 267 268 # TODO: Do whatever we need to do to place the keyboard correctly 269 # in GNOME Shell and Unity. 270 # 271 272 return root_bbox 273 274 def _calculate_position(self, placement=None): 275 root_bbox = self._get_root_bbox() 276 placement = placement or self.placement 277 278 x = self._calculate_axis(placement.x, root_bbox) 279 y = self._calculate_axis(placement.y, root_bbox) 280 281 return x, y 282 283 def get_expected_position(self): 284 x, y = self._calculate_position() 285 origx, origy = x, y 286 root_bbox = self._get_root_bbox() 287 proposed_position = Rectangle(x, y, self.get_allocated_width(), 288 self.get_allocated_height()) 289 290 x += self.placement.x.adjust_to_bounds(root_bbox, proposed_position) 291 y += self.placement.y.adjust_to_bounds(root_bbox, proposed_position) 292 return self.get_position() != (x, y) != y, x, y 293 294 def _update_position(self): 295 changed, x, y = self.get_expected_position() 296 if changed: 297 self.move(x, y) 298 299 def _calculate_axis(self, axis_placement, root_bbox): 300 bbox = root_bbox 301 302 if axis_placement.stickto == AntlerWindowPlacement.CURSOR: 303 bbox = self._cursor_location 304 elif axis_placement.stickto == AntlerWindowPlacement.ENTRY: 305 bbox = self._entry_location 306 307 offset = axis_placement.get_offset(bbox.x, bbox.y) 308 309 if axis_placement.align == AntlerWindowPlacement.END: 310 offset += axis_placement.get_length(bbox.width, bbox.height) 311 if axis_placement.gravitate == AntlerWindowPlacement.INSIDE: 312 offset -= axis_placement.get_length( 313 self.get_allocated_width(), 314 self.get_allocated_height()) 315 elif axis_placement.align == AntlerWindowPlacement.START: 316 if axis_placement.gravitate == AntlerWindowPlacement.OUTSIDE: 317 offset -= axis_placement.get_length( 318 self.get_allocated_width(), 319 self.get_allocated_height()) 320 elif axis_placement.align == AntlerWindowPlacement.CENTER: 321 offset += axis_placement.get_length(bbox.width, bbox.height)/2 322 323 return offset 324 325class AntlerWindowDocked(AntlerWindow): 326 def __init__(self, keyboard_view_factory, horizontal_roll=False): 327 placement = AntlerWindowPlacement( 328 xalign=AntlerWindowPlacement.START, 329 yalign=AntlerWindowPlacement.END, 330 xstickto=AntlerWindowPlacement.SCREEN, 331 ystickto=AntlerWindowPlacement.SCREEN, 332 xgravitate=AntlerWindowPlacement.INSIDE) 333 334 AntlerWindow.__init__(self, keyboard_view_factory, placement) 335 336 self.horizontal_roll = horizontal_roll 337 self._rolled_in = False 338 339 340 def show_all(self): 341 super(AntlerWindow, self).show_all() 342 343 def on_size_allocate(self, widget, allocation): 344 self._roll_in() 345 346 def _roll_in(self): 347 if self._rolled_in: 348 return 349 self._rolled_in = True 350 351 x, y = self._get_preroll_position() 352 self.move(x, y) 353 354 x, y = self._get_postroll_position() 355 return self.animated_move(x, y) 356 357 def _get_preroll_position(self): 358 _, x, y = self.get_expected_position() 359 360 if self.horizontal_roll: 361 newy = y 362 if self.placement.x.align == AntlerWindowPlacement.END: 363 newx = x + self.get_allocated_width() 364 else: 365 newx = x - self.get_allocated_width() 366 else: 367 newx = x 368 if self.placement.y.align == AntlerWindowPlacement.END: 369 newy = y + self.get_allocated_height() 370 else: 371 newy = y - self.get_allocated_height() 372 373 return newx, newy 374 375 def _get_postroll_position(self): 376 x, y = self.get_position() 377 378 if self.horizontal_roll: 379 newy = y 380 if self.placement.x.align != AntlerWindowPlacement.END: 381 newx = x + self.get_allocated_width() 382 else: 383 newx = x - self.get_allocated_width() 384 else: 385 newx = x 386 if self.placement.y.align != AntlerWindowPlacement.END: 387 newy = y + self.get_allocated_height() 388 else: 389 newy = y - self.get_allocated_height() 390 391 return newx, newy 392 393 def _roll_out(self): 394 if not self._rolled_in: 395 return 396 self._rolled_in = False; 397 x, y = self.get_position() 398 return self.animated_move(x + self.get_allocated_width(), y) 399 400 def hide(self): 401 animation = self._roll_out() 402 animation.connect('completed', lambda x: AntlerWindow.hide(self)) 403 404class AntlerWindowEntry(AntlerWindow): 405 def __init__(self, keyboard_view_factory): 406 placement = AntlerWindowPlacement( 407 xalign=AntlerWindowPlacement.START, 408 xstickto=AntlerWindowPlacement.ENTRY, 409 ystickto=AntlerWindowPlacement.ENTRY, 410 xgravitate=AntlerWindowPlacement.INSIDE, 411 ygravitate=AntlerWindowPlacement.OUTSIDE) 412 413 AntlerWindow.__init__(self, keyboard_view_factory, placement) 414 415 416 def _calculate_axis(self, axis_placement, root_bbox): 417 offset = AntlerWindow._calculate_axis(self, axis_placement, root_bbox) 418 if axis_placement.axis == 'y': 419 if offset + self.get_allocated_height() > root_bbox.height + root_bbox.y: 420 new_axis_placement = axis_placement.copy(align=AntlerWindowPlacement.START) 421 offset = AntlerWindow._calculate_axis(self, new_axis_placement, root_bbox) 422 423 return offset 424 425class AntlerWindowPlacement(object): 426 START = 'start' 427 END = 'end' 428 CENTER = 'center' 429 430 SCREEN = 'screen' 431 ENTRY = 'entry' 432 CURSOR = 'cursor' 433 434 INSIDE = 'inside' 435 OUTSIDE = 'outside' 436 437 class _AxisPlacement(object): 438 def __init__(self, axis, align, stickto, gravitate): 439 self.axis = axis 440 self.align = align 441 self.stickto = stickto 442 self.gravitate = gravitate 443 444 def copy(self, align=None, stickto=None, gravitate=None): 445 return self.__class__(self.axis, 446 align or self.align, 447 stickto or self.stickto, 448 gravitate or self.gravitate) 449 450 def get_offset(self, x, y): 451 return x if self.axis == 'x' else y 452 453 def get_length(self, width, height): 454 return width if self.axis == 'x' else height 455 456 def adjust_to_bounds(self, root_bbox, child_bbox): 457 child_vector_start = self.get_offset(child_bbox.x, child_bbox.y) 458 child_vector_end = \ 459 self.get_length(child_bbox.width, child_bbox.height) + \ 460 child_vector_start 461 root_vector_start = self.get_offset(root_bbox.x, root_bbox.y) 462 root_vector_end = self.get_length( 463 root_bbox.width, root_bbox.height) + root_vector_start 464 465 if root_vector_end < child_vector_end: 466 return root_vector_end - child_vector_end 467 468 if root_vector_start > child_vector_start: 469 return root_vector_start - child_vector_start 470 471 return 0 472 473 474 def __init__(self, 475 xalign=None, xstickto=None, xgravitate=None, 476 yalign=None, ystickto=None, ygravitate=None): 477 self.x = self._AxisPlacement('x', 478 xalign or self.END, 479 xstickto or self.CURSOR, 480 xgravitate or self.OUTSIDE) 481 self.y = self._AxisPlacement('y', 482 yalign or self.END, 483 ystickto or self.CURSOR, 484 ygravitate or self.OUTSIDE) 485 486class Rectangle(object): 487 def __init__(self, x=0, y=0, width=0, height=0): 488 self.x = x 489 self.y = y 490 self.width = width 491 self.height = height 492 493if __name__ == "__main__": 494 import keyboard_view 495 import signal 496 signal.signal(signal.SIGINT, signal.SIG_DFL) 497 498 w = AntlerWindowDocked(keyboard_view.AntlerKeyboardView) 499 w.show_all() 500 501 try: 502 Gtk.main() 503 except KeyboardInterrupt: 504 Gtk.main_quit() 505