Пример #1
0
    def show(self):
        self.main_area = Gtk.Fixed()
        self.background = SVGWidget(self,
                                    os.path.join(self.imagepath, self.IMAGE))
        self.lpadTest = Gtk.Image.new_from_file(
            os.path.join(self.imagepath, "inputdisplay-cursor.svg"))
        self.rpadTest = Gtk.Image.new_from_file(
            os.path.join(self.imagepath, "inputdisplay-cursor.svg"))
        self.stickTest = Gtk.Image.new_from_file(
            os.path.join(self.imagepath, "inputdisplay-cursor.svg"))

        self.main_area.set_property("margin-left", 10)
        self.main_area.set_property("margin-right", 10)
        self.main_area.set_property("margin-top", 10)
        self.main_area.set_property("margin-bottom", 10)

        self.main_area.put(self.background, 0, 0)
        self.main_area.put(self.lpadTest, 40, 40)
        self.main_area.put(self.rpadTest, 290, 90)
        self.main_area.put(self.stickTest, 150, 40)

        self.add(self.main_area)

        OSDWindow.show(self)
        self.lpadTest.hide()
        self.rpadTest.hide()
        self.stickTest.hide()
Пример #2
0
 def create_parent(self):
     background = os.path.join(get_share_path(), "images",
                               'radial-menu.svg')
     self.b = SVGWidget(background)
     self.b.connect('size-allocate', self.on_size_allocate)
     self.recolor()
     return self.b
 def __init__(self, app, config=None):
     self.app = app
     self.current = self._ensure_config({})
     filename = self._make_controller_image_path(ControllerImage.DEFAULT)
     SVGWidget.__init__(self, filename)
     if config:
         self._controller_image.use_config(config)
Пример #4
0
	def __init__(self, app, config=None):
		self.app = app
		self.backup = None
		self.current = self._ensure_config({})
		filename = self._make_controller_image_path(ControllerImage.DEFAULT)
		SVGWidget.__init__(self, filename)
		if config:
			self._controller_image.use_config(config)
Пример #5
0
	def setup_widgets(self):
		# Important stuff
		self.builder = Gtk.Builder()
		self.builder.add_from_file(os.path.join(self.gladepath, "app.glade"))
		self.builder.connect_signals(self)
		self.window = self.builder.get_object("window")
		self.add_window(self.window)
		self.window.set_title(_("SC Controller"))
		self.window.set_wmclass("SC Controller", "SC Controller")
		self.ribar = None
		self.create_binding_buttons()
		
		ps = self.add_switcher(10, 10)
		ps.set_allow_new(True)
		ps.set_profile(self.load_profile_selection())
		ps.connect('new-clicked', self.on_new_clicked)
		ps.connect('save-clicked', self.on_save_clicked)
		
		# Drag&drop target
		self.builder.get_object("content").drag_dest_set(Gtk.DestDefaults.ALL, [
			Gtk.TargetEntry.new("text/uri-list", Gtk.TargetFlags.OTHER_APP, 0),
			Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.OTHER_APP, 0)
			], Gdk.DragAction.COPY
		)
		
		# 'C' button
		vbc = self.builder.get_object("vbC")
		self.main_area = self.builder.get_object("mainArea")
		vbc.get_parent().remove(vbc)
		vbc.connect('size-allocate', self.on_vbc_allocated)
		
		# Background
		self.background = SVGWidget(self, os.path.join(self.imagepath, self.IMAGE))
		self.background.connect('hover', self.on_background_area_hover)
		self.background.connect('leave', self.on_background_area_hover, None)
		self.background.connect('click', self.on_background_area_click)
		self.main_area.put(self.background, 0, 0)
		self.main_area.put(vbc, 0, 0) # (self.IMAGE_SIZE[0] / 2) - 90, self.IMAGE_SIZE[1] - 100)
		
		# Test markers (those blue circles over PADs and sticks)
		self.lpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg"))
		self.rpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg"))
		self.stickTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg"))
		self.main_area.put(self.lpadTest, 40, 40)
		self.main_area.put(self.rpadTest, 290, 90)
		self.main_area.put(self.stickTest, 150, 40)
		
		# OSD mode (if used)
		if self.osd_mode:
			self.builder.get_object("btDaemon").set_sensitive(False)
			self.window.set_title(_("Edit Profile"))
		
		# Headerbar
		headerbar(self.builder.get_object("hbWindow"))
Пример #6
0
class RadialMenu(Menu):
    RECOLOR_BACKGROUNDS = ("background", "menuitem_hilight_border", "text")
    RECOLOR_STROKES = ("border", "menuitem_border")
    MIN_DISTANCE = 3000  # Minimal cursor distance from center (in px^2)
    ICON_SIZE = 96

    def __init__(self, ):
        Menu.__init__(self, "osd-radial-menu")
        self.angle = 0
        self.rotation = 0
        self.scale = 1.0
        self.items_with_icon = []

    def create_parent(self):
        background = os.path.join(get_share_path(), "images",
                                  'radial-menu.svg')
        self.b = SVGWidget(background)
        self.b.connect('size-allocate', self.on_size_allocate)
        self.recolor()
        return self.b

    def recolor(self):
        config = Config()
        source_colors = {}
        try:
            # Try to read json file and bail out if it fails
            desc = os.path.join(get_share_path(), "images",
                                'radial-menu.svg.json')
            source_colors = json.loads(open(desc, "r").read())['colors']
        except Exception, e:
            log.warning("Failed to load keyboard description")
            log.warning(e)
            return
        editor = self.b.edit()

        for k in RadialMenu.RECOLOR_BACKGROUNDS:
            if k in config['osd_colors'] and k in source_colors:
                editor.recolor_background(source_colors[k],
                                          config['osd_colors'][k])
        editor.recolor_background(source_colors["background"],
                                  config['osd_colors']["background"])

        for k in RadialMenu.RECOLOR_STROKES:
            if k in config['osd_colors'] and k in source_colors:
                print "REC", source_colors[k], config['osd_colors'][k]
                editor.recolor_strokes(source_colors[k],
                                       config['osd_colors'][k])

        editor.commit()
Пример #7
0
    def pack_items(self, trash, items):
        index = 0
        pb = self.b.get_pixbuf()
        image_width = pb.get_width()
        item_width = 360.0 / len(self.items)
        a1, a2 = (-90.0 - item_width * 0.5) * PI / 180.0, (
            -90.0 + item_width * 0.5) * PI / 180.0
        for i in items:
            # Set size of each arc
            if SVGWidget.get_element(i.widget, "arc") is not None:
                l = SVGWidget.get_element(i.widget, "arc")
                radius = float(
                    l.attrib["radius"]
                )  # TODO: Find how to get value of 'sodipodi:rx'
                l.attrib["d"] = l.attrib["d-template"] % (
                    radius * cos(a1) + image_width / 2,
                    radius * sin(a1) + image_width / 2,
                    radius * cos(a2) + image_width / 2,
                    radius * sin(a2) + image_width / 2,
                )
            # Rotate arc to correct position
            i.a = (360.0 / float(len(self.items))) * float(index)
            i.widget.attrib['transform'] = "%s rotate(%s, %s, %s)" % (
                i.widget.attrib['transform'], i.a, image_width / 2,
                image_width / 2)
            # Rotate text in arc to other direction to keep it horisontal
            if SVGWidget.get_element(i.widget, "menuitem_text") is not None:
                l = SVGWidget.get_element(i.widget, "menuitem_text")
                l.attrib['id'] = "text_" + i.id
                l.attrib['transform'] = "%s rotate(%s)" % (
                    l.attrib['transform'], -i.a)
            # Place up to 3 lines of item label
            label = i.label.split("\n")
            first_line = 0
            if len(label) == 1:
                self.editor.remove_element(
                    SVGWidget.get_element(i.widget, "line0"))
                self.editor.remove_element(
                    SVGWidget.get_element(i.widget, "line2"))
                first_line = 1
            elif len(label) == 2:
                self.editor.remove_element(
                    SVGWidget.get_element(i.widget, "line0"))
                first_line = 1
            for line in xrange(0, len(label)):
                l = SVGWidget.get_element(i.widget,
                                          "line%s" % (first_line + line, ))
                if l is None:
                    break
                SVGEditor.set_text(l, label[line])
            # Continue with next menu item
            i.index = index

            index += 1

        self.editor.remove_element("menuitem_template")
        self.editor.commit()
        del self.editor
Пример #8
0
class RadialMenu(Menu):
	RECOLOR_BACKGROUNDS = ( "background", "menuitem_hilight_border", "text" )
	RECOLOR_STROKES = ( "border", "menuitem_border" )
	MIN_DISTANCE = 3000		# Minimal cursor distance from center (in px^2)
	ICON_SIZE = 96
	
	def __init__(self,):
		Menu.__init__(self, "osd-radial-menu")
		self.angle = 0
		self.rotation = 0
		self.scale = 1.0
		self.items_with_icon = []
	
	
	def create_parent(self):
		background = os.path.join(get_share_path(), "images", 'radial-menu.svg')
		self.b = SVGWidget(background)
		self.b.connect('size-allocate', self.on_size_allocate)
		self.recolor()
		return self.b
	
	
	def recolor(self):
		config = Config()
		source_colors = {}
		try:
			# Try to read json file and bail out if it fails
			desc = os.path.join(get_share_path(), "images", 'radial-menu.svg.json')
			source_colors = json.loads(open(desc, "r").read())['colors']
		except Exception, e:
			log.warning("Failed to load keyboard description")
			log.warning(e)
			return
		editor = self.b.edit()
		
		for k in RadialMenu.RECOLOR_BACKGROUNDS:
			if k in config['osd_colors'] and k in source_colors:
				editor.recolor_background(source_colors[k], config['osd_colors'][k])
		editor.recolor_background(source_colors["background"], config['osd_colors']["background"])
		
		for k in RadialMenu.RECOLOR_STROKES:
			if k in config['osd_colors'] and k in source_colors:
				print "REC", source_colors[k], config['osd_colors'][k]
				editor.recolor_strokes(source_colors[k], config['osd_colors'][k])
		
		editor.commit()
Пример #9
0
    def show(self, *a):
        if self.background is None:
            self.realize()
            self.background = SVGWidget(self.args.image, init_hilighted=True)
            self.c.add(self.background)
            self.add(self.c)

        OSDWindow.show(self, *a)
        self.move(*self.compute_position())
Пример #10
0
	def setup_image(self):
		for id in self.IMAGES:
			parent = self.builder.get_object(id)
			if parent is not None:
				image = SVGWidget(self.app, os.path.join(self.app.imagepath, self.IMAGES[id]))
				image.connect('hover', self.on_background_area_hover)
				image.connect('leave', self.on_background_area_hover, None)
				image.connect('click', self.on_background_area_click)
				self.images.append(image)
				parent.pack_start(image, True, True, 0)
				parent.show_all()
Пример #11
0
    def __init__(self):
        OSDWindow.__init__(self, "osd-keyboard")
        TimerManager.__init__(self)
        self.daemon = None
        self.mapper = None
        self.keymap = Gdk.Keymap.get_default()
        self.keymap.connect('state-changed', self.on_state_changed)
        self.profile = Profile(TalkingActionParser())

        kbimage = os.path.join(get_config_path(), 'keyboard.svg')
        if not os.path.exists(kbimage):
            # Prefer image in ~/.config/scc, but load default one as fallback
            kbimage = os.path.join(get_share_path(), "images", 'keyboard.svg')
        self.background = SVGWidget(self, kbimage)

        self.limits = {}
        self.limits[LEFT] = self.background.get_rect_area(
            self.background.get_element("LIMIT_LEFT"))
        self.limits[RIGHT] = self.background.get_rect_area(
            self.background.get_element("LIMIT_RIGHT"))

        cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg')
        self.cursors = {}
        self.cursors[LEFT] = Gtk.Image.new_from_file(cursor)
        self.cursors[LEFT].set_name("osd-keyboard-cursor")
        self.cursors[RIGHT] = Gtk.Image.new_from_file(cursor)
        self.cursors[RIGHT].set_name("osd-keyboard-cursor")

        self._eh_ids = []
        self._stick = 0, 0
        self._hovers = {self.cursors[LEFT]: None, self.cursors[RIGHT]: None}
        self._pressed = {self.cursors[LEFT]: None, self.cursors[RIGHT]: None}

        self.c = Gtk.Box()
        self.c.set_name("osd-keyboard-container")

        self.f = Gtk.Fixed()
        self.f.add(self.background)
        self.f.add(self.cursors[LEFT])
        self.f.add(self.cursors[RIGHT])
        self.c.add(self.f)
        self.add(self.c)

        self.timer('labels', 0.1, self.update_labels)
Пример #12
0
	def recolor(self):
		# TODO: keyboard description is probably not needed anymore
		_get = lambda a: SVGWidget.color_to_float(self.config['osk_colors'].get(a, ""))
		self.background.color_button1 = _get("button1")
		self.background.color_button1_border = _get("button1_border")
		self.background.color_button2 = _get("button2")
		self.background.color_button2_border = _get("button2_border")
		self.background.color_hilight = _get("hilight")
		self.background.color_pressed = _get("pressed")
		self.background.color_text = _get("text")
Пример #13
0
    def __init__(self, image):
        Gtk.DrawingArea.__init__(self)
        self.connect('size-allocate', self.on_size_allocate)
        self.connect('draw', self.on_draw)

        areas = []
        self.color_button1 = 0.8, 0, 0, 1  # Just random mess,
        self.color_button1_border = 1, 0, 0, 1  # config overrides it anyway
        self.color_button2 = 0.8, 0.8, 0, 1
        self.color_button2_border = 1, 1, 0, 1
        self.color_hilight = 0, 1, 1, 1
        self.color_pressed = 1, 1, 1, 1
        self.color_text = 1, 1, 1, 1

        self.overlay = SVGWidget(image, False)
        self.tree = ET.fromstring(self.overlay.current_svg.encode("utf-8"))
        SVGWidget.find_areas(self.tree, None, areas, get_colors=True)

        self._hilight = ()
        self._pressed = ()
        self._button_images = {}
        self._help_areas = [
            self.get_limit("HELP_LEFT"),
            self.get_limit("HELP_RIGHT")
        ]
        self._help_lines = ([], [])

        # TODO: It would be cool to use user-set font here, but cairo doesn't
        # have glyph replacement and most of default fonts (Ubuntu, Cantarell,
        # similar shit) misses pretty-much everything but letters, notably ↲
        #
        # For that reason, DejaVu Sans is hardcoded for now. On systems
        # where DejaVu Sans is not available, Cairo will automatically fallback
        # to default font.
        self.font_face = "DejaVu Sans"
        # self.font_face = Gtk.Label(label="X").get_style().font_desc.get_family()
        log.debug("Using font %s", self.font_face)

        self.buttons = [Button(self.tree, area) for area in areas]
        background = SVGEditor.find_by_id(self.tree, "BACKGROUND")
        self.set_size_request(*SVGEditor.get_size(background))
        self.overlay.edit().keep("overlay").commit()
        self.overlay.hilight({})
Пример #14
0
 def recolor(self):
     # TODO: keyboard description is probably not needed anymore
     _get = lambda a: SVGWidget.color_to_float(self.config['osk_colors'].
                                               get(a, ""))
     self.background.color_button1 = _get("button1")
     self.background.color_button1_border = _get("button1_border")
     self.background.color_button2 = _get("button2")
     self.background.color_button2_border = _get("button2_border")
     self.background.color_hilight = _get("hilight")
     self.background.color_pressed = _get("pressed")
     self.background.color_text = _get("text")
Пример #15
0
	def __init__(self, image):
		Gtk.DrawingArea.__init__(self)
		self.connect('size-allocate', self.on_size_allocate)
		self.connect('draw', self.on_draw)
		
		areas = []
		self.color_button1 = 0.8, 0, 0, 1			# Just random mess,
		self.color_button1_border = 1, 0, 0, 1		# config overrides it anyway
		self.color_button2 = 0.8, 0.8, 0, 1
		self.color_button2_border = 1, 1, 0, 1
		self.color_hilight = 0, 1, 1, 1
		self.color_pressed = 1, 1, 1, 1
		self.color_text = 1, 1, 1, 1
		
		self.overlay = SVGWidget(image, False)
		self.tree = ET.fromstring(self.overlay.current_svg.encode("utf-8"))
		SVGWidget.find_areas(self.tree, None, areas, get_colors=True)
		
		self._hilight = ()
		self._pressed = ()
		self._button_images = {}
		self._help_areas = [ self.get_limit("HELP_LEFT"), self.get_limit("HELP_RIGHT") ]
		self._help_lines = ( [], [] )
		
		# TODO: It would be cool to use user-set font here, but cairo doesn't
		# have glyph replacement and most of default fonts (Ubuntu, Cantarell,
		# similar shit) misses pretty-much everything but letters, notably ↲
		#
		# For that reason, DejaVu Sans is hardcoded for now. On systems
		# where DejaVu Sans is not available, Cairo will automatically fallback
		# to default font.
		self.font_face = "DejaVu Sans"
		# self.font_face = Gtk.Label(label="X").get_style().font_desc.get_family()
		log.debug("Using font %s", self.font_face)
		
		self.buttons = [ Button(self.tree, area) for area in areas ]
		background = SVGEditor.find_by_id(self.tree, "BACKGROUND")
		self.set_size_request(*SVGEditor.get_size(background))
		self.overlay.edit().keep("overlay").commit()
		self.overlay.hilight({})
Пример #16
0
	def pack_items(self, trash, items):
		index = 0
		pb = self.b.get_pixbuf()
		image_width = pb.get_width()
		item_width = 360.0 / len(self.items)
		a1, a2 = (-90.0 - item_width * 0.5) * PI / 180.0, (-90.0 + item_width * 0.5) * PI / 180.0
		for i in items:
			# Set size of each arc
			if SVGWidget.get_element(i.widget, "arc") is not None:
				l = SVGWidget.get_element(i.widget, "arc")
				radius = float(l.attrib["radius"])	# TODO: Find how to get value of 'sodipodi:rx'
				l.attrib["d"] = l.attrib["d-template"] % (
					radius * cos(a1) + image_width / 2,
					radius * sin(a1) + image_width / 2,
					radius * cos(a2) + image_width / 2,
					radius * sin(a2) + image_width / 2,
				)
			# Rotate arc to correct position
			i.a = (360.0 / float(len(self.items))) * float(index)
			i.widget.attrib['transform'] = "%s rotate(%s, %s, %s)" % (
				i.widget.attrib['transform'], i.a, image_width / 2, image_width / 2)
			# Rotate text in arc to other direction to keep it horisontal
			if SVGWidget.get_element(i.widget, "menuitem_text") is not None:
				l = SVGWidget.get_element(i.widget, "menuitem_text")
				l.attrib['id'] = "text_" + i.id
				l.attrib['transform'] = "%s rotate(%s)" % (l.attrib['transform'], -i.a)
			# Place up to 3 lines of item label
			label = i.label.split("\n")
			first_line = 0
			if len(label) == 1:
				self.editor.remove_element(SVGWidget.get_element(i.widget, "line0"))
				self.editor.remove_element(SVGWidget.get_element(i.widget, "line2"))
				first_line = 1
			elif len(label) == 2:
				self.editor.remove_element(SVGWidget.get_element(i.widget, "line0"))
				first_line = 1
			for line in xrange(0, len(label)):
				l = SVGWidget.get_element(i.widget, "line%s" % (first_line + line,))
				if l is None:
					break
				SVGEditor.set_text(l, label[line])
			# Continue with next menu item
			i.index = index
			
			index += 1
		
		self.editor.remove_element("menuitem_template")
		self.editor.commit()
		del self.editor
Пример #17
0
 def setup_image(self, grid_columns=0):
     for id in self.IMAGES:
         parent = self.builder.get_object(id)
         if parent is not None:
             image = SVGWidget(
                 os.path.join(self.app.imagepath, self.IMAGES[id]))
             image.connect('hover', self.on_background_area_hover)
             image.connect('leave', self.on_background_area_hover, None)
             image.connect('click', self.on_background_area_click)
             self.images.append(image)
             if grid_columns:
                 # Grid
                 parent.attach(image, 0, 0, grid_columns, 1)
             else:
                 # Box
                 parent.pack_start(image, True, True, 0)
             parent.show_all()
Пример #18
0
	def setup_widgets(self):
		# Important stuff
		self.builder = Gtk.Builder()
		self.builder.add_from_file(os.path.join(self.gladepath, "app.glade"))
		self.builder.connect_signals(self)
		self.window = self.builder.get_object("window")
		self.add_window(self.window)
		self.window.set_title(_("SC Controller"))
		self.window.set_wmclass("SC Controller", "SC Controller")
		self.ribar = None
		self.create_binding_buttons()
		
		ps = self.add_switcher(10, 10)
		ps.set_allow_new(True)
		ps.set_profile(self.load_profile_selection())
		ps.connect('new-clicked', self.on_new_clicked)
		ps.connect('save-clicked', self.on_save_clicked)
		
		# Drag&drop target
		self.builder.get_object("content").drag_dest_set(Gtk.DestDefaults.ALL, [
			Gtk.TargetEntry.new("text/uri-list", Gtk.TargetFlags.OTHER_APP, 0)
			], Gdk.DragAction.COPY
		)
		
		# 'C' button
		vbc = self.builder.get_object("vbC")
		self.main_area = self.builder.get_object("mainArea")
		vbc.get_parent().remove(vbc)
		vbc.connect('size-allocate', self.on_vbc_allocated)
		
		# Background
		self.background = SVGWidget(self, os.path.join(self.imagepath, self.IMAGE))
		self.background.connect('hover', self.on_background_area_hover)
		self.background.connect('leave', self.on_background_area_hover, None)
		self.background.connect('click', self.on_background_area_click)
		self.main_area.put(self.background, 0, 0)
		self.main_area.put(vbc, 0, 0) # (self.IMAGE_SIZE[0] / 2) - 90, self.IMAGE_SIZE[1] - 100)
		
		# Test markers (those blue circles over PADs and sticks)
		self.lpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg"))
		self.rpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg"))
		self.stickTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg"))
		self.main_area.put(self.lpadTest, 40, 40)
		self.main_area.put(self.rpadTest, 290, 90)
		self.main_area.put(self.stickTest, 150, 40)
		
		# Headerbar
		headerbar(self.builder.get_object("hbWindow"))
Пример #19
0
	def setup_image(self, grid_columns=0):
		for id in self.IMAGES:
			parent = self.builder.get_object(id)
			if parent is not None:
				image = SVGWidget(os.path.join(self.app.imagepath, self.IMAGES[id]))
				image.connect('hover', self.on_background_area_hover)
				image.connect('leave', self.on_background_area_hover, None)
				image.connect('click', self.on_background_area_click)
				self.images.append(image)
				if grid_columns:
					# Grid
					parent.attach(image, 0, 0, grid_columns, 1)
				else:
					# Box
					parent.pack_start(image, True, True, 0)
				parent.show_all()
Пример #20
0
	def __init__(self):
		OSDWindow.__init__(self, "osd-keyboard")
		TimerManager.__init__(self)
		self.daemon = None
		self.keyboard = None
		self.keymap = Gdk.Keymap.get_default()
		self.keymap.connect('state-changed', self.on_state_changed)
		
		
		kbimage = os.path.join(get_config_path(), 'keyboard.svg')
		if not os.path.exists(kbimage):
			# Prefer image in ~/.config/scc, but load default one as fallback
			kbimage = os.path.join(get_share_path(), "images", 'keyboard.svg')
		self.background = SVGWidget(self, kbimage)
		
		self.limit_left  = self.background.get_rect_area(self.background.get_element("LIMIT_LEFT"))
		self.limit_right = self.background.get_rect_area(self.background.get_element("LIMIT_RIGHT"))
		
		cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg')
		self.cursor_left = Gtk.Image.new_from_file(cursor)
		self.cursor_left.set_name("osd-keyboard-cursor")
		self.cursor_right = Gtk.Image.new_from_file(cursor)
		self.cursor_right.set_name("osd-keyboard-cursor")
		
		self._eh_ids = []
		self._stick = 0, 0
		self._hovers = { self.cursor_left : None, self.cursor_right : None }
		self._pressed = { self.cursor_left : None, self.cursor_right : None }
		
		self.c = Gtk.Box()
		self.c.set_name("osd-keyboard-container")
		
		self.f = Gtk.Fixed()
		self.f.add(self.background)
		self.f.add(self.cursor_left)
		self.f.add(self.cursor_right)
		self.c.add(self.f)
		self.add(self.c)
		
		self.set_cursor_position(0, 0, self.cursor_left, self.limit_left)
		self.set_cursor_position(0, 0, self.cursor_right, self.limit_right)
		
		self.timer('labels', 0.1, self.update_labels)
Пример #21
0
	def show(self):
		self.main_area = Gtk.Fixed()
		self.background = SVGWidget(os.path.join(self.imagepath, self.IMAGE))
		self.lpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "inputdisplay-cursor.svg"))
		self.rpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "inputdisplay-cursor.svg"))
		self.stickTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "inputdisplay-cursor.svg"))
		
		self.main_area.set_property("margin-left", 10)
		self.main_area.set_property("margin-right", 10)
		self.main_area.set_property("margin-top", 10)
		self.main_area.set_property("margin-bottom", 10)
		
		self.main_area.put(self.background, 0, 0)
		self.main_area.put(self.lpadTest, 40, 40)
		self.main_area.put(self.rpadTest, 290, 90)
		self.main_area.put(self.stickTest, 150, 40)
		
		self.add(self.main_area)
		
		OSDWindow.show(self)
		self.lpadTest.hide()
		self.rpadTest.hide()
		self.stickTest.hide()
Пример #22
0
	def setup_widgets(self):
		self.builder = Gtk.Builder()
		self.builder.add_from_file(os.path.join(self.gladepath, "app.glade"))
		self.builder.connect_signals(self)
		self.window = self.builder.get_object("window")
		self.add_window(self.window)
		self.window.set_title(_("SC Controller"))
		self.window.set_wmclass("SC Controller", "SC Controller")
		self.ribar = None
		
		for b in BUTTONS:
			self.button_widgets[b] = ControllerButton(self, b, self.builder.get_object("bt" + b.name))
		for b in TRIGGERS:
			self.button_widgets[b] = ControllerTrigger(self, b, self.builder.get_object("btTrigger" + b))
		for b in PADS:
			self.button_widgets[b] = ControllerPad(self, b, self.builder.get_object("bt" + b))
		for b in STICKS:
			self.button_widgets[b] = ControllerStick(self, b, self.builder.get_object("bt" + b))
		for b in GYROS:
			self.button_widgets[b] = ControllerGyro(self, b, self.builder.get_object("bt" + b))
		
		self.builder.get_object("cbProfile").set_row_separator_func(
			lambda model, iter : model.get_value(iter, 1) is None and model.get_value(iter, 0) == "-" )
		
		self.set_daemon_status("unknown")
		
		vbc = self.builder.get_object("vbC")
		main_area = self.builder.get_object("mainArea")
		vbc.get_parent().remove(vbc)
		vbc.connect('size-allocate', self.on_vbc_allocated)
		self.background = SVGWidget(self, os.path.join(self.imagepath, self.IMAGE))
		self.background.connect('hover', self.on_background_area_hover)
		self.background.connect('leave', self.on_background_area_hover, None)
		self.background.connect('click', self.on_background_area_click)
		main_area.put(self.background, 0, 0)
		main_area.put(vbc, 0, 0) # (self.IMAGE_SIZE[0] / 2) - 90, self.IMAGE_SIZE[1] - 100)
		headerbar(self.builder.get_object("hbWindow"))
Пример #23
0
class KeyboardImage(Gtk.DrawingArea):
	LINE_WIDTH = 2
	
	__gsignals__ = {}
	
	
	def __init__(self, image):
		Gtk.DrawingArea.__init__(self)
		self.connect('size-allocate', self.on_size_allocate)
		self.connect('draw', self.on_draw)
		
		areas = []
		self.color_button1 = 0.8, 0, 0, 1			# Just random mess,
		self.color_button1_border = 1, 0, 0, 1		# config overrides it anyway
		self.color_button2 = 0.8, 0.8, 0, 1
		self.color_button2_border = 1, 1, 0, 1
		self.color_hilight = 0, 1, 1, 1
		self.color_pressed = 1, 1, 1, 1
		self.color_text = 1, 1, 1, 1
		
		self.overlay = SVGWidget(image, False)
		self.tree = ET.fromstring(self.overlay.current_svg.encode("utf-8"))
		SVGWidget.find_areas(self.tree, None, areas, get_colors=True)
		
		self._hilight = ()
		self._pressed = ()
		self._button_images = {}
		self._help_areas = [ self.get_limit("HELP_LEFT"), self.get_limit("HELP_RIGHT") ]
		self._help_lines = ( [], [] )
		
		# TODO: It would be cool to use user-set font here, but cairo doesn't
		# have glyph replacement and most of default fonts (Ubuntu, Cantarell,
		# similar shit) misses pretty-much everything but letters, notably ↲
		#
		# For that reason, DejaVu Sans is hardcoded for now. On systems
		# where DejaVu Sans is not available, Cairo will automatically fallback
		# to default font.
		self.font_face = "DejaVu Sans"
		# self.font_face = Gtk.Label(label="X").get_style().font_desc.get_family()
		log.debug("Using font %s", self.font_face)
		
		self.buttons = [ Button(self.tree, area) for area in areas ]
		background = SVGEditor.find_by_id(self.tree, "BACKGROUND")
		self.set_size_request(*SVGEditor.get_size(background))
		self.overlay.edit().keep("overlay").commit()
		self.overlay.hilight({})
		# open("/tmp/a.svg", "w").write(self.overlay.current_svg.encode("utf-8"))
	
	
	def hilight(self, hilight, pressed):
		self._hilight = hilight
		self._pressed = pressed
		self.queue_draw()
	
	
	def set_help(self, left, right):
		self._help_lines = ( left, right )
		self.queue_draw()
	
	
	def set_labels(self, labels):
		for b in self.buttons:
			label = labels.get(b)
			if type(label) in (long, int):
				pass
			elif label:
				b.label = label.encode("utf-8")
		self.queue_draw()
	
	
	def get_limit(self, id):
		a = SVGEditor.find_by_id(self.tree, id)
		width, height = 0, 0
		if not hasattr(a, "parent"): a.parent = None
		x, y = SVGEditor.get_translation(a, absolute=True)
		if 'width' in a.attrib:  width = float(a.attrib['width'])
		if 'height' in a.attrib: height = float(a.attrib['height'])
		return x, y, width, height
	
	
	@staticmethod
	def increase_contrast(buf):
		"""
		Takes input image, which is assumed to be grayscale RGBA and turns it
		into "symbolic" image by inverting colors of pixels where opacity is
		greater than threshold.
		"""
		pixels = [ ord(x) for x in buf.get_pixels() ]
		bpp = 4 if buf.get_has_alpha() else 3
		w, h = buf.get_width(), buf.get_height()
		stride = buf.get_rowstride()
		for i in xrange(0, len(pixels), bpp):
			if pixels[i + 3] > 64:
				pixels[i + 0] = 255 - pixels[i + 0]
				pixels[i + 1] = 255 - pixels[i + 1]
				pixels[i + 2] = 255 - pixels[i + 2]
		
		pixels = b"".join([ chr(x) for x in pixels])
		rv = GdkPixbuf.Pixbuf.new_from_data(
			pixels,
			buf.get_colorspace(),
			buf.get_has_alpha(),
			buf.get_bits_per_sample(),
			w, h, stride,
			None
		)
		rv.pixels = pixels	# Has to be kept in memory
		return rv
	
	
	def get_button_image(self, x, size):
		"""
		Loads and returns button image as pixbuf.
		Pixbufs are cached.
		"""
		if x not in self._button_images:
			path, bw = find_button_image(x, prefer_bw=True)
			if path is None:
				self._button_images[x] = None
				return None
			buf = GdkPixbuf.Pixbuf.new_from_file_at_size(path, size, size)
			buf = self.increase_contrast(buf)
			self._button_images[x] = buf
		i = self._button_images[x]
		return i
	
	
	def on_draw(self, self2, ctx):
		ctx.select_font_face(self.font_face, 0, 0)
		
		ctx.set_line_width(self.LINE_WIDTH)
		ctx.set_font_size(48)
		ascent, descent, height, max_x_advance, max_y_advance = ctx.font_extents()
		
		# Buttons
		for button in self.buttons:
			if button in self._pressed:
				ctx.set_source_rgba(*self.color_pressed)
			elif button in self._hilight:
				ctx.set_source_rgba(*self.color_hilight)
			elif button.dark:
				ctx.set_source_rgba(*self.color_button2)
			else:
				ctx.set_source_rgba(*self.color_button1)
			# filled rectangle
			x, y, w, h = button
			ctx.move_to(x, y)
			ctx.line_to(x + w, y)
			ctx.line_to(x + w, y + h)
			ctx.line_to(x, y + h)
			ctx.line_to(x, y)
			ctx.fill()
			
			# border
			ctx.set_source_rgba(*self.color_button1_border)
			ctx.move_to(x, y)
			ctx.line_to(x + w, y)
			ctx.line_to(x + w, y + h)
			ctx.line_to(x, y + h)
			ctx.line_to(x, y)
			ctx.stroke()
			
			# label
			if button.label:
				ctx.set_source_rgba(*self.color_text)
				extents = ctx.text_extents(button.label)
				x_bearing, y_bearing, width, trash, x_advance, y_advance = extents
				ctx.move_to(x + w * 0.5 - width * 0.5 - x_bearing, y + h * 0.5 + height * 0.3)
				ctx.show_text(button.label)
				ctx.stroke()
		
		# Overlay
		Gdk.cairo_set_source_pixbuf(ctx, self.overlay.get_pixbuf(), 0, 0)
		ctx.paint()
		
		# Help
		ctx.set_source_rgba(*self.color_text)
		ctx.set_font_size(16)
		ascent, descent, height, max_x_advance, max_y_advance = ctx.font_extents()
		for left_right in (0, 1):
			x, y, w, h = self._help_areas[left_right]
			lines = self._help_lines[left_right]
			xx = x if left_right == 0 else x + w
			yy = y
			for (icon, line) in lines:
				yy += height
				if yy > y + h:
					break
				image = self.get_button_image(icon, height * 0.9)
				if image is None: continue
				iw, ih = image.get_width(), image.get_height()
				
				if left_right == 1:	# Right align
					extents = ctx.text_extents(line)
					x_bearing, y_bearing, width, trash, x_advance, y_advance = extents
					ctx.save()
					ctx.translate(xx - height + (height - iw) * 0.5,
						1 + yy - (ascent + ih) * 0.5)
					Gdk.cairo_set_source_pixbuf(ctx, image, 0, 0)
					ctx.paint()
					ctx.restore()
					ctx.move_to(xx - x_bearing - width - 5 - height, yy)
				else:
					ctx.save()
					ctx.translate(1 + xx + (height - iw) * 0.5,
						1 + yy - (ascent + ih) * 0.5)
					Gdk.cairo_set_source_pixbuf(ctx, image, 0, 0)
					ctx.paint()
					ctx.restore()
					ctx.move_to(xx + 5 + height, yy)
					
				ctx.show_text(line)
				ctx.stroke()
	
	
	def on_size_allocate(self, *a):
		pass
Пример #24
0
class InputDisplay(OSDWindow):
	IMAGE = "inputdisplay.svg"
	HILIGHT_COLOR = "#FF00FF00"		# ARGB
	OBSERVE_COLOR = "#00007FFF"		# ARGB
	
	def __init__(self, imagepath="/usr/share/scc/images"):
		OSDWindow.__init__(self, "osd-menu")
		self.daemon = None
		self.config = None
		self.hilights = { self.HILIGHT_COLOR : set(), self.OBSERVE_COLOR : set() }
		self.imagepath = imagepath
		
		self._eh_ids = []
	
	
	def show(self):
		self.main_area = Gtk.Fixed()
		self.background = SVGWidget(os.path.join(self.imagepath, self.IMAGE))
		self.lpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "inputdisplay-cursor.svg"))
		self.rpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "inputdisplay-cursor.svg"))
		self.stickTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "inputdisplay-cursor.svg"))
		
		self.main_area.set_property("margin-left", 10)
		self.main_area.set_property("margin-right", 10)
		self.main_area.set_property("margin-top", 10)
		self.main_area.set_property("margin-bottom", 10)
		
		self.main_area.put(self.background, 0, 0)
		self.main_area.put(self.lpadTest, 40, 40)
		self.main_area.put(self.rpadTest, 290, 90)
		self.main_area.put(self.stickTest, 150, 40)
		
		self.add(self.main_area)
		
		OSDWindow.show(self)
		self.lpadTest.hide()
		self.rpadTest.hide()
		self.stickTest.hide()
	
	
	def run(self):
		self.daemon = DaemonManager()
		self._connect_handlers()
		OSDWindow.run(self)
	
	
	def use_daemon(self, d):
		"""
		Allows (re)using already existing DaemonManager instance in same process.
		use_config() should be be called before parse_argumets() if this is used.
		"""
		self.daemon = d
		self._connect_handlers()
		self.on_daemon_connected(self.daemon)	
	
	
	def _connect_handlers(self):
		self._eh_ids += [
			(self.daemon, self.daemon.connect('dead', self.on_daemon_died)),
			(self.daemon, self.daemon.connect('error', self.on_daemon_died)),
			(self.daemon, self.daemon.connect('alive', self.on_daemon_connected)),
		]
	
	
	def on_daemon_connected(self, *a):
		c = self.daemon.get_controllers()[0]
		c.unlock_all()
		c.observe(DaemonManager.nocallback, self.on_observe_failed,
			'A', 'B', 'C', 'X', 'Y', 'START', 'BACK', 'LB', 'RB',
			'LPAD', 'RPAD', 'LGRIP', 'RGRIP', 'LT', 'RT', 'LEFT',
			'RIGHT', 'STICK', 'STICKPRESS')	
		c.connect('event', self.on_daemon_event_observer)
		c.connect('lost', self.on_controller_lost)
	
	
	def on_observe_failed(self, error):
		log.error("Failed to enable test mode: %s", error)
		if "Sniffing" in error:
			log.error("")
			log.error("=================================================================================")
			log.error("[!!] Please, enable 'Input Test Mode' on 'Advanced' tab in SC-Controller settings")
			log.error("=================================================================================")
		self.quit(3)
	
	
	def on_daemon_event_observer(self, daemon, what, data):
		if what in (LEFT, RIGHT, STICK):
			widget, area = {
				LEFT  : (self.lpadTest,  "LPADTEST"),
				RIGHT : (self.rpadTest,  "RPADTEST"),
				STICK : (self.stickTest, "STICKTEST"),
			}[what]
			# Check if stick or pad is released
			if data[0] == data[1] == 0:
				widget.hide()
				return
			if not widget.is_visible():
				widget.show()
			# Grab values
			ax, ay, aw, trash = self.background.get_area_position(area)
			cw = widget.get_allocation().width
			# Compute center
			x, y = ax + aw * 0.5 - cw * 0.5, ay + 1.0 - cw * 0.5
			# Add pad position
			x += data[0] * aw / STICK_PAD_MAX * 0.5
			y -= data[1] * aw / STICK_PAD_MAX * 0.5
			# Move circle
			self.main_area.move(widget, x, y)
		elif what in ("LT", "RT", "STICKPRESS"):
			what = {
				"LT" : "LEFT",
				"RT" : "RIGHT",
				"STICKPRESS" : "STICK"
			}[what]
			if data[0]:
				self.hilights[self.OBSERVE_COLOR].add(what)
			else:
				self.hilights[self.OBSERVE_COLOR].remove(what)
			self._update_background()
		elif hasattr(SCButtons, what):
			try:
				if data[0]:
					self.hilights[self.OBSERVE_COLOR].add(what)
				else:
					self.hilights[self.OBSERVE_COLOR].remove(what)
				self._update_background()
			except KeyError, e:
				# Non fatal
				pass
		else:
Пример #25
0
	def create_parent(self):
		background = os.path.join(get_share_path(), "images", 'radial-menu.svg')
		self.b = SVGWidget(background)
		self.b.connect('size-allocate', self.on_size_allocate)
		self.recolor()
		return self.b
Пример #26
0
	def create_parent(self):
		background = os.path.join(get_share_path(), "images", 'radial-menu.svg')
		self.b = SVGWidget(self, background)
		return self.b
Пример #27
0
class RadialMenu(Menu):
	MIN_DISTANCE = 3000		# Minimal cursor distance from center (in px^2)
	
	def __init__(self,):
		Menu.__init__(self, "osd-radial-menu")
		self.angle = 0
		self.rotation = 0
	
	
	def create_parent(self):
		background = os.path.join(get_share_path(), "images", 'radial-menu.svg')
		self.b = SVGWidget(self, background)
		return self.b
	
	
	def _add_arguments(self):
		Menu._add_arguments(self)
		self.argparser.add_argument('--rotation', type=float, default=0,
			help="rotates input by angle (default: 0)")
	
	
	def parse_argumets(self, argv):
		self.editor = self.b.edit()
		rv = Menu.parse_argumets(self, argv)
		self.rotation = self.args.rotation
		if rv:
			self.enable_cursor()
		return rv
	
	
	def generate_widget(self, item):
		if item.id is None:
			# Labels and separators, radial menu can't show these
			return None
		e = self.editor.clone_element("menuitem_template")
		SVGEditor.set_text(e, item.label)
		e.attrib['id'] = "menuitem_" + item.id
		return e
	
	
	def pack_items(self, trash, items):
		index = 0
		pb = self.b.get_pixbuf()
		image_width = pb.get_width()
		item_width = 360.0 / len(self.items)
		a1, a2 = (-90.0 - item_width * 0.5) * PI / 180.0, (-90.0 + item_width * 0.5) * PI / 180.0
		for i in items:
			# Set size of each arc
			if SVGWidget.get_element(i.widget, "arc") is not None:
				l = SVGWidget.get_element(i.widget, "arc")
				radius = float(l.attrib["radius"])	# TODO: Find how to get value of 'sodipodi:rx'
				l.attrib["d"] = l.attrib["d-template"] % (
					radius * cos(a1) + image_width / 2,
					radius * sin(a1) + image_width / 2,
					radius * cos(a2) + image_width / 2,
					radius * sin(a2) + image_width / 2,
				)
			# Rotate arc to correct position
			i.a = (360.0 / float(len(self.items))) * float(index)
			i.widget.attrib['transform'] = "%s rotate(%s, %s, %s)" % (
				i.widget.attrib['transform'], i.a, image_width / 2, image_width / 2)
			# Rotate text in arc to other direction to keep it horisontal
			if SVGWidget.get_element(i.widget, "menuitem_text") is not None:
				l = SVGWidget.get_element(i.widget, "menuitem_text")
				l.attrib['id'] = "text_" + i.id
				l.attrib['transform'] = "%s rotate(%s)" % (l.attrib['transform'], -i.a)
			# Place up to 3 lines of item label
			label = i.label.split("\n")
			first_line = 0
			if len(label) == 1:
				self.editor.remove_element(SVGWidget.get_element(i.widget, "line0"))
				self.editor.remove_element(SVGWidget.get_element(i.widget, "line2"))
				first_line = 1
			elif len(label) == 2:
				self.editor.remove_element(SVGWidget.get_element(i.widget, "line0"))
				first_line = 1
			for line in xrange(0, len(label)):
				l = SVGWidget.get_element(i.widget, "line%s" % (first_line + line,))
				if l is None:
					break
				SVGEditor.set_text(l, label[line])
			# Continue with next menu item
			i.index = index
			
			index += 1
		
		self.editor.remove_element("menuitem_template")
		self.editor.commit()
		del self.editor
	
	
	def show(self):
		OSDWindow.show(self)

		from ctypes import byref

		pb = self.b.get_pixbuf()
		win = X.XID(self.get_window().get_xid())
		
		pixmap = X.create_pixmap(self.xdisplay, win,
			pb.get_width(), pb.get_height(), 1)
		width = pb.get_width()
		height = pb.get_height()
		self.f.move(self.cursor, int(width / 2), int(height / 2))
		
		gc = X.create_gc(self.xdisplay, pixmap, 0, None)
		X.set_foreground(self.xdisplay, gc, 0)
		X.fill_rectangle(self.xdisplay, pixmap, gc, 0, 0, pb.get_width(), pb.get_height())
		X.set_foreground(self.xdisplay, gc, 1)
		X.set_background(self.xdisplay, gc, 1)
		
		r = int(pb.get_width() * 0.985)
		x = (pb.get_width() - r) / 2
		
		X.fill_arc(self.xdisplay, pixmap, gc,
			x, x, r, r, 0, 360*64)
		
		X.flush_gc(self.xdisplay, gc)
		X.flush(self.xdisplay)
		
		X.shape_combine_mask(self.xdisplay, win, X.SHAPE_BOUNDING, 0, 0, pixmap, X.SHAPE_SET)
		
		X.flush(self.xdisplay)
	
	
	def select(self, index):
		self._selected = self.items[index]
	
	
	def on_event(self, daemon, what, data):
		if self._submenu:
			return self._submenu.on_event(daemon, what, data)
		if what == self._control_with:
			x, y = data
			# Special case, both confirm_with and cancel_with can be set to STICK
			if self._cancel_with == STICK and self._control_with == STICK:
				if self._control_equals_cancel(daemon, x, y):
					return
			
			if self.rotation:
				rx = x * cos(self.rotation) - y * sin(self.rotation)
				ry = x * sin(self.rotation) + y * cos(self.rotation)
				x, y = rx, ry
			
			max_w = self.get_allocation().width - (self.cursor.get_allocation().width * 1.0)
			max_h = self.get_allocation().height - (self.cursor.get_allocation().height * 1.0)
			cx = ((x * 0.75 / (STICK_PAD_MAX * 2.0)) + 0.5) * max_w
			cy = (0.5 - (y * 0.75 / (STICK_PAD_MAX * 2.0))) * max_h
			
			cx -= self.cursor.get_allocation().width * 0.5
			cy -= self.cursor.get_allocation().height * 0.5
			self.f.move(self.cursor, int(cx), int(cy))
			
			if abs(x) + abs(y) > RadialMenu.MIN_DISTANCE:
				angle = atan2(x, y) * 180.0 / PI
				half_width = 180.0 / len(self.items)
				for i in self.items:
					if abs(degdiff(i.a, angle)) < half_width:
						if self._selected != i:
							self._selected = i
							self.b.hilight({
								"menuitem_" + i.id : "#" + self.config["osd_colors"]["menuitem_hilight"],
								"text_" + i.id :  "#" + self.config["osd_colors"]["menuitem_hilight_text"],
							})
		else:
			return Menu.on_event(self, daemon, what, data)
Пример #28
0
class RadialMenu(Menu):
    MIN_DISTANCE = 3000  # Minimal cursor distance from center (in px^2)
    ICON_SIZE = 96

    def __init__(self, ):
        Menu.__init__(self, "osd-radial-menu")
        self.angle = 0
        self.rotation = 0
        self.scale = 1.0
        self.items_with_icon = []

    def create_parent(self):
        background = os.path.join(get_share_path(), "images",
                                  'radial-menu.svg')
        self.b = SVGWidget(self, background)
        self.b.connect('size-allocate', self.on_size_allocate)
        return self.b

    def on_size_allocate(self, trash, allocation):
        """ (Re)centers all icons when menu is displayed or size is changed """
        cx = allocation.width * self.scale * 0.5
        cy = allocation.height * self.scale * 0.5
        radius = min(cx, cy) * 2 / 3
        for i in self.items_with_icon:
            angle, icon = float(i.a) * PI / 180.0, i.icon_widget
            x, y = cx + sin(angle) * radius, cy - cos(angle) * radius
            x = x - (self.ICON_SIZE * self.scale * 0.5)
            y = y - (self.ICON_SIZE * self.scale * 0.5)
            i.icon_widget.get_parent().move(i.icon_widget, x, y)

    def get_window_size(self):
        w, h = Menu.get_window_size(self)
        if self.scale != 1.0:
            w = int(w * self.scale)
            h = int(h * self.scale)
        return w, h

    def _add_arguments(self):
        Menu._add_arguments(self)
        self.argparser.add_argument('--rotation',
                                    type=float,
                                    default=0,
                                    help="rotates input by angle (default: 0)")

    def parse_argumets(self, argv):
        self.editor = self.b.edit()
        rv = Menu.parse_argumets(self, argv)
        self.rotation = self.args.rotation
        if rv:
            self.enable_cursor()
        return rv

    def generate_widget(self, item):
        if item.id is None:
            # Labels and separators, radial menu can't show these
            return None
        e = self.editor.clone_element("menuitem_template")
        SVGEditor.set_text(e, item.label)
        e.attrib['id'] = "menuitem_" + item.id
        return e

    def pack_items(self, trash, items):
        if self._size > 0 and self._size < 100:
            self.scale = self._size / 100.0
            root = SVGEditor.get_element(self.editor, "root")
            SVGEditor.scale(root, self.scale)
        pb = self.b.get_pixbuf()
        # Image width is not scaled as everything bellow operates
        # in 'root' object coordinate space
        image_width = pb.get_width()

        index = 0
        item_offset = 360.0 / len(self.items)
        a1 = (-90.0 - item_offset * 0.5) * PI / 180.0
        a2 = (-90.0 + item_offset * 0.5) * PI / 180.0
        for i in self.items_with_icon:
            i.icon_widget.get_parent().remove_child(i.icon_widget)
        self.items_with_icon = []
        for i in items:
            # Set size of each arc
            if SVGEditor.get_element(i.widget, "arc") is not None:
                l = SVGEditor.get_element(i.widget, "arc")
                radius = float(
                    l.attrib["radius"]
                )  # TODO: Find how to get value of 'sodipodi:rx'
                l.attrib["d"] = l.attrib["d-template"] % (
                    radius * cos(a1) + image_width / 2,
                    radius * sin(a1) + image_width / 2,
                    radius * cos(a2) + image_width / 2,
                    radius * sin(a2) + image_width / 2,
                )
            # Rotate arc to correct position
            i.a = (360.0 / float(len(self.items))) * float(index)
            SVGEditor.rotate(i.widget, i.a, image_width * 0.5,
                             image_width * 0.5)
            # Check if there is any icon
            icon_file, has_colors = find_icon(i.icon, False) if hasattr(
                i, "icon") else (None, False)
            if icon_file:
                # Icon - hide all text and place MenuIcon widget on top of image
                self.editor.remove_element(
                    SVGEditor.get_element(i.widget, "menuitem_text"))
                self.editor.remove_element(
                    SVGEditor.get_element(i.widget, "line0"))
                self.editor.remove_element(
                    SVGEditor.get_element(i.widget, "line2"))
                i.icon_widget = MenuIcon(icon_file, has_colors)
                i.icon_widget.set_name("osd-radial-menu-icon")
                i.icon_widget.set_size_request(self.ICON_SIZE * self.scale,
                                               self.ICON_SIZE * self.scale)
                self.b.get_parent().put(i.icon_widget, 200, 200)
                self.items_with_icon.append(i)
            else:
                # No icon - rotate text in arc to other direction to keep it horisontal
                if SVGEditor.get_element(i.widget,
                                         "menuitem_text") is not None:
                    l = SVGEditor.get_element(i.widget, "menuitem_text")
                    l.attrib['id'] = "text_" + i.id
                    l.attrib['transform'] = "%s rotate(%s)" % (
                        l.attrib['transform'], -i.a)
                # Place up to 3 lines of item label
                label = i.label.split("\n")
                first_line = 0
                if len(label) == 1:
                    self.editor.remove_element(
                        SVGEditor.get_element(i.widget, "line0"))
                    self.editor.remove_element(
                        SVGEditor.get_element(i.widget, "line2"))
                    first_line = 1
                elif len(label) == 2:
                    self.editor.remove_element(
                        SVGEditor.get_element(i.widget, "line0"))
                    first_line = 1
                for line in xrange(0, len(label)):
                    l = SVGEditor.get_element(i.widget,
                                              "line%s" % (first_line + line, ))
                    if l is None:
                        break
                    SVGEditor.set_text(l, label[line])
            # Continue with next menu item
            i.index = index

            index += 1

        self.editor.remove_element("menuitem_template")
        self.editor.commit()
        del self.editor

    def show(self):
        OSDWindow.show(self)

        from ctypes import byref

        pb = self.b.get_pixbuf()
        win = X.XID(self.get_window().get_xid())

        width = int(pb.get_width() * self.scale)
        height = int(pb.get_height() * self.scale)
        pixmap = X.create_pixmap(self.xdisplay, win, width, height, 1)
        self.f.move(self.cursor, int(width / 2), int(height / 2))

        gc = X.create_gc(self.xdisplay, pixmap, 0, None)
        X.set_foreground(self.xdisplay, gc, 0)
        X.fill_rectangle(self.xdisplay, pixmap, gc, 0, 0, width, height)
        X.set_foreground(self.xdisplay, gc, 1)
        X.set_background(self.xdisplay, gc, 1)

        r = int(width * 0.985)
        x = (width - r) / 2

        X.fill_arc(self.xdisplay, pixmap, gc, x, x, r, r, 0, 360 * 64)

        X.flush_gc(self.xdisplay, gc)
        X.flush(self.xdisplay)

        X.shape_combine_mask(self.xdisplay, win, X.SHAPE_BOUNDING, 0, 0,
                             pixmap, X.SHAPE_SET)

        X.flush(self.xdisplay)

    def select(self, i):
        if type(i) == int:
            i = self.items[i]
        if self._selected and hasattr(self._selected, "icon_widget"):
            if self._selected.icon_widget:
                self._selected.icon_widget.set_name("osd-radial-menu-icon")
        self._selected = i
        if hasattr(self._selected,
                   "icon_widget") and self._selected.icon_widget:
            self._selected.icon_widget.set_name(
                "osd-radial-menu-icon-selected")
        self.b.hilight({
            "menuitem_" + i.id:
            "#" + self.config["osd_colors"]["menuitem_hilight"],
            "text_" + i.id:
            "#" + self.config["osd_colors"]["menuitem_hilight_text"],
        })

    def on_event(self, daemon, what, data):
        if self._submenu:
            return self._submenu.on_event(daemon, what, data)
        if what == self._control_with:
            x, y = data
            # Special case, both confirm_with and cancel_with can be set to STICK
            if self._cancel_with == STICK and self._control_with == STICK:
                if self._control_equals_cancel(daemon, x, y):
                    return

            if self.rotation:
                rx = x * cos(self.rotation) - y * sin(self.rotation)
                ry = x * sin(self.rotation) + y * cos(self.rotation)
                x, y = rx, ry

            max_w = self.get_allocation().width * self.scale - (
                self.cursor.get_allocation().width * 1.0)
            max_h = self.get_allocation().height * self.scale - (
                self.cursor.get_allocation().height * 1.0)
            cx = ((x * 0.75 / (STICK_PAD_MAX * 2.0)) + 0.5) * max_w
            cy = (0.5 - (y * 0.75 / (STICK_PAD_MAX * 2.0))) * max_h

            cx -= self.cursor.get_allocation().width * 0.5
            cy -= self.cursor.get_allocation().height * 0.5
            self.f.move(self.cursor, int(cx), int(cy))

            if abs(x) + abs(y) > RadialMenu.MIN_DISTANCE:
                angle = atan2(x, y) * 180.0 / PI
                half_width = 180.0 / len(self.items)
                for i in self.items:
                    if abs(degdiff(i.a, angle)) < half_width:
                        if self._selected != i:
                            if self.feedback and self.controller:
                                self.controller.feedback(*self.feedback)
                            self.select(i)
        else:
            return Menu.on_event(self, daemon, what, data)
Пример #29
0
class App(Gtk.Application, UserDataManager, BindingEditor):
	"""
	Main application / window.
	"""
	
	IMAGE = "background.svg"
	HILIGHT_COLOR = "#FF00FF00"		# ARGB
	OBSERVE_COLOR = "#00007FFF"		# ARGB
	CONFIG = "scc.config.json"
	
	def __init__(self, gladepath="/usr/share/scc",
						imagepath="/usr/share/scc/images"):
		Gtk.Application.__init__(self,
				application_id="me.kozec.scc",
				flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE | Gio.ApplicationFlags.NON_UNIQUE )
		UserDataManager.__init__(self)
		BindingEditor.__init__(self, self)
		# Setup Gtk.Application
		self.setup_commandline()
		# Setup DaemonManager
		self.dm = DaemonManager()
		self.dm.connect("alive", self.on_daemon_alive)
		self.dm.connect("controller-count-changed", self.on_daemon_ccunt_changed)
		self.dm.connect("dead", self.on_daemon_dead)
		self.dm.connect("error", self.on_daemon_error)
		self.dm.connect('reconfigured', self.on_daemon_reconfigured),
		self.dm.connect("version", self.on_daemon_version)
		# Set variables
		self.config = Config()
		self.gladepath = gladepath
		self.imagepath = imagepath
		self.builder = None
		self.recursing = False
		self.statusicon = None
		self.status = "unknown"
		self.context_menu_for = None
		self.daemon_changed_profile = False
		self.background = None
		self.outdated_version = None
		self.profile_switchers = []
		self.current_file = None	# Currently edited file
		self.controller_count = 0
		self.current = Profile(GuiActionParser())
		self.just_started = True
		self.button_widgets = {}
		self.hilights = { App.HILIGHT_COLOR : set(), App.OBSERVE_COLOR : set() }
		self.undo = []
		self.redo = []
	
	
	def setup_widgets(self):
		# Important stuff
		self.builder = Gtk.Builder()
		self.builder.add_from_file(os.path.join(self.gladepath, "app.glade"))
		self.builder.connect_signals(self)
		self.window = self.builder.get_object("window")
		self.add_window(self.window)
		self.window.set_title(_("SC Controller"))
		self.window.set_wmclass("SC Controller", "SC Controller")
		self.ribar = None
		self.create_binding_buttons()
		
		ps = self.add_switcher(10, 10)
		ps.set_allow_new(True)
		ps.set_profile(self.load_profile_selection())
		ps.connect('new-clicked', self.on_new_clicked)
		ps.connect('save-clicked', self.on_save_clicked)
		
		# Drag&drop target
		self.builder.get_object("content").drag_dest_set(Gtk.DestDefaults.ALL, [
			Gtk.TargetEntry.new("text/uri-list", Gtk.TargetFlags.OTHER_APP, 0)
			], Gdk.DragAction.COPY
		)
		
		# 'C' button
		vbc = self.builder.get_object("vbC")
		self.main_area = self.builder.get_object("mainArea")
		vbc.get_parent().remove(vbc)
		vbc.connect('size-allocate', self.on_vbc_allocated)
		
		# Background
		self.background = SVGWidget(self, os.path.join(self.imagepath, self.IMAGE))
		self.background.connect('hover', self.on_background_area_hover)
		self.background.connect('leave', self.on_background_area_hover, None)
		self.background.connect('click', self.on_background_area_click)
		self.main_area.put(self.background, 0, 0)
		self.main_area.put(vbc, 0, 0) # (self.IMAGE_SIZE[0] / 2) - 90, self.IMAGE_SIZE[1] - 100)
		
		# Test markers (those blue circles over PADs and sticks)
		self.lpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg"))
		self.rpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg"))
		self.stickTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg"))
		self.main_area.put(self.lpadTest, 40, 40)
		self.main_area.put(self.rpadTest, 290, 90)
		self.main_area.put(self.stickTest, 150, 40)
		
		# Headerbar
		headerbar(self.builder.get_object("hbWindow"))
	
	
	def setup_statusicon(self):
		menu = self.builder.get_object("mnuDaemon")
		self.statusicon = get_status_icon(self.imagepath, menu)
		self.statusicon.connect('clicked', self.on_statusicon_clicked)
		GLib.idle_add(self.statusicon.set, "scc-%s" % (self.status,), _("SC-Controller"))
	
	
	def destroy_statusicon(self):
		self.statusicon.destroy()
		self.statusicon = None
	
	
	def check(self):
		""" Performs various (three) checks and reports possible problems """
		# TODO: Maybe not best place to do this
		try:
			kernel_mods = [ line.split(" ")[0] for line in file("/proc/modules", "r").read().split("\n") ]
		except Exception:
			# Maybe running on BSD or Windows...
			kernel_mods = [ ]
		
		if len(kernel_mods) > 0 and "uinput" not in kernel_mods:
			# There is no uinput
			msg = _('uinput kernel module not loaded')
			msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput')
			self.show_error(msg)
		elif not os.path.exists("/dev/uinput"):
			# /dev/uinput missing
			msg = _('/dev/uinput doesn\'t exists')
			msg += "\n" + _('uinput kernel module is loaded, but /dev/uinput is missing.')
			#msg += "\n\n" + _('Please, consult your distribution manual on what in the world could cause this.')
			msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput')
			self.show_error(msg)
		elif not check_access("/dev/uinput"):
			# Cannot acces uinput
			msg = _('You don\'t have required access to /dev/uinput.')
			msg += "\n" + _('This will most likely prevent emulation from working.')
			msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput')
			self.show_error(msg)
	
	
	def hilight(self, button):
		""" Hilights specified button on background image """
		if button:
			self.hilights[App.HILIGHT_COLOR] = set([button])
		else:
			self.hilights[App.HILIGHT_COLOR] = set()
		self._update_background()
	
	
	def _update_background(self):
		h = {}
		for color in self.hilights:
			for i in self.hilights[color]:
				h[i] = color
		self.background.hilight(h)
	
	
	def hint(self, button):
		""" As hilight, but marks GTK Button as well """
		active = None
		for b in self.button_widgets.values():
			b.widget.set_state(Gtk.StateType.NORMAL)
			if b.name == button:
				active = b.widget
		
		if active is not None:
			active.set_state(Gtk.StateType.ACTIVE)
		
		self.hilight(button)
	
	
	def show_editor(self, id):
		action = self.get_action(self.current, id)
		ae = self.choose_editor(action, "")
		ae.set_input(id, action)
		ae.show(self.window)
	
	
	def show_context_menu(self, for_id):
		""" Sets sensitivity of popup menu items and displays it on screen """
		mnuPopup = self.builder.get_object("mnuPopup")
		mnuCopy = self.builder.get_object("mnuCopy")
		mnuClear = self.builder.get_object("mnuClear")
		mnuPaste = self.builder.get_object("mnuPaste")
		self.context_menu_for = for_id
		clp = Gtk.Clipboard.get_default(Gdk.Display.get_default())
		mnuCopy.set_sensitive(bool(self.get_action(self.current, for_id)))
		mnuClear.set_sensitive(bool(self.get_action(self.current, for_id)))
		mnuPaste.set_sensitive(clp.wait_is_text_available())
		mnuPopup.popup(None, None, None, None,
			3, Gtk.get_current_event_time())
	
	
	def save_config(self):
		self.config.save()
		self.dm.reconfigure()
		self.enable_test_mode()
	
	
	def on_statusicon_clicked(self, *a):
		""" Handler for user clicking on tray icon button """
		self.window.set_visible(not self.window.get_visible())
	
	
	def on_window_delete_event(self, *a):
		""" Called when user tries to close window """
		if not IS_UNITY and self.config['gui']['enable_status_icon'] and self.config['gui']['minimize_to_status_icon']:
			# Override closing and hide instead
			self.window.set_visible(False)
			return True
		return False # Allow
	
	
	def on_mnuClear_activate(self, *a):
		"""
		Handler for 'Clear' context menu item.
		Simply sets NoAction to input.
		"""
		self.on_action_chosen(self.context_menu_for, NoAction())
	
	
	def on_mnuCopy_activate(self, *a):
		"""
		Handler for 'Copy' context menu item.
		Converts action to string and sends that string to clipboard.
		"""
		a = self.get_action(self.current, self.context_menu_for)
		if a:
			if a.name:
				a = NameModifier(a.name, a)
			clp = Gtk.Clipboard.get_default(Gdk.Display.get_default())
			clp.set_text(a.to_string().encode('utf-8'), -1)
			clp.store()
	
	
	def on_mnuPaste_activate(self, *a):
		"""
		Handler for 'Paste' context menu item.
		Reads string from clipboard, parses it as action and sets that action
		on selected input.
		"""
		clp = Gtk.Clipboard.get_default(Gdk.Display.get_default())
		text = clp.wait_for_text()
		if text:
			a = GuiActionParser().restart(text.decode('utf-8')).parse()
			if not isinstance(a, InvalidAction):
				self.on_action_chosen(self.context_menu_for, a)
		
	
	def on_mnuGlobalSettings_activate(self, *a):
		from scc.gui.global_settings import GlobalSettings
		gs = GlobalSettings(self)
		gs.show(self.window)
	
	
	def on_mnuImport_activate(self, *a):
		"""
		Handler for 'Import Steam Profile' context menu item.
		Displays apropriate dialog.
		"""
		from scc.gui.import_dialog import ImportDialog
		gs = ImportDialog(self)
		gs.show(self.window)
	
	
	def on_btUndo_clicked(self, *a):
		if len(self.undo) < 1: return
		undo, self.undo = self.undo[-1], self.undo[0:-1]
		self.set_action(self.current, undo.id, undo.before)
		self.redo.append(undo)
		self.builder.get_object("btRedo").set_sensitive(True)
		if len(self.undo) < 1:
			self.builder.get_object("btUndo").set_sensitive(False)
		self.on_profile_modified()
	
	
	def on_btRedo_clicked(self, *a):
		if len(self.redo) < 1: return
		redo, self.redo = self.redo[-1], self.redo[0:-1]
		self.set_action(self.current, redo.id, redo.after)
		self.undo.append(redo)
		self.builder.get_object("btUndo").set_sensitive(True)
		if len(self.redo) < 1:
			self.builder.get_object("btRedo").set_sensitive(False)
		self.on_profile_modified()
	
	
	def on_profiles_loaded(self, profiles):
		for ps in self.profile_switchers:
			ps.set_profile_list(profiles)
	
	
	def on_dlgNewProfile_delete_event(self, dlg, *a):
		dlg.hide()
		return True
	
	
	def on_btNewProfile_clicked(self, *a):
		""" Called when new profile name is set and OK is clicked """
		txNewProfile = self.builder.get_object("txNewProfile")
		dlg = self.builder.get_object("dlgNewProfile")
		self.new_profile(self.current, txNewProfile.get_text())
		dlg.hide()
	
	
	def on_profile_modified(self, *a):
		"""
		Called when selected profile is modified in memory.
		"""
		self.profile_switchers[0].set_profile_modified(True)
		
		if not self.current_file.get_path().endswith(".mod"):
			mod = self.current_file.get_path() + ".mod"
			self.current_file = Gio.File.new_for_path(mod)
		
		self.save_profile(self.current_file, self.current)
	
	
	def on_profile_loaded(self, profile, giofile):
		self.current = profile
		self.current_file = giofile
		self.profile_switchers[0].set_profile_modified(False)
		for b in self.button_widgets.values():
			b.update()
	
	
	def on_profile_selected(self, ps, name, giofile):
		if ps == self.profile_switchers[0]:
			self.load_profile(giofile)
		if ps.get_controller():
			ps.get_controller().set_profile(giofile.get_path())
	
	
	def on_unknown_profile(self, ps, name):
		log.warn("Daemon reported unknown profile: '%s'; Overriding.", name)
		if self.current_file is not None:
			ps.get_controller().set_profile(self.current_file.get_path())
	
	
	def on_save_clicked(self, *a):
		if self.current_file.get_path().endswith(".mod"):
			orig = self.current_file.get_path()[0:-4]
			self.current_file = Gio.File.new_for_path(orig)
		
		self.save_profile(self.current_file, self.current)
	
	
	def on_profile_saved(self, giofile, send=True):
		"""
		Called when selected profile is saved to disk
		"""
		if giofile.get_path().endswith(".mod"):
			# Special case, this one is saved only to be sent to daemon
			# and user doesn't need to know about it
			if self.dm.is_alive():
				self.dm.set_profile(giofile.get_path())
			return
		
		self.profile_switchers[0].set_profile_modified(False)
		if send and self.dm.is_alive() and not self.daemon_changed_profile:
			self.dm.set_profile(giofile.get_path())
		
		self.current_file = giofile	
	
	
	def on_new_clicked(self, ps, name):
		new_name = _("Copy of %s") % (name,)
		filename = os.path.join(get_profiles_path(), new_name + ".sccprofile")
		i = 0
		while os.path.exists(filename):
			i += 1
			new_name = _("Copy of %s (%s)") % (name, i)
			filename = os.path.join(get_profiles_path(), new_name + ".sccprofile")
		
		dlg = self.builder.get_object("dlgNewProfile")
		txNewProfile = self.builder.get_object("txNewProfile")
		txNewProfile.set_text(new_name)
		dlg.set_transient_for(self.window)
		dlg.show()
	
	
	def on_action_chosen(self, id, action):
		before = self.set_action(self.current, id, action)
		if before.to_string() != action.to_string():
			# TODO: Maybe better comparison
			self.undo.append(UndoRedo(id, before, action))
			self.builder.get_object("btUndo").set_sensitive(True)
		self.on_profile_modified()
	
	
	def on_background_area_hover(self, trash, area):
		self.hint(area)
	
	
	def on_background_area_click(self, trash, area):
		if area in [ x.name for x in BUTTONS ]:
			self.hint(None)
			self.show_editor(getattr(SCButtons, area))
		elif area in TRIGGERS + STICKS + PADS:
			self.hint(None)
			self.show_editor(area)
	
	
	def on_vbc_allocated(self, vbc, allocation):
		"""
		Called when size of 'Button C' is changed. Centers button
		on background image
		"""
		main_area = self.builder.get_object("mainArea")
		x = (main_area.get_allocation().width - allocation.width) / 2
		y = main_area.get_allocation().height - allocation.height
		main_area.move(vbc, x, y)
	
	
	def on_ebImage_motion_notify_event(self, box, event):
		self.background.on_mouse_moved(event.x, event.y)
	
	
	def on_mnuExit_activate(self, *a):
		self.quit()
	
	
	def on_mnuAbout_activate(self, *a):
		from scc.gui.aboutdialog import AboutDialog
		AboutDialog(self).show(self.window)
	
	
	def on_daemon_alive(self, *a):
		self.set_daemon_status("alive", True)
		self.hide_error()
		self.just_started = False
		if self.profile_switchers[0].get_file() is not None and not self.just_started:
			self.dm.set_profile(self.current_file.get_path())
		GLib.timeout_add_seconds(1, self.check)
		self.enable_test_mode()
	
	
	def on_daemon_ccunt_changed(self, daemon, count):
		if (self.controller_count, count) == (0, 1):
			# First controller connected
			# 
			# 'event' signal should be connected only on first controller,
			# so this block is executed only when number of connected
			# controllers changes from 0 to 1
			c = self.dm.get_controllers()[0]
			c.connect('event', self.on_daemon_event_observer)
		elif count > self.controller_count:
			# Controller added
			while len(self.profile_switchers) < count:
				s = self.add_switcher()
		elif count < self.controller_count:
			# Controller removed
			while len(self.profile_switchers) > max(1, count):
				s = self.profile_switchers.pop()
				s.set_controller(None)
				self.remove_switcher(s)
		
		# Assign controllers to widgets
		for i in xrange(0, count):
			c = self.dm.get_controllers()[i]
			self.profile_switchers[i].set_controller(c)
		
		if count < 1:
			# Special case, no controllers are connected, but one widget
			# has to stay on screen
			self.profile_switchers[0].set_controller(None)
		
		self.controller_count = count
	
	
	def new_profile(self, profile, name):
		filename = os.path.join(get_profiles_path(), name + ".sccprofile")
		self.current_file = Gio.File.new_for_path(filename)
		self.save_profile(self.current_file, profile)
	
	
	def add_switcher(self, margin_left=30, margin_right=40, margin_bottom=2):
		"""
		Adds new profile switcher widgets on top of window. Called
		when new controller is connected to daemon.
		
		Returns generated ProfileSwitcher instance.
		"""
		vbAllProfiles = self.builder.get_object("vbAllProfiles")
		
		ps = ProfileSwitcher(self.imagepath, self.config)
		ps.set_margin_left(margin_left)
		ps.set_margin_right(margin_right)
		ps.set_margin_bottom(margin_bottom)
		ps.connect('right-clicked', self.on_profile_right_clicked)
		
		vbAllProfiles.pack_start(ps, False, False, 0)
		vbAllProfiles.reorder_child(ps, 0)
		vbAllProfiles.show_all()
		
		if len(self.profile_switchers) > 0:
			ps.set_profile_list(self.profile_switchers[0].get_profile_list())
		
		self.profile_switchers.append(ps)
		ps.connect('changed', self.on_profile_selected)
		ps.connect('unknown-profile', self.on_unknown_profile)
		return ps
	
	
	def remove_switcher(self, s):
		"""
		Removes given profile switcher from UI.
		"""
		vbAllProfiles = self.builder.get_object("vbAllProfiles")
		vbAllProfiles.remove(s)
		s.destroy()
	
	
	def enable_test_mode(self):
		"""
		Disables and re-enables Input Test mode. If sniffing is disabled in
		daemon configuration, 2nd call fails and logs error.
		"""
		if self.dm.is_alive():
			try:
				c = self.dm.get_controllers()[0]
			except IndexError:
				# Zero controllers
				return
			c.unlock_all()
			c.observe(DaemonManager.nocallback, self.on_observe_failed,
				'A', 'B', 'C', 'X', 'Y', 'START', 'BACK', 'LB', 'RB',
				'LPAD', 'RPAD', 'LGRIP', 'RGRIP', 'LT', 'RT', 'LEFT',
				'RIGHT', 'STICK', 'STICKPRESS')
	
	
	def on_observe_failed(self, error):
		log.debug("Failed to enable test mode: %s", error)
	
	
	def on_daemon_version(self, daemon, version):
		"""
		Checks if reported version matches expected one.
		If not, daemon is restarted.
		"""
		if version != DAEMON_VERSION and self.outdated_version != version:
			log.warning(
				"Running daemon instance is too old (version %s, expected %s). Restarting...",
				version, DAEMON_VERSION)
			self.outdated_version = version
			self.set_daemon_status("unknown", False)
			self.dm.restart()
	
	
	def on_daemon_error(self, daemon, error):
		log.debug("Daemon reported error '%s'", error)
		msg = _('There was an error with enabling emulation: <b>%s</b>') % (error,)
		# Known errors are handled with aditional message
		if "Device not found" in error:
			msg += "\n" + _("Please, check if you have reciever dongle connected to USB port.")
		elif "LIBUSB_ERROR_ACCESS" in error:
			msg += "\n" + _("You don't have access to controller device.")
			msg += "\n\n" + ( _("Consult your distribution manual, try installing Steam package or <a href='%s'>install required udev rules manually</a>.") %
					'https://wiki.archlinux.org/index.php/Gamepad#Steam_Controller_Not_Pairing' )
			# TODO: Write howto somewhere instead of linking to ArchWiki
		elif "LIBUSB_ERROR_BUSY" in error:
			msg += "\n" + _("Another application (most likely Steam) is using the controller.")
		elif "LIBUSB_ERROR_PIPE" in error:
			msg += "\n" + _("USB dongle was removed.")
		
		self.show_error(msg)
		self.set_daemon_status("error", True)
	
	
	def on_daemon_event_observer(self, daemon, what, data):
		if what in (LEFT, RIGHT, STICK):
			widget, area = {
				LEFT  : (self.lpadTest,  "LPADTEST"),
				RIGHT : (self.rpadTest,  "RPADTEST"),
				STICK : (self.stickTest, "STICKTEST"),
			}[what]
			# Check if stick or pad is released
			if data[0] == data[1] == 0:
				widget.hide()
				return
			if not widget.is_visible():
				widget.show()
			# Grab values
			ax, ay, aw, trash = self.background.get_area_position(area)
			cw = widget.get_allocation().width
			# Compute center
			x, y = ax + aw * 0.5 - cw * 0.5, ay + 1.0 - cw * 0.5
			# Add pad position
			x += data[0] * aw / STICK_PAD_MAX * 0.5
			y -= data[1] * aw / STICK_PAD_MAX * 0.5
			# Move circle
			self.main_area.move(widget, x, y)
		elif what in ("LT", "RT", "STICKPRESS"):
			what = {
				"LT" : "LEFT",
				"RT" : "RIGHT",
				"STICKPRESS" : "STICK"
			}[what]
			if data[0]:
				self.hilights[App.OBSERVE_COLOR].add(what)
			else:
				self.hilights[App.OBSERVE_COLOR].remove(what)
			self._update_background()
		elif hasattr(SCButtons, what):
			if data[0]:
				self.hilights[App.OBSERVE_COLOR].add(what)
			else:
				self.hilights[App.OBSERVE_COLOR].remove(what)
			self._update_background()
		else:
			print "event", what
	
	
	def on_profile_right_clicked(self, ps):
		for name in ("mnuConfigureController", "mnuTurnoffController"):
			# Disable controller-related menu items if controller is not connected
			obj = self.builder.get_object(name)
			obj.set_sensitive(ps.get_controller() is not None)
		
		mnuPS = self.builder.get_object("mnuPS")
		mnuPS.ps = ps
		mnuPS.popup(None, None, None, None,
			3, Gtk.get_current_event_time())
	
	
	def on_mnuConfigureController_activate(self, *a):
		from scc.gui.controller_settings import ControllerSettings
		mnuPS = self.builder.get_object("mnuPS")
		cs = ControllerSettings(self, mnuPS.ps.get_controller(), mnuPS.ps)
		cs.show(self.window)
	
	
	def mnuTurnoffController_activate(self, *a):
		mnuPS = self.builder.get_object("mnuPS")
		if mnuPS.ps.get_controller():
			mnuPS.ps.get_controller().turnoff()
	
	def show_error(self, message):
		if self.ribar is None:
			self.ribar = RIBar(message, Gtk.MessageType.ERROR)
			content = self.builder.get_object("content")
			content.pack_start(self.ribar, False, False, 1)
			content.reorder_child(self.ribar, 0)
			self.ribar.connect("close", self.hide_error)
			self.ribar.connect("response", self.hide_error)
		else:
			self.ribar.get_label().set_markup(message)
		self.ribar.show()
		self.ribar.set_reveal_child(True)
	
	
	def hide_error(self, *a):
		if self.ribar is not None:
			if self.ribar.get_parent() is not None:
				self.ribar.get_parent().remove(self.ribar)
		self.ribar = None
	
	
	def on_daemon_reconfigured(self, *a):
		log.debug("Reloading config...")
		self.config.reload()
		for ps in self.profile_switchers:
			ps.set_controller(ps.get_controller())
	
	
	def on_daemon_dead(self, *a):
		if self.just_started:
			self.dm.restart()
			self.just_started = False
			self.set_daemon_status("unknown", True)
			return
		
		for ps in self.profile_switchers:
			ps.set_controller(None)
			ps.on_daemon_dead()
		self.set_daemon_status("dead", False)
	
	
	def on_mnuEmulationEnabled_toggled(self, cb):
		if self.recursing : return
		if cb.get_active():
			# Turning daemon on
			self.set_daemon_status("unknown", True)
			cb.set_sensitive(False)
			self.dm.start()
		else:
			# Turning daemon off
			self.set_daemon_status("unknown", False)
			cb.set_sensitive(False)
			self.hide_error()
			self.dm.stop()
			
	
	def do_startup(self, *a):
		Gtk.Application.do_startup(self, *a)
		self.load_profile_list()
		self.setup_widgets()
		if self.app.config['gui']['enable_status_icon']:
			self.setup_statusicon()
		self.set_daemon_status("unknown", True)
	
	
	def do_local_options(self, trash, lo):
		set_logging_level(lo.contains("verbose"), lo.contains("debug") )
		return -1
	
	
	def do_command_line(self, cl):
		Gtk.Application.do_command_line(self, cl)
		if len(cl.get_arguments()) > 1:
			filename = cl.get_arguments()[-1]
			giofile = Gio.File.new_for_path(filename)
			# Local file, looks like vdf profile
			from scc.gui.import_dialog import ImportDialog
			gs = ImportDialog(self)
			def i_told_you_to_quit(*a):
				sys.exit(0)
			gs.window.connect('destroy', i_told_you_to_quit)
			gs.show(self.window)
			# Skip first screen and try to import this file
			gs.on_preload_finished(gs.set_file, giofile.get_path())
		else:
			self.activate()
		return 0
	
	
	def do_activate(self, *a):
		self.builder.get_object("window").show()
	
	
	def remove_dot_profile(self):
		"""
		Checks if first profile in list begins with dot and if yes, removes it.
		This is done to undo automatic addition that is done when daemon reports
		selecting such profile.
		"""
		cb = self.builder.get_object("cbProfile")
		model = cb.get_model()
		if len(model) == 0:
			# Nothing to remove
			return
		if not model[0][0].startswith("."):
			# Not dot profile
			return
		active = model.get_path(cb.get_active_iter())
		first = model[0].path
		if active == first:
			# Can't remove active item
			return
		model.remove(model[0].iter)
	
	
	def set_daemon_status(self, status, daemon_runs):
		""" Updates image that shows daemon status and menu shown when image is clicked """
		log.debug("daemon status: %s", status)
		icon = os.path.join(self.imagepath, "scc-%s.svg" % (status,))
		imgDaemonStatus = self.builder.get_object("imgDaemonStatus")
		btDaemon = self.builder.get_object("btDaemon")
		mnuEmulationEnabled = self.builder.get_object("mnuEmulationEnabled")
		imgDaemonStatus.set_from_file(icon)
		mnuEmulationEnabled.set_sensitive(True)
		self.window.set_icon_from_file(icon)
		self.status = status
		if self.statusicon:
			GLib.idle_add(self.statusicon.set, "scc-%s" % (self.status,), _("SC-Controller"))
		self.recursing = True
		if status == "alive":
			btDaemon.set_tooltip_text(_("Emulation is active"))
		elif status == "error":
			btDaemon.set_tooltip_text(_("Error enabling emulation"))
		elif status == "dead":
			btDaemon.set_tooltip_text(_("Emulation is inactive"))
		else:
			btDaemon.set_tooltip_text(_("Checking emulation status..."))
		mnuEmulationEnabled.set_active(daemon_runs)
		self.recursing = False
	
	
	def setup_commandline(self):
		def aso(long_name, short_name, description,
				arg=GLib.OptionArg.NONE,
				flags=GLib.OptionFlags.IN_MAIN):
			""" add_simple_option, adds program argument in simple way """
			o = GLib.OptionEntry()
			if short_name:
				o.long_name = long_name
				o.short_name = short_name
			o.description = description
			o.flags = flags
			o.arg = arg
			self.add_main_option_entries([o])
		
		self.connect('handle-local-options', self.do_local_options)
		
		aso("verbose",	b"v", "Be verbose")
		aso("debug",	b"d", "Be more verbose (debug mode)")
	
	
	def save_profile_selection(self, path):
		""" Saves name of profile into config file """
		name = os.path.split(path)[-1]
		if name.endswith(".sccprofile"):
			name = name[0:-11]
		
		data = dict(current_profile=name)
		jstr = json.dumps(data, sort_keys=True, indent=4)
		
		open(os.path.join(get_config_path(), self.CONFIG), "w").write(jstr)
	
	
	def load_profile_selection(self):
		""" Returns name profile from config file or None if there is none saved """
		try:
			data = json.loads(open(os.path.join(get_config_path(), self.CONFIG), "r").read())
			return data['current_profile']
		except:
			return None
	
	
	def on_drag_data_received(self, widget, context, x, y, data, info, time):
		""" Drag-n-drop handler """
		if str(data.get_data_type()) == "text/uri-list":
			# Only file can be dropped here
			if len(data.get_uris()):
				uri = data.get_uris()[0]
				giofile = Gio.File.new_for_uri(uri)
				if giofile.get_path():
					path = giofile.get_path()
					if path.endswith(".vdf") or path.endswith(".vdffz"):
						# Local file, looks like vdf profile
						from scc.gui.import_dialog import ImportDialog
						gs = ImportDialog(self)
						gs.show(self.window)
						# Skip first screen and try to import this file
						gs.on_preload_finished(gs.set_file, giofile.get_path())
Пример #30
0
class RadialMenu(Menu):
	MIN_DISTANCE = 3000		# Minimal cursor distance from center (in px^2)
	
	def __init__(self,):
		Menu.__init__(self, "osd-radial-menu")
		self.angle = 0
	
	
	def create_parent(self):
		background = os.path.join(get_share_path(), "images", 'radial-menu.svg')
		self.b = SVGWidget(self, background)
		return self.b
	
	
	def parse_argumets(self, argv):
		self.editor = self.b.edit()
		rv = Menu.parse_argumets(self, argv)
		if rv:
			self.enable_cursor()
		return rv
	
	
	def generate_widget(self, item):
		e = self.editor.clone_element("menuitem_template")
		SVGEditor.set_text(e, item.label)
		e.attrib['id'] = "menuitem_" + item.id
		return e
	
	
	def pack_items(self, trash, items):
		index = 0
		pb = self.b.get_pixbuf()
		image_width = pb.get_width()
		item_width = 360.0 / len(self.items)
		a1, a2 = (-90.0 - item_width * 0.5) * PI / 180.0, (-90.0 + item_width * 0.5) * PI / 180.0
		for i in items:
			# Set size of each arc
			if SVGWidget.get_element(i.widget, "arc") is not None:
				l = SVGWidget.get_element(i.widget, "arc")
				radius = float(l.attrib["radius"])	# TODO: Find how to get value of 'sodipodi:rx'
				l.attrib["d"] = l.attrib["d-template"] % (
					radius * cos(a1) + image_width / 2,
					radius * sin(a1) + image_width / 2,
					radius * cos(a2) + image_width / 2,
					radius * sin(a2) + image_width / 2,
				)
			# Rotate arc to correct position
			i.a = (360.0 / float(len(self.items))) * float(index)
			i.widget.attrib['transform'] = "%s rotate(%s, %s, %s)" % (
				i.widget.attrib['transform'], i.a, image_width / 2, image_width / 2)
			# Rotate text in arc to other direction to keep it horisontal
			if SVGWidget.get_element(i.widget, "menuitem_text") is not None:
				l = SVGWidget.get_element(i.widget, "menuitem_text")
				l.attrib['id'] = "text_" + i.id
				l.attrib['transform'] = "%s rotate(%s)" % (l.attrib['transform'], -i.a)
			# Place up to 3 lines of item label
			label = i.label.split("\n")
			first_line = 0
			if len(label) == 1:
				self.editor.remove_element(SVGWidget.get_element(i.widget, "line0"))
				self.editor.remove_element(SVGWidget.get_element(i.widget, "line2"))
				first_line = 1
			elif len(label) == 2:
				self.editor.remove_element(SVGWidget.get_element(i.widget, "line0"))
				first_line = 1
			for line in xrange(0, len(label)):
				l = SVGWidget.get_element(i.widget, "line%s" % (first_line + line,))
				if l is None:
					break
				SVGEditor.set_text(l, label[line])
			# Continue with next menu item
			i.index = index
			
			index += 1
		
		self.editor.remove_element("menuitem_template")
		self.editor.commit()
		del self.editor
	
	
	def show(self):
		OSDWindow.show(self)

		from ctypes import byref

		pb = self.b.get_pixbuf()
		win = X.XID(self.get_window().get_xid())
		
		pixmap = X.create_pixmap(self.xdisplay, win,
			pb.get_width(), pb.get_height(), 1)
		width = pb.get_width()
		height = pb.get_height()
		self.f.move(self.cursor, int(width / 2), int(height / 2))
		
		gc = X.create_gc(self.xdisplay, pixmap, 0, None)
		X.set_foreground(self.xdisplay, gc, 0)
		X.fill_rectangle(self.xdisplay, pixmap, gc, 0, 0, pb.get_width(), pb.get_height())
		X.set_foreground(self.xdisplay, gc, 1)
		X.set_background(self.xdisplay, gc, 1)
		
		r = int(pb.get_width() * 0.985)
		x = (pb.get_width() - r) / 2
		
		X.fill_arc(self.xdisplay, pixmap, gc,
			x, x, r, r, 0, 360*64)
		
		X.flush_gc(self.xdisplay, gc)
		X.flush(self.xdisplay)
		
		X.shape_combine_mask(self.xdisplay, win, X.SHAPE_BOUNDING, 0, 0, pixmap, X.SHAPE_SET)
		
		X.flush(self.xdisplay)
	
	
	def select(self, index):
		self._selected = self.items[index]
	
	
	def on_event(self, daemon, what, data):
		if self._submenu:
			return self._submenu.on_event(daemon, what, data)
		if what == self._control_with:
			x, y = data
			max_w = self.get_allocation().width - (self.cursor.get_allocation().width * 1.0)
			max_h = self.get_allocation().height - (self.cursor.get_allocation().height * 1.0)
			x = ((x * 0.75 / (STICK_PAD_MAX * 2.0)) + 0.5) * max_w
			y = (0.5 - (y * 0.75 / (STICK_PAD_MAX * 2.0))) * max_h
			
			x -= self.cursor.get_allocation().width * 0.5
			y -= self.cursor.get_allocation().height * 0.5
			
			self.f.move(self.cursor, int(x), int(y))
			x, y = data
			if abs(x) + abs(y) > RadialMenu.MIN_DISTANCE:
				angle = atan2(*data) * 180.0 / PI
				half_width = 180.0 / len(self.items)
				for i in self.items:
					if abs(degdiff(i.a, angle)) < half_width:
						if self._selected != i:
							self._selected = i
							self.b.hilight({
								"menuitem_" + i.id : "#" + self.config["osd_colors"]["menuitem_hilight"],
								"text_" + i.id :  "#" + self.config["osd_colors"]["menuitem_hilight_text"],
							})
		else:
			return Menu.on_event(self, daemon, what, data)
Пример #31
0
class InputDisplay(OSDWindow):
    IMAGE = "inputdisplay.svg"
    HILIGHT_COLOR = "#FF00FF00"  # ARGB
    OBSERVE_COLOR = "#00007FFF"  # ARGB

    def __init__(self, imagepath="/usr/share/scc/images"):
        OSDWindow.__init__(self, "osd-menu")
        self.daemon = None
        self.config = None
        self.hilights = {self.HILIGHT_COLOR: set(), self.OBSERVE_COLOR: set()}
        self.imagepath = imagepath

        self._eh_ids = []

    def show(self):
        self.main_area = Gtk.Fixed()
        self.background = SVGWidget(self,
                                    os.path.join(self.imagepath, self.IMAGE))
        self.lpadTest = Gtk.Image.new_from_file(
            os.path.join(self.imagepath, "inputdisplay-cursor.svg"))
        self.rpadTest = Gtk.Image.new_from_file(
            os.path.join(self.imagepath, "inputdisplay-cursor.svg"))
        self.stickTest = Gtk.Image.new_from_file(
            os.path.join(self.imagepath, "inputdisplay-cursor.svg"))

        self.main_area.set_property("margin-left", 10)
        self.main_area.set_property("margin-right", 10)
        self.main_area.set_property("margin-top", 10)
        self.main_area.set_property("margin-bottom", 10)

        self.main_area.put(self.background, 0, 0)
        self.main_area.put(self.lpadTest, 40, 40)
        self.main_area.put(self.rpadTest, 290, 90)
        self.main_area.put(self.stickTest, 150, 40)

        self.add(self.main_area)

        OSDWindow.show(self)
        self.lpadTest.hide()
        self.rpadTest.hide()
        self.stickTest.hide()

    def run(self):
        self.daemon = DaemonManager()
        self._connect_handlers()
        OSDWindow.run(self)

    def use_daemon(self, d):
        """
		Allows (re)using already existing DaemonManager instance in same process.
		use_config() should be be called before parse_argumets() if this is used.
		"""
        self.daemon = d
        self._connect_handlers()
        self.on_daemon_connected(self.daemon)

    def _connect_handlers(self):
        self._eh_ids += [
            (self.daemon, self.daemon.connect('dead', self.on_daemon_died)),
            (self.daemon, self.daemon.connect('error', self.on_daemon_died)),
            (self.daemon, self.daemon.connect('alive',
                                              self.on_daemon_connected)),
        ]

    def on_daemon_connected(self, *a):
        c = self.daemon.get_controllers()[0]
        c.unlock_all()
        c.observe(DaemonManager.nocallback, self.on_observe_failed, 'A', 'B',
                  'C', 'X', 'Y', 'START', 'BACK', 'LB', 'RB', 'LPAD', 'RPAD',
                  'LGRIP', 'RGRIP', 'LT', 'RT', 'LEFT', 'RIGHT', 'STICK',
                  'STICKPRESS')
        c.connect('event', self.on_daemon_event_observer)

    def on_observe_failed(self, error):
        log.error("Failed to enable test mode: %s", error)
        self.quit(3)

    def on_daemon_died(self, *a):
        log.error("Daemon died")
        self.quit(2)

    def on_daemon_event_observer(self, daemon, what, data):
        if what in (LEFT, RIGHT, STICK):
            widget, area = {
                LEFT: (self.lpadTest, "LPADTEST"),
                RIGHT: (self.rpadTest, "RPADTEST"),
                STICK: (self.stickTest, "STICKTEST"),
            }[what]
            # Check if stick or pad is released
            if data[0] == data[1] == 0:
                widget.hide()
                return
            if not widget.is_visible():
                widget.show()
            # Grab values
            ax, ay, aw, trash = self.background.get_area_position(area)
            cw = widget.get_allocation().width
            # Compute center
            x, y = ax + aw * 0.5 - cw * 0.5, ay + 1.0 - cw * 0.5
            # Add pad position
            x += data[0] * aw / STICK_PAD_MAX * 0.5
            y -= data[1] * aw / STICK_PAD_MAX * 0.5
            # Move circle
            self.main_area.move(widget, x, y)
        elif what in ("LT", "RT", "STICKPRESS"):
            what = {"LT": "LEFT", "RT": "RIGHT", "STICKPRESS": "STICK"}[what]
            if data[0]:
                self.hilights[self.OBSERVE_COLOR].add(what)
            else:
                self.hilights[self.OBSERVE_COLOR].remove(what)
            self._update_background()
        elif hasattr(SCButtons, what):
            try:
                if data[0]:
                    self.hilights[self.OBSERVE_COLOR].add(what)
                else:
                    self.hilights[self.OBSERVE_COLOR].remove(what)
                self._update_background()
            except KeyError, e:
                # Non fatal
                pass
        else:
Пример #32
0
	def create_parent(self):
		background = os.path.join(get_share_path(), "images", 'radial-menu.svg')
		self.b = SVGWidget(self, background)
		return self.b
Пример #33
0
class KeyboardImage(Gtk.DrawingArea):
    LINE_WIDTH = 2

    __gsignals__ = {}

    def __init__(self, image):
        Gtk.DrawingArea.__init__(self)
        self.connect('size-allocate', self.on_size_allocate)
        self.connect('draw', self.on_draw)

        areas = []
        self.color_button1 = 0.8, 0, 0, 1  # Just random mess,
        self.color_button1_border = 1, 0, 0, 1  # config overrides it anyway
        self.color_button2 = 0.8, 0.8, 0, 1
        self.color_button2_border = 1, 1, 0, 1
        self.color_hilight = 0, 1, 1, 1
        self.color_pressed = 1, 1, 1, 1
        self.color_text = 1, 1, 1, 1

        self.overlay = SVGWidget(image, False)
        self.tree = ET.fromstring(self.overlay.current_svg.encode("utf-8"))
        SVGWidget.find_areas(self.tree, None, areas, get_colors=True)

        self._hilight = ()
        self._pressed = ()
        self._button_images = {}
        self._help_areas = [
            self.get_limit("HELP_LEFT"),
            self.get_limit("HELP_RIGHT")
        ]
        self._help_lines = ([], [])

        # TODO: It would be cool to use user-set font here, but cairo doesn't
        # have glyph replacement and most of default fonts (Ubuntu, Cantarell,
        # similar shit) misses pretty-much everything but letters, notably ↲
        #
        # For that reason, DejaVu Sans is hardcoded for now. On systems
        # where DejaVu Sans is not available, Cairo will automatically fallback
        # to default font.
        self.font_face = "DejaVu Sans"
        # self.font_face = Gtk.Label(label="X").get_style().font_desc.get_family()
        log.debug("Using font %s", self.font_face)

        self.buttons = [Button(self.tree, area) for area in areas]
        background = SVGEditor.find_by_id(self.tree, "BACKGROUND")
        self.set_size_request(*SVGEditor.get_size(background))
        self.overlay.edit().keep("overlay").commit()
        self.overlay.hilight({})
        # open("/tmp/a.svg", "w").write(self.overlay.current_svg.encode("utf-8"))

    def hilight(self, hilight, pressed):
        self._hilight = hilight
        self._pressed = pressed
        self.queue_draw()

    def set_help(self, left, right):
        self._help_lines = (left, right)
        self.queue_draw()

    def set_labels(self, labels):
        for b in self.buttons:
            label = labels.get(b)
            if type(label) in (long, int):
                pass
            elif label:
                b.label = label.encode("utf-8")
        self.queue_draw()

    def get_limit(self, id):
        a = SVGEditor.find_by_id(self.tree, id)
        width, height = 0, 0
        if not hasattr(a, "parent"): a.parent = None
        x, y = SVGEditor.get_translation(a, absolute=True)
        if 'width' in a.attrib: width = float(a.attrib['width'])
        if 'height' in a.attrib: height = float(a.attrib['height'])
        return x, y, width, height

    @staticmethod
    def increase_contrast(buf):
        """
		Takes input image, which is assumed to be grayscale RGBA and turns it
		into "symbolic" image by inverting colors of pixels where opacity is
		greater than threshold.
		"""
        pixels = [ord(x) for x in buf.get_pixels()]
        bpp = 4 if buf.get_has_alpha() else 3
        w, h = buf.get_width(), buf.get_height()
        stride = buf.get_rowstride()
        for i in xrange(0, len(pixels), bpp):
            if pixels[i + 3] > 64:
                pixels[i + 0] = 255 - pixels[i + 0]
                pixels[i + 1] = 255 - pixels[i + 1]
                pixels[i + 2] = 255 - pixels[i + 2]

        pixels = b"".join([chr(x) for x in pixels])
        rv = GdkPixbuf.Pixbuf.new_from_data(pixels, buf.get_colorspace(),
                                            buf.get_has_alpha(),
                                            buf.get_bits_per_sample(), w, h,
                                            stride, None)
        rv.pixels = pixels  # Has to be kept in memory
        return rv

    def get_button_image(self, x, size):
        """
		Loads and returns button image as pixbuf.
		Pixbufs are cached.
		"""
        if x not in self._button_images:
            path, bw = find_button_image(x, prefer_bw=True)
            if path is None:
                self._button_images[x] = None
                return None
            buf = GdkPixbuf.Pixbuf.new_from_file_at_size(path, size, size)
            buf = self.increase_contrast(buf)
            self._button_images[x] = buf
        i = self._button_images[x]
        return i

    def on_draw(self, self2, ctx):
        ctx.select_font_face(self.font_face, 0, 0)

        ctx.set_line_width(self.LINE_WIDTH)
        ctx.set_font_size(48)
        ascent, descent, height, max_x_advance, max_y_advance = ctx.font_extents(
        )

        # Buttons
        for button in self.buttons:
            if button in self._pressed:
                ctx.set_source_rgba(*self.color_pressed)
            elif button in self._hilight:
                ctx.set_source_rgba(*self.color_hilight)
            elif button.dark:
                ctx.set_source_rgba(*self.color_button2)
            else:
                ctx.set_source_rgba(*self.color_button1)
            # filled rectangle
            x, y, w, h = button
            ctx.move_to(x, y)
            ctx.line_to(x + w, y)
            ctx.line_to(x + w, y + h)
            ctx.line_to(x, y + h)
            ctx.line_to(x, y)
            ctx.fill()

            # border
            ctx.set_source_rgba(*self.color_button1_border)
            ctx.move_to(x, y)
            ctx.line_to(x + w, y)
            ctx.line_to(x + w, y + h)
            ctx.line_to(x, y + h)
            ctx.line_to(x, y)
            ctx.stroke()

            # label
            if button.label:
                ctx.set_source_rgba(*self.color_text)
                extents = ctx.text_extents(button.label)
                x_bearing, y_bearing, width, trash, x_advance, y_advance = extents
                ctx.move_to(x + w * 0.5 - width * 0.5 - x_bearing,
                            y + h * 0.5 + height * 0.3)
                ctx.show_text(button.label)
                ctx.stroke()

        # Overlay
        Gdk.cairo_set_source_pixbuf(ctx, self.overlay.get_pixbuf(), 0, 0)
        ctx.paint()

        # Help
        ctx.set_source_rgba(*self.color_text)
        ctx.set_font_size(16)
        ascent, descent, height, max_x_advance, max_y_advance = ctx.font_extents(
        )
        for left_right in (0, 1):
            x, y, w, h = self._help_areas[left_right]
            lines = self._help_lines[left_right]
            xx = x if left_right == 0 else x + w
            yy = y
            for (icon, line) in lines:
                yy += height
                if yy > y + h:
                    break
                image = self.get_button_image(icon, height * 0.9)
                if image is None: continue
                iw, ih = image.get_width(), image.get_height()

                if left_right == 1:  # Right align
                    extents = ctx.text_extents(line)
                    x_bearing, y_bearing, width, trash, x_advance, y_advance = extents
                    ctx.save()
                    ctx.translate(xx - height + (height - iw) * 0.5,
                                  1 + yy - (ascent + ih) * 0.5)
                    Gdk.cairo_set_source_pixbuf(ctx, image, 0, 0)
                    ctx.paint()
                    ctx.restore()
                    ctx.move_to(xx - x_bearing - width - 5 - height, yy)
                else:
                    ctx.save()
                    ctx.translate(1 + xx + (height - iw) * 0.5,
                                  1 + yy - (ascent + ih) * 0.5)
                    Gdk.cairo_set_source_pixbuf(ctx, image, 0, 0)
                    ctx.paint()
                    ctx.restore()
                    ctx.move_to(xx + 5 + height, yy)

                ctx.show_text(line)
                ctx.stroke()

    def on_size_allocate(self, *a):
        pass
Пример #34
0
class App(Gtk.Application, UserDataManager, BindingEditor):
	"""
	Main application / window.
	"""
	
	IMAGE = "background.svg"
	HILIGHT_COLOR = "#FF00FF00"		# ARGB
	OBSERVE_COLOR = "#00007FFF"		# ARGB
	CONFIG = "scc.config.json"
	RELEASE_URL = "https://github.com/kozec/sc-controller/releases/tag/v%s"
	OSD_MODE_PROF_NAME = ".scc-osd.profile_editor"
	
	def __init__(self, gladepath="/usr/share/scc",
						imagepath="/usr/share/scc/images"):
		Gtk.Application.__init__(self,
				application_id="me.kozec.scc",
				flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE | Gio.ApplicationFlags.NON_UNIQUE )
		UserDataManager.__init__(self)
		BindingEditor.__init__(self, self)
		# Setup Gtk.Application
		self.setup_commandline()
		# Setup DaemonManager
		self.dm = DaemonManager()
		self.dm.connect("alive", self.on_daemon_alive)
		self.dm.connect("controller-count-changed", self.on_daemon_ccunt_changed)
		self.dm.connect("dead", self.on_daemon_dead)
		self.dm.connect("error", self.on_daemon_error)
		self.dm.connect('reconfigured', self.on_daemon_reconfigured),
		self.dm.connect("version", self.on_daemon_version)
		# Set variables
		self.config = Config()
		self.gladepath = gladepath
		self.imagepath = imagepath
		self.builder = None
		self.recursing = False
		self.statusicon = None
		self.status = "unknown"
		self.context_menu_for = None
		self.daemon_changed_profile = False
		self.osd_mode = False	# In OSD mode, only active profile can be editted
		self.osd_mode_mapper = None
		self.background = None
		self.outdated_version = None
		self.profile_switchers = []
		self.current_file = None	# Currently edited file
		self.controller_count = 0
		self.current = Profile(GuiActionParser())
		self.just_started = True
		self.button_widgets = {}
		self.hilights = { App.HILIGHT_COLOR : set(), App.OBSERVE_COLOR : set() }
		self.undo = []
		self.redo = []
	
	
	def setup_widgets(self):
		# Important stuff
		self.builder = Gtk.Builder()
		self.builder.add_from_file(os.path.join(self.gladepath, "app.glade"))
		self.builder.connect_signals(self)
		self.window = self.builder.get_object("window")
		self.add_window(self.window)
		self.window.set_title(_("SC Controller"))
		self.window.set_wmclass("SC Controller", "SC Controller")
		self.ribar = None
		self.create_binding_buttons()
		
		ps = self.add_switcher(10, 10)
		ps.set_allow_new(True)
		ps.set_profile(self.load_profile_selection())
		ps.connect('new-clicked', self.on_new_clicked)
		ps.connect('save-clicked', self.on_save_clicked)
		
		# Drag&drop target
		self.builder.get_object("content").drag_dest_set(Gtk.DestDefaults.ALL, [
			Gtk.TargetEntry.new("text/uri-list", Gtk.TargetFlags.OTHER_APP, 0),
			Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.OTHER_APP, 0)
			], Gdk.DragAction.COPY
		)
		
		# 'C' button
		vbc = self.builder.get_object("vbC")
		self.main_area = self.builder.get_object("mainArea")
		vbc.get_parent().remove(vbc)
		vbc.connect('size-allocate', self.on_vbc_allocated)
		
		# Background
		self.background = SVGWidget(self, os.path.join(self.imagepath, self.IMAGE))
		self.background.connect('hover', self.on_background_area_hover)
		self.background.connect('leave', self.on_background_area_hover, None)
		self.background.connect('click', self.on_background_area_click)
		self.main_area.put(self.background, 0, 0)
		self.main_area.put(vbc, 0, 0) # (self.IMAGE_SIZE[0] / 2) - 90, self.IMAGE_SIZE[1] - 100)
		
		# Test markers (those blue circles over PADs and sticks)
		self.lpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg"))
		self.rpadTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg"))
		self.stickTest = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg"))
		self.main_area.put(self.lpadTest, 40, 40)
		self.main_area.put(self.rpadTest, 290, 90)
		self.main_area.put(self.stickTest, 150, 40)
		
		# OSD mode (if used)
		if self.osd_mode:
			self.builder.get_object("btDaemon").set_sensitive(False)
			self.window.set_title(_("Edit Profile"))
		
		# Headerbar
		headerbar(self.builder.get_object("hbWindow"))
	
	
	def setup_statusicon(self):
		menu = self.builder.get_object("mnuTray")
		self.statusicon = get_status_icon(self.imagepath, menu)
		self.statusicon.connect('clicked', self.on_statusicon_clicked)
		if not self.statusicon.is_clickable():
			self.builder.get_object("mnuShowWindowTray").set_visible(True)
		GLib.idle_add(self.statusicon.set, "scc-%s" % (self.status,), _("SC Controller"))
	
	
	def destroy_statusicon(self):
		self.statusicon.destroy()
		self.statusicon = None
	
	
	def check(self):
		""" Performs various (three) checks and reports possible problems """
		# TODO: Maybe not best place to do this
		try:
			# Dynamic modules
			rawlist = file("/proc/modules", "r").read().split("\n")
			kernel_mods = [ line.split(" ")[0] for line in rawlist ]
			# Built-in modules
			release = platform.uname()[2]
			rawlist = file("/lib/modules/%s/modules.builtin" % release, "r").read().split("\n")
			kernel_mods += [ os.path.split(x)[-1].split(".")[0] for x in rawlist ]
		except Exception:
			# Maybe running on BSD or Windows...
			kernel_mods = [ ]
		
		if len(kernel_mods) > 0 and "uinput" not in kernel_mods:
			# There is no uinput
			msg = _('uinput kernel module not loaded')
			msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput')
			msg += "\n"   + _('or click on "Fix Temporary" button to attempt fix that should work until next restart.')
			ribar = self.show_error(msg)
			gksudo = find_gksudo()
			modprobe = find_binary("modprobe")
			if gksudo and not hasattr(ribar, "_fix_tmp"):
				button = Gtk.Button.new_with_label(_("Fix Temporary"))
				ribar._fix_tmp = button
				button.connect('clicked', self.apply_temporary_fix,
					gksudo + [modprobe, "uinput"],
					_("This will load missing uinput module.")
				)
				ribar.add_button(button, -1)
			return True
		elif not os.path.exists("/dev/uinput"):
			# /dev/uinput missing
			msg = _('/dev/uinput doesn\'t exists')
			msg += "\n" + _('uinput kernel module is loaded, but /dev/uinput is missing.')
			#msg += "\n\n" + _('Please, consult your distribution manual on what in the world could cause this.')
			msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput')
			self.show_error(msg)
			return True
		elif not check_access("/dev/uinput"):
			# Cannot acces uinput
			msg = _('You don\'t have required access to /dev/uinput.')
			msg += "\n"   + _('This will most likely prevent emulation from working.')
			msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput')
			msg += "\n"   + _('or click on "Fix Temporary" button to attempt fix that should work until next restart.')
			ribar = self.show_error(msg)
			gksudo = find_gksudo()
			if gksudo and not hasattr(ribar, "_fix_tmp"):
				button = Gtk.Button.new_with_label(_("Fix Temporary"))
				ribar._fix_tmp = button
				button.connect('clicked', self.apply_temporary_fix,
					gksudo + ["chmod", "666", "/dev/uinput"],
					_("This will enable input emulation for <i>every application</i> and <i>all users</i> on this machine.")
				)
				ribar.add_button(button, -1)
			return True
		return False
	
	
	def apply_temporary_fix(self, trash, shell_command, message):
		"""
		Displays MessageBox with confirmation, tries to run passed shell
		command and restarts daemon.
		
		Doing this allows user to teporary fix some uinput-related problems
		by his vaim belief I'll not format his harddrive.
		"""
		d = Gtk.MessageDialog(parent=self.window,
			flags = Gtk.DialogFlags.MODAL,
			type = Gtk.MessageType.WARNING,
			buttons = Gtk.ButtonsType.OK_CANCEL,
			message_format = _("sudo fix-my-pc")
		)
		
		def on_response(dialog, response_id):
			if response_id == -5:	# OK button, not defined anywhere
				sudo = Gio.Subprocess.new(shell_command, 0)
				sudo.communicate(None, None)
				if sudo.get_exit_status() == 0:
					self.dm.restart()
				else:
					d2 = Gtk.MessageDialog(parent=d,
						flags = Gtk.DialogFlags.MODAL,
						type = Gtk.MessageType.ERROR,
						buttons = Gtk.ButtonsType.OK,
						message_format = _("Command Failed")
					)
					d2.run()
					d2.destroy()
			d.destroy()
		
		d.connect("response", on_response)
		d.format_secondary_markup( _("""Following command is going to be executed:

<b>%s</b>

%s""") % (" ".join(shell_command), message), )
		d.show()
	
	
	def hilight(self, button):
		""" Hilights specified button on background image """
		if button:
			self.hilights[App.HILIGHT_COLOR] = set([button])
		else:
			self.hilights[App.HILIGHT_COLOR] = set()
		self._update_background()
	
	
	def _update_background(self):
		h = {}
		for color in self.hilights:
			for i in self.hilights[color]:
				h[i] = color
		self.background.hilight(h)
	
	
	def hint(self, button):
		""" As hilight, but marks GTK Button as well """
		active = None
		for b in self.button_widgets.values():
			b.widget.set_state(Gtk.StateType.NORMAL)
			if b.name == button:
				active = b.widget
		
		if active is not None:
			active.set_state(Gtk.StateType.ACTIVE)
		
		self.hilight(button)
	
	
	def show_editor(self, id):
		action = self.get_action(self.current, id)
		ae = self.choose_editor(action, "", id)
		ae.allow_first_page()
		ae.set_input(id, action)
		ae.show(self.window)
	
	
	def show_context_menu(self, for_id):
		""" Sets sensitivity of popup menu items and displays it on screen """
		mnuPopup = self.builder.get_object("mnuPopup")
		mnuCopy = self.builder.get_object("mnuCopy")
		mnuClear = self.builder.get_object("mnuClear")
		mnuPaste = self.builder.get_object("mnuPaste")
		mnuEPress = self.builder.get_object("mnuEditPress")
		mnuEPressS = self.builder.get_object("mnuEditPressSeparator")
		self.context_menu_for = for_id
		clp = Gtk.Clipboard.get_default(Gdk.Display.get_default())
		mnuCopy.set_sensitive(bool(self.get_action(self.current, for_id)))
		mnuClear.set_sensitive(bool(self.get_action(self.current, for_id)))
		mnuPaste.set_sensitive(clp.wait_is_text_available())
		mnuEPress.set_visible(for_id in STICKS + PADS)
		mnuEPressS.set_visible(mnuEPress.get_visible())
		
		mnuPopup.popup(None, None, None, None,
			3, Gtk.get_current_event_time())
	
	
	def save_config(self):
		self.config.save()
		self.dm.reconfigure()
		self.enable_test_mode()
	
	
	def on_statusicon_clicked(self, *a):
		""" Handler for user clicking on tray icon button """
		self.window.set_visible(not self.window.get_visible())
	
	
	def on_window_delete_event(self, *a):
		""" Called when user tries to close window """
		if not IS_UNITY and self.config['gui']['enable_status_icon'] and self.config['gui']['minimize_to_status_icon']:
			# Override closing and hide instead
			self.window.set_visible(False)
		else:
			self.on_mnuExit_activate()
		return True
	
	
	def on_mnuClear_activate(self, *a):
		"""
		Handler for 'Clear' context menu item.
		Simply sets NoAction to input.
		"""
		self.on_action_chosen(self.context_menu_for, NoAction())
	
	
	def on_mnuCopy_activate(self, *a):
		"""
		Handler for 'Copy' context menu item.
		Converts action to string and sends that string to clipboard.
		"""
		a = self.get_action(self.current, self.context_menu_for)
		if a:
			if a.name:
				a = NameModifier(a.name, a)
			clp = Gtk.Clipboard.get_default(Gdk.Display.get_default())
			clp.set_text(a.to_string().encode('utf-8'), -1)
			clp.store()
	
	
	def on_mnuPaste_activate(self, *a):
		"""
		Handler for 'Paste' context menu item.
		Reads string from clipboard, parses it as action and sets that action
		on selected input.
		"""
		clp = Gtk.Clipboard.get_default(Gdk.Display.get_default())
		text = clp.wait_for_text()
		if text:
			a = GuiActionParser().restart(text.decode('utf-8')).parse()
			if not isinstance(a, InvalidAction):
				self.on_action_chosen(self.context_menu_for, a)
	
	
	def on_mnuEditPress_activate(self, *a):
		"""
		Handler for 'Edit Pressed Action' context menu item.
		"""
		self.show_editor(getattr(SCButtons, self.context_menu_for))
	
	
	def on_mnuGlobalSettings_activate(self, *a):
		from scc.gui.global_settings import GlobalSettings
		gs = GlobalSettings(self)
		gs.show(self.window)
	
	
	def on_mnuImport_activate(self, *a):
		"""
		Handler for 'Import Steam Profile' context menu item.
		Displays apropriate dialog.
		"""
		from scc.gui.importexport.dialog import Dialog
		ied = Dialog(self)
		ied.show(self.window)
	
	
	def on_btUndo_clicked(self, *a):
		if len(self.undo) < 1: return
		undo, self.undo = self.undo[-1], self.undo[0:-1]
		self.set_action(self.current, undo.id, undo.before)
		self.redo.append(undo)
		self.builder.get_object("btRedo").set_sensitive(True)
		if len(self.undo) < 1:
			self.builder.get_object("btUndo").set_sensitive(False)
		self.on_profile_modified()
	
	
	def on_btRedo_clicked(self, *a):
		if len(self.redo) < 1: return
		redo, self.redo = self.redo[-1], self.redo[0:-1]
		self.set_action(self.current, redo.id, redo.after)
		self.undo.append(redo)
		self.builder.get_object("btUndo").set_sensitive(True)
		if len(self.redo) < 1:
			self.builder.get_object("btRedo").set_sensitive(False)
		self.on_profile_modified()
	
	
	def on_profiles_loaded(self, profiles):
		for ps in self.profile_switchers:
			ps.set_profile_list(profiles)
	
	
	def undeletable_dialog(self, dlg, *a):
		dlg.hide()
		return True
	
	
	def on_btNewProfile_clicked(self, *a):
		""" Called when new profile name is set and OK is clicked """
		txNewProfile = self.builder.get_object("txNewProfile")
		rbNewProfile = self.builder.get_object("rbNewProfile")
		
		dlg = self.builder.get_object("dlgNewProfile")
		if rbNewProfile.get_active():
			# Creating blank profile is requested
			self.current.clear()
		else:
			self.current.is_template = False
		self.new_profile(self.current, txNewProfile.get_text())
		dlg.hide()
	
	
	def on_rbNewProfile_group_changed(self, *a):
		"""
		Called when user clicks 'Copy current profile' button.
		If profile name was not changed by user before clicking it,
		it's automatically changed.
		"""
		txNewProfile = self.builder.get_object("txNewProfile")
		rbNewProfile = self.builder.get_object("rbNewProfile")
		
		if not txNewProfile._changed:
			self.recursing = True
			if rbNewProfile.get_active():
				# Create empty profile
				txNewProfile.set_text(self.generate_new_name())
			else:
				# Copy current profile
				txNewProfile.set_text(self.generate_copy_name(txNewProfile._name))
			self.recursing = False
	
	
	def on_profile_modified(self, update_ui=True):
		"""
		Called when selected profile is modified in memory.
		"""
		if update_ui:
			self.profile_switchers[0].set_profile_modified(True, self.current.is_template)
		
		if not self.current_file.get_path().endswith(".mod"):
			mod = self.current_file.get_path() + ".mod"
			self.current_file = Gio.File.new_for_path(mod)
		
		self.save_profile(self.current_file, self.current)
	
	
	def on_profile_loaded(self, profile, giofile):
		self.current = profile
		self.current_file = giofile
		self.recursing = True
		self.profile_switchers[0].set_profile_modified(False, self.current.is_template)
		self.builder.get_object("txProfileFilename").set_text(giofile.get_path())
		self.builder.get_object("txProfileDescription").get_buffer().set_text(self.current.description)
		self.builder.get_object("cbProfileIsTemplate").set_active(self.current.is_template)
		for b in self.button_widgets.values():
			b.update()
		self.recursing = False
	
	
	def on_profile_selected(self, ps, name, giofile):
		if ps == self.profile_switchers[0]:
			self.load_profile(giofile)
		if ps.get_controller():
			ps.get_controller().set_profile(giofile.get_path())
	
	
	def on_unknown_profile(self, ps, name):
		log.warn("Daemon reported unknown profile: '%s'; Overriding.", name)
		if self.current_file is not None:
			ps.get_controller().set_profile(self.current_file.get_path())
	
	
	def on_save_clicked(self, *a):
		if self.current_file.get_path().endswith(".mod"):
			orig = self.current_file.get_path()[0:-4]
			self.current_file = Gio.File.new_for_path(orig)
		
		if self.current.is_template:
			# Ask user if he is OK with overwriting template
			d = Gtk.MessageDialog(parent=self.window,
				flags = Gtk.DialogFlags.MODAL,
				type = Gtk.MessageType.QUESTION,
				buttons = Gtk.ButtonsType.YES_NO,
				message_format = _("You are about to save changes over template.\nAre you sure?")
			)
			NEW_PROFILE_BUTTON = 7
			d.add_button(_("Create New Profile"), NEW_PROFILE_BUTTON)
			
			
			r = d.run()
			d.destroy()
			if r == NEW_PROFILE_BUTTON:
				# New profile button clicked
				ps = self.profile_switchers[0]
				rbCopyProfile = self.builder.get_object("rbCopyProfile")
				self.on_new_clicked(ps, ps.get_profile_name())
				rbCopyProfile.set_active(True)
				return
			if r != -8:
				# Bail out if user answers anything but yes
				return
		
		self.save_profile(self.current_file, self.current)
	
	
	def on_profile_saved(self, giofile, send=True):
		"""
		Called when selected profile is saved to disk
		"""
		if self.osd_mode:
			# Special case, profile shouldn't be changed while in osd_mode
			return
		
		if giofile.get_path().endswith(".mod"):
			# Special case, this one is saved only to be sent to daemon
			# and user doesn't need to know about it
			if self.dm.is_alive():
				self.dm.set_profile(giofile.get_path())
			return
		
		self.profile_switchers[0].set_profile_modified(False, self.current.is_template)
		if send and self.dm.is_alive() and not self.daemon_changed_profile:
			self.dm.set_profile(giofile.get_path())
		
		self.current_file = giofile	
	
	
	def generate_new_name(self):
		"""
		Generates name for new profile.
		That is 'New Profile X', where X is number that makes name unique.
		"""
		i = 1
		new_name = _("New Profile %s") % (i,)
		filename = os.path.join(get_profiles_path(), new_name + ".sccprofile")
		while os.path.exists(filename):
			i += 1
			new_name = _("New Profile %s") % (i,)
			filename = os.path.join(get_profiles_path(), new_name + ".sccprofile")
		return new_name
	
	
	def generate_copy_name(self, name):
		"""
		Generates name for profile copy.
		That is 'New Profile X', where X is number that makes name unique.
		"""
		new_name = _("%s (copy)") % (name,)
		filename = os.path.join(get_profiles_path(), new_name + ".sccprofile")
		i = 2
		while os.path.exists(filename):
			new_name = _("%s (copy %s)") % (name,)
			filename = os.path.join(get_profiles_path(), new_name + ".sccprofile")
			i += 1
		return new_name
	
	
	def on_txNewProfile_changed(self, tx):
		if self.recursing:
			return
		tx._changed = True
	
	
	def on_new_clicked(self, ps, name):
		dlg = self.builder.get_object("dlgNewProfile")
		txNewProfile = self.builder.get_object("txNewProfile")
		rbNewProfile = self.builder.get_object("rbNewProfile")
		self.recursing = True
		rbNewProfile.set_active(True)
		txNewProfile.set_text(self.generate_new_name())
		txNewProfile._name = name
		txNewProfile._changed = False
		self.recursing = False
		dlg.set_transient_for(self.window)
		dlg.show()
	
	
	def on_action_chosen(self, id, action, mark_changed=True):
		before = self.set_action(self.current, id, action)
		if mark_changed:
			if before.to_string() != action.to_string():
				# TODO: Maybe better comparison
				self.undo.append(UndoRedo(id, before, action))
				self.builder.get_object("btUndo").set_sensitive(True)
			self.on_profile_modified()
		else:
			self.on_profile_modified(update_ui=False)
		return before
	
	
	def on_background_area_hover(self, trash, area):
		self.hint(area)
	
	
	def on_background_area_click(self, trash, area):
		if area in [ x.name for x in BUTTONS ]:
			self.hint(None)
			self.show_editor(getattr(SCButtons, area))
		elif area in TRIGGERS + STICKS + PADS:
			self.hint(None)
			self.show_editor(area)
	
	
	def on_vbc_allocated(self, vbc, allocation):
		"""
		Called when size of 'Button C' is changed. Centers button
		on background image
		"""
		main_area = self.builder.get_object("mainArea")
		x = (main_area.get_allocation().width - allocation.width) / 2
		y = main_area.get_allocation().height - allocation.height
		main_area.move(vbc, x, y)
	
	
	def on_ebImage_motion_notify_event(self, box, event):
		self.background.on_mouse_moved(event.x, event.y)
	
	
	def on_exiting_n_daemon_killed(self, *a):
		self.quit()
	
	
	def on_mnuExit_activate(self, *a):
		if self.app.config['gui']['autokill_daemon']:
			log.debug("Terminating scc-daemon")
			for x in ("content", "mnuEmulationEnabled", "mnuEmulationEnabledTray"):
				w = self.builder.get_object(x)
				w.set_sensitive(False)
			self.set_daemon_status("unknown", False)
			self.hide_error()
			if self.dm.is_alive():
				self.dm.connect("dead", self.on_exiting_n_daemon_killed)
				self.dm.connect("error", self.on_exiting_n_daemon_killed)
				self.dm.stop()
			else:
				# Daemon appears to be dead, kill it just in case
				self.dm.stop()
				self.quit()
		else:
			self.quit()
	
	
	def on_mnuAbout_activate(self, *a):
		from scc.gui.aboutdialog import AboutDialog
		AboutDialog(self).show(self.window)
	
	
	def on_daemon_alive(self, *a):
		self.set_daemon_status("alive", True)
		if not self.release_notes_visible():
			self.hide_error()
		self.just_started = False
		if self.osd_mode:
			self.enable_osd_mode()
		elif self.profile_switchers[0].get_file() is not None and not self.just_started:
			self.dm.set_profile(self.current_file.get_path())
		GLib.timeout_add_seconds(1, self.check)
		self.enable_test_mode()
	
	
	def on_daemon_ccunt_changed(self, daemon, count):
		if (self.controller_count, count) == (0, 1):
			# First controller connected
			# 
			# 'event' signal should be connected only on first controller,
			# so this block is executed only when number of connected
			# controllers changes from 0 to 1
			c = self.dm.get_controllers()[0]
			c.connect('event', self.on_daemon_event_observer)
		elif count > self.controller_count:
			# Controller added
			while len(self.profile_switchers) < count:
				s = self.add_switcher()
		elif count < self.controller_count:
			# Controller removed
			while len(self.profile_switchers) > max(1, count):
				s = self.profile_switchers.pop()
				s.set_controller(None)
				self.remove_switcher(s)
		
		# Assign controllers to widgets
		for i in xrange(0, count):
			c = self.dm.get_controllers()[i]
			self.profile_switchers[i].set_controller(c)
		
		if count < 1:
			# Special case, no controllers are connected, but one widget
			# has to stay on screen
			self.profile_switchers[0].set_controller(None)
		
		self.controller_count = count
	
	
	def new_profile(self, profile, name):
		filename = os.path.join(get_profiles_path(), name + ".sccprofile")
		self.current_file = Gio.File.new_for_path(filename)
		self.save_profile(self.current_file, profile)
	
	
	def add_switcher(self, margin_left=30, margin_right=40, margin_bottom=2):
		"""
		Adds new profile switcher widgets on top of window. Called
		when new controller is connected to daemon.
		
		Returns generated ProfileSwitcher instance.
		"""
		vbAllProfiles = self.builder.get_object("vbAllProfiles")
		
		ps = ProfileSwitcher(self.imagepath, self.config)
		ps.set_margin_left(margin_left)
		ps.set_margin_right(margin_right)
		ps.set_margin_bottom(margin_bottom)
		ps.connect('right-clicked', self.on_profile_right_clicked)
		
		vbAllProfiles.pack_start(ps, False, False, 0)
		vbAllProfiles.reorder_child(ps, 0)
		vbAllProfiles.show_all()
		
		if self.osd_mode:
			ps.set_allow_switch(False)
		
		if len(self.profile_switchers) > 0:
			ps.set_profile_list(self.profile_switchers[0].get_profile_list())
		
		self.profile_switchers.append(ps)
		ps.connect('changed', self.on_profile_selected)
		ps.connect('unknown-profile', self.on_unknown_profile)
		return ps
	
	
	def remove_switcher(self, s):
		"""
		Removes given profile switcher from UI.
		"""
		vbAllProfiles = self.builder.get_object("vbAllProfiles")
		vbAllProfiles.remove(s)
		s.destroy()
	
	
	def enable_test_mode(self):
		"""
		Disables and re-enables Input Test mode. If sniffing is disabled in
		daemon configuration, 2nd call fails and logs error.
		"""
		if self.dm.is_alive() and not self.osd_mode:
			try:
				c = self.dm.get_controllers()[0]
			except IndexError:
				# Zero controllers
				return
			c.unlock_all()
			c.observe(DaemonManager.nocallback, self.on_observe_failed,
				'A', 'B', 'C', 'X', 'Y', 'START', 'BACK', 'LB', 'RB',
				'LPAD', 'RPAD', 'LGRIP', 'RGRIP', 'LT', 'RT', 'LEFT',
				'RIGHT', 'STICK', 'STICKPRESS')
	
	
	def enable_osd_mode(self):
		# TODO: Support for multiple controllers here
		self.osd_mode_controller = 0
		osd_mode_profile = Profile(GuiActionParser())
		osd_mode_profile.load(find_profile(App.OSD_MODE_PROF_NAME))
		try:
			c = self.dm.get_controllers()[self.osd_mode_controller]
		except IndexError:
			log.error("osd_mode: Controller not connected")
			self.quit()
			return
		
		def on_lock_failed(*a):
			log.error("osd_mode: Locking failed")
			self.quit()
		
		def on_lock_success(*a):
			log.debug("osd_mode: Locked everything")
			from scc.gui.osd_mode_mapper import OSDModeMapper
			self.osd_mode_mapper = OSDModeMapper(osd_mode_profile)
			self.osd_mode_mapper.set_target_window(self.window.get_window())
			GLib.timeout_add(10, self.osd_mode_mapper.run_scheduled)
		
		# Locks everything but pads. Pads are emulating mouse and this is
		# better left in daemon - involving socket in mouse controls
		# adds too much lags.
		c.lock(on_lock_success, on_lock_failed,
			'A', 'B', 'X', 'Y', 'START', 'BACK', 'LB', 'RB', 'C', 'LPAD', 'RPAD',
			'STICK', 'LGRIP', 'RGRIP', 'LT', 'RT', 'STICKPRESS')
		
		# Ask daemon to temporaly reconfigure pads for mouse emulation
		c.replace(DaemonManager.nocallback, on_lock_failed,
			LEFT, osd_mode_profile.pads[LEFT])
		c.replace(DaemonManager.nocallback, on_lock_failed,
			RIGHT, osd_mode_profile.pads[RIGHT])
	
	
	def on_observe_failed(self, error):
		log.debug("Failed to enable test mode: %s", error)
	
	
	def on_daemon_version(self, daemon, version):
		"""
		Checks if reported version matches expected one.
		If not, daemon is restarted.
		"""
		if version != DAEMON_VERSION and self.outdated_version != version:
			log.warning(
				"Running daemon instance is too old (version %s, expected %s). Restarting...",
				version, DAEMON_VERSION)
			self.outdated_version = version
			self.set_daemon_status("unknown", False)
			self.dm.restart()
		else:
			# At this point, correct daemon version of daemon is running
			# and we can check if there is anything new to inform user about
			if self.app.config['gui']['news']['last_version'] != App.get_release():
				if self.app.config['gui']['news']['enabled']:
					if not self.osd_mode:
						self.check_release_notes()
	
	
	def on_daemon_error(self, daemon, error):
		log.debug("Daemon reported error '%s'", error)
		msg = _('There was an error with enabling emulation: <b>%s</b>') % (error,)
		# Known errors are handled with aditional message
		if "Device not found" in error:
			msg += "\n" + _("Please, check if you have reciever dongle connected to USB port.")
		elif "LIBUSB_ERROR_ACCESS" in error:
			msg += "\n" + _("You don't have access to controller device.")
			msg += "\n\n" + ( _("Consult your distribution manual, try installing Steam package or <a href='%s'>install required udev rules manually</a>.") %
					'https://wiki.archlinux.org/index.php/Gamepad#Steam_Controller_Not_Pairing' )
			# TODO: Write howto somewhere instead of linking to ArchWiki
		elif "LIBUSB_ERROR_BUSY" in error:
			msg += "\n" + _("Another application (most likely Steam) is using the controller.")
		elif "LIBUSB_ERROR_PIPE" in error:
			msg += "\n" + _("USB dongle was removed.")
		elif "Failed to create uinput device." in error:
			# Call check() method and try to determine what went wrong.
			if self.check():
				# Check() returns True if error was "handled".
				return
			# If check() fails to find error reason, error message is displayed as it is
		
		if self.osd_mode:
			self.quit()
		
		self.show_error(msg)
		self.set_daemon_status("error", True)
	
	
	def on_daemon_event_observer(self, daemon, what, data):
		if self.osd_mode_mapper:
			self.osd_mode_mapper.handle_event(daemon, what, data)
		elif what in (LEFT, RIGHT, STICK):
			widget, area = {
				LEFT  : (self.lpadTest,  "LPADTEST"),
				RIGHT : (self.rpadTest,  "RPADTEST"),
				STICK : (self.stickTest, "STICKTEST"),
			}[what]
			# Check if stick or pad is released
			if data[0] == data[1] == 0:
				widget.hide()
				return
			if not widget.is_visible():
				widget.show()
			# Grab values
			ax, ay, aw, trash = self.background.get_area_position(area)
			cw = widget.get_allocation().width
			# Compute center
			x, y = ax + aw * 0.5 - cw * 0.5, ay + 1.0 - cw * 0.5
			# Add pad position
			x += data[0] * aw / STICK_PAD_MAX * 0.5
			y -= data[1] * aw / STICK_PAD_MAX * 0.5
			# Move circle
			self.main_area.move(widget, x, y)
		elif what in ("LT", "RT", "STICKPRESS"):
			what = {
				"LT" : "LEFT",
				"RT" : "RIGHT",
				"STICKPRESS" : "STICK"
			}[what]
			if data[0]:
				self.hilights[App.OBSERVE_COLOR].add(what)
			else:
				self.hilights[App.OBSERVE_COLOR].remove(what)
			self._update_background()
		elif hasattr(SCButtons, what):
			try:
				if data[0]:
					self.hilights[App.OBSERVE_COLOR].add(what)
				else:
					self.hilights[App.OBSERVE_COLOR].remove(what)
				self._update_background()
			except KeyError, e:
				# Non fatal
				pass
		else:
Пример #35
0
class Keyboard(OSDWindow, TimerManager):
	EPILOG="""Exit codes:
   0  - clean exit, user closed keyboard
   1  - error, invalid arguments
   2  - error, failed to access sc-daemon, sc-daemon reported error or died while menu is displayed.
   3  - erorr, failed to lock input stick, pad or button(s)
	"""
	HILIGHT_COLOR = "#00688D"
	BUTTON_MAP = {
		SCButtons.A.name : Keys.KEY_ENTER,
		SCButtons.B.name : Keys.KEY_ESC,
		SCButtons.LB.name : Keys.KEY_BACKSPACE,
		SCButtons.RB.name : Keys.KEY_SPACE,
		SCButtons.LGRIP.name : Keys.KEY_LEFTSHIFT,
		SCButtons.RGRIP.name : Keys.KEY_RIGHTALT,
	}
	
	def __init__(self):
		OSDWindow.__init__(self, "osd-keyboard")
		TimerManager.__init__(self)
		self.daemon = None
		self.keyboard = None
		self.keymap = Gdk.Keymap.get_default()
		self.keymap.connect('state-changed', self.on_state_changed)
		
		
		kbimage = os.path.join(get_config_path(), 'keyboard.svg')
		if not os.path.exists(kbimage):
			# Prefer image in ~/.config/scc, but load default one as fallback
			kbimage = os.path.join(get_share_path(), "images", 'keyboard.svg')
		self.background = SVGWidget(self, kbimage)
		
		self.limit_left  = self.background.get_rect_area(self.background.get_element("LIMIT_LEFT"))
		self.limit_right = self.background.get_rect_area(self.background.get_element("LIMIT_RIGHT"))
		
		cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg')
		self.cursor_left = Gtk.Image.new_from_file(cursor)
		self.cursor_left.set_name("osd-keyboard-cursor")
		self.cursor_right = Gtk.Image.new_from_file(cursor)
		self.cursor_right.set_name("osd-keyboard-cursor")
		
		self._eh_ids = []
		self._stick = 0, 0
		self._hovers = { self.cursor_left : None, self.cursor_right : None }
		self._pressed = { self.cursor_left : None, self.cursor_right : None }
		
		self.c = Gtk.Box()
		self.c.set_name("osd-keyboard-container")
		
		self.f = Gtk.Fixed()
		self.f.add(self.background)
		self.f.add(self.cursor_left)
		self.f.add(self.cursor_right)
		self.c.add(self.f)
		self.add(self.c)
		
		self.set_cursor_position(0, 0, self.cursor_left, self.limit_left)
		self.set_cursor_position(0, 0, self.cursor_right, self.limit_right)
		
		self.timer('labels', 0.1, self.update_labels)
	
	
	def use_daemon(self, d):
		"""
		Allows (re)using already existing DaemonManager instance in same process
		"""
		self.daemon = d
		self._cononect_handlers()
		self.on_daemon_connected(self.daemon)
	
	
	def on_state_changed(self, x11keymap):
		if not self.timer_active('labels'):
			self.timer('labels', 0.1, self.update_labels)
	
	
	def update_labels(self):
		""" Updates keyboard labels based on active X keymap """
		labels = {}
		# Get current layout group
		dpy = X.Display(hash(GdkX11.x11_get_default_xdisplay()))		# Still no idea why...
		group = X.get_xkb_state(dpy).group
		# Get state of shift/alt/ctrl key
		mt = Gdk.ModifierType(self.keymap.get_modifier_state())
		for a in self.background.areas:
			# Iterate over all translatable keys...
			if hasattr(Keys, a.name) and getattr(Keys, a.name) in KEY_TO_GDK:
				# Try to convert GKD key to keycode
				gdkkey = KEY_TO_GDK[getattr(Keys, a.name)]
				found, entries = self.keymap.get_entries_for_keyval(gdkkey)
				
				if gdkkey == Gdk.KEY_equal:
					# Special case, GDK reports nonsense here
					entries = [ [ e for e in entries if e.level == 0 ][-1] ]
				
				if not found: continue
				for k in sorted(entries, key=lambda a : a.level):
					# Try to convert keycode to label
					code = Gdk.keyval_to_unicode(
						self.keymap.translate_keyboard_state(k.keycode, mt, group)
						.keyval)
					if code != 0:
						labels[a.name] = unichr(code)
						break
		
		self.background.set_labels(labels)
	
	
	def parse_argumets(self, argv):
		if not OSDWindow.parse_argumets(self, argv):
			return False
		return True
	
	
	def _cononect_handlers(self):
		self._eh_ids += [
			self.daemon.connect('dead', self.on_daemon_died),
			self.daemon.connect('error', self.on_daemon_died),
			self.daemon.connect('event', self.on_event),
			self.daemon.connect('alive', self.on_daemon_connected),
		]
	
	
	def run(self):
		self.daemon = DaemonManager()
		self._cononect_handlers()
		OSDWindow.run(self)
	
	
	def on_daemon_died(self, *a):
		log.error("Daemon died")
		self.quit(2)
	
	
	def on_failed_to_lock(self, error):
		log.error("Failed to lock input: %s", error)
		self.quit(3)
	
	
	def on_daemon_connected(self, *a):
		def success(*a):
			log.info("Sucessfully locked input")
			pass
		
		# Lock everything just in case
		locks = [ LEFT, RIGHT, STICK ] + [ b.name for b in SCButtons ]
		self.daemon.lock(success, self.on_failed_to_lock, *locks)
	
	
	def quit(self, code=-1):
		self.daemon.unlock_all()
		for x in self._eh_ids:
			self.daemon.disconnect(x)
		self._eh_ids = []
		del self.keyboard
		OSDWindow.quit(self, code)
	
	
	def show(self, *a):
		OSDWindow.show(self, *a)
		self.keyboard = uinputKeyboard(b"SCC OSD Keyboard")
	
	
	def set_cursor_position(self, x, y, cursor, limit):
		"""
		Moves cursor image.
		"""
		w = limit[2] - (cursor.get_allocation().width * 0.5)
		h = limit[3] - (cursor.get_allocation().height * 0.5)
		x = x / float(STICK_PAD_MAX)
		y = y / float(STICK_PAD_MAX) * -1.0
		
		x, y = circle_to_square(x, y)
		
		x = (limit[0] + w * 0.5) + x * w * 0.5
		y = (limit[1] + h * 0.5) + y * h * 0.5
		
		self.f.move(cursor, int(x), int(y))
		for a in self.background.areas:
			if a.contains(x, y):
				if a != self._hovers[cursor]:
					self._hovers[cursor] = a
					if self._pressed[cursor] is not None:
						self.keyboard.releaseEvent([ self._pressed[cursor] ])
						self.key_from_cursor(cursor, True)
					if not self.timer_active('redraw'):
						self.timer('redraw', 0.01, self.redraw_background)
					break
	
	
	def redraw_background(self, *a):
		"""
		Updates hilighted keys on bacgkround image.
		"""
		self.background.hilight({
			"AREA_" + a.name : Keyboard.HILIGHT_COLOR
			for a in [ a for a in self._hovers.values() if a ]
		})
	
	
	def on_event(self, daemon, what, data):
		"""
		Called when button press, button release or stick / pad update is
		send by daemon.
		"""
		if what == LEFT:
			x, y = data
			self.set_cursor_position(x, y, self.cursor_left, self.limit_left)
		elif what == RIGHT:
			x, y = data
			self.set_cursor_position(x, y, self.cursor_right, self.limit_right)
		elif what == STICK:
			self._stick = data
			if not self.timer_active('stick'):
				self.timer("stick", 0.05, self._move_window)
		elif what == SCButtons.LPAD.name:
			self.key_from_cursor(self.cursor_left, data[0] == 1)
		elif what == SCButtons.RPAD.name:
			self.key_from_cursor(self.cursor_right, data[0] == 1)
		elif what in (SCButtons.RPADTOUCH.name, SCButtons.LPADTOUCH.name):
			pass
		elif what in self.BUTTON_MAP:
			if data[0]:
				self.keyboard.pressEvent([ self.BUTTON_MAP[what] ])
			else:
				self.keyboard.releaseEvent([ self.BUTTON_MAP[what] ])
		elif what in [ b.name for b in SCButtons ]:
			self.quit(0)
	
	
	def _move_window(self, *a):
		"""
		Called by timer while stick is tilted to move window around the screen.
		"""
		x, y = self._stick
		x = x * 50.0 / STICK_PAD_MAX
		y = y * -50.0 / STICK_PAD_MAX
		rx, ry = self.get_position()
		self.move(rx + x, ry + y)
		if abs(self._stick[0]) > 100 or abs(self._stick[1]) > 100:
			self.timer("stick", 0.05, self._move_window)
	
	
	def key_from_cursor(self, cursor, pressed):
		"""
		Sends keypress/keyrelease event to emulated keyboard, based on
		position of cursor on OSD keyboard.
		"""
		x = self.f.child_get_property(cursor, "x")
		y = self.f.child_get_property(cursor, "y")
		
		if pressed:
			for a in self.background.areas:
				if a.contains(x, y):
					if a.name.startswith("KEY_") and hasattr(Keys, a.name):
						key = getattr(Keys, a.name)
						if self._pressed[cursor] is not None:
							self.keyboard.releaseEvent([ self._pressed[cursor] ])
						self.keyboard.pressEvent([ key ])
						self._pressed[cursor] = key
					break
		elif self._pressed[cursor] is not None:
			self.keyboard.releaseEvent([ self._pressed[cursor] ])
			self._pressed[cursor] = None
Пример #36
0
class App(Gtk.Application, ProfileManager):
	"""
	Main application / window.
	"""
	
	IMAGE = "background.svg"
	HILIGHT_COLOR = "#FF00FF00"		# ARGB
	CONFIG = "scc.config.json"
	
	def __init__(self, gladepath="/usr/share/scc",
						imagepath="/usr/share/scc/images"):
		Gtk.Application.__init__(self,
				application_id="me.kozec.scc",
				flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE | Gio.ApplicationFlags.NON_UNIQUE )
		ProfileManager.__init__(self)
		# Setup Gtk.Application
		self.setup_commandline()
		# Setup DaemonManager
		self.dm = DaemonManager()
		self.dm.connect("alive", self.on_daemon_alive)
		self.dm.connect("profile-changed", self.on_daemon_profile_changed)
		self.dm.connect("error", self.on_daemon_error)
		self.dm.connect("dead", self.on_daemon_dead)
		# Set variables
		self.gladepath = gladepath
		self.imagepath = imagepath
		self.builder = None
		self.recursing = False
		self.daemon_changed_profile = False
		self.background = None
		self.current = Profile(GuiActionParser())
		self.current_file = None
		self.just_started = True
		self.button_widgets = {}
		self.undo = []
		self.redo = []
	
	
	def setup_widgets(self):
		self.builder = Gtk.Builder()
		self.builder.add_from_file(os.path.join(self.gladepath, "app.glade"))
		self.builder.connect_signals(self)
		self.window = self.builder.get_object("window")
		self.add_window(self.window)
		self.window.set_title(_("SC Controller"))
		self.window.set_wmclass("SC Controller", "SC Controller")
		self.ribar = None
		
		for b in BUTTONS:
			self.button_widgets[b] = ControllerButton(self, b, self.builder.get_object("bt" + b.name))
		for b in TRIGGERS:
			self.button_widgets[b] = ControllerTrigger(self, b, self.builder.get_object("btTrigger" + b))
		for b in PADS:
			self.button_widgets[b] = ControllerPad(self, b, self.builder.get_object("bt" + b))
		for b in STICKS:
			self.button_widgets[b] = ControllerStick(self, b, self.builder.get_object("bt" + b))
		for b in GYROS:
			self.button_widgets[b] = ControllerGyro(self, b, self.builder.get_object("bt" + b))
		
		self.builder.get_object("cbProfile").set_row_separator_func(
			lambda model, iter : model.get_value(iter, 1) is None and model.get_value(iter, 0) == "-" )
		
		self.set_daemon_status("unknown")
		
		vbc = self.builder.get_object("vbC")
		main_area = self.builder.get_object("mainArea")
		vbc.get_parent().remove(vbc)
		vbc.connect('size-allocate', self.on_vbc_allocated)
		self.background = SVGWidget(self, os.path.join(self.imagepath, self.IMAGE))
		self.background.connect('hover', self.on_background_area_hover)
		self.background.connect('leave', self.on_background_area_hover, None)
		self.background.connect('click', self.on_background_area_click)
		main_area.put(self.background, 0, 0)
		main_area.put(vbc, 0, 0) # (self.IMAGE_SIZE[0] / 2) - 90, self.IMAGE_SIZE[1] - 100)
		headerbar(self.builder.get_object("hbWindow"))
	
	
	def check(self):
		""" Performs various (two) checks and reports possible problems """
		# TODO: Maybe not best place to do this
		if os.path.exists("/dev/uinput"):
			if not os.access("/dev/uinput", os.R_OK | os.W_OK):
				# Cannot acces uinput
				msg = _('You don\'t have required access to /dev/uinput.')
				msg += "\n" + _('This will most likely prevent emulation from working.')
				msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput')
				self.show_error(msg)
		else:
			# There is no uinput
			msg = _('/dev/uinput not found')
			msg += "\n" + _('Your kernel is either outdated or compiled without uinput support.')
			msg += "\n\n" + _('Please, consult your distribution manual on how to enable uinput')
			self.show_error(msg)
	
	
	def hilight(self, button):
		""" Hilights specified button on background image """
		self.background.hilight({ button : App.HILIGHT_COLOR })
	
	
	def hint(self, button):
		""" As hilight, but marks GTK Button as well """
		active = None
		for b in self.button_widgets.values():
			b.widget.set_state(Gtk.StateType.NORMAL)
			if b.name == button:
				active = b.widget
		
		if active is not None:
			active.set_state(Gtk.StateType.ACTIVE)
		
		self.hilight(button)
	
	
	def _choose_editor(self, action, title):
		if isinstance(action, SensitivityModifier):
			action = action.action
		if isinstance(action, ModeModifier) and not is_gyro_enable(action):
			e = ModeshiftEditor(self, self.on_action_chosen)
			e.set_title(_("Mode Shift for %s") % (title,))
		elif isinstance(action, Macro):
			e = MacroEditor(self, self.on_action_chosen)
			e.set_title(_("Macro for %s") % (title,))
		else:
			e = ActionEditor(self, self.on_action_chosen)
			e.set_title(title)
		return e
		
	
	def show_editor(self, id, press=False):
		if id in SCButtons:
			title = _("%s Button") % (id.name,)
			if press:
				title = _("%s Press") % (id.name,)
			ae = self._choose_editor(self.current.buttons[id], title)
			ae.set_button(id, self.current.buttons[id])
			ae.show(self.window)
		elif id in TRIGGERS:
			ae = self._choose_editor(self.current.triggers[id],
				_("%s Trigger") % (id,))
			ae.set_trigger(id, self.current.triggers[id])
			ae.show(self.window)
		elif id in STICKS:
			ae = self._choose_editor(self.current.stick,
				_("Stick"))
			ae.set_stick(self.current.stick)
			ae.show(self.window)
		elif id in GYROS:
			ae = self._choose_editor(self.current.gyro,
				_("Gyro"))
			ae.set_gyro(self.current.gyro)
			ae.show(self.window)
		elif id in PADS:
			data = NoAction()
			if id == "LPAD":
				data = self.current.pads[Profile.LEFT]
				ae = self._choose_editor(data, _("Left Pad"))
			else:
				data = self.current.pads[Profile.RIGHT]
				ae = self._choose_editor(data, _("Right Pad"))
			ae.set_pad(id, data)
			ae.show(self.window)
	
	
	def on_btUndo_clicked(self, *a):
		if len(self.undo) < 1: return
		undo, self.undo = self.undo[-1], self.undo[0:-1]
		self._set_action(undo.id, undo.before)
		self.redo.append(undo)
		self.builder.get_object("btRedo").set_sensitive(True)
		if len(self.undo) < 1:
			self.builder.get_object("btUndo").set_sensitive(False)
		self.on_profile_changed()
	
	
	def on_btRedo_clicked(self, *a):
		if len(self.redo) < 1: return
		redo, self.redo = self.redo[-1], self.redo[0:-1]
		self._set_action(redo.id, redo.after)
		self.undo.append(redo)
		self.builder.get_object("btUndo").set_sensitive(True)
		if len(self.redo) < 1:
			self.builder.get_object("btRedo").set_sensitive(False)
		self.on_profile_changed()
	
	
	def on_profiles_loaded(self, profiles):
		cb = self.builder.get_object("cbProfile")
		model = cb.get_model()
		model.clear()
		i = 0
		current_profile, current_index = self.load_profile_selection(), 0
		for f in profiles:
			name = f.get_basename()
			if name.endswith(".mod"):
				continue
			if name.endswith(".sccprofile"):
				name = name[0:-11]
			if name == current_profile:
				current_index = i
			model.append((name, f, None))
			i += 1
		model.append(("-", None, None))
		model.append((_("New profile..."), None, None))
		
		if cb.get_active_iter() is None:
			cb.set_active(current_index)
	
	
	def on_cbProfile_changed(self, cb, *a):
		""" Called when user chooses profile in selection combo """
		if self.recursing : return
		model = cb.get_model()
		iter = cb.get_active_iter()
		f = model.get_value(iter, 1)
		if f is None:
			if self.current_file is None:
				cb.set_active(0)
			else:
				self.select_profile(self.current_file.get_path())
			
			new_name = os.path.split(self.current_file.get_path())[-1]
			if new_name.endswith(".mod"): new_name = new_name[0:-4]
			if new_name.endswith(".sccprofile"): new_name = new_name[0:-11]
			new_name = _("Copy of") + " " + new_name
			dlg = self.builder.get_object("dlgNewProfile")
			txNewProfile = self.builder.get_object("txNewProfile")
			txNewProfile.set_text(new_name)
			dlg.set_transient_for(self.window)
			dlg.show()
		else:
			self.load_profile(f)
			if not self.daemon_changed_profile:
				self.dm.set_profile(f.get_path())
			self.save_profile_selection(f.get_path())
	
	
	def on_dlgNewProfile_delete_event(self, dlg, *a):
		dlg.hide()
		return True
	
	
	def on_btNewProfile_clicked(self, *a):
		""" Called when new profile name is set and OK is clicked """
		txNewProfile = self.builder.get_object("txNewProfile")
		dlg = self.builder.get_object("dlgNewProfile")
		cb = self.builder.get_object("cbProfile")
		name = txNewProfile.get_text()
		filename = txNewProfile.get_text() + ".sccprofile"
		path = os.path.join(get_profiles_path(), filename)
		self.current_file = Gio.File.new_for_path(path)
		self.recursing = True
		model = cb.get_model()
		model.insert(0, ( name, self.current_file, None ))
		cb.set_active(0)
		self.recursing = False
		self.save_profile(self.current_file, self.current)
		dlg.hide()
	
	
	def on_profile_loaded(self, profile, giofile):
		self.current = profile
		self.current_file = giofile
		for b in self.button_widgets.values():
			b.update()
		self.on_profile_saved(giofile, False)	# Just to indicate that there are no changes to save
	
	
	def on_profile_changed(self, *a):
		"""
		Called when selected profile is modified in memory.
		Displays "changed" next to profile name and shows Save button.
		
		If sccdaemon is alive, creates 'original.filename.mod' and loads it
		into daemon to activate changes right away.
		"""
		cb = self.builder.get_object("cbProfile")
		self.builder.get_object("rvProfileChanged").set_reveal_child(True)
		model = cb.get_model()
		for row in model:
			if self.current_file == model.get_value(row.iter, 1):
				model.set_value(row.iter, 2, _("(changed)"))
				break
		
		if self.dm.is_alive():
			modfile = Gio.File.new_for_path(self.current_file.get_path() + ".mod")
			self.save_profile(modfile, self.current)
	
	
	def on_profile_saved(self, giofile, send=True):
		"""
		Called when selected profile is saved to disk
		"""
		if giofile.get_path().endswith(".mod"):
			# Special case, this one is saved only to be sent to daemon
			# and user doesn't need to know about it
			if self.dm.is_alive():
				self.dm.set_profile(giofile.get_path())
			return
		
		cb = self.builder.get_object("cbProfile")
		self.builder.get_object("rvProfileChanged").set_reveal_child(False)
		model = cb.get_model()
		for row in model:
			if model.get_value(row.iter, 1) == self.current_file:
				model.set_value(row.iter, 1, giofile)
			model.set_value(row.iter, 2, None)
		
		if send and self.dm.is_alive() and not self.daemon_changed_profile:
			self.dm.set_profile(giofile.get_path())
		
		self.current_file = giofile
	
	
	def on_btSaveProfile_clicked(self, *a):
		self.save_profile(self.current_file, self.current)
	
	
	def on_action_chosen(self, id, action, reopen=False):
		before = self._set_action(id, action)
		if type(before) != type(action) or before.to_string() != action.to_string():
			# TODO: Maybe better comparison
			self.undo.append(UndoRedo(id, before, action))
			self.builder.get_object("btUndo").set_sensitive(True)
		self.on_profile_changed()
		if reopen:
			self.show_editor(id)
	
	
	def _set_action(self, id, action):
		"""
		Stores action in profile.
		Returns formely stored action.
		"""
		before = NoAction()
		if id in BUTTONS:
			before, self.current.buttons[id] = self.current.buttons[id], action
			self.button_widgets[id].update()
		if id in PRESSABLE:
			before, self.current.buttons[id] = self.current.buttons[id], action
			self.button_widgets[id.name].update()
		elif id in TRIGGERS:
			before, self.current.triggers[id] = self.current.triggers[id], action
			self.button_widgets[id].update()
		elif id in GYROS:
			before, self.current.gyro = self.current.gyro, action
			self.button_widgets[id].update()
		elif id in STICKS + PADS:
			if id in STICKS:
				before, self.current.stick = self.current.stick, action
			elif id == "LPAD":
				before, self.current.pads[Profile.LEFT] = self.current.pads[Profile.LEFT], action
			else:
				before, self.current.pads[Profile.RIGHT] = self.current.pads[Profile.RIGHT], action
			self.button_widgets[id].update()
		return before
	
	
	def on_background_area_hover(self, trash, area):
		self.hint(area)
	
	
	def on_background_area_click(self, trash, area):
		if area in [ x.name for x in BUTTONS ]:
			self.hint(None)
			self.show_editor(getattr(SCButtons, area))
		elif area in TRIGGERS + STICKS + PADS:
			self.hint(None)
			self.show_editor(area)
	
	
	def on_vbc_allocated(self, vbc, allocation):
		"""
		Called when size of 'Button C' is changed. Centers button
		on background image
		"""
		main_area = self.builder.get_object("mainArea")
		x = (main_area.get_allocation().width - allocation.width) / 2
		y = main_area.get_allocation().height - allocation.height
		main_area.move(vbc, x, y)
	
	
	def on_ebImage_motion_notify_event(self, box, event):
		self.background.on_mouse_moved(event.x, event.y)
	
	
	def on_mnuExit_activate(self, *a):
		self.quit()
	
	
	def on_daemon_alive(self, *a):
		self.set_daemon_status("alive")
		self.hide_error()
		if self.current_file is not None and not self.just_started:
			self.dm.set_profile(self.current_file.get_path())
	
	
	def on_daemon_error(self, trash, error):
		log.debug("Daemon reported error '%s'", error)
		msg = _('There was an error with enabling emulation: <b>%s</b>') % (error,)
		# Known errors are handled with aditional message
		if "Device not found" in error:
			msg += "\n" + _("Please, check if you have reciever dongle connected to USB port.")
		elif "LIBUSB_ERROR_ACCESS" in error:
			msg += "\n" + _("You don't have access to controller device.")
			msg += "\n\n" + ( _("Consult your distribution manual, try installing Steam package or <a href='%s'>install required udev rules manually</a>.") %
					'https://wiki.archlinux.org/index.php/Gamepad#Steam_Controller_Not_Pairing' )
			# TODO: Write howto somewhere instead of linking to ArchWiki
		elif "LIBUSB_ERROR_BUSY" in error:
			msg += "\n" + _("Another application (most likely Steam) is using the controller.")
		elif "LIBUSB_ERROR_PIPE" in error:
			msg += "\n" + _("USB dongle was removed.")
		
		self.show_error(msg)
		self.set_daemon_status("dead")
	
	
	def show_error(self, message):
		if self.ribar is None:
			self.ribar = RIBar(message, Gtk.MessageType.ERROR)
			content = self.builder.get_object("content")
			content.pack_start(self.ribar, False, False, 1)
			content.reorder_child(self.ribar, 0)
			self.ribar.connect("close", self.hide_error)
			self.ribar.connect("response", self.hide_error)
		else:
			self.ribar.get_label().set_markup(message)
		self.ribar.show()
		self.ribar.set_reveal_child(True)
	
	
	def hide_error(self, *a):
		if self.ribar is not None:
			if self.ribar.get_parent() is not None:
				self.ribar.get_parent().remove(self.ribar)
		self.ribar = None
	
	
	def on_daemon_profile_changed(self, trash, profile):
		current_changed = self.builder.get_object("rvProfileChanged").get_reveal_child()
		if profile.endswith(".mod"):
			try:
				os.unlink(profile)
			except Exception, e:
				log.warning("Failed to remove .mod file")
				log.warning(e)
		if self.just_started or not current_changed:
			log.debug("Daemon uses profile '%s', selecting it in UI", profile)
			self.daemon_changed_profile = True
			found = self.select_profile(profile)
			self.daemon_changed_profile = False
			if not found:
				# Daemon uses unknown profile, override it with something I know about
				if self.current_file is not None:
					self.dm.set_profile(self.current_file.get_path())
				
		self.just_started = False
Пример #37
0
class Keyboard(OSDWindow, TimerManager):
    EPILOG = """Exit codes:
   0  - clean exit, user closed keyboard
   1  - error, invalid arguments
   2  - error, failed to access sc-daemon, sc-daemon reported error or died while menu is displayed.
   3  - erorr, failed to lock input stick, pad or button(s)
	"""
    HILIGHT_COLOR = "#00688D"
    BUTTON_MAP = {
        SCButtons.A.name: Keys.KEY_ENTER,
        SCButtons.B.name: Keys.KEY_ESC,
        SCButtons.LB.name: Keys.KEY_BACKSPACE,
        SCButtons.RB.name: Keys.KEY_SPACE,
        SCButtons.LGRIP.name: Keys.KEY_LEFTSHIFT,
        SCButtons.RGRIP.name: Keys.KEY_RIGHTALT,
    }

    def __init__(self):
        OSDWindow.__init__(self, "osd-keyboard")
        TimerManager.__init__(self)
        self.daemon = None
        self.mapper = None
        self.keymap = Gdk.Keymap.get_default()
        self.keymap.connect('state-changed', self.on_state_changed)
        self.profile = Profile(TalkingActionParser())

        kbimage = os.path.join(get_config_path(), 'keyboard.svg')
        if not os.path.exists(kbimage):
            # Prefer image in ~/.config/scc, but load default one as fallback
            kbimage = os.path.join(get_share_path(), "images", 'keyboard.svg')
        self.background = SVGWidget(self, kbimage)

        self.limits = {}
        self.limits[LEFT] = self.background.get_rect_area(
            self.background.get_element("LIMIT_LEFT"))
        self.limits[RIGHT] = self.background.get_rect_area(
            self.background.get_element("LIMIT_RIGHT"))

        cursor = os.path.join(get_share_path(), "images", 'menu-cursor.svg')
        self.cursors = {}
        self.cursors[LEFT] = Gtk.Image.new_from_file(cursor)
        self.cursors[LEFT].set_name("osd-keyboard-cursor")
        self.cursors[RIGHT] = Gtk.Image.new_from_file(cursor)
        self.cursors[RIGHT].set_name("osd-keyboard-cursor")

        self._eh_ids = []
        self._stick = 0, 0
        self._hovers = {self.cursors[LEFT]: None, self.cursors[RIGHT]: None}
        self._pressed = {self.cursors[LEFT]: None, self.cursors[RIGHT]: None}

        self.c = Gtk.Box()
        self.c.set_name("osd-keyboard-container")

        self.f = Gtk.Fixed()
        self.f.add(self.background)
        self.f.add(self.cursors[LEFT])
        self.f.add(self.cursors[RIGHT])
        self.c.add(self.f)
        self.add(self.c)

        self.timer('labels', 0.1, self.update_labels)

    def use_daemon(self, d):
        """
		Allows (re)using already existing DaemonManager instance in same process
		"""
        self.daemon = d
        self._cononect_handlers()
        self.on_daemon_connected(self.daemon)

    def on_state_changed(self, x11keymap):
        if not self.timer_active('labels'):
            self.timer('labels', 0.1, self.update_labels)

    def update_labels(self):
        """ Updates keyboard labels based on active X keymap """
        labels = {}
        # Get current layout group
        dpy = X.Display(hash(
            GdkX11.x11_get_default_xdisplay()))  # Still no idea why...
        group = X.get_xkb_state(dpy).group
        # Get state of shift/alt/ctrl key
        mt = Gdk.ModifierType(self.keymap.get_modifier_state())
        for a in self.background.areas:
            # Iterate over all translatable keys...
            if hasattr(Keys, a.name) and getattr(Keys, a.name) in KEY_TO_GDK:
                # Try to convert GKD key to keycode
                gdkkey = KEY_TO_GDK[getattr(Keys, a.name)]
                found, entries = self.keymap.get_entries_for_keyval(gdkkey)

                if gdkkey == Gdk.KEY_equal:
                    # Special case, GDK reports nonsense here
                    entries = [[e for e in entries if e.level == 0][-1]]

                if not found: continue
                for k in sorted(entries, key=lambda a: a.level):
                    # Try to convert keycode to label
                    translation = self.keymap.translate_keyboard_state(
                        k.keycode, mt, group)
                    if hasattr(translation, "keyval"):
                        code = Gdk.keyval_to_unicode(translation.keyval)
                    else:
                        code = Gdk.keyval_to_unicode(translation[1])
                    if code != 0:
                        labels[a.name] = unichr(code)
                        break

        self.background.set_labels(labels)

    def parse_argumets(self, argv):
        if not OSDWindow.parse_argumets(self, argv):
            return False
        return True

    def _cononect_handlers(self):
        self._eh_ids += [
            self.daemon.connect('dead', self.on_daemon_died),
            self.daemon.connect('error', self.on_daemon_died),
            self.daemon.connect('event', self.on_event),
            self.daemon.connect('alive', self.on_daemon_connected),
        ]

    def run(self):
        self.daemon = DaemonManager()
        self._cononect_handlers()
        OSDWindow.run(self)

    def on_daemon_died(self, *a):
        log.error("Daemon died")
        self.quit(2)

    def on_failed_to_lock(self, error):
        log.error("Failed to lock input: %s", error)
        self.quit(3)

    def on_daemon_connected(self, *a):
        def success(*a):
            log.info("Sucessfully locked input")
            pass

        # Lock everything
        locks = [LEFT, RIGHT, STICK] + [b.name for b in SCButtons]
        self.daemon.lock(success, self.on_failed_to_lock, *locks)

    def quit(self, code=-1):
        self.daemon.unlock_all()
        for x in self._eh_ids:
            self.daemon.disconnect(x)
        self._eh_ids = []
        del self.mapper
        OSDWindow.quit(self, code)

    def show(self, *a):
        OSDWindow.show(self, *a)
        self.profile.load(find_profile(".scc-osd.keyboard")).compress()
        self.mapper = SlaveMapper(self.profile, keyboard=b"SCC OSD Keyboard")
        self.mapper.set_special_actions_handler(self)
        self.set_cursor_position(0, 0, self.cursors[LEFT], self.limits[LEFT])
        self.set_cursor_position(0, 0, self.cursors[RIGHT], self.limits[RIGHT])

    def on_event(self, daemon, what, data):
        """
		Called when button press, button release or stick / pad update is
		send by daemon.
		"""
        self.mapper.handle_event(daemon, what, data)

    def on_sa_close(self, *a):
        """ Called by CloseOSDKeyboardAction """
        self.quit(0)

    def on_sa_cursor(self, mapper, action, x, y):
        self.set_cursor_position(x * action.speed[0], y * action.speed[1],
                                 self.cursors[action.side],
                                 self.limits[action.side])

    def on_sa_move(self, mapper, action, x, y):
        self._stick = x, y
        if not self.timer_active('stick'):
            self.timer("stick", 0.05, self._move_window)

    def on_sa_press(self, mapper, action, pressed):
        self.key_from_cursor(self.cursors[action.side], pressed)

    def set_cursor_position(self, x, y, cursor, limit):
        """
		Moves cursor image.
		"""
        w = limit[2] - (cursor.get_allocation().width * 0.5)
        h = limit[3] - (cursor.get_allocation().height * 0.5)
        x = x / float(STICK_PAD_MAX)
        y = y / float(STICK_PAD_MAX) * -1.0

        x, y = circle_to_square(x, y)

        x = clamp(cursor.get_allocation().width * 0.5,
                  (limit[0] + w * 0.5) + x * w * 0.5,
                  self.get_allocation().width - cursor.get_allocation().width
                  ) - cursor.get_allocation().width * 0.5

        y = clamp(
            cursor.get_allocation().height * 0.5,
            (limit[1] + h * 0.5) + y * h * 0.5,
            self.get_allocation().height - cursor.get_allocation().height
        ) - cursor.get_allocation().height * 0.5

        cursor.position = int(x), int(y)
        self.f.move(cursor, *cursor.position)
        for a in self.background.areas:
            if a.contains(x, y):
                if a != self._hovers[cursor]:
                    self._hovers[cursor] = a
                    if self._pressed[cursor] is not None:
                        self.mapper.keyboard.releaseEvent(
                            [self._pressed[cursor]])
                        self.key_from_cursor(cursor, True)
                    if not self.timer_active('redraw'):
                        self.timer('redraw', 0.01, self.redraw_background)
                    break

    def redraw_background(self, *a):
        """
		Updates hilighted keys on bacgkround image.
		"""
        self.background.hilight({
            "AREA_" + a.name: Keyboard.HILIGHT_COLOR
            for a in [a for a in self._hovers.values() if a]
        })

    def _move_window(self, *a):
        """
		Called by timer while stick is tilted to move window around the screen.
		"""
        x, y = self._stick
        x = x * 50.0 / STICK_PAD_MAX
        y = y * -50.0 / STICK_PAD_MAX
        rx, ry = self.get_position()
        self.move(rx + x, ry + y)
        if abs(self._stick[0]) > 100 or abs(self._stick[1]) > 100:
            self.timer("stick", 0.05, self._move_window)

    def key_from_cursor(self, cursor, pressed):
        """
		Sends keypress/keyrelease event to emulated keyboard, based on
		position of cursor on OSD keyboard.
		"""
        x, y = cursor.position

        if pressed:
            for a in self.background.areas:
                if a.contains(x, y):
                    if a.name.startswith("KEY_") and hasattr(Keys, a.name):
                        key = getattr(Keys, a.name)
                        if self._pressed[cursor] is not None:
                            self.mapper.keyboard.releaseEvent(
                                [self._pressed[cursor]])
                        self.mapper.keyboard.pressEvent([key])
                        self._pressed[cursor] = key
                    break
        elif self._pressed[cursor] is not None:
            self.mapper.keyboard.releaseEvent([self._pressed[cursor]])
            self._pressed[cursor] = None