Forum Archive

Old methods to grab console text no longer work in Pythonista 3.3?

shinyformica

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

@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

@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 
shinyformica

@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

@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

@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

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

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:

cls =ObjCInstance(c.object_getClass(instance.ptr))

Although I have no evidence, what I suspect is that objc categories or extensions might be creating new classes.

shinyformica

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