├── .gitignore ├── LICENSE.md ├── README.md ├── app-android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── at │ │ │ └── bitfire │ │ │ └── gfxtablet │ │ │ ├── CanvasActivity.java │ │ │ ├── CanvasView.java │ │ │ ├── NetEvent.java │ │ │ ├── NetworkClient.java │ │ │ ├── SettingsActivity.java │ │ │ └── SettingsFragment.java │ │ └── res │ │ ├── drawable-hdpi │ │ ├── ic_action_picture.png │ │ ├── ic_arrow_expand_white_48dp.png │ │ ├── ic_launcher.png │ │ └── ic_settings_white_48dp.png │ │ ├── drawable-mdpi │ │ ├── ic_action_picture.png │ │ ├── ic_arrow_expand_white_48dp.png │ │ ├── ic_launcher.png │ │ └── ic_settings_white_48dp.png │ │ ├── drawable-xhdpi │ │ ├── ic_action_picture.png │ │ ├── ic_arrow_expand_white_48dp.png │ │ ├── ic_launcher.png │ │ └── ic_settings_white_48dp.png │ │ ├── drawable-xxhdpi │ │ ├── ic_action_picture.png │ │ ├── ic_arrow_expand_white_48dp.png │ │ └── ic_settings_white_48dp.png │ │ ├── drawable │ │ ├── bg_grid.png │ │ └── bg_grid_pattern.xml │ │ ├── layout │ │ ├── activity_canvas.xml │ │ └── activity_settings.xml │ │ ├── menu │ │ ├── activity_canvas.xml │ │ └── set_template_image.xml │ │ ├── values │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── preferences.xml ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── import-summary.txt └── settings.gradle ├── doc └── protocol.txt └── driver-uinput ├── .gitignore ├── Makefile ├── networktablet.c └── protocol.h /.gitignore: -------------------------------------------------------------------------------- 1 | ### ANDROID 2 | 3 | # built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # files for the dex VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # generated files 14 | bin/ 15 | gen/ 16 | 17 | # Local configuration file (sdk path, etc) 18 | local.properties 19 | 20 | # Eclipse project files 21 | .classpath 22 | .project 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Intellij project files 28 | *.iml 29 | *.ipr 30 | *.iws 31 | .idea/ 32 | 33 | 34 | ### ECLIPSE 35 | 36 | *.pydevproject 37 | .project 38 | .metadata 39 | bin/** 40 | tmp/** 41 | tmp/**/* 42 | *.tmp 43 | *.bak 44 | *.swp 45 | *~.nib 46 | local.properties 47 | .classpath 48 | .settings/ 49 | .loadpath 50 | 51 | # External tool builders 52 | .externalToolBuilders/ 53 | 54 | # Locally stored "Eclipse launch configurations" 55 | *.launch 56 | 57 | # CDT-specific 58 | .cproject 59 | 60 | # PDT-specific 61 | .buildpath 62 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Author: Ricki Hirner 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 19 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | **This project is currently not actively maintained/managed. If you're interested in taking it over, 3 | please tell me at info@bitfire.at.** 4 | 5 | To be informed about updates: 6 | 7 | * [follow GfxTablet on Twitter](https://twitter.com/GfxTablet) 8 | 9 | 10 | What is GfxTablet? 11 | ================== 12 | 13 | GfxTablet shall make it possible to use your Android device (especially 14 | tablets) like a graphics tablet. 15 | 16 | It consists of two components: 17 | 18 | * the GfxTablet Android app 19 | * the input driver for your PC 20 | 21 | The GfxTablet app sends motion and touch events via UDP to a specified host 22 | on port 40118. 23 | 24 | The input driver must be installed on your PC. It creates a virtual "network tablet" 25 | on your PC that is controlled by your Android device. 26 | 27 | So, you can use your Android tablet or smartphone to control the PC and, 28 | for instance _use GIMP with your Android tablet as a graphics tablet_ 29 | (even pressure-sensitive, if your hardware supports it). 30 | 31 | Help and discussion: [GfxTablet discussions](https://github.com/rfc2822/GfxTablet/discussions) 32 | 33 | 34 | License 35 | ------- 36 | 37 | GfxTablet is licensed under The MIT License. 38 | 39 | Initial contributor: Ricki Hirner / powered by [bitfire web engineering](https://www.bitfire.at) / [gimpusers.com](http://www.gimpusers.com) 40 | 41 | 42 | Features 43 | -------- 44 | 45 | * Pressure sensitivity supported 46 | * Size of canvas will be detected and sent to the client 47 | * Option for ignoring events that are not triggered by a stylus pen: 48 | so you can lay your hand on the tablet and draw with the pen. 49 | 50 | 51 | Requirements 52 | ------------ 53 | 54 | * App: Any device with Android 4.0+ and touch screen 55 | * Driver: Linux with uinput kernel module (included in modern versions of Fedora, Ubuntu etc.) 56 | 57 | If you use Xorg (you probably do): 58 | 59 | * Xorg-evdev module loaded and configured – probably on by default, but if it doesn't work, you may 60 | need to activate the module. 61 | 62 | 63 | Installation 64 | ============ 65 | 66 | Github repository: https://github.com/rfc2822/GfxTablet 67 | 68 | 69 | Part 1: uinput driver 70 | --------------------- 71 | 72 | On your PC, either download one of these binaries (don't forget to `chmod a+x` it): 73 | 74 | * [networktablet 64-bit, dynamically linked, tested with Debian Stretch/Buster](https://github.com/rfc2822/GfxTablet/releases/download/android-app-1.4-linux-driver-1.5/networktablet) 75 | 76 | or compile it yourself (don't be afraid, it's only one file) 77 | 78 | 1. Clone the repository: 79 | `git clone git://github.com/rfc2822/GfxTablet.git` 80 | 2. Install gcc, make and linux kernel header includes (`kernel-headers` on Fedora) 81 | 3. `cd GfxTablet/driver-uinput; make` 82 | 83 | Then, run the binary. The driver runs in user-mode, so it doesn't need any special privileges. 84 | However, it needs access to `/dev/uinput`. If your distribution doesn't create a group for 85 | uinput access, you'll need to do it yourself or just run the driver as root: 86 | 87 | `sudo ./networktablet` 88 | 89 | Then you should see a status message saying the driver is ready. If you do `xinput list` in a separate 90 | terminal, should show a "Network Tablet" device. 91 | 92 | You can start and stop (Ctrl+C) the Network Tablet at any time, but please be aware that applications 93 | which use the device may be confused by that and could crash. 94 | 95 | `networktablet` will display a status line for every touch/motion event it receives. 96 | 97 | 98 | Part 2: App 99 | ----------- 100 | 101 | You can either 102 | 103 | 1. compile the app from the source code in the Github repository, or 104 | 2. [download it from the open-source market F-Droid](https://f-droid.org/repository/browse/?fdcategory=Multimedia&fdid=at.bitfire.gfxtablet), or 105 | 3. download it from Samsung Galaxy Apps (if you have a Samsung device), or 106 | 4. [download it directly from Github](https://github.com/rfc2822/GfxTablet/releases), or 107 | 5. ~~[download it from Google Play](https://play.google.com/store/apps/details?id=at.bitfire.gfxtablet)~~ [removed by Google](https://forums.bitfire.at/topic/1071/google-has-removed-gfxtablet-from-google-play) 108 | 109 | After installing, enter your host IP in the Settings / Host name and it should be ready. 110 | 111 | 112 | Part 3: Use it 113 | -------------- 114 | 115 | Now you can use your tablet as an input device in every Linux application (including X.org 116 | applications). For instance, when networktablet is running, GIMP should have a "Network Tablet" 117 | entry in "Edit / Input Devices". Set its mode to "Screen" and it's ready to use. 118 | 119 | 120 | Frequently Asked Questions 121 | ========================== 122 | 123 | Using with multiple monitors 124 | ---------------------------- 125 | 126 | If you're using multiple screens, you can assign the Network Tablet device to a specific screen 127 | once it's running (thanks to @symbally and @Evi1M4chine, see https://forums.bitfire.at/topic/82/multi-monitor-problem): 128 | 129 | 1. Use `xrandr` to identify which monitor you would like to have the stylus picked up on. In this example, `DVI-I-1` 130 | is the display to assign. 131 | 2. Do `xinput map-to-output "$( xinput list --id-only "Network Tablet" )" DVI-I-1`. 132 | 133 | Known problems 134 | -------------- 135 | 136 | * With Gnome 3.16 (as shipped with Fedora 22), [Gnome Shell crashes when using GfxTablet](https://bugzilla.redhat.com/show_bug.cgi?id=1209008). 137 | 138 | 139 | -------------------------------------------------------------------------------- /app-android/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Android ### 4 | # Built application files 5 | *.apk 6 | *.ap_ 7 | 8 | # Files for the Dalvik VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # Generated files 15 | bin/ 16 | gen/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | 32 | ### Intellij ### 33 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 34 | 35 | *.iml 36 | 37 | ## Directory-based project format: 38 | .idea/ 39 | # if you remove the above rule, at least ignore the following: 40 | 41 | # User-specific stuff: 42 | # .idea/workspace.xml 43 | # .idea/tasks.xml 44 | # .idea/dictionaries 45 | 46 | # Sensitive or high-churn files: 47 | # .idea/dataSources.ids 48 | # .idea/dataSources.xml 49 | # .idea/sqlDataSources.xml 50 | # .idea/dynamic.xml 51 | # .idea/uiDesigner.xml 52 | 53 | # Gradle: 54 | # .idea/gradle.xml 55 | # .idea/libraries 56 | 57 | # Mongo Explorer plugin: 58 | # .idea/mongoSettings.xml 59 | 60 | ## File-based project format: 61 | *.ipr 62 | *.iws 63 | 64 | ## Plugin-specific files: 65 | 66 | # IntelliJ 67 | out/ 68 | 69 | # mpeltonen/sbt-idea plugin 70 | .idea_modules/ 71 | 72 | # JIRA plugin 73 | atlassian-ide-plugin.xml 74 | 75 | # Crashlytics plugin (for Android Studio and IntelliJ) 76 | com_crashlytics_export_strings.xml 77 | crashlytics.properties 78 | crashlytics-build.properties 79 | 80 | 81 | ### Gradle ### 82 | .gradle 83 | build/ 84 | 85 | # Ignore Gradle GUI config 86 | gradle-app.setting 87 | 88 | 89 | ### external libs ### 90 | .svn 91 | -------------------------------------------------------------------------------- /app-android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | applicationId "at.bitfire.gfxtablet" 9 | minSdkVersion 14 10 | targetSdkVersion 22 11 | } 12 | 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile "com.android.support:appcompat-v7:22.+" 23 | } 24 | -------------------------------------------------------------------------------- /app-android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app-android/app/src/main/java/at/bitfire/gfxtablet/CanvasActivity.java: -------------------------------------------------------------------------------- 1 | package at.bitfire.gfxtablet; 2 | 3 | import android.content.Intent; 4 | import android.content.SharedPreferences; 5 | import android.database.Cursor; 6 | import android.graphics.drawable.BitmapDrawable; 7 | import android.graphics.drawable.Drawable; 8 | import android.net.Uri; 9 | import android.os.AsyncTask; 10 | import android.os.Build; 11 | import android.os.Bundle; 12 | import android.preference.PreferenceFragment; 13 | import android.preference.PreferenceManager; 14 | import android.provider.MediaStore; 15 | import android.support.v7.app.AppCompatActivity; 16 | import android.support.v7.widget.PopupMenu; 17 | import android.util.Log; 18 | import android.view.Menu; 19 | import android.view.MenuItem; 20 | import android.view.View; 21 | import android.view.WindowManager; 22 | import android.widget.ImageView; 23 | import android.widget.Toast; 24 | 25 | public class CanvasActivity extends AppCompatActivity implements View.OnSystemUiVisibilityChangeListener, SharedPreferences.OnSharedPreferenceChangeListener { 26 | private static final int RESULT_LOAD_IMAGE = 1; 27 | private static final String TAG = "GfxTablet.Canvas"; 28 | 29 | final Uri homepageUri = Uri.parse(("https://gfxtablet.bitfire.at")); 30 | 31 | NetworkClient netClient; 32 | 33 | SharedPreferences preferences; 34 | boolean fullScreen = false; 35 | 36 | 37 | @Override 38 | protected void onCreate(Bundle savedInstanceState) { 39 | super.onCreate(savedInstanceState); 40 | 41 | preferences = PreferenceManager.getDefaultSharedPreferences(this); 42 | preferences.registerOnSharedPreferenceChangeListener(this); 43 | 44 | setContentView(R.layout.activity_canvas); 45 | 46 | // create network client in a separate thread 47 | netClient = new NetworkClient(PreferenceManager.getDefaultSharedPreferences(this)); 48 | new Thread(netClient).start(); 49 | new ConfigureNetworkingTask().execute(); 50 | 51 | // notify CanvasView of the network client 52 | CanvasView canvas = (CanvasView)findViewById(R.id.canvas); 53 | canvas.setNetworkClient(netClient); 54 | } 55 | 56 | @Override 57 | protected void onResume() { 58 | super.onResume(); 59 | 60 | if (preferences.getBoolean(SettingsActivity.KEY_KEEP_DISPLAY_ACTIVE, true)) 61 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 62 | else 63 | getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 64 | 65 | showTemplateImage(); 66 | } 67 | 68 | @Override 69 | protected void onDestroy() { 70 | super.onDestroy(); 71 | netClient.getQueue().add(new NetEvent(NetEvent.Type.TYPE_DISCONNECT)); 72 | } 73 | 74 | @Override 75 | public boolean onCreateOptionsMenu(Menu menu) { 76 | getMenuInflater().inflate(R.menu.activity_canvas, menu); 77 | return true; 78 | } 79 | 80 | @Override 81 | public void onBackPressed() { 82 | if (fullScreen) 83 | switchFullScreen(null); 84 | else 85 | super.onBackPressed(); 86 | } 87 | 88 | public void showAbout(MenuItem item) { 89 | startActivity(new Intent(Intent.ACTION_VIEW, homepageUri)); 90 | } 91 | 92 | public void showDonate(MenuItem item) { 93 | startActivity(new Intent(Intent.ACTION_VIEW, homepageUri.buildUpon().appendPath("donate").build())); 94 | } 95 | 96 | public void showSettings(MenuItem item) { 97 | startActivityForResult(new Intent(this, SettingsActivity.class), 0); 98 | } 99 | 100 | 101 | // preferences were changed 102 | 103 | @Override 104 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 105 | switch (key) { 106 | case SettingsActivity.KEY_PREF_HOST: 107 | Log.i(TAG, "Recipient host changed, reconfiguring network client"); 108 | new ConfigureNetworkingTask().execute(); 109 | break; 110 | } 111 | } 112 | 113 | 114 | // full-screen methods 115 | 116 | public void switchFullScreen(MenuItem item) { 117 | final View decorView = getWindow().getDecorView(); 118 | int uiFlags = decorView.getSystemUiVisibility(); 119 | 120 | if (Build.VERSION.SDK_INT >= 14) 121 | uiFlags ^= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; 122 | if (Build.VERSION.SDK_INT >= 16) 123 | uiFlags ^= View.SYSTEM_UI_FLAG_FULLSCREEN; 124 | if (Build.VERSION.SDK_INT >= 19) 125 | uiFlags ^= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; 126 | 127 | decorView.setOnSystemUiVisibilityChangeListener(this); 128 | decorView.setSystemUiVisibility(uiFlags); 129 | } 130 | 131 | @Override 132 | public void onSystemUiVisibilityChange(int visibility) { 133 | Log.i("GfxTablet", "System UI changed " + visibility); 134 | 135 | fullScreen = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0; 136 | 137 | // show/hide action bar according to full-screen mode 138 | if (fullScreen) { 139 | CanvasActivity.this.getSupportActionBar().hide(); 140 | Toast.makeText(CanvasActivity.this, "Press Back button to leave full-screen mode.", Toast.LENGTH_LONG).show(); 141 | } else 142 | CanvasActivity.this.getSupportActionBar().show(); 143 | } 144 | 145 | 146 | // template image logic 147 | 148 | private String getTemplateImagePath() { 149 | return preferences.getString(SettingsActivity.KEY_TEMPLATE_IMAGE, null); 150 | } 151 | 152 | public void setTemplateImage(MenuItem item) { 153 | if (getTemplateImagePath() == null) 154 | selectTemplateImage(item); 155 | else { 156 | // template image already set, show popup 157 | PopupMenu popup = new PopupMenu(this, findViewById(R.id.menu_set_template_image)); 158 | popup.getMenuInflater().inflate(R.menu.set_template_image, popup.getMenu()); 159 | popup.show(); 160 | } 161 | } 162 | 163 | public void selectTemplateImage(MenuItem item) { 164 | Intent i = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 165 | startActivityForResult(i, RESULT_LOAD_IMAGE); 166 | } 167 | 168 | public void clearTemplateImage(MenuItem item) { 169 | preferences.edit().remove(SettingsActivity.KEY_TEMPLATE_IMAGE).commit(); 170 | showTemplateImage(); 171 | } 172 | 173 | @Override 174 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 175 | super.onActivityResult(requestCode, resultCode, data); 176 | if (requestCode == RESULT_LOAD_IMAGE && resultCode == RESULT_OK && data != null) { 177 | Uri selectedImage = data.getData(); 178 | String[] filePathColumn = { MediaStore.Images.Media.DATA }; 179 | 180 | Cursor cursor = getContentResolver().query(selectedImage, filePathColumn, null, null, null); 181 | try { 182 | cursor.moveToFirst(); 183 | 184 | int columnIndex = cursor.getColumnIndex(filePathColumn[0]); 185 | String picturePath = cursor.getString(columnIndex); 186 | 187 | preferences.edit().putString(SettingsActivity.KEY_TEMPLATE_IMAGE, picturePath).commit(); 188 | showTemplateImage(); 189 | } finally { 190 | cursor.close(); 191 | } 192 | } 193 | } 194 | 195 | public void showTemplateImage() { 196 | ImageView template = (ImageView)findViewById(R.id.canvas_template); 197 | template.setImageDrawable(null); 198 | 199 | if (template.getVisibility() == View.VISIBLE) { 200 | String picturePath = preferences.getString(SettingsActivity.KEY_TEMPLATE_IMAGE, null); 201 | if (picturePath != null) 202 | try { 203 | // TODO load bitmap efficiently, for intended view size and display resolution 204 | // https://developer.android.com/training/displaying-bitmaps/load-bitmap.html 205 | final Drawable drawable = new BitmapDrawable(getResources(), picturePath); 206 | template.setImageDrawable(drawable); 207 | } catch (Exception e) { 208 | Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); 209 | } 210 | } 211 | } 212 | 213 | 214 | private class ConfigureNetworkingTask extends AsyncTask { 215 | @Override 216 | protected Boolean doInBackground(Void... params) { 217 | return netClient.reconfigureNetworking(); 218 | } 219 | 220 | protected void onPostExecute(Boolean success) { 221 | if (success) 222 | Toast.makeText(CanvasActivity.this, "Touch events will be sent to " + netClient.destAddress.getHostAddress() + ":" + NetworkClient.GFXTABLET_PORT, Toast.LENGTH_LONG).show(); 223 | 224 | findViewById(R.id.canvas_template).setVisibility(success ? View.VISIBLE : View.GONE); 225 | findViewById(R.id.canvas).setVisibility(success ? View.VISIBLE : View.GONE); 226 | findViewById(R.id.canvas_message).setVisibility(success ? View.GONE : View.VISIBLE); 227 | } 228 | } 229 | 230 | } 231 | -------------------------------------------------------------------------------- /app-android/app/src/main/java/at/bitfire/gfxtablet/CanvasView.java: -------------------------------------------------------------------------------- 1 | package at.bitfire.gfxtablet; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.SharedPreferences; 6 | import android.graphics.Color; 7 | import android.preference.PreferenceManager; 8 | import android.support.annotation.NonNull; 9 | import android.util.AttributeSet; 10 | import android.util.Log; 11 | import android.view.MotionEvent; 12 | import android.view.View; 13 | 14 | import at.bitfire.gfxtablet.NetEvent.Type; 15 | 16 | @SuppressLint("ViewConstructor") 17 | public class CanvasView extends View implements SharedPreferences.OnSharedPreferenceChangeListener { 18 | private static final String TAG = "GfxTablet.CanvasView"; 19 | 20 | private enum InRangeStatus { 21 | OutOfRange, 22 | InRange, 23 | FakeInRange 24 | } 25 | 26 | final SharedPreferences settings; 27 | NetworkClient netClient; 28 | boolean acceptStylusOnly; 29 | int maxX, maxY; 30 | InRangeStatus inRangeStatus; 31 | 32 | 33 | // setup 34 | 35 | public CanvasView(Context context, AttributeSet attributeSet) { 36 | super(context, attributeSet); 37 | 38 | // view is disabled until a network client is set 39 | setEnabled(false); 40 | 41 | settings = PreferenceManager.getDefaultSharedPreferences(context); 42 | settings.registerOnSharedPreferenceChangeListener(this); 43 | setBackground(); 44 | setInputMethods(); 45 | inRangeStatus = InRangeStatus.OutOfRange; 46 | } 47 | 48 | public void setNetworkClient(NetworkClient networkClient) { 49 | netClient = networkClient; 50 | setEnabled(true); 51 | } 52 | 53 | 54 | // settings 55 | 56 | protected void setBackground() { 57 | if (settings.getBoolean(SettingsActivity.KEY_DARK_CANVAS, false)) 58 | setBackgroundColor(Color.BLACK); 59 | else 60 | setBackgroundResource(R.drawable.bg_grid_pattern); 61 | } 62 | 63 | protected void setInputMethods() { 64 | acceptStylusOnly = settings.getBoolean(SettingsActivity.KEY_PREF_STYLUS_ONLY, false); 65 | } 66 | 67 | @Override 68 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 69 | switch (key) { 70 | case SettingsActivity.KEY_PREF_STYLUS_ONLY: 71 | setInputMethods(); 72 | break; 73 | case SettingsActivity.KEY_DARK_CANVAS: 74 | setBackground(); 75 | break; 76 | } 77 | } 78 | 79 | 80 | // drawing 81 | 82 | @Override 83 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 84 | Log.i(TAG, "Canvas size changed: " + w + "x" + h + " (before: " + oldw + "x" + oldh + ")"); 85 | maxX = w; 86 | maxY = h; 87 | } 88 | 89 | @Override 90 | public boolean onGenericMotionEvent(MotionEvent event) { 91 | if (isEnabled()) { 92 | for (int ptr = 0; ptr < event.getPointerCount(); ptr++) 93 | if (!acceptStylusOnly || (event.getToolType(ptr) == MotionEvent.TOOL_TYPE_STYLUS)) { 94 | short nx = normalizeX(event.getX(ptr)), 95 | ny = normalizeY(event.getY(ptr)), 96 | npressure = normalizePressure(event.getPressure(ptr)); 97 | Log.v(TAG, String.format("Generic motion event logged: %f|%f, pressure %f", event.getX(ptr), event.getY(ptr), event.getPressure(ptr))); 98 | switch (event.getActionMasked()) { 99 | case MotionEvent.ACTION_HOVER_MOVE: 100 | netClient.getQueue().add(new NetEvent(Type.TYPE_MOTION, nx, ny, npressure)); 101 | break; 102 | case MotionEvent.ACTION_HOVER_ENTER: 103 | inRangeStatus = InRangeStatus.InRange; 104 | netClient.getQueue().add(new NetEvent(Type.TYPE_BUTTON, nx, ny, npressure, -1, true)); 105 | break; 106 | case MotionEvent.ACTION_HOVER_EXIT: 107 | inRangeStatus = InRangeStatus.OutOfRange; 108 | netClient.getQueue().add(new NetEvent(Type.TYPE_BUTTON, nx, ny, npressure, -1, false)); 109 | break; 110 | } 111 | } 112 | return true; 113 | } 114 | return false; 115 | } 116 | 117 | @Override 118 | public boolean onTouchEvent(@NonNull MotionEvent event) { 119 | if (isEnabled()) { 120 | for (int ptr = 0; ptr < event.getPointerCount(); ptr++) 121 | if (!acceptStylusOnly || (event.getToolType(ptr) == MotionEvent.TOOL_TYPE_STYLUS)) { 122 | short nx = normalizeX(event.getX(ptr)), 123 | ny = normalizeY(event.getY(ptr)), 124 | npressure = normalizePressure(event.getPressure(ptr)); 125 | Log.v(TAG, String.format("Touch event logged: action %d @ %f|%f (pressure %f)", event.getActionMasked(), event.getX(ptr), event.getY(ptr), event.getPressure(ptr))); 126 | switch (event.getActionMasked()) { 127 | case MotionEvent.ACTION_MOVE: 128 | netClient.getQueue().add(new NetEvent(Type.TYPE_MOTION, nx, ny, npressure)); 129 | break; 130 | case MotionEvent.ACTION_DOWN: 131 | if (inRangeStatus == inRangeStatus.OutOfRange) { 132 | inRangeStatus = inRangeStatus.FakeInRange; 133 | netClient.getQueue().add(new NetEvent(Type.TYPE_BUTTON, nx, ny, (short)0, -1, true)); 134 | } 135 | netClient.getQueue().add(new NetEvent(Type.TYPE_BUTTON, nx, ny, npressure, 0, true)); 136 | break; 137 | case MotionEvent.ACTION_UP: 138 | case MotionEvent.ACTION_CANCEL: 139 | netClient.getQueue().add(new NetEvent(Type.TYPE_BUTTON, nx, ny, npressure, 0, false)); 140 | if (inRangeStatus == inRangeStatus.FakeInRange) { 141 | inRangeStatus = inRangeStatus.OutOfRange; 142 | netClient.getQueue().add(new NetEvent(Type.TYPE_BUTTON, nx, ny, (short)0, -1, false)); 143 | } 144 | break; 145 | } 146 | 147 | } 148 | return true; 149 | } 150 | return false; 151 | } 152 | 153 | // these overflow and wrap around to negative short values, but thankfully Java will continue 154 | // on regardless, so we can just ignore Java's interpretation of them and send them anyway. 155 | short normalizeX(float x) { 156 | return (short)(Math.min(Math.max(0, x), maxX) * 2*Short.MAX_VALUE/maxX); 157 | } 158 | 159 | short normalizeY(float x) { 160 | return (short)(Math.min(Math.max(0, x), maxY) * 2*Short.MAX_VALUE/maxY); 161 | } 162 | 163 | short normalizePressure(float x) { 164 | return (short)(Math.min(Math.max(0, x), 2.0) * Short.MAX_VALUE); 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /app-android/app/src/main/java/at/bitfire/gfxtablet/NetEvent.java: -------------------------------------------------------------------------------- 1 | package at.bitfire.gfxtablet; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.DataOutputStream; 7 | import java.io.IOException; 8 | 9 | public class NetEvent { 10 | enum Type { 11 | TYPE_MOTION, 12 | TYPE_BUTTON, 13 | 14 | // not specified in protocol, only needed to shut down network thread 15 | TYPE_DISCONNECT 16 | } 17 | static final String signature = "GfxTablet"; 18 | static final short protocol_version = 2; 19 | 20 | final Type type; 21 | short x, y, pressure; 22 | byte button, button_down; 23 | 24 | 25 | public NetEvent(Type type) { 26 | this.type = type; 27 | } 28 | 29 | public NetEvent(Type type, short x, short y, short pressure) { 30 | this.type = type; 31 | this.x = x; 32 | this.y = y; 33 | this.pressure = pressure; 34 | } 35 | 36 | public NetEvent(Type type, short x, short y, short pressure, int button, boolean button_down) { 37 | this(type, x, y, pressure); 38 | this.button = (byte)button; 39 | this.button_down = (byte)(button_down ? 1 : 0); 40 | } 41 | 42 | public byte[] toByteArray() { 43 | if (type == Type.TYPE_DISCONNECT) 44 | return null; 45 | 46 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 47 | DataOutputStream dos = new DataOutputStream(baos); 48 | 49 | try { 50 | dos.writeBytes(signature); 51 | dos.writeShort(protocol_version); 52 | 53 | switch (type) { 54 | case TYPE_MOTION: 55 | dos.writeByte(0); 56 | break; 57 | case TYPE_BUTTON: 58 | dos.writeByte(1); 59 | break; 60 | default: 61 | } 62 | 63 | dos.writeShort(x); 64 | dos.writeShort(y); 65 | dos.writeShort(pressure); 66 | 67 | if (type == Type.TYPE_BUTTON) { 68 | dos.writeByte(button); 69 | dos.writeByte(button_down); 70 | } 71 | } catch(IOException e) { 72 | Log.wtf(signature, "Couldn't generate network packet"); 73 | } 74 | 75 | return baos.toByteArray(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app-android/app/src/main/java/at/bitfire/gfxtablet/NetworkClient.java: -------------------------------------------------------------------------------- 1 | package at.bitfire.gfxtablet; 2 | 3 | import android.content.SharedPreferences; 4 | import android.util.Log; 5 | 6 | import java.net.DatagramPacket; 7 | import java.net.DatagramSocket; 8 | import java.net.InetAddress; 9 | import java.net.UnknownHostException; 10 | import java.util.concurrent.LinkedBlockingQueue; 11 | 12 | import at.bitfire.gfxtablet.NetEvent.Type; 13 | 14 | 15 | public class NetworkClient implements Runnable { 16 | static final int GFXTABLET_PORT = 40118; 17 | 18 | final LinkedBlockingQueue motionQueue = new LinkedBlockingQueue<>(); 19 | LinkedBlockingQueue getQueue() { return motionQueue; } 20 | 21 | InetAddress destAddress; 22 | final SharedPreferences preferences; 23 | 24 | NetworkClient(SharedPreferences preferences) { 25 | this.preferences = preferences; 26 | } 27 | 28 | boolean reconfigureNetworking() { 29 | try { 30 | String hostName = preferences.getString(SettingsActivity.KEY_PREF_HOST, "unknown.invalid"); 31 | destAddress = InetAddress.getByName(hostName); 32 | } catch (UnknownHostException e) { 33 | destAddress = null; 34 | return false; 35 | } 36 | return true; 37 | } 38 | 39 | @Override 40 | public void run() { 41 | try { 42 | DatagramSocket socket = new DatagramSocket(); 43 | 44 | while (true) { 45 | NetEvent event = motionQueue.take(); 46 | 47 | // graceful shutdown 48 | if (event.type == Type.TYPE_DISCONNECT) 49 | break; 50 | 51 | if (destAddress == null) // no valid destination host 52 | continue; 53 | 54 | byte[] data = event.toByteArray(); 55 | DatagramPacket pkt = new DatagramPacket(data, data.length, destAddress, GFXTABLET_PORT); 56 | socket.send(pkt); 57 | } 58 | } catch (Exception e) { 59 | Log.e("GfxTablet", "motionQueue failed: " + e.getMessage()); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app-android/app/src/main/java/at/bitfire/gfxtablet/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package at.bitfire.gfxtablet; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.ActionBarActivity; 5 | import android.support.v7.app.AppCompatActivity; 6 | 7 | public class SettingsActivity extends AppCompatActivity { 8 | public static final String 9 | KEY_PREF_HOST = "host_preference", 10 | KEY_PREF_STYLUS_ONLY = "stylus_only_preference", 11 | KEY_DARK_CANVAS = "dark_canvas_preference", 12 | KEY_KEEP_DISPLAY_ACTIVE = "keep_display_active_preference", 13 | KEY_TEMPLATE_IMAGE = "key_template_image"; 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | 19 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 20 | 21 | setContentView(R.layout.activity_settings); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app-android/app/src/main/java/at/bitfire/gfxtablet/SettingsFragment.java: -------------------------------------------------------------------------------- 1 | package at.bitfire.gfxtablet; 2 | 3 | import android.os.Bundle; 4 | import android.preference.PreferenceFragment; 5 | 6 | public class SettingsFragment extends PreferenceFragment { 7 | @Override 8 | public void onCreate(Bundle savedInstanceState) { 9 | super.onCreate(savedInstanceState); 10 | addPreferencesFromResource(R.xml.preferences); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable-hdpi/ic_action_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable-hdpi/ic_action_picture.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable-hdpi/ic_arrow_expand_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable-hdpi/ic_arrow_expand_white_48dp.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable-hdpi/ic_settings_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable-hdpi/ic_settings_white_48dp.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable-mdpi/ic_action_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable-mdpi/ic_action_picture.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable-mdpi/ic_arrow_expand_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable-mdpi/ic_arrow_expand_white_48dp.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable-mdpi/ic_settings_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable-mdpi/ic_settings_white_48dp.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable-xhdpi/ic_action_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable-xhdpi/ic_action_picture.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable-xhdpi/ic_arrow_expand_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable-xhdpi/ic_arrow_expand_white_48dp.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable-xhdpi/ic_settings_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable-xhdpi/ic_settings_white_48dp.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable-xxhdpi/ic_action_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable-xxhdpi/ic_action_picture.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable-xxhdpi/ic_arrow_expand_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable-xxhdpi/ic_arrow_expand_white_48dp.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable-xxhdpi/ic_settings_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable-xxhdpi/ic_settings_white_48dp.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable/bg_grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/app/src/main/res/drawable/bg_grid.png -------------------------------------------------------------------------------- /app-android/app/src/main/res/drawable/bg_grid_pattern.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /app-android/app/src/main/res/layout/activity_canvas.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 18 | 19 | 28 | 29 | -------------------------------------------------------------------------------- /app-android/app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | -------------------------------------------------------------------------------- /app-android/app/src/main/res/menu/activity_canvas.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | 18 | 19 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /app-android/app/src/main/res/menu/set_template_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /app-android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GfxTablet 5 | Settings 6 | About / Help 7 | Donate 8 | 9 | Full-screen mode 10 | Set template image 11 | Clear template image 12 | Select another image 13 | 14 | No valid recipient host defined. Please configure in \"Settings / Recipient host\". 15 | 16 | Recipient host 17 | Host name or IP address where touch input is sent to 18 | Sense stylus only 19 | Only stylus input will be processed 20 | Finger and stylus input will be processed 21 | Use dark canvas 22 | Black canvas will be used (covers template image) 23 | White/transparent gridded canvas will be used 24 | Keep display active 25 | Display won\'t turn off while GfxTablet is active 26 | Display will turn off according to system settings 27 | 28 | 29 | -------------------------------------------------------------------------------- /app-android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 11 | 12 | -------------------------------------------------------------------------------- /app-android/app/src/main/res/xml/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 19 | 20 | 26 | 27 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app-android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:1.2.3' 8 | } 9 | } 10 | allprojects { 11 | repositories { 12 | jcenter() 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /app-android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfc2822/GfxTablet/7bd5181d53384572724793a24ade469f22ca4fcd/app-android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app-android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /app-android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /app-android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app-android/import-summary.txt: -------------------------------------------------------------------------------- 1 | ECLIPSE ANDROID PROJECT IMPORT SUMMARY 2 | ====================================== 3 | 4 | Ignored Files: 5 | -------------- 6 | The following files were *not* copied into the new Gradle project; you 7 | should evaluate whether these are still needed in your project and if 8 | so manually move them: 9 | 10 | * IDEAS 11 | * build.xml 12 | * proguard-project.txt 13 | 14 | Moved Files: 15 | ------------ 16 | Android Gradle projects use a different directory structure than ADT 17 | Eclipse projects. Here's how the projects were restructured: 18 | 19 | * AndroidManifest.xml => app/src/main/AndroidManifest.xml 20 | * res/ => app/src/main/res/ 21 | * src/ => app/src/main/java/ 22 | 23 | Next Steps: 24 | ----------- 25 | You can now build the project. The Gradle project needs network 26 | connectivity to download dependencies. 27 | 28 | Bugs: 29 | ----- 30 | If for some reason your project does not build, and you determine that 31 | it is due to a bug or limitation of the Eclipse to Gradle importer, 32 | please file a bug at http://b.android.com with category 33 | Component-Tools. 34 | 35 | (This import summary is for your information only, and can be deleted 36 | after import once you are satisfied with the results.) 37 | -------------------------------------------------------------------------------- /app-android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /doc/protocol.txt: -------------------------------------------------------------------------------- 1 | Network protocol used by GfxTablet 2 | 3 | 4 | 5 | Version 2 6 | --------- 7 | 8 | GfxTablet app sends UDP packets to port 40118 of the destination host. 9 | 10 | Packet structure, uses network byte order (big endian): 11 | 12 | 9 bytes "GfxTablet" 13 | 1 unsigned int16 version number 14 | 1 byte event type: 15 | 0: motion event (hovering) 16 | 1: button event (finger, pen etc. touches surface) 17 | 18 | 1 unsigned int16 x (using full range: 0..65535) 19 | 1 unsigned int16 y (using full range: 0..65535) 20 | 1 unsigned int16 pressure (accepting full range 0..65535, but will clip to 32768 == pressure 1.0f on Android device) 21 | 22 | when type == button event: 23 | 1 signed int8 button id: 24 | -1: stylus in range pseudo-button 25 | 0: left click / stylus in contact / button 0 26 | 1: extra button 1 27 | 2: extra button 2 28 | 1 byte button status: 29 | 0 button is released ("up") 30 | 1 button is pressed ("down") 31 | 32 | XInput will ignore BTN_TOUCH events if they are not preceeded by 33 | a BTN_TOOL_PEN event -- this would never happen for a real stylus, 34 | because it would imply the stylus is touching the pad, yet too far 35 | away from the pad to be detected. 36 | 37 | A GfxTablet client must therefore be careful to send a "Button -1 38 | down" event before a "Button 0 down" event, to emulate this 39 | behaviour. If they are faking these events, they'll probably want 40 | to likewise send a "Button -1 up" event after a "Button 0 up" event. 41 | -------------------------------------------------------------------------------- /driver-uinput/.gitignore: -------------------------------------------------------------------------------- 1 | networktablet 2 | -------------------------------------------------------------------------------- /driver-uinput/Makefile: -------------------------------------------------------------------------------- 1 | TARGET = networktablet 2 | PREFIX = /usr/local 3 | BINDIR = $(PREFIX)/bin 4 | 5 | networktablet : networktablet.c 6 | 7 | clean : 8 | rm -f networktablet 9 | 10 | install : 11 | install -D networktablet $(DESTDIR)$(BINDIR)/networktablet 12 | -------------------------------------------------------------------------------- /driver-uinput/networktablet.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "protocol.h" 15 | 16 | #define die(str, args...) { \ 17 | perror(str); \ 18 | exit(EXIT_FAILURE); \ 19 | } 20 | 21 | 22 | int udp_socket; 23 | 24 | 25 | void init_device(int fd) 26 | { 27 | // enable synchronization 28 | if (ioctl(fd, UI_SET_EVBIT, EV_SYN) < 0) 29 | die("error: ioctl UI_SET_EVBIT EV_SYN"); 30 | 31 | // enable 1 button 32 | if (ioctl(fd, UI_SET_EVBIT, EV_KEY) < 0) 33 | die("error: ioctl UI_SET_EVBIT EV_KEY"); 34 | if (ioctl(fd, UI_SET_KEYBIT, BTN_TOUCH) < 0) 35 | die("error: ioctl UI_SET_KEYBIT"); 36 | if (ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_PEN) < 0) 37 | die("error: ioctl UI_SET_KEYBIT"); 38 | if (ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS) < 0) 39 | die("error: ioctl UI_SET_KEYBIT"); 40 | if (ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS2) < 0) 41 | die("error: ioctl UI_SET_KEYBIT"); 42 | 43 | // enable 2 main axes + pressure (absolute positioning) 44 | if (ioctl(fd, UI_SET_EVBIT, EV_ABS) < 0) 45 | die("error: ioctl UI_SET_EVBIT EV_ABS"); 46 | if (ioctl(fd, UI_SET_ABSBIT, ABS_X) < 0) 47 | die("error: ioctl UI_SETEVBIT ABS_X"); 48 | if (ioctl(fd, UI_SET_ABSBIT, ABS_Y) < 0) 49 | die("error: ioctl UI_SETEVBIT ABS_Y"); 50 | if (ioctl(fd, UI_SET_ABSBIT, ABS_PRESSURE) < 0) 51 | die("error: ioctl UI_SETEVBIT ABS_PRESSURE"); 52 | 53 | { 54 | struct uinput_abs_setup abs_setup; 55 | struct uinput_setup setup; 56 | 57 | memset(&abs_setup, 0, sizeof(abs_setup)); 58 | abs_setup.code = ABS_X; 59 | abs_setup.absinfo.value = 0; 60 | abs_setup.absinfo.minimum = 0; 61 | abs_setup.absinfo.maximum = UINT16_MAX; 62 | abs_setup.absinfo.fuzz = 0; 63 | abs_setup.absinfo.flat = 0; 64 | abs_setup.absinfo.resolution = 400; 65 | if (ioctl(fd, UI_ABS_SETUP, &abs_setup) < 0) 66 | die("error: UI_ABS_SETUP ABS_X"); 67 | 68 | memset(&abs_setup, 0, sizeof(abs_setup)); 69 | abs_setup.code = ABS_Y; 70 | abs_setup.absinfo.value = 0; 71 | abs_setup.absinfo.minimum = 0; 72 | abs_setup.absinfo.maximum = UINT16_MAX; 73 | abs_setup.absinfo.fuzz = 0; 74 | abs_setup.absinfo.flat = 0; 75 | abs_setup.absinfo.resolution = 400; 76 | if (ioctl(fd, UI_ABS_SETUP, &abs_setup) < 0) 77 | die("error: UI_ABS_SETUP ABS_Y"); 78 | 79 | memset(&abs_setup, 0, sizeof(abs_setup)); 80 | abs_setup.code = ABS_PRESSURE; 81 | abs_setup.absinfo.value = 0; 82 | abs_setup.absinfo.minimum = 0; 83 | abs_setup.absinfo.maximum = INT16_MAX; 84 | abs_setup.absinfo.fuzz = 0; 85 | abs_setup.absinfo.flat = 0; 86 | abs_setup.absinfo.resolution = 0; 87 | if (ioctl(fd, UI_ABS_SETUP, &abs_setup) < 0) 88 | die("error: UI_ABS_SETUP ABS_PRESSURE"); 89 | 90 | memset(&setup, 0, sizeof(setup)); 91 | snprintf(setup.name, UINPUT_MAX_NAME_SIZE, "Network Tablet"); 92 | setup.id.bustype = BUS_VIRTUAL; 93 | setup.id.vendor = 0x1; 94 | setup.id.product = 0x1; 95 | setup.id.version = 2; 96 | setup.ff_effects_max = 0; 97 | if (ioctl(fd, UI_DEV_SETUP, &setup) < 0) 98 | die("error: UI_DEV_SETUP"); 99 | 100 | if (ioctl(fd, UI_DEV_CREATE) < 0) 101 | die("error: ioctl"); 102 | } 103 | } 104 | 105 | int prepare_socket() 106 | { 107 | int s; 108 | struct sockaddr_in addr; 109 | 110 | if ((s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -1) 111 | die("error: prepare_socket()"); 112 | 113 | bzero(&addr, sizeof(struct sockaddr_in)); 114 | addr.sin_family = AF_INET; 115 | addr.sin_port = htons(GFXTABLET_PORT); 116 | addr.sin_addr.s_addr = htonl(INADDR_ANY); 117 | 118 | if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) == -1) 119 | die("error: prepare_socket()"); 120 | 121 | return s; 122 | } 123 | 124 | void send_event(int device, int type, int code, int value) 125 | { 126 | struct input_event ev; 127 | ev.type = type; 128 | ev.code = code; 129 | ev.value = value; 130 | if (write(device, &ev, sizeof(ev)) < 0) 131 | die("error: write()"); 132 | } 133 | 134 | void quit(int signal) { 135 | close(udp_socket); 136 | } 137 | 138 | 139 | int main(void) 140 | { 141 | int device; 142 | struct event_packet ev_pkt; 143 | 144 | if ((device = open("/dev/uinput", O_WRONLY | O_NONBLOCK)) < 0) 145 | die("error: open"); 146 | 147 | init_device(device); 148 | udp_socket = prepare_socket(); 149 | 150 | printf("GfxTablet driver (protocol version %u) is ready and listening on 0.0.0.0:%u (UDP)\n" 151 | "Hint: Make sure that this port is not blocked by your firewall.\n", PROTOCOL_VERSION, GFXTABLET_PORT); 152 | 153 | signal(SIGINT, quit); 154 | signal(SIGTERM, quit); 155 | 156 | while (recv(udp_socket, &ev_pkt, sizeof(ev_pkt), 0) >= 9) { // every packet has at least 9 bytes 157 | printf("."); fflush(0); 158 | 159 | if (memcmp(ev_pkt.signature, "GfxTablet", 9) != 0) { 160 | fprintf(stderr, "\nGot unknown packet on port %i, ignoring\n", GFXTABLET_PORT); 161 | continue; 162 | } 163 | ev_pkt.version = ntohs(ev_pkt.version); 164 | if (ev_pkt.version != PROTOCOL_VERSION) { 165 | fprintf(stderr, "\nGfxTablet app speaks protocol version %i but driver speaks version %i, please update\n", 166 | ev_pkt.version, PROTOCOL_VERSION); 167 | break; 168 | } 169 | 170 | ev_pkt.x = ntohs(ev_pkt.x); 171 | ev_pkt.y = ntohs(ev_pkt.y); 172 | ev_pkt.pressure = ntohs(ev_pkt.pressure); 173 | printf("x: %hu, y: %hu, pressure: %hu\n", ev_pkt.x, ev_pkt.y, ev_pkt.pressure); 174 | 175 | send_event(device, EV_ABS, ABS_X, ev_pkt.x); 176 | send_event(device, EV_ABS, ABS_Y, ev_pkt.y); 177 | send_event(device, EV_ABS, ABS_PRESSURE, ev_pkt.pressure); 178 | 179 | switch (ev_pkt.type) { 180 | case EVENT_TYPE_MOTION: 181 | send_event(device, EV_SYN, SYN_REPORT, 1); 182 | break; 183 | case EVENT_TYPE_BUTTON: 184 | // stylus hovering 185 | if (ev_pkt.button == -1) 186 | send_event(device, EV_KEY, BTN_TOOL_PEN, ev_pkt.down); 187 | // stylus touching 188 | if (ev_pkt.button == 0) 189 | send_event(device, EV_KEY, BTN_TOUCH, ev_pkt.down); 190 | // button 1 191 | if (ev_pkt.button == 1) 192 | send_event(device, EV_KEY, BTN_STYLUS, ev_pkt.down); 193 | // button 2 194 | if (ev_pkt.button == 2) 195 | send_event(device, EV_KEY, BTN_STYLUS2, ev_pkt.down); 196 | printf("sent button: %hhi, %hhu\n", ev_pkt.button, ev_pkt.down); 197 | send_event(device, EV_SYN, SYN_REPORT, 1); 198 | break; 199 | 200 | } 201 | } 202 | close(udp_socket); 203 | 204 | printf("Removing network tablet from device list\n"); 205 | ioctl(device, UI_DEV_DESTROY); 206 | close(device); 207 | 208 | printf("GfxTablet driver shut down gracefully\n"); 209 | return 0; 210 | } 211 | -------------------------------------------------------------------------------- /driver-uinput/protocol.h: -------------------------------------------------------------------------------- 1 | #define GFXTABLET_PORT 40118 2 | 3 | #define PROTOCOL_VERSION 2 4 | 5 | 6 | #pragma pack(push) 7 | #pragma pack(1) 8 | 9 | #define EVENT_TYPE_MOTION 0 10 | #define EVENT_TYPE_BUTTON 1 11 | 12 | struct event_packet 13 | { 14 | char signature[9]; 15 | uint16_t version; 16 | uint8_t type; /* EVENT_TYPE_... */ 17 | struct { /* required */ 18 | uint16_t x, y; 19 | uint16_t pressure; 20 | }; 21 | 22 | struct { /* only required for EVENT_TYPE_BUTTON */ 23 | int8_t button; /* button id: 24 | -1 = stylus in range, 25 | 0 = tap/left click/button 0, 26 | 1 = button 1, 27 | 2 = button 2 */ 28 | int8_t down; /* 1 = button down, 0 = button up */ 29 | }; 30 | }; 31 | 32 | #pragma pack(pop) 33 | --------------------------------------------------------------------------------