-
-
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') |
toga-mobile.py
shows 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.
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?
Three platforms (Web, iOS and Android), one app, native on each of them.
The Web app is delivered using Toga-web, a prototype wrapper API that that builds on top of Django's APIs. In the next few weeks, I'm going to provide a similar wrapped API for the Android and iOS versions. For the moment, Android and iOS are implemented using native platform APIs.
The iOS implementation uses Rubicon to bridge between Python and the Objective C runtime. It's running on a simulator because I needed my iPhone to shoot the video - but the app will run on the device as well.
The Android implementation uses VOC to compile Python to a set of Java classfiles. It also uses @baoyongzhang's SwipeListView implementation to get the "Swipe to Delete" behavior. It should run on any Android 3 device or higher.
Video of the apps running is on YouTube