Old methods to grab console text no longer work in Pythonista 3.3?
So I used to be able to do one of these two tricks to find the console text view in Pythonista 3.2 and grab the text out of it...
Search the view hierarchy from the root and compare objc classes:
def findConsoleView(): OMTextView = objc_util.ObjCClass("OMTextView") OMTextEditorView = objc_util.ObjCClass("OMTextEditorView") app = objc_util.UIApplication.sharedApplication() mainwindow = app.keyWindow() mainview = mainwindow.rootViewController().view() consoleview = None visit = [mainview] while visit and consoleview is None: v = visit.pop() if v.isKindOfClass_(OMTextEditorView): # main editor, skip it continue if v.isKindOfClass_(OMTextView): consoleview = v continue visit += v.subviews() return consoleview
or a similar search, but comparing classname strings:
def findConsoleView(): app = objc_util.UIApplication.sharedApplication() mainwindow = app.keyWindow() mainview = mainwindow.rootViewController().view() consoleview = None visit = [mainview] while visit and consoleview is None: v = visit.pop() classname = str(v._get_objc_classname()) if classname == "OMTextEditorView": # main editor, skip continue if classname == "OMTextView": consoleview = v continue visit += v.subviews() return consoleview
now, in Pythonista 3.3, neither of these methods is working...the class-object comparison doesn't match, and it doesn't find a view matching by classname either.
Anyone know where the console text view has gone?
I tried looking for all views with "Text" in the classname from the root view, as above, but there's no view that appears to contain the console text.
cvp last edited by
@shinyformica my script still works
from objc_util import * import ui @on_main_thread def test(sender): import clipboard import console from objc_util import ObjCClass import re import ui txt = str(sender.console.text()) if txt[-1] == '\n': txt = txt[:-1] win = ObjCClass('UIApplication').sharedApplication().keyWindow() main_view = win.rootViewController().view() ret = '' def analyze(v): for tv in v.subviews(): if 'OMTextView' in str(tv._get_objc_classname()): su = tv.superview() if 'OMTextEditorView' in str(su._get_objc_classname()): continue for sv in tv.subviews(): if 'SUIButton_PY3' in str(sv._get_objc_classname()): sv.removeFromSuperview() if txt == '': return t = str(tv.text()) #print('search',txt,'in',t) #for m in re.finditer('(?i)'+txt, t): first = True clp = '' for m in re.finditer(txt, t): st,en = m.span() p1 = tv.positionFromPosition_offset_(tv.beginningOfDocument(), st) p2 = tv.positionFromPosition_offset_(tv.beginningOfDocument(), en) rge = tv.textRangeFromPosition_toPosition_(p1,p2) rect = tv.firstRectForRange_(rge) # CGRect x,y = rect.origin.x,rect.origin.y w,h = rect.size.width,rect.size.height if first: first = False tv.setContentOffset_(CGPoint(0,y)) #print(x,y,w,h) l = ui.Button() l.frame = (x,y,w,h) if '|' not in txt: l.background_color = (1,0,0,0.2) else: # search multiple strings wrds = txt.split('|') idx = wrds.index(t[st:en]) cols = [(1,0,0,0.2), (0,1,0,0.2), (0,0,1,0.2), (1,1,0,0.2), (1,0,1,0.2), (0,1,1,0.2)] col = cols[idx % len(cols)] l.background_color = col l.corner_radius = 4 l.border_width = 1 tv.addSubview_(l) clp += t[st:en] + '\n' clipboard.set(clp) ret = analyze(tv) if ret: return ret ret = analyze(main_view) @on_main_thread def FindTextInConsole(): global console_tv win = ObjCClass('UIApplication').sharedApplication().keyWindow() main_view = win.rootViewController().view() ret = '' next_is_console = False def analyze(v,indent): global next_is_console ret = None for sv in v.subviews(): #print(indent,sv._get_objc_classname()) # try: # if str(sv.currentTitle()) == 'Clear': # print(dir(sv)) # # add a target action to clear button? # my_clear_button = ui.Button() # my_clear_button.action = test # sv.addTarget_action_forControlEvents_(my_clear_button, sel('invokeAction:'),8) # except: # pass #if 'UIImageView' in str(sv._get_objc_classname()): # print(sv.image()) if 'UILabel' in str(sv._get_objc_classname()): #print(indent,sv.text()) if str(sv.text()) == '>': next_is_console = sv else: next_is_console = False elif 'OMTextView' in str(sv._get_objc_classname()): if next_is_console: su = next_is_console.superview() for ssv in su.subviews(): if 'SUIButton_PY3'in str(ssv._get_objc_classname()): # rerun of this script, remove previous button ssv.removeFromSuperview() b = ui.Button(name='clipboard') b.tint_color ='red' b.image = ui.Image.named('iob:ios7_search_32') b.background_color = 'white' h = su.frame().size.height b.frame = (2,2,h-4,h-4) b.action = test b.console = sv retain_global(b) su.addSubview(ObjCInstance(b)) ret = analyze(sv,indent+' ') if ret: return ret ret = analyze(main_view,'') return ret if __name__ == '__main__': r = FindTextInConsole()
cvp last edited by
@shinyformica is that what you want?
from objc_util import * def GetConsoleText(): print('called') win = ObjCClass('UIApplication').sharedApplication().keyWindow() main_view = win.rootViewController().view() ret = '' def analyze(v): ret = None for sv in v.subviews(): if 'textview' in str(sv._get_objc_classname()).lower(): if 'OMTextEditorView' not in str(sv.superview()._get_objc_classname()): # not TextView of script return sv.text() ret = analyze(sv) if ret: return ret ret = analyze(main_view) return ret
@cvp so strange...there were definitely some issues in those methods which make it so they honestly shouldn't have been working except by luck. But also, it is definitely now the case that the isKindOfClass_() calls no longer work...which is why these were failing in my tests.
So this works:
def findConsoleView(sender): app = objc_util.UIApplication.sharedApplication() mainwindow = app.keyWindow() mainview = mainwindow.rootViewController().view() visit = list(mainview.subviews()) while visit: v = visit.pop() if str(v._get_objc_classname()) == "OMTextView": if v.superview() and not \ str(v.superview()._get_objc_classname()) == "OMTextEditorView": return v visit += list(v.subviews()) return None
but at some point isKindOfClass_() was returning True for me when comparing an actual OMTextView instance against objc_util.ObjCClass("OMTextView").
So not sure when that stopped working. But this updated version works for my purposes.
mikael last edited by
@shinyformica, your method returns None for me as well. At the same time, some other methods using isKindOf_ work as they used to (disabling swipe to close and finding the root view).
mikael last edited by
@shinyformica, looking at ”my stuff”, I have always used the .ptr after the ObjC class for them to work. But that does not seem to make a difference for your code. 😕
shinyformica last edited by shinyformica
That is odd. I haven't tried the isKindOfClass_() call with the .ptr...I don't see it called that way elsewhere in places like objc_util or the Gestures module.
What's disturbing is that I'm relying on calling isKindOfClass_() on objc_util.ObjCClass() objects in a lot of places, so now I'm worried it'll fail elsewhere. Those calls usually check against UIView or UIVIewController, so maybe they're safe...and so far I see no issues.
This was the only place that was suddenly broken...and given the other issues in the logic, I'm actually pretty confused how my old code was working at all...and yet it was.
JonB last edited by
Have you swizzled textview to allow custom menu's or anything else?
As an aside, I remember something similar when implementing swizzle.
To swizzle a class it should be possible to use ObjCClass(classname) to get the class. But for many objects, it doesn't work, and instead I needed to call the runtime getClass method on an instance:
Although I have no evidence, what I suspect is that objc categories or extensions might be creating new classes.
@JonB I try very hard not to swizzle anything...it sounds and feels dirty. I did do it once, I admit, in order to make the tableview have the tableView_willDisplayHeaderView_forSection_ method, so I could customize header display.
I suspect you are right about why it isn't working. Using the string comparison is working well enough.