본문 바로가기
C.W.K.
Stream
Lesson 02 of 05 · published

전역에서 긁어놓은 글까지

~12 min · axuielement, focused-element, selected-text, range

Level 0식은 부싯돌
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"전체에서 시작해 포커스 쥔 놈으로 좁히고, 거기다 글을 물어봐. 세 걸음이면 입력이 손에 들어와."

전역에서 포커스로

읽는 건 질문 몇 개가 이어진 짧은 사슬이야. 시스템 전역 요소를 만들고, 거기서 kAXFocusedUIElementAttribute 를 가져와. 그러면 지금 앞에 있는 앱에서 키보드 포커스를 쥔 요소가 나와. 그게 어느 앱인지는 내가 알 필요도 없어. AX 가 '지금 포커스 쥔 놈' 을 알아서 찾아주거든. 그 요소가 앞으로 캐물을 마디고.

긁어놓은 글 물어보기

그 요소한테 두 가지를 더 물어봐. kAXSelectedTextAttribute 로 하이라이트된 글자를, kAXSelectedTextRangeAttribute 로 어디서 어디까지인지를. 둘 다 필요해. 글은 {input} 이 되고, 범위는 나중에 결과를 원래 자리에 정확히 돌려놓을 때 쓰거든. 이 범위를 지금, 읽는 그 순간에 챙겨두는 게 왕복이 끝난 뒤에도 제자리에 딱 맞춰 넣을 수 있게 해줘.

시스템 전역 요소
   kAXFocusedUIElementAttribute  -> 포커스 쥔 요소
        kAXSelectedTextAttribute       -> "하이라이트된 글"
        kAXSelectedTextRangeAttribute  -> 범위 { 시작, 길이 }

글은 {input} 이 되고, 범위는 넣을 때 쓰려고 기억해둔다.

아무것도 안 긁혀 있을 때

칠 때마다 뭔가 긁혀 있는 건 아니야. 그냥 커서만 깜빡이고 있을 수도 있지. 그것도 에러가 아니라 멀쩡한 상태고. Flint 의 규칙은 분명해. 긁어놓은 글이 있으면 그게 {input} 이 되고 결과가 그 자리를 대신해. 없으면 입력이 비고, 매크로가 만든 글이 뭘 대신하는 대신 커서 자리에 들어가. 그래서 읽는 쪽은 '빈 채로 긁혔다' 랑 '포커스된 요소가 아예 없다' 를 구별해서 알려줘야 해. 그 둘이 이후에 완전히 다른 길로 갈라지거든.

범위는 넣을 때 말고 읽을 때 챙겨. 긁어놓은 글만 잡아두고 범위는 나중에 넣을 때 다시 구하고 싶어져. 그러지 마. 넣을 즈음이면 긁어놓은 게 이미 사라졌을 수 있거든. 칠 때 챙긴 범위는 '그 순간 내가 뭘 하려 했는지' 에 대한 사실이야. 왕복이 끝난 뒤 다시 읽으면 그새 달라진 딴 세상을 읽는 거고. 글이랑 범위는 처음에 같이 챙겨.
역할까지 같이 물어봐. 온 김에 kAXRoleAttribute 랑 kAXSubroleAttribute 도 챙겨. 대상이 어떤 종류인지 알려주거든. 여러 줄 글 칸인지, 검색 칸인지, 비밀번호 칸인지. 나중에 어떻게 넣을지에도 쓰이고, 무엇보다 비밀번호 칸은 아예 안 건드리는 데(Track 7) 필요해. 읽을 때 질문 하나 더 던져두면 나중에 짐작할 일이 없어져.

Code

읽기: 포커스된 요소, 그다음 긁어놓은 글과 범위·swift
func readSelection() -> (text: String?, range: CFRange?, element: AXUIElement)? {
    let system = AXUIElementCreateSystemWide()

    // 1. 포커스 쥔 요소. 어느 앱에 있든 상관없다.
    var focusedRef: CFTypeRef?
    guard AXUIElementCopyAttributeValue(
            system, kAXFocusedUIElementAttribute as CFString, &focusedRef
          ) == .success else { return nil }        // nil = 포커스된 요소가 없음
    let element = focusedRef as! AXUIElement

    // 2. 긁어놓은 글. 비어 있을 수 있고, 그건 실패가 아니라 멀쩡한 상태다.
    var textRef: CFTypeRef?
    let text = AXUIElementCopyAttributeValue(
        element, kAXSelectedTextAttribute as CFString, &textRef
    ) == .success ? (textRef as? String) : nil

    // 3. 범위. AXValue 로 싸여 온다. 나중에 넣을 때 쓴다.
    var range: CFRange? = nil
    var rangeRef: CFTypeRef?
    if AXUIElementCopyAttributeValue(
         element, kAXSelectedTextRangeAttribute as CFString, &rangeRef
       ) == .success, let v = rangeRef, CFGetTypeID(v) == AXValueGetTypeID() {
        var r = CFRange()
        if AXValueGetValue(v as! AXValue, .cfRange, &r) { range = r }
    }
    return (text, range, element)
}

External links

Exercise

읽기 함수가 세 가지 상황에서 뭘 돌려주는지 따라가 봐. 첫째, 네이티브 에디터에서 글이 하이라이트돼 있을 때. 둘째, 커서는 칸 안에 있는데 아무것도 안 긁혀 있을 때. 셋째, 포커스를 쥔 요소가 아예 없을 때. 각각 {input} 이 뭐가 되고 친 결과가 어떻게 돼야 할까? 그리고 반환값이 왜 '빈 채로 긁혔다' 랑 '포커스된 요소가 없다' 를 구별해야 하는지도.
Hint
첫째는 글이랑 범위가 다 있으니 {input} 은 그 글이고 결과가 그 자리를 대신해. 둘째는 요소는 있는데 긁힌 게 없으니 {input} 은 비고 결과가 커서 자리에 들어가고. 셋째는 포커스된 요소가 없으니 안전하게 손댈 데가 없어. 구별이 중요한 이유는 둘째가 커서에 넣는 정상 경로인 반면, 셋째는 칠 대상 자체가 없어서 아예 진행하면 안 되는 상황이기 때문이야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.