-
-
Save freakboy3742/7beb22c587e57240610777a44af645d8 to your computer and use it in GitHub Desktop.
| import android | |
| from android.app import AlertDialog | |
| from android.graphics import Color | |
| from android.graphics.drawable import ColorDrawable | |
| from android.os import AsyncTask | |
| from android.os import Looper | |
| from android.os import Handler | |
| from android.util import TypedValue | |
| from android.view import View | |
| from android.view import MenuItem | |
| from android.widget import LinearLayout | |
| from android.widget import ArrayAdapter | |
| from android.widget import ImageView | |
| from android.widget import TextView | |
| from android.widget import EditText | |
| from android.support.v4.widget import SwipeRefreshLayout | |
| from android.util import Log | |
| from java.io import BufferedInputStream, BufferedReader, InputStreamReader, OutputStreamWriter | |
| from java.net import URL | |
| from org.json import JSONArray, JSONObject | |
| from com.baoyz.swipemenulistview import SwipeMenuListView, SwipeMenuCreator, SwipeMenuItem | |
| class RefreshTask(implements=java.lang.Runnable): | |
| def __init__(self, adapter): | |
| self.adapter = adapter | |
| def run(self) -> void: | |
| Log.i("TESTAPP", "BACKGROUND REFRESH DONE TASK") | |
| self.adapter.notifyDataSetChanged() | |
| Log.i("TESTAPP", "BACKGROUND REFRESH DONE TASK DONE") | |
| class UpdateDataTask(implements=java.lang.Runnable): | |
| def __init__(self, adapter): | |
| self.adapter = adapter | |
| def run(self) -> void: | |
| url = URL("http://freakboy3742.pythonanywhere.com/api/todo/?format=json") | |
| connection = url.openConnection() | |
| Log.i("TESTAPP", "GET RESPONSE: %s" % connection.getResponseCode()) | |
| reader = BufferedReader(InputStreamReader(connection.getInputStream())) | |
| content = "" | |
| line = reader.readLine() | |
| while line: | |
| content = content + '\n' + line | |
| line = reader.readLine() | |
| json = JSONArray(content) | |
| self.adapter.data = [] | |
| for i in range(0, json.length()): | |
| obj = json.get(i) | |
| self.adapter.data.append(obj) | |
| handler = Handler(Looper.getMainLooper()) | |
| handler.post(RefreshTask(self.adapter)) | |
| connection.disconnect() | |
| class DeleteItemTask(implements=java.lang.Runnable): | |
| def __init__(self, adapter, index): | |
| self.adapter = adapter | |
| self.index = index | |
| def run(self) -> void: | |
| Log.i("TESTAPP", "DELETE ITEM %s" % self.index) | |
| url = URL("http://freakboy3742.pythonanywhere.com/api/todo/%s/" % self.adapter.data[self.index].get('id')) | |
| Log.i("TESTAPP", "DELETE URL %s" % url) | |
| connection = url.openConnection() | |
| connection.setRequestMethod("DELETE") | |
| Log.i("TESTAPP", "DELETE RESPONSE: %s" % connection.getResponseCode()) | |
| del self.adapter.data[self.index] | |
| handler = Handler(Looper.getMainLooper()) | |
| handler.post(RefreshTask(self.adapter)) | |
| class AddItemTask(implements=java.lang.Runnable): | |
| def __init__(self, adapter, description): | |
| self.adapter = adapter | |
| self.description = description | |
| def run(self) -> void: | |
| url = URL("http://freakboy3742.pythonanywhere.com/api/todo/") | |
| connection = url.openConnection() | |
| connection.setRequestMethod("POST") | |
| connection.setDoOutput(True) | |
| connection.setRequestProperty("Content-Type", "application/json") | |
| writer = OutputStreamWriter(connection.getOutputStream()) | |
| writer.write('{"description": "%s", "completed": false}' % self.description) | |
| writer.flush() | |
| # writer.close() | |
| Log.i("TESTAPP", "PUT RESPONSE: %s %s" % (connection.getResponseCode(), connection.getResponseMessage())) | |
| reader = BufferedReader(InputStreamReader(connection.getInputStream())) | |
| content = "" | |
| line = reader.readLine() | |
| while line: | |
| content = content + '\n' + line | |
| line = reader.readLine() | |
| self.adapter.data.append(JSONObject(content)) | |
| handler = Handler(Looper.getMainLooper()) | |
| handler.post(RefreshTask(self.adapter)) | |
| connection.disconnect() | |
| class DataAdapter(extends=com.baoyz.swipemenulistview.BaseSwipeListAdapter): | |
| def __init__(self, context): | |
| Log.i("TESTAPP", "INIT DATA ADAPTER " + str(data)) | |
| self.context = context | |
| self.updateData() | |
| def updateData(self): | |
| task = UpdateDataTask(self) | |
| AsyncTask.execute(task) | |
| def deleteItem(self, item): | |
| task = DeleteItemTask(self, item) | |
| AsyncTask.execute(task) | |
| def addItem(self, description): | |
| task = AddItemTask(self, description) | |
| AsyncTask.execute(task) | |
| def getCount(self) -> int: | |
| # Log.i("TESTAPP", "GET COUNT") | |
| # Log.i("TESTAPP", "DATA SIZE of " + str(self.data)) | |
| # Log.i("TESTAPP", "DATA SIZE " + str(len(self.data))) | |
| return len(self.data) | |
| def getItem(self, position: int) -> java.lang.String: | |
| # Log.i("TESTAPP", "GET ITEM " + str(position)) | |
| return self.data[position] | |
| def getItemId(self, position: int) -> long: | |
| # Log.i("TESTAPP", "GET ITEM ID " + str(position)) | |
| return position | |
| def getView(self, position: int, convertView: android.view.View, parent: android.view.ViewGroup) -> android.view.View: | |
| if convertView is None: | |
| convertView = View.inflate(self.context, android.R.layout.simple_list_item_1, None) | |
| item = self.getItem(position) | |
| convertView.setText(item.get('description')) | |
| return convertView | |
| class RefreshListener(implements=android.support.v4.widget.SwipeRefreshLayout[OnRefreshListener]): | |
| def __init__(self, layout: android.support.v4.widget.SwipeRefreshLayout, adapter): | |
| self.layout = layout | |
| self.adapter = adapter | |
| def onRefresh(self) -> void: | |
| Log.i("TESTAPP", "REFRESH!!") | |
| self.adapter.updateData() | |
| self.layout.setRefreshing(False) | |
| class ListSwipeMenuCreator(implements=com.baoyz.swipemenulistview.SwipeMenuCreator): | |
| def __init__(self, context): | |
| self.context = context | |
| def create(self, menu: com.baoyz.swipemenulistview.SwipeMenu) -> void: | |
| # # Create "Open" item | |
| # openItem = SwipeMenuItem(self.context) | |
| # openItem.setBackground(ColorDrawable(Color.rgb(0xC9, 0xC9, 0xCE))) | |
| # openItem.setWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 90, self.context.getResources().getDisplayMetrics())) | |
| # openItem.setTitle("Open") | |
| # openItem.setTitleSize(18) | |
| # openItem.setTitleColor(Color.WHITE) | |
| # menu.addMenuItem(openItem) | |
| # create "delete" item | |
| deleteItem = SwipeMenuItem(self.context) | |
| deleteItem.setBackground(ColorDrawable(Color.rgb(0xF9, 0x3F, 0x25))) | |
| deleteItem.setWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 90, self.context.getResources().getDisplayMetrics())) | |
| deleteItem.setIcon(android.R.drawable.ic_delete) | |
| menu.addMenuItem(deleteItem) | |
| class ListMenuItemClickListener(implements=com.baoyz.swipemenulistview.SwipeMenuListView[OnMenuItemClickListener]): | |
| def __init__(self, listview, adapter): | |
| self.listview = listview | |
| self.adapter = adapter | |
| def onMenuItemClick(self, position: int, menu: com.baoyz.swipemenulistview.SwipeMenu, index: int) -> bool: | |
| Log.i("TESTAPP", "CLICK MENU ITEM %s, action %s" % (position, index)) | |
| # if index == 0: | |
| # Log.i("TESTAPP", "OPEN item %s" % position) | |
| # self.adapter.addItem('New item') | |
| # elif index == 1: | |
| Log.i("TESTAPP", "DELETE item %s" % position) | |
| self.adapter.deleteItem(position) | |
| return True | |
| class DialogOKClickListener(implements=android.content.DialogInterface[OnClickListener]): | |
| def __init__(self, input_field, adapter): | |
| self.input_field = input_field | |
| self.adapter = adapter | |
| def onClick(self, dialog: android.content.DialogInterface, id: int) -> void: | |
| Log.i("TESTAPP", "User input: %s" % self.input_field.getText()) | |
| self.adapter.addItem(self.input_field.getText()) | |
| class DialogCancelClickListener(implements=android.content.DialogInterface[OnClickListener]): | |
| def onClick(self, dialog: android.content.DialogInterface, id: int) -> void: | |
| Log.i("TESTAPP", "Cancel dialog") | |
| dialog.cancel() | |
| class MainActivity(extends=android.support.v4.app.FragmentActivity): | |
| # /** Called when the activity is first created. */ | |
| def onCreate(self, savedInstanceState: android.os.Bundle) -> void: | |
| super().onCreate(savedInstanceState) | |
| Log.i("TESTAPP", "CREATE APP") | |
| adapter = DataAdapter(self) | |
| adapter.data = [] | |
| adapter.context = self | |
| adapter.updateData() | |
| self.adapter = adapter | |
| listview = SwipeMenuListView(self) | |
| listview.setSwipeDirection(SwipeMenuListView.DIRECTION_LEFT) | |
| layout = SwipeRefreshLayout(self) | |
| layout.setLayoutParams( | |
| LinearLayout.LayoutParams( | |
| LinearLayout.LayoutParams.MATCH_PARENT, | |
| LinearLayout.LayoutParams.WRAP_CONTENT | |
| ) | |
| ) | |
| listener = RefreshListener(layout, adapter) | |
| layout.setOnRefreshListener(listener) | |
| listview.setAdapter(adapter) | |
| listview.setMenuCreator(ListSwipeMenuCreator(self)) | |
| listview.setOnMenuItemClickListener(ListMenuItemClickListener(listview, adapter)) | |
| layout.addView(listview) | |
| layout.setId(1234) | |
| self.layout_id = layout.getId() | |
| self.setContentView(layout) | |
| def onCreateOptionsMenu(self, menu: android.view.Menu) -> bool: | |
| Log.i("TESTAPP", "CREATE OPTIONS MENU %s" % menu) | |
| item = menu.add(0, 42, 0, 'Add Item') | |
| # item.setIcon(android.R.drawable.ic_input_add) | |
| item.setIcon(android.R.drawable.ic_menu_add) | |
| item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) | |
| return True | |
| def onOptionsItemSelected(self, item: android.view.MenuItem) -> bool: | |
| item_id = item.getItemId() | |
| if item_id == 42: | |
| alertDialogBuilder = AlertDialog.Builder(self) | |
| layout = LinearLayout(self) | |
| layout.setLayoutParams( | |
| LinearLayout.LayoutParams( | |
| LinearLayout.LayoutParams.MATCH_PARENT, | |
| LinearLayout.LayoutParams.MATCH_PARENT | |
| ) | |
| ) | |
| layout.setPadding( | |
| TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, self.getResources().getDisplayMetrics()), | |
| TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, self.getResources().getDisplayMetrics()), | |
| TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, self.getResources().getDisplayMetrics()), | |
| TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, self.getResources().getDisplayMetrics()) | |
| ) | |
| layout.setOrientation(LinearLayout.VERTICAL) | |
| input_field = EditText(self) | |
| input_field.setLayoutParams( | |
| LinearLayout.LayoutParams( | |
| LinearLayout.LayoutParams.MATCH_PARENT, | |
| LinearLayout.LayoutParams.WRAP_CONTENT | |
| ) | |
| ) | |
| input_field.setHint("What to do...") | |
| layout.addView(input_field) | |
| alertDialogBuilder.setView(layout) | |
| # set dialog message | |
| alertDialogBuilder.setCancelable(False) | |
| alertDialogBuilder.setPositiveButton("OK", DialogOKClickListener(input_field, self.adapter)) | |
| alertDialogBuilder.setNegativeButton("Cancel", DialogCancelClickListener()) | |
| # create alert dialog | |
| alertDialog = alertDialogBuilder.create() | |
| # show it | |
| alertDialog.show() | |
| return True |
| import toga | |
| class Example(toga.App): | |
| def startup(self): | |
| self.list = toga.List( | |
| widget_id='todo', | |
| source='todo-list', | |
| detail='todo-detail', | |
| item_class=toga.SimpleListElement, | |
| on_item_press=self.remove_entry | |
| ) | |
| self.input = toga.TextInput( | |
| widget_id='data', | |
| placeholder="new todo", | |
| flex=1, margin=5 | |
| ) | |
| container = toga.Container( | |
| self.list, | |
| toga.Container( | |
| self.input, | |
| toga.Button('Add', on_press=self.add_entry), | |
| flex_direction='row' | |
| ), | |
| flex_direction='column' | |
| ) | |
| self.main_window.title = "Toga demo" | |
| self.main_window.content = container | |
| def add_entry(self, widget): | |
| toga.post(self.list.create_url, { | |
| 'description': self.input.value(), | |
| 'completed': False, | |
| }) | |
| self.list.add(self.input.value()) | |
| self.input.clear() | |
| def remove_entry(self, widget): | |
| if widget.delete_url: | |
| toga.delete(widget.delete_url) | |
| widget.remove() | |
| else: | |
| dom.window.alert("Can't delete new item") |
| from http.client import HTTPConnection | |
| import json | |
| import random | |
| from rubicon.objc import objc_method | |
| from toga_iOS.libs import * | |
| class AddItemController(UIViewController): | |
| @objc_method | |
| def loadView(self) -> None: | |
| self.title = 'Add item' | |
| self.__dict__['cancelButton'] = UIBarButtonItem.alloc().initWithBarButtonSystemItem_target_action_( | |
| UIBarButtonSystemItemCancel, | |
| self, | |
| get_selector('cancelClicked') | |
| ) | |
| self.navigationController.navigationBar.topItem.leftBarButtonItem = self.__dict__['cancelButton'] | |
| self.__dict__['doneButton'] = UIBarButtonItem.alloc().initWithBarButtonSystemItem_target_action_( | |
| UIBarButtonSystemItemDone, | |
| self, | |
| get_selector('doneClicked') | |
| ) | |
| self.navigationController.navigationBar.topItem.rightBarButtonItem = self.__dict__['doneButton'] | |
| self.view = UIView.alloc().initWithFrame_(UIScreen.mainScreen().bounds) | |
| self.view.setBackgroundColor_(UIColor.whiteColor()) | |
| self.__dict__['input'] = UITextField.alloc().init() | |
| inputsize = self.__dict__['input'].systemLayoutSizeFittingSize_(CGSize(0, 0)) | |
| self.__dict__['input'].setFrame_(NSRect(NSPoint(10, 100), NSSize(300, inputsize.height + 14))) | |
| self.__dict__['input'].setBorderStyle_(UITextBorderStyleRoundedRect) | |
| self.__dict__['input'].setTranslatesAutoresizingMaskIntoConstraints_(False) | |
| self.__dict__['input'].setAutoresizesSubviews_(False) | |
| self.__dict__['input'].setPlaceholder_("what to do...") | |
| self.__dict__['input'].becomeFirstResponder() | |
| self.view.addSubview_(self.__dict__['input']) | |
| @objc_method | |
| def cancelClicked(self): | |
| self.dismissModalViewControllerAnimated_(True) | |
| @objc_method | |
| def doneClicked(self): | |
| self.dismissModalViewControllerAnimated_(True) | |
| new_item = self.__dict__['input'].text | |
| if new_item: | |
| conn = HTTPConnection('freakboy3742.pythonanywhere.com') | |
| conn.request( | |
| 'POST', '/api/todo/', | |
| body=json.dumps({ | |
| 'description': new_item, | |
| 'completed': False | |
| }).encode('utf-8'), | |
| headers={ | |
| 'Content-type': 'application/json', | |
| 'Content-encoding': 'utf-8', | |
| } | |
| ) | |
| r = conn.getresponse() | |
| data = json.loads(r.read().decode('utf8')) | |
| self.__dict__['tablecontroller'].__dict__['data'].append(data) | |
| self.__dict__['tablecontroller'].tableView.reloadData() | |
| class TableViewController(UITableViewController): | |
| @objc_method | |
| def numberOfSectionsInTableView_(self) -> int: | |
| return 1 | |
| @objc_method | |
| def tableView_numberOfRowsInSection_(self, tableView, section: int) -> int: | |
| return len(self.__dict__['data']) | |
| @objc_method | |
| def tableView_cellForRowAtIndexPath_(self, tableView, indexPath): | |
| cell = tableView.dequeueReusableCellWithIdentifier_("row") | |
| if cell is None: | |
| cell = UITableViewCell.alloc().initWithStyle_reuseIdentifier_(UITableViewCellStyleDefault, "row") | |
| cell.textLabel.text = self.__dict__['data'][indexPath.item]['description'] | |
| return cell | |
| @objc_method | |
| def tableView_commitEditingStyle_forRowAtIndexPath_(self, tableView, editingStyle: int, indexPath): | |
| if editingStyle == UITableViewCellEditingStyleDelete: | |
| item = self.__dict__['data'][indexPath.row] | |
| conn = HTTPConnection('freakboy3742.pythonanywhere.com') | |
| conn.request('DELETE', '/api/todo/%s/' % item['id']) | |
| del self.__dict__['data'][indexPath.row] | |
| paths = NSArray.alloc().initWithObjects_(indexPath, None) | |
| tableView.deleteRowsAtIndexPaths_withRowAnimation_(paths, UITableViewRowAnimationFade) | |
| @objc_method | |
| def refreshTable(self): | |
| conn = HTTPConnection('freakboy3742.pythonanywhere.com') | |
| conn.request('GET', '/api/todo/') | |
| r = conn.getresponse() | |
| data = json.loads(r.read().decode('utf8')) | |
| self.__dict__['data'] = data | |
| self.refreshControl.endRefreshing() | |
| self.tableView.reloadData() | |
| @objc_method | |
| def addClicked(self): | |
| self.__dict__['additemcontroller'] = AddItemController.alloc().init() | |
| self.__dict__['additemcontroller'].__dict__['tablecontroller'] = self | |
| navigationController = UINavigationController.alloc().initWithRootViewController_(self.__dict__['additemcontroller']) | |
| self.presentModalViewController_animated_(navigationController, True) | |
| class PythonAppDelegate(UIResponder): | |
| # @objc_method | |
| # def applicationDidBecomeActive(self) -> None: | |
| # print("BECAME ACTIVE") | |
| @objc_method | |
| def application_didFinishLaunchingWithOptions_(self, application, launchOptions) -> bool: | |
| self.__dict__['tablecontroller'] = TableViewController.alloc().init() | |
| self.__dict__['tablecontroller'].refreshControl = UIRefreshControl.alloc().init() | |
| self.__dict__['tablecontroller'].refreshControl.addTarget_action_forControlEvents_( | |
| self.__dict__['tablecontroller'], | |
| get_selector('refreshTable'), | |
| UIControlEventValueChanged | |
| ) | |
| conn = HTTPConnection('freakboy3742.pythonanywhere.com') | |
| conn.request('GET', '/api/todo/') | |
| r = conn.getresponse() | |
| data = json.loads(r.read().decode('utf8')) | |
| self.__dict__['tablecontroller'].__dict__['data'] = data | |
| self.__dict__['navcontroller'] = UINavigationController.alloc().initWithRootViewController_(self.__dict__['tablecontroller']) | |
| # self.__dict__['navcontroller'].navigationBar.topItem.title = "Hello World" | |
| self.__dict__['tablecontroller'].title = "TodoList" | |
| self.__dict__['addButton'] = UIBarButtonItem.alloc().initWithBarButtonSystemItem_target_action_( | |
| UIBarButtonSystemItemAdd, | |
| self.__dict__['tablecontroller'], | |
| get_selector('addClicked') | |
| ) | |
| self.__dict__['navcontroller'].navigationBar.topItem.rightBarButtonItem = self.__dict__['addButton'] | |
| self.__dict__['window'] = UIWindow.alloc().initWithFrame_(UIScreen.mainScreen().bounds) | |
| self.__dict__['window'].rootViewController = self.__dict__['navcontroller'] | |
| self.__dict__['window'].makeKeyAndVisible() | |
| return True |
| import toga | |
| class TodoApp(toga.App): | |
| def startup(self): | |
| self.list = toga.List( | |
| widget_id='todo', | |
| data=[ | |
| {'description': 'item 1'}, | |
| {'description': 'item 2'}, | |
| {'description': 'item 3'}, | |
| ], | |
| # item_class=toga.SimpleListElement, | |
| on_delete=self.remove_entry, | |
| on_refresh=self.refresh | |
| ) | |
| self.input = toga.TextInput(placeholder="thing to do...") | |
| self.add_item_dialog = toga.Dialog( | |
| title="Add item", | |
| content=toga.Container( | |
| self.input | |
| ), | |
| on_accept=self.add_entry | |
| ) | |
| container = toga.NavigationView( | |
| title="Todo List", | |
| content=self.list, | |
| on_action=self.show_add_dialog | |
| ) | |
| self.main_window.content = container | |
| def show_add_dialog(self, widget): | |
| self.input.clear() | |
| self.show_dialog(self.add_item_dialog) | |
| def add_entry(self, widget): | |
| if self.input.value: | |
| self.list.add({'description': self.input.value}) | |
| def remove_entry(self, widget): | |
| print("REMOVE ENTRY", widget) | |
| def refresh(self, list_widget): | |
| print("REFRESH LIST", list_widget) | |
| if __name__ == '__main__': | |
| app = TodoApp('TodoList', 'org.pybee.todolist') |
This is pretty amazing!
The methods remove_entry and refresh in the toga-mobile version aren't doing anything with the list. Are the delete and refresh functionality baked in to toga's List class, or is there a bit more code that's not shown in toga-mobile here? I'm guessing there must be a bit of extra code to handle syncing the data in the different apps, as shown in the video?
@takluyver You've caught me - that's a bit of detail that I glossed over in the interests of getting a demo working :-)
The issue was that when returning values from the call to the API POST, Batavia (the underlying Python->Javascript layer) was breaking. Longer term, this should be addressed, and the call to the API should be backed by receiving a response from the API and handling it.
Hi!
Could anyone please provide a briefcase setup.py example for android.py?
toga-mobile.pyshows the slightly different API required for mobile apps. This is to be expected; there are UI/UX metaphors that work on web apps that don't work on mobile.