-
-
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.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.