Beispiel #1
0
    def __init__(self):
        Gtk.DrawingArea.__init__(self)
        WindowManipulator.__init__(self)

        self.active_key = None

        self._active_event_type = None
        self._last_click_time = 0
        self._last_click_key = None

        self._outside_click_timer = Timer()
        self._outside_click_detected = False
        self._outside_click_start_time = None

        self._long_press_timer = Timer()
        self._auto_release_timer = AutoReleaseTimer(self)

        self.dwell_timer = None
        self.dwell_key = None
        self.last_dwelled_key = None

        self._window_fade = FadeTimer()
        self._last_transition = None
        self.inactivity_timer = InactivityTimer(self)
        self.auto_show = AtspiAutoShow(self)
        self.auto_show.enable(config.is_auto_show_enabled())

        self.touch_handles = TouchHandles()
        self.touch_handles_hide_timer = Timer()
        self.touch_handles_fade = FadeTimer()
        self.touch_handles_auto_hide = True

        self._aspect_ratio = None
        self._first_draw = True

        # self.set_double_buffered(False)
        self.set_app_paintable(True)

        # no tooltips when embedding, gnome-screen-saver flickers (Oneiric)
        if not config.xid_mode:
            self.set_has_tooltip(True) # works only at window creation -> always on

        self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK
                        | Gdk.EventMask.BUTTON_RELEASE_MASK
                        | Gdk.EventMask.POINTER_MOTION_MASK
                        | Gdk.EventMask.LEAVE_NOTIFY_MASK
                        | Gdk.EventMask.ENTER_NOTIFY_MASK
                        )

        self.connect("parent-set",           self._on_parent_set)
        self.connect("draw",                 self._on_draw)
        self.connect("button-press-event",   self._on_mouse_button_press)
        self.connect("button_release_event", self._on_mouse_button_release)
        self.connect("motion-notify-event",  self._on_motion)
        self.connect("query-tooltip",        self._on_query_tooltip)
        self.connect("enter-notify-event",   self._on_mouse_enter)
        self.connect("leave-notify-event",   self._on_mouse_leave)
        self.connect("configure-event",      self._on_configure_event)

        self.update_resize_handles()

        self.show()
Beispiel #2
0
class KeyboardGTK(Gtk.DrawingArea, WindowManipulator):

    def __init__(self):
        Gtk.DrawingArea.__init__(self)
        WindowManipulator.__init__(self)

        self.active_key = None

        self._active_event_type = None
        self._last_click_time = 0
        self._last_click_key = None

        self._outside_click_timer = Timer()
        self._outside_click_detected = False
        self._outside_click_start_time = None

        self._long_press_timer = Timer()
        self._auto_release_timer = AutoReleaseTimer(self)

        self.dwell_timer = None
        self.dwell_key = None
        self.last_dwelled_key = None

        self._window_fade = FadeTimer()
        self._last_transition = None
        self.inactivity_timer = InactivityTimer(self)
        self.auto_show = AtspiAutoShow(self)
        self.auto_show.enable(config.is_auto_show_enabled())

        self.touch_handles = TouchHandles()
        self.touch_handles_hide_timer = Timer()
        self.touch_handles_fade = FadeTimer()
        self.touch_handles_auto_hide = True

        self._aspect_ratio = None
        self._first_draw = True

        # self.set_double_buffered(False)
        self.set_app_paintable(True)

        # no tooltips when embedding, gnome-screen-saver flickers (Oneiric)
        if not config.xid_mode:
            self.set_has_tooltip(True) # works only at window creation -> always on

        self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK
                        | Gdk.EventMask.BUTTON_RELEASE_MASK
                        | Gdk.EventMask.POINTER_MOTION_MASK
                        | Gdk.EventMask.LEAVE_NOTIFY_MASK
                        | Gdk.EventMask.ENTER_NOTIFY_MASK
                        )

        self.connect("parent-set",           self._on_parent_set)
        self.connect("draw",                 self._on_draw)
        self.connect("button-press-event",   self._on_mouse_button_press)
        self.connect("button_release_event", self._on_mouse_button_release)
        self.connect("motion-notify-event",  self._on_motion)
        self.connect("query-tooltip",        self._on_query_tooltip)
        self.connect("enter-notify-event",   self._on_mouse_enter)
        self.connect("leave-notify-event",   self._on_mouse_leave)
        self.connect("configure-event",      self._on_configure_event)

        self.update_resize_handles()

        self.show()

    def initial_update(self):
        pass

    def _on_parent_set(self, widget, old_parent):
        win = self.get_kbd_window()
        if win:
            self.touch_handles.set_window(win)

    def cleanup(self):
        # stop timer callbacks for unused, but not yet destructed keyboards
        self.touch_handles_fade.stop()
        self.touch_handles_hide_timer.stop()
        self._window_fade.stop()
        self.inactivity_timer.stop()
        self._long_press_timer.stop()
        self._auto_release_timer.stop()
        self.auto_show.cleanup()
        self.stop_click_polling()

    def set_startup_visibility(self):
        win = self.get_kbd_window()
        assert(win)

        # Show the keyboard when turning off auto-show.
        # Hide the keyboard when turning on auto-show.
        #   (Fix this when we know how to get the active accessible)
        # Hide the keyboard on start when start-minimized is set.
        # Start with active transparency if the inactivity_timer is enabled.
        #
        # start_minimized            False True  False True
        # auto_show                  False False True  True
        # --------------------------------------------------
        # window visible on start    True  False False False
        # window visible later       True  True  False False

        if config.xid_mode:
            win.set_visible(True) # simply show the window
        else:
            # determine the initial transition
            if config.is_auto_show_enabled():
                transition = Transition.AUTO_HIDE
            else:
                if config.is_visible_on_start():
                    if self.inactivity_timer.is_enabled():
                        transition = Transition.ACTIVATE
                    else:
                        transition = Transition.SHOW
                else:
                    transition = Transition.HIDE

            # transition to initial opacity
            if win.supports_alpha:
                win.set_opacity(0.0) # fade in from full transparency
            self.begin_transition(transition, 0.2)

            # kick off inactivity timer, i.e. DEACTIVATE on timeout
            if transition == Transition.ACTIVATE:
                self.inactivity_timer.begin_transition(False)

            # Be sure to show/hide window and icon palette
            if transition in [Transition.SHOW,
                              Transition.AUTO_SHOW,
                              Transition.ACTIVATE]:
                win.set_visible(True)
            else:
                win.set_visible(False)

    def update_resize_handles(self):
        """ Tell WindowManipulator about the active resize handles """
        self.set_drag_handles(config.window.resize_handles)

    def update_auto_show(self):
        """
        Turn on/off auto-show and show/hide the window accordingly.
        """
        enable = config.is_auto_show_enabled()
        self.auto_show.enable(enable)
        self.auto_show.set_visible(not enable)

    def update_transparency(self):
        self.begin_transition(Transition.ACTIVATE)
        if self.inactivity_timer.is_enabled():
            self.inactivity_timer.begin_transition(False)
        else:
            self.inactivity_timer.stop()
        self.redraw() # for background transparency

    def update_inactive_transparency(self):
        if self.inactivity_timer.is_enabled():
            self.begin_transition(Transition.DEACTIVATE)

    def get_transition_target_opacity(self, transition):
        transparency = 0

        if transition in [Transition.ACTIVATE]:
            transparency = config.window.transparency

        elif transition in [Transition.SHOW,
                            Transition.AUTO_SHOW]:
            if not self.inactivity_timer.is_enabled() or \
               self.inactivity_timer.is_active():
                transparency = config.window.transparency
            else:
                transparency = config.window.inactive_transparency

        elif transition in [Transition.HIDE,
                            Transition.AUTO_HIDE]:
            transparency = 100

        elif transition == Transition.DEACTIVATE:
            transparency = config.window.inactive_transparency

        return 1.0 - transparency / 100.0

    def begin_transition(self, transition, duration = None):
        """ Start the transition to a different opacity """


        window = self.get_kbd_window()
        if window:
            _duration = 0.3
            if transition in [Transition.SHOW,
                              Transition.HIDE,
                              Transition.AUTO_HIDE,
                              Transition.AUTO_SHOW,
                              Transition.ACTIVATE]:
                _duration = 0.15
            if duration is None:
                duration = _duration


            if transition in [Transition.SHOW,
                              Transition.AUTO_SHOW]:
                if not window.is_visible():
                    window.set_visible(True)

            start_opacity  = window.get_opacity()
            target_opacity = self.get_transition_target_opacity(transition)
            _logger.debug("begin opacity transition: {} to {}" \
                           .format(start_opacity, target_opacity))

            # no fade delay for screens that can't fade (unity-2d)
            # Don't fade again when the target opacity has already
            # been reached.
            screen = window.get_screen()
            if screen and not screen.is_composited() or \
               self._last_transition == transition and \
               start_opacity == target_opacity:

                if transition in [Transition.HIDE,
                                  Transition.AUTO_HIDE]:
                    window.set_visible(False)
            else:
                self._last_transition = transition
                self._window_fade.fade_to(start_opacity, target_opacity, duration,
                                         self._on_opacity_step, transition)

    def _on_opacity_step(self, opacity, done, transition):
        window = self.get_kbd_window()
        if window:
            window.set_opacity(opacity)
            if done:
                if transition in [Transition.HIDE,
                                  Transition.AUTO_HIDE]:
                    window.set_visible(False)

    def toggle_visible(self):
        """ main method to show/hide onboard manually"""
        window = self.get_kbd_window()
        visible = not window.is_visible() if window else False
        self.set_user_visible(visible)

    def set_user_visible(self, visible):
        """ main method to show/hide onboard manually"""
        self.lock_auto_show_visible(visible)
        self.set_visible(visible)

    def set_visible(self, visible):
        """ Start show/hide transition. """
        window = self.get_kbd_window()
        if window:
            if visible:
                self.begin_transition(Transition.SHOW)
            else:
                self.begin_transition(Transition.HIDE)

    def lock_auto_show_visible(self, visible):
        """
        If the user unhides onboard, don't auto-hide it until
        he manually hides it again.
        """
        if config.is_auto_show_enabled():
            self.auto_show.lock_visible(visible)

    def start_click_polling(self):
        if self.has_latched_sticky_keys():
            self._outside_click_timer.start(0.01, self._on_click_timer)
            self._outside_click_detected = False
            self._outside_click_start_time = time.time()

    def stop_click_polling(self):
        self._outside_click_timer.stop()

    def _on_click_timer(self):
        """ poll for mouse click outside of onboards window """
        rootwin = Gdk.get_default_root_window()
        dunno, x, y, mask = rootwin.get_pointer()
        if mask & (Gdk.ModifierType.BUTTON1_MASK |
                   Gdk.ModifierType.BUTTON2_MASK |
                   Gdk.ModifierType.BUTTON3_MASK):
            self._outside_click_detected = True
        elif self._outside_click_detected:
            # button released anywhere outside of onboards control
            self.stop_click_polling()
            self.on_outside_click()
            return False

        # stop after 30 seconds
        if time.time() - self._outside_click_start_time > 30.0:
            self.stop_click_polling()
            self.on_cancel_outside_click()
            return False

        return True

    def get_drag_window(self):
        """ Overload for WindowManipulator """
        return self.get_kbd_window()

    def get_drag_threshold(self):
        """ Overload for WindowManipulator """
        return config.get_drag_threshold()

    def on_drag_initiated(self):
        """ Overload for WindowManipulator """
        window = self.get_drag_window()
        if window:
            window.on_user_positioning_begin()

    def on_drag_done(self):
        """ Overload for WindowManipulator """
        window = self.get_drag_window()
        if window:
            window.on_user_positioning_done()

    def get_always_visible_rect(self):
        """
        Returns the bounding rectangle of all move buttons
        in canvas coordinates.
        Overload for WindowManipulator
        """
        keys = self.find_keys_from_ids(["move"])
        bounds = None
        for key in keys:
            r = key.get_canvas_border_rect()
            if not bounds:
                bounds = r
            else:
                bounds = bounds.union(r)

        return bounds

    def _on_configure_event(self, widget, user_data):
        self.update_layout()
        self.update_font_sizes()
        self.touch_handles.update_positions(self.canvas_rect)

    def _on_mouse_enter(self, widget, event):
        # ignore event if a mouse button is held down
        # we get the event once the button is released
        if event.state & (Gdk.ModifierType.BUTTON1_MASK |
                          Gdk.ModifierType.BUTTON2_MASK |
                          Gdk.ModifierType.BUTTON3_MASK):
            return

        # stop inactivity timer
        if self.inactivity_timer.is_enabled():
            self.inactivity_timer.begin_transition(True)

        # stop click polling
        self.stop_click_polling()

        # Force into view for WindowManipulator's system drag mode.
        #if not config.xid_mode and \
        #   not config.window.window_decoration and \
        #   not config.window.force_to_top:
        #    GObject.idle_add(self.force_into_view)

    def _on_mouse_leave(self, widget, event):
        # ignore event if a mouse button is held down
        # we get the event once the button is released
        if event.state & (Gdk.ModifierType.BUTTON1_MASK |
                          Gdk.ModifierType.BUTTON2_MASK |
                          Gdk.ModifierType.BUTTON3_MASK):
            return

        # start a timer to detect clicks outside of onboard
        self.start_click_polling()

        # start inactivity timer
        if self.inactivity_timer.is_enabled():
            self.inactivity_timer.begin_transition(False)

        self.stop_dwelling()
        self.reset_touch_handles()

    def _on_motion(self, widget, event):
        point = (event.x, event.y)
        hit_key = None

        # hit-test touch handles first
        hit_handle = None
        if self.touch_handles.active:
            hit_handle = self.touch_handles.hit_test(point)
            self.touch_handles.set_prelight(hit_handle)

        # hit-test keys
        if hit_handle is None:
            hit_key = self.get_key_at_location(point)

        if event.state & (Gdk.ModifierType.BUTTON1_MASK |
                          Gdk.ModifierType.BUTTON2_MASK |
                          Gdk.ModifierType.BUTTON3_MASK):

            # move/resize
            self.handle_motion(event, fallback = True)

            # stop long press when drag threshold has been overcome
            if self.is_drag_active():
                self.stop_long_press()

        else:
            if not hit_handle is None:
                # handle hovered over -> extend its visible time
                self.start_touch_handles_auto_show()

            # start dwelling if we have entered a dwell-enabled key
            if hit_key and \
               hit_key.sensitive and \
               not self.is_dwelling() and \
               not self.already_dwelled(hit_key) and \
               not config.scanner.enabled:

                controller = self.button_controllers.get(hit_key)
                if controller and controller.can_dwell():
                    self.start_dwelling(hit_key)

            self.do_set_cursor_at(point, hit_key)

        # cancel dwelling when the hit key changes
        if self.dwell_key and self.dwell_key != hit_key or \
           self.last_dwelled_key and self.last_dwelled_key != hit_key:
            self.cancel_dwelling()

    def do_set_cursor_at(self, point, hit_key = None):
        """ Set/reset the cursor for frame resize handles """
        if not config.xid_mode:

            allow_drag_cursors = not config.has_window_decoration() and \
                                 not hit_key
            self.set_drag_cursor_at(point, allow_drag_cursors)

    def _on_mouse_button_press(self, widget, event):
        self.stop_click_polling()
        self.stop_dwelling()

        key = None
        point = (event.x, event.y)

        if event.type == Gdk.EventType.BUTTON_PRESS:
            # hit-test touch handles first
            hit_handle = None
            if self.touch_handles.active:
                hit_handle = self.touch_handles.hit_test(point)
                self.touch_handles.set_pressed(hit_handle)
                if not hit_handle is None:
                    # handle clicked -> stop auto-show until button release
                    self.stop_touch_handles_auto_show()
                else:
                    # no handle clicked -> hide them now
                    self.show_touch_handles(False)

            # hit-test keys
            if hit_handle is None:
                key = self.get_key_at_location(point)

            # enable/disable the drag threshold
            if not hit_handle is None:
                self.enable_drag_protection(False)
            elif key and key.id == "move":
                # Move key needs to support long press;
                # always use the drag threshold.
                self.enable_drag_protection(True)
                self.reset_drag_protection()
            else:
                self.enable_drag_protection(config.drag_protection)

            # handle resizing
            if not key and \
               not config.has_window_decoration() and \
               not config.xid_mode:
                if self.handle_press(event):
                    return True

            # bail if we are in scanning mode
            if config.scanner.enabled:
                return True

            # press the key
            self.active_key = key
            if key:
                double_click_time = Gtk.Settings.get_default() \
                        .get_property("gtk-double-click-time")

                # single click?
                if self._last_click_key != key or \
                   event.time - self._last_click_time > double_click_time:
                    self.press_key(key, event.button)

                    # start long press detection
                    controller = self.button_controllers.get(key)
                    if controller and controller.can_long_press():
                        self._long_press_timer.start(1.0, self._on_long_press,
                                                    key, event.button)
                # double click?
                else:
                    self.press_key(key, event.button, EventType.DOUBLE_CLICK)

                self._last_click_key = key
                self._last_click_time = event.time

        return True

    def _on_long_press(self, key, button):
        controller = self.button_controllers.get(key)
        controller.long_press(button)

    def stop_long_press(self):
        self._long_press_timer.stop()

    def _on_mouse_button_release(self, widget, event):
        if not config.scanner.enabled:
            self.release_active_key()
        self.stop_drag()
        self._long_press_timer.stop()

        # reset cursor when there was no cursor motion
        point = (event.x, event.y)
        hit_key = self.get_key_at_location(point)
        self.do_set_cursor_at(point, hit_key)

        # reset touch handles
        self.reset_touch_handles()
        self.start_touch_handles_auto_show()

    def press_key(self, key, button = 1, event_type = EventType.CLICK):
        Keyboard.press_key(self, key, button, event_type)
        self._auto_release_timer.start()
        self._active_event_type = event_type

    def release_key(self, key, button = 1, event_type = None):
        if event_type is None:
            event_type = self._active_event_type
        Keyboard.release_key(self, key, button, event_type)
        self._active_event_type = None

    def is_dwelling(self):
        return not self.dwell_key is None

    def already_dwelled(self, key):
        return self.last_dwelled_key is key

    def start_dwelling(self, key):
        self.cancel_dwelling()
        self.dwell_key = key
        self.last_dwelled_key = key
        key.start_dwelling()
        self.dwell_timer = GObject.timeout_add(50, self._on_dwell_timer)

    def cancel_dwelling(self):
        self.stop_dwelling()
        self.last_dwelled_key = None

    def stop_dwelling(self):
        if self.dwell_timer:
            GObject.source_remove(self.dwell_timer)
            self.dwell_timer = None
            self.redraw([self.dwell_key])
            self.dwell_key.stop_dwelling()
            self.dwell_key = None

    def _on_dwell_timer(self):
        if self.dwell_key:
            self.redraw([self.dwell_key])

            if self.dwell_key.is_done():
                key = self.dwell_key
                self.stop_dwelling()

                self.press_key(key, 0, EventType.DWELL)
                self.release_key(key, 0, EventType.DWELL)

                return False
        return True

    def release_active_key(self):
        if self.active_key:
            self.release_key(self.active_key)
            self.active_key = None
        return True

    def _on_query_tooltip(self, widget, x, y, keyboard_mode, tooltip):
        if config.show_tooltips and \
           not self.is_drag_initiated():
            key = self.get_key_at_location((x, y))
            if key:
                if key.tooltip:
                    r = Gdk.Rectangle()
                    r.x, r.y, r.width, r.height = key.get_canvas_rect()
                    tooltip.set_tip_area(r)   # no effect in oneiric?
                    tooltip.set_text(_(key.tooltip))
                    return True
        return False

    def _on_draw(self, widget, context):
        #_logger.debug("Draw: clip_extents=" + str(context.clip_extents()))
        #self.get_window().set_debug_updates(True)

        if not Gtk.cairo_should_draw_window(context, self.get_window()):
            return

        clip_rect = Rect.from_extents(*context.clip_extents())

        # draw background
        decorated = self.draw_background(context)

        # On first run quickly overwrite the background only.
        # This gives a slightly smoother startup with desktop remnants
        # flashing though for a shorter time.
        if self._first_draw:
            self._first_draw = False
            self.queue_draw()
            return

        if not self.layout:
            return

        # run through all visible layout items
        layer_ids = self.layout.get_layer_ids()
        for item in self.layout.iter_visible_items():
            if item.layer_id:

                # draw layer background
                layer_index = layer_ids.index(item.layer_id)
                parent = item.parent
                if parent and \
                   layer_index != 0:
                    rect = parent.get_canvas_rect()
                    context.rectangle(*rect.inflate(1))

                    if self.color_scheme:
                        rgba = self.color_scheme.get_layer_fill_rgba(layer_index)
                    else:
                        rgba = [0.5, 0.5, 0.5, 0.9]
                    context.set_source_rgba(*rgba)

                    context.fill()

                    self.draw_dish_key_background(context, 1.0, item.layer_id)

            # draw key
            if item.is_key() and \
               clip_rect.intersects(item.get_canvas_rect()):
                item.draw(context)
                item.draw_image(context)
                item.draw_label(context)

        # draw touch handles (enlarged move and resize handles)
        if self.touch_handles.active:
            corner_radius = config.CORNER_RADIUS if decorated else 0
            self.touch_handles.set_corner_radius(corner_radius)
            self.touch_handles.draw(context)

    def show_touch_handles(self, show, auto_hide = True):
        """
        Show/hide the enlarged resize/move handels.
        Initiates an opacity fade.
        """
        if show and config.lockdown.disable_touch_handles:
            return

        if show:
            self.touch_handles.set_prelight(None)
            self.touch_handles.set_pressed(None)
            self.touch_handles.active = True
            self.touch_handles_auto_hide = auto_hide
            start, end = 0.0, 1.0
        else:
            self.stop_touch_handles_auto_show()
            start, end = 1.0, 0.0

        if self.touch_handles_fade.target_value != end:
            self.touch_handles_fade.time_step = 0.025
            self.touch_handles_fade.fade_to(start, end, 0.2,
                                      self._on_touch_handles_opacity)

    def reset_touch_handles(self):
        if self.touch_handles.active:
            self.touch_handles.set_prelight(None)
            self.touch_handles.set_pressed(None)

    def start_touch_handles_auto_show(self):
        """ (re-) starts the timer to hide touch handles """
        if self.touch_handles.active and self.touch_handles_auto_hide:
            self.touch_handles_hide_timer.start(3.5,
                                                self.show_touch_handles, False)

    def stop_touch_handles_auto_show(self):
        """ stops the timer to hide touch handles """
        self.touch_handles_hide_timer.stop()

    def _on_touch_handles_opacity(self, opacity, done):
        if done and opacity < 0.1:
            self.touch_handles.active = False

        self.touch_handles.opacity = opacity

        # Convoluted workaround for a weird cairo glitch (Precise).
        # When queuing all handles for drawing, the background only
        # under the move handle is clipped and remains transparent.
        # -> Fade with double frequency and queue some handles
        # for drawing only every other time.
        if 0:
            self.touch_handles.redraw()
        else:
            for handle in self.touch_handles.handles:
                if bool(self.touch_handles_fade.iteration & 1) != \
                   (handle.id in [Handle.MOVE, Handle.NORTH, Handle.SOUTH]):
                    handle.redraw()

            if done:
                GObject.idle_add(self._on_touch_handles_opacity, 1.0, False)


    def hit_test_move_resize(self, point):
        hit = self.touch_handles.hit_test(point)
        if hit is None:
            hit = WindowManipulator.hit_test_move_resize(self, point)
        return hit

    def draw_background(self, context):
        """ Draw keyboard background """
        win = self.get_kbd_window()

        decorated = False

        if config.xid_mode:
            # xembed mode
            # Disable transparency in lightdm and g-s-s for now.
            # There are too many issues and there is no real
            # visual improvement.
            if False and \
               win.supports_alpha:
                self.clear_background(context)
                decorated = True
                self.draw_transparent_background(context, decorated)
            else:
                self.draw_plain_background(context)

        elif config.has_window_decoration():
            # decorated window
            if win.supports_alpha and \
               config.window.transparent_background:
                self.clear_background(context)
            else:
                self.draw_plain_background(context)

        else:
            # undecorated window
            if win.supports_alpha:
                self.clear_background(context)
                if not config.window.transparent_background:
                    decorated = True
                    self.draw_transparent_background(context, decorated)
            else:
                self.draw_plain_background(context)

        return decorated

    def clear_background(self, context):
        """
        Clear the whole gtk background.
        Makes the whole strut transparent in xembed mode.
        """
        context.save()
        context.set_operator(cairo.OPERATOR_CLEAR)
        context.paint()
        context.restore()

    def get_layer_fill_rgba(self, layer_index):
        if self.color_scheme:
            return self.color_scheme.get_layer_fill_rgba(layer_index)
        else:
            return [0.5, 0.5, 0.5, 1.0]

    def get_background_rgba(self):
        """ layer 0 color * background_transparency """
        layer0_rgba = self.get_layer_fill_rgba(0)
        background_alpha = 1.0 - config.window.background_transparency / 100.0
        background_alpha *= layer0_rgba[3]
        return layer0_rgba[:3] + [background_alpha]

    def draw_transparent_background(self, context, decorated = True):
        """ fill with the transparent background color """
        rgba = self.get_background_rgba()
        context.set_source_rgba(*rgba)

        # draw on the potentially aspect-corrected frame around the layout
        rect = self.layout.get_canvas_border_rect()
        rect = rect.inflate(config.get_frame_width())
        corner_radius = config.CORNER_RADIUS

        if decorated:
            roundrect_arc(context, rect, corner_radius)
        else:
            context.rectangle(*rect)
        context.fill()

        if decorated:
            # inner decoration line
            line_rect = rect.deflate(1)
            roundrect_arc(context, line_rect, corner_radius)
            context.stroke()

        self.draw_dish_key_background(context, rgba[3])

    def draw_plain_background(self, context, layer_index = 0):
        """ fill with plain layer 0 color; no alpha support required """
        rgba = self.get_layer_fill_rgba(layer_index)
        context.set_source_rgba(*rgba)
        context.paint()

        self.draw_dish_key_background(context)

    def draw_dish_key_background(self, context, alpha = 1.0, layer_id = None):
        """
        Black background following the contours of key clusters
        to simulate the opening in the keyboard plane.
        """
        if config.theme_settings.key_style == "dish":
            context.push_group()

            context.set_source_rgba(0, 0, 0, 1)
            enlargement = self.layout.context.scale_log_to_canvas((0.8, 0.8))
            corner_radius = self.layout.context.scale_log_to_canvas_x(2.4)

            if layer_id is None:
                generator = self.layout.iter_visible_items()
            else:
                generator = self.layout.iter_layer_items(layer_id)

            for item in generator:
                if item.is_key():
                    rect = item.get_canvas_fullsize_rect()
                    rect = rect.inflate(*enlargement)
                    roundrect_curve(context, rect, corner_radius)
                    context.fill()

            context.pop_group_to_source()
            context.paint_with_alpha(alpha);

    def _on_mods_changed(self):
        _logger.info("Modifiers have been changed")
        self.update_font_sizes()

    def redraw(self, keys = None):
        """
        Queue redrawing for individual keys or the whole keyboard.
        """
        if keys:
            area = None
            for key in keys:
                rect = key.context.log_to_canvas_rect(key.get_border_rect())
                area = area.union(rect) if area else rect
            area = area.inflate(2.0) # account for stroke width, anti-aliasing
            self.queue_draw_area(*area)
        else:
            self.queue_draw()

    def update_font_sizes(self):
        """
        Cycles through each group of keys and set each key's
        label font size to the maximum possible for that group.
        """
        context = self.create_pango_context()
        for keys in list(self.layout.get_key_groups().values()):

            max_size = 0
            for key in keys:
                key.configure_label(self.mods)
                best_size = key.get_best_font_size(context)
                if best_size:
                    if not max_size or best_size < max_size:
                        max_size = best_size

            for key in keys:
                key.font_size = max_size

    def emit_quit_onboard(self, data=None):
        _logger.debug("Entered emit_quit_onboard")
        self.get_kbd_window().emit("quit-onboard")

    def get_kbd_window(self):
        return self.get_parent()

    def get_click_type_button_rects(self):
        """
        Returns bounding rectangles of all click type buttons
        in root window coordinates.
        """
        keys = self.find_keys_from_ids(["singleclick",
                                        "secondaryclick",
                                        "middleclick",
                                        "doubleclick",
                                        "dragclick"])
        rects = []
        for key in keys:
            r = key.get_canvas_border_rect()
            x0, y0 = self.get_window().get_root_coords(r.x, r.y)
            x1, y1 = self.get_window().get_root_coords(r.x + r.w,
                                                       r.y + r.h)
            rects.append((x0, y0, x1 - x0, y1 -y0))

        return rects

    def on_layout_updated(self):
        # experimental support for keeping window aspect ratio
        # Currently, in Oneiric, neither lightdm, nor gnome-screen-saver
        # appear to honor these hints.

        aspect_ratio = None
        if config.is_keep_aspect_ratio_enabled():
            log_rect = self.layout.get_border_rect()
            aspect_ratio = log_rect.w / float(log_rect.h)
            aspect_ratio = self.layout.get_log_aspect_ratio()

        if self._aspect_ratio != aspect_ratio:
            window = self.get_kbd_window()
            if window:
                geom = Gdk.Geometry()
                if aspect_ratio is None:
                    window.set_geometry_hints(self, geom, 0)
                else:
                    geom.min_aspect = geom.max_aspect = aspect_ratio
                    window.set_geometry_hints(self, geom, Gdk.WindowHints.ASPECT)

                self._aspect_ratio = aspect_ratio

    def refresh_pango_layouts(self):
        """
        When the systems font dpi setting changes our pango layout object,
        it still caches the old setting, leading to wrong font scaling.
        Refresh the pango layout object.
        """
        _logger.info(_("Refreshing pango layout, new font dpi setting is '{}'") \
                .format(Gtk.Settings.get_default().get_property("gtk-xft-dpi")))

        Key.reset_pango_layout()