Forum Archive

Pythonista special key row problems in IOS 14

dthomson

It appears IpadOS 14 broke the special key row in Pythonista. After editing for a short while the center section of keys disappears. A while later the remaining keys shrink further to the sides. It also appears the cursor positioning by sliding over the key row does not work in the center portion, even when the keys are still showing.

https://share.icloud.com/photos/0IcGyZcOAkshHXxcFR2087a5Q
https://share.icloud.com/photos/02XeIxGZlrfj1JsxYDWYm6YgQ

mikael

@dthomson, looks like something related to the split keyboard option on iPadOS. How does that work if you try to use it on purpose?

cvp

I got sometimes the same problem and it does not look like the split keyboard, only the supplementary row.

The problem occurs when I run a script in a editor's tab where the keyboard is visible.
But not in other tabs, or not if I run a script where the keyboard is hidden.

Enez Houad

Has someone found a solution ? πŸ₯΅ the problem appears quite all the time for me and it’s really difficult to type text in rhe reduced special key row - I’ve got an iPad mini and the keyboard is small ;-)

cvp

@Enez-Houad no solution found, except if I restart Pythonista 😒

rb

I turned on and off the β€œExtra Key Row” option in the β€œSettings>Keyboard and Typing” option.seems to fix it if it happens.Annoying though!

Enez Houad

The only solution I've found is to make my own special key row with the help of the cvp example ;-). It's quite easy for normal keys, but I don't find solution to implement the undo/redo key? Perhaps with objC ?

Enez Houad

My new special key row !

Enez Houad

@cvp Please, how do you insert directly your pictures in comments ?

cvp

@Enez-Houad I use

import pyimgur,photos,clipboard,os,console
i=photos.pick_image()
if i:
    print(i.format)
    format = 'gif' if (i.format == 'GIF') else 'jpg'
    i.save('img.'+format)
    clipboard.set(pyimgur.Imgur("303d632d723a549").upload_image('img.'+format, title="Uploaded-Image").link)
    console.hud_alert("link copied!")
    os.remove('img.'+format)

With pyImgur from here

cvp

@Enez-Houad I don't know which code of mines you use to add keys to Pythonista keyboard, but you can add these lines at end to replace the extra row of Pythonista by only undo and redo keys...
And that after 3 full hours of searching πŸ˜…

.
.
.
.
@on_main_thread 
def AddButtonsToPythonistaKeyboard(pad=None):
    if not pad:
        pad = [
        {'key':'next word','width':2},
        {'key':'find','icon':'iob:ios7_search_32'},
        {'key':'nul'},
        {'key':'>'},
#       {'key':'new row'},
        {'key':'nul'},
        {'key':'nul'},
        {'key':'#'},
        {'key':'nul'},
        {'key':'"'},
        {'key':'nul'},
        {'key':'left','icon':'iob:arrow_left_a_32'},
        {'key':'right','icon':'iob:arrow_right_a_32'},
        {'key':'down','icon':'iob:arrow_down_a_32'},
        {'key':'\\'},
        {'key':'nul'},
        {'key':'delete','title':'Del'}]

    ev = editor._get_editor_tab().editorView()
    tv = ev.textView()
    #print(tv._get_objc_classname())
    #print(dir(tv))

    # create ui.View for InputAccessoryView above keyboard
    v = MyView(pad)                                             # view above keyboard
    vo = ObjCInstance(v)                                    # get ObjectiveC object of v
    retain_global(v) # see https://forum.omz-software.com/topic/4653/button-action-not-called-when-view-is-added-to-native-view
    tv.setInputAccessoryView_(vo)   # attach accessory to textview
    tv.strfind = ''

    #======================     
    UIBarButtonItem = ObjCClass('UIBarButtonItem')
    UIBarButtonItemGroup = ObjCClass('UIBarButtonItemGroup')

    def btnActionUndo(_self, _cmd):
        tv = ObjCClass('UIApplication').sharedApplication().keyWindow().firstResponder()
        tv.undoManager().undo()

    ActionTargetUndo = create_objc_class('ActionTarget', methods=[btnActionUndo])
    targetUndo = ActionTargetUndo.new().autorelease()
    bUndo = UIBarButtonItem.alloc().initWithTitle_style_target_action_('β†ͺ️', 0, targetUndo, 'btnActionUndo').autorelease()      

    def btnActionRedo(_self, _cmd):
        tv = ObjCClass('UIApplication').sharedApplication().keyWindow().firstResponder()
        tv.undoManager().redo()

    ActionTargetRedo = create_objc_class('ActionTarget', methods=[btnActionRedo])
    targetRedo = ActionTargetRedo.new().autorelease()

    bRedo = UIBarButtonItem.alloc().initWithTitle_style_target_action_('↩️', 0, targetRedo, 'btnActionRedo').autorelease()  

    group = UIBarButtonItemGroup.alloc().initWithBarButtonItems_representativeItem_([bUndo, bRedo], None).autorelease()
    ObjCInstance(tv).inputAssistantItem().leadingBarButtonGroups = [group]
    ObjCInstance(tv).inputAssistantItem().trailingBarButtonGroups = []
    #======================                 

if __name__ == '__main__':  
    AddButtonsToPythonistaKeyboard()

link text

Enez Houad

@cvp Thanks a lot for your work. But my goal is to get a reduced row of keys as I think that the original one is to large for the small screen of my iPad mini 4. I regret that it’s not possible to assign the fonction to a normal ui button.

cvp

@Enez-Houad said:

. I regret that it’s not possible to assign the fonction to a normal ui button.

I think it is also possible, let me some time today

cvp

@Enez-Houad much easier, if I would know πŸ˜‚

import editor
from   objc_util import *
import ui

@on_main_thread
def test(tv):
    import console
    import re
    import ui
    tv.strfind = console.input_alert('text or regex to search', '', tv.strfind, hide_cancel_button=True)
    txt = tv.strfind

    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):
    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
        #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)

def key_pressed(sender):
        import ui
        from objc_util import ObjCClass
        tv = sender.objc_instance.firstResponder()  # associated TextView
        # get actual cursor position                    
        cursor = tv.offsetFromPosition_toPosition_(tv.beginningOfDocument(), tv.selectedTextRange().start())

        if sender.name == 'left':   
            if cursor == 0:
                return                                              # already first character
            cursor = cursor - 1                         
        elif sender.name == 'right':    
            if cursor == (len(str(tv.text()))-1):
                return                                              # already after last character
            cursor = cursor + 1     
        elif sender.name == 'down':         
            # surely not correct because not tested in limit cases,
            # just to show for topic https://forum.omz-software.com/topic/6408/defining-a-line-up-line-down-keyboard-function
            # count position in line
            c = str(tv.text()).rfind('\n',0,cursor) # begin of current line
            p = cursor - c
            e = str(tv.text()).find('\n',cursor)        # end   of current line
            cursor = e + 1                                                  # begin of next line
            e = str(tv.text()).find('\n',cursor)        # end   of next line
            cursor = cursor + min(p-1,e-cursor)
        elif sender.name == 'next word':
            t = str(tv.text())[cursor+1:]   
            for i in range(0,len(t)):
                # search 1st separator
                if t[i] in ' _.\n\t':
                    # search 1st not separator
                    ch_found = False
                    for j in range(i+1,len(t)):
                        if t[j] not in ' _.\n\t':
                            ch_found = True
                            cursor = cursor + 1 + j
                            break
                    if ch_found:
                        break
        elif sender.name == 'delete':                       
            # delete at right = delete at left of next character
            if cursor == (len(str(tv.text()))-1):
                return                                              # already after last character
            cursor_position = tv.positionFromPosition_offset_(tv.beginningOfDocument(), cursor+1)
            tv.selectedTextRange = tv.textRangeFromPosition_toPosition_(cursor_position, cursor_position)
            tv.deleteBackward()                         
        elif sender.name == 'find':
            test(tv)
        elif sender.name == 'undo':
            #tv = ObjCClass('UIApplication').sharedApplication().keyWindow().firstResponder()
            tv.undoManager().undo()
        elif sender.name == 'redo':
            tv.undoManager().redo()
        else:
            # normal key
            tv.insertText_(sender.title)    
            return
        # set cursor
        cursor_position = tv.positionFromPosition_offset_(tv.beginningOfDocument(), cursor)
        tv.selectedTextRange = tv.textRangeFromPosition_toPosition_(cursor_position, cursor_position)   

class MyView(ui.View):
    def __init__(self, pad, *args, **kwargs):
        #super().__init__(self, *args, **kwargs)    
        self.width = ui.get_screen_size()[0]            # width of keyboard = screen
        self.background_color = 'lightgray'#(0,1,0,0.2)
        self.h_button = 32  
        self.pad = pad

        #================================================ for the fun begin     
        # cable for road
        self.road = ui.Label()
        self.road.frame = (0,40,self.width,1)
        self.road.border_width = 1
        self.road.border_color = 'green'
        self.road.flex = 'W'
        self.add_subview(self.road)

        # cable for tramway
        self.line = ui.Label()
        self.line.frame = (0,12,self.width,1)
        self.line.border_width = 1
        self.line.border_color = 'gray'
        self.line.flex = 'W'
        self.line.hidden = True
        self.add_subview(self.line)

        # moving emoji behind buttons
        self.moving = ui.Button()
        self.moving.font = ('<system>',self.h_button-4)
        self.moving.frame = (0,10,self.h_button,self.h_button)
        self.moving.icons = ['emj:Delivery_Truck', 'emj:Car_1','emj:Car_2', 'emj:Bus', 'emj:Police_Car', 'emj:Railway_Car','emj:Speedboat']
        self.moving.action = self.fun
        self.moving.index = 0
        self.add_subview(self.moving)
        self.update_interval = 0.06
        #================================================ for the fun end

        # build buttons
        for pad_elem in self.pad:
            if pad_elem['key'] in ('nul', 'new row'):       #  free space or new row
                continue
            button = ui.Button()                                    # Button for user functionnality
            button.name = pad_elem['key']
            button.background_color = 'white'           # or any other color
            button.tint_color = 'black'
            button.corner_radius = 5        
            button.font = ('<system>',self.h_button - 8)
            button.title = ''
            if 'title' in pad_elem:
                button.title = pad_elem['title']
            elif 'icon' in pad_elem:
                button.image = ui.Image.named(pad_elem['icon']).with_rendering_mode(ui.RENDERING_MODE_ORIGINAL)
            else:
                button.title = pad_elem['key']

            button.action = key_pressed
            retain_global(button) # see https://forum.omz-software.com/topic/4653/button-action-not-called-when-view-is-added-to-native-view
            self.add_subview(button)    
        self.layout()       

    #================================================ for the fun begin     
    def fun(self,sender):   
            self.update_interval = 0.06 - self.update_interval

    def update(self):
        import ui
        x = self.moving.x - 5
        if x < -self.moving.width:
            x = ui.get_screen_size()[0]
            self.moving.index = self.moving.index+1
            if self.moving.index == len(self.moving.icons):
                self.moving.index = 0
            emoji = self.moving.icons[self.moving.index]
            self.moving.image = ui.Image.named(emoji).with_rendering_mode(ui.RENDERING_MODE_ORIGINAL)
            self.line.hidden = not (emoji == 'emj:Railway_Car')
            self.road.border_color = 'blue' if emoji == 'emj:Speedboat' else 'green'
        self.moving.x = x
        #================================================ for the fun end

    def layout(self):
        import ui
        #print('layout')
        # supports changing orientation
        #print(ui.get_screen_size())
        dx = 8
        dy = 2
        x0 = 15
        y0 = 10
        dx_middle = 25 
        y = y0
        x = x0
        w_button = (ui.get_screen_size()[0] - 2*x0 - 17*dx - dx_middle)/18
        for pad_elem in self.pad:
            nw = pad_elem.get('width', 1)
            wb = w_button*nw + dx*(nw-1)
            if (x + wb + dx) > self.width:
                y = y + self.h_button + dy
                x = x0
            if pad_elem['key'] == 'nul':                    # let free space    
                x = x + wb + dx 
                continue
            elif pad_elem['key'] == 'new row':      # new row
                y = y + self.h_button + dy
                x = x0
                continue
            button = self[pad_elem['key']]
            xb = x + dx_middle if (x+wb) > self.width/2 else x
            button.frame = (xb,y,wb,self.h_button)
            if button.title != '':
                font_size = self.h_button - 8
                while True:
                    d = ui.measure_string(button.title,font=(button.font[0],font_size))[0]+4
                    if d <= wb:
                        break
                    font_size = font_size - 1           
            button.font = (button.font[0],font_size)
            x = x + wb + dx
        self.height = y + self.h_button + dy    

@on_main_thread 
def AddButtonsToPythonistaKeyboard(pad=None):
    if not pad:
        pad = [
        {'key':'undo','title':'β†ͺ️'},
        {'key':'redo','title':'↩️'},
        {'key':'next word','width':2},
        {'key':'find','icon':'iob:ios7_search_32'},
        {'key':'nul'},
        {'key':'>'},
#       {'key':'new row'},
#       {'key':'nul'},
#       {'key':'nul'},
        {'key':'#'},
        {'key':'nul'},
        {'key':'"'},
        {'key':'nul'},
        {'key':'left','icon':'iob:arrow_left_a_32'},
        {'key':'right','icon':'iob:arrow_right_a_32'},
        {'key':'down','icon':'iob:arrow_down_a_32'},
        {'key':'\\'},
        {'key':'nul'},
        {'key':'delete','title':'Del'}]

    ev = editor._get_editor_tab().editorView()
    tv = ev.textView()
    #print(tv._get_objc_classname())
    #print(dir(tv))

    # create ui.View for InputAccessoryView above keyboard
    v = MyView(pad)                                             # view above keyboard
    vo = ObjCInstance(v)                                    # get ObjectiveC object of v
    retain_global(v) # see https://forum.omz-software.com/topic/4653/button-action-not-called-when-view-is-added-to-native-view
    tv.setInputAccessoryView_(vo)   # attach accessory to textview
    tv.strfind = ''

if __name__ == '__main__':  
    AddButtonsToPythonistaKeyboard()
Enez Houad

@cvp Thank’s a lot Magic cvpπŸ‘πŸ‘πŸ‘ πŸ™

Enez Houad

Here is my scrolling special key row to replace the standard one.
This script is a mix between @cvpe's script AddButtonsToPythonistaKeyboard https://github.com/cvpe/Pythonista-scripts/blob/master/AddButtonsToPythonistaKeyboard.py
and my own work.
It uses module ui3.sfsymbol of @mikaelho https://github.com/mikaelho/ui3
Thanks for all their work for Pythonista's community.
It certainly contains errors as I am not an expert coder like the majority of you but it does work for me πŸ˜‰
Feel free to improve and adapt it to your needs!

```

=================================================================

This script is a mix between @cvpe's script AddButtonsToPythonistaKeyboard

https://github.com/cvpe/Pythonista-scripts/blob/master/AddButtonsToPythonistaKeyboard.py

and my own work.

It uses module ui3.sfsymbol of @mikaelho

https://github.com/mikaelho/ui3

enez.houad@free.fr

=================================================================

import ui, editor, clipboard
from objc_util import *
from ui3.sfsymbol import *

@on_main_thread
def tv_find(tv):
import console, re

tv.find = console.input_alert('Expression Γ  rechercher :', '', tv.find, hide_cancel_button=True)
txt = tv.find

for sv in tv.subviews():
    if 'SUIButton_PY3' in str(sv._get_objc_classname()):
        sv.removeFromSuperview()
if txt == '':
    return
t = str(tv.text())

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)
    x,y = rect.origin.x,rect.origin.y
    w,h = rect.size.width,rect.size.height

    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)

β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”

def key_pressed(sender):

tv = sender.objc_instance.firstResponder() # associated TextView

cursor = tv.offsetFromPosition_toPosition_(tv.beginningOfDocument(),     tv.selectedTextRange().start()) # get actual cursor position

if sender.name == 'tab':
    tv.insertText_('\t')

elif sender.name == 'paste':
    tv.insertText_(clipboard.get())

elif sender.name == 'undo':
    tv.undoManager().undo()

elif sender.name == 'redo':
    tv.undoManager().redo()

elif sender.name == 'del_right':
    # delete at right = delete at left of next
    if cursor == (len(str(tv.text()))-1): # already after last character
        return
    cursor_position = tv.positionFromPosition_offset_(tv.beginningOfDocument(), cursor+1)
    tv.selectedTextRange = tv.textRangeFromPosition_toPosition_(cursor_position, cursor_position)
    tv.deleteBackward()

elif sender.name == 'find':
    tv_find(tv)

elif sender.name == 'line-':
    tv.insertText_('# ' + 65 * 'β€”')
elif sender.name == 'line=':
    tv.insertText_('# ' + 65 * '=')
elif sender.name == 'line#':
    tv.insertText_(67 * '#')

else: # all other keys > insert button title
    tv.insertText_(sender.title)

===================================================================

class SpecialKeyRow(ui.View):

def __init__(self, pad, *args, **kwargs):

    self.pad = pad

    sw, sh = ui.get_screen_size()   
    self.uiStyle = ui.get_ui_style()

    self.buttonsList = []
    self.buttonWidth = (sw - (2*8) - (24*4)) / 25
    self.buttonHeight = 40

    # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
    # MAIN VIEW

    self.width, self.height = sw, 50
    self.alpha = 0.98

    # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
    # SCROLL VIEW

    sv = ui.ScrollView(name='scrollview')
    sv.width, sv.height = (sw, 50)
    sv.content_size = (2*sw, 50)
    sv.bounces = False
    sv.shows_horizontal_scroll_indicator = False
    sv.paging_enabled = True
    sv.x, sv.y = (0, 0)
    colorDict = {'light':'#D6D8DD', 'dark':'#343639'}
    sv.background_color = colorDict[self.uiStyle]

    self.add_subview(sv)

    # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
    # BUTTONS IN SCROLL VIEW
    # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”

    for pad_elem in self.pad:
        if not 'style' in pad_elem: bStyle = 'light'
        else: bStyle = pad_elem['style']
        if 'title' in pad_elem:
            b = self.add_text_button(name=pad_elem['key'], title=pad_elem['title'], style=bStyle)
        elif 'symbol' in pad_elem:
            b = self.add_symbol_button(name=pad_elem['key'], symbol_name=pad_elem['symbol'], style=bStyle)
        self.add_scrollview_button(b)

# ===================================================================

def add_scrollview_button(self, b):
    b.y = 10
    if self.buttonsList == []: b.x = 8 # 1er bouton
    else:
        lastButton = self.buttonsList[-1]
        b.x = lastButton.x + lastButton.width + 4 # intervalle 4 px
        if len(self.buttonsList) == 25: b.x += 12 # 2e page
    b.action = key_pressed
    retain_global(b)
    self['scrollview'].add_subview(b)
    self.buttonsList.append(b)

# β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”

def add_text_button(self, name='', title='', width=40, style='light'):
    b = self.add_button(name, style)
    b.title = title
    b.font = ('<system>', 18)
    if width == None: b.width = ui.measure_string(b.title,font=b.font)[0] + 28
    else: b.width = self.buttonWidth        
    return b

# β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”

def add_symbol_button(self, name='', symbol_name='', style='light'):
    b = self.add_button(name, style)
    symbol_image = SymbolImage(symbol_name, point_size=11, weight=LIGHT, scale=SMALL)
    b.image = symbol_image  
    b.width = self.buttonWidth  
    return b

# β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”

def add_button(self, name='', backgroundStyle='light'):

    b = ui.Button(name=name)
    b.corner_radius = 8

    colorsDict = {  'light' :({'light':'#FFFFFF', 'dark':'#B4B9C1'}, 'black'), 
                    'dark'  :({'light':'#717274', 'dark':'#4D4F50'}, 'white') }
    b.background_color = colorsDict[self.uiStyle][0][backgroundStyle]
    b.tint_color = colorsDict[self.uiStyle][1]

    b.alpha = self.alpha

    b.font = ('<system>', 18)
    b.height = self.buttonHeight        
    return b

===================================================================

@on_main_thread
def AddButtonsToPythonistaKeyboard(pad=None):

def numeric_keys(): 
    list = []
    for i in range(1, 10):
        list.append({'key':str(i), 'title':str(i)})
    list.append({'key':'0', 'title':'0'})
    return list

if not pad:     
    pad = [
            {'key':'tab', 'symbol':'arrow.right.to.line.alt'},

            {'key':'undo', 'symbol':'arrow.uturn.left', 'style':'dark'},
            {'key':'redo','symbol':'arrow.uturn.right', 'style':'dark'},

            {'key':'paste', 'symbol':'doc.on.clipboard'},

            {'key':'#', 'title':'#'},
            {'key':'_', 'title':'_'},

            {'key':"'", 'title':"'"},
            {'key':'"', 'title':'"'},
            {'key':"'''", 'title':"'''"},

            {'key':'(', 'title':'('},
            {'key':')', 'title':')'},           
            {'key':'[', 'title':'['},
            {'key':']', 'title':']'},
            {'key':'{', 'title':'{'},
            {'key':'}', 'title':'}'},

            {'key':'+', 'title':'+'},
            {'key':'-', 'title':'-'},
            {'key':'*', 'title':'*'},
            {'key':'/', 'title':'/'},
            {'key':"\\", 'title':"\\"},

            {'key':'<', 'title':'<'},
            {'key':'>', 'title':'>'},
            {'key':'=', 'title':'='},
            {'key':':', 'title':':'},

            {'key':'del_right', 'symbol':'delete.right', 'style':'dark'},

            {'key':'find', 'symbol':'magnifyingglass', 'style':'dark'}
            ]

    pad += numeric_keys()

    pad += [
            {'key':'+', 'title':'+'},
            {'key':'-', 'title':'-'},
            {'key':'*', 'title':'*'},
            {'key':'/', 'title':'/'},
            {'key':'<', 'title':'<'},
            {'key':'>', 'title':'>'},
            {'key':'=', 'title':'='},

            {'key':'line-', 'title':'---'},
            {'key':'line=', 'title':'==='},
            {'key':'line#', 'title':'###'}
            ]

ev = editor._get_editor_tab().editorView()
tv = ev.textView()

v = SpecialKeyRow(pad)                        
vo = ObjCInstance(v)

retain_global(v)

tv.setInputAccessoryView_(vo)   # attach accessory to textview
tv.find = ''

===================================================================

if name == 'main':
AddButtonsToPythonistaKeyboard() ```

cvp

@Enez-Houad Good job. I let real Python specialists like @ccc, @mikael and other ones advice you about Python it-self, they are far ahead of me

For those who want to see the keyboard

And even horizontally scroll this first row, whaaaa

Enez Houad

@cvp I learn a lot adapting your scripts to my needs !
You will find after this message my/your script to add a keyboard icon in the titlebar to call my script to add my special key row to the keyboard.
The system is rather practical, but to make the handling as transparent as possible : when I add my key row, I hide the keyboard with tv.endEditing(True), and I would like to make it reappear with my special key row : but I’ve not found a command as simple as tv.startEditing πŸ™ ; it’s perharps possible to simulate a touch in the TextView, but I neither can’t find a solution to do it πŸ₯Ά
After several hours of searching, I throw in the towel and ask for help…πŸ₯΅

from objc_util import *
import ui
from ui3.sfsymbol import *

w = ObjCClass('UIApplication').sharedApplication().keyWindow()
main_view = w.rootViewController().view()

def get_toolbar(view):
    # get main editor toolbar, by recursively walking the view
    sv = view.subviews()
    for v in sv:
        if v._get_objc_classname().startswith(b'OMTabViewToolbar'):
            return v
        tb = get_toolbar(v)
        if tb:
            return tb

# β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”

def keyboard_btn_action(sender):

    def run_script(scriptPath):
        import os
        from objc_util import ObjCInstance,ObjCClass

        dir = os.path.expanduser(scriptPath)
        I3=ObjCClass('PYK3Interpreter').sharedInterpreter()
        I3.runScriptAtPath_argv_resetEnvironment_(dir, [''], True)

    iCloudPath = "/private/var/mobile/Library/Mobile Documents/iCloud~com~omz-software~Pythonista3/Documents/"
    iPadPath = '~/Documents/'
    scriptPath = 'PROJETS/KEYBOARD/my_special_key_row.py'
    run_script(iCloudPath + scriptPath)

# β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”

def create_keyboard_button(action,index=0):
    global __persistent_views

    assert(callable(action))
    tb = get_toolbar(main_view)

    try:
        __persistent_views
    except NameError:
        __persistent_views={}

    #check for existing button in this index and delete if needed
    remove_toolbar_button(index)    
    btn = ui.Button()
    btn.frame = (110, 24, 40, 40)

    if ui.get_ui_style() == 'dark':
        btn.tint_color = 'white' 
    else:
        btn.tint_color = '#0D89B5' 
    btn.image = SymbolImage('keyboard', point_size=14, weight=THIN, scale=SMALL)
    btn.image = btn.image.with_rendering_mode(ui.RENDERING_MODE_AUTOMATIC)

    btn.action=action
    btn_obj=ObjCInstance(btn)
    __persistent_views[index]=(btn,action)
    tb.superview().superview().addSubview_(btn_obj) # in front of all buttons

    return btn

# β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”

def remove_toolbar_button(index):
    global __persistent_views

    try:
        btn,action = __persistent_views.pop(index)
        btn.action= None
        ObjCInstance(btn).removeFromSuperview()
    except KeyError:
        pass

# ===================================================================

#if __name__=='__main__': # if imported by pythonista startup   

create_keyboard_button(keyboard_btn_action) 

create_keyboard_button(keyboard_btn_action)

cvp

@Enez-Houad try

def keyboard_btn_action(sender):

    def run_script(scriptPath):
        import os
        from objc_util import ObjCInstance,ObjCClass

        dir = os.path.expanduser(scriptPath)
        I3=ObjCClass('PYK3Interpreter').sharedInterpreter()
        I3.runScriptAtPath_argv_resetEnvironment_(dir, [''], True)

    iCloudPath = "/private/var/mobile/Library/Mobile Documents/iCloud~com~omz-software~Pythonista3/Documents/"
    iPadPath = '~/Documents/'
    scriptPath = 'PROJETS/KEYBOARD/my_special_key_row.py'
    run_script(iCloudPath + scriptPath)

    @on_main_thread
    def disp_kbd():
      from editor import _get_editor_tab
      tab = _get_editor_tab()
      if tab:
        tab.editorView().textView().becomeFirstResponder()
    disp_kbd()
cvp

@Enez-Houad said:

my/your script to add a keyboard icon in the titlebar

I must admit with humulity that the most complex part of this code comes from @JonB 🀫

Enez Houad

@cvp Thanks a lot ! I’m happy to see that I progress slowly…
I had found textView().becomeFirstResponder() but couldn’t find to witch element it had to be applied. The _get_editor_tab is not documented ! To find it, do you open editor.py in the site-packages of Standard Library ?
It's very instructive πŸ€“, I'm going to end up managing on my own. πŸ˜‰

cvp

@Enez-Houad said:

do you open editor.py in the site-packages of Standard Library ?

Yes sir

cvp

the problem gets worse, and don't ask me how it happens...

cvp

I begin to panic

janplxt

With me on iOS 14.3 / iPad it seems to have something to do with the β€œdarkmode” setting. When running in β€œlight” mode the problem did not occur yet. When I switch back to dark mode, the extra keyboard row splits again after switching between file tabs in the editor.
so, maybe use light mode for Pythonista?

cvp

@janplxt said:

iPad it seems to have something to do with the β€œdarkmode” setting.

I don't think so, see my first examples in the topic.