diff --git a/.gitignore b/.gitignore index 3b211fa3e..29d652fd7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,10 @@ *.so *.patch !scc/*tester* -*test* +test*.py build/ .atomignore commit* profiles +.idea/ +out.svg diff --git a/README.md b/README.md index 0f883495d..a660dd4e9 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Based on [Standalone Steam Controller Driver](https://github.com/ynsta/steamcont ##### Packages - Ubuntu (deb-based distros): in [OpenSUSE Build Service](http://software.opensuse.org/download.html?project=home%3Akozec&package=sc-controller). - Fedora, SUSE (rpm-based distros): in [OpenSUSE Build Service](http://software.opensuse.org/download.html?project=home%3Akozec&package=sc-controller). +- Arch, Manjaro: in [AUR](https://aur.archlinux.org/packages/sc-controller-git/) - Solus: search for `sc-controller` in Software Center - Exherbo: in [hardware](https://git.exherbo.org/summer/packages/input/sc-controller) - Voidlinux: [template available](https://github.com/Vintodrimmer/void-packages/blob/sc-controller-branch/srcpkgs/sc-controller/template) 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" }] diff --git a/glade/app.glade b/glade/app.glade index db6a2dd82..0c12c8320 100644 --- a/glade/app.glade +++ b/glade/app.glade @@ -246,15 +246,6 @@ - - - True - False - _Register New Controller - True - - - True @@ -609,27 +600,42 @@ True False + + 100 + 80 True False + vertical - + + 150 True - False + True + True + 20 + 20 + + + - True + False True 0 - - 170 + + 40 True True True + 5 + + + False @@ -637,22 +643,7 @@ 1 - - - True - False - - - True - True - 2 - - - - 10 - 150 - diff --git a/glade/global_settings.glade b/glade/global_settings.glade index a7b109ffc..cf2b8c8b3 100644 --- a/glade/global_settings.glade +++ b/glade/global_settings.glade @@ -1,5 +1,5 @@ - + @@ -23,6 +23,16 @@ 1 0.01 + + + + + + + + + + @@ -33,6 +43,58 @@ + + + + + + + + + + None + None / Custom + + + Green.colors.json + Green + + + Blue.colors.json + Blue + + + Cyan.colors.json + Cyan + + + Yellow.colors.json + Yellow + + + Red.colors.json + Red + + + + + + + + + + + + + Classic.gtkstyle.css + Classic + + + Reloaded.gtkstyle.css + Reloaded + + + @@ -108,1618 +170,2051 @@ - + 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 + On-Screen Keyboard Bindings + 0 + + + + + + 0 + 7 + 6 + - - - 1 - 1 - 5 - - - - - True - False - 20 - 5 - True - lstTriggersAction - - - - 0 - + + True + False + 20 + 10 + 5 + Stick + 0 + + + 0 + 8 + - - - 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 + 9 + + + + + Advanced... + True + True + True + 50 + 50 + 15 + True + + + + 0 + 10 + 6 + - - - 5 - 8 - - - - - True - True - True - 10 - 20 - 5 - - + True False - gtk-clear + 20 + 5 + True + lstStickAction + + + + + 0 + + + + 1 + 8 + 5 + + + + + True + False + 20 + 5 + True + lstTriggersAction + + + + + 0 + + + + + 1 + 9 + 5 + + + + + True + False + 20 + Pad Sensitivity + 0 + + + + + + 0 + 11 + 6 + + + + + True + False + 20 + 10 + 5 + Horizontal + 0 + + + 0 + 12 + 2 + + + + + True + False + 20 + 10 + 5 + Vertical + 0 + + + 0 + 13 + 2 + + + + + True + True + 5 + True + adjSensitivityX + 1 + right + + + + 2 + 12 + 3 + + + + + True + True + 5 + True + adjSensitivityY + 1 + right + + + + 2 + 13 + 3 + + + + + True + True + True + 10 + 20 + 5 + + + + True + False + gtk-clear + + + + + 5 + 12 + + + + + True + True + True + 10 + 20 + 5 + + + + True + False + gtk-clear + + + + + 5 + 13 + + + + + True + False + 5 + Default Menu Items + 0 + + + + + + 0 + 0 + 6 + + + + + cbMI_0 + True + True + False + 20 + 10 + 5 + True + True + + + + True + False + List of recent profiles + 0 + + + + + 0 + 1 + 3 + + + + + cbMI_6 + True + True + False + 5 + 20 + 5 + True + True + + + + True + False + Run Program... + 0 + + + + + 3 + 1 + 3 + + + + + cbMI_1 + True + True + False + 20 + 5 + 5 + True + + + + True + False + Autoswitch options + 0 + + + + + 0 + 2 + 3 + + + + + cbMI_5 + True + True + False + 5 + 20 + 5 + True + + + + True + False + Kill Current Window + 0 + + + + + 3 + 2 + 3 + + + + + cbMI_2 + True + True + False + 20 + 5 + 5 + True + + + + True + False + Window switcher + 0 + + + + + 0 + 3 + 3 + + + + + cbMI_7 + True + True + False + 5 + 20 + 5 + True + + + + True + False + Show Current Bindings + 0 + + + + + 3 + 3 + 3 + + + + + cbMI_8 + True + True + False + 20 + 5 + 5 + True + + + + True + False + Games List + 0 + + + + + 0 + 4 + 3 + + + + + cbMI_4 + True + True + False + 5 + 20 + 5 + True + + + + True + False + Turn Controller OFF + 0 + + + + + 3 + 4 + 3 + + + + + cbMI_3 + True + True + False + 20 + 5 + 5 + True + + + + True + False + Display Keyboard + 0 + + + + + 0 + 5 + 3 + + + + + True + False + + + 0 + 6 + 3 + + + + + Customize... + True + True + True + 5 + 20 + 10 + + + 3 + 5 + 3 + 2 + - - 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 - 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 - + Menus & Keyboard - 1 - 8 + True + False - + True False - 20 - 5 - 5 - Border - 0 - - - 0 - 9 - - - - - True - True - True - 20 + 10 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 + True + True + 20 + 10 + 5 + True + + + + 1 + 3 + + + + + True + True + True + 20 + 10 + 5 + True + + + + 1 + 6 + + + + + True + True + True + 20 + 10 + 5 + True + + + + 1 + 4 + + + + + True + True + True + 20 + 10 + 5 + True + + + + 1 + 5 + + + + + True + False + 20 + 5 + 5 + Separator Text + 0 + + + 2 + 3 + + + + + True + False + 20 + 5 + 5 + Selected Text + 0 + + + 2 + 4 + + + + + True + False + 20 + 5 + 5 + Selected Background + 0 + + + 2 + 5 + + + + + True + False + 20 + 5 + 5 + Selected Border + 0 + + + 2 + 6 + + + + + True + True + True + 20 + 20 + 5 + True + + + + 3 + 3 + + + + + True + True + True + 20 + 20 + 5 + True + + + + 3 + 4 + + - + + True + True + True + 20 + 20 + 5 + True + + + + 3 + 5 + + + + + True + True + True + 20 + 20 + 5 + True + + + + 3 + 6 + + + + True False - 5 - List of recent profiles + OSD Menu Colors 0 + + + + + 0 + 2 + 4 + - - - 0 - 1 - 2 - - - - - cbMI_1 - True - True - False - 20 - 5 - True - - + True False - 5 - Autoswitch options + 20 + 5 + 5 + Text 0 + + 0 + 3 + - - - 0 - 2 - 2 - - - - - cbMI_2 - True - True - False - 20 - 5 - True - - + True False - 5 - Window switcher + 20 + 5 + 5 + Item Border 0 + + 0 + 6 + - - - 0 - 3 - 2 - - - - - Customize... - True - True - True - 20 - 20 - 10 - - - 2 - 5 - 2 - 2 - - - - - cbMI_6 - True - True - False - 20 - 5 - True - - + True False - 5 - Run Program... + 20 + 5 + 5 + Background 0 + + 0 + 4 + - - - 2 - 1 - 2 - - - - - cbMI_3 - True - True - False - 20 - 5 - True - - + True False - 5 - Display Keyboard + 20 + 5 + 5 + Border 0 + + 0 + 5 + - - - 0 - 5 - 2 - - - - - cbMI_8 - True - True - False - 20 - 5 - True - - + True False - 5 - Games List + 10 + On-Screen Keyboard Colors 0 + + + + + 0 + 7 + 4 + - - - 0 - 4 - 2 - - - - - cbMI_4 - True - True - False - 20 - 5 - True - - + True False - 5 - Turn Controller OFF + 20 + 5 + 5 + Normal Button 0 + + 0 + 8 + + + + + True + True + True + 20 + 10 + 5 + True + + + + 1 + 8 + - - - 2 - 4 - 2 - - - - - cbMI_7 - True - True - False - 20 - 5 - True - - + True False - 5 - Show Current Bindings + 20 + 5 + 5 + Pressed Button 0 + + 0 + 9 + + + + + 20 + True + True + True + 20 + 10 + 5 + True + + + + 1 + 9 + - - - 2 - 3 - 2 - - - - - cbMI_5 - True - True - False - 20 - 5 - True - - + True False - 5 - Kill Current Window + 20 + 5 + 5 + Special Button 0 + + 0 + 10 + + + + + 20 + True + True + True + 20 + 10 + 5 + True + + + + 1 + 10 + + + + + 20 + True + False + 20 + 5 + 5 + Text + 0 + + + 2 + 8 + + + + + 20 + True + True + True + 20 + 20 + 5 + True + + + + 3 + 8 + + + + + True + False + 20 + 5 + 5 + Higlight + 0 + + + 2 + 9 + + + + + 20 + True + True + True + 20 + 20 + 5 + True + + + + 3 + 9 + + + + + True + False + + + + 2 + 10 + 2 + + + + + True + False + OSD Style + 0 + + + + + + 0 + 0 + 2 + + + + + True + False + 50 + 10 + 5 + 10 + True + lstOSDStyle + 0 + + + + + 1 + + + + + 0 + 1 + 2 + + + + + True + False + Color Preset + 0 + + + + + + 2 + 0 + 2 + - - - 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 + 50 + 5 + 10 + True + lstOSDColorPreset + 0 + + + + + 1 + + + + 2 + 1 + 2 + - 0 - 1 + 1 - - + + True False - 10 - Switching Rules - 0 - - - + OSD Colors - 0 - 2 + 1 + True + False - - sw - 200 + True - True + False 10 - 5 - 5 - True - in + 10 + 10 + 10 - + True + False True - True - lstItems - True - True - - - - - - - - True - When window... - - - - 1 - - - - + False + True + True - - True - ... then - - - - 2 - - + + True + False + Enable Steam Controller support + 0 + + + + + 0 + 0 + 3 + - - - 0 - 3 - - - - - True - False - Automatic Profile Switching Options - 0 - - - - - - 0 - 0 - - - - - True - False - 10 - True - + + ds4drv 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 Dualshock4 (PS4 controller) support + 0 + + + - False - True - 0 + 0 + 1 + 3 - + True False - True + 20 + Other registered controllers + 0 + 0 + + + - False - True - 1 + 0 + 5 + 3 + + + + + True + False + 30 + If enabled, any connected Dualshock4 controller will be automatically used by SC-Controller + 0 + 0 + + + 0 + 2 + 3 + + + + + True + False + 20 + Controller listed here are automatically used by SC-Controller whenever they are connected. + 0 + + + 0 + 6 + 3 - + + 100 True True - True - 10 - False - False - + 20 + 20 + True + in - + True - False - gtk-remove - 3 + True + lstControllers + False + + + + + + column + + + + 2 + + + + + + 1 + + + + - False - True - 2 + 0 + 7 + 3 - + + hiddrv True True - True - 10 - False - False - + False + 5 + True + - + True False - gtk-add - 3 + Enable HID device support + 0 + + + - False - True - 3 + 0 + 3 + 3 - - - 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 - - + + evdevdrv True - False - Use serial numbers to identify controllers - True - cbShowOSD - - - + True + False + 5 + True + + + + True + False + Enable evdev support + 0 + + + + + + + 0 + 4 + 3 + - - - 0 - 4 - - - - - True - True - False - 15 - True - - + True False - Automatically disable emulation when closing GUI - 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 - 9 + 2 - - + + True - True - False - 30 - 5 - True - - - - True - False - Minimize to status icon instead closing - cbShowOSD - - + False + Controllers - 0 - 7 + 2 + False - + True - True - False - 15 - True - + False + 10 + 10 + 10 + 10 + True + True - + True - False - Enable Status (Systray) Icon - True - cbShowOSD - - - - - - - - 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 + 10 + 5 + True + + + + True + False + Show _OSD notification when profile is switched automatically + True + cbShowOSD + + + + + 0 + 1 + + - + True False - Enable Rumble Support - True - cbShowOSD + 10 + Switching Rules + 0 + + 0 + 2 + - - - 0 - 3 - - - - - True - False - True - + + sw + 200 True - False - warning - - - False - 6 - end + 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 + + + True + True + True + False + False + - - Restart Emulation + True - True - True - + False + + + True + False + gtk-edit + 3 + + + False + True + 0 + + + + + True + False + 5 + Edit Condition + + + False + True + 1 + + - - False - True - 2 - False - False + True 0 - - + + + True False - 16 + True + + + False + True + 1 + + + + + True + True + True + 10 + False + False + - + True False - gtk-dialog-warning - 6 + gtk-remove + 3 - - False - True - 0 - + + + False + True + 2 + + + + + True + True + True + 10 + False + False + - + 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 + gtk-add + 3 - - False - True - 1 - - - - False - False - 0 + True + 3 - - - + + 0 + 4 + - 0 - 0 + 3 - - + + True - True - False - 15 - True - - - - True - False - Display message about new version after SC-Controller upgrade - True - cbShowOSD - - - - - + False + Autoswitcher - 0 - 10 + 3 + True + False - + True - True - False - 30 - 5 - True - + False + 10 + 10 + 10 + 10 + + + True + True + False + 5 + True + + + + True + False + Enable Input Test Mode + True + cbShowOSD + + + + + + + + 0 + 0 + + + + + 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 + 1 + + + + + True + True + False + 15 + True + + + + True + False + Use Serial Numbers to Identify Controllers + True + cbShowOSD + + + + + + + + 0 + 3 + + + + + True + True + False + 15 + True + + + + True + False + Automatically Disable Emulation When Closing GUI + True + cbShowOSD + + + + + + + + 0 + 8 + + + + + True + True + False + 30 + 5 + True + + + + True + False + Minimize to status icon instead closing + True + cbShowOSD + + + + + 0 + 6 + + + + + True + True + False + 15 + True + + + + True + False + Enable Status (Systray) Icon + True + cbShowOSD + + + + + + + + 0 + 5 + + - + True False - Minimize to tray on start - cbShowOSD + 25 + 25 + 5 + Try disabling this option your controller stops working randomly. + True + True + 0 + + + 0 + 4 + + + + + True + True + False + 15 + True + + + + True + False + Enable Rumble Support + True + cbShowOSD + + + + + + + + 0 + 2 + + + + + True + True + False + 15 + True + + + + True + False + Display message about new version after SC-Controller upgrade + True + cbShowOSD + + + + + + + + 0 + 9 + + + + + True + True + False + 30 + 5 + True + + + True + False + Minimize to tray on start + cbShowOSD + + + + 0 + 7 + - 0 - 8 + 4 + + + + + True + False + Advanced + + + 4 + True + False - 3 - - - - - True - False - Advanced - - - 3 - True - False + False + True + 1 @@ -1866,7 +2361,7 @@ or even crash. 0 True - + True False Match Window Class @@ -1895,7 +2390,7 @@ or even crash. - + True False 15 @@ -1977,7 +2472,7 @@ or even crash. True rbProfile - + True False Turn Off Controller @@ -2003,7 +2498,7 @@ or even crash. True rbProfile - + True False Restart Daemon @@ -2043,39 +2538,4 @@ or even crash. - - False - center-on-parent - dialog - - - False - vertical - 2 - - - False - end - - - - - - - - - False - False - 0 - - - - - - - - - - - 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 new file mode 100644 index 000000000..99f0152f0 --- /dev/null +++ b/images/button-images/CPAD.svg @@ -0,0 +1,68 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + 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/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/images/menu-icons/buttons/1.png b/images/menu-icons/buttons/1.png new file mode 100644 index 000000000..63f9b9dfd Binary files /dev/null and b/images/menu-icons/buttons/1.png differ diff --git a/images/menu-icons/buttons/2.png b/images/menu-icons/buttons/2.png new file mode 100644 index 000000000..e592cdf8e Binary files /dev/null and b/images/menu-icons/buttons/2.png differ diff --git a/images/menu-icons/buttons/3.png b/images/menu-icons/buttons/3.png new file mode 100644 index 000000000..6aef210d8 Binary files /dev/null and b/images/menu-icons/buttons/3.png differ diff --git a/images/menu-icons/buttons/4.png b/images/menu-icons/buttons/4.png new file mode 100644 index 000000000..7009a3fca Binary files /dev/null and b/images/menu-icons/buttons/4.png differ diff --git a/images/menu-icons/buttons/CIRCLE.png b/images/menu-icons/buttons/CIRCLE.png new file mode 100644 index 000000000..0b7932328 Binary files /dev/null and b/images/menu-icons/buttons/CIRCLE.png differ diff --git a/images/menu-icons/buttons/CROSS.png b/images/menu-icons/buttons/CROSS.png new file mode 100644 index 000000000..d53447f73 Binary files /dev/null and b/images/menu-icons/buttons/CROSS.png differ diff --git a/images/menu-icons/buttons/SQUARE.png b/images/menu-icons/buttons/SQUARE.png new file mode 100644 index 000000000..c047ad8d5 Binary files /dev/null and b/images/menu-icons/buttons/SQUARE.png differ diff --git a/images/menu-icons/buttons/TRIANGLE.png b/images/menu-icons/buttons/TRIANGLE.png new file mode 100644 index 000000000..7c1444de8 Binary files /dev/null and b/images/menu-icons/buttons/TRIANGLE.png differ diff --git a/images/menu-icons/buttons/snesA.png b/images/menu-icons/buttons/snesA.png new file mode 100644 index 000000000..f70876d29 Binary files /dev/null and b/images/menu-icons/buttons/snesA.png differ diff --git a/images/menu-icons/buttons/snesB.png b/images/menu-icons/buttons/snesB.png new file mode 100644 index 000000000..7a5b30f86 Binary files /dev/null and b/images/menu-icons/buttons/snesB.png differ diff --git a/images/menu-icons/buttons/snesX.png b/images/menu-icons/buttons/snesX.png new file mode 100644 index 000000000..916ea4e1d Binary files /dev/null and b/images/menu-icons/buttons/snesX.png differ diff --git a/images/menu-icons/buttons/snesY.png b/images/menu-icons/buttons/snesY.png new file mode 100644 index 000000000..7fd17b042 Binary files /dev/null and b/images/menu-icons/buttons/snesY.png differ 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/Classic.gtkstyle.css b/osd_styles/Classic.gtkstyle.css new file mode 100644 index 000000000..6e94fa54c --- /dev/null +++ b/osd_styles/Classic.gtkstyle.css @@ -0,0 +1,117 @@ +/* Used colors: all */ + +#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 { + 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/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.colors.json b/osd_styles/Green.colors.json new file mode 100644 index 000000000..76ec7c983 --- /dev/null +++ b/osd_styles/Green.colors.json @@ -0,0 +1,26 @@ +{ + "####" : "This is default color scheme", + + "###" : "Colors used by OSD", + "osd_colors": { + "background": "101010", + "border": "00FF00", + "text": "16BF24", + "menuitem_border": "004000", + "menuitem_hilight": "000070", + "menuitem_hilight_text": "16FF26", + "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/Red.colors.json b/osd_styles/Red.colors.json new file mode 100644 index 000000000..4334f8541 --- /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": "FF0000", + "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/Reloaded.gtkstyle.css b/osd_styles/Reloaded.gtkstyle.css new file mode 100644 index 000000000..1781f36c5 --- /dev/null +++ b/osd_styles/Reloaded.gtkstyle.css @@ -0,0 +1,121 @@ +/* Used colors: background text menuseparator osk_text osk_button1 osk_button1_border osk_hilight osk_pressed */ + +#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; +} + +#osd-area { + background-color: #%(background+1)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 #%(background+1)s solid; +} + +#osd-menu-separator { + color: #%(background+50)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: #%(text)s; + background-color: #%(background+15)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/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 diff --git a/run.sh b/run.sh index 806dc4da1..418a83e99 100755 --- a/run.sh +++ b/run.sh @@ -1,7 +1,7 @@ #!/bin/bash C_MODULES=(uinput hiddrv) -C_VERSION_uinput=6 -C_VERSION_hiddrv=2 +C_VERSION_uinput=8 +C_VERSION_hiddrv=3 function rebuild_c_modules() { echo "lib$1.so is outdated or missing, building one" diff --git a/scc/actions.py b/scc/actions.py index abe85531b..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 @@ -827,7 +827,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) @@ -868,6 +868,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): @@ -895,7 +898,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)) @@ -952,8 +955,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) @@ -2113,6 +2116,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)) @@ -2230,7 +2235,11 @@ def whole(self, mapper, x, y, what): else: self._old_pos = None - if what in (LEFT, 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) + elif what in (LEFT, RIGHT, CPAD): self.x.pad(mapper, x, what) self.y.pad(mapper, y, what) else: @@ -2238,11 +2247,6 @@ def whole(self, mapper, x, y, 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) - - def describe(self, context): if self.name: return self.name rv = [] diff --git a/scc/config.py b/scc/config.py index 8d2e52182..229cc90d7 100644 --- a/scc/config.py +++ b/scc/config.py @@ -76,26 +76,27 @@ 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": "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" : { @@ -103,6 +104,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, } @@ -221,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/constants.py b/scc/constants.py index 088e5b98b..6c007536e 100644 --- a/scc/constants.py +++ b/scc/constants.py @@ -28,7 +28,7 @@ If SC-Controller is updated while daemon is running, DAEMON_VERSION send by daemon will differ one one expected by UI and daemon will be forcefully restarted. """ -DAEMON_VERSION = "0.3.99.4" +DAEMON_VERSION = "0.3.99.5" HPERIOD = 0.02 LPERIOD = 0.5 @@ -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 @@ -111,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/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..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 @@ -38,13 +38,14 @@ class DS4Controller(HIDController): SCButtons.STICKPRESS, SCButtons.RPAD, SCButtons.C, - SCButtons.CPAD, + SCButtons.CPADPRESS, ) - 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.HAS_CPAD + | ControllerFlags.SEPARATE_STICK + ) def _load_hid_descriptor(self, config, max_size, vid, pid, test_mode): @@ -100,6 +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.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 @@ -117,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? @@ -150,7 +169,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", @@ -163,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 }, @@ -185,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 }, @@ -197,16 +218,21 @@ 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.HAS_CPAD + | 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 +243,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 @@ -263,10 +290,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: @@ -276,7 +303,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 +315,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 +323,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 +374,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..696c0b64e 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 @@ -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() 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/drivers/hiddrv.c b/scc/drivers/hiddrv.c index 7d0155583..225bea6c7 100644 --- a/scc/drivers/hiddrv.c +++ b/scc/drivers/hiddrv.c @@ -8,7 +8,7 @@ #define HIDDRV_MODULE_VERSION 2 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,7 @@ enum AxisMode { HATSWITCH = 4, DS4ACCEL = 5, DS4GYRO = 6, - + DS4TOUCHPAD = 7, _AxisMode_force_int = INT_MAX }; @@ -243,6 +245,11 @@ bool decode(struct HIDDecoder* dec, const char* data) { dec->axes[i].bit_offset); dec->state.axes[i] = -value.s16; break; + case DS4TOUCHPAD: + value = grab_value(data, dec->axes[i].byte_offset, + dec->axes[i].bit_offset); + dec->state.axes[i] = (value.u16 & 0x0FFF); + break; default: break; } diff --git a/scc/drivers/hiddrv.py b/scc/drivers/hiddrv.py index 146d108f2..77e423876 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,7 @@ class AxisMode(IntEnum): HATSWITCH = 4 DS4ACCEL = 5 # 16bit, signed, no additional math needed DS4GYRO = 6 # 16bit, signed, inverted + DS4TOUCHPAD = 7 # 12bit class AxisModeData(ctypes.Structure): @@ -163,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) @@ -191,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) @@ -213,7 +218,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 +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/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/gui/action_editor.py b/scc/gui/action_editor.py index a43ec1beb..ae1ac047b 100644 --- a/scc/gui/action_editor.py +++ b/scc/gui/action_editor.py @@ -1010,10 +1010,12 @@ def set_input(self, id, action, mode=None): self._set_mode(action, mode or Action.AC_PAD) self.set_action(action) self.hide_macro() - if id == "LPAD": + if id == Profile.LPAD: self.set_title(_("Left Pad")) - else: + elif id == Profile.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 d5ac75508..49928ccff 100644 --- a/scc/gui/app.py +++ b/scc/gui/app.py @@ -7,8 +7,8 @@ from __future__ import unicode_literals from scc.tools import _, set_logging_level -from gi.repository import Gtk, Gdk, Gio, GLib, GObject -from scc.gui.controller_widget import TRIGGERS, PADS, STICKS, GYROS, BUTTONS +from gi.repository import Gtk, Gdk, Gio, GLib +from scc.gui.controller_widget import TRIGGERS, PADS, STICKS, BUTTONS, GYROS from scc.gui.parser import GuiActionParser, InvalidAction from scc.gui.controller_image import ControllerImage from scc.gui.profile_switcher import ProfileSwitcher @@ -19,6 +19,7 @@ from scc.gui.dwsnc import headerbar, IS_UNITY from scc.gui.ribar import RIBar from scc.tools import check_access, find_gksudo, profile_is_override, nameof +from scc.tools import get_profile_name, profile_is_default, find_profile from scc.constants import SCButtons, STICK, STICK_PAD_MAX from scc.constants import DAEMON_VERSION, LEFT, RIGHT from scc.tools import get_profile_name, profile_is_default, profile_is_override @@ -113,11 +114,10 @@ def setup_widgets(self): ], Gdk.DragAction.COPY ) - # 'C' button + # 'C' and 'CPAD' buttons vbc = self.builder.get_object("vbC") self.main_area = self.builder.get_object("mainArea") vbc.get_parent().remove(vbc) - vbc.connect('size-allocate', self.on_vbc_allocated) # Background self.background = ControllerImage(self) @@ -156,8 +156,10 @@ 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') - config = self.background.load_config(controller.get_gui_config_file()) + btC = self.builder.get_object('btC') + btCPAD = self.builder.get_object('btCPAD') + config = controller.load_gui_config(self.imagepath or {}) + config = self.background.use_config(config) def do_loading(): """ Called after transition is finished """ @@ -181,15 +183,20 @@ 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) - # vbC.set_visible(True) + for w in (btC, 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'] @@ -241,12 +248,11 @@ def check(self): msg += "\n" + _('or click on "Fix Temporary" button to attempt fix that should work until next restart.') ribar = self.show_error(msg) gksudo = find_gksudo() - modprobe = find_binary("modprobe") if gksudo and not hasattr(ribar, "_fix_tmp"): button = Gtk.Button.new_with_label(_("Fix Temporary")) ribar._fix_tmp = button button.connect('clicked', self.apply_temporary_fix, - gksudo + [modprobe, "uinput"], + gksudo + ["modprobe", "uinput"], _("This will load missing uinput module.") ) ribar.add_button(button, -1) @@ -450,12 +456,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. @@ -634,17 +634,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 @@ -725,15 +729,22 @@ 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 + 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 def on_ebImage_motion_notify_event(self, box, event): @@ -879,10 +890,14 @@ def enable_test_mode(self): Disables and re-enables Input Test mode. If sniffing is disabled in daemon configuration, 2nd call fails and logs error. """ - if self.dm.is_alive(): + if self.dm.is_alive() and not self.osd_mode: if self.test_mode_controller: self.test_mode_controller.unlock_all() - c = self.profile_switchers[0].get_controller() + try: + c = self.dm.get_controllers()[0] + except IndexError: + # Zero controllers + return if c: c.unlock_all() c.observe(DaemonManager.nocallback, self.on_observe_failed, @@ -913,13 +928,12 @@ 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 # adds too much lags. c.lock(on_lock_success, on_lock_failed, - 'A', 'B', 'X', 'Y', 'START', 'BACK', 'LB', 'RB', 'C', 'LPAD', 'RPAD', + 'A', 'B', 'X', 'Y', 'START', 'BACK', 'LB', 'RB', 'C', 'STICK', 'LGRIP', 'RGRIP', 'LT', 'RT', 'STICKPRESS') # Ask daemon to temporaly reconfigure pads for mouse emulation @@ -984,7 +998,9 @@ def on_daemon_error(self, daemon, error): def on_daemon_event_observer(self, daemon, c, what, data): - if what in (LEFT, RIGHT, STICK): + if self.osd_mode_mapper: + self.osd_mode_mapper.handle_event(daemon, what, data) + elif what in (LEFT, RIGHT, STICK): widget, area = { LEFT : (self.lpad_test, "LPADTEST"), RIGHT : (self.rpad_test, "RPADTEST"), @@ -1260,12 +1276,7 @@ def i_told_you_to_quit(*a): def do_activate(self, *a): - if (not IS_UNITY and self.app.config['gui']['enable_status_icon'] - and self.app.config['gui']['minimize_on_start']): - log.info("") - log.info(_("SC-Controller started and running in notification area")) - else: - self.builder.get_object("window").show() + self.builder.get_object("window").show() def remove_dot_profile(self): @@ -1352,10 +1363,11 @@ def aso(long_name, short_name, description, o.arg = arg self.add_main_option_entries([o]) + self.connect('handle-local-options', self.do_local_options) + aso("verbose", b"v", "Be verbose") aso("debug", b"d", "Be more verbose (debug mode)") aso("osd", b"o", "OSD mode (displays only editor only)") - self.connect('handle-local-options', self.do_local_options) def save_profile_selection(self, path): diff --git a/scc/gui/binding_editor.py b/scc/gui/binding_editor.py index ccac259af..129f079eb 100644 --- a/scc/gui/binding_editor.py +++ b/scc/gui/binding_editor.py @@ -78,12 +78,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 @@ -95,10 +98,12 @@ def set_action(self, profile, id, action): elif id in STICKS + PADS: if id in STICKS: before, profile.stick = profile.stick, action - elif id == "LPAD": + elif id == Profile.LPAD: before, profile.pads[Profile.LEFT] = profile.pads[Profile.LEFT], action - else: + elif id == Profile.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 @@ -122,10 +127,12 @@ def get_action(self, profile, id): elif id in STICKS + PADS: if id in STICKS: return profile.stick - elif id == "LPAD": + elif id == Profile.LPAD: return profile.pads[Profile.LEFT] - else: + elif id == Profile.RPAD: return profile.pads[Profile.RIGHT] + else: + return profile.pads[Profile.CPAD] return None diff --git a/scc/gui/controller_image.py b/scc/gui/controller_image.py index 0a764848c..949680561 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.current = self._ensure_config(config or {}) 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/controller_widget.py b/scc/gui/controller_widget.py index 446722ddf..872fe0962 100644 --- a/scc/gui/controller_widget.py +++ b/scc/gui/controller_widget.py @@ -22,10 +22,11 @@ log = logging.getLogger("ControllerWidget") TRIGGERS = [ "LT", "RT" ] -PADS = [ "LPAD", "RPAD" ] +PADS = [ Profile.LPAD, Profile.RPAD, Profile.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 ] @@ -158,11 +159,12 @@ def on_cursor_motion(self, trash, event): ix2 = 74 # Check if cursor is placed on icon if event.x < ix2: - what = dict( - LPAD = LEFT, - RPAD = RIGHT, - STICK = nameof(SCButtons.STICKPRESS) - )[self.name] + what = { + Profile.LPAD : LEFT, + Profile.RPAD : RIGHT, + Profile.CPAD : nameof(SCButtons.CPADPRESS), + Profile.STICK : nameof(SCButtons.STICKPRESS), + }[self.name] self.app.hilight(what) self.over_icon = True else: @@ -215,16 +217,22 @@ class ControllerPad(ControllerStick): def __init__(self, app, name, use_icon, enable_press, widget): ControllerStick.__init__(self, app, name, use_icon, enable_press, widget) - self.click_button = getattr(SCButtons, self.id) + if name in (Profile.LPAD, Profile.RPAD): + self.click_button = getattr(SCButtons, name) + elif name == Profile.CPAD: + self.click_button = SCButtons.CPADPRESS def update(self): - if self.id == "LPAD": + if self.id == Profile.LPAD: action = self.app.current.pads[Profile.LEFT] pressed = self.app.current.buttons[SCButtons.LPAD] - else: + elif self.id == Profile.RPAD: action = self.app.current.pads[Profile.RIGHT] pressed = self.app.current.buttons[SCButtons.RPAD] + else: + action = self.app.current.pads[Profile.CPAD] + pressed = self.app.current.buttons[SCButtons.CPADPRESS] self._set_label(action) if self.pressed: diff --git a/scc/gui/daemon_manager.py b/scc/gui/daemon_manager.py index 812992b67..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") @@ -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, @@ -373,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/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/gui/global_settings.py b/scc/gui/global_settings.py index 2e616aacc..1597f1342 100644 --- a/scc/gui/global_settings.py +++ b/scc/gui/global_settings.py @@ -7,16 +7,17 @@ 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 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): @@ -60,6 +61,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() @@ -70,6 +75,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 @@ -89,6 +107,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") @@ -114,6 +134,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: @@ -124,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): @@ -233,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"]: @@ -243,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() @@ -312,11 +347,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() @@ -553,6 +630,44 @@ def on_entTitle_changed(self, ent): btSave.set_sensitive(True) + def on_cbOSDColorPreset_changed(self, cb): + 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 + 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.config["osd_color_theme"] = theme + self.app.save_config() + + + def on_cbOSDStyle_changed(self, cb): + 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() + + @staticmethod def _make_mi_instance(index): """ Helper method used by on_cbMI_toggled and load_cbMIs """ @@ -663,3 +778,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))) 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 diff --git a/scc/lib/daemon.py b/scc/lib/daemon.py index 2bbfc990c..2108c345c 100644 --- a/scc/lib/daemon.py +++ b/scc/lib/daemon.py @@ -124,14 +124,13 @@ def stop(self, once=False): try: for x in xrange(0, 10): # Waits max 1s os.kill(pid, signal.SIGTERM) - time.sleep(0.1) if once: break for x in xrange(50): # This loop waits 5s os.kill(pid, 0) time.sleep(0.1) - if not once: - os.kill(pid, signal.SIGKILL) + time.sleep(0.1) + os.kill(pid, signal.SIGKILL) except OSError as err: e = str(err.args) if e.find("No such process") > 0: diff --git a/scc/mapper.py b/scc/mapper.py index 8e0de84c8..95b864491 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 @@ -212,6 +212,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 self.controller.flags + + def is_touched(self, what): """ Returns True if specified pad is being touched. @@ -223,6 +231,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 @@ -240,6 +250,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 @@ -326,7 +338,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 @@ -395,8 +407,14 @@ def input(self, controller, old_state, state): self.lpad_touched = False self.profile.pads[LEFT].whole(self, 0, 0, LEFT) - except Exception, e: + # 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: # 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 543b1bb21..0bf3204e5 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 -from scc.constants import CUT, ROUND, LINEAR +from scc.constants import FE_PAD, SCButtons, HapticPos, ControllerFlags +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 @@ -218,30 +218,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): @@ -256,6 +266,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) @@ -351,6 +366,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 @@ -378,14 +394,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) @@ -394,15 +408,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() @@ -431,11 +444,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) + self._roll_task = mapper.schedule(0.02, self._roll) def encode(self): @@ -485,10 +498,14 @@ 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): + 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() @@ -498,6 +515,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): @@ -1330,6 +1349,8 @@ def _get_pos(self): 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._last_pos is None: # Just pressed - fill deque with current position @@ -1346,6 +1367,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() diff --git a/scc/osd/__init__.py b/scc/osd/__init__.py index c0dc005df..0dd617695 100644 --- a/scc/osd/__init__.py +++ b/scc/osd/__init__.py @@ -10,127 +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 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; - } - - """ + # 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; @@ -142,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 @@ -176,15 +64,17 @@ 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 = 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((css % colors).encode("utf-8")) Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), OSDWindow.css_provider, @@ -195,7 +85,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((css % colors).encode("utf-8")) Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), OSDWindow.css_provider, @@ -328,6 +222,48 @@ 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]) + 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] + + class StickController(GObject.GObject, TimerManager): """ Simple utility class that gets fed by with position and emits @@ -380,3 +316,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 diff --git a/scc/osd/menu.py b/scc/osd/menu.py index bd584bb14..50f8fac3c 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)), @@ -402,6 +433,7 @@ 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 @@ -432,6 +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() + self.set_name("osd-menu-inactive") def _control_equals_cancel(self, daemon, x, y): @@ -494,8 +527,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 """ 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/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. diff --git a/scc/profile.py b/scc/profile.py index 6cb268caa..10a41eb27 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,9 @@ class Profile(object): LEFT = LEFT RIGHT = RIGHT + LPAD = SCButtons.LPAD.name + RPAD = SCButtons.RPAD.name + CPAD = CPAD WHOLE = WHOLE STICK = STICK GYRO = GYRO @@ -63,6 +66,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 +140,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 +154,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 +184,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() 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 = () + diff --git a/scc/uinput.c b/scc/uinput.c index d7df10af9..bc7e57f44 100644 --- a/scc/uinput.c +++ b/scc/uinput.c @@ -34,7 +34,7 @@ #include #pragma GCC diagnostic ignored "-Wunused-result" -#define UNPUT_MODULE_VERSION 7 +#define UNPUT_MODULE_VERSION 8 #define MAX_FF_EVENTS 4 #define INFINITE_RUMBLE 10000 // Not really infinite, but longer than controller can handle diff --git a/scc/uinput.py b/scc/uinput.py index e602266e3..866a76087 100644 --- a/scc/uinput.py +++ b/scc/uinput.py @@ -30,7 +30,7 @@ from scc.cheader import defines from scc.lib import IntEnum -UNPUT_MODULE_VERSION = 7 +UNPUT_MODULE_VERSION = 8 # Get All defines from linux headers if os.path.exists('/usr/include/linux/input-event-codes.h'): 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) 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" diff --git a/tests/test_inputs.py b/tests/test_inputs.py new file mode 100644 index 000000000..4f74330ac --- /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) == -24479 + + + @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] == 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] == 1570 + # '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..e81e87410 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,14 @@ def test_packages(self): """ Tests if every known Action is documentated in docs/actions.md """ + 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): if ispkg: