From 6a14aff75c350eeee70aeb5ca73b04313a21d1d9 Mon Sep 17 00:00:00 2001 From: predr Date: Sun, 17 Sep 2017 15:40:57 +0200 Subject: [PATCH 01/44] ignore out.svg and pycharm ide --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7bbd4b6c0..f07381b1e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ build/ .atomignore commit* -*test.* \ No newline at end of file +*test.* +.idea/ +out.svg From 90009a8e58b735d3349b148e71d2ad3b97884d30 Mon Sep 17 00:00:00 2001 From: predr Date: Sun, 17 Sep 2017 15:57:46 +0200 Subject: [PATCH 02/44] add separators to submenus, used as menu titles --- default_menus/.games.menu | 3 +++ default_menus/.windowlist.menu | 3 +++ default_menus/Profiles.menu | 3 +++ 3 files changed, 9 insertions(+) diff --git a/default_menus/.games.menu b/default_menus/.games.menu index 0e488f719..7f6b91b4a 100644 --- a/default_menus/.games.menu +++ b/default_menus/.games.menu @@ -1,3 +1,6 @@ [{ + "separator": true, + "name": "Games" +}, { "generator" : "games" }] diff --git a/default_menus/.windowlist.menu b/default_menus/.windowlist.menu index 2467afa53..6439d92b7 100644 --- a/default_menus/.windowlist.menu +++ b/default_menus/.windowlist.menu @@ -1,3 +1,6 @@ [{ + "separator": true, + "name": "Switch to" +}, { "generator" : "windowlist" }] diff --git a/default_menus/Profiles.menu b/default_menus/Profiles.menu index 6fba7a8c1..59f8d725e 100644 --- a/default_menus/Profiles.menu +++ b/default_menus/Profiles.menu @@ -1,3 +1,6 @@ [{ + "separator": true, + "name": "All profiles" +}, { "generator" : "profiles" }] From 419bad9b30d62792c3ffe4e3fa13886c519e8447 Mon Sep 17 00:00:00 2001 From: predr Date: Sun, 17 Sep 2017 16:04:31 +0200 Subject: [PATCH 03/44] double boarders to solid, opacity to all windows, padding and left border for messages --- scc/osd/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scc/osd/__init__.py b/scc/osd/__init__.py index cc7995fa9..aff064770 100644 --- a/scc/osd/__init__.py +++ b/scc/osd/__init__.py @@ -21,7 +21,12 @@ class OSDWindow(Gtk.Window): CSS = """ #osd-message, #osd-menu, #osd-gesture, #osd-keyboard { background-color: #%(background)s; - border: 6px #%(border)s double; + border: 6px #%(border)s solid; + opacity: 0.95; + } + + #osd-message { + border-left: 5px #%(menuitem_hilight_text)s solid; } #osd-area { @@ -32,7 +37,7 @@ class OSDWindow(Gtk.Window): color: #%(text)s; border: none; font-size: xx-large; - margin: 15px 15px 15px 15px; + padding: 15px 15px 15px 15px; } #osd-menu, #osd-gesture { From a17869173715dfe54df66e52057a7546ce384e17 Mon Sep 17 00:00:00 2001 From: predr Date: Sun, 17 Sep 2017 16:10:47 +0200 Subject: [PATCH 04/44] colors for osd/osk, todo opacity config --- scc/config.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/scc/config.py b/scc/config.py index b15ae80a9..4aafb091c 100644 --- a/scc/config.py +++ b/scc/config.py @@ -68,24 +68,24 @@ class Config(object): "enable_sniffing" : False, # Colors used by OSD "osd_colors": { - "background": "160c00", - "border": "00FF00", - "text": "00FF00", - "menuitem_border": "004000", - "menuitem_hilight": "000070", - "menuitem_hilight_text": "FFFFFF", - "menuitem_hilight_border": "00FF00", - "menuseparator": "109010", + "background": "101010", + "border": "101010", + "text": "16BF24", + "menuitem_border": "101010", + "menuitem_hilight": "202020", + "menuitem_hilight_text": "16FF26", + "menuitem_hilight_border": "16FF26", + "menuseparator": "2e3436", }, # Colors used by on-screen keyboard "osk_colors": { - 'hilight' : '00688D', - 'pressed' : '1A9485', - "button1" : "162082", - "button1_border" : "262b5e", - "button2" : "162d44", - "button2_border" : "27323e", - "text" : "ffffff" + 'hilight' : '7A7A7A', + 'pressed' : 'B0B0B0', + "button1" : "101010", + "button1_border" : "101010", + "button2" : "2e3436", + "button2_border" : "2e3436", + "text" : "16BF24" }, # Colors used by gesture display. Unlike OSD and OSK, these are RGBA "gesture_colors" : { @@ -93,6 +93,8 @@ class Config(object): "grid": "004000ff", "line": "ffffff1a", }, + # TODO: Config for opacity + "windows_opacity": 0.95, # See drivers/sc_dongle.py, read_serial method "ignore_serials" : True, } From 2c6c203aaf5c7f153bbbb39b8fcccafd85149b8d Mon Sep 17 00:00:00 2001 From: predr Date: Tue, 19 Sep 2017 02:05:40 +0200 Subject: [PATCH 05/44] use set_opacity to fade main menu --- scc/osd/menu.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/scc/osd/menu.py b/scc/osd/menu.py index bd584bb14..b4363b68b 100644 --- a/scc/osd/menu.py +++ b/scc/osd/menu.py @@ -407,8 +407,10 @@ def on_submenu_closed(self, *a): self._selected = self._submenu._selected self.quit(self._submenu.get_exit_code()) self._submenu = None - - + + log.info("opacity set to 0.95") + self.set_opacity(0.95) + def show_submenu(self, trash, trash2, trash3, menuitem): """ Called when user chooses menu item pointing to submenu """ filename = find_menu(menuitem.filename) @@ -432,7 +434,10 @@ def show_submenu(self, trash, trash2, trash3, menuitem): self._submenu.controller = self.controller self._submenu.connect('destroy', self.on_submenu_closed) self._submenu.show() - + + log.info("opacity set to 0.35") + self.set_opacity(0.35) + def _control_equals_cancel(self, daemon, x, y): """ From 6ec7b7ac1c54b828173eb34e342c083c0c935558 Mon Sep 17 00:00:00 2001 From: kozec Date: Sun, 8 Oct 2017 12:28:45 +0200 Subject: [PATCH 06/44] Working gyro *motion* DS4 over evdev, orientation yields weird values --- scc/controller.py | 2 +- scc/drivers/ds4drv.py | 60 +++++++++++++++++++++-------------------- scc/drivers/evdevdrv.py | 2 +- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/scc/controller.py b/scc/controller.py index 4f13f9609..cfd10d46f 100644 --- a/scc/controller.py +++ b/scc/controller.py @@ -13,11 +13,11 @@ class Controller(object): Derived class should implement every method from here. """ + flags = 0 def __init__(self): global next_id self.mapper = None - self.flags = 0 self._id = next_id next_id += 1 diff --git a/scc/drivers/ds4drv.py b/scc/drivers/ds4drv.py index 7223ac41c..2dbdd098d 100644 --- a/scc/drivers/ds4drv.py +++ b/scc/drivers/ds4drv.py @@ -41,10 +41,8 @@ class DS4Controller(HIDController): SCButtons.CPAD, ) - def __init__(self, *a, **b): - HIDController.__init__(self, *a, **b) - self.flags = ( ControllerFlags.EUREL_GYROS | ControllerFlags.HAS_RSTICK - | ControllerFlags.SEPARATE_STICK ) + flags = ( ControllerFlags.EUREL_GYROS | ControllerFlags.HAS_RSTICK + | ControllerFlags.SEPARATE_STICK ) def _load_hid_descriptor(self, config, max_size, vid, pid, test_mode): @@ -197,16 +195,18 @@ class DS4EvdevController(EvdevController): 16: { "axis": "lpad_x", "deadzone": 0, "max": 1, "min": -1 }, 17: { "axis": "lpad_y", "deadzone": 0, "max": -1, "min": 1 } } - MOTION_MAP = { - EvdevController.ECODES.ABS_RX : 'gpitch', - EvdevController.ECODES.ABS_RY : 'groll', - EvdevController.ECODES.ABS_RZ : 'gyaw', - EvdevController.ECODES.ABS_X : 'q1', - EvdevController.ECODES.ABS_Y : 'q2', - EvdevController.ECODES.ABS_Z : 'q3', + GYRO_MAP = { + EvdevController.ECODES.ABS_RX : ('gpitch', 0.01), + EvdevController.ECODES.ABS_RY : ('gyaw', 0.01), + EvdevController.ECODES.ABS_RZ : ('groll', 0.01), + EvdevController.ECODES.ABS_X : (None, 1), # 'q2' + EvdevController.ECODES.ABS_Y : (None, 1), # 'q3' + EvdevController.ECODES.ABS_Z : (None, -1), # 'q1' } + flags = ( ControllerFlags.EUREL_GYROS | ControllerFlags.HAS_RSTICK + | ControllerFlags.SEPARATE_STICK ) - def __init__(self, daemon, controllerdevice, motion, touchpad): + def __init__(self, daemon, controllerdevice, gyro, touchpad): config = { 'axes' : DS4EvdevController.AXIS_MAP, 'buttons' : DS4EvdevController.BUTTON_MAP, @@ -217,26 +217,27 @@ def __init__(self, daemon, controllerdevice, motion, touchpad): # see kernel source, drivers/hid/hid-sony.c#L2748 config['axes'] = DS4EvdevController.AXIS_MAP_OLD config['buttons'] = DS4EvdevController.BUTTON_MAP_OLD - self._motion = motion + self._gyro = gyro self._touchpad = touchpad - for device in (self._motion, self._touchpad): + for device in (self._gyro, self._touchpad): if device: device.grab() EvdevController.__init__(self, daemon, controllerdevice, None, config) if self.poller: self.poller.register(touchpad.fd, self.poller.POLLIN, self._touchpad_input) - self.poller.register(motion.fd, self.poller.POLLIN, self._motion_input) + self.poller.register(gyro.fd, self.poller.POLLIN, self._gyro_input) - def _motion_input(self, *a): + def _gyro_input(self, *a): new_state = self._state try: - for event in self._motion.read(): + for event in self._gyro.read(): if event.type == self.ECODES.EV_ABS: - new_state = new_state._replace(**{ - DS4EvdevController.MOTION_MAP[event.code] : event.value - }) - except IOError, e: + axis, factor = DS4EvdevController.GYRO_MAP[event.code] + if axis: + new_state = new_state._replace( + **{ axis : int(event.value * factor) }) + except IOError: # Errors here are not even reported, evdev class handles important ones return @@ -276,7 +277,7 @@ def _touchpad_input(self, *a): b = new_state.buttons & ~SCButtons.CPADTOUCH new_state = new_state._replace(buttons = b, cpad_x = 0, cpad_y = 0) - except IOError, e: + except IOError: # Errors here are not even reported, evdev class handles important ones return @@ -288,7 +289,7 @@ def _touchpad_input(self, *a): def close(self): EvdevController.close(self) - for device in (self._motion, self._touchpad): + for device in (self._gyro, self._touchpad): try: self.poller.unregister(device.fd) device.ungrab() @@ -296,8 +297,9 @@ def close(self): def get_gyro_enabled(self): - # TODO: Gyro over evdev - return False + # Cannot be actually turned off, so it's always active + # TODO: Maybe emulate turning off? + return True def get_type(self): @@ -346,19 +348,19 @@ def evdev_make_device_callback(daemon, evdevdevices): return # 2nd, find motion sensor and touchpad with physical address matching # controllerdevice - motion, touchpad = None, None + gyro, touchpad = None, None phys = device.phys.split("/")[0] for device in evdevdevices: if device.phys.startswith(phys): count = len(get_axes(device)) if count == 6: - # 6 axes - Motion sensor - motion = device + # 6 axes - gyro sensor + gyro = device elif count == 4: # 4 axes - Touchpad touchpad = device # 3rd, do a magic - return DS4EvdevController(daemon, controllerdevice, motion, touchpad) + return DS4EvdevController(daemon, controllerdevice, gyro, touchpad) def fail_cb(vid, pid): diff --git a/scc/drivers/evdevdrv.py b/scc/drivers/evdevdrv.py index 5c38272ef..927d51b0c 100644 --- a/scc/drivers/evdevdrv.py +++ b/scc/drivers/evdevdrv.py @@ -53,6 +53,7 @@ class EvdevController(Controller): """ PADPRESS_EMULATION_TIMEOUT = 0.2 ECODES = evdev.ecodes + flags = ControllerFlags.HAS_RSTICK | ControllerFlags.SEPARATE_STICK def __init__(self, daemon, device, config_file, config): try: @@ -61,7 +62,6 @@ def __init__(self, daemon, device, config_file, config): log.error("Failed to parse config for evdev controller") raise Controller.__init__(self) - self.flags = ControllerFlags.HAS_RSTICK | ControllerFlags.SEPARATE_STICK self.device = device self.config_file = config_file self.config = config From 45075cd02ce96e702bbefb8e922d65a5d91a7555 Mon Sep 17 00:00:00 2001 From: kozec Date: Sat, 21 Oct 2017 13:24:46 +0200 Subject: [PATCH 07/44] Added 'Controllers' page in global settings so new pad can be added or _removed_ as well --- glade/app.glade | 17 +- glade/global_settings.glade | 2994 +++++++++++++++++++---------------- scc/gui/app.py | 51 +- scc/gui/daemon_manager.py | 6 + scc/gui/global_settings.py | 118 +- 5 files changed, 1835 insertions(+), 1351 deletions(-) diff --git a/glade/app.glade b/glade/app.glade index 541cc9fbd..c86aafd60 100644 --- a/glade/app.glade +++ b/glade/app.glade @@ -524,6 +524,7 @@ True False + True @@ -564,10 +565,18 @@ - - 10 - 150 - + + + + 190 + 50 + True + True + True + + + + diff --git a/glade/global_settings.glade b/glade/global_settings.glade index 3e9577324..24c1cda18 100644 --- a/glade/global_settings.glade +++ b/glade/global_settings.glade @@ -23,6 +23,16 @@ 1 0.01 + + + + + + + + + + @@ -108,1544 +118,1874 @@ - + True - True + False + vertical - + True False - 10 - 10 - 10 - 10 - - - True - False - Bindings - 0 - - - - - - 0 - 0 - 6 - - + True - + True False - 20 - 10 - 5 - Stick - 0 + warning + + + False + 6 + end + + + + + + + + + Restart Emulation + True + True + True + + + + False + True + 2 + + + + + False + False + 0 + + + + + False + 16 + + + True + False + gtk-dialog-warning + 6 + + + False + True + 0 + + + + + True + False + Emulation has to be restarted to apply all settings. +If you have any games running, restarting emulation will "unplug" +virtual gamepad, what may cause them to ignore future inputs +or even crash. + True + + + False + True + 1 + + + + + + + + False + False + 0 + + + + + - - 0 - 1 - + + + False + True + 0 + + + + + True + True - + True False - 20 + 10 10 - 5 - Triggers - 0 - - - 0 - 2 - - - - - Advanced... - True - True - True - 50 - 50 - 15 - True - - - - 0 - 3 - 6 - - - - - True - False - 20 - 5 - True - lstStickAction - + 10 + 10 - - - 0 - + + True + False + Bindings + 0 + + + + + + 0 + 0 + 6 + - - - 1 - 1 - 5 - - - - - True - False - 20 - 5 - True - lstTriggersAction - - - - 0 - + + True + False + 20 + 10 + 5 + Stick + 0 + + + 0 + 1 + - - - 1 - 2 - 5 - - - - - True - False - 20 - Pad Sensitivity - 0 - - - - - - 0 - 7 - 6 - - - - - True - False - 20 - 10 - 5 - Horizontal - 0 - - - 0 - 8 - 2 - - - - - True - False - 20 - 10 - 5 - Vertical - 0 - - - 0 - 9 - 2 - - - - - True - True - 5 - True - adjSensitivityX - 1 - right - - - - 2 - 8 - 3 - - - - - True - True - 5 - True - adjSensitivityY - 1 - right - - - - 2 - 9 - 3 - - - - - True - True - True - 10 - 20 - 5 - - + True False - gtk-clear + 20 + 10 + 5 + Triggers + 0 + + 0 + 2 + - - - 5 - 8 - - - - - True - True - True - 10 - 20 - 5 - - + + Advanced... + True + True + True + 50 + 50 + 15 + True + + + + 0 + 3 + 6 + + + + True False - gtk-clear + 20 + 5 + True + lstStickAction + + + + + 0 + + + + 1 + 1 + 5 + - - - 5 - 9 - - - - - True - False - 20 - Colors - 0 - - - - - - 0 - 4 - 6 - - - - - True - True - True - 10 - 20 - 5 - True - - - - 1 - 5 - - - - - True - False - 20 - 5 - Normal Button - 0 - - - 0 - 5 - - - - - True - False - 5 - Special Button - 0 - - - 2 - 5 - - - - - 20 - True - True - True - 10 - 20 - 5 - True - - - - 3 - 5 - - - - + + + True + False + 20 + 5 + True + lstTriggersAction + + + + + 0 + + + + + 1 + 2 + 5 + + + + + True + False + 20 + Pad Sensitivity + 0 + + + + + + 0 + 7 + 6 + + + + + True + False + 20 + 10 + 5 + Horizontal + 0 + + + 0 + 8 + 2 + + + + + True + False + 20 + 10 + 5 + Vertical + 0 + + + 0 + 9 + 2 + + + + + True + True + 5 + True + adjSensitivityX + 1 + right + + + + 2 + 8 + 3 + + + + + True + True + 5 + True + adjSensitivityY + 1 + right + + + + 2 + 9 + 3 + + + + + True + True + True + 10 + 20 + 5 + + + + True + False + gtk-clear + + + + + 5 + 8 + + + + + True + True + True + 10 + 20 + 5 + + + + True + False + gtk-clear + + + + + 5 + 9 + + + + + True + False + 20 + Colors + 0 + + + + + + 0 + 4 + 6 + + + + + True + True + True + 10 + 20 + 5 + True + + + + 1 + 5 + + + + + True + False + 20 + 5 + Normal Button + 0 + + + 0 + 5 + + + + + True + False + 5 + Special Button + 0 + + + 2 + 5 + + + + + 20 + True + True + True + 10 + 20 + 5 + True + + + + 3 + 5 + + + + + True + False + 20 + 5 + Pressed Button + 0 + + + 0 + 6 + + + + + 20 + True + True + True + 10 + 20 + 5 + True + + + + 1 + 6 + + + + + 20 + True + False + 5 + Text + 0 + + + 4 + 5 + + + + + True + False + 5 + Higlight + 0 + + + 2 + 6 + + + + + 20 + True + True + True + 10 + 20 + 5 + True + + + + 5 + 5 + + + + + 20 + True + True + True + 10 + 20 + 5 + True + + + + 3 + 6 + + + + + True + False + + + 4 + 6 + 2 + + + + + + True False - 20 - 5 - Pressed Button - 0 + OSD Keyboard - 0 - 6 + True + False - - 20 + True - True - True + False 10 - 20 - 5 - True - + 10 + 10 + 10 + + + True + False + 5 + Default Menu Items + 0 + + + + + + 0 + 0 + 4 + + + + + True + False + 10 + Colors + 0 + + + + + + 0 + 6 + 4 + + + + + True + False + 20 + 5 + 5 + Text + 0 + + + 0 + 7 + + + + + True + True + True + 20 + 10 + 5 + True + + + + 1 + 7 + + + + + True + True + True + 20 + 10 + 5 + True + + + + 1 + 10 + + + + + True + False + 20 + 5 + 5 + Item Border + 0 + + + 0 + 10 + + + + + True + False + 20 + 5 + 5 + Background + 0 + + + 0 + 8 + + + + + True + True + True + 20 + 10 + 5 + True + + + + 1 + 8 + + + + + True + False + 20 + 5 + 5 + Border + 0 + + + 0 + 9 + + + + + True + True + True + 20 + 10 + 5 + True + + + + 1 + 9 + + + + + True + False + 20 + 5 + 5 + Separator Text + 0 + + + 2 + 7 + + + + + True + False + 20 + 5 + 5 + Selected Text + 0 + + + 2 + 8 + + + + + True + False + 20 + 5 + 5 + Selected Background + 0 + + + 2 + 9 + + + + + True + False + 20 + 5 + 5 + Selected Border + 0 + + + 2 + 10 + + + + + True + True + True + 20 + 20 + 5 + True + + + + 3 + 7 + + + + + True + True + True + 20 + 20 + 5 + True + + + + 3 + 8 + + + + + True + True + True + 20 + 20 + 5 + True + + + + 3 + 9 + + + + + True + True + True + 20 + 20 + 5 + True + + + + 3 + 10 + + + + + cbMI_0 + True + True + False + 20 + 5 + True + + + + True + False + 5 + List of recent profiles + 0 + + + + + 0 + 1 + 2 + + + + + cbMI_1 + True + True + False + 20 + 5 + True + + + + True + False + 5 + Autoswitch options + 0 + + + + + 0 + 2 + 2 + + + + + cbMI_2 + True + True + False + 20 + 5 + True + + + + True + False + 5 + Window switcher + 0 + + + + + 0 + 3 + 2 + + + + + cbMI_3 + True + True + False + 20 + 5 + True + + + + True + False + 5 + Display Keyboard + 0 + + + + + 0 + 4 + 2 + + + + + Customize... + True + True + True + 20 + 20 + 10 + + + 2 + 4 + 2 + 2 + + + + + cbMI_4 + True + True + False + 20 + 5 + True + + + + True + False + 5 + Turn Controller OFF + 0 + + + + + 2 + 3 + 2 + + + + + cbMI_5 + True + True + False + 20 + 5 + True + + + + True + False + 5 + Kill Current Window + 0 + + + + + 0 + 5 + 2 + + + + + cbMI_6 + True + True + False + 20 + 5 + True + + + + True + False + 5 + Run Program... + 0 + + + + + 2 + 1 + 2 + + + + + cbMI_7 + True + True + False + 20 + 5 + True + + + + True + False + 5 + Show Current Bindings + 0 + + + + + 2 + 2 + 2 + + - 1 - 6 + 1 - - - 20 + + True False - 5 - Text - 0 + OSD & Menus - 4 - 5 + 1 + True + False - + True False - 5 - Higlight - 0 - - - 2 - 6 - - - - - 20 - True - True - True - 10 - 20 - 5 - True - - - - 5 - 5 - - - - - 20 - True - True - True 10 - 20 - 5 - True - - - - 3 - 6 - - - - - True - False - - - 4 - 6 - 2 - - - - - - - True - False - OSD Keyboard - - - True - False - - - - - True - False - 10 - 10 - 10 - 10 - - - True - False - 5 - Default Menu Items - 0 - - - - - - 0 - 0 - 4 - - - - - True - False - 10 - Colors - 0 - - - - - - 0 - 6 - 4 - - - - - True - False - 20 - 5 - 5 - Text - 0 - - - 0 - 7 - - - - - True - True - True - 20 - 10 - 5 - True - - - - 1 - 7 - - - - - True - True - True - 20 - 10 - 5 - True - - - - 1 - 10 - - - - - True - False - 20 - 5 - 5 - Item Border - 0 - - - 0 - 10 - - - - - True - False - 20 - 5 - 5 - Background - 0 - - - 0 - 8 - - - - - True - True - True - 20 - 10 - 5 - True - - - - 1 - 8 - - - - - True - False - 20 - 5 - 5 - Border - 0 - - - 0 - 9 - - - - - True - True - True - 20 10 - 5 - True - - - - 1 - 9 - - - - - True - False - 20 - 5 - 5 - Separator Text - 0 - - - 2 - 7 - - - - - True - False - 20 - 5 - 5 - Selected Text - 0 - - - 2 - 8 - - - - - True - False - 20 - 5 - 5 - Selected Background - 0 - - - 2 - 9 - - - - - True - False - 20 - 5 - 5 - Selected Border - 0 - - - 2 - 10 - - - - - True - True - True - 20 - 20 - 5 - True - - - - 3 - 7 - - - - - True - True - True - 20 - 20 - 5 - True - - - - 3 - 8 - - - - - True - True - True - 20 - 20 - 5 - True - - - - 3 - 9 - - - - - True - True - True - 20 - 20 - 5 - True - - - - 3 - 10 - - - - - cbMI_0 - True - True - False - 20 - 5 - True - + 10 + 10 - + True - False - 5 - List of recent profiles - 0 + False + True + False + True + True + + + True + False + Enable Steam Controller support + 0 + + + + + + + 0 + 0 + 3 + - - - 0 - 1 - 2 - - - - - cbMI_1 - True - True - False - 20 - 5 - True - - + + ds4drv True - False - 5 - Autoswitch options - 0 + True + False + 5 + True + + + + True + False + Enable Dualshock®4 (PS4 controller) support + 0 + + + + + + + 0 + 1 + 3 + - - - 0 - 2 - 2 - - - - - cbMI_2 - True - True - False - 20 - 5 - True - - + True False - 5 - Window switcher + 20 + Other registered controllers 0 + 0 + + + + + 0 + 5 + 3 + - - - 0 - 3 - 2 - - - - - cbMI_3 - True - True - False - 20 - 5 - True - - + True False - 5 - Display Keyboard + 30 + If enabled, any connected Dualshock®4 controller will be automatically used by SC-Controller 0 + 0 + + 0 + 2 + 3 + - - - 0 - 4 - 2 - - - - - Customize... - True - True - True - 20 - 20 - 10 - - - 2 - 4 - 2 - 2 - - - - - cbMI_4 - True - True - False - 20 - 5 - True - - + True False - 5 - Turn Controller OFF + 20 + Controller listed here are automatically used by SC-Controller whenever they are connected. 0 + + 0 + 6 + 3 + - - - 2 - 3 - 2 - - - - - cbMI_5 - True - True - False - 20 - 5 - True - - + + 100 True - False - 5 - Kill Current Window - 0 + True + 20 + 20 + in + + + True + True + lstControllers + False + + + + + + column + + + + 2 + + + + + + 1 + + + + + + + + 0 + 7 + 3 + - - - 0 - 5 - 2 - - - - - cbMI_6 - True - True - False - 20 - 5 - True - - + + hiddrv True - False - 5 - Run Program... - 0 + True + False + 5 + True + + + + True + False + Enable HID device support + 0 + + + + + + + 0 + 3 + 3 + - - - 2 - 1 - 2 - - - - - cbMI_7 - True - True - False - 20 - 5 - True - - + + evdevdrv True - False - 5 - Show Current Bindings - 0 + True + False + 5 + True + + + + True + False + Enable evdev support + 0 + + + + + + + 0 + 4 + 3 + - - - 2 - 2 - 2 - - - - - 1 - - - - - True - False - OSD & Menu - - - 1 - True - False - - - - - True - False - 10 - 10 - 10 - 10 - True - True - - - True - True - False - 10 - 5 - True - - + True False - Show _OSD notification when profile is switched automatically - True - cbShowOSD + 20 + 20 + 3 + True + + + True + True + True + False + False + + + + True + False + + + True + False + gtk-add + 3 + + + False + True + 0 + + + + + True + False + Register New Controller + + + False + True + 1 + + + + + + + False + True + 0 + + + + + True + False + True + + + False + True + 1 + + + + + True + True + True + False + False + + + + True + False + gtk-remove + 3 + + + + + False + True + 2 + + + + 0 + 8 + 3 + - 0 - 1 + 2 - - + + True False - 10 - Switching Rules - 0 - - - + Controllers - 0 - 2 + 2 + False - - sw - 200 + True - True + False 10 - 5 - 5 + 10 + 10 + 10 + True True - in - + + True + True + False + 10 + 5 + True + + + + True + False + Show _OSD notification when profile is switched automatically + True + cbShowOSD + + + + + 0 + 1 + + + + + True + False + 10 + Switching Rules + 0 + + + + + + 0 + 2 + + + + + sw + 200 True True + 10 + 5 + 5 + True + in + + + True + True + True + lstItems + True + True + + + + + + + + True + When window... + + + + 1 + + + + + + + True + ... then + + + + 2 + + + + + + + + + 0 + 3 + + + + + True + False + Automatic Profile Switching Options + 0 + + + + + + 0 + 0 + + + + + True + False + 10 True - lstItems - True - True - - - - + + + True + True + True + False + False + + + + True + False + + + True + False + gtk-edit + 3 + + + False + True + 0 + + + + + True + False + 5 + Edit Condition + + + False + True + 1 + + + + + + + False + True + 0 + + + + + True + False + True + + + False + True + 1 + - - True - When window... + + True + True + True + 10 + False + False + - - - 1 - + + True + False + gtk-remove + 3 + + + False + True + 2 + - - True - ... then + + True + True + True + 10 + False + False + - - - 2 - + + True + False + gtk-add + 3 + + + False + True + 3 + + + 0 + 4 + - 0 - 3 + 3 - - + + True False - Automatic Profile Switching Options - 0 - - - + Autoswitcher - 0 - 0 + 3 + True + False - + True False 10 - True + 10 + 10 + 10 - + True True - True - False - False - + False + 5 + True + - + True False - - - True - False - gtk-edit - 3 - - - False - True - 0 - - - - - True - False - 5 - Edit Condition - - - False - True - 1 - - + Enable Input Test Mode + True + cbShowOSD + + + - False - True - 0 + 0 + 0 - + True False - True + 25 + 25 + 5 + Allows applications to <b>watch and possibly log</b> gamepad inputs. + +When enabled, main application window will display pressed buttons, +grips, triggers, finger positions on both pads and stick angle. + True + True + 0 - False - True - 1 + 0 + 1 - + True True - True - 10 - False - False - + False + 15 + True + - + True False - gtk-remove - 3 + Use Serial Numbers to Identify Controllers + True + cbShowOSD + + + - False - True - 2 + 0 + 3 - + True True - True - 10 - False - False - + False + 15 + True + - + True False - gtk-add - 3 + Automatically Disable Emulation When Closing GUI + True + cbShowOSD + + + - False - True - 3 + 0 + 7 - - - 0 - 4 - - - - - 2 - - - - - True - False - Autoswitcher - - - 2 - True - False - - - - - True - False - 10 - 10 - 10 - 10 - - - True - True - False - 5 - True - - - - True - False - Enable Input Test Mode - True - cbShowOSD - - - - - - - - 0 - 1 - - - - - True - False - 25 - 25 - 5 - Allows applications to <b>watch and possibly log</b> gamepad inputs. - -When enabled, main application window will display pressed buttons, -grips, triggers, finger positions on both pads and stick angle. - True - True - 0 - - - 0 - 2 - - - - - True - True - False - 15 - True - - - - True - False - Use Serial Numbers to Identify Controllers - True - cbShowOSD - - - - - - - - 0 - 4 - - - - - True - True - False - 15 - True - - + True - False - Automatically Disable Emulation When Closing GUI - True - cbShowOSD - - - + True + False + 30 + 5 + True + + + + True + False + Minimize to status icon instead closing + True + cbShowOSD + + + + 0 + 6 + - - - 0 - 8 - - - - - True - True - False - 30 - 5 - True - - + True - False - Minimize to status icon instead closing - True - cbShowOSD + True + False + 15 + True + + + + True + False + Enable Status (Systray) Icon + True + cbShowOSD + + + + + + + 0 + 5 + - - - 0 - 7 - - - - - True - True - False - 15 - True - - + True False - Enable Status (Systray) Icon - True - cbShowOSD - - - + 25 + 25 + 5 + Try disabling this option your controller stops working randomly. + True + True + 0 + + 0 + 4 + - - - 0 - 6 - - - - - True - False - 25 - 25 - 5 - Try disabling this option your controller stops working randomly. - True - True - 0 - - - 0 - 5 - - - - - True - True - False - 15 - True - - + True - False - Enable Rumble Support - True - cbShowOSD - - - + True + False + 15 + True + + + + True + False + Enable Rumble Support + True + cbShowOSD + + + + + + + 0 + 2 + - 0 - 3 + 4 - - + + True False - True - - - True - False - warning - - - False - 6 - end - - - - - - - - - Restart Emulation - True - True - True - - - - False - True - 2 - - - - - False - False - 0 - - - - - False - 16 - - - True - False - gtk-dialog-warning - 6 - - - False - True - 0 - - - - - True - False - Emulation has to be restarted to apply all settings. -If you have any games running, restarting emulation will "unplug" -virtual gamepad, what may cause them to ignore future inputs -or even crash. - True - - - False - True - 1 - - - - - - - - False - False - 0 - - - - - - - + Advanced - 0 - 0 + 4 + True + False - 3 - - - - - True - False - Advanced - - - 3 - True - False + False + True + 1 diff --git a/scc/gui/app.py b/scc/gui/app.py index 124762de6..fc42311e8 100644 --- a/scc/gui/app.py +++ b/scc/gui/app.py @@ -107,11 +107,11 @@ def setup_widgets(self): ], Gdk.DragAction.COPY ) - # 'C' button - vbc = self.builder.get_object("vbC") + # 'C' and 'CPAD' buttons self.main_area = self.builder.get_object("mainArea") - vbc.get_parent().remove(vbc) - vbc.connect('size-allocate', self.on_vbc_allocated) + for name in ("vbC", "btCPAD"): + w = self.builder.get_object(name) + w.get_parent().remove(w) # Background self.background = ControllerImage(self) @@ -119,7 +119,6 @@ def setup_widgets(self): 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.lpad_test = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg")) @@ -146,6 +145,7 @@ def load_gui_config_for_controller(self, controller, first): lblEmpty = self.builder.get_object('lblEmpty') grEditor = self.builder.get_object('grEditor') vbC = self.builder.get_object('vbC') + btCPAD = self.builder.get_object('btCPAD') config = self.background.load_config(controller.get_gui_config_file()) def do_loading(): @@ -177,8 +177,10 @@ def do_loading(): if w: # TODO: Maybe actual detection w.set_sensitive(gyros) - # vbC.set_visible(True) + for w in (vbC, btCPAD): + w.set_visible(w.get_sensitive()) stckEditor.set_visible_child(grEditor) + GLib.idle_add(self.on_c_size_allocate) if first: b1 = self.background.get_config()['gui']['background'] @@ -617,17 +619,21 @@ def on_profile_saved(self, giofile, send=True): # and user doesn't need to know about it if self.dm.is_alive(): controller = self.profile_switchers[0].get_controller() - controller.set_profile(giofile.get_path()) + if controller: + controller.set_profile(giofile.get_path()) + else: + 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: for ps in self.profile_switchers: controller = ps.get_controller() - active = controller.get_profile() - if active.endswith(".mod"): active = active[0:-4] - if active == giofile.get_path(): - controller.set_profile(giofile.get_path()) + if controller: + active = controller.get_profile() + if active.endswith(".mod"): active = active[0:-4] + if active == giofile.get_path(): + controller.set_profile(giofile.get_path()) self.current_file = giofile @@ -709,15 +715,26 @@ def on_background_area_click(self, trash, area): self.show_editor(area) - def on_vbc_allocated(self, vbc, allocation): + def on_c_size_allocate(self, *a): """ - Called when size of 'Button C' is changed. Centers button - on background image + Called when size of 'Button C' or CPAD is changed. + Centers buttons 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) + y = main_area.get_allocation().height - 5 + for name in ("btCPAD", "vbC"): + w = self.builder.get_object(name) + if not w.get_visible(): + continue + allocation = w.get_allocation() + x = (main_area.get_allocation().width - allocation.width) / 2 + y -= allocation.height + if w.get_parent(): + main_area.move(w, x, y) + else: + main_area.put(w, x, y) + y -= 5 + return False def on_ebImage_motion_notify_event(self, box, event): diff --git a/scc/gui/daemon_manager.py b/scc/gui/daemon_manager.py index 76af8af3e..9a351b524 100644 --- a/scc/gui/daemon_manager.py +++ b/scc/gui/daemon_manager.py @@ -258,6 +258,12 @@ def nocallback(*a): pass + def set_profile(self, filename): + """ Asks daemon to change 1st controller profile """ + self.request("Controller.\nProfile: %s" % (filename,), + DaemonManager.nocallback, DaemonManager.nocallback) + + def reconfigure(self): """ Asks daemon reload configuration file """ self.request("Reconfigure.", DaemonManager.nocallback, diff --git a/scc/gui/global_settings.py b/scc/gui/global_settings.py index fda7d4581..c65235f96 100644 --- a/scc/gui/global_settings.py +++ b/scc/gui/global_settings.py @@ -7,12 +7,12 @@ from __future__ import unicode_literals from scc.tools import _ -from gi.repository import Gdk, GObject, GLib +from gi.repository import Gtk, Gdk, GObject, GLib, GdkPixbuf +from scc.menu_data import MenuData, MenuItem, Submenu, Separator, MenuGenerator +from scc.paths import get_profiles_path, get_menus_path, get_config_path from scc.special_actions import TurnOffAction, RestartDaemonAction from scc.special_actions import ChangeProfileAction -from scc.menu_data import MenuData, MenuItem, Submenu, Separator, MenuGenerator from scc.tools import find_profile, find_menu, find_binary -from scc.paths import get_profiles_path, get_menus_path from scc.modifiers import SensitivityModifier from scc.profile import Profile, Encoder from scc.actions import Action, NoAction @@ -59,6 +59,10 @@ def __init__(self, app): self.setup_widgets() self._timer = None self._recursing = False + self._gamepad_icons = { + 'unknown': GdkPixbuf.Pixbuf.new_from_file(os.path.join( + self.app.imagepath, "controller-icons", "unknown.svg")) + } self.app.config.reload() Action.register_all(sys.modules['scc.osd.osk_actions'], prefix="OSK") self.load_settings() @@ -69,6 +73,19 @@ def __init__(self, app): ) + def _get_gamepad_icon(self, drv): + if drv in self._gamepad_icons: + return self._gamepad_icons[drv] + try: + p = GdkPixbuf.Pixbuf.new_from_file(os.path.join( + self.app.imagepath, "controller-icons", drv + "-4.svg")) + except: + log.warning("Failed to load gamepad icon for driver '%s'", drv) + p = self._gamepad_icons["unknown"] + self._gamepad_icons[drv] = p + return p + + def on_daemon_reconfigured(self, *a): # config is reloaded in main window 'reconfigured' handler. # Using GLib.idle_add here ensures that main window hanlder will run @@ -88,6 +105,8 @@ def load_settings(self): self.load_osk() self.load_colors() self.load_cbMIs() + self.load_drivers() + self.load_controllers() # Load rest self._recursing = True (self.builder.get_object("cbInputTestMode") @@ -107,6 +126,13 @@ def load_settings(self): self._recursing = False + def load_drivers(self): + for key, value in self.app.config['drivers'].items(): + w = self.builder.get_object("cbEnableDriver_%s" % (key, )) + if w: + w.set_active(value) + + def _load_color(self, w, dct, key): """ Common part of load_colors """ if w: @@ -302,11 +328,53 @@ def on_btRestartEmulation_clicked(self, *a): def on_restarting_checkbox_toggled(self, *a): if self._recursing: return self.on_random_checkbox_toggled() + self._needs_restart() + + + def _needs_restart(self): if self.app.dm.is_alive(): rvRestartWarning = self.builder.get_object("rvRestartWarning") rvRestartWarning.set_reveal_child(True) + DRIVER_DEPS = { + 'ds4drv' : ( "evdevdrv", "hiddrv" ) + } + + def on_cbEnableDriver_toggled(self, cb): + if self._recursing: return + drv = cb.get_name() + self.app.config["drivers"][drv] = cb.get_active() + if cb.get_active() and drv in self.DRIVER_DEPS: + # Driver has dependencies, make sure at least one of them is active + one_active = any([ self.app.config["drivers"].get(x) + for x in self.DRIVER_DEPS[drv] ]) + if not one_active: + # Nothing is, make everything active just to be sure + self._recursing = True + for x in self.DRIVER_DEPS[drv]: + w = self.builder.get_object("cbEnableDriver_%s" % (x, )) + if w : w.set_active(True) + self.app.config["drivers"][x] = True + self._recursing = False + + if not cb.get_active() and any([ drv in x for x in self.DRIVER_DEPS.values() ]): + # Something depends on this driver, + # disable anything that has no dependent drivers active + self._recursing = True + for x, deps in self.DRIVER_DEPS.items(): + w = self.builder.get_object("cbEnableDriver_%s" % (x, )) + one_active = any([ self.app.config["drivers"].get(y) + for y in self.DRIVER_DEPS[x] ]) + if not one_active and w: + w.set_active(False) + self.app.config["drivers"][x] = False + self._recursing = False + + self.save_config() + self._needs_restart() + + def on_random_checkbox_toggled(self, *a): if self._recursing: return self.save_config() @@ -653,3 +721,47 @@ def load_cbMIs(self): else: cbMI_5.set_sensitive(True) cbMI_5.set_tooltip_text("") + + + def on_btAddController_clicked(self, *a): + from scc.gui.creg.dialog import ControllerRegistration + cr = ControllerRegistration(self.app) + cr.window.connect("destroy", self.load_controllers) + cr.show(self.window) + + + def on_btRemoveController_clicked(self, *a): + tvControllers = self.builder.get_object("tvControllers") + d = Gtk.MessageDialog(parent=self.window, + flags = Gtk.DialogFlags.MODAL, + type = Gtk.MessageType.WARNING, + buttons = Gtk.ButtonsType.YES_NO, + message_format = _("Unregister controller?"), + ) + d.format_secondary_text(_("You'll lose all settings for it")) + if d.run() == -8: + # Yes + model, iter = tvControllers.get_selection().get_selected() + path = model[iter][0] + try: + os.unlink(path) + except Exception, e: + log.exception(e) + self._needs_restart() + self.load_controllers() + d.destroy() + + + def load_controllers(self, *a): + lstControllers = self.builder.get_object("lstControllers") + lstControllers.clear() + for filename in os.listdir(os.path.join(get_config_path(), "devices")): + if filename.endswith(".json"): + if filename.startswith("hid-"): + drv, usbid, name = filename.split("-", 2) + name = "%s (%s)" % (name[0:-5], usbid.upper()) + else: + drv, name = filename.split("-", 1) + name = name[0:-5] + path = os.path.join(get_config_path(), "devices", filename) + lstControllers.append((path, name, self._get_gamepad_icon(drv))) From af2cabcfb14900f416856457fee260a84bf8b2f2 Mon Sep 17 00:00:00 2001 From: tom Date: Sat, 21 Oct 2017 16:33:12 +0200 Subject: [PATCH 08/44] Removed 'Register new controller' menu item, button in settings is enough --- glade/app.glade | 9 --------- scc/gui/app.py | 6 ------ 2 files changed, 15 deletions(-) diff --git a/glade/app.glade b/glade/app.glade index c86aafd60..77c91e7c5 100644 --- a/glade/app.glade +++ b/glade/app.glade @@ -161,15 +161,6 @@ - - - True - False - _Register New Controller - True - - - True diff --git a/scc/gui/app.py b/scc/gui/app.py index fc42311e8..ccf5f8ba9 100644 --- a/scc/gui/app.py +++ b/scc/gui/app.py @@ -439,12 +439,6 @@ def on_mnuGlobalSettings_activate(self, *a): gs.show(self.window) - def on_mnuRegisterController_activate(self, *a): - from scc.gui.creg.dialog import ControllerRegistration - cr = ControllerRegistration(self) - cr.show(self.window) - - def on_mnuImport_activate(self, *a): """ Handler for 'Import Steam Profile' context menu item. From 9c034be25fae95e1f787b03d2b527360766acffa Mon Sep 17 00:00:00 2001 From: tom Date: Sat, 21 Oct 2017 17:05:55 +0200 Subject: [PATCH 09/44] Allowed rstick to be used as mouse --- scc/actions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scc/actions.py b/scc/actions.py index 3311b7f58..a5c6e909c 100644 --- a/scc/actions.py +++ b/scc/actions.py @@ -803,6 +803,9 @@ def whole(self, mapper, x, y, what): if what == STICK: mapper.mouse_move(x * self.speed[0] * 0.01, y * self.speed[1] * 0.01) mapper.force_event.add(FE_STICK) + elif what == RIGHT and mapper.controller.flags & ControllerFlags.HAS_RSTICK: + mapper.mouse_move(x * self.speed[0] * 0.01, y * self.speed[1] * 0.01) + mapper.force_event.add(FE_PAD) else: # left or right pad if mapper.is_touched(what): if self._old_pos and mapper.was_touched(what): From 267238c167341ccf556ca0db361bbc7ad1a5c560 Mon Sep 17 00:00:00 2001 From: kozec Date: Sun, 22 Oct 2017 08:00:28 +0200 Subject: [PATCH 10/44] Fixed glade IDs --- glade/global_settings.glade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glade/global_settings.glade b/glade/global_settings.glade index 24c1cda18..ed85cf22a 100644 --- a/glade/global_settings.glade +++ b/glade/global_settings.glade @@ -118,7 +118,7 @@ - + True False vertical From c43357cb8287bc4aa45940d2b576008744830983 Mon Sep 17 00:00:00 2001 From: kozec Date: Sun, 22 Oct 2017 10:57:22 +0200 Subject: [PATCH 11/44] Added test_inputs script --- scc/drivers/fake.py | 34 +++--- scc/mapper.py | 4 +- scc/modifiers.py | 4 +- tests/test_inputs.py | 249 +++++++++++++++++++++++++++++++++++++++++++ tests/test_setup.py | 7 +- 5 files changed, 277 insertions(+), 21 deletions(-) create mode 100644 tests/test_inputs.py diff --git a/scc/drivers/fake.py b/scc/drivers/fake.py index 4741c23d0..557d602b0 100644 --- a/scc/drivers/fake.py +++ b/scc/drivers/fake.py @@ -27,22 +27,22 @@ def start(daemon): log.debug("Creating %s fake controllers", num) for x in xrange(0, num): daemon.add_controller(FakeController(x)) + + +class FakeController(Controller): + def __init__(self, number): + Controller.__init__(self) + self._number = number + self._id = "fake%s" % (self._number,) + + + def get_type(self): + return "fake" + + + def set_led_level(self, level): + log.debug("FakeController %s led level set to %s", self.get_id(), level) - class FakeController(Controller): - def __init__(self, number): - Controller.__init__(self) - self._number = number - self._id = "fake%s" % (self._number,) - - - def get_type(self): - return "fake" - - - def set_led_level(self, level): - log.debug("FakeController %s led level set to %s", self.get_id(), level) - - - def __repr__(self): - return "" % (self.get_id(),) + def __repr__(self): + return "" % (self.get_id(),) diff --git a/scc/mapper.py b/scc/mapper.py index 378b6240e..dfc1f70c0 100644 --- a/scc/mapper.py +++ b/scc/mapper.py @@ -386,9 +386,11 @@ def input(self, controller, old_state, state): if self.lpad_touched: self.lpad_touched = False self.profile.pads[LEFT].whole(self, 0, 0, LEFT) - + except Exception, e: # Log error but don't crash here, it breaks too many things at once + if hasattr(self, "_testing"): + raise log.error("Error while processing controller event") log.error(traceback.format_exc()) diff --git a/scc/modifiers.py b/scc/modifiers.py index 57e0f0950..a4a1aadfa 100644 --- a/scc/modifiers.py +++ b/scc/modifiers.py @@ -373,11 +373,11 @@ def _roll(self, mapper): self._xvel = _xvel self._yvel = _yvel + self.action.add(mapper, dx * self.speed[0], dy * self.speed[1]) if dx or dy: - self.action.add(mapper, dx * self.speed[0], dy * self.speed[1]) if self.haptic: WholeHapticAction.add(self, mapper, dx, dy) - mapper.schedule(0, self._roll) + mapper.schedule(0.01, self._roll) def encode(self): diff --git a/tests/test_inputs.py b/tests/test_inputs.py new file mode 100644 index 000000000..0fe3d078c --- /dev/null +++ b/tests/test_inputs.py @@ -0,0 +1,249 @@ +from scc.constants import STICK_PAD_MIN, STICK_PAD_MAX +from scc.drivers.fake import FakeController +from scc.uinput import Dummy, Keys, Axes +from scc.constants import SCButtons +from scc.parser import ActionParser +from scc.profile import Profile +from scc.scheduler import Scheduler +from scc.mapper import Mapper +from collections import namedtuple +import time + +""" +Tests various inputs for crashes and incorrect behaviour, +mostly using dummy outputs and FakeController +""" + +FakeControllerInput = namedtuple('FakeControllerInput', + 'buttons ltrig rtrig stick_x stick_y lpad_x lpad_y rpad_x rpad_y ' + 'gpitch groll gyaw q1 q2 q3 q4 ' +) +ZERO_STATE = FakeControllerInput( *[0] * len(FakeControllerInput._fields) ) +parser = ActionParser() + +def input_test(fn): + """ Decorator that creates usable mapper """ + def wrapper(*a): + _time = time.time + + def fake_time(): + return fake_time.t + def add(n): + fake_time.t += n + fake_time.t = _time() + fake_time.add = add + time.time = fake_time + + controller = FakeController(0) + profile = Profile(parser) + scheduler = Scheduler() + mapper = Mapper(profile, scheduler, keyboard=False, mouse=False, gamepad=False, poller=None) + mapper.keyboard = RememberingDummy() + mapper.gamepad = RememberingDummy() + mapper.mouse = RememberingDummy() + mapper.set_controller(controller) + mapper._testing = True + mapper._tick_rate = 0.01 + + _mapper_input = mapper.input + def mapper_input(*a): + add(mapper._tick_rate) + _mapper_input(*a) + scheduler.run() + mapper.input = mapper_input + + a = list(a) + [ mapper ] + try: + return fn(*a) + finally: + time.time = _time + return wrapper + + +class RememberingDummy(Dummy): + def __init__(self, *a, **b): + Dummy.__init__(self, *a, **b) + self.pressed = set([]) + self.mouse_x = 0 + self.mouse_y = 0 + self.scroll_x = 0 + self.scroll_y = 0 + self.axes = {} + + + def axisEvent(self, axis, val): + self.axes[axis] = val + + + def moveEvent(self, dx=0, dy=0): + self.mouse_x += dx + self.mouse_y += dy + + + def scrollEvent(self, dx=0, dy=0): + self.scroll_x += dx + self.scroll_y += dx + + + def pressEvent(self, keys): + for k in keys: + assert k not in self.pressed + self.pressed.add(k) + + + def releaseEvent(self, keys=[]): + for k in keys: + if k in self.pressed: + self.pressed.remove(k) + + +class TestInputs(object): + @input_test + def test_button(self, mapper): + """ + Just test for test, this should work every time. + """ + mapper.profile.buttons[SCButtons.A] = (parser + .restart("button(Keys.KEY_ENTER)")).parse() + state = ZERO_STATE._replace(buttons=SCButtons.A) + mapper.input(mapper.controller, ZERO_STATE, state) + assert Keys.KEY_ENTER in mapper.keyboard.pressed + mapper.input(mapper.controller, state, state._replace(buttons=0)) + assert Keys.KEY_ENTER not in mapper.keyboard.pressed + + + @input_test + def test_trackball(self, mapper): + """ + Tests trackball emulation + """ + mapper.profile.pads[Profile.LEFT] = (parser.restart( + "ball(XY(" + " mouse(Rels.REL_HWHEEL, 1.0), " + " mouse(Rels.REL_WHEEL, 1.0)" + "))" + )).parse() + + # Create movement over left pad + state = ZERO_STATE + for x in reversed(xrange(STICK_PAD_MIN * 2 / 3, -10, 1000)): + new_state = state._replace(buttons=SCButtons.LPADTOUCH, lpad_x=x) + mapper.input(mapper.controller, state, new_state) + state = new_state + assert mapper.mouse.scroll_x == -21000.0 + # Release left pad + mapper.input(mapper.controller, state, ZERO_STATE) + # 'Wait' for 2s + for x in xrange(20): + mapper.input(mapper.controller, ZERO_STATE, ZERO_STATE) + assert int(mapper.mouse.scroll_x) == -21926 + + + @input_test + def test_dpad(self, mapper): + """ + Tests WSAD + """ + mapper.profile.pads[Profile.LEFT] = (parser.restart( + "dpad(" + " button(Keys.KEY_W), button(Keys.KEY_S)," + " button(Keys.KEY_A), button(Keys.KEY_D))" + )).parse() + + # Create movements over left pad + # - A + state = ZERO_STATE._replace(buttons=SCButtons.LPADTOUCH, lpad_x=STICK_PAD_MIN) + mapper.input(mapper.controller, ZERO_STATE, state) + assert Keys.KEY_A in mapper.keyboard.pressed + mapper.input(mapper.controller, state, ZERO_STATE) + # - S + state = ZERO_STATE._replace(buttons=SCButtons.LPADTOUCH, lpad_y=STICK_PAD_MIN) + mapper.input(mapper.controller, ZERO_STATE, state) + assert Keys.KEY_S in mapper.keyboard.pressed + mapper.input(mapper.controller, state, ZERO_STATE) + # - D + state = ZERO_STATE._replace(buttons=SCButtons.LPADTOUCH, lpad_x=STICK_PAD_MAX) + mapper.input(mapper.controller, ZERO_STATE, state) + assert Keys.KEY_D in mapper.keyboard.pressed + mapper.input(mapper.controller, state, ZERO_STATE) + + + @input_test + def test_joystick_camera(self, mapper): + """ + Tests joystick camera, mapping trackball to right joystick + """ + mapper.profile.pads[Profile.RIGHT] = (parser.restart( + "ball(XY(" + " axis(Axes.ABS_RX)," + " axis(Axes.ABS_RY)" + "))" + )).parse() + + # Create movement over right pad + state = ZERO_STATE + for x in xrange(10, STICK_PAD_MAX * 2 / 3, 3000): + new_state = state._replace(buttons=SCButtons.RPADTOUCH, rpad_x=x) + mapper.input(mapper.controller, state, new_state) + state = new_state + assert mapper.gamepad.axes[Axes.ABS_RX] == 3000 + # Release left pad + mapper._tick_rate = 0.001 + mapper.input(mapper.controller, state, ZERO_STATE) + # 'Wait' for 1s + for x in xrange(100): + mapper.input(mapper.controller, ZERO_STATE, ZERO_STATE) + assert mapper.gamepad.axes[Axes.ABS_RX] == 1516 + # 'Wait' for another 0.5s + for x in xrange(50): + mapper.input(mapper.controller, ZERO_STATE, ZERO_STATE) + assert mapper.gamepad.axes[Axes.ABS_RX] == 806 + # 'Wait' for long time so stick recenters + for x in xrange(100): + mapper.input(mapper.controller, ZERO_STATE, ZERO_STATE) + assert mapper.gamepad.axes[Axes.ABS_RX] == 0 + + + @input_test + def test_modeshift(self, mapper): + """ + Tests WSAD + """ + mapper.profile.buttons[SCButtons.A] = (parser.restart( + "mode(B, button(Keys.KEY_V), button(Keys.KEY_Y))" + )).parse() + + # Press single button + state = ZERO_STATE._replace(buttons=SCButtons.A) + mapper.input(mapper.controller, ZERO_STATE, state) + assert Keys.KEY_Y in mapper.keyboard.pressed + mapper.input(mapper.controller, state, ZERO_STATE) + assert Keys.KEY_Y not in mapper.keyboard.pressed + + # Press modeshifting button + state = ZERO_STATE._replace(buttons=SCButtons.B) + mapper.input(mapper.controller, ZERO_STATE, state) + assert Keys.KEY_Y not in mapper.keyboard.pressed + assert Keys.KEY_V not in mapper.keyboard.pressed + + # Press button again + _state, state = state, state._replace(buttons=SCButtons.B | SCButtons.A) + mapper.input(mapper.controller, _state, state) + assert Keys.KEY_V in mapper.keyboard.pressed + assert Keys.KEY_Y not in mapper.keyboard.pressed + + # Release modeshifting button + _state, state = state, state._replace(buttons=SCButtons.A) + mapper.input(mapper.controller, _state, state) + assert Keys.KEY_V in mapper.keyboard.pressed + assert Keys.KEY_Y not in mapper.keyboard.pressed + + # Release original button and press it again + _state, state = state, state._replace(buttons=0) + mapper.input(mapper.controller, _state, state) + assert Keys.KEY_V not in mapper.keyboard.pressed + assert Keys.KEY_Y not in mapper.keyboard.pressed + + _state, state = state, state._replace(buttons=SCButtons.A) + mapper.input(mapper.controller, _state, state) + assert Keys.KEY_Y in mapper.keyboard.pressed diff --git a/tests/test_setup.py b/tests/test_setup.py index a540e1df0..9f48ec280 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -1,5 +1,5 @@ import scc -import os, pkgutil +import pkgutil class TestSetup(object): """ @@ -10,6 +10,11 @@ def test_packages(self): """ Tests if every known Action is documentated in docs/actions.md """ + import gi + gi.require_version('Gtk', '3.0') + gi.require_version('GdkX11', '3.0') + gi.require_version('Rsvg', '2.0') + from setup import packages for importer, modname, ispkg in pkgutil.walk_packages(path=scc.__path__, prefix="scc.", onerror=lambda x: None): if ispkg: From 70f418d928659ad1c146b2b35ad10f649ad5f30c Mon Sep 17 00:00:00 2001 From: tom Date: Sun, 22 Oct 2017 15:52:55 +0200 Subject: [PATCH 12/44] GUI: Fixed centering on 'C' button --- glade/app.glade | 46 +++++++++++++++++----------------------------- scc/gui/app.py | 26 +++++++++++--------------- 2 files changed, 28 insertions(+), 44 deletions(-) diff --git a/glade/app.glade b/glade/app.glade index 77c91e7c5..5aada02f3 100644 --- a/glade/app.glade +++ b/glade/app.glade @@ -518,25 +518,36 @@ + 100 + 80 True False + vertical - + + 150 True - False + True + True + 20 + 20 - True + False True 0 - - 170 - True + + 40 True True + True + 5 + + + False @@ -544,29 +555,6 @@ 1 - - - True - False - - - True - True - 2 - - - - - - - 190 - 50 - True - True - True - - - diff --git a/scc/gui/app.py b/scc/gui/app.py index ccf5f8ba9..481cc9780 100644 --- a/scc/gui/app.py +++ b/scc/gui/app.py @@ -108,10 +108,9 @@ def setup_widgets(self): ) # 'C' and 'CPAD' buttons + vbc = self.builder.get_object("vbC") self.main_area = self.builder.get_object("mainArea") - for name in ("vbC", "btCPAD"): - w = self.builder.get_object(name) - w.get_parent().remove(w) + vbc.get_parent().remove(vbc) # Background self.background = ControllerImage(self) @@ -119,6 +118,7 @@ def setup_widgets(self): 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.lpad_test = Gtk.Image.new_from_file(os.path.join(self.imagepath, "test-cursor.svg")) @@ -716,18 +716,14 @@ def on_c_size_allocate(self, *a): """ main_area = self.builder.get_object("mainArea") y = main_area.get_allocation().height - 5 - for name in ("btCPAD", "vbC"): - w = self.builder.get_object(name) - if not w.get_visible(): - continue - allocation = w.get_allocation() - x = (main_area.get_allocation().width - allocation.width) / 2 - y -= allocation.height - if w.get_parent(): - main_area.move(w, x, y) - else: - main_area.put(w, x, y) - y -= 5 + w = self.builder.get_object("vbC") + allocation = w.get_allocation() + x = (self.background.get_allocation().width - allocation.width) / 2 + y -= allocation.height + if w.get_parent(): + main_area.move(w, x, y) + else: + main_area.put(w, x, y) return False From 0793e235f8c3651bbf056869e0aa36fb3c25e629 Mon Sep 17 00:00:00 2001 From: kozec Date: Sun, 22 Oct 2017 08:04:00 +0200 Subject: [PATCH 13/44] Better way to map rstick to mouse --- scc/actions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scc/actions.py b/scc/actions.py index a5c6e909c..ef7fc4e6a 100644 --- a/scc/actions.py +++ b/scc/actions.py @@ -2151,7 +2151,10 @@ def whole(self, mapper, x, y, what): else: self._old_pos = None - if what in (LEFT, RIGHT): + if what == RIGHT and mapper.controller.flags & ControllerFlags.HAS_RSTICK: + self.x.axis(mapper, x * MouseAbsAction.MOUSE_FACTOR, what) + self.y.axis(mapper, y * MouseAbsAction.MOUSE_FACTOR, what) + elif what in (LEFT, RIGHT): self.x.pad(mapper, x, what) self.y.pad(mapper, y, what) else: From be556d8ad442130b5873fa605f6e0b2cebe7efea Mon Sep 17 00:00:00 2001 From: kozec Date: Fri, 27 Oct 2017 19:05:00 +0200 Subject: [PATCH 14/44] Evdev: Fully working stick to mouse mapping --- scc/actions.py | 18 ++++++++---------- scc/drivers/evdevdrv.py | 7 ++++++- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/scc/actions.py b/scc/actions.py index ef7fc4e6a..258c7617f 100644 --- a/scc/actions.py +++ b/scc/actions.py @@ -833,7 +833,7 @@ class MouseAbsAction(Action): or scroll wheel. """ COMMAND = "mouseabs" - MOUSE_FACTOR = 0.01 # Just random number to put default sensitivity into sane range + MOUSE_FACTOR = 0.005 # Just random number to put default sensitivity into sane range def __init__(self, axis = None): Action.__init__(self, *strip_none(axis)) @@ -890,8 +890,8 @@ def axis(self, mapper, position, what): def whole(self, mapper, x, y, what): - dx = dx * self.speed[0] * MouseAbsAction.MOUSE_FACTOR - dy = dy * self.speed[0] * MouseAbsAction.MOUSE_FACTOR + dx = x * self.speed[0] * MouseAbsAction.MOUSE_FACTOR + dy = y * self.speed[0] * MouseAbsAction.MOUSE_FACTOR mapper.mouse.moveEvent(dx, dy) @@ -2034,6 +2034,8 @@ class XYAction(WholeHapticAction, Action): COMMAND = "XY" PROFILE_KEYS = ("X", "Y") PROFILE_KEY_PRIORITY = -10 # First possible, but not before MultiAction + STICK_REPEAT_INTERVAL = 0.01 + STICK_REPEAT_MIN = 10 def __init__(self, x=None, y=None): Action.__init__(self, *strip_none(x, y)) @@ -2154,17 +2156,13 @@ def whole(self, mapper, x, y, what): if what == RIGHT and mapper.controller.flags & ControllerFlags.HAS_RSTICK: self.x.axis(mapper, x * MouseAbsAction.MOUSE_FACTOR, what) self.y.axis(mapper, y * MouseAbsAction.MOUSE_FACTOR, what) + mapper.force_event.add(FE_PAD) elif what in (LEFT, RIGHT): self.x.pad(mapper, x, what) self.y.pad(mapper, y, what) else: - self.x.axis(mapper, x, what) - self.y.axis(mapper, y, what) - - - def pad(self, mapper, x, y, what): - self.x.pad(mapper, sci.lpad_x, what) - self.y.pad(mapper, sci.lpad_y, what) + self.x.axis(mapper, x * MouseAbsAction.MOUSE_FACTOR, what) + self.y.axis(mapper, y * MouseAbsAction.MOUSE_FACTOR, what) def describe(self, context): diff --git a/scc/drivers/evdevdrv.py b/scc/drivers/evdevdrv.py index 927d51b0c..696c0b64e 100644 --- a/scc/drivers/evdevdrv.py +++ b/scc/drivers/evdevdrv.py @@ -390,7 +390,11 @@ def make_new_device(self, vendor_id, product_id, factory, repeat=0): self.daemon.get_scheduler().schedule(1, self.make_new_device, vendor_id, product_id, factory, repeat + 1) return - controller = factory(self.daemon, devices) + try: + controller = factory(self.daemon, devices) + except IOError, e: + print >>sys.stderr, "Failed to open device:", str(e) + return if controller: self._devices[controller.device.fn] = controller self.daemon.add_controller(controller) @@ -474,6 +478,7 @@ def dumb_mainloop(self): if not self._new_devices.empty(): self.add_new_devices() + if HAVE_EVDEV: # Just like USB driver, EvdevDriver is process-wide singleton _evdevdrv = EvdevDriver() From 1f5fb53c964c5ca9157491834c851581ec800f66 Mon Sep 17 00:00:00 2001 From: kozec Date: Fri, 27 Oct 2017 19:23:06 +0200 Subject: [PATCH 15/44] Added passtrough for ball() and smooth() modifiers so actions designed for steam controller can be used on evdev controllers --- scc/actions.py | 2 +- scc/modifiers.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/scc/actions.py b/scc/actions.py index 258c7617f..aba39ee73 100644 --- a/scc/actions.py +++ b/scc/actions.py @@ -2153,7 +2153,7 @@ def whole(self, mapper, x, y, what): else: self._old_pos = None - if what == RIGHT and mapper.controller.flags & ControllerFlags.HAS_RSTICK: + if mapper.controller.flags & ControllerFlags.HAS_RSTICK and what == RIGHT: self.x.axis(mapper, x * MouseAbsAction.MOUSE_FACTOR, what) self.y.axis(mapper, y * MouseAbsAction.MOUSE_FACTOR, what) mapper.force_event.add(FE_PAD) diff --git a/scc/modifiers.py b/scc/modifiers.py index a4a1aadfa..e35777d7a 100644 --- a/scc/modifiers.py +++ b/scc/modifiers.py @@ -12,7 +12,7 @@ from scc.actions import NoAction, WholeHapticAction, HapticEnabledAction from scc.constants import TRIGGER_MAX, LEFT, RIGHT, STICK, FE_STICK, FE_TRIGGER from scc.constants import STICK_PAD_MIN, STICK_PAD_MAX, STICK_PAD_MAX_HALF -from scc.constants import FE_PAD, SCButtons, HapticPos +from scc.constants import FE_PAD, SCButtons, HapticPos, ControllerFlags from scc.constants import CUT, ROUND, LINEAR from scc.controller import HapticData from scc.tools import nameof, clamp @@ -427,6 +427,8 @@ def pad(self, mapper, position, what): def whole(self, mapper, x, y, what): + if mapper.controller.flags & ControllerFlags.HAS_RSTICK and what == RIGHT: + return self.action.whole(mapper, x, y, what) if mapper.is_touched(what): if self._old_pos and mapper.was_touched(what): dx, dy = x - self._old_pos[0], self._old_pos[1] - y @@ -439,6 +441,8 @@ def whole(self, mapper, x, y, what): velocity = sqrt(self._xvel * self._xvel + self._yvel * self._yvel) if velocity > BallModifier.MIN_LIFT_VELOCITY: self._roll(mapper) + elif what == STICK: + return self.action.whole(mapper, x, y, what) def set_haptic(self, hd): @@ -1260,6 +1264,8 @@ def _get_pos(self): return x / self._w_sum, y / self._w_sum def whole(self, mapper, x, y, what): + if mapper.controller.flags & ControllerFlags.HAS_RSTICK and what == RIGHT: + return self.action.whole(mapper, x, y, what) if mapper.is_touched(what): if mapper.was_touched(what): # Pressed for longer time @@ -1275,6 +1281,8 @@ def whole(self, mapper, x, y, what): if abs(x + y - self._last_pos) > self.filter: self.action.whole(mapper, x, y, what) self._last_pos = x + y + elif what == STICK: + return self.action.whole(mapper, x, y, what) else: # Pad was just released x, y = self._get_pos() From 83d7ea2eeb973da158c4784be9824e1a58286de3 Mon Sep 17 00:00:00 2001 From: tom Date: Sat, 28 Oct 2017 17:11:46 +0200 Subject: [PATCH 16/44] Trying to get one last buttom to display --- glade/app.glade | 3 ++ images/button-images/CPAD.svg | 68 +++++++++++++++++++++++++++++++++++ images/ds4-config.json | 1 + scc/constants.py | 3 +- scc/gui/action_editor.py | 4 ++- scc/gui/app.py | 12 +++++-- scc/gui/binding_editor.py | 8 +++-- scc/gui/controller_widget.py | 13 ++++--- scc/profile.py | 9 +++-- 9 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 images/button-images/CPAD.svg diff --git a/glade/app.glade b/glade/app.glade index 5aada02f3..21e8dd5fd 100644 --- a/glade/app.glade +++ b/glade/app.glade @@ -531,6 +531,9 @@ True 20 20 + + + False diff --git a/images/button-images/CPAD.svg b/images/button-images/CPAD.svg new file mode 100644 index 000000000..333e68adf --- /dev/null +++ b/images/button-images/CPAD.svg @@ -0,0 +1,68 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/images/ds4-config.json b/images/ds4-config.json index 3e5fdabf5..0bd76bdfa 100644 --- a/images/ds4-config.json +++ b/images/ds4-config.json @@ -19,6 +19,7 @@ "294": "RB", "295": "LB", "298": "C", + "293": "CPAD", "296": "BACK", "297": "START" } diff --git a/scc/constants.py b/scc/constants.py index 5d8b13af6..88d022b6b 100644 --- a/scc/constants.py +++ b/scc/constants.py @@ -43,6 +43,7 @@ # Trigger names, pads, etc. These constants are used on multiple places LEFT = "LEFT" RIGHT = "RIGHT" +CPAD = "CPAD" WHOLE = "WHOLE" STICK = "STICK" GYRO = "GYRO" @@ -85,7 +86,7 @@ class SCButtons(IntEnum): LT = 0b00000000000000000000001000000000 RT = 0b00000000000000000000000100000000 CPADTOUCH = 0b00000000000000000000000000000100 # Available on DS4 pad - CPAD = 0b00000000000000000000000000000010 # Available on DS4 pad + CPADPRESS = 0b00000000000000000000000000000010 # Available on DS4 pad # If lpad and stick is used at once, this is sent as diff --git a/scc/gui/action_editor.py b/scc/gui/action_editor.py index 1d0653711..78910df36 100644 --- a/scc/gui/action_editor.py +++ b/scc/gui/action_editor.py @@ -975,8 +975,10 @@ def set_input(self, id, action, mode=None): self.hide_macro() if id == "LPAD": self.set_title(_("Left Pad")) - else: + elif id == "RPAD": self.set_title(_("Right Pad")) + else: + self.set_title(_("Touch Pad")) if mode == Action.AC_OSK: self.hide_name() self.hide_modeshift() diff --git a/scc/gui/app.py b/scc/gui/app.py index 481cc9780..c34c20491 100644 --- a/scc/gui/app.py +++ b/scc/gui/app.py @@ -144,7 +144,7 @@ def load_gui_config_for_controller(self, controller, first): stckEditor = self.builder.get_object('stckEditor') lblEmpty = self.builder.get_object('lblEmpty') grEditor = self.builder.get_object('grEditor') - vbC = self.builder.get_object('vbC') + btC = self.builder.get_object('btC') btCPAD = self.builder.get_object('btCPAD') config = self.background.load_config(controller.get_gui_config_file()) @@ -170,15 +170,21 @@ def do_loading(): for b in PADS + STICKS: w = self.builder.get_object("bt" + nameof(b)) if w: - w.set_sensitive(b.lower() + "_x" in axes or b.lower() + "_y" in axes) + w.set_sensitive( + b.lower() + "_x" in axes + or b.lower() + "_y" in axes + or nameof(b) in buttons) # Gyro for b in GYROS: w = self.builder.get_object("bt" + b) if w: # TODO: Maybe actual detection w.set_sensitive(gyros) - for w in (vbC, btCPAD): + for w in (btC, btCPAD): w.set_visible(w.get_sensitive()) + if w.get_sensitive(): + w.show_all() + print "#", w, w == btCPAD, w.get_children() stckEditor.set_visible_child(grEditor) GLib.idle_add(self.on_c_size_allocate) diff --git a/scc/gui/binding_editor.py b/scc/gui/binding_editor.py index 933f09b36..54ec0ab78 100644 --- a/scc/gui/binding_editor.py +++ b/scc/gui/binding_editor.py @@ -96,8 +96,10 @@ def set_action(self, profile, id, action): before, profile.stick = profile.stick, action elif id == "LPAD": before, profile.pads[Profile.LEFT] = profile.pads[Profile.LEFT], action - else: + elif id == "RPAD": before, profile.pads[Profile.RIGHT] = profile.pads[Profile.RIGHT], action + else: + before, profile.pads[Profile.CPAD] = profile.pads[Profile.CPAD], action self.button_widgets[id].update() return before @@ -123,8 +125,10 @@ def get_action(self, profile, id): return profile.stick elif id == "LPAD": return profile.pads[Profile.LEFT] - else: + elif id == "RPAD": return profile.pads[Profile.RIGHT] + else: + return profile.pads[Profile.CPAD] return None diff --git a/scc/gui/controller_widget.py b/scc/gui/controller_widget.py index 8b1bc3262..1edfac08c 100644 --- a/scc/gui/controller_widget.py +++ b/scc/gui/controller_widget.py @@ -11,7 +11,7 @@ from scc.tools import _ from gi.repository import Gtk, Gdk, Pango -from scc.constants import SCButtons, STICK, GYRO, LEFT, RIGHT +from scc.constants import SCButtons, STICK, GYRO, LEFT, RIGHT, CPAD from scc.actions import Action, XYAction, MultiAction from scc.gui.ae.gyro_action import is_gyro_enable from scc.profile import Profile @@ -21,7 +21,7 @@ log = logging.getLogger("ControllerWidget") TRIGGERS = [ "LT", "RT" ] -PADS = [ "LPAD", "RPAD" ] +PADS = [ "LPAD", "RPAD", "CPAD" ] STICKS = [ STICK ] GYROS = [ GYRO ] PRESSABLE = [ SCButtons.LPAD, SCButtons.RPAD, SCButtons.STICKPRESS ] @@ -158,7 +158,8 @@ def on_cursor_motion(self, trash, event): what = dict( LPAD = LEFT, RPAD = RIGHT, - STICK = nameof(SCButtons.STICKPRESS) + STICK = nameof(SCButtons.STICKPRESS), + CPAD = nameof(SCButtons.CPADPRESS) )[self.name] self.app.hilight(what) self.over_icon = True @@ -194,12 +195,16 @@ def update(self): class ControllerPad(ControllerStick): ACTION_CONTEXT = Action.AC_PAD def update(self): + print "UPDATE!", self.id if self.id == "LPAD": action = self.app.current.pads[Profile.LEFT] pressed = self.app.current.buttons[SCButtons.LPAD] - else: + elif self.id == "RPAD": action = self.app.current.pads[Profile.RIGHT] pressed = self.app.current.buttons[SCButtons.RPAD] + else: + action = self.app.current.pads[CPAD] + pressed = self.app.current.buttons[SCButtons.CPADPRESS] self._set_label(action) txt = pressed.describe(self.ACTION_CONTEXT) diff --git a/scc/profile.py b/scc/profile.py index 6cb268caa..8d5f583e5 100644 --- a/scc/profile.py +++ b/scc/profile.py @@ -6,7 +6,7 @@ """ from __future__ import unicode_literals -from scc.constants import LEFT, RIGHT, WHOLE, STICK, GYRO +from scc.constants import LEFT, RIGHT, CPAD, WHOLE, STICK, GYRO from scc.constants import SCButtons, HapticPos from scc.special_actions import MenuAction from scc.modifiers import HoldModifier @@ -25,6 +25,7 @@ class Profile(object): LEFT = LEFT RIGHT = RIGHT + CPAD = CPAD WHOLE = WHOLE STICK = STICK GYRO = GYRO @@ -63,6 +64,7 @@ def save_fileobj(self, fileobj): 'trigger_right' : self.triggers[Profile.RIGHT], "pad_left" : self.pads[Profile.LEFT], "pad_right" : self.pads[Profile.RIGHT], + "cpad" : self.pads[Profile.CPAD], "menus" : { id : self.menus[id].encode() for id in self.menus }, "is_template" : self.is_template, "version" : Profile.VERSION, @@ -136,6 +138,7 @@ def load_fileobj(self, fileobj): self.pads = { Profile.LEFT : self.parser.from_json_data(data, "left_pad"), Profile.RIGHT : self.parser.from_json_data(data, "right_pad"), + Profile.CPAD : NoAction() } else: # New format @@ -149,6 +152,7 @@ def load_fileobj(self, fileobj): self.pads = { Profile.LEFT : self.parser.from_json_data(data, "pad_left"), Profile.RIGHT : self.parser.from_json_data(data, "pad_right"), + Profile.CPAD : self.parser.from_json_data(data, "cpad"), } # Menus @@ -178,7 +182,8 @@ def clear(self): self.stick = NoAction() self.is_template = False self.triggers = { Profile.LEFT : NoAction(), Profile.RIGHT : NoAction() } - self.pads = { Profile.LEFT : NoAction(), Profile.RIGHT : NoAction() } + self.pads = { Profile.LEFT : NoAction(), + Profile.RIGHT : NoAction(), Profile.CPAD : NoAction() } self.gyro = NoAction() From 3529af3a6fe4af94ab72be508a2c7c1f68de6afd Mon Sep 17 00:00:00 2001 From: tom Date: Sat, 28 Oct 2017 18:01:32 +0200 Subject: [PATCH 17/44] ... finally --- glade/app.glade | 2 +- images/CPAD.svg | 1 + images/button-images/CPAD.svg | 12 ++++++------ scc/gui/app.py | 3 --- scc/gui/controller_widget.py | 1 - 5 files changed, 8 insertions(+), 11 deletions(-) create mode 120000 images/CPAD.svg diff --git a/glade/app.glade b/glade/app.glade index 21e8dd5fd..415d34505 100644 --- a/glade/app.glade +++ b/glade/app.glade @@ -544,9 +544,9 @@ 40 + True True True - True 5 diff --git a/images/CPAD.svg b/images/CPAD.svg new file mode 120000 index 000000000..a18f9fb27 --- /dev/null +++ b/images/CPAD.svg @@ -0,0 +1 @@ +button-images/CPAD.svg \ No newline at end of file diff --git a/images/button-images/CPAD.svg b/images/button-images/CPAD.svg index 333e68adf..99f0152f0 100644 --- a/images/button-images/CPAD.svg +++ b/images/button-images/CPAD.svg @@ -25,9 +25,9 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="25.054125" - inkscape:cx="20.598207" - inkscape:cy="25.084975" + inkscape:zoom="8.8579708" + inkscape:cx="25.599434" + inkscape:cy="-7.4939363" inkscape:document-units="px" inkscape:current-layer="g4486" showgrid="false" @@ -49,7 +49,7 @@ image/svg+xml - + @@ -59,8 +59,8 @@ id="g4486" style="opacity:1"> diff --git a/scc/gui/app.py b/scc/gui/app.py index c34c20491..34590153e 100644 --- a/scc/gui/app.py +++ b/scc/gui/app.py @@ -182,9 +182,6 @@ def do_loading(): w.set_sensitive(gyros) for w in (btC, btCPAD): w.set_visible(w.get_sensitive()) - if w.get_sensitive(): - w.show_all() - print "#", w, w == btCPAD, w.get_children() stckEditor.set_visible_child(grEditor) GLib.idle_add(self.on_c_size_allocate) diff --git a/scc/gui/controller_widget.py b/scc/gui/controller_widget.py index 1edfac08c..8a86b6319 100644 --- a/scc/gui/controller_widget.py +++ b/scc/gui/controller_widget.py @@ -195,7 +195,6 @@ def update(self): class ControllerPad(ControllerStick): ACTION_CONTEXT = Action.AC_PAD def update(self): - print "UPDATE!", self.id if self.id == "LPAD": action = self.app.current.pads[Profile.LEFT] pressed = self.app.current.buttons[SCButtons.LPAD] From 3a8c3e680feefd970a70d7f6d71001baa1e76102 Mon Sep 17 00:00:00 2001 From: kozec Date: Sat, 28 Oct 2017 19:50:19 +0200 Subject: [PATCH 18/44] woo, I broke everything --- scc/drivers/ds4drv.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scc/drivers/ds4drv.py b/scc/drivers/ds4drv.py index 2dbdd098d..0af69e20d 100644 --- a/scc/drivers/ds4drv.py +++ b/scc/drivers/ds4drv.py @@ -38,7 +38,7 @@ class DS4Controller(HIDController): SCButtons.STICKPRESS, SCButtons.RPAD, SCButtons.C, - SCButtons.CPAD, + SCButtons.CPADPRESS, ) flags = ( ControllerFlags.EUREL_GYROS | ControllerFlags.HAS_RSTICK @@ -264,10 +264,10 @@ def _touchpad_input(self, *a): pass elif event.code == self.ECODES.BTN_LEFT: if event.value == 1: - b = new_state.buttons | SCButtons.CPAD + b = new_state.buttons | SCButtons.CPADPRESS new_state = new_state._replace(buttons = b) else: - b = new_state.buttons & ~SCButtons.CPAD + b = new_state.buttons & ~SCButtons.CPADPRESS new_state = new_state._replace(buttons = b) elif event.code == self.ECODES.BTN_TOUCH: if event.value == 1: From 79228f530e81bccfddc1127821809c0b98b3092a Mon Sep 17 00:00:00 2001 From: kozec Date: Sat, 28 Oct 2017 20:22:05 +0200 Subject: [PATCH 19/44] Fix: Broken sticks as well 8-) --- scc/actions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scc/actions.py b/scc/actions.py index aba39ee73..468c119c4 100644 --- a/scc/actions.py +++ b/scc/actions.py @@ -762,7 +762,7 @@ def button_release(self, mapper): def axis(self, mapper, position, what): - self.change(mapper, position, 0) + self.change(mapper, position * MouseAbsAction.MOUSE_FACTOR, 0) mapper.force_event.add(FE_STICK) @@ -2154,15 +2154,15 @@ def whole(self, mapper, x, y, what): self._old_pos = None if mapper.controller.flags & ControllerFlags.HAS_RSTICK and what == RIGHT: - self.x.axis(mapper, x * MouseAbsAction.MOUSE_FACTOR, what) - self.y.axis(mapper, y * MouseAbsAction.MOUSE_FACTOR, what) + self.x.axis(mapper, x, what) + self.y.axis(mapper, y, what) mapper.force_event.add(FE_PAD) elif what in (LEFT, RIGHT): self.x.pad(mapper, x, what) self.y.pad(mapper, y, what) else: - self.x.axis(mapper, x * MouseAbsAction.MOUSE_FACTOR, what) - self.y.axis(mapper, y * MouseAbsAction.MOUSE_FACTOR, what) + self.x.axis(mapper, x, what) + self.y.axis(mapper, y, what) def describe(self, context): From 7d7cb920fdb06825208396aa2de34cd0ba55c608 Mon Sep 17 00:00:00 2001 From: kozec Date: Sat, 28 Oct 2017 21:41:02 +0200 Subject: [PATCH 20/44] A lot of UI fixed, CPAD is fully editable now --- images/controller-images/ds4.svg | 90 +++++++++++++++++--------------- scc/gui/binding_editor.py | 7 ++- scc/gui/controller_widget.py | 11 +++- 3 files changed, 62 insertions(+), 46 deletions(-) diff --git a/images/controller-images/ds4.svg b/images/controller-images/ds4.svg index 5b7695b46..dca86fd85 100644 --- a/images/controller-images/ds4.svg +++ b/images/controller-images/ds4.svg @@ -7,7 +7,6 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="446" @@ -15,7 +14,7 @@ viewBox="0 0 445.99998 345" id="svg2" version="1.1" - inkscape:version="0.92.1 r" + inkscape:version="0.92.2 5c3e80d, 2017-08-06" sodipodi:docname="ds4.svg"> @@ -26,17 +25,17 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="1.979899" - inkscape:cx="135.87878" - inkscape:cy="184.66668" + inkscape:zoom="2.8" + inkscape:cx="202.73177" + inkscape:cy="137.49096" inkscape:document-units="px" inkscape:current-layer="layer2" showgrid="false" units="px" - inkscape:window-width="1701" + inkscape:window-width="1315" inkscape:window-height="1042" - inkscape:window-x="885" - inkscape:window-y="0" + inkscape:window-x="1378" + inkscape:window-y="1" inkscape:window-maximized="0" fit-margin-top="0" fit-margin-left="0" @@ -228,11 +227,19 @@ inkscape:label="CPAD" style="display:inline"> + sodipodi:nodetypes="cccsscc" + inkscape:label="#rect4581" /> + + id="RIGHT" + style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#000000;fill-opacity:0.21182269;fill-rule:evenodd;stroke:#000000;stroke-width:0.91912121;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" + inkscape:label="#RPAD" /> - + id="AREA_STICK_1" + style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#00000a;fill-opacity:0.04313725;fill-rule:evenodd;stroke:#ed00b4;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" + inkscape:label="#AREA_STICK" /> - - diff --git a/scc/gui/binding_editor.py b/scc/gui/binding_editor.py index 54ec0ab78..3fb3a1357 100644 --- a/scc/gui/binding_editor.py +++ b/scc/gui/binding_editor.py @@ -77,12 +77,15 @@ def set_action(self, profile, id, action): if id == SCButtons.STICKPRESS and Profile.STICK in self.button_widgets: before, profile.buttons[id] = profile.buttons[id], action self.button_widgets[Profile.STICK].update() - elif id in BUTTONS: + elif id == SCButtons.CPADPRESS and Profile.CPAD in self.button_widgets: before, profile.buttons[id] = profile.buttons[id], action - self.button_widgets[id].update() + self.button_widgets[Profile.CPAD].update() elif id in PRESSABLE: before, profile.buttons[id] = profile.buttons[id], action self.button_widgets[id.name].update() + elif id in BUTTONS: + before, profile.buttons[id] = profile.buttons[id], action + self.button_widgets[id].update() elif id in TRIGGERS: # TODO: Use LT and RT in profile as well side = LEFT if id == "LT" else RIGHT diff --git a/scc/gui/controller_widget.py b/scc/gui/controller_widget.py index 8a86b6319..c08e5b450 100644 --- a/scc/gui/controller_widget.py +++ b/scc/gui/controller_widget.py @@ -24,7 +24,8 @@ PADS = [ "LPAD", "RPAD", "CPAD" ] STICKS = [ STICK ] GYROS = [ GYRO ] -PRESSABLE = [ SCButtons.LPAD, SCButtons.RPAD, SCButtons.STICKPRESS ] +PRESSABLE = [ SCButtons.LPAD, SCButtons.RPAD, + SCButtons.STICKPRESS, SCButtons.CPADPRESS ] _NOT_BUTTONS = PADS + STICKS + GYROS + TRIGGERS _NOT_BUTTONS += [ x + "TOUCH" for x in PADS ] BUTTONS = [ b for b in SCButtons if b.name not in _NOT_BUTTONS ] @@ -145,7 +146,13 @@ def on_cursor_enter(self, *a): def on_click(self, *a): if self.over_icon and self.enable_press: - self.app.show_editor(SCButtons.STICKPRESS) + what = dict( + LPAD = SCButtons.LPAD, + RPAD = SCButtons.RPAD, + STICK = SCButtons.STICKPRESS, + CPAD = SCButtons.CPADPRESS + )[self.id] + self.app.show_editor(what) else: self.app.show_editor(self.id) From 6db3c440ef85c6b0e481b94f635cccb31e6d619d Mon Sep 17 00:00:00 2001 From: kozec Date: Sat, 28 Oct 2017 22:17:13 +0200 Subject: [PATCH 21/44] DS4 touchpad support almost done --- scc/actions.py | 2 +- scc/constants.py | 1 + scc/drivers/ds4drv.py | 16 +++++++++++----- scc/mapper.py | 13 +++++++++++-- scc/modifiers.py | 33 ++++++++++++++++++++++++--------- 5 files changed, 48 insertions(+), 17 deletions(-) diff --git a/scc/actions.py b/scc/actions.py index 468c119c4..292534ee0 100644 --- a/scc/actions.py +++ b/scc/actions.py @@ -2157,7 +2157,7 @@ def whole(self, mapper, x, y, what): self.x.axis(mapper, x, what) self.y.axis(mapper, y, what) mapper.force_event.add(FE_PAD) - elif what in (LEFT, RIGHT): + elif what in (LEFT, RIGHT, CPAD): self.x.pad(mapper, x, what) self.y.pad(mapper, y, what) else: diff --git a/scc/constants.py b/scc/constants.py index 88d022b6b..477c1832a 100644 --- a/scc/constants.py +++ b/scc/constants.py @@ -112,6 +112,7 @@ class ControllerFlags(IntEnum): EUREL_GYROS = 1 << 2 # Gyro sensor values are provided as pitch, yaw # and roll instead of quaterion. 'q4' is unused # in such case. + HAS_CPAD = 1 << 3 # Controller has DS4-like touchpad in center STICK_PAD_MIN = -32768 diff --git a/scc/drivers/ds4drv.py b/scc/drivers/ds4drv.py index 0af69e20d..71304e6f7 100644 --- a/scc/drivers/ds4drv.py +++ b/scc/drivers/ds4drv.py @@ -41,8 +41,11 @@ class DS4Controller(HIDController): SCButtons.CPADPRESS, ) - flags = ( ControllerFlags.EUREL_GYROS | ControllerFlags.HAS_RSTICK - | ControllerFlags.SEPARATE_STICK ) + flags = ( ControllerFlags.EUREL_GYROS + | ControllerFlags.HAS_RSTICK + | ControllerFlags.HAS_CPAD + | ControllerFlags.SEPARATE_STICK + ) def _load_hid_descriptor(self, config, max_size, vid, pid, test_mode): @@ -148,7 +151,7 @@ def _generate_id(self): class DS4EvdevController(EvdevController): TOUCH_FACTOR_X = STICK_PAD_MAX / 940.0 - TOUCH_FACTOR_Y = STICK_PAD_MAX / 470.0 + TOUCH_FACTOR_Y = STICK_PAD_MAX / -470.0 BUTTON_MAP = { 304: "A", 305: "B", @@ -203,8 +206,11 @@ class DS4EvdevController(EvdevController): EvdevController.ECODES.ABS_Y : (None, 1), # 'q3' EvdevController.ECODES.ABS_Z : (None, -1), # 'q1' } - flags = ( ControllerFlags.EUREL_GYROS | ControllerFlags.HAS_RSTICK - | ControllerFlags.SEPARATE_STICK ) + flags = ( ControllerFlags.EUREL_GYROS + | ControllerFlags.HAS_RSTICK + | ControllerFlags.HAS_CPAD + | ControllerFlags.SEPARATE_STICK + ) def __init__(self, daemon, controllerdevice, gyro, touchpad): config = { diff --git a/scc/mapper.py b/scc/mapper.py index dfc1f70c0..ccb45e92c 100644 --- a/scc/mapper.py +++ b/scc/mapper.py @@ -4,8 +4,8 @@ from collections import deque from scc.lib import xwrappers as X from scc.uinput import UInput, Keyboard, Mouse, Dummy, Rels +from scc.constants import SCButtons, LEFT, RIGHT, CPAD, HapticPos from scc.constants import FE_STICK, FE_TRIGGER, FE_PAD, GYRO -from scc.constants import SCButtons, LEFT, RIGHT, HapticPos from scc.constants import STICK, STICKTILT, ControllerFlags from scc.aliases import ALL_AXES, ALL_BUTTONS from scc.actions import ButtonAction, GyroAbsAction @@ -215,6 +215,8 @@ def is_touched(self, what): return self.buttons & SCButtons.LPADTOUCH elif what == RIGHT: return self.buttons & SCButtons.RPADTOUCH + elif what == CPAD: + return self.buttons & SCButtons.CPADTOUCH else: return False @@ -232,6 +234,8 @@ def was_touched(self, what): return self.old_buttons & SCButtons.LPADTOUCH elif what == RIGHT: return self.old_buttons & SCButtons.RPADTOUCH + elif what == CPAD: + return self.old_buttons & SCButtons.CPADTOUCH else: return False @@ -318,7 +322,7 @@ def input(self, controller, old_state, state): # Store states self.old_state = old_state self.old_buttons = self.buttons - + self.state = state self.buttons = state.buttons @@ -386,6 +390,11 @@ def input(self, controller, old_state, state): if self.lpad_touched: self.lpad_touched = False self.profile.pads[LEFT].whole(self, 0, 0, LEFT) + + # CPAD (touchpad on DS4 controller) + if controller.flags & ControllerFlags.HAS_CPAD: + if FE_PAD in fe or self.old_state.cpad_x != state.cpad_x or self.old_state.cpad_y != state.cpad_y: + self.profile.pads[CPAD].whole(self, state.cpad_x, state.cpad_y, CPAD) except Exception, e: # Log error but don't crash here, it breaks too many things at once diff --git a/scc/modifiers.py b/scc/modifiers.py index e35777d7a..e447dcfe4 100644 --- a/scc/modifiers.py +++ b/scc/modifiers.py @@ -217,30 +217,40 @@ def axis(self, mapper, position, what): if what in (STICK, LEFT) and mapper.is_pressed(SCButtons.LPAD): if what == STICK: mapper.force_event.add(FE_STICK) return self.action.axis(mapper, position, what) - if what in (STICK, LEFT) and mapper.was_pressed(SCButtons.LPAD): + elif what in (STICK, LEFT) and mapper.was_pressed(SCButtons.LPAD): # Just released return self.action.axis(mapper, 0, what) - # what == RIGHT, there are only three options - if mapper.is_pressed(SCButtons.RPAD): + elif what == CPAD and mapper.is_pressed(SCButtons.CPAD): return self.action.axis(mapper, position, what) - if mapper.was_pressed(SCButtons.RPAD): + elif what == CPAD and mapper.was_pressed(SCButtons.CPAD): # Just released return self.action.axis(mapper, 0, what) + elif mapper.is_pressed(SCButtons.RPAD): + # what == RIGHT, last option + return self.action.axis(mapper, position, what) + elif mapper.was_pressed(SCButtons.RPAD): + # what == RIGHT, last option, Just released + return self.action.axis(mapper, 0, what) def pad(self, mapper, position, what): if what == LEFT and mapper.is_pressed(SCButtons.LPAD): if what == STICK: mapper.force_event.add(FE_STICK) return self.action.pad(mapper, position, what) - if what == LEFT and mapper.was_pressed(SCButtons.LPAD): + elif what == LEFT and mapper.was_pressed(SCButtons.LPAD): # Just released return self.action.pad(mapper, 0, what) - # what == RIGHT, there are only two options - if mapper.is_pressed(SCButtons.RPAD): + elif what == CPAD and mapper.is_pressed(SCButtons.CPAD): return self.action.pad(mapper, position, what) - if mapper.was_pressed(SCButtons.RPAD): + elif what == CPAD and mapper.was_pressed(SCButtons.CPAD): # Just released return self.action.pad(mapper, 0, what) + elif mapper.is_pressed(SCButtons.RPAD): + # what == RIGHT, there are only two options + return self.action.pad(mapper, position, what) + elif mapper.was_pressed(SCButtons.RPAD): + # what == RIGHT, there are only two options, Just released + return self.action.pad(mapper, 0, what) def whole(self, mapper, x, y, what): @@ -255,6 +265,11 @@ def whole(self, mapper, x, y, what): elif what == RIGHT and mapper.was_pressed(SCButtons.RPAD): # Just released return self.action.whole(mapper, 0, 0, what) + elif what == CPAD and mapper.is_pressed(SCButtons.CPAD): + return self.action.whole(mapper, x, y, what) + elif what == CPAD and mapper.was_pressed(SCButtons.CPAD): + # Just released + return self.action.whole(mapper, 0, 0, what) else: # Nothing is pressed, but finger moves over pad self.action.whole_blocked(mapper, x, y, what) @@ -377,7 +392,7 @@ def _roll(self, mapper): if dx or dy: if self.haptic: WholeHapticAction.add(self, mapper, dx, dy) - mapper.schedule(0.01, self._roll) + mapper.schedule(0.02, self._roll) def encode(self): From f3ec7ea0354bea5e4b56778e474596e0f371c0d9 Mon Sep 17 00:00:00 2001 From: kozec Date: Sun, 29 Oct 2017 11:18:22 +0100 Subject: [PATCH 22/44] Redone controller flags checking --- scc/actions.py | 4 ++-- scc/mapper.py | 8 ++++++++ scc/modifiers.py | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/scc/actions.py b/scc/actions.py index 292534ee0..cf3297371 100644 --- a/scc/actions.py +++ b/scc/actions.py @@ -803,7 +803,7 @@ def whole(self, mapper, x, y, what): if what == STICK: mapper.mouse_move(x * self.speed[0] * 0.01, y * self.speed[1] * 0.01) mapper.force_event.add(FE_STICK) - elif what == RIGHT and mapper.controller.flags & ControllerFlags.HAS_RSTICK: + elif what == RIGHT and mapper.controller_flags() & ControllerFlags.HAS_RSTICK: mapper.mouse_move(x * self.speed[0] * 0.01, y * self.speed[1] * 0.01) mapper.force_event.add(FE_PAD) else: # left or right pad @@ -2153,7 +2153,7 @@ def whole(self, mapper, x, y, what): else: self._old_pos = None - if mapper.controller.flags & ControllerFlags.HAS_RSTICK and what == RIGHT: + if mapper.controller_flags() & ControllerFlags.HAS_RSTICK and what == RIGHT: self.x.axis(mapper, x, what) self.y.axis(mapper, y, what) mapper.force_event.add(FE_PAD) diff --git a/scc/mapper.py b/scc/mapper.py index ccb45e92c..966d99c90 100644 --- a/scc/mapper.py +++ b/scc/mapper.py @@ -204,6 +204,14 @@ def send_feedback(self, hapticdata): self.feedbacks[hapticdata.get_position()] = hapticdata + def controller_flags(self): + """ + Returns controller flags or, if there is no controller set to + this mapper, sc_by_cable driver matching defaults. + """ + return 0 if self.controller is None else controller.flags + + def is_touched(self, what): """ Returns True if specified pad is being touched. diff --git a/scc/modifiers.py b/scc/modifiers.py index e447dcfe4..470f21ce2 100644 --- a/scc/modifiers.py +++ b/scc/modifiers.py @@ -442,7 +442,7 @@ def pad(self, mapper, position, what): def whole(self, mapper, x, y, what): - if mapper.controller.flags & ControllerFlags.HAS_RSTICK and what == RIGHT: + if mapper.controller_flags() & ControllerFlags.HAS_RSTICK and what == RIGHT: return self.action.whole(mapper, x, y, what) if mapper.is_touched(what): if self._old_pos and mapper.was_touched(what): @@ -1279,7 +1279,7 @@ def _get_pos(self): return x / self._w_sum, y / self._w_sum def whole(self, mapper, x, y, what): - if mapper.controller.flags & ControllerFlags.HAS_RSTICK and what == RIGHT: + if mapper.controller_flags() & ControllerFlags.HAS_RSTICK and what == RIGHT: return self.action.whole(mapper, x, y, what) if mapper.is_touched(what): if mapper.was_touched(what): From 0571c52578264c59051718a3a7c7bd4a3a47548c Mon Sep 17 00:00:00 2001 From: kozec Date: Sun, 29 Oct 2017 20:16:37 +0100 Subject: [PATCH 23/44] Fixed tests --- .gitignore | 2 +- scc/actions.py | 2 +- tests/test_inputs.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 3b211fa3e..8e8ff486d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ *.so *.patch !scc/*tester* -*test* +test*.py build/ .atomignore commit* diff --git a/scc/actions.py b/scc/actions.py index cbdfdb31f..c7e2840a0 100644 --- a/scc/actions.py +++ b/scc/actions.py @@ -15,7 +15,7 @@ from scc.lib import xwrappers as X from scc.constants import STICK_PAD_MIN, STICK_PAD_MAX, STICK_PAD_MIN_HALF from scc.constants import STICK_PAD_MAX_HALF, TRIGGER_MIN, TRIGGER_HALF -from scc.constants import LEFT, RIGHT, STICK, PITCH, YAW, ROLL +from scc.constants import LEFT, RIGHT, CPAD, STICK, PITCH, YAW, ROLL from scc.constants import PARSER_CONSTANTS, ControllerFlags from scc.constants import FE_STICK, FE_TRIGGER, FE_PAD from scc.constants import TRIGGER_CLICK, TRIGGER_MAX diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 0fe3d078c..4f74330ac 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -136,7 +136,7 @@ def test_trackball(self, mapper): # 'Wait' for 2s for x in xrange(20): mapper.input(mapper.controller, ZERO_STATE, ZERO_STATE) - assert int(mapper.mouse.scroll_x) == -21926 + assert int(mapper.mouse.scroll_x) == -24479 @input_test @@ -193,11 +193,11 @@ def test_joystick_camera(self, mapper): # 'Wait' for 1s for x in xrange(100): mapper.input(mapper.controller, ZERO_STATE, ZERO_STATE) - assert mapper.gamepad.axes[Axes.ABS_RX] == 1516 + assert mapper.gamepad.axes[Axes.ABS_RX] == 3510 # 'Wait' for another 0.5s for x in xrange(50): mapper.input(mapper.controller, ZERO_STATE, ZERO_STATE) - assert mapper.gamepad.axes[Axes.ABS_RX] == 806 + assert mapper.gamepad.axes[Axes.ABS_RX] == 1570 # 'Wait' for long time so stick recenters for x in xrange(100): mapper.input(mapper.controller, ZERO_STATE, ZERO_STATE) From ce2933a57048e14145315eb9ccb6f630f496b4f8 Mon Sep 17 00:00:00 2001 From: kozec Date: Sun, 29 Oct 2017 20:30:41 +0100 Subject: [PATCH 24/44] Fixed tests for travis --- tests/test_setup.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_setup.py b/tests/test_setup.py index 9f48ec280..e81e87410 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -10,10 +10,13 @@ def test_packages(self): """ Tests if every known Action is documentated in docs/actions.md """ - import gi - gi.require_version('Gtk', '3.0') - gi.require_version('GdkX11', '3.0') - gi.require_version('Rsvg', '2.0') + try: + import gi + gi.require_version('Gtk', '3.0') + gi.require_version('GdkX11', '3.0') + gi.require_version('Rsvg', '2.0') + except ImportError: + pass from setup import packages for importer, modname, ispkg in pkgutil.walk_packages(path=scc.__path__, prefix="scc.", onerror=lambda x: None): From dfc7e4bddab74b9aa2b05bfd5ad5eb717bcea800 Mon Sep 17 00:00:00 2001 From: kozec Date: Sat, 4 Nov 2017 23:27:56 +0100 Subject: [PATCH 25/44] Changing quick menu button images to match active controller. Ref #280 --- images/menu-icons/buttons/1.png | Bin 0 -> 8568 bytes images/menu-icons/buttons/2.png | Bin 0 -> 12134 bytes images/menu-icons/buttons/3.png | Bin 0 -> 12971 bytes images/menu-icons/buttons/4.png | Bin 0 -> 9730 bytes images/menu-icons/buttons/CIRCLE.png | Bin 0 -> 11571 bytes images/menu-icons/buttons/CROSS.png | Bin 0 -> 11353 bytes images/menu-icons/buttons/SQUARE.png | Bin 0 -> 5680 bytes images/menu-icons/buttons/TRIANGLE.png | Bin 0 -> 10405 bytes images/menu-icons/buttons/snesA.png | Bin 0 -> 12205 bytes images/menu-icons/buttons/snesB.png | Bin 0 -> 12993 bytes images/menu-icons/buttons/snesX.png | Bin 0 -> 14010 bytes images/menu-icons/buttons/snesY.png | Bin 0 -> 11168 bytes scc/gui/app.py | 3 ++- scc/gui/controller_image.py | 25 ++++++------------------- scc/gui/daemon_manager.py | 20 +++++++++++++++++++- scc/osd/quick_menu.py | 18 ++++++++++++++++++ scc/sccdaemon.py | 20 -------------------- 17 files changed, 45 insertions(+), 41 deletions(-) create mode 100644 images/menu-icons/buttons/1.png create mode 100644 images/menu-icons/buttons/2.png create mode 100644 images/menu-icons/buttons/3.png create mode 100644 images/menu-icons/buttons/4.png create mode 100644 images/menu-icons/buttons/CIRCLE.png create mode 100644 images/menu-icons/buttons/CROSS.png create mode 100644 images/menu-icons/buttons/SQUARE.png create mode 100644 images/menu-icons/buttons/TRIANGLE.png create mode 100644 images/menu-icons/buttons/snesA.png create mode 100644 images/menu-icons/buttons/snesB.png create mode 100644 images/menu-icons/buttons/snesX.png create mode 100644 images/menu-icons/buttons/snesY.png diff --git a/images/menu-icons/buttons/1.png b/images/menu-icons/buttons/1.png new file mode 100644 index 0000000000000000000000000000000000000000..63f9b9dfddd5fd3994aedfe98dce9319a91a318f GIT binary patch literal 8568 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 literal 0 HcmV?d00001 diff --git a/images/menu-icons/buttons/2.png b/images/menu-icons/buttons/2.png new file mode 100644 index 0000000000000000000000000000000000000000..e592cdf8e8a416ff1c5c6782f7ea0000049b7ca4 GIT binary patch literal 12134 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

I`!H)gNBrj5n|Fv$79A%_9$%bWu*kH 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 literal 0 HcmV?d00001 diff --git a/images/menu-icons/buttons/3.png b/images/menu-icons/buttons/3.png new file mode 100644 index 0000000000000000000000000000000000000000..6aef210d8eeadc4662167e2896750dbac21b229b GIT binary patch literal 12971 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 W=l1V*Wp0CmAT?!erAh^>7yk#H|6hRs literal 0 HcmV?d00001 diff --git a/images/menu-icons/buttons/4.png b/images/menu-icons/buttons/4.png new file mode 100644 index 0000000000000000000000000000000000000000..7009a3fca21e7464ace700ae3e5ba3c0e00cfb76 GIT binary patch literal 9730 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 K7qxO`A^!uGBtl96 literal 0 HcmV?d00001 diff --git a/images/menu-icons/buttons/CROSS.png b/images/menu-icons/buttons/CROSS.png new file mode 100644 index 0000000000000000000000000000000000000000..d53447f737bdaab4561a8094af4c72956ec92474 GIT binary patch literal 11353 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 HtFZqAT{9-e literal 0 HcmV?d00001 diff --git a/images/menu-icons/buttons/SQUARE.png b/images/menu-icons/buttons/SQUARE.png new file mode 100644 index 0000000000000000000000000000000000000000..c047ad8d5f578b6923534918b23e92fd27e19fb6 GIT binary patch literal 5680 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 C<%KW+ literal 0 HcmV?d00001 diff --git a/images/menu-icons/buttons/TRIANGLE.png b/images/menu-icons/buttons/TRIANGLE.png new file mode 100644 index 0000000000000000000000000000000000000000..7c1444de89ce468f638d4cbb61a32008f5f41330 GIT binary patch literal 10405 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 literal 0 HcmV?d00001 diff --git a/images/menu-icons/buttons/snesX.png b/images/menu-icons/buttons/snesX.png new file mode 100644 index 0000000000000000000000000000000000000000..916ea4e1d86af310f8a98fbfd957528e799a5184 GIT binary patch literal 14010 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

8hG= zz>a8GXG?kY#3ky_uvcpsLCTRMBZoXY9m3KQc9s!4hoptKhMo8l)B8AB=FcYK4m4Mi zxRVr1rhM|1sOfl1YO0G$Qa_cIf$kJ{mz!mgR<9OInnO!3Wt6*z#D}z39}9HRp|Nw( z4f2Ay)4MHRuINhMXPlTRKMeWvo&8EyWto20M`jeNppmpsPtKYkNhoqY zGQ`2`0Wt{3sNNRbOJGW7z`n0r?b5`^#1#P_rG5{=Ew2Y!#VY9wOoppinJv{p!kga`I8M+iw^jEwVWzRqVY6dYHeb> zzCBVvd_>Aa<;|?W_&dFG;N~%QP*d|AD^|XAe5bCHI;ijKe>tYtde0{?*#;x|!{Uf2 zEUb3Lh$KlW7~QP-^Oo63`RT)m@jM%d2P<5fK5a`b4%PD0KYiv}g{$&<)7AKh!h; z0Kx?D@>f$;6Wd79Jm7}!zW?=WioQg1^jBro>zDuP@2379hx{BKW|2VqpL)rOOi!1n z-{2tIy&3-L-5}w8ke$%%JfG|5@Nip(h9hMQacuG=ub>Bn0M1QL4UO!W3r|{(3pw4y zPpPSq&qU7FFFQI&16|C|_y11NjDyCDcqH3+c$QbUXnMue{cz!aZgVh%wW^#kop#(Y zo%>nL*RO_&I$Usjf|j>&_%j3mJSh50QAnwgW`)tK->MG;**lXHj}$(a2cDK?b^Bgz zXXK+fUdkXRPXuuIKC!dod5d~G4t(V>kY6Ty_ln!>VqkAFitKR)Ah62aj&DTCBSY*% zf8!+S=uRQihB!8BNBEGDkTi+ATgh0!)|*A6#&^rlhqJ}@A6dBt#e&=Gc|{~7_EfF6 zU_sViqRS7}&P#?9>vOf$&iV$k84FDsd6OzNj`QjQ0;R$ZOGqa!@4N<$Kn zeh&qgyzUfv*iex95)*sA9^I?0YsDIRd((T)912AN_c|T1~es zB*Vnq1h%-!(HtD3zPt_otzdBwI)!w+yU|@tI=Sj;^{4uJ1G_yge6*iMTRU3-r@+*A z>*tG!pjsvGzCN5sZlS9su)Yzea;3n#wS}err4C0=&t&}+3-CnKV%ml!DobAKPw($rL*#6C)b6l z)D42@Quazp1;ij}KL74i%`>8=0(yAlR54c#lM_rAtc$0tNBA9{=*zqOjqOgrQn~$P`R+fZnI|XbR_E1{!-e9s(A4P< z=z}zzMUvd%oVLrb*!rpP*_nNg;eqlL!XH;)V`Yy7~3-ucHo8Lx(%xM{DV1a02Dwq1T1iAzn!Ow zC+Df#$@@xz&PY#>({*1*VN}^3Y7~!9ylP}qc0v`5;xQzQ`BO38u;<5V{s;*S$`<^_ z-H#^hFM#oGCJ-y3xGdC&8q)EzS%t4wj|ZS-0I|KawzwdkufpU8Q#GZ9z_O1Bpm$`m@X$FG1Lf8UYal>o?9w3!>OquA1P7=rKE- z$a*!T6O=iFW%(K(aC{CJX~6^5HOo4p6ynL~Uru~~*dV!EYhLX+Js&-$j`@pRBf^Ik z#$nu)=86K2AA}sBBRWrZw!EIjlX1x}Hf?nn7ip zG5`Rz@*g1pYlQ0g^JYGeZHtQ|Q{}dL5_GG>h3CSab~z9ae1u~65qi}R1O0&yAeYGB zcmr#}%M71BS^oNr&llVgr1-vQjyaw@3DN3N9SXzg6z#&g{PHBYcO;1zM**Se;R%@o zos3%Sm4k2y$H|Pb00D~vMPs8*$K4UrQpZihhm22%oO{8vi+(~-xcrfofwkYuZn1c# z?aqk1NuWy?gI(T&RC6-NCJZUTNQMP1;0u03g7nBaFJl1f;dbv^n3Nk={Qz z^#sb)5+~AN4m7VmVoy&G5Y0W3BPQvtzl{8KR$f!GqtS6Xz_L4A4L2w! z*|Aw}6Q&UrKsTx^`H&%o7^ttAmtX}kfWR6ulzcADz}c$K0f(vqX|g1j{yz|)jxk4n zPvoZS)=y!Ir@z%mywv(XR#{MNL1Vs5dxC6OUbKD_6EhckwYrMK!*i#`s!~hC#ujYY z>5m6GCd#sTK(O$kS={JFVm$^Tyeb%H&9q=ypS}L{Z@D>ij4spHK#l&_&$J-7`|C_Q zJ6;=p9#meugM}2+HFcyDI3d*O(+b)aP=8ywe@tC16~?v%1z@}n>7+(% zS6#mcfu8+eP}gLxY$4Yq@Ka7OWqF~V)j}1HzNr37FfQgleP?;C9;!GQ@gJO&8KLFL zE{2##U4vwIuMBwc8ogl{r(>`X+Wx(mh>*6RG-hRRzjnm{gMx^SjU#cXTFL{}u4OT` zGT>1Fi|{7QrH1f)Lv z4@c{px(>`BV{GdggL-s*?NZi0miQmWac_cN*vC2SL*6rd4;%ys;I8~XobIj?DiqVO zafh6;)qc|RJ^xXgF^OD>;9AMQkUiFjWWuBqGg2}fy#qf<}GWvIs3UQPUWUEuvD78C;a zfwKOYGlz|J+_?1(9`{XZt};mgB}Gm8asT!M)_;BB1E*Q`u(&`zVA(k!1U1@Kqtcz;`27E+%P|UV@;#q|GRbHh4ENHI<%~JCTnlMUyrpEa+cA z#)X!$G^#Yk5m1bD%>qomN#RH{7|-vezu%+{VGx))y8J-AwL4D7#Dvq6KpmdIw7SNC zijW1+Rq!L3bb-EZVgysod_1zoV}tzH&}tXag(ivE)6LO5>tTlp=dSB> z%Rw{OYh?w8M(WFBm+14I&E(uwVi2f{WHcF?fO&&?u+(7522RhBfgWQ-Atz%ys4$ny zrZ5{2Q20_FE}Hk$z}|Y({uX8|V0*ko{uJMjf~huLes__ES~ zhb5lNKs^v$Sb1rzT&f_x|Evu{o9l>z@`p}b9KADOwQe4@Kb1r(ON=~jX^EGFG;;m% zp-CLs`6LwLN`vr$<{p>5NdXT@R3wocn07POoNg>fzA8Z%YL@l&UkOvygEMPPom?3d zSP#p3?tN`)5iTsbTRYp5{)j}Cg<+iTwx{&aUPO1 z8BYx#)*uoEmZXeI1VhTSbUtgetTr0?C#;sX^xo-4BzHfFr6ohR9tVQ~-c?hptb0>SRS-@kj# zy=Tsz-s!3Cu6d@by1VL$)lieiLMKB9002u-K~@U@K=8jH02LWtn7pgBgBOVQQmRq_ z@GTkR(Gm%MPh+j1r3wJ=836zq0RaEtRnQ{<@Zkc0Ulsr$oC5&F9t9nmqVNaER?70S zz{`JE$@hwMcnz9|f}s}xBzFGy0~Pv&LgAGt-ioRalx++ofP@9r>WUQrSez7PrF8sP zei!-$5kSa?y8W`{q@^oMWsWG3*ie75#xYr;$l_vzA+e&j5JL!%2qBULQkv$U4J6Gn z%SsGz5FrE7i5XDIBSlm+X1{Z4ax)~i+uJi=zuU8ehx;{@w%q(eL*EdQ2T|V1-K$a! zCM&|$(^pIc=mAV1_zejIfDXa@h$vYaW0?Uk5pXBq0g<#(n&1+RelE{YmNuKe8n~Fc zp=A3ZMR$rIywMX~ju+lnOzba#+!`9sf5WaOpRXo=ggkzDqrntdf^LmybcnA35G3^? z*??U`TD7 z0DQsSpzf>?J%kgqM=mV`N!Wui8q~V-l{az+*dA0kB}+rLgi;OsV10Q+B^Wu=VJks| zwbPfZpAu(=tc;$eBvK${f#RW&FzN;of*K}}2N8ehSZI9c^|$lF^<$flu{}XsUUR1~ zz6@ZvLfC_UidYEcV(I;j(3K)ha@)JH58!B!ho)T6z2CsTw2mBwE&X<%Dq2-WiV~@! zKyjVumi3kxA2(CW{hekc9`N>4$@;}2c)fn7+)rB?FHpe_yJ|1xC)Jm98OmGD^q^nq z^5j<+5spEt%HWW+QD3cPh;V1+&tNBy7)`(DyjrDB#6EIB+oG9?w~T;HQ!MnOL_lIDVtvk90!v6zTS-Cil4&(G z7sl#Phl!5B1gbA0c>ZxL60x3_kH?Y0iIs;B(hBVUOeI8L9S5EM)eBn{Uu<9DVi_o* z2+YQYH}w!-ZDg#&#$W_HwfmVE`w-ssYh4!5IzKktaW{wo` z!b~PY)9WVXI$%QXf?{RoV~#wUKvd8l_EooB9{q-T3GHN_{@*k=_;IC*tF;6wvC~-opKr~%g5_D4t`&s z=*3L;cG#R+8b9<>3d)v*LWRIe#QpzeZ z6E8}Ja(_|?qjNYxv- zn*~X|@-0ra92!(S6Z&Sl{9&+&{&H_7DnPVnXteSWM^7VBzddfkgdhTE4@?)T2-Sy6 zOQS(^%p5pAzK?PmN>x`D-`@<9{XmE}mKvn&Q$Cel<2_{W|1!YSq+4)e>$i!X0Tiep zbCJ46n^~+;eCGF>Po$QD&yzYYy3S*l3*txz-k!ryp5=8xd7Qr9imDJsW=PvXi zr?>qyb=?z!+9fUWJVtXh!gm*+=@7NWwrnbh!2pmc!Wtc%{~};BC$iPrk%b$wcJ9V; zG4~B9{Mg^)q@M>gPCWEDFQ@MN;|1vcl3EWAuRQ4E6x6bQO zrY%e1Ve{nFm^hT&{&hIpEo|s?%`m`&=1=#^9@il%TjT$|2s3J}6H4OtwtMZTO~!RS zqt*nmkr+E`z_pSBOkfUM(@_3=?qtO-)n708RxAV|jbTorBQkq4w2~7hCD=kC$fUo) zDX3kbo5dRV^uov;whm(L>_d;%hDHar7d79!(r6@@5E6p;X8(z?164+3N$yar8;zXq zsTFN2wTztA(}t1s+y@Qdbb+j;6Tn3&>!!HVEP(E`mZHy0jcy?|$s`7!)~XNT>kFzx z|3q8@O)<%dOBjL>*OCGsx+JoZsQ=UV2dw)9$1Wu$Z`U)8rE!)o7eaKyA0452vr|Mx zZ=IYyo6sc1EYPM@{Kj2#C!EJK)(s&U_2Kkqb|h~La7zwcwvav4`VSrOpvW+N2aFGo zy9Z3!7vSW2Xh_~feuJJR41IRC_(AIW=F3OV27n#`BQ&X( zO!0Kzl4^&gW%R6?c93L1tK-@Vz->cezo@kK{DK9;3W~`}8k4xB*m+H*=lqXQD3t*{bFbwcpReae@}ehAQ6e?&hP9rY_1lD#=^R+RKxHvJQgkF+JQWoB6w z4R+f{um?2)}d zsA>O8Fjfz^Agp z96rlL-rUjf>Wp>GNa^e<6*`%}!eaOngZrojGysQ%@RX9lMJ`!bQd4^lfQq;m{ z8&7f0lQ{>(j{Ph;ZJ@lw5c0IWi#|BLF76jAZHe~LK#4Kl+sQ<|BqxiARvADBFQO{1 z@`gVcvRApIN4BeM^Rt|s=@cfI6<}bbz6MmGXuUJPst;^8vOReS(3KTFXUO{}30We4 zQ97y#{f5(t1WUZ#FrRjJ6m3ti2;=$3T$WUOx=YJ=vM>dXUx`}>b0HqlvVD(GEEtsP zR1Y(y?@Y4|G$r5($T+W@#499IFCo4g1fO=%oFGi1Dw8+T80r6OR(C7DI;;wL_0lo1 zC^&i+FU_;-pJ=nqv4HL{9qGCnW?cbvfy26-M@}s$W?fI?T}2l!+I?A9YW>?awLW?Y zmwR`;GkNl?7(PpvruAXokz1Ffed!(44f92V`OqYk|Jo&EgyeTi%oqN;ul?h>CxAQ7n;atU!;~u9J=D` zJ@5*ofHa5(R~y<>-E3X&>* ztUQay`^Rg;zhYB;+n&SvYd>huIC7S5D>|mag1XqQ10GzO#u7oaKr}KqPgmRmKKc(O zg{}+)s&1YLG*8cEvjnmT63siNOl^$^OdP=RtW&cxsIgS-&^dkg`&)>hvXk#dp7vq_ zwVrcMjU4xCr5gA5+z9`%X8<)PhTf(673vJCLA=AuHDN*$QJ?w4-Yv6N{v_oLQ`Pia zxnJ4$qe&Yb`wI`{hbo(|JI@ggr}sY{J~_DBtDg0Og`hGwWms`Xj($QuH5EzQ#kY)(<+z>90HM%C8Y;eTYyCV+Y0}Y6zg$(65N{><3gE z_3tcvJ;F-soa(#lPOH}ZHLoF|4db%>vt+6wjkfQ`Fc9Xy5}DWRkK6IS8FvxBHg9-v zVPPLW^xl}S;wDn~_Ul3q9cEA{HMwhYYvAuPMw-K2UpqWSjQQ=9>@;=b<0uxt8;-H?n|Gr1TkV#6G1_iNac`z*bJXV&T=%LG4LLN9cg_s4!PC*@1J#vVdjzh`M4JA9{#q< zKV|tdV#in$Zh}7v^|yORiEo|FN&}FVtuf<`a}wjVq>fD`33;*YTL4VLER*5R)W;i% z!ReWgKSD!ZQJC+rj$n5;r^nhYZYk$-`Z#2-)n*gAJ_Lzx?wO_!xihW7Pf-Py(}kvd zTgp<7kfynSa?5g{AkpF2ieW)sJEjf^h4?hkZ?iKC$tyu_(IPv}lx4lh^WlR*)4Y#Q z&#&Ytdu(&Mz=CoQltxc&TBh&h_`Ddo%)rjybc{02{zX`I=$IgZ+g)#5%g1Scp|AJV zo%p7)S#1sP#@@A>wwyJPHViqC**9kYlQx!QZ(46Ix5xtckRH{4r`^Xt+)a;P?-Fo4 z@9r>1e~)HY)TC0a?NK)lT4^+s7E=~z@xLUJ$a^|!GyU8d*XKSVWim<8uY7+#^eH#P z5bRM`U-N1_KFTj7#D#fTi;pF6PN;6um@L@&4L(+Fj}&S6FVl?RY00lE=Kdbr}OixSI$k3d+FufrUa^>PulOll16jM0RKvBbcA!W#w&!U`T4KI_iKCJ z??%a&IeU119YFXpsPF0oSsnPXvj!qZR$;o%gzTTnx2w18;Dh^K7x(tqBj-nfu$z~+x;ca*RH!1u zE^Xi`yra|BPmHW@3lXx4C{RjG!b4`o+dqs>t;n4FZ8z97F4xHi7fRTSrBK@>&k~*O z8H@IQ=WWF+=jK{_p}8!J;0SX2o$E<8%3sOddcP);zCJQv;~>(?=J&vG!Wp1_OXB*G z1H!lEEahmuwdRqBEf0vonuC?omWFVGTlNv(CK&i(1us4JVURKK3$MnteD_zDbEAIi zHZ)DC>8>xF6z@Y_Zuy-oiVj$Zk_9X0U6U%xnm-C4^fx$P*~7N5ASmykuazF zcU6ei$>@?LrnY*lo|VLX!9uf_sJo3SN!{~T{2!$CgcE1iH*xHrz{_gRJFM~!Z81*! zACHm+tQ^a6w_Zx`g3p_)7DbnLGvu6Q>#~k%Fj>e_G>~XsvHzMJ(Xq1r6#c*xPDTE{ zKf~EWa}4tIWkBEA60*whBdss|h2pUnZd21IR~WJqS;zh8ZH#kvGK5eJ;6<+JAjtVV z*OL+ML4vwAcG*QLe9oM89~y5b@*YQe)Ne=BBO)(*@;ZRNN-Gg}(ZvnG=2-3*zA z^7>ZBRq8Y=rs3DdKdH5*O{ddDeUUc8O!eKm-WlDoL(mZJ)ZSDb4V3bc#sN(-8Ey5< zWBUc9_V_imJ;$o$#q(@c%K>^#zx(tBZtK2r$_7EPc^%a`G9&9iEojg?Nn*LfmQ9aX zxUEl)ihVUT|NXDC+9|~FxgqzXU&4mPVGeDW%J!?CD9I@~gKNYR@Y|dIv$qP2E^=RWe2u8#? zQ>8z_*Ow*}E`^Li5fk+9kq@;S9)E0DSCWJ|_67Sd!JUY+I$ZFhlJ|vi+G}rBCPdjh zXAh_JD*GlSC)?PHp2O>V#?!ouok=7K>Xx#n%8%>;^o`mYPPu@$bkkZr(uqxRx&mFA zN%*0TaK#Lku6zCXek)JNK1#BU#8tlTZ+Qm(2<&wZL|l4$5)ck^d@@ya_6Kp=R0WU! zEusmGyl2YZZ_16Qhr(@98sEGAvR!@~V?>5Z-7!DadqYl30=jTFQLuaSdn6OyjIg2; z`LH!7#Be>Lqp?l=O)sRU=5JN0o>6T+Z4wu48_*= zkQI?#T9YSGo_PkSDDOV0w-CBNJb%H`XK!&D*GxvCu5uyF$m0NNrmcV?6*|i|1X5dk zi(7{^65Y#jd>cUIMlZq#!9Rr#kX3F7VGEiPX59=Q3$TMcsR_0qQuD-0nxqJEddRrV z`gDmL>NapP)J7ubFAdmsG|CbV>5=&B63BW{R{ARAoqS+K@%`PYdj9 z$HzxY=*;rs+7^P5rnD}lR^7j<3rkjB*CWCioy3u213pIG!L;OHvb_kaDDZ@SM$eoZB?oU)cl#`YyXZ&~NR^NdgCbW53oHSldD z+e+L9&3(SiQv86fi1QOhPUhHK3GiJ!xH1qvEutr7QT%Q#oe#rBhsSj%Qv@|11ke%6+pBcD20IWAp+P&|U1?7kfr(_A|cc%G??liCXM0*a60I;SVDTrynOxe`GlkJO(!+Cj8B{=U7n+%5P}An z1_?gEw$=_SbDPg~4d<7fX92pR#C%%dnHa?2A$!|qP57SxSK!O($cxy;=6CoAUi}yd z;fUjFHnPCu8?u;+|4Dr{4KJ;K9{$H52#=?&UKnd3CMx@LB4ZxlbpZX$pp@8jlW@4B zUe|Z;FoMsBNR;Vt8jCC%I`Eb`q#s3hDFWXlTs+8j^7+pxhI4YYZe2t}y+86+H!eRy zN)r6p7&d3|(_B)8j)8x!f5hK1W?sM+!GjCq701%wVzHOh9kMm!2EX;lZu7ezr(QVM zZ8aZ|>goA1dPR|4scbr(>*4;Dk1z1ZfB(u0;E;Vm0iCn7f%X$ zF7{ta5Ir8uKxzyF?yJY0&QI3r z4y*V!UVY9{3QKzm4JPXV2#_}%Kcyr&Itw)<+kG_+R--Q#8jWbYgkl45G`dqiIw5a) zn{Z@1f;FPhPkt*bq{kT?u@`_cDoLTUp7$n27sk^}*I8ABA!>V^>km^Iq|gu$lO8Wn zVQtsuUbp%c5f*zo{T&+{@n3r?Mt{iKC6CsyUJ>xd`hZo^(Xa{+cypEQwlxWD7fD2g z3ZdL5+YD26ODE+1?{hK*M*OwlK;=bwp{64q*0WS3;hBHVCY-6iquFQw3IY|I(`qJ zg)D!DgGcX=uR;jrDPFP? z2vjc%1mD8nAaR*o2r0GK3~vVyj!4FP)~lZo`QN><@R2K zB6*n?YGwW{4F}nFCWTyieA?GYa^A+)!c%G;?{cu-3XC; zL#gR<;`eB_B+&L=F#u|G)0qxum?b(B07ah}-f%@~iMJt_DpFwTo%|dT3u$0P0{XLW#uCZ9#*%B!PBIyNq0YZJefxvU~`+6ttTJ;({Pb zKmm;9OA4n+P%GEcfpojVBQw`HxeY(#cl<@oyj+0+Gh4)rpJsSV zbbJ5@C`@ggXcVG_8FbuAAFals)5fcCkt2ejApoFoC8}kh?Ygs}e;(1`OluGB{*wmt zR?GHqB6wqQs|1raFU13vAN!ZX(zLeHHS#e3(B7jmT^TpXEh$DO=C~rVaj@&jN#^dQ zC?rkIKO-ThLGS2vK*I<`5A4gzC<5tL4`^Bx@?Aq~q%hpg|9sGzRTzJ#$0yNjxPbH=Zgy;P+S=Uz>T?_cBogrs4Q!=nYtUI$+^EO{=sxF?XeuJ7k=%o*Li zLe=ihm$&&S&$M@ZEJs8+n(msIyt(Evw&2MSwo#97MHq;mqo**hUgMuZ8b~y+rfR(D zqGjXNlck~L*Diqg9Ht~$x}*B*X2j1gprnY|pf=)I;+(VAPZ(2%O zc0@;!alR##5;OE38M)){3{r>W1f*sFoDb}m+Xt?-{FerCKwB4wEkcZ}Z%Y5U$!n%R zApCLBg5__I6*O^59qF|}C0Qt|kb>2IA3zB>-F2*;3ivlEMUWvVJ5B@@wJ0bVA7HEH zz4u24*+n7g%Sq-h$)bmNw3p0bh}z!1t{FbyMO!*)wGp(xBAQ?qD&Ij|YnzuYQP?$n zudM`;I(AVpxbvdbLxovx8A|2GFR#_=p%^8Tx4wDQgXeLVV0caqW_LH7ePI<626Ov+s}~<$-S?Y!5*cdDj@Xb8-LCRxann5Jw_~}*X8@WdX;ZX` z$9bt@8N-8W6u#=ZfeAa&RSsp}c6O@Idlu%WlFEo2qpv0cpeWFf!OE{@qU3b|O;#=+ z-joKAL7(zkZR55*KUd!`j-}T?ekgIoXV$h@U7c^G)ZCF6%>+x+345aNgj6+*kTH9r z#7KNm0SEE%#IVwGuBg`nf9UKqE&+;hGM3BC+U;{&i%d$>Z^}R%w8tTzRE;s3*IyQb zD}djsT!ZLMZ`Zbe(PTrg>tNzs1ab8N}Rr&x=iZ^xSenxJ42G``sJ7 zKOIr(G*m*1zN{`<$7OPOu!*#0@7Hc@}(^~=-wtzbfAe3eq^|BqBI0+Ae!huAC6>br5*<8 zd?wPC6+KTMAzrJ}-u2tsZ0LBS3?yNhoSqH8Ne`U=2S5-Ti*(Alz;zc=dZ;GhM^X+5 zJV?M6L^co!XgD6!;7DJSkEHd#irtl>`V>PZ+5Qsi70?Ck_C` zf+fZ*p!h3pu|H2NcFZzIN{g#cXBPVqU+)1QaDJyWlUBe#dkge1;`4xx9Uq;c`j`K* zOTht|EkT1C{TL4Mp+8`+mQBT?(h-3s0F^iLGt%6lnOvh3iLha5N}mXyHDmH|_04su zYs`b3ul`HyEFuhL4a`QoFkFHAms<;oP**6bozI3$T|*A3(1>oj>_Hg4QxJjlGfN&S zAO1pRFWJVP;umeUigckpB@CwCgrm0A;RRE`vWIgiqtFW3%mxqr%Ju&*+f z!O1-xplhk2i|oN-(aGH=LJlS^re1rpnmcSdW;(-)1-UH>zT$43G6JPE?)59%=yYWua^7QtAl$rs;GU$Igs z;kuoDrtY3jhGjp?S}+Ohonu5B-~dw#C|!vy^I9)KO;)dVb4$~bnUbtY=Sjw%C`w$R zd7m9(MJh)A%X343B%eQ|Tcyx-HM;mfl8TuWW`I3r$m;A9-DCyJZIKF|1yXTp66Dy(GHa-87AW3(FhbXI_iiqGRKPt15*b$oT%NUy3Q`Uc5#OlKBzjVsG#1EE7l{6Jgny`y63YyHN zOm*?J8)$6kq-6QV1`KtCaB>l0+K_%3H;UqlRW879RJEjqTN%Te z9NICmle^T|T<4mk=@8xtTwz~_*|r6ca|d%N(zSos-zrv4>-b*T1MTc8Osl%>PmR1& zY)`n|jXG$sO?r?z(8rC3!5xZF@RO}*&nvCq_lYhld^Mb$$^T=Lq;F4rytxPR1Dy|_ zbr>>%gmQ~uIweV^D~>K@oUW=Y1pYXfhh!I;+3rUZ z6#p6^w}4d^?OK67oG0UfOn{_<>VhxtWazG*wK2)-J_Y4Ys^)cl z>{CB)QR98l4=I&DprjJjKk^9Cw%C2IG62NMZfN$DbQ<}a$eMy4(X)Ny%<6vH9~CUK z?KyvLKJisjq>5V*t%#>q7M{f;*GA(3FaA?n8HtEaD1XL;Ud^U_HW@v+K?yt}pkXYQ_#xxl|9f?ZmQc?TiPeMlz)t*|1Uh}bqac$=3e#F zM7S}vmf{mqscCRGf_fR`BsL%)PM-5plvyS8#YRjZJfs#Jn6u!oxhoQ{E*EQWKw-#C zc7SOKH>7gx9n*(njq_MgTyXyye34KBs(Y|Zz>nl$xrF&Qk-zyY7=9b{O*~TlNsWW= zxJ-;L=qeh+d6~=Jlhg!ETS+Iq?iKuP0Oy~byrp)Po5GL5_{xBdq+==8Y$z|X@Q#q2 zPCV}tTgQ%@eOZCkcQ!4EY_|{J?kmc4*bf!NW*TK|2H;kcoE_y+l4dr8$TTBu)b2|v z#IHs4mOOuhgon5nlVbR?|-Jy%q8|U+Cs{fJ9s0CN5EpUvw z?$EdS52NXCQ>TLV*43{GZj%?A8?rG+Wl99z!5u=Mt zw;cXR%JSC6MN|Uzv~bzwEX}uQ6A&5Dh_NH-Q{+Y4>C@C{19WJL{o@qj-S{gh2yM@o zr4V$rUEs;1#S)~d;5rhd%Aq+@Fl1_JU8Y&|1&m1rj&>*8t;Kh!3j5+tk^REB(9Drz zXrjlXF11ZX^ZsT1yAjKukex87O#lpe8>-d;-=g;*6olQzO<}N47*-el(z0MMDqCcL zsaM)39ari@hf6qCs3JJ_ix)@|wQ=53NEsKlV!8&L3CdbztSiW%N!p|(Wr5mxJy@oQ zDmgo~r8K}UK(I{barQ^CZDkujwgotmOzhJ~w4QJ3)+Mi}02l7@dpctFhy@}(j3@W~ zVxJOUSz!=T?u2GDU(*I#e5Jv*%iAm%3{lAHw<1c`b(ROR0r*br$ zh|1s%G4Qsr^tKhYerF3W03I%GL3VC#b}l|09$sM{K4Cr{RxWN~F0M^8bN~OVfvdZX zqg}xNzd3<_=yW4sDS-!Ic{QUel9NnC~tSvokIo#jb=bwp^!G!=th?;Dz Iv_;te0i57}lmGw# literal 0 HcmV?d00001 diff --git a/scc/gui/app.py b/scc/gui/app.py index f6193f969..637b8bfbe 100644 --- a/scc/gui/app.py +++ b/scc/gui/app.py @@ -156,7 +156,8 @@ def load_gui_config_for_controller(self, controller, first): grEditor = self.builder.get_object('grEditor') btC = self.builder.get_object('btC') btCPAD = self.builder.get_object('btCPAD') - config = self.background.load_config(controller.get_gui_config_file()) + config = controller.load_gui_config(self.imagepath or {}) + config = self.background.use_config(config) def do_loading(): """ Called after transition is finished """ diff --git a/scc/gui/controller_image.py b/scc/gui/controller_image.py index 0a764848c..099cea7fb 100644 --- a/scc/gui/controller_image.py +++ b/scc/gui/controller_image.py @@ -81,30 +81,17 @@ def get_names(dict_or_tuple): ] - def load_config(self, filename): + def use_config(self, config): """ - Loads controller settings from config file sent by daemon. - May be None or invalid, in which case, defaults are loaded. + Loads controller settings from provided config, adding default values + when needed. Returns same config. """ - if filename: - if "/" not in filename: - filename = os.path.join(self.app.imagepath, filename) - try: - data = json.loads(open(filename, "r").read()) or {} - except Exception, e: - log.exception(e) - data = {} - else: - data = {} - return self._ensure_config(data) - - - def use_config(self, config): self.current = self._ensure_config(config) self.set_image(os.path.join(self.app.imagepath, - "controller-images/%s.svg" % (config["gui"]["background"], ))) - self._fill_button_images(config["gui"]["buttons"]) + "controller-images/%s.svg" % (self.current["gui"]["background"], ))) + self._fill_button_images(self.current["gui"]["buttons"]) self.hilight({}) + return self.current def get_button_groups(self): diff --git a/scc/gui/daemon_manager.py b/scc/gui/daemon_manager.py index c827e649e..30826aa40 100644 --- a/scc/gui/daemon_manager.py +++ b/scc/gui/daemon_manager.py @@ -14,7 +14,7 @@ from scc.tools import find_binary from gi.repository import GObject, Gio, GLib -import os, sys, logging +import os, sys, json, logging log = logging.getLogger("DaemonCtrl") @@ -379,6 +379,24 @@ def get_gui_config_file(self): return self._config_file + def load_gui_config(self, default_path): + """ + As get_gui_config_file, but returns loaded and parsed config. + Returns None if config cannot be loaded. + """ + filename = self.get_gui_config_file() + if filename: + if "/" not in filename: + filename = os.path.join(default_path, filename) + try: + data = json.loads(open(filename, "r").read()) or None + return data + except Exception, e: + log.exception(e) + return None + + + def get_profile(self): """ Returns profile set for this controller. Value is cached locally. """ return self._profile diff --git a/scc/osd/quick_menu.py b/scc/osd/quick_menu.py index 198935394..2a93363c7 100644 --- a/scc/osd/quick_menu.py +++ b/scc/osd/quick_menu.py @@ -11,6 +11,7 @@ from gi.repository import Gtk, GLib from scc.menu_data import MenuItem, Submenu from scc.tools import find_icon, find_menu +from scc.paths import get_share_path from scc.config import Config from scc.osd.menu import Menu, MenuIcon from scc.osd import OSDWindow @@ -21,12 +22,15 @@ class QuickMenu(Menu): BUTTONS = [ "A", "B", "X", "Y", "LB", "RB"] + BUTTON_INDEXES = [ 0, 1, 2, 3, 7, 8] # indexes to gui->buttons list + # in controller gui config def __init__(self, cls="osd-menu"): Menu.__init__(self, cls) self._cancel_with = 'START' self._pressed = [] + self._icons = [] self._timer = None @@ -69,6 +73,7 @@ def generate_widget(self, item): label = widget.get_children()[0] for c in [] + widget.get_children(): widget.remove(c) + self._icons.append(icon) box = Gtk.Box() box.pack_start(icon, False, True, 0) box.pack_start(label, True, True, 10) @@ -102,6 +107,19 @@ def _add_arguments(self): def lock_inputs(self): def success(*a): log.error("Sucessfully locked input") + config = self.controller.load_gui_config(os.path.join( + get_share_path(), "images")) + if config and config["gui"] and config["gui"]["buttons"]: + buttons = config["gui"]["buttons"] + try: + for i in xrange(len(self._icons)): + icon = self._icons[i] + name = buttons[self.BUTTON_INDEXES[i]] + filename, trash = find_icon("buttons/%s" % name) + icon.set_filename(filename) + icon.queue_draw() + except IndexError: + pass locks = [ x for x in self.BUTTONS ] + [ self._cancel_with ] self.controller.lock(success, self.on_failed_to_lock, *locks) diff --git a/scc/sccdaemon.py b/scc/sccdaemon.py index b29cd5d90..60a3c4588 100644 --- a/scc/sccdaemon.py +++ b/scc/sccdaemon.py @@ -1295,26 +1295,6 @@ def _unobserve(a): self.mapper.get_controller(), hash(self.client)) - def reaply(self, client, daemon): - client.observe_action(daemon, self.what) - - - def unlock(self, client, daemon): - def _unobserve(a): - if isinstance(a, ObservingAction): - if a.client == self: - return a.original_action - a.original_action = _unobserve(a.original_action) - return a - if isinstance(a, LockedAction): - a.original_action = _unobserve(a.original_action) - return a - return a - - daemon._apply(client.mapper, self.what, _unobserve) - log.debug("%s no longer observed by %s", self.what, client) - - def trigger(self, mapper, position, old_position): ReportingAction.trigger(self, mapper, position, old_position) self.original_action.trigger(mapper, position, old_position) From 1d0c12602f9b05dba96fb423ab166b88d91298a4 Mon Sep 17 00:00:00 2001 From: tom Date: Sat, 11 Nov 2017 17:59:55 +0100 Subject: [PATCH 26/44] Getting DS4 HID touchpad ready, it doesn't work at all :( --- glade/global_settings.glade | 4 ++-- run.sh | 2 +- scc/drivers/ds4drv.py | 6 ++++++ scc/drivers/hiddrv.c | 14 +++++++++++--- scc/drivers/hiddrv.py | 13 ++++++++++--- 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/glade/global_settings.glade b/glade/global_settings.glade index 85a823c67..7072b01d6 100644 --- a/glade/global_settings.glade +++ b/glade/global_settings.glade @@ -1264,7 +1264,7 @@ or even crash. True False - Enable Dualshock®4 (PS4 controller) support + Enable Dualshock4 (PS4 controller) support 0 @@ -1301,7 +1301,7 @@ or even crash. True False 30 - If enabled, any connected Dualshock®4 controller will be automatically used by SC-Controller + If enabled, any connected Dualshock4 controller will be automatically used by SC-Controller 0 0 diff --git a/run.sh b/run.sh index 4febc07d0..418a83e99 100755 --- a/run.sh +++ b/run.sh @@ -1,7 +1,7 @@ #!/bin/bash C_MODULES=(uinput hiddrv) C_VERSION_uinput=8 -C_VERSION_hiddrv=2 +C_VERSION_hiddrv=3 function rebuild_c_modules() { echo "lib$1.so is outdated or missing, building one" diff --git a/scc/drivers/ds4drv.py b/scc/drivers/ds4drv.py index 71304e6f7..44994222d 100644 --- a/scc/drivers/ds4drv.py +++ b/scc/drivers/ds4drv.py @@ -101,6 +101,12 @@ def _load_hid_descriptor(self, config, max_size, vid, pid, test_mode): mode = AxisMode.DS4GYRO, byte_offset = 19) self._decoder.axes[AxisType.AXIS_Q3] = AxisData( mode = AxisMode.DS4GYRO, byte_offset = 21) + + + self._decoder.axes[AxisType.AXIS_CPAD_X] = AxisData( + mode = AxisMode.DS4TOUCHPADX, byte_offset = 36) + #self._decoder.axes[AxisType.AXIS_CPAD_Y] = AxisData( + # mode = AxisMode.DS4TOUCHPADY, byte_offset = 37) self._decoder.buttons = ButtonData( enabled = True, byte_offset=5, bit_offset=4, size=14, button_count = 14 diff --git a/scc/drivers/hiddrv.c b/scc/drivers/hiddrv.c index 7d0155583..a311a856f 100644 --- a/scc/drivers/hiddrv.c +++ b/scc/drivers/hiddrv.c @@ -5,10 +5,10 @@ #include #define CLAMP(min, x, max) x -#define HIDDRV_MODULE_VERSION 2 +#define HIDDRV_MODULE_VERSION 3 PyObject* module; -#define AXIS_COUNT 15 +#define AXIS_COUNT 17 #define BUTTON_COUNT 32 struct HIDControllerInput { @@ -33,6 +33,8 @@ enum AxisType { AXIS_Q2 = 12, AXIS_Q3 = 13, AXIS_Q4 = 14, + AXIS_CPAD_X = 15, + AXIS_CPAD_Y = 16, _AxisType_force_int = INT_MAX }; @@ -45,7 +47,8 @@ enum AxisMode { HATSWITCH = 4, DS4ACCEL = 5, DS4GYRO = 6, - + DS4TOUCHPADX = 7, + DS4TOUCHPADY = 8, _AxisMode_force_int = INT_MAX }; @@ -243,6 +246,11 @@ bool decode(struct HIDDecoder* dec, const char* data) { dec->axes[i].bit_offset); dec->state.axes[i] = -value.s16; break; + case DS4TOUCHPADX: + value = grab_value(data, dec->axes[i].byte_offset, + dec->axes[i].bit_offset); + dec->state.axes[i] = (value.u16 & 0x00FF) | ((value.u16 & 0x0F00) >> 8); + // dec->state.axes[i] = (value.u16 & 0xFF00); default: break; } diff --git a/scc/drivers/hiddrv.py b/scc/drivers/hiddrv.py index 146d108f2..61acf7ed1 100644 --- a/scc/drivers/hiddrv.py +++ b/scc/drivers/hiddrv.py @@ -24,7 +24,7 @@ DEV_CLASS_HID = 3 TRANSFER_TYPE_INTERRUPT = 3 LIBUSB_DT_REPORT = 0x22 -AXIS_COUNT = 15 # Must match number of axis fields in HIDControllerInput and values in AxisType +AXIS_COUNT = 17 # Must match number of axis fields in HIDControllerInput and values in AxisType BUTTON_COUNT = 32 # Must match (or be less than) number of bits in HIDControllerInput.buttons ALLOWED_SIZES = [1, 2, 4, 8, 16, 32] SYS_DEVICES = "/sys/devices" @@ -53,6 +53,8 @@ class HIDControllerInput(ctypes.Structure): ('q2', ctypes.c_int32), ('q3', ctypes.c_int32), ('q4', ctypes.c_int32), + ('cpad_x', ctypes.c_int32), + ('cpad_y', ctypes.c_int32), ] @@ -72,6 +74,8 @@ class AxisType(IntEnum): AXIS_Q2 = 12 AXIS_Q3 = 13 AXIS_Q4 = 14 + AXIS_CPAD_X = 15 + AXIS_CPAD_Y = 16 class AxisMode(IntEnum): @@ -82,6 +86,8 @@ class AxisMode(IntEnum): HATSWITCH = 4 DS4ACCEL = 5 # 16bit, signed, no additional math needed DS4GYRO = 6 # 16bit, signed, inverted + DS4TOUCHPADX = 7 # 15bit + DS4TOUCHPADY = 8 # 15bit, pure magic involved class AxisModeData(ctypes.Structure): @@ -213,7 +219,6 @@ def __init__(self, device, daemon, handle, config_file, config, test_mode=False) def _load_hid_descriptor(self, config, max_size, vid, pid, test_mode): hid_descriptor = HIDController.find_sys_devices_descriptor(vid, pid) if hid_descriptor is None: - print "get raw" hid_descriptor = self.handle.getRawDescriptor( LIBUSB_DT_REPORT, 0, 512) open("report", "wb").write(b"".join([ chr(x) for x in hid_descriptor ])) @@ -485,7 +490,9 @@ def test_input(self, endpoint, data): for attr, trash in self._decoder.state._fields_: if attr == "buttons": continue if getattr(self._decoder.state, attr) != getattr(self._decoder.old_state, attr): - print "Axis", code, getattr(self._decoder.state, attr) + # if code not in (8, 9, 10, 11, 12, 13): + if code == 15: + print "Axis", code, getattr(self._decoder.state, attr) sys.stdout.flush() code += 1 From 9a05d1d92610db0888a0dc50353db3732c846f19 Mon Sep 17 00:00:00 2001 From: kozec Date: Sat, 11 Nov 2017 21:17:02 +0100 Subject: [PATCH 27/44] DS4 touchpad over HID. Ref #280 --- scc/drivers/ds4drv.py | 7 +++---- scc/drivers/hiddrv.c | 9 ++++----- scc/drivers/hiddrv.py | 7 ++----- scripts/90-sc-controller.rules | 2 ++ 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/scc/drivers/ds4drv.py b/scc/drivers/ds4drv.py index 44994222d..25d5e598c 100644 --- a/scc/drivers/ds4drv.py +++ b/scc/drivers/ds4drv.py @@ -101,12 +101,11 @@ def _load_hid_descriptor(self, config, max_size, vid, pid, test_mode): mode = AxisMode.DS4GYRO, byte_offset = 19) self._decoder.axes[AxisType.AXIS_Q3] = AxisData( mode = AxisMode.DS4GYRO, byte_offset = 21) - self._decoder.axes[AxisType.AXIS_CPAD_X] = AxisData( - mode = AxisMode.DS4TOUCHPADX, byte_offset = 36) - #self._decoder.axes[AxisType.AXIS_CPAD_Y] = AxisData( - # mode = AxisMode.DS4TOUCHPADY, byte_offset = 37) + mode = AxisMode.DS4TOUCHPAD, byte_offset = 36) + self._decoder.axes[AxisType.AXIS_CPAD_Y] = AxisData( + mode = AxisMode.DS4TOUCHPAD, byte_offset = 37, bit_offset=4) self._decoder.buttons = ButtonData( enabled = True, byte_offset=5, bit_offset=4, size=14, button_count = 14 diff --git a/scc/drivers/hiddrv.c b/scc/drivers/hiddrv.c index a311a856f..7a6750db8 100644 --- a/scc/drivers/hiddrv.c +++ b/scc/drivers/hiddrv.c @@ -47,8 +47,7 @@ enum AxisMode { HATSWITCH = 4, DS4ACCEL = 5, DS4GYRO = 6, - DS4TOUCHPADX = 7, - DS4TOUCHPADY = 8, + DS4TOUCHPAD = 7, _AxisMode_force_int = INT_MAX }; @@ -246,11 +245,11 @@ bool decode(struct HIDDecoder* dec, const char* data) { dec->axes[i].bit_offset); dec->state.axes[i] = -value.s16; break; - case DS4TOUCHPADX: + case DS4TOUCHPAD: value = grab_value(data, dec->axes[i].byte_offset, dec->axes[i].bit_offset); - dec->state.axes[i] = (value.u16 & 0x00FF) | ((value.u16 & 0x0F00) >> 8); - // dec->state.axes[i] = (value.u16 & 0xFF00); + dec->state.axes[i] = (value.u16 & 0x0FFF); + break; default: break; } diff --git a/scc/drivers/hiddrv.py b/scc/drivers/hiddrv.py index 61acf7ed1..baf1619c2 100644 --- a/scc/drivers/hiddrv.py +++ b/scc/drivers/hiddrv.py @@ -86,8 +86,7 @@ class AxisMode(IntEnum): HATSWITCH = 4 DS4ACCEL = 5 # 16bit, signed, no additional math needed DS4GYRO = 6 # 16bit, signed, inverted - DS4TOUCHPADX = 7 # 15bit - DS4TOUCHPADY = 8 # 15bit, pure magic involved + DS4TOUCHPAD = 7 # 12bit class AxisModeData(ctypes.Structure): @@ -490,9 +489,7 @@ def test_input(self, endpoint, data): for attr, trash in self._decoder.state._fields_: if attr == "buttons": continue if getattr(self._decoder.state, attr) != getattr(self._decoder.old_state, attr): - # if code not in (8, 9, 10, 11, 12, 13): - if code == 15: - print "Axis", code, getattr(self._decoder.state, attr) + print "Axis", code, getattr(self._decoder.state, attr) sys.stdout.flush() code += 1 diff --git a/scripts/90-sc-controller.rules b/scripts/90-sc-controller.rules index 40b16e8b6..75e64642c 100644 --- a/scripts/90-sc-controller.rules +++ b/scripts/90-sc-controller.rules @@ -4,5 +4,7 @@ # Valve USB devices SUBSYSTEM=="usb", ATTRS{idVendor}=="28de", MODE="0666" +# Sony USB devices +SUBSYSTEM=="usb", ATTRS{idVendor}=="054c", MODE="0666" # uinput kernel module write access (allows keyboard, mouse and gamepad emulation) KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess" From c0baeca7948540201f69020a396523c3205dc43b Mon Sep 17 00:00:00 2001 From: tom Date: Sun, 12 Nov 2017 17:45:47 +0100 Subject: [PATCH 28/44] DS4 over HID: Detecting touch on touchpad --- scc/drivers/ds4drv.py | 21 ++++++++++++++++++--- scc/drivers/hiddrv.py | 4 ++-- scc/modifiers.py | 4 ++-- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/scc/drivers/ds4drv.py b/scc/drivers/ds4drv.py index 25d5e598c..31aa4c751 100644 --- a/scc/drivers/ds4drv.py +++ b/scc/drivers/ds4drv.py @@ -8,14 +8,14 @@ from scc.drivers.hiddrv import BUTTON_COUNT, ButtonData, AxisType, AxisData from scc.drivers.hiddrv import HIDController, HIDDecoder, hiddrv_test from scc.drivers.hiddrv import AxisMode, AxisDataUnion, AxisModeData -from scc.drivers.hiddrv import HatswitchModeData +from scc.drivers.hiddrv import HatswitchModeData, _lib from scc.drivers.evdevdrv import HAVE_EVDEV, EvdevController from scc.drivers.evdevdrv import make_new_device, get_axes from scc.drivers.usb import register_hotplug_device from scc.constants import SCButtons, ControllerFlags from scc.constants import STICK_PAD_MIN, STICK_PAD_MAX from scc.tools import init_logging, set_logging_level -import sys, logging +import sys, logging, ctypes log = logging.getLogger("DS4") VENDOR_ID = 0x054c @@ -123,6 +123,19 @@ def _load_hid_descriptor(self, config, max_size, vid, pid, test_mode): self._packet_size = 64 + def input(self, endpoint, data): + # Special override for CPAD touch button + if _lib.decode(ctypes.byref(self._decoder), data): + if self.mapper: + if ord(data[35]) >> 7: + # cpad is not touched + self._decoder.state.buttons &= ~SCButtons.CPADTOUCH + else: + self._decoder.state.buttons |= SCButtons.CPADTOUCH + self.mapper.input(self, + self._decoder.old_state, self._decoder.state) + + def get_gyro_enabled(self): # Cannot be actually turned off, so it's always active # TODO: Maybe emulate turning off? @@ -169,6 +182,7 @@ class DS4EvdevController(EvdevController): 316: "C", 317: "STICKPRESS", 318: "RPAD" + # 319: "CPAD", } AXIS_MAP = { 0: { "axis": "stick_x", "deadzone": 4, "max": 255, "min": 0 }, @@ -191,7 +205,8 @@ class DS4EvdevController(EvdevController): 313: "START", 314: "STICKPRESS", 315: "RPAD", - 316: "C" + 316: "C", + # 317: "CPAD", } AXIS_MAP_OLD = { 0: { "axis": "stick_x", "deadzone": 4, "max": 255, "min": 0 }, diff --git a/scc/drivers/hiddrv.py b/scc/drivers/hiddrv.py index baf1619c2..77e423876 100644 --- a/scc/drivers/hiddrv.py +++ b/scc/drivers/hiddrv.py @@ -168,6 +168,7 @@ class HIDDecoder(ctypes.Structure): class HIDController(USBDevice, Controller): + flags = ControllerFlags.HAS_RSTICK | ControllerFlags.SEPARATE_STICK def __init__(self, device, daemon, handle, config_file, config, test_mode=False): USBDevice.__init__(self, device, handle) @@ -196,7 +197,6 @@ def __init__(self, device, daemon, handle, config_file, config, test_mode=False) self._load_hid_descriptor(config, max_size, vid, pid, test_mode) self.claim_by(klass=DEV_CLASS_HID, subclass=0, protocol=0) Controller.__init__(self) - self.flags = ControllerFlags.HAS_RSTICK | ControllerFlags.SEPARATE_STICK if test_mode: self.set_input_interrupt(id, self._packet_size, self.test_input) @@ -489,7 +489,7 @@ def test_input(self, endpoint, data): for attr, trash in self._decoder.state._fields_: if attr == "buttons": continue if getattr(self._decoder.state, attr) != getattr(self._decoder.old_state, attr): - print "Axis", code, getattr(self._decoder.state, attr) + # print "Axis", code, getattr(self._decoder.state, attr) sys.stdout.flush() code += 1 diff --git a/scc/modifiers.py b/scc/modifiers.py index 5caf8b478..b5d3ccc2e 100644 --- a/scc/modifiers.py +++ b/scc/modifiers.py @@ -10,10 +10,10 @@ from scc.actions import Action, MouseAction, XYAction, AxisAction, RangeOP from scc.actions import NoAction, WholeHapticAction, HapticEnabledAction -from scc.constants import TRIGGER_MAX, LEFT, RIGHT, STICK, FE_STICK, FE_TRIGGER from scc.constants import STICK_PAD_MIN, STICK_PAD_MAX, STICK_PAD_MAX_HALF from scc.constants import FE_PAD, SCButtons, HapticPos, ControllerFlags -from scc.constants import CUT, ROUND, LINEAR +from scc.constants import CUT, ROUND, LINEAR, FE_STICK, FE_TRIGGER +from scc.constants import TRIGGER_MAX, LEFT, CPAD, RIGHT, STICK from scc.controller import HapticData from scc.tools import nameof, clamp from scc.uinput import Axes, Rels From af60a9a4ed7e39ab6a21625019ab31a6f20fd59f Mon Sep 17 00:00:00 2001 From: kozec Date: Mon, 13 Nov 2017 16:32:04 +0100 Subject: [PATCH 29/44] Reorganizing settings so I can fit more stuff there --- glade/global_settings.glade | 738 +++++++++++++++++++----------------- 1 file changed, 380 insertions(+), 358 deletions(-) diff --git a/glade/global_settings.glade b/glade/global_settings.glade index 7072b01d6..a338c134c 100644 --- a/glade/global_settings.glade +++ b/glade/global_settings.glade @@ -235,7 +235,7 @@ or even crash. True False - Bindings + On-Screen Keyboard Bindings 0 @@ -243,7 +243,7 @@ or even crash. 0 - 0 + 7 6 @@ -259,7 +259,7 @@ or even crash.

0 - 1 + 8
@@ -274,7 +274,7 @@ or even crash.
0 - 2 + 9 @@ -291,7 +291,7 @@ or even crash.
0 - 3 + 10 6
@@ -313,7 +313,7 @@ or even crash.
1 - 1 + 8 5
@@ -335,7 +335,7 @@ or even crash.
1 - 2 + 9 5
@@ -352,7 +352,7 @@ or even crash.
0 - 7 + 11 6
@@ -368,7 +368,7 @@ or even crash.
0 - 8 + 12 2
@@ -384,7 +384,7 @@ or even crash.
0 - 9 + 13 2 @@ -401,7 +401,7 @@ or even crash.
2 - 8 + 12 3
@@ -418,7 +418,7 @@ or even crash.
2 - 9 + 13 3
@@ -441,7 +441,7 @@ or even crash.
5 - 8 + 12 @@ -463,15 +463,15 @@ or even crash.
5 - 9 + 13
- + True False - 20 - Colors + 5 + Default Menu Items 0 @@ -479,171 +479,272 @@ or even crash. 0 - 4 + 0 6 - + + cbMI_0 True True - True - 10 - 20 + False + 20 + 10 5 True - + True + + + + True + False + List of recent profiles + 0 + + - 1 - 5 + 0 + 1 + 3 - + + cbMI_6 True - False - 20 + True + False + 5 + 20 5 - Normal Button - 0 + True + True + + + + True + False + Run Program... + 0 + + - 0 - 5 + 3 + 1 + 3 - + + cbMI_1 True - False + True + False + 20 + 5 5 - Special Button - 0 + True + + + + True + False + Autoswitch options + 0 + + - 2 - 5 + 0 + 2 + 3 - - 20 + + cbMI_5 True True - True - 10 + False + 5 20 5 - True - + True + + + + True + False + Kill Current Window + 0 + + 3 - 5 + 2 + 3 - + + cbMI_2 True - False + True + False 20 + 5 5 - Pressed Button - 0 + True + + + + True + False + Window switcher + 0 + + 0 - 6 + 3 + 3 - - 20 + + cbMI_7 True True - True - 10 + False + 5 20 5 - True - + True + + + + True + False + Show Current Bindings + 0 + + - 1 - 6 + 3 + 3 + 3 - - 20 + + cbMI_8 True - False + True + False + 20 + 5 5 - Text - 0 + True + + + + True + False + Games List + 0 + + - 4 - 5 + 0 + 4 + 3 - + + cbMI_4 True - False + True + False + 5 + 20 5 - Higlight - 0 + True + + + + True + False + Turn Controller OFF + 0 + + - 2 - 6 + 3 + 4 + 3 - - 20 + + cbMI_3 True True - True - 10 - 20 + False + 20 + 5 5 - True - + True + + + + True + False + Display Keyboard + 0 + + - 5 + 0 5 + 3 - - 20 + True - True - True - 10 - 20 - 5 - True - + False - 3 + 0 6 + 3 - - True - False + + Customize... + True + True + True + 5 + 20 + 10 - 4 - 6 - 2 + 3 + 5 + 3 + 2 @@ -652,7 +753,7 @@ or even crash. True False - OSD Keyboard + Menus & Keyboard True @@ -667,55 +768,6 @@ or even crash. 10 10 10 - - - True - False - 5 - Default Menu Items - 0 - - - - - - 0 - 0 - 4 - - - - - True - False - 10 - Colors - 0 - - - - - - 0 - 6 - 4 - - - - - True - False - 20 - 5 - 5 - Text - 0 - - - 0 - 7 - - True @@ -729,7 +781,7 @@ or even crash. 1 - 7 + 1 @@ -745,37 +797,7 @@ or even crash. 1 - 10 - - - - - True - False - 20 - 5 - 5 - Item Border - 0 - - - 0 - 10 - - - - - True - False - 20 - 5 - 5 - Background - 0 - - - 0 - 8 + 4 @@ -791,22 +813,7 @@ or even crash. 1 - 8 - - - - - True - False - 20 - 5 - 5 - Border - 0 - - - 0 - 9 + 2 @@ -822,7 +829,7 @@ or even crash. 1 - 9 + 3 @@ -837,7 +844,7 @@ or even crash. 2 - 7 + 1 @@ -852,7 +859,7 @@ or even crash. 2 - 8 + 2 @@ -867,7 +874,7 @@ or even crash. 2 - 9 + 3 @@ -882,7 +889,7 @@ or even crash. 2 - 10 + 4 @@ -898,7 +905,7 @@ or even crash. 3 - 7 + 1 @@ -914,7 +921,7 @@ or even crash. 3 - 8 + 2 @@ -930,7 +937,7 @@ or even crash. 3 - 9 + 3 @@ -946,257 +953,271 @@ or even crash. 3 - 10 + 4 - - cbMI_0 + True - True - False + False + OSD Menu Colors + 0 + + + + + + 0 + 0 + 4 + + + + + True + False 20 + 5 5 - True - - - - True - False - 5 - List of recent profiles - 0 - - + Text + 0 0 1 - 2 - - cbMI_1 + True - True - False + False 20 + 5 5 - True - - - - True - False - 5 - Autoswitch options - 0 - - + Item Border + 0 + + + 0 + 4 + + + + + True + False + 20 + 5 + 5 + Background + 0 0 2 - 2 - - cbMI_2 + True - True - False + False 20 + 5 5 - True - - - - True - False - 5 - Window switcher - 0 - - + Border + 0 0 3 - 2 - - Customize... - True - True - True - 20 - 20 + + True + False 10 + On-Screen Keyboard Colors + 0 + + + - 2 + 0 5 - 2 - 2 + 4 - - cbMI_6 + True - True - False + False 20 + 5 5 - True - - - - True - False - 5 - Run Program... - 0 - - + Normal Button + 0 - 2 - 1 - 2 + 0 + 6 - - cbMI_3 + True True - False + True 20 + 10 5 - True - - - - True - False - 5 - Display Keyboard - 0 - - + True + + + + 1 + 6 + + + + + True + False + 20 + 5 + 5 + Pressed Button + 0 0 - 5 - 2 + 7 - - cbMI_8 + + 20 True True - False + True 20 + 10 5 - True - - - - True - False - 5 - Games List - 0 - - + True + + + + 1 + 7 + + + + + True + False + 20 + 5 + 5 + Special Button + 0 0 - 4 - 2 + 8 - - cbMI_4 + + 20 True True - False + True 20 + 10 5 - True - - - - True - False - 5 - Turn Controller OFF - 0 - - + True + + + + 1 + 8 + + + + + 20 + True + False + 20 + 5 + 5 + Text + 0 2 - 4 - 2 + 6 - - cbMI_7 + + 20 True True - False + True 20 + 20 5 - True - - - - True - False - 5 - Show Current Bindings - 0 - - + True + + + + 3 + 6 + + + + + True + False + 20 + 5 + 5 + Higlight + 0 2 - 3 - 2 + 7 - - cbMI_5 + + 20 True True - False + True 20 + 20 5 - True - - - - True - False - 5 - Kill Current Window - 0 - - + True + + + + 3 + 7 + + + + + True + False + 2 - 2 + 8 2 @@ -1209,7 +1230,7 @@ or even crash. True False - OSD & Menus + OSD Colors 1 @@ -1332,6 +1353,7 @@ or even crash. True 20 20 + True in From cbcc34035e119e6dabac4ba6a763f4ede679a05e Mon Sep 17 00:00:00 2001 From: kozec Date: Mon, 13 Nov 2017 16:43:57 +0100 Subject: [PATCH 30/44] Added GUI for OSD themes, does nothing for now. Ref #256 --- glade/global_settings.glade | 116 +++++++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 29 deletions(-) diff --git a/glade/global_settings.glade b/glade/global_settings.glade index a338c134c..211ff6b50 100644 --- a/glade/global_settings.glade +++ b/glade/global_settings.glade @@ -43,6 +43,24 @@ + + + + + + + + + + original + Original (Green) + + + reloaded + Reloaded + + + @@ -781,7 +799,7 @@ or even crash. 1 - 1 + 3 @@ -797,7 +815,7 @@ or even crash. 1 - 4 + 6 @@ -813,7 +831,7 @@ or even crash. 1 - 2 + 4 @@ -829,7 +847,7 @@ or even crash. 1 - 3 + 5 @@ -844,7 +862,7 @@ or even crash. 2 - 1 + 3 @@ -859,7 +877,7 @@ or even crash. 2 - 2 + 4 @@ -874,7 +892,7 @@ or even crash. 2 - 3 + 5 @@ -889,7 +907,7 @@ or even crash. 2 - 4 + 6 @@ -905,7 +923,7 @@ or even crash. 3 - 1 + 3 @@ -921,7 +939,7 @@ or even crash. 3 - 2 + 4 @@ -937,7 +955,7 @@ or even crash. 3 - 3 + 5 @@ -953,7 +971,7 @@ or even crash.
3 - 4 + 6 @@ -968,7 +986,7 @@ or even crash.
0 - 0 + 2 4
@@ -984,7 +1002,7 @@ or even crash.
0 - 1 + 3
@@ -999,7 +1017,7 @@ or even crash.
0 - 4 + 6 @@ -1014,7 +1032,7 @@ or even crash.
0 - 2 + 4
@@ -1029,7 +1047,7 @@ or even crash.
0 - 3 + 5
@@ -1045,7 +1063,7 @@ or even crash.
0 - 5 + 7 4
@@ -1061,7 +1079,7 @@ or even crash.
0 - 6 + 8
@@ -1077,7 +1095,7 @@ or even crash.
1 - 6 + 8
@@ -1092,7 +1110,7 @@ or even crash.
0 - 7 + 9
@@ -1109,7 +1127,7 @@ or even crash.
1 - 7 + 9
@@ -1124,7 +1142,7 @@ or even crash.
0 - 8 + 10 @@ -1141,7 +1159,7 @@ or even crash.
1 - 8 + 10
@@ -1157,7 +1175,7 @@ or even crash.
2 - 6 + 8
@@ -1174,7 +1192,7 @@ or even crash.
3 - 6 + 8
@@ -1189,7 +1207,7 @@ or even crash.
2 - 7 + 9
@@ -1206,7 +1224,7 @@ or even crash.
3 - 7 + 9
@@ -1217,10 +1235,50 @@ or even crash.
2 - 8 + 10 2
+ + + True + False + Theme + 0 + + + + + + 0 + 0 + 4 + + + + + True + False + 50 + 50 + 5 + 10 + True + lstOSDTheme + 0 + + + + 1 + + + + + 0 + 1 + 4 + + 1 From 25557d3e19619a16ad824cc9f24ca5316f24e05d Mon Sep 17 00:00:00 2001 From: kozec Date: Thu, 16 Nov 2017 19:41:27 +0100 Subject: [PATCH 31/44] Fixing glade broken by merge --- glade/global_settings.glade | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/glade/global_settings.glade b/glade/global_settings.glade index 211ff6b50..521ebb7d5 100644 --- a/glade/global_settings.glade +++ b/glade/global_settings.glade @@ -1974,7 +1974,7 @@ grips, triggers, finger positions on both pads and stick angle.
0 - 7 + 8 @@ -2093,7 +2093,29 @@ grips, triggers, finger positions on both pads and stick angle.
0 - 8 + 9 + +
+ + + True + True + False + 30 + 5 + True + + + True + False + Minimize to tray on start + cbShowOSD + + + + + 0 + 7
From 0851079fcf4e8b0a6ce454d8da7f4b6efac6e62c Mon Sep 17 00:00:00 2001 From: kozec Date: Thu, 16 Nov 2017 20:10:49 +0100 Subject: [PATCH 32/44] OSD style settings UI pretty-much works. Ref #259 --- glade/global_settings.glade | 79 ++++++++++++++-- osd_styles/Classic.gtkstyle.css | 111 +++++++++++++++++++++++ osd_styles/Green (dark).colors.json | 26 ++++++ osd_styles/Green.colors.json | 26 ++++++ osd_styles/Reloaded.gtkstyle.css | 115 +++++++++++++++++++++++ scc/config.py | 3 +- scc/gui/global_settings.py | 24 ++++- scc/osd/__init__.py | 136 +++------------------------- 8 files changed, 384 insertions(+), 136 deletions(-) create mode 100644 osd_styles/Classic.gtkstyle.css create mode 100644 osd_styles/Green (dark).colors.json create mode 100644 osd_styles/Green.colors.json create mode 100644 osd_styles/Reloaded.gtkstyle.css diff --git a/glade/global_settings.glade b/glade/global_settings.glade index 521ebb7d5..5c767b433 100644 --- a/glade/global_settings.glade +++ b/glade/global_settings.glade @@ -43,7 +43,7 @@
- + @@ -52,11 +52,29 @@ - original - Original (Green) + Green.colors.json + Green - reloaded + Green (dark).colors.json + Green (dark) + + + + + + + + + + + + + Classic.gtkstyle.css + Classic + + + Reloaded.gtkstyle.css Reloaded @@ -1243,7 +1261,7 @@ or even crash. True False - Theme + OSD Style 0 @@ -1252,20 +1270,21 @@ or even crash. 0 0 - 4 + 2 - + True False 50 - 50 + 10 5 10 True - lstOSDTheme + lstOSDStyle 0 + @@ -1276,7 +1295,47 @@ or even crash. 0 1 - 4 + 2 + + + + + True + False + Color Preset + 0 + + + + + + 2 + 0 + 2 + + + + + True + False + 50 + 5 + 10 + True + lstOSDColorPreset + 0 + + + + + 1 + + + + + 2 + 1 + 2 diff --git a/osd_styles/Classic.gtkstyle.css b/osd_styles/Classic.gtkstyle.css new file mode 100644 index 000000000..a2a7a073e --- /dev/null +++ b/osd_styles/Classic.gtkstyle.css @@ -0,0 +1,111 @@ + +#osd-message, #osd-menu, #osd-gesture, #osd-keyboard { + background-color: #%(background)s; + border: 6px #%(border)s double; +} + +#osd-area { + background-color: #%(border)s; +} + +#osd-label { + color: #%(text)s; + border: none; + font-size: xx-large; + margin: 15px 15px 15px 15px; +} + +#osd-menu, #osd-gesture { + padding: 7px 7px 7px 7px; +} + +#osd-keyboard-container { + padding: 6px 6px 6px 6px; +} + +#osd-menu-item, #osd-menu-item-selected, #osd-menu-dummy, +#osd-menu-item-big-icon, #osd-menu-item-big-icon-selected, +#osd-key-buton, #osd-key-buton-hilight, #osd-launcher-item, +#osd-launcher-item-selected, #osd-hidden-item, +#osd-hidden-item-selected, #osd-key-buton-selected { + color: #%(text)s; + border-radius: 0; + font-size: x-large; + background-image: none; + background-color: #%(background)s; + margin: 0px 0px 2px 0px; +} + + +#osd-hidden-item, #osd-hidden-item-selected { + color: #%(background)s; + border-color: #%(background)s; +} + + +#osd-radial-menu-icon { + color: #%(text)s; +} + +#osd-radial-menu-icon-selected { + color: #%(menuitem_hilight_text)s; +} + +#osd-menu-item, #osd-menu-item-big-icon, +#osd-launcher-item, #osd-launcher-item-selected { + border: 1px #%(menuitem_border)s solid; +} + +#osd-menu-separator { + color: #%(menuseparator)s; + font-size: large; + background-image: none; + background-color: #%(background)s; + margin: 5px 0px 0px 0px; + padding: 0px 0px 0px 0px; +} + +#osd-gesture-separator { + color: #%(menuseparator)s; + background-color: #%(menuseparator)s; + margin: 0px 5px 0px 5px; +} + +#osd-menu-item-selected, #osd-menu-item-big-icon-selected, +#osd-launcher-item-selected { + color: #%(menuitem_hilight_text)s; + background-color: #%(menuitem_hilight)s; + border: 1px #%(menuitem_hilight_border)s solid; +} + +#osd-menu-cursor, #osd-keyboard-cursor { +} + +#osd-dialog-buttons { + margin: 10px 20px 10px 20px; +} + +#osd-dialog-text { + color: #%(text)s; + font-size: large; + margin: 10px 20px 0px 20px; +} + +#osd-application-list { + margin: 15px 15px 0px 15px; +} + +#osd-key-buton, #osd-key-buton-selected { + background-color: #%(osk_button1)s; + border: 1px #%(osk_button1_border)s solid; + color: #%(osk_text)s; +} + +#osd-key-buton-hilight { + color: #%(osk_text)s; + background-color: #%(osk_hilight)s; +} + +#osd-key-buton-selected { + background-color: #%(osk_pressed)s; +} diff --git a/osd_styles/Green (dark).colors.json b/osd_styles/Green (dark).colors.json new file mode 100644 index 000000000..fe5f042f3 --- /dev/null +++ b/osd_styles/Green (dark).colors.json @@ -0,0 +1,26 @@ +{ + "####" : "This is default color scheme", + + "###" : "Colors used by OSD", + "osd_colors": { + "background": "101010", + "border": "101010", + "text": "16BF24", + "menuitem_border": "101010", + "menuitem_hilight": "202020", + "menuitem_hilight_text": "16FF26", + "menuitem_hilight_border": "16FF26", + "menuseparator": "2e3436" + }, + + "###" : "Colors used by on-screen keyboard", + "osk_colors": { + "hilight" : "7A7A7A", + "pressed" : "B0B0B0", + "button1" : "101010", + "button1_border" : "101010", + "button2" : "2e3436", + "button2_border" : "2e3436", + "text" : "16BF24" + } +} \ No newline at end of file diff --git a/osd_styles/Green.colors.json b/osd_styles/Green.colors.json new file mode 100644 index 000000000..31ae80436 --- /dev/null +++ b/osd_styles/Green.colors.json @@ -0,0 +1,26 @@ +{ + "####" : "This is default color scheme", + + "###" : "Colors used by OSD", + "osd_colors": { + "background": "160c00", + "border": "00FF00", + "text": "00FF00", + "menuitem_border": "004000", + "menuitem_hilight": "000070", + "menuitem_hilight_text": "FFFFFF", + "menuitem_hilight_border": "00FF00", + "menuseparator": "109010" + }, + + "###" : "Colors used by on-screen keyboard", + "osk_colors": { + "hilight" : "00688D", + "pressed" : "1A9485", + "button1" : "162082", + "button1_border" : "262b5e", + "button2" : "162d44", + "button2_border" : "27323e", + "text" : "ffffff" + } +} \ No newline at end of file diff --git a/osd_styles/Reloaded.gtkstyle.css b/osd_styles/Reloaded.gtkstyle.css new file mode 100644 index 000000000..bbb1ea5ed --- /dev/null +++ b/osd_styles/Reloaded.gtkstyle.css @@ -0,0 +1,115 @@ +#osd-message, #osd-menu, #osd-gesture, #osd-keyboard { + background-color: #%(background)s; + border: 6px #%(border)s solid; + opacity: 0.95; +} + +#osd-message { + border-left: 5px #%(menuitem_hilight_text)s solid; +} + +#osd-area { + background-color: #%(border)s; +} + +#osd-label { + color: #%(text)s; + border: none; + font-size: xx-large; + padding: 15px 15px 15px 15px; +} + +#osd-menu, #osd-gesture { + padding: 7px 7px 7px 7px; +} + +#osd-keyboard-container { + padding: 6px 6px 6px 6px; +} + +#osd-menu-item, #osd-menu-item-selected, #osd-menu-dummy, +#osd-menu-item-big-icon, #osd-menu-item-big-icon-selected, +#osd-key-buton, #osd-key-buton-hilight, #osd-launcher-item, +#osd-launcher-item-selected, #osd-hidden-item, +#osd-hidden-item-selected, #osd-key-buton-selected { + color: #%(text)s; + border-radius: 0; + font-size: x-large; + background-image: none; + background-color: #%(background)s; + margin: 0px 0px 2px 0px; +} + + +#osd-hidden-item, #osd-hidden-item-selected { + color: #%(background)s; + border-color: #%(background)s; +} + + +#osd-radial-menu-icon { + color: #%(text)s; +} + +#osd-radial-menu-icon-selected { + color: #%(menuitem_hilight_text)s; +} + +#osd-menu-item, #osd-menu-item-big-icon, +#osd-launcher-item, #osd-launcher-item-selected { + border: 1px #%(menuitem_border)s solid; +} + +#osd-menu-separator { + color: #%(menuseparator)s; + font-size: large; + background-image: none; + background-color: #%(background)s; + margin: 5px 0px 0px 0px; + padding: 0px 0px 0px 0px; +} + +#osd-gesture-separator { + color: #%(menuseparator)s; + background-color: #%(menuseparator)s; + margin: 0px 5px 0px 5px; +} + +#osd-menu-item-selected, #osd-menu-item-big-icon-selected, +#osd-launcher-item-selected { + color: #%(menuitem_hilight_text)s; + background-color: #%(menuitem_hilight)s; + border: 1px #%(menuitem_hilight_border)s solid; +} + +#osd-menu-cursor, #osd-keyboard-cursor { +} + +#osd-dialog-buttons { + margin: 10px 20px 10px 20px; +} + +#osd-dialog-text { + color: #%(text)s; + font-size: large; + margin: 10px 20px 0px 20px; +} + +#osd-application-list { + margin: 15px 15px 0px 15px; +} + +#osd-key-buton, #osd-key-buton-selected { + background-color: #%(osk_button1)s; + border: 1px #%(osk_button1_border)s solid; + color: #%(osk_text)s; +} + +#osd-key-buton-hilight { + color: #%(osk_text)s; + background-color: #%(osk_hilight)s; +} + +#osd-key-buton-selected { + background-color: #%(osk_pressed)s; +} diff --git a/scc/config.py b/scc/config.py index e86932dae..4a8e3b2f8 100644 --- a/scc/config.py +++ b/scc/config.py @@ -76,7 +76,8 @@ class Config(object): # (or only some) inputs. # This enables GUI to display which physical button was pressed to user. "enable_sniffing" : False, - # Colors used by OSD + # Style and colors used by OSD + "osd_style": "Classic.gtkstyle.css", "osd_colors": { "background": "101010", "border": "101010", diff --git a/scc/gui/global_settings.py b/scc/gui/global_settings.py index 4f9d44051..e6f949f49 100644 --- a/scc/gui/global_settings.py +++ b/scc/gui/global_settings.py @@ -17,6 +17,7 @@ from scc.profile import Profile, Encoder from scc.actions import Action, NoAction from scc.constants import LEFT, RIGHT +from scc.paths import get_share_path from scc.gui.osk_binding_editor import OSKBindingEditor from scc.gui.userdata_manager import UserDataManager from scc.gui.editor import Editor, ComboSetter @@ -29,7 +30,7 @@ from scc.osd.osk_actions import OSKCursorAction import scc.osd.osk_actions -import re, sys, os, logging, traceback +import re, sys, os, json, logging, traceback log = logging.getLogger("GS") class GlobalSettings(Editor, UserDataManager, ComboSetter): @@ -621,6 +622,27 @@ def on_entTitle_changed(self, ent): btSave.set_sensitive(True) + def on_cbOSDColorPreset_changed(self, cb): + filename = os.path.join(get_share_path(), "osd_styles", + cb.get_model().get_value(cb.get_active_iter(), 0)) + data = json.loads(file(filename, "r").read()) + + # Transfer values from json to config + for grp in ("osd_colors", "osk_colors"): + if grp in data: + for subkey in self.app.config[grp]: + if subkey in data[grp]: + self.app.config[grp][subkey] = data[grp][subkey] + + # Save + self.app.save_config() + + + def on_cbOSDStyle_changed(self, cb): + self.app.config["osd_style"] = cb.get_model().get_value(cb.get_active_iter(), 0) + self.app.save_config() + + @staticmethod def _make_mi_instance(index): """ Helper method used by on_cbMI_toggled and load_cbMIs """ diff --git a/scc/osd/__init__.py b/scc/osd/__init__.py index 1d74fdf1f..82a4d48ad 100644 --- a/scc/osd/__init__.py +++ b/scc/osd/__init__.py @@ -10,132 +10,16 @@ from gi.repository import Gtk, Gdk, GLib, GObject, GdkX11 from scc.constants import STICK_PAD_MIN, STICK_PAD_MAX from scc.osd.timermanager import TimerManager +from scc.paths import get_share_path from scc.lib import xwrappers as X from scc.config import Config -import argparse, traceback, logging +import os, argparse, traceback, logging log = logging.getLogger("osd") class OSDWindow(Gtk.Window): - CSS = """ - #osd-message, #osd-menu, #osd-gesture, #osd-keyboard { - background-color: #%(background)s; - border: 6px #%(border)s solid; - opacity: 0.95; - } - - #osd-message { - border-left: 5px #%(menuitem_hilight_text)s solid; - } - - #osd-area { - background-color: #%(border)s; - } - - #osd-label { - color: #%(text)s; - border: none; - font-size: xx-large; - padding: 15px 15px 15px 15px; - } - - #osd-menu, #osd-gesture { - padding: 7px 7px 7px 7px; - } - - #osd-keyboard-container { - padding: 6px 6px 6px 6px; - } - - #osd-menu-item, #osd-menu-item-selected, #osd-menu-dummy, - #osd-menu-item-big-icon, #osd-menu-item-big-icon-selected, - #osd-key-buton, #osd-key-buton-hilight, #osd-launcher-item, - #osd-launcher-item-selected, #osd-hidden-item, - #osd-hidden-item-selected, #osd-key-buton-selected { - color: #%(text)s; - border-radius: 0; - font-size: x-large; - background-image: none; - background-color: #%(background)s; - margin: 0px 0px 2px 0px; - } - - - #osd-hidden-item, #osd-hidden-item-selected { - color: #%(background)s; - border-color: #%(background)s; - } - - - #osd-radial-menu-icon { - color: #%(text)s; - } - - #osd-radial-menu-icon-selected { - color: #%(menuitem_hilight_text)s; - } - - #osd-menu-item, #osd-menu-item-big-icon, - #osd-launcher-item, #osd-launcher-item-selected { - border: 1px #%(menuitem_border)s solid; - } - - #osd-menu-separator { - color: #%(menuseparator)s; - font-size: large; - background-image: none; - background-color: #%(background)s; - margin: 5px 0px 0px 0px; - padding: 0px 0px 0px 0px; - } - - #osd-gesture-separator { - color: #%(menuseparator)s; - background-color: #%(menuseparator)s; - margin: 0px 5px 0px 5px; - } - - #osd-menu-item-selected, #osd-menu-item-big-icon-selected, - #osd-launcher-item-selected { - color: #%(menuitem_hilight_text)s; - background-color: #%(menuitem_hilight)s; - border: 1px #%(menuitem_hilight_border)s solid; - } - - #osd-menu-cursor, #osd-keyboard-cursor { - } - - #osd-dialog-buttons { - margin: 10px 20px 10px 20px; - } - - #osd-dialog-text { - color: #%(text)s; - font-size: large; - margin: 10px 20px 0px 20px; - } - - #osd-application-list { - margin: 15px 15px 0px 15px; - } - - #osd-key-buton, #osd-key-buton-selected { - background-color: #%(osk_button1)s; - border: 1px #%(osk_button1_border)s solid; - color: #%(osk_text)s; - } - - #osd-key-buton-hilight { - color: #%(osk_text)s; - background-color: #%(osk_hilight)s; - } - - #osd-key-buton-selected { - background-color: #%(osk_pressed)s; - } - - """ + # TODO: Get rid of CSS_3_20, maybe just by dropping support CSS_3_20 = """ #osd-menu-item-big-icon, #osd-menu-item-big-icon-selected { min-width: 48pt; @@ -147,8 +31,7 @@ class OSDWindow(Gtk.Window): min-width: 100px; margin: 0px 5px 0px 5px; } - - """ # oh fuck me :( + """ EPILOG = "" css_provider = None # Used by staticmethods @@ -183,13 +66,14 @@ def _apply_css(config): try: colors = {} - css = OSDWindow.CSS + css_file = os.path.join(get_share_path(), "osd_styles", config["osd_style"]) + css = file(css_file, "r").read() if ((Gtk.get_major_version(), Gtk.get_minor_version()) > (3, 20)): css += OSDWindow.CSS_3_20 for x in config['osk_colors'] : colors["osk_%s" % (x,)] = config['osk_colors'][x] for x in config['osd_colors'] : colors[x] = config['osd_colors'][x] OSDWindow.css_provider = Gtk.CssProvider() - OSDWindow.css_provider.load_from_data(str(OSDWindow.CSS % colors)) + OSDWindow.css_provider.load_from_data(str(css % colors)) Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), OSDWindow.css_provider, @@ -200,7 +84,11 @@ def _apply_css(config): log.error("Retrying with default values") OSDWindow.css_provider = Gtk.CssProvider() - OSDWindow.css_provider.load_from_data(str(OSDWindow.CSS % Config.DEFAULTS['osd_colors'])) + css_file = os.path.join(get_share_path(), "osd_styles", "Classic.gtkstyle.css") + css = file(css_file, "r").read() + if ((Gtk.get_major_version(), Gtk.get_minor_version()) > (3, 20)): + css += OSDWindow.CSS_3_20 + OSDWindow.css_provider.load_from_data(str(css % Config.DEFAULTS['osd_colors'])) Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), OSDWindow.css_provider, From 49118ba126c04420908eb99d7b51ab051d89f6c5 Mon Sep 17 00:00:00 2001 From: kozec Date: Sat, 18 Nov 2017 14:06:44 +0100 Subject: [PATCH 33/44] Working on OSD styles --- osd_styles/Green.colors.json | 4 +-- osd_styles/Reloaded.gtkstyle.css | 5 ++- scc/gui/gestures.py | 24 +++----------- scc/osd/__init__.py | 55 +++++++++++++++++++++++++++++--- 4 files changed, 61 insertions(+), 27 deletions(-) diff --git a/osd_styles/Green.colors.json b/osd_styles/Green.colors.json index 31ae80436..deb6c6976 100644 --- a/osd_styles/Green.colors.json +++ b/osd_styles/Green.colors.json @@ -5,10 +5,10 @@ "osd_colors": { "background": "160c00", "border": "00FF00", - "text": "00FF00", + "text": "16BF24", "menuitem_border": "004000", "menuitem_hilight": "000070", - "menuitem_hilight_text": "FFFFFF", + "menuitem_hilight_text": "16FF26", "menuitem_hilight_border": "00FF00", "menuseparator": "109010" }, diff --git a/osd_styles/Reloaded.gtkstyle.css b/osd_styles/Reloaded.gtkstyle.css index bbb1ea5ed..5afc256ab 100644 --- a/osd_styles/Reloaded.gtkstyle.css +++ b/osd_styles/Reloaded.gtkstyle.css @@ -1,6 +1,9 @@ +/* Used colors: background, */ + + #osd-message, #osd-menu, #osd-gesture, #osd-keyboard { background-color: #%(background)s; - border: 6px #%(border)s solid; + border: 6px #%(background+10)s solid; opacity: 0.95; } diff --git a/scc/gui/gestures.py b/scc/gui/gestures.py index 7bd993d44..a8f82526d 100644 --- a/scc/gui/gestures.py +++ b/scc/gui/gestures.py @@ -7,6 +7,7 @@ from gi.repository import Gtk, Gdk, GLib, GObject from scc.constants import STICK_PAD_MIN, STICK_PAD_MAX +from scc.osd import parse_rgba from collections import deque import math, logging @@ -27,29 +28,14 @@ def __init__(self, size, detector): self.set_colors() - @staticmethod - def parse_rgba(col): - """ Parses color specified by #RRGGBBAA string """ - # Because GTK can parse everything but theese :( - if not col.startswith("#"): - col = "#" + col - if len(col) > 7: - col, alpha = col[0:7], col[7:] - rgba = Gdk.RGBA() - if not rgba.parse(col): - log.warning("Failed to parse RGBA color: %s", col) - rgba.alpha = float(int(alpha, 16)) / 255.0 - return rgba - - def set_colors(self, background="000000FF", line="FF00FFFF", grid="7A7A7AFF", hilight="0030AAFF", **a): """ Expects colors in RRGGBB, as stored in config file """ self.colors = { - 'background' : GestureDraw.parse_rgba(background), - 'line' : GestureDraw.parse_rgba(line), - 'grid' : GestureDraw.parse_rgba(grid), - 'hilight': GestureDraw.parse_rgba(hilight), + 'background' : parse_rgba(background), + 'line' : parse_rgba(line), + 'grid' : parse_rgba(grid), + 'hilight': parse_rgba(hilight), } diff --git a/scc/osd/__init__.py b/scc/osd/__init__.py index 82a4d48ad..1c16f8726 100644 --- a/scc/osd/__init__.py +++ b/scc/osd/__init__.py @@ -64,16 +64,18 @@ def _apply_css(config): Gtk.StyleContext.remove_provider_for_screen( Gdk.Screen.get_default(), OSDWindow.css_provider) + colors = {} + for x in config['osk_colors'] : colors["osk_%s" % (x,)] = config['osk_colors'][x] + for x in config['osd_colors'] : colors[x] = config['osd_colors'][x] + colors = OSDCssMagic(colors) try: - colors = {} css_file = os.path.join(get_share_path(), "osd_styles", config["osd_style"]) css = file(css_file, "r").read() if ((Gtk.get_major_version(), Gtk.get_minor_version()) > (3, 20)): css += OSDWindow.CSS_3_20 - for x in config['osk_colors'] : colors["osk_%s" % (x,)] = config['osk_colors'][x] - for x in config['osd_colors'] : colors[x] = config['osd_colors'][x] OSDWindow.css_provider = Gtk.CssProvider() - OSDWindow.css_provider.load_from_data(str(css % colors)) + print css % colors + OSDWindow.css_provider.load_from_data((css % colors).encode("utf-8")) Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), OSDWindow.css_provider, @@ -88,7 +90,7 @@ def _apply_css(config): css = file(css_file, "r").read() if ((Gtk.get_major_version(), Gtk.get_minor_version()) > (3, 20)): css += OSDWindow.CSS_3_20 - OSDWindow.css_provider.load_from_data(str(css % Config.DEFAULTS['osd_colors'])) + OSDWindow.css_provider.load_from_data((css % colors).encode("utf-8")) Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), OSDWindow.css_provider, @@ -221,6 +223,31 @@ def quit(self, code=-1): self.destroy() +class OSDCssMagic(dict): + """ + Basically, I reinvented templating. + This is passed to string.format, allowing to use some simple expressions in + addition to normal %(placeholder)s. + + Supported magic: + %(background)s - just color + %(background+10)s - color, 10 values brighter + %(background-10)s - color, 10 values darker + """ + + def __init__(self, dict_to_wrap): + self._dict = dict_to_wrap + + + def __getitem__(self, a): + if "+" in a: + key, number = a.rsplit("+", 1) + rgba = parse_rgba(self[key]) + print rgba + return "#ff0000" + return self._dict[a] + + class StickController(GObject.GObject, TimerManager): """ Simple utility class that gets fed by with position and emits @@ -273,3 +300,21 @@ def set_stick(self, *data): if direction != self._direction: self._direction = direction self._move() + + +def parse_rgba(col): + """ + Parses color specified by #RRGGBBAA string. + '#' and 'AA' is optional. + """ + # Because GTK can parse everything but theese :( + alpha = "FF" + if not col.startswith("#"): + col = "#" + col + if len(col) > 7: + col, alpha = col[0:7], col[7:] + rgba = Gdk.RGBA() + if not rgba.parse(col): + log.warning("Failed to parse RGBA color: %s", col) + rgba.alpha = float(int(alpha, 16)) / 255.0 + return rgba \ No newline at end of file From c26aa6f65fbabfe4819e5fc9ecd5626f04a475c3 Mon Sep 17 00:00:00 2001 From: tom Date: Sat, 18 Nov 2017 16:59:26 +0100 Subject: [PATCH 34/44] Dark/reloaded green color theme can now be default. Fixes #259 --- osd_styles/Green.colors.json | 2 +- osd_styles/Reloaded.gtkstyle.css | 10 +++++----- scc/osd/__init__.py | 22 +++++++++++++++++++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/osd_styles/Green.colors.json b/osd_styles/Green.colors.json index deb6c6976..76ec7c983 100644 --- a/osd_styles/Green.colors.json +++ b/osd_styles/Green.colors.json @@ -3,7 +3,7 @@ "###" : "Colors used by OSD", "osd_colors": { - "background": "160c00", + "background": "101010", "border": "00FF00", "text": "16BF24", "menuitem_border": "004000", diff --git a/osd_styles/Reloaded.gtkstyle.css b/osd_styles/Reloaded.gtkstyle.css index 5afc256ab..965e0c3b6 100644 --- a/osd_styles/Reloaded.gtkstyle.css +++ b/osd_styles/Reloaded.gtkstyle.css @@ -3,7 +3,7 @@ #osd-message, #osd-menu, #osd-gesture, #osd-keyboard { background-color: #%(background)s; - border: 6px #%(background+10)s solid; + border: 6px #%(background+1)s solid; opacity: 0.95; } @@ -60,11 +60,11 @@ #osd-menu-item, #osd-menu-item-big-icon, #osd-launcher-item, #osd-launcher-item-selected { - border: 1px #%(menuitem_border)s solid; + border: 1px #%(background+1)s solid; } #osd-menu-separator { - color: #%(menuseparator)s; + color: #%(background+50)s; font-size: large; background-image: none; background-color: #%(background)s; @@ -80,8 +80,8 @@ #osd-menu-item-selected, #osd-menu-item-big-icon-selected, #osd-launcher-item-selected { - color: #%(menuitem_hilight_text)s; - background-color: #%(menuitem_hilight)s; + color: #%(text)s; + background-color: #%(background+15)s; border: 1px #%(menuitem_hilight_border)s solid; } diff --git a/scc/osd/__init__.py b/scc/osd/__init__.py index 1c16f8726..0dd617695 100644 --- a/scc/osd/__init__.py +++ b/scc/osd/__init__.py @@ -74,7 +74,6 @@ def _apply_css(config): if ((Gtk.get_major_version(), Gtk.get_minor_version()) > (3, 20)): css += OSDWindow.CSS_3_20 OSDWindow.css_provider = Gtk.CssProvider() - print css % colors OSDWindow.css_provider.load_from_data((css % colors).encode("utf-8")) Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), @@ -243,8 +242,25 @@ def __getitem__(self, a): if "+" in a: key, number = a.rsplit("+", 1) rgba = parse_rgba(self[key]) - print rgba - return "#ff0000" + number = float(number) / 255.0 + rgba.red = min(1.0, rgba.red + number) + rgba.green = min(1.0, rgba.green + number) + rgba.blue = min(1.0, rgba.blue + number) + return "%s%s%s" % ( + hex(int(rgba.red * 255)).split("x")[-1].zfill(2), + hex(int(rgba.green * 255)).split("x")[-1].zfill(2), + hex(int(rgba.blue * 255)).split("x")[-1].zfill(2)) + elif "-" in a: + key, number = a.rsplit("-", 1) + rgba = parse_rgba(self[key]) + number = float(number) / 255.0 + rgba.red = max(0.0, rgba.red - number) + rgba.green = max(0.0, rgba.green - number) + rgba.blue = max(0.0, rgba.blue - number) + return "%s%s%s" % ( + hex(int(rgba.red * 255)).split("x")[-1].zfill(2), + hex(int(rgba.green * 255)).split("x")[-1].zfill(2), + hex(int(rgba.blue * 255)).split("x")[-1].zfill(2)) return self._dict[a] From 4b1e1b5bece745082f7109ab7336c8736146c4b7 Mon Sep 17 00:00:00 2001 From: tom Date: Sat, 18 Nov 2017 17:59:29 +0100 Subject: [PATCH 35/44] Disabling color buttons not used in current theme --- glade/global_settings.glade | 28 ++++++++++++++-------------- osd_styles/Classic.gtkstyle.css | 1 + osd_styles/Reloaded.gtkstyle.css | 5 ++--- scc/gui/global_settings.py | 17 ++++++++++++++++- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/glade/global_settings.glade b/glade/global_settings.glade index 5c767b433..82431da7f 100644 --- a/glade/global_settings.glade +++ b/glade/global_settings.glade @@ -1,5 +1,5 @@ - + @@ -869,7 +869,7 @@ or even crash. - + True False 20 @@ -884,7 +884,7 @@ or even crash. - + True False 20 @@ -899,7 +899,7 @@ or even crash. - + True False 20 @@ -914,7 +914,7 @@ or even crash. - + True False 20 @@ -1009,7 +1009,7 @@ or even crash. - + True False 20 @@ -1024,7 +1024,7 @@ or even crash. - + True False 20 @@ -1039,7 +1039,7 @@ or even crash. - + True False 20 @@ -1054,7 +1054,7 @@ or even crash. - + True False 20 @@ -1086,7 +1086,7 @@ or even crash. - + True False 20 @@ -1117,7 +1117,7 @@ or even crash. - + True False 20 @@ -1149,7 +1149,7 @@ or even crash. - + True False 20 @@ -1181,7 +1181,7 @@ or even crash. - + 20 True False @@ -1214,7 +1214,7 @@ or even crash. - + True False 20 diff --git a/osd_styles/Classic.gtkstyle.css b/osd_styles/Classic.gtkstyle.css index a2a7a073e..ef1ba75fb 100644 --- a/osd_styles/Classic.gtkstyle.css +++ b/osd_styles/Classic.gtkstyle.css @@ -1,3 +1,4 @@ +/* Used colors: all */ #osd-message, #osd-menu, #osd-gesture, #osd-keyboard { background-color: #%(background)s; diff --git a/osd_styles/Reloaded.gtkstyle.css b/osd_styles/Reloaded.gtkstyle.css index 965e0c3b6..31a8960ab 100644 --- a/osd_styles/Reloaded.gtkstyle.css +++ b/osd_styles/Reloaded.gtkstyle.css @@ -1,5 +1,4 @@ -/* Used colors: background, */ - +/* Used colors: background menuitem_hilight_text text menuseparator osk_text osk_button1 osk_button1_border osk_hilight osk_pressed */ #osd-message, #osd-menu, #osd-gesture, #osd-keyboard { background-color: #%(background)s; @@ -12,7 +11,7 @@ } #osd-area { - background-color: #%(border)s; + background-color: #%(background+1)s; } #osd-label { diff --git a/scc/gui/global_settings.py b/scc/gui/global_settings.py index e6f949f49..7680eb36a 100644 --- a/scc/gui/global_settings.py +++ b/scc/gui/global_settings.py @@ -639,7 +639,22 @@ def on_cbOSDColorPreset_changed(self, cb): def on_cbOSDStyle_changed(self, cb): - self.app.config["osd_style"] = cb.get_model().get_value(cb.get_active_iter(), 0) + color_keys = self.app.config['osk_colors'].keys() + self.app.config['osd_colors'].keys() + osd_style = cb.get_model().get_value(cb.get_active_iter(), 0) + css_file = os.path.join(get_share_path(), "osd_styles", osd_style) + first_line = file(css_file, "r").read().split("\n")[0] + used_colors = None # None means "all" + if "Used colors:" in first_line: + used_colors = set(first_line.split(":", 1)[1].strip(" */").split(" ")) + if "all" in used_colors: + used_colors = None # None means "all" + + for key in color_keys: + cb = self.builder.get_object("cb%s" % (key, )) + lbl = self.builder.get_object("lbl%s" % (key, )) + if cb: cb.set_sensitive ((used_colors is None) or (key in used_colors)) + if lbl: lbl.set_sensitive((used_colors is None) or (key in used_colors)) + self.app.config["osd_style"] = osd_style self.app.save_config() From a09ceb185339e1ecf2dabba13477970fc3ff2281 Mon Sep 17 00:00:00 2001 From: tom Date: Sat, 18 Nov 2017 18:27:42 +0100 Subject: [PATCH 36/44] Colors, oh, glorious colors --- glade/global_settings.glade | 16 ++++++++++++++-- osd_styles/Blue.colors.json | 24 ++++++++++++++++++++++++ osd_styles/Cyan.colors.json | 24 ++++++++++++++++++++++++ osd_styles/Green (dark).colors.json | 26 -------------------------- osd_styles/Red.colors.json | 24 ++++++++++++++++++++++++ osd_styles/Yellow.colors.json | 24 ++++++++++++++++++++++++ 6 files changed, 110 insertions(+), 28 deletions(-) create mode 100644 osd_styles/Blue.colors.json create mode 100644 osd_styles/Cyan.colors.json delete mode 100644 osd_styles/Green (dark).colors.json create mode 100644 osd_styles/Red.colors.json create mode 100644 osd_styles/Yellow.colors.json diff --git a/glade/global_settings.glade b/glade/global_settings.glade index 82431da7f..7ef16c463 100644 --- a/glade/global_settings.glade +++ b/glade/global_settings.glade @@ -56,8 +56,20 @@ Green - Green (dark).colors.json - Green (dark) + Blue.colors.json + Blue + + + Cyan.colors.json + Cyan + + + Yellow.colors.json + Yellow + + + Red.colors.json + Red diff --git a/osd_styles/Blue.colors.json b/osd_styles/Blue.colors.json new file mode 100644 index 000000000..8dcf34ca1 --- /dev/null +++ b/osd_styles/Blue.colors.json @@ -0,0 +1,24 @@ +{ + "###" : "Colors used by OSD", + "osd_colors": { + "background": "101010", + "border": "0000DF", + "text": "4060FF", + "menuitem_border": "000040", + "menuitem_hilight": "000050", + "menuitem_hilight_text": "FFFFFF", + "menuitem_hilight_border": "0000FF", + "menuseparator": "505090" + }, + + "###" : "Colors used by on-screen keyboard", + "osk_colors": { + "hilight" : "00688D", + "pressed" : "1A9485", + "button1" : "162082", + "button1_border" : "262b5e", + "button2" : "162d44", + "button2_border" : "27323e", + "text" : "ffffff" + } +} \ No newline at end of file diff --git a/osd_styles/Cyan.colors.json b/osd_styles/Cyan.colors.json new file mode 100644 index 000000000..40b2844a9 --- /dev/null +++ b/osd_styles/Cyan.colors.json @@ -0,0 +1,24 @@ +{ + "###" : "Colors used by OSD", + "osd_colors": { + "background": "101010", + "border": "00DFDF", + "text": "40F0FF", + "menuitem_border": "004040", + "menuitem_hilight": "005050", + "menuitem_hilight_text": "FFFFFF", + "menuitem_hilight_border": "00FFFF", + "menuseparator": "509090" + }, + + "###" : "Colors used by on-screen keyboard", + "osk_colors": { + "hilight" : "00688D", + "pressed" : "1A9485", + "button1" : "162082", + "button1_border" : "262b5e", + "button2" : "162d44", + "button2_border" : "27323e", + "text" : "ffffff" + } +} \ No newline at end of file diff --git a/osd_styles/Green (dark).colors.json b/osd_styles/Green (dark).colors.json deleted file mode 100644 index fe5f042f3..000000000 --- a/osd_styles/Green (dark).colors.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "####" : "This is default color scheme", - - "###" : "Colors used by OSD", - "osd_colors": { - "background": "101010", - "border": "101010", - "text": "16BF24", - "menuitem_border": "101010", - "menuitem_hilight": "202020", - "menuitem_hilight_text": "16FF26", - "menuitem_hilight_border": "16FF26", - "menuseparator": "2e3436" - }, - - "###" : "Colors used by on-screen keyboard", - "osk_colors": { - "hilight" : "7A7A7A", - "pressed" : "B0B0B0", - "button1" : "101010", - "button1_border" : "101010", - "button2" : "2e3436", - "button2_border" : "2e3436", - "text" : "16BF24" - } -} \ No newline at end of file diff --git a/osd_styles/Red.colors.json b/osd_styles/Red.colors.json new file mode 100644 index 000000000..42b8c1287 --- /dev/null +++ b/osd_styles/Red.colors.json @@ -0,0 +1,24 @@ +{ + "###" : "Colors used by OSD", + "osd_colors": { + "background": "101010", + "border": "DF0000", + "text": "FF2020", + "menuitem_border": "400000", + "menuitem_hilight": "600000", + "menuitem_hilight_text": "FFFFFF", + "menuitem_hilight_border": "FFFFFF", + "menuseparator": "905050" + }, + + "###" : "Colors used by on-screen keyboard", + "osk_colors": { + "hilight" : "00688D", + "pressed" : "1A9485", + "button1" : "162082", + "button1_border" : "262b5e", + "button2" : "162d44", + "button2_border" : "27323e", + "text" : "ffffff" + } +} \ No newline at end of file diff --git a/osd_styles/Yellow.colors.json b/osd_styles/Yellow.colors.json new file mode 100644 index 000000000..e9128b6e2 --- /dev/null +++ b/osd_styles/Yellow.colors.json @@ -0,0 +1,24 @@ +{ + "###" : "Colors used by OSD", + "osd_colors": { + "background": "101010", + "border": "FFFF00", + "text": "BFBF24", + "menuitem_border": "404000", + "menuitem_hilight": "FFFF00", + "menuitem_hilight_text": "000000", + "menuitem_hilight_border": "FFFF00", + "menuseparator": "A5A5A5" + }, + + "###" : "Colors used by on-screen keyboard", + "osk_colors": { + "hilight" : "00688D", + "pressed" : "1A9485", + "button1" : "162082", + "button1_border" : "262b5e", + "button2" : "162d44", + "button2_border" : "27323e", + "text" : "ffffff" + } +} \ No newline at end of file From 34d39294992e84c80b33c5c15048ece41c714f1a Mon Sep 17 00:00:00 2001 From: kozec Date: Sat, 18 Nov 2017 19:16:30 +0100 Subject: [PATCH 37/44] Allowed ChangeProfileAction and ShellCommandAction to be used as whole pad binding. Ref #287 --- scc/special_actions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scc/special_actions.py b/scc/special_actions.py index 1662d3500..a1820b828 100644 --- a/scc/special_actions.py +++ b/scc/special_actions.py @@ -56,6 +56,10 @@ def button_release(self, mapper): # is pressed would send following button_release event to another # action from loaded profile) self.execute(mapper) + + + def whole(self, mapper, *a): + self.execute(mapper) class ShellCommandAction(Action, SpecialAction): @@ -110,6 +114,10 @@ def button_release(self, mapper): # is pressed would hold stuck any other action bound to same button, # as button_release is not sent after controller turns off) self.execute(mapper) + + + def whole(self, mapper, *a): + self.execute(mapper) class RestartDaemonAction(Action, SpecialAction): From d4708c41986e7729ba32cf15237321c4e83e3d21 Mon Sep 17 00:00:00 2001 From: tom Date: Sun, 19 Nov 2017 16:37:49 +0100 Subject: [PATCH 38/44] Final theming touches --- glade/global_settings.glade | 4 ++++ osd_styles/Red.colors.json | 2 +- osd_styles/Reloaded.gtkstyle.css | 2 +- scc/config.py | 4 ++-- scc/gui/global_settings.py | 14 ++++++++++++-- scc/x11/scc-osd-daemon.py | 1 + 6 files changed, 21 insertions(+), 6 deletions(-) diff --git a/glade/global_settings.glade b/glade/global_settings.glade index 7ef16c463..cf2b8c8b3 100644 --- a/glade/global_settings.glade +++ b/glade/global_settings.glade @@ -51,6 +51,10 @@ + + None + None / Custom + Green.colors.json Green diff --git a/osd_styles/Red.colors.json b/osd_styles/Red.colors.json index 42b8c1287..4334f8541 100644 --- a/osd_styles/Red.colors.json +++ b/osd_styles/Red.colors.json @@ -7,7 +7,7 @@ "menuitem_border": "400000", "menuitem_hilight": "600000", "menuitem_hilight_text": "FFFFFF", - "menuitem_hilight_border": "FFFFFF", + "menuitem_hilight_border": "FF0000", "menuseparator": "905050" }, diff --git a/osd_styles/Reloaded.gtkstyle.css b/osd_styles/Reloaded.gtkstyle.css index 31a8960ab..2dc3a6645 100644 --- a/osd_styles/Reloaded.gtkstyle.css +++ b/osd_styles/Reloaded.gtkstyle.css @@ -1,4 +1,4 @@ -/* Used colors: background menuitem_hilight_text text menuseparator osk_text osk_button1 osk_button1_border osk_hilight osk_pressed */ +/* Used colors: background text menuseparator osk_text osk_button1 osk_button1_border osk_hilight osk_pressed */ #osd-message, #osd-menu, #osd-gesture, #osd-keyboard { background-color: #%(background)s; diff --git a/scc/config.py b/scc/config.py index 4a8e3b2f8..229cc90d7 100644 --- a/scc/config.py +++ b/scc/config.py @@ -224,8 +224,8 @@ def __iter__(self): for k in self.values: yield k - def get(self, key): - return self.values[key] + def get(self, key, default=None): + return self.values.get(key, default) def set(self, key, value): self.values[key] = value diff --git a/scc/gui/global_settings.py b/scc/gui/global_settings.py index 7680eb36a..1597f1342 100644 --- a/scc/gui/global_settings.py +++ b/scc/gui/global_settings.py @@ -151,12 +151,17 @@ def _load_color(self, w, dct, key): def load_colors(self): + cbOSDStyle = self.builder.get_object("cbOSDStyle") + cbOSDColorPreset = self.builder.get_object("cbOSDColorPreset") for k in self.app.config["osd_colors"]: w = self.builder.get_object("cb%s" % (k,)) self._load_color(w, "osd_colors", k) for k in self.app.config["osk_colors"]: w = self.builder.get_object("cbosk_%s" % (k,)) self._load_color(w, "osk_colors", k) + theme = self.app.config.get("osd_color_theme", "None") + self.set_cb(cbOSDColorPreset, theme) + self.set_cb(cbOSDStyle, self.app.config.get("osd_style")) def load_autoswitch(self): @@ -260,6 +265,7 @@ def on_osd_color_set(self, *a): # Following lambdas converts Gdk.Color into #rrggbb notation. # Gdk.Color can do similar, except it uses #rrrrggggbbbb notation that # is not understood by Gdk css parser.... + cbOSDColorPreset = self.builder.get_object("cbOSDColorPreset") striphex = lambda a: hex(a).strip("0x").zfill(2) tohex = lambda a: "".join([ striphex(int(x * 0xFF)) for x in a.to_floats() ]) for k in self.app.config["osd_colors"]: @@ -270,6 +276,8 @@ def on_osd_color_set(self, *a): w = self.builder.get_object("cbosk_%s" % (k,)) if w: self.app.config["osk_colors"][k] = tohex(w.get_color()) + self.app.config["osd_color_theme"] = None + self.set_cb(cbOSDColorPreset, "None") self.app.save_config() @@ -623,8 +631,9 @@ def on_entTitle_changed(self, ent): def on_cbOSDColorPreset_changed(self, cb): - filename = os.path.join(get_share_path(), "osd_styles", - cb.get_model().get_value(cb.get_active_iter(), 0)) + theme = cb.get_model().get_value(cb.get_active_iter(), 0) + if theme in (None, "None"): return + filename = os.path.join(get_share_path(), "osd_styles", theme) data = json.loads(file(filename, "r").read()) # Transfer values from json to config @@ -635,6 +644,7 @@ def on_cbOSDColorPreset_changed(self, cb): self.app.config[grp][subkey] = data[grp][subkey] # Save + self.app.config["osd_color_theme"] = theme self.app.save_config() diff --git a/scc/x11/scc-osd-daemon.py b/scc/x11/scc-osd-daemon.py index dad64216b..3b597018a 100755 --- a/scc/x11/scc-osd-daemon.py +++ b/scc/x11/scc-osd-daemon.py @@ -236,6 +236,7 @@ def _check_colorconfig_change(self): """ h = sum([ hash(self.config['osd_colors'][x]) for x in self.config['osd_colors'] ]) h += sum([ hash(self.config['osk_colors'][x]) for x in self.config['osk_colors'] ]) + h += hash(self.config['osd_style']) if self._hash_of_colors != h: self._hash_of_colors = h OSDWindow._apply_css(self.config) From 1e3a72dddfad8fda41b6d580764294ee3e8a439d Mon Sep 17 00:00:00 2001 From: tom Date: Sun, 19 Nov 2017 16:51:40 +0100 Subject: [PATCH 39/44] Fix: Removed run_scheduled from slave mapper --- scc/gui/app.py | 1 - scc/osd/slave_mapper.py | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/scc/gui/app.py b/scc/gui/app.py index 637b8bfbe..4afb036d9 100644 --- a/scc/gui/app.py +++ b/scc/gui/app.py @@ -926,7 +926,6 @@ def on_lock_success(*a): 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 diff --git a/scc/osd/slave_mapper.py b/scc/osd/slave_mapper.py index 7ed4d0f4f..5d97e973b 100644 --- a/scc/osd/slave_mapper.py +++ b/scc/osd/slave_mapper.py @@ -52,17 +52,6 @@ def send_feedback(self, hapticdata): self._feedback_cb(hapticdata) - def run_scheduled(self): - """ - Should be called periodically to keep timers going. - Since SlaveMapper doesn't communicate with controller device, it is not - possible to drive this automatically - """ - now = time.time() - Mapper.run_scheduled(self, now) - return True - - def handle_event(self, daemon, what, data): """ Handles event sent by scc-daemon. From 3f2233ee66189e62b2005817ce31c32d8b4b2e7c Mon Sep 17 00:00:00 2001 From: tom Date: Sat, 25 Nov 2017 16:48:06 +0100 Subject: [PATCH 40/44] Ported WIP OSD mode to v0.4. Ref #250 --- scc/gui/osd_mode_mapper.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scc/gui/osd_mode_mapper.py b/scc/gui/osd_mode_mapper.py index b817a6e5f..4eb14ac51 100644 --- a/scc/gui/osd_mode_mapper.py +++ b/scc/gui/osd_mode_mapper.py @@ -21,10 +21,8 @@ class OSDModeMapper(SlaveMapper): - def __init__(self, profile, keyboard="osd", mouse="osd"): - # 'keyboard' and 'mouse' strings are _not_ passed to UInput - # nor visible anywhere else when this class is used - SlaveMapper.__init__(self, profile, keyboard, mouse) + def __init__(self, profile): + SlaveMapper.__init__(self, profile, None, keyboard="osd", mouse="osd") self.target_window = None From a79fbab9aa99227c38746a25737bdcda6f9b10e4 Mon Sep 17 00:00:00 2001 From: tom Date: Sun, 26 Nov 2017 16:36:50 +0100 Subject: [PATCH 41/44] OSD: Moving menu window when it can't fit the screen --- scc/osd/menu.py | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/scc/osd/menu.py b/scc/osd/menu.py index b4363b68b..886d330c7 100644 --- a/scc/osd/menu.py +++ b/scc/osd/menu.py @@ -40,6 +40,7 @@ class Menu(OSDWindow): SUBMENU_OFFSET = 50 PREFER_BW_ICONS = True + def __init__(self, cls="osd-menu"): OSDWindow.__init__(self, cls) self.daemon = None @@ -157,6 +158,20 @@ def _add_arguments(self): help="Menu items") + @staticmethod + def _get_on_screen_position(w): + a = w.get_allocation() + parent = w.get_parent() + if parent: + if isinstance(parent, Menu) and parent.get_window() is not None: + x, y = parent.get_window().get_position() + else: + x, y = Menu._get_on_screen_position(parent) + return a.x + x, a.y + y + else: + return a.x, a.y + + def parse_menu(self): if self.args.from_profile: try: @@ -312,10 +327,26 @@ def select(self, index): self._selected = self.items[index] self._selected.widget.set_name( self._selected.widget.get_name() + "-selected") + GLib.timeout_add(2, self._check_on_screen_position) return True return False + def _check_on_screen_position(self): + x, y = Menu._get_on_screen_position(self._selected.widget) + screen_height = self.get_window().get_screen().get_height() + if y < 50: + wx, wy = self.get_window().get_position() + wy += 5 + self.get_window().move(wx, wy) + GLib.timeout_add(2, self._check_on_screen_position) + if y > screen_height - 100: + wx, wy = self.get_window().get_position() + wy -= 5 + self.get_window().move(wx, wy) + GLib.timeout_add(2, self._check_on_screen_position) + + def _connect_handlers(self): self._eh_ids += [ (self.daemon, self.daemon.connect('dead', self.on_daemon_died)), @@ -410,7 +441,8 @@ def on_submenu_closed(self, *a): log.info("opacity set to 0.95") self.set_opacity(0.95) - + + def show_submenu(self, trash, trash2, trash3, menuitem): """ Called when user chooses menu item pointing to submenu """ filename = find_menu(menuitem.filename) @@ -437,7 +469,7 @@ def show_submenu(self, trash, trash2, trash3, menuitem): log.info("opacity set to 0.35") self.set_opacity(0.35) - + def _control_equals_cancel(self, daemon, x, y): """ @@ -499,8 +531,8 @@ def on_event(self, daemon, what, data): self.quit(0) else: self.quit(-1) - - + + class MenuIcon(Gtk.DrawingArea): """ Auti-sized, auto-recolored icon for menus """ From 4758834443c1c0e381448eac8c32eaf73861c3b8 Mon Sep 17 00:00:00 2001 From: tom Date: Sun, 26 Nov 2017 16:54:09 +0100 Subject: [PATCH 42/44] CSS opacity for OSD menus --- osd_styles/Classic.gtkstyle.css | 7 ++++++- osd_styles/Reloaded.gtkstyle.css | 6 +++++- scc/osd/menu.py | 8 ++------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/osd_styles/Classic.gtkstyle.css b/osd_styles/Classic.gtkstyle.css index ef1ba75fb..6e94fa54c 100644 --- a/osd_styles/Classic.gtkstyle.css +++ b/osd_styles/Classic.gtkstyle.css @@ -1,8 +1,13 @@ /* Used colors: all */ -#osd-message, #osd-menu, #osd-gesture, #osd-keyboard { +#osd-message, #osd-menu, #osd-menu-inactive, #osd-gesture, #osd-keyboard { background-color: #%(background)s; border: 6px #%(border)s double; + opacity: 1; +} + +#osd-menu-inactive { + opacity: 0.90; } #osd-area { diff --git a/osd_styles/Reloaded.gtkstyle.css b/osd_styles/Reloaded.gtkstyle.css index 2dc3a6645..1781f36c5 100644 --- a/osd_styles/Reloaded.gtkstyle.css +++ b/osd_styles/Reloaded.gtkstyle.css @@ -1,11 +1,15 @@ /* Used colors: background text menuseparator osk_text osk_button1 osk_button1_border osk_hilight osk_pressed */ -#osd-message, #osd-menu, #osd-gesture, #osd-keyboard { +#osd-message, #osd-menu, #osd-menu-inactive, #osd-gesture, #osd-keyboard { background-color: #%(background)s; border: 6px #%(background+1)s solid; opacity: 0.95; } +#osd-menu-inactive { + opacity: 0.50; +} + #osd-message { border-left: 5px #%(menuitem_hilight_text)s solid; } diff --git a/scc/osd/menu.py b/scc/osd/menu.py index 886d330c7..50f8fac3c 100644 --- a/scc/osd/menu.py +++ b/scc/osd/menu.py @@ -433,14 +433,12 @@ def next_item(self, direction): def on_submenu_closed(self, *a): + self.set_name("osd-menu") if self._submenu.get_exit_code() in (0, -2): self._menuid = self._submenu._menuid self._selected = self._submenu._selected self.quit(self._submenu.get_exit_code()) self._submenu = None - - log.info("opacity set to 0.95") - self.set_opacity(0.95) def show_submenu(self, trash, trash2, trash3, menuitem): @@ -466,9 +464,7 @@ def show_submenu(self, trash, trash2, trash3, menuitem): self._submenu.controller = self.controller self._submenu.connect('destroy', self.on_submenu_closed) self._submenu.show() - - log.info("opacity set to 0.35") - self.set_opacity(0.35) + self.set_name("osd-menu-inactive") def _control_equals_cancel(self, daemon, x, y): From 301a87297aef70f759690ae705ee8abf81ca8a05 Mon Sep 17 00:00:00 2001 From: kozec Date: Sun, 26 Nov 2017 11:36:58 +0100 Subject: [PATCH 43/44] Fix: How did I managed to break SC support? --- scc/gui/controller_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scc/gui/controller_image.py b/scc/gui/controller_image.py index 099cea7fb..949680561 100644 --- a/scc/gui/controller_image.py +++ b/scc/gui/controller_image.py @@ -86,7 +86,7 @@ def use_config(self, config): Loads controller settings from provided config, adding default values when needed. Returns same config. """ - self.current = self._ensure_config(config) + self.current = self._ensure_config(config or {}) self.set_image(os.path.join(self.app.imagepath, "controller-images/%s.svg" % (self.current["gui"]["background"], ))) self._fill_button_images(self.current["gui"]["buttons"]) From 70f82f07763d197f3e0833e8cfc93531fafcd938 Mon Sep 17 00:00:00 2001 From: kozec Date: Sun, 26 Nov 2017 11:46:37 +0100 Subject: [PATCH 44/44] Fixed rollmodifier acting up with new scheduler --- scc/drivers/sc_by_cable.py | 2 +- scc/modifiers.py | 24 ++++++++++++------------ scc/scheduler.py | 7 +++++++ 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/scc/drivers/sc_by_cable.py b/scc/drivers/sc_by_cable.py index 7cb9d45aa..551e69bd0 100644 --- a/scc/drivers/sc_by_cable.py +++ b/scc/drivers/sc_by_cable.py @@ -57,7 +57,7 @@ def __repr__(self): def on_serial_got(self): log.debug("Got wired SC with serial %s", self._serial) - self.set_id("sc%s" % (self._serial,), True) + self._id = "sc%s" % (self._serial,) self.set_input_interrupt(ENDPOINT, 64, self._wait_input) diff --git a/scc/modifiers.py b/scc/modifiers.py index b5d3ccc2e..f4ecdd194 100644 --- a/scc/modifiers.py +++ b/scc/modifiers.py @@ -308,6 +308,7 @@ def _mod_init(self, friction=DEFAULT_FRICTION, mass=80.0, self._degree = degree self._radscale = (degree * PI / 180) / ampli self._mass = mass + self._roll_task = None self._r = r self._I = (2 * self._mass * self._r**2) / 5.0 self._a = self._r * self.friction / self._I @@ -335,14 +336,12 @@ def _stop(self): """ Stops rolling of the 'ball' """ self._xvel_dq.clear() self._yvel_dq.clear() + if self._roll_task: + self._roll_task.cancel() + self._roll_task = None def _add(self, dx, dy): - # Compute time step - _tmp = time.time() - dt = _tmp - self._lastTime - self._lastTime = _tmp - # Compute instant velocity try: self._xvel = sum(self._xvel_dq) / len(self._xvel_dq) @@ -351,15 +350,14 @@ def _add(self, dx, dy): self._xvel = 0.0 self._yvel = 0.0 - self._xvel_dq.append(dx * self._radscale / dt) - self._yvel_dq.append(dy * self._radscale / dt) + self._xvel_dq.append(dx * self._radscale) + self._yvel_dq.append(dy * self._radscale) def _roll(self, mapper): # Compute time step - _tmp = time.time() - dt = _tmp - self._lastTime - self._lastTime = _tmp + t = time.time() + dt, self._lastTime = t - self._lastTime, t # Free movement update velocity and compute movement self._xvel_dq.clear() @@ -392,7 +390,7 @@ def _roll(self, mapper): if dx or dy: if self.haptic: WholeHapticAction.add(self, mapper, dx, dy) - mapper.schedule(0.02, self._roll) + self._roll_task = mapper.schedule(0.02, self._roll) def encode(self): @@ -446,8 +444,10 @@ def whole(self, mapper, x, y, what): return self.action.whole(mapper, x, y, what) if mapper.is_touched(what): if self._old_pos and mapper.was_touched(what): + t = time.time() + dt, self._lastTime = t - self._lastTime, t dx, dy = x - self._old_pos[0], self._old_pos[1] - y - self._add(dx, dy) + self._add(dx / dt, dy / dt) self.action.add(mapper, dx * self.speed[0], dy * self.speed[1]) else: self._stop() diff --git a/scc/scheduler.py b/scc/scheduler.py index f4984c2c5..ef5f53b51 100644 --- a/scc/scheduler.py +++ b/scc/scheduler.py @@ -78,3 +78,10 @@ def __init__(self, time, callback, data): self.time = time self.callback = callback self.data = data + + + def cancel(self): + """ Marks task as canceled, without actually removing it from scheduler """ + self.callback = lambda *a, **b: False + self.data = () +