├── .gitignore ├── examples ├── editor.py ├── console.py └── app.py ├── pythonista ├── shared.py ├── classes.py ├── console.py ├── app.py ├── __init__.py └── editor.py ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /examples/editor.py: -------------------------------------------------------------------------------- 1 | import pythonista.editor 2 | 3 | if __name__ == "__main__": 4 | pythonista.editor.Tab().present() 5 | pythonista.editor.WebTab().present() -------------------------------------------------------------------------------- /examples/console.py: -------------------------------------------------------------------------------- 1 | import pythonista.console 2 | 3 | if __name__ == "__main__": 4 | print(pythonista.console.getConsoleFont()) 5 | print(pythonista.console.getDefaultFont()) -------------------------------------------------------------------------------- /examples/app.py: -------------------------------------------------------------------------------- 1 | import pythonista.app 2 | 3 | if __name__ == "__main__": 4 | ##pythonista.app.setBadgeString("Test") 5 | ##pythonista.app.setBadgeNumber(1) 6 | ##pythonista.app.clearBadge() 7 | ##pythonista.app.openURL("http://forum.omz-software.com/") -------------------------------------------------------------------------------- /pythonista/shared.py: -------------------------------------------------------------------------------- 1 | from .classes import NSUserDefaults, UIApplication 2 | 3 | __all__ = [ 4 | "app", 5 | "consoleVC", 6 | "rootVC", 7 | "tabVC", 8 | "userDefaults", 9 | ] 10 | 11 | app = UIApplication.sharedApplication() 12 | consoleVC = app.delegate().consoleViewController() 13 | rootVC = app.keyWindow().rootViewController() 14 | tabVC = rootVC.detailViewController() 15 | userDefaults = NSUserDefaults.standardUserDefaults() -------------------------------------------------------------------------------- /pythonista/classes.py: -------------------------------------------------------------------------------- 1 | # This is not the real classes module. 2 | # The first tim you import the pythonista module, it installs a custom loader for this module into sys.meta_path, so this file is never loaded. 3 | # The loader creates an instance of ObjCClassModuleProxy and uses that as the module pythonista.classes. 4 | # ObjCClassModuleProxy is a subclass of the built-in module type. It overrides __getattr__ so that pythonista.classes.ClassName is the same as objc_util.ObjCClass("ClassName"). The main purpose of this is that you can quickly load multiple Objective-C classes using a normal from import. 5 | 6 | raise ImportError("This file should never be imported. The pythonista.classes module must be loaded by a custom loader that should have been installed by the pythonista module.") -------------------------------------------------------------------------------- /pythonista/console.py: -------------------------------------------------------------------------------- 1 | """Methods relating to the console.""" 2 | 3 | from objc_util import on_main_thread 4 | 5 | from . import shared 6 | 7 | __all__ = [ 8 | "getConsoleFont", 9 | "getDefaultFont", 10 | ] 11 | 12 | @on_main_thread 13 | def getConsoleFont(): 14 | """Return the font name and size that is currently active for the console.""" 15 | 16 | shared.consoleVC.view() 17 | desc = shared.consoleVC.outputFont().fontDescriptor() 18 | font = [str(desc.objectForKey_(a)) for a in ("NSFontNameAttribute", "NSFontSizeAttribute")] 19 | font[1] = int(font[1]) 20 | return tuple(font) 21 | 22 | @on_main_thread 23 | def getDefaultFont(): 24 | """Get the user default for console font.""" 25 | 26 | return ( 27 | str(shared.userDefaults.stringForKey_("OutputFontName")), 28 | shared.userDefaults.integerForKey_("OutputFontSize"), 29 | ) -------------------------------------------------------------------------------- /pythonista/app.py: -------------------------------------------------------------------------------- 1 | """Miscellaneous functions for controlling the app.""" 2 | 3 | from objc_util import nsurl, on_main_thread 4 | 5 | from . import shared 6 | 7 | __all__ = [ 8 | "clearBadge", 9 | "openURL", 10 | "setBadgeNumber", 11 | "setBadgeString", 12 | ] 13 | 14 | @on_main_thread 15 | def setBadgeString(s): 16 | """Set the badge on the app icon to be a certain string.""" 17 | shared.app.setApplicationBadgeString_(s) 18 | 19 | @on_main_thread 20 | def setBadgeNumber(i): 21 | """Set the badge on the app icon to be a certain number.""" 22 | shared.app.setApplicationIconBadgeNumber_(i) 23 | 24 | @on_main_thread 25 | def clearBadge(): 26 | """Clear the badge on the app icon.""" 27 | shared.app.setApplicationBadgeString_("") 28 | shared.app.setApplicationIconBadgeNumber_(0) 29 | 30 | @on_main_thread 31 | def openURL(url): 32 | """Open a url in a way that works through appex. This is useful for using 33 | URL schemes to open other apps with data gained from appex.""" 34 | shared.app._openURL_(nsurl(url)) -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tony Kainos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pythonista/__init__.py: -------------------------------------------------------------------------------- 1 | """Root of the pythonista package.""" 2 | 3 | import objc_util 4 | import sys 5 | 6 | __all__ = [] 7 | 8 | # Install the loader for pythonista.classes. 9 | 10 | # Note: objc_util already caches ObjCClass instances. We don't need to worry about that. 11 | class ObjCClassModuleProxy(type(sys)): 12 | def __dir__(self): 13 | return list(sorted(set(self.__dict__.keys()) | set(objc_util._cached_classes.keys()))) 14 | 15 | def __getattr__(self, name): 16 | try: 17 | return objc_util.ObjCClass(name) 18 | except ValueError as err: 19 | raise AttributeError(err.message) 20 | 21 | class FinderLoaderForClasses(object): 22 | def find_module(self, fullname, path=None): 23 | return self if fullname == "pythonista.classes" else None 24 | 25 | def load_module(self, fullname): 26 | assert fullname == "pythonista.classes" 27 | mod = sys.modules.setdefault(fullname, ObjCClassModuleProxy(fullname)) 28 | mod.__file__ = "".format(cls=type(self)) 29 | mod.__loader__ = self 30 | mod.__package__ = "pythonista" 31 | mod.__all__ = ["NSDontEvenTryToStarImportThisModuleYouCrazyPerson"] 32 | return mod 33 | 34 | # This is for removing old versions of the loader if the pythonista module is reloaded. 35 | for obj in sys.meta_path: 36 | if type(obj).__module__ == "pythonista" and type(obj).__name__ == "FinderLoaderForClasses": 37 | sys.meta_path.remove(obj) 38 | break 39 | 40 | sys.meta_path.append(FinderLoaderForClasses()) 41 | from . import classes 42 | reload(classes) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pythonista-Tweaks 2 | 3 | ## Installation 4 | 5 | Download the repo in zip format and extract it, or `git clone` it using [stash](https://github.com/ywangd/stash). Then move or copy the `pythonista` folder into `site-packages`. If the module was installed correctly, you should be able to run the examples. 6 | 7 | ## Vision 8 | 9 | * Bring together in one place everything possible with `UIApplication` and `objc_util` 10 | * Make customising the Pythonista app similar to, and as easy as, using the `ui` module 11 | 12 | ## Notes 13 | 14 | The code is now broken into several submodules: 15 | 16 | * `pythonista.app` contains functions for setting the badge string/number, clearing the badge, and opening URLs in an `appex`-safe way. 17 | * `pythonista.classes` is a proxy module that can be used to load Objective-C classes. For example `pythonista.classes.NSObject == objc_util.ObjCClass("NSObject")`. 18 | * `pythonista.console` contains functions for getting the current and default console fonts. 19 | * `pythonista.editor` contains the `Tab` and `WebTab` classes. 20 | * `pythonista.shared` contains a few commonly used objects, such as the shared application and a few view controllers. 21 | 22 | **Note:** Please do NOT use `from pythonista import *`, as this will cause name conflicts with the default `console` and `editor` modules. Instead, use it only with `import pythonista`, as this way the module's submodules will not overwrite Pythonista's default ones. It is fine to use `from pythonista.module import *` with all submodules except for `pythonista.classes`. 23 | 24 | As of right now, each submodule contains only a small amount of functionality, but as they're expanded, the submodule approach will make much more sense. 25 | 26 | `pythonista.editor` will hopefully eventually contain the classes for easily controlling the editor's look and feel. The structure of these submodules, as well as their names, is likely to change. 27 | 28 | ## Credits 29 | 30 | * Based on examples posted on the forums by @omz, @JonB, @Webmaster4o, etc. -------------------------------------------------------------------------------- /pythonista/editor.py: -------------------------------------------------------------------------------- 1 | """Classes to support customising the Pythonista app in a similar way to using the ui module.""" 2 | 3 | import dialogs 4 | import urllib 5 | 6 | from objc_util import ObjCInstance, ObjCInstanceMethod, create_objc_class, nsurl, on_main_thread, sel 7 | 8 | from . import shared 9 | from .classes import NSDataDetector, NSURLRequest, UIBarButtonItem, UIButton, UIImage, UISearchBar, UIViewController, WKWebView 10 | 11 | __all__ = [ 12 | "Button", 13 | "ButtonItem", 14 | "SearchBar", 15 | "Tab", 16 | "View", 17 | "WebTab", 18 | "WebView", 19 | ] 20 | 21 | # objCClasses 22 | 23 | @on_main_thread 24 | def Button(s): 25 | return UIButton.new().autorelease() 26 | 27 | @on_main_thread 28 | def WebView(s): 29 | return WKWebView.new().autorelease() 30 | 31 | @on_main_thread 32 | def ButtonItem(s, image, action): 33 | return ( 34 | UIBarButtonItem.alloc() 35 | .initWithImage_style_target_action_(UIImage.imageNamed_(image), 0, s.newVC, sel(action)) 36 | ) 37 | 38 | @on_main_thread 39 | def SearchBar(s): 40 | sb = UISearchBar.alloc().initWithFrame_(((0, 0), (200, 32))) 41 | sb.searchBarStyle = 2 42 | sb.placeholder = "Search or enter address" 43 | sb.delegate = s.newVC 44 | ObjCInstanceMethod(sb, "setAutocapitalizationType:")(0) 45 | 46 | return sb 47 | 48 | # View classes 49 | 50 | class View(object): 51 | @on_main_thread 52 | def __init__(self): 53 | self.name = "" 54 | self.right_button_items = [] 55 | self.newVC = self.customVC() 56 | self.makeSelf() 57 | 58 | @on_main_thread 59 | def makeSelf(self): 60 | pass 61 | 62 | @on_main_thread 63 | def customVC(self): 64 | return None 65 | 66 | @on_main_thread 67 | def present(self): 68 | pass 69 | 70 | 71 | class Tab(View): 72 | @on_main_thread 73 | def makeSelf(self): 74 | self.name = "Tab" 75 | 76 | @on_main_thread 77 | def customVC(self): 78 | return create_objc_class( 79 | "CustomViewController", 80 | UIViewController, 81 | methods=[], 82 | protocols=["OMTabContent"], 83 | ).new().autorelease() 84 | 85 | @on_main_thread 86 | def present(self): 87 | self.newVC.title = self.name 88 | self.newVC.navigationItem().rightBarButtonItems = self.right_button_items 89 | shared.tabVC.addTabWithViewController_(self.newVC) 90 | 91 | 92 | class WebTab(Tab): 93 | @on_main_thread 94 | def makeSelf(self): 95 | self.name = "WebTab" 96 | self.right_button_items = [ 97 | ButtonItem(self, image=image, action=action) 98 | for image, action 99 | in (("Action", "wtShare"), ("Forward", "wtGoForward"), ("Back", "wtGoBack")) 100 | ] 101 | self.newVC.navigationItem().titleView = SearchBar(self) 102 | wv = WebView(self) 103 | wv.loadRequest_(NSURLRequest.requestWithURL_(nsurl("https://google.com"))) 104 | self.newVC.view = wv 105 | 106 | @on_main_thread 107 | def customVC(self): 108 | return create_objc_class( 109 | "CustomViewController", 110 | UIViewController, 111 | methods=[wtShare, wtGoBack, wtGoForward, searchBarSearchButtonClicked_], 112 | protocols=["OMTabContent", "UISearchBarDelegate"], 113 | ).new().autorelease() 114 | 115 | # actions 116 | 117 | def wtShare(_self, _cmd): 118 | url = ObjCInstance(_self).view().URL() 119 | if url: 120 | dialogs.share_url(str(url.absoluteString())) 121 | 122 | def wtGoBack(_self, _cmd): 123 | ObjCInstance(_self).view().goBack() 124 | 125 | def wtGoForward(_self, _cmd): 126 | ObjCInstance(_self).view().goForward() 127 | 128 | def searchBarSearchButtonClicked_(_self, _cmd, _sb): 129 | searchbar = ObjCInstance(_sb) 130 | term = str(searchbar.text()) 131 | searchbar.resignFirstResponder() 132 | 133 | if term: 134 | det = NSDataDetector.dataDetectorWithTypes_error_(1<<5, None) 135 | res = det.firstMatchInString_options_range_(term, 0, (0, len(term))) 136 | view = ObjCInstance(_self).view() 137 | if res: 138 | view.loadRequest_(NSURLRequest.requestWithURL_(res.URL())) 139 | searchbar.text = res.URL().absoluteString() 140 | else: 141 | view.loadRequest_(NSURLRequest.requestWithURL_(nsurl('https://google.com/search?q=' + urllib.quote(term)))) --------------------------------------------------------------------------------