Hammerspoon / hammerspoon

Staggeringly powerful macOS desktop automation with Lua
http://www.hammerspoon.org
MIT License
12.1k stars 582 forks source link

hs.chooser enhancements #3274

Open bviefhues opened 2 years ago

bviefhues commented 2 years ago

Would be great if sometime hs.chooser could get these configuration enhancements:

cmsj commented 2 years ago

Yep, I would love to have pretty much all of these. We've struggled a lot to get the hs.chooser UI working even as well as it does now, so I think if I was going to try again to make it even more dynamic, I would want to rewrite the whole thing and probably use SwiftUI. I can't make any promises about when we'll be able to do that though!

cmsj commented 2 years ago

There is a long way to go in terms of the behaviours and keyboard navigation, but I have a very barebones SwiftUI Playground up and running that looks kinda like a Chooser:

import SwiftUI
import PlaygroundSupport

struct ChooserResult: Hashable {
    var icon: String?
    var text: String
    var subText: String?
}

struct ContentView: View {
    @State private var showQuery = true
    @State private var showDetail = true

    @State private var queryString: String = ""

    @State private var results:[ChooserResult] = []

    var body: some View {
        let queryBinding = Binding<String>(get: { self.queryString }, set: {
            self.queryString = $0
            self.results.append(ChooserResult(icon: "app.gift", text: "lol", subText: "asdf"))
            self.results.append(ChooserResult(icon: "scribble", text: "otherlol"))
        })
        VStack {
            if (showQuery) {
                HStack {
                    Image(systemName: "magnifyingglass")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .foregroundColor(.gray)
                        .frame(maxWidth: 30, maxHeight: 30)
                        .padding([.leading])
                    TextField("Type query...", text: queryBinding)
                        .font(.largeTitle)
                        .padding([.trailing, .top, .bottom])
                    //                        .border(.red)
                }
            }
            if (results.count > 0) {
                HStack {
                    List {
                        ForEach(results, id: \.self) { result in
                            HStack {
                                Image(systemName: result.icon ?? "arrow.forward.circle")
                                    .resizable()
                                    .aspectRatio(contentMode: .fit)
                                    .frame(maxWidth: 40, maxHeight: 40)
                                VStack {
                                    if (result.subText != nil) {
                                        Text(result.text)
                                            .font(.title)
                                            .padding([.leading, .trailing, .top])
                                        Text(result.subText!)
                                            .foregroundColor(.gray)
                                            .padding([.leading, .trailing, .bottom])
                                    } else {
                                        Text(result.text)
                                            .font(.title)
                                            .padding()
                                    }
                                }
                                Spacer()
                                Text("%1")
                                    .font(.title)
                                    .foregroundColor(.gray)
                            }
                            .padding([.leading, .trailing])
                        }
                    }
                    .listStyle(.plain)
                    //                .border(.green)
                    if (showDetail) {
                        VStack {
                            Text("Detail here")
                                .padding()
                            //                            .border(.yellow)
                            Spacer()
                            //                            .border(.gray)
                        }
                    }
                }
            }
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())
unphased commented 2 months ago

I have a question about existing chooser implementation, which I couldn't find much info on. Can we customize the hotkeys to pick specific items beyond cmd+number? Because I would prefer to just hit a number key, but ideally I would be able to explicitly specify the hotkey for any given choice.

latenitefilms commented 2 months ago

You could use a separate hs.eventtap to watch for keypresses, but not sure how you'd know if the user is typing a number versus pressing a number to trigger a chooser line item.

specter78 commented 2 months ago

I would like to add a request to chooser enhancements. Ability to use emoji/favicon in place of image, so that it does not look like the image attached.

latenitefilms commented 2 months ago

You could use hs.canvas to create a new image that contains the emoji, and use that in hs.chooser?

specter78 commented 2 months ago

You could use hs.canvas to create a new image that contains the emoji, and use that in hs.chooser?

I've tried looking into the docs, but cannot understand how to do it. Is it possible for you to share a working code?

latenitefilms commented 2 months ago

Interestingly, ChatGPT 4o gives me this, which actually works!

-- Create a canvas and draw an emoji on it
local function createEmojiImage(emoji, size)
    local canvas = hs.canvas.new({x = 0, y = 0, w = size, h = size})

    -- Draw the emoji in the center of the canvas
    canvas[1] = {
        type = "text",
        text = emoji,
        textSize = size * 0.8,
        textAlignment = "center",
        frame = {x = "0%", y = "0%", w = "100%", h = "100%"},
    }

    -- Convert the canvas to an image
    local image = canvas:imageFromCanvas()
    canvas:delete()

    return image
end

-- Example usage in hs.chooser
local function showEmojiChooser()
    local chooser = hs.chooser.new(function(choice)
        if choice then
            hs.alert.show("You chose: " .. choice.text)
        end
    end)

    local emojiList = {"😀", "😂", "😎", "❤️", "👍"}
    local choices = {}

    for i, emoji in ipairs(emojiList) do
        local emojiImage = createEmojiImage(emoji, 64) -- create a 64x64 image
        table.insert(choices, {
            text = emoji,
            image = emojiImage,
        })
    end

    chooser:choices(choices)
    chooser:show()
end

-- Call the function to show the chooser
showEmojiChooser()
specter78 commented 2 months ago

@latenitefilms Thank you. That helped.

Just want to mention here, that keeping textSize = size does not properly align the image. So the image needs to be shifted frame = {x = "0%", y = "-15%", w = "100%", h = "100%"}.

s s

While such an issue is not found (or maybe not noticeable) if textSize = size * 0.8, but then the image is smaller.

s
unphased commented 2 months ago

Thanks for sharing the tips. So we are able to set icons via emoji in that way? Awesome.

I wanted to be able to set different background colors for choices but marking them out with different icons will be effective too.

latenitefilms commented 2 months ago

You could customise the colour behind the emoji.