diff options
Diffstat (limited to 'src/main.go')
| -rw-r--r-- | src/main.go | 1047 |
1 files changed, 800 insertions, 247 deletions
diff --git a/src/main.go b/src/main.go index bab289c..910aef8 100644 --- a/src/main.go +++ b/src/main.go | |||
| @@ -4,10 +4,14 @@ import ( | |||
| 4 | "encoding/json" | 4 | "encoding/json" |
| 5 | "fmt" | 5 | "fmt" |
| 6 | "log" | 6 | "log" |
| 7 | "math" | ||
| 8 | "math/rand" | ||
| 7 | "os" | 9 | "os" |
| 8 | "os/exec" | 10 | "os/exec" |
| 9 | "path/filepath" | 11 | "path/filepath" |
| 12 | "sort" | ||
| 10 | "strings" | 13 | "strings" |
| 14 | "time" | ||
| 11 | 15 | ||
| 12 | "github.com/gotk3/gotk3/gdk" | 16 | "github.com/gotk3/gotk3/gdk" |
| 13 | "github.com/gotk3/gotk3/glib" | 17 | "github.com/gotk3/gotk3/glib" |
| @@ -15,8 +19,9 @@ import ( | |||
| 15 | ) | 19 | ) |
| 16 | 20 | ||
| 17 | const ( | 21 | const ( |
| 18 | appTitle = "kjagave" | 22 | appTitle = "kjagave" |
| 19 | appVersion = "20251221-0050" | 23 | appVersion = "20260315-0200" |
| 24 | maxHistoryLen = 250 | ||
| 20 | ) | 25 | ) |
| 21 | 26 | ||
| 22 | type SavedColor struct { | 27 | type SavedColor struct { |
| @@ -24,316 +29,613 @@ type SavedColor struct { | |||
| 24 | Name string `json:"name"` | 29 | Name string `json:"name"` |
| 25 | } | 30 | } |
| 26 | 31 | ||
| 32 | type AppConfig struct { | ||
| 33 | Favorites []SavedColor `json:"favorites"` | ||
| 34 | LastColor string `json:"lastColor"` | ||
| 35 | LastScheme string `json:"lastScheme"` | ||
| 36 | Palette string `json:"palette"` | ||
| 37 | } | ||
| 38 | |||
| 39 | type SwatchCard struct { | ||
| 40 | button *gtk.Button | ||
| 41 | image *gtk.Image | ||
| 42 | label *gtk.Label | ||
| 43 | hex string | ||
| 44 | } | ||
| 45 | |||
| 27 | type App struct { | 46 | type App struct { |
| 28 | window *gtk.Window | 47 | window *gtk.Window |
| 48 | |||
| 29 | colorButton *gtk.ColorButton | 49 | colorButton *gtk.ColorButton |
| 50 | hexEntry *gtk.Entry | ||
| 51 | schemeCombo *gtk.ComboBoxText | ||
| 52 | paletteCombo *gtk.ComboBoxText | ||
| 53 | |||
| 54 | historyBackBtn *gtk.Button | ||
| 55 | historyFwdBtn *gtk.Button | ||
| 56 | lightenBtn *gtk.Button | ||
| 57 | darkenBtn *gtk.Button | ||
| 58 | saturateBtn *gtk.Button | ||
| 59 | desaturateBtn *gtk.Button | ||
| 60 | |||
| 61 | swatchCards []SwatchCard | ||
| 62 | |||
| 63 | paletteStore *gtk.ListStore | ||
| 64 | paletteView *gtk.TreeView | ||
| 65 | |||
| 66 | favoritesStore *gtk.ListStore | ||
| 67 | favoritesView *gtk.TreeView | ||
| 68 | removeFavBtn *gtk.Button | ||
| 69 | clearFavBtn *gtk.Button | ||
| 70 | selectedIter *gtk.TreeIter | ||
| 71 | |||
| 30 | currentColor *gdk.RGBA | 72 | currentColor *gdk.RGBA |
| 31 | listStore *gtk.ListStore | 73 | currentHex string |
| 32 | treeView *gtk.TreeView | 74 | |
| 33 | deleteBtn *gtk.Button | 75 | savedColors []SavedColor |
| 34 | savedColors []SavedColor | 76 | history []string |
| 35 | configFile string | 77 | historyPos int |
| 36 | selectedIter *gtk.TreeIter | 78 | |
| 79 | configFile string | ||
| 80 | config AppConfig | ||
| 81 | |||
| 82 | suppressColorSet bool | ||
| 83 | rng *rand.Rand | ||
| 84 | } | ||
| 85 | |||
| 86 | var schemeNames = []string{ | ||
| 87 | "Triads", | ||
| 88 | "Complements", | ||
| 89 | "Split Complements", | ||
| 90 | "Tetrads", | ||
| 91 | "Analogous", | ||
| 92 | "Monochromatic", | ||
| 93 | } | ||
| 94 | |||
| 95 | var paletteNames = []string{ | ||
| 96 | "Web-safe colors", | ||
| 97 | "Tango", | ||
| 98 | "Visibone Core", | ||
| 37 | } | 99 | } |
| 38 | 100 | ||
| 39 | func main() { | 101 | func main() { |
| 40 | gtk.Init(nil) | 102 | gtk.Init(nil) |
| 41 | 103 | ||
| 42 | configDir := filepath.Join(os.Getenv("HOME"), ".config") | 104 | configDir := filepath.Join(os.Getenv("HOME"), ".config") |
| 43 | os.MkdirAll(configDir, 0755) | 105 | _ = os.MkdirAll(configDir, 0755) |
| 44 | 106 | ||
| 45 | app := &App{ | 107 | app := &App{ |
| 46 | configFile: filepath.Join(configDir, "kjagave.json"), | 108 | configFile: filepath.Join(configDir, "kjagave.json"), |
| 109 | rng: rand.New(rand.NewSource(time.Now().UnixNano())), | ||
| 110 | historyPos: -1, | ||
| 47 | } | 111 | } |
| 48 | 112 | ||
| 49 | app.loadColors() | 113 | app.loadConfig() |
| 50 | app.createUI() | 114 | app.createUI() |
| 51 | app.populateList() | 115 | app.refreshFavoritesView() |
| 116 | app.restoreStartupState() | ||
| 117 | app.populatePaletteList() | ||
| 118 | app.updateSchemePreview() | ||
| 119 | app.updateActionStates() | ||
| 52 | 120 | ||
| 53 | gtk.Main() | 121 | gtk.Main() |
| 54 | } | 122 | } |
| 55 | 123 | ||
| 56 | func (app *App) createUI() { | 124 | func (app *App) createUI() { |
| 57 | var err error | 125 | var err error |
| 58 | |||
| 59 | // main window | ||
| 60 | app.window, err = gtk.WindowNew(gtk.WINDOW_TOPLEVEL) | 126 | app.window, err = gtk.WindowNew(gtk.WINDOW_TOPLEVEL) |
| 61 | if err != nil { | 127 | if err != nil { |
| 62 | log.Fatal("Unable to create window:", err) | 128 | log.Fatal("Unable to create window:", err) |
| 63 | } | 129 | } |
| 64 | app.window.SetTitle(appTitle) | 130 | app.window.SetTitle(appTitle) |
| 65 | app.window.SetDefaultSize(550, 450) | 131 | app.window.SetDefaultSize(980, 680) |
| 66 | app.window.SetResizable(false) | 132 | app.window.Connect("destroy", func() { |
| 67 | app.window.Connect("destroy", gtk.MainQuit) | 133 | app.saveConfig() |
| 134 | gtk.MainQuit() | ||
| 135 | }) | ||
| 136 | |||
| 137 | root, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 8) | ||
| 138 | root.SetMarginTop(8) | ||
| 139 | root.SetMarginBottom(8) | ||
| 140 | root.SetMarginStart(8) | ||
| 141 | root.SetMarginEnd(8) | ||
| 142 | |||
| 143 | toolbar, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 4) | ||
| 144 | app.historyBackBtn, _ = gtk.ButtonNewWithLabel("Back") | ||
| 145 | app.historyBackBtn.Connect("clicked", func() { app.navigateHistory(-1) }) | ||
| 146 | toolbar.PackStart(app.historyBackBtn, false, false, 0) | ||
| 68 | 147 | ||
| 69 | // vertical box | 148 | app.historyFwdBtn, _ = gtk.ButtonNewWithLabel("Forward") |
| 70 | mainBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 10) | 149 | app.historyFwdBtn.Connect("clicked", func() { app.navigateHistory(1) }) |
| 71 | mainBox.SetMarginTop(15) | 150 | toolbar.PackStart(app.historyFwdBtn, false, false, 0) |
| 72 | mainBox.SetMarginBottom(15) | ||
| 73 | mainBox.SetMarginStart(15) | ||
| 74 | mainBox.SetMarginEnd(15) | ||
| 75 | 151 | ||
| 76 | // color selection area | 152 | randomBtn, _ := gtk.ButtonNewWithLabel("Random") |
| 77 | colorBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 10) | 153 | randomBtn.Connect("clicked", func() { app.randomizeColor() }) |
| 78 | colorBox.SetHAlign(gtk.ALIGN_CENTER) | 154 | toolbar.PackStart(randomBtn, false, false, 0) |
| 79 | 155 | ||
| 80 | label, _ := gtk.LabelNew("Select Color:") | 156 | app.lightenBtn, _ = gtk.ButtonNewWithLabel("Lighten") |
| 81 | colorBox.PackStart(label, false, false, 0) | 157 | app.lightenBtn.Connect("clicked", func() { app.adjustSV(0, 5) }) |
| 158 | toolbar.PackStart(app.lightenBtn, false, false, 0) | ||
| 82 | 159 | ||
| 83 | // initialize color | 160 | app.darkenBtn, _ = gtk.ButtonNewWithLabel("Darken") |
| 161 | app.darkenBtn.Connect("clicked", func() { app.adjustSV(0, -5) }) | ||
| 162 | toolbar.PackStart(app.darkenBtn, false, false, 0) | ||
| 163 | |||
| 164 | app.saturateBtn, _ = gtk.ButtonNewWithLabel("Saturate") | ||
| 165 | app.saturateBtn.Connect("clicked", func() { app.adjustSV(5, 0) }) | ||
| 166 | toolbar.PackStart(app.saturateBtn, false, false, 0) | ||
| 167 | |||
| 168 | app.desaturateBtn, _ = gtk.ButtonNewWithLabel("Desaturate") | ||
| 169 | app.desaturateBtn.Connect("clicked", func() { app.adjustSV(-5, 0) }) | ||
| 170 | toolbar.PackStart(app.desaturateBtn, false, false, 0) | ||
| 171 | |||
| 172 | pasteBtn, _ := gtk.ButtonNewWithLabel("Paste") | ||
| 173 | pasteBtn.Connect("clicked", func() { app.pasteColorFromClipboard() }) | ||
| 174 | toolbar.PackStart(pasteBtn, false, false, 0) | ||
| 175 | |||
| 176 | aboutBtn, _ := gtk.ButtonNewWithLabel("About") | ||
| 177 | aboutBtn.Connect("clicked", func() { app.onAboutClicked() }) | ||
| 178 | toolbar.PackStart(aboutBtn, false, false, 0) | ||
| 179 | |||
| 180 | root.PackStart(toolbar, false, false, 0) | ||
| 181 | |||
| 182 | schemeRow, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 8) | ||
| 183 | app.swatchCards = make([]SwatchCard, 0, 4) | ||
| 184 | for i := 0; i < 4; i++ { | ||
| 185 | card := app.newSwatchCard() | ||
| 186 | cardIdx := i | ||
| 187 | card.button.Connect("clicked", func() { | ||
| 188 | hex := app.swatchCards[cardIdx].hex | ||
| 189 | if hex == "" { | ||
| 190 | return | ||
| 191 | } | ||
| 192 | rgba := gdk.NewRGBA() | ||
| 193 | if !rgba.Parse(hex) { | ||
| 194 | return | ||
| 195 | } | ||
| 196 | app.setCurrentColor(rgba, true) | ||
| 197 | }) | ||
| 198 | app.swatchCards = append(app.swatchCards, card) | ||
| 199 | schemeRow.PackStart(card.button, true, true, 0) | ||
| 200 | } | ||
| 201 | root.PackStart(schemeRow, false, false, 0) | ||
| 202 | |||
| 203 | controlRow, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 8) | ||
| 84 | app.currentColor = gdk.NewRGBA() | 204 | app.currentColor = gdk.NewRGBA() |
| 85 | app.currentColor.Parse("#69BAA7") | 205 | app.currentColor.Parse("#0066FF") |
| 86 | 206 | ||
| 87 | app.colorButton, err = gtk.ColorButtonNewWithRGBA(app.currentColor) | 207 | app.colorButton, err = gtk.ColorButtonNewWithRGBA(app.currentColor) |
| 88 | if err != nil { | 208 | if err != nil { |
| 89 | log.Fatal("Unable to create color button:", err) | 209 | log.Fatal("Unable to create color button:", err) |
| 90 | } | 210 | } |
| 91 | app.colorButton.SetUseAlpha(true) | 211 | app.colorButton.SetUseAlpha(false) |
| 92 | app.colorButton.SetTitle("Choose a Color") | 212 | app.colorButton.SetTitle("Pick a Color") |
| 93 | app.colorButton.Connect("color-set", func() { | 213 | app.colorButton.Connect("color-set", func() { |
| 94 | app.currentColor = app.colorButton.GetRGBA() | 214 | if app.suppressColorSet { |
| 215 | return | ||
| 216 | } | ||
| 217 | app.setCurrentColor(app.colorButton.GetRGBA(), true) | ||
| 95 | }) | 218 | }) |
| 219 | controlRow.PackStart(app.colorButton, false, false, 0) | ||
| 96 | 220 | ||
| 97 | colorBox.PackStart(app.colorButton, false, false, 0) | 221 | app.schemeCombo, _ = gtk.ComboBoxTextNew() |
| 98 | 222 | for _, schemeName := range schemeNames { | |
| 99 | hexEntry, _ := gtk.EntryNew() | 223 | app.schemeCombo.AppendText(schemeName) |
| 100 | hexEntry.SetEditable(false) | 224 | } |
| 101 | hexEntry.SetWidthChars(10) | 225 | app.schemeCombo.SetActive(0) |
| 102 | hexEntry.SetText(rgbaToHex(app.currentColor)) | 226 | app.schemeCombo.Connect("changed", func() { |
| 103 | colorBox.PackStart(hexEntry, false, false, 0) | 227 | app.updateSchemePreview() |
| 104 | 228 | app.saveConfig() | |
| 105 | // color picker button | 229 | }) |
| 106 | pickerBtn, _ := gtk.ButtonNewWithLabel("Pick from Screen") | 230 | controlRow.PackStart(app.schemeCombo, false, false, 0) |
| 107 | pickerBtn.Connect("clicked", func() { | 231 | |
| 108 | if color, err := app.pickColorFromScreen(); err == nil { | 232 | app.hexEntry, _ = gtk.EntryNew() |
| 109 | app.colorButton.SetRGBA(color) | 233 | app.hexEntry.SetWidthChars(11) |
| 110 | app.currentColor = color | 234 | app.hexEntry.Connect("activate", func() { app.applyHexEntry() }) |
| 111 | hexEntry.SetText(rgbaToHex(color)) | 235 | controlRow.PackStart(app.hexEntry, false, false, 0) |
| 236 | |||
| 237 | useHexBtn, _ := gtk.ButtonNewWithLabel("Use") | ||
| 238 | useHexBtn.Connect("clicked", func() { app.applyHexEntry() }) | ||
| 239 | controlRow.PackStart(useHexBtn, false, false, 0) | ||
| 240 | |||
| 241 | pickBtn, _ := gtk.ButtonNewWithLabel("Pick from Screen") | ||
| 242 | pickBtn.Connect("clicked", func() { | ||
| 243 | clr, err := app.pickColorFromScreen() | ||
| 244 | if err == nil { | ||
| 245 | app.setCurrentColor(clr, true) | ||
| 112 | } | 246 | } |
| 113 | }) | 247 | }) |
| 114 | colorBox.PackStart(pickerBtn, false, false, 0) | 248 | controlRow.PackStart(pickBtn, false, false, 0) |
| 115 | 249 | ||
| 116 | // bump hex entry when color changes | 250 | copyBtn, _ := gtk.ButtonNewWithLabel("Copy") |
| 117 | app.colorButton.Connect("color-set", func() { | 251 | copyBtn.Connect("clicked", func() { |
| 118 | app.currentColor = app.colorButton.GetRGBA() | 252 | clipboard, _ := gtk.ClipboardGet(gdk.SELECTION_CLIPBOARD) |
| 119 | hexEntry.SetText(rgbaToHex(app.currentColor)) | 253 | clipboard.SetText(app.currentHex) |
| 120 | }) | 254 | }) |
| 255 | controlRow.PackStart(copyBtn, false, false, 0) | ||
| 121 | 256 | ||
| 122 | mainBox.PackStart(colorBox, false, false, 0) | 257 | root.PackStart(controlRow, false, false, 0) |
| 123 | |||
| 124 | expander, _ := gtk.ExpanderNew("Saved Colors") | ||
| 125 | expander.SetExpanded(true) | ||
| 126 | expanderBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 5) | ||
| 127 | expanderBox.SetMarginTop(5) | ||
| 128 | expanderBox.SetMarginBottom(5) | ||
| 129 | btnBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5) | ||
| 130 | btnBox.SetHAlign(gtk.ALIGN_END) | ||
| 131 | app.deleteBtn, _ = gtk.ButtonNewWithLabel("Delete") | ||
| 132 | app.deleteBtn.SetSensitive(false) | ||
| 133 | app.deleteBtn.Connect("clicked", app.onDeleteClicked) | ||
| 134 | btnBox.PackStart(app.deleteBtn, false, false, 0) | ||
| 135 | saveBtn, _ := gtk.ButtonNewWithLabel("Save...") | ||
| 136 | saveBtn.Connect("clicked", app.onSaveClicked) | ||
| 137 | btnBox.PackStart(saveBtn, false, false, 0) | ||
| 138 | expanderBox.PackStart(btnBox, false, false, 0) | ||
| 139 | |||
| 140 | // treeview | ||
| 141 | scrolled, _ := gtk.ScrolledWindowNew(nil, nil) | ||
| 142 | scrolled.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) | ||
| 143 | scrolled.SetSizeRequest(-1, 180) | ||
| 144 | |||
| 145 | app.listStore, _ = gtk.ListStoreNew(gdk.PixbufGetType(), glib.TYPE_STRING, glib.TYPE_STRING) | ||
| 146 | app.treeView, _ = gtk.TreeViewNew() | ||
| 147 | app.treeView.SetModel(app.listStore) | ||
| 148 | app.treeView.SetHeadersVisible(true) | ||
| 149 | |||
| 150 | // color column with swatch | ||
| 151 | colorCol, _ := gtk.TreeViewColumnNew() | ||
| 152 | colorCol.SetTitle("Color") | ||
| 153 | colorCol.SetSortColumnID(1) | ||
| 154 | |||
| 155 | pixbufRenderer, _ := gtk.CellRendererPixbufNew() | ||
| 156 | colorCol.PackStart(pixbufRenderer, false) | ||
| 157 | colorCol.AddAttribute(pixbufRenderer, "pixbuf", 0) | ||
| 158 | |||
| 159 | textRenderer, _ := gtk.CellRendererTextNew() | ||
| 160 | colorCol.PackStart(textRenderer, true) | ||
| 161 | colorCol.AddAttribute(textRenderer, "text", 1) | ||
| 162 | |||
| 163 | app.treeView.AppendColumn(colorCol) | ||
| 164 | |||
| 165 | // name column | ||
| 166 | nameRenderer, _ := gtk.CellRendererTextNew() | ||
| 167 | nameCol, _ := gtk.TreeViewColumnNewWithAttribute("Name", nameRenderer, "text", 2) | ||
| 168 | nameCol.SetSortColumnID(2) | ||
| 169 | app.treeView.AppendColumn(nameCol) | ||
| 170 | |||
| 171 | selection, _ := app.treeView.GetSelection() | ||
| 172 | selection.SetMode(gtk.SELECTION_SINGLE) | ||
| 173 | selection.Connect("changed", app.onSelectionChanged) | ||
| 174 | |||
| 175 | scrolled.Add(app.treeView) | ||
| 176 | expanderBox.PackStart(scrolled, true, true, 0) | ||
| 177 | expander.Add(expanderBox) | ||
| 178 | mainBox.PackStart(expander, true, true, 0) | ||
| 179 | |||
| 180 | // bottom button box | ||
| 181 | bottomBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5) | ||
| 182 | bottomBox.SetHAlign(gtk.ALIGN_END) | ||
| 183 | |||
| 184 | copyBtn, _ := gtk.ButtonNewWithLabel("Copy to Clipboard") | ||
| 185 | copyBtn.Connect("clicked", app.onCopyClicked) | ||
| 186 | bottomBox.PackStart(copyBtn, false, false, 0) | ||
| 187 | 258 | ||
| 188 | aboutBtn, _ := gtk.ButtonNewWithLabel("About") | 259 | lower, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 8) |
| 189 | aboutBtn.Connect("clicked", app.onAboutClicked) | 260 | |
| 190 | bottomBox.PackStart(aboutBtn, false, false, 0) | 261 | paletteFrame, _ := gtk.FrameNew("Palette") |
| 262 | paletteBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 6) | ||
| 263 | paletteBox.SetMarginTop(8) | ||
| 264 | paletteBox.SetMarginBottom(8) | ||
| 265 | paletteBox.SetMarginStart(8) | ||
| 266 | paletteBox.SetMarginEnd(8) | ||
| 267 | |||
| 268 | app.paletteCombo, _ = gtk.ComboBoxTextNew() | ||
| 269 | for _, name := range paletteNames { | ||
| 270 | app.paletteCombo.AppendText(name) | ||
| 271 | } | ||
| 272 | app.paletteCombo.SetActive(0) | ||
| 273 | app.paletteCombo.Connect("changed", func() { | ||
| 274 | app.populatePaletteList() | ||
| 275 | app.saveConfig() | ||
| 276 | }) | ||
| 277 | paletteBox.PackStart(app.paletteCombo, false, false, 0) | ||
| 278 | |||
| 279 | paletteScroll, _ := gtk.ScrolledWindowNew(nil, nil) | ||
| 280 | paletteScroll.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) | ||
| 281 | paletteScroll.SetSizeRequest(430, 260) | ||
| 282 | |||
| 283 | app.paletteStore, _ = gtk.ListStoreNew(gdk.PixbufGetType(), glib.TYPE_STRING) | ||
| 284 | app.paletteView, _ = gtk.TreeViewNew() | ||
| 285 | app.paletteView.SetModel(app.paletteStore) | ||
| 286 | app.paletteView.SetHeadersVisible(false) | ||
| 287 | paletteCol, _ := gtk.TreeViewColumnNew() | ||
| 288 | palettePix, _ := gtk.CellRendererPixbufNew() | ||
| 289 | paletteCol.PackStart(palettePix, false) | ||
| 290 | paletteCol.AddAttribute(palettePix, "pixbuf", 0) | ||
| 291 | paletteText, _ := gtk.CellRendererTextNew() | ||
| 292 | paletteCol.PackStart(paletteText, true) | ||
| 293 | paletteCol.AddAttribute(paletteText, "text", 1) | ||
| 294 | app.paletteView.AppendColumn(paletteCol) | ||
| 295 | palSel, _ := app.paletteView.GetSelection() | ||
| 296 | palSel.SetMode(gtk.SELECTION_SINGLE) | ||
| 297 | palSel.Connect("changed", app.onPaletteSelectionChanged) | ||
| 298 | paletteScroll.Add(app.paletteView) | ||
| 299 | paletteBox.PackStart(paletteScroll, true, true, 0) | ||
| 300 | paletteFrame.Add(paletteBox) | ||
| 301 | lower.PackStart(paletteFrame, true, true, 0) | ||
| 302 | |||
| 303 | favFrame, _ := gtk.FrameNew("Favorites") | ||
| 304 | favBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 6) | ||
| 305 | favBox.SetMarginTop(8) | ||
| 306 | favBox.SetMarginBottom(8) | ||
| 307 | favBox.SetMarginStart(8) | ||
| 308 | favBox.SetMarginEnd(8) | ||
| 309 | |||
| 310 | favScroll, _ := gtk.ScrolledWindowNew(nil, nil) | ||
| 311 | favScroll.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) | ||
| 312 | favScroll.SetSizeRequest(260, 260) | ||
| 313 | |||
| 314 | app.favoritesStore, _ = gtk.ListStoreNew(gdk.PixbufGetType(), glib.TYPE_STRING, glib.TYPE_STRING) | ||
| 315 | app.favoritesView, _ = gtk.TreeViewNew() | ||
| 316 | app.favoritesView.SetModel(app.favoritesStore) | ||
| 317 | app.favoritesView.SetHeadersVisible(false) | ||
| 318 | |||
| 319 | favCol, _ := gtk.TreeViewColumnNew() | ||
| 320 | favPix, _ := gtk.CellRendererPixbufNew() | ||
| 321 | favCol.PackStart(favPix, false) | ||
| 322 | favCol.AddAttribute(favPix, "pixbuf", 0) | ||
| 323 | favHexText, _ := gtk.CellRendererTextNew() | ||
| 324 | favCol.PackStart(favHexText, true) | ||
| 325 | favCol.AddAttribute(favHexText, "text", 1) | ||
| 326 | app.favoritesView.AppendColumn(favCol) | ||
| 327 | |||
| 328 | favNameText, _ := gtk.CellRendererTextNew() | ||
| 329 | favNameCol, _ := gtk.TreeViewColumnNewWithAttribute("", favNameText, "text", 2) | ||
| 330 | app.favoritesView.AppendColumn(favNameCol) | ||
| 331 | |||
| 332 | favSel, _ := app.favoritesView.GetSelection() | ||
| 333 | favSel.SetMode(gtk.SELECTION_SINGLE) | ||
| 334 | favSel.Connect("changed", app.onFavoriteSelectionChanged) | ||
| 335 | |||
| 336 | favScroll.Add(app.favoritesView) | ||
| 337 | favBox.PackStart(favScroll, true, true, 0) | ||
| 338 | |||
| 339 | favBtns, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 4) | ||
| 340 | addFavBtn, _ := gtk.ButtonNewWithLabel("+") | ||
| 341 | addFavBtn.Connect("clicked", func() { app.addCurrentToFavorites() }) | ||
| 342 | favBtns.PackStart(addFavBtn, true, true, 0) | ||
| 343 | |||
| 344 | app.removeFavBtn, _ = gtk.ButtonNewWithLabel("-") | ||
| 345 | app.removeFavBtn.Connect("clicked", func() { app.removeSelectedFavorite() }) | ||
| 346 | favBtns.PackStart(app.removeFavBtn, true, true, 0) | ||
| 347 | |||
| 348 | app.clearFavBtn, _ = gtk.ButtonNewWithLabel("Clear") | ||
| 349 | app.clearFavBtn.Connect("clicked", func() { | ||
| 350 | app.savedColors = nil | ||
| 351 | app.refreshFavoritesView() | ||
| 352 | app.saveConfig() | ||
| 353 | }) | ||
| 354 | favBtns.PackStart(app.clearFavBtn, true, true, 0) | ||
| 355 | |||
| 356 | exportBtn, _ := gtk.ButtonNewWithLabel("Export GPL") | ||
| 357 | exportBtn.Connect("clicked", func() { app.exportFavoritesGPLToDefaultPath() }) | ||
| 358 | favBtns.PackStart(exportBtn, true, true, 0) | ||
| 359 | |||
| 360 | favBox.PackStart(favBtns, false, false, 0) | ||
| 361 | favFrame.Add(favBox) | ||
| 362 | lower.PackStart(favFrame, false, false, 0) | ||
| 191 | 363 | ||
| 192 | mainBox.PackStart(bottomBox, false, false, 0) | 364 | root.PackStart(lower, true, true, 0) |
| 193 | 365 | ||
| 194 | app.window.Add(mainBox) | 366 | status, _ := gtk.LabelNew("Choose a color and a scheme type") |
| 367 | status.SetHAlign(gtk.ALIGN_START) | ||
| 368 | root.PackStart(status, false, false, 0) | ||
| 369 | |||
| 370 | app.window.Add(root) | ||
| 195 | app.window.ShowAll() | 371 | app.window.ShowAll() |
| 196 | } | 372 | } |
| 197 | 373 | ||
| 198 | func (app *App) populateList() { | 374 | func (app *App) newSwatchCard() SwatchCard { |
| 199 | app.listStore.Clear() | 375 | button, _ := gtk.ButtonNew() |
| 376 | button.SetSizeRequest(220, 240) | ||
| 200 | 377 | ||
| 201 | for _, color := range app.savedColors { | 378 | vbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 4) |
| 202 | pixbuf := app.createColorSwatch(color.Hex) | 379 | vbox.SetMarginTop(8) |
| 203 | iter := app.listStore.Append() | 380 | vbox.SetMarginBottom(8) |
| 204 | app.listStore.Set(iter, []int{0, 1, 2}, []interface{}{pixbuf, color.Hex, color.Name}) | 381 | vbox.SetMarginStart(8) |
| 205 | } | 382 | vbox.SetMarginEnd(8) |
| 383 | |||
| 384 | image, _ := gtk.ImageNew() | ||
| 385 | image.SetFromPixbuf(solidPixbuf("#000000", 220, 150)) | ||
| 386 | vbox.PackStart(image, false, false, 0) | ||
| 387 | |||
| 388 | label, _ := gtk.LabelNew("") | ||
| 389 | label.SetJustify(gtk.JUSTIFY_CENTER) | ||
| 390 | label.SetHAlign(gtk.ALIGN_CENTER) | ||
| 391 | vbox.PackStart(label, false, false, 0) | ||
| 392 | |||
| 393 | button.Add(vbox) | ||
| 394 | |||
| 395 | return SwatchCard{button: button, image: image, label: label} | ||
| 206 | } | 396 | } |
| 207 | 397 | ||
| 208 | func (app *App) createColorSwatch(hexColor string) *gdk.Pixbuf { | 398 | func (app *App) setCurrentColor(rgba *gdk.RGBA, pushHistory bool) { |
| 209 | pixbuf, err := gdk.PixbufNew(gdk.COLORSPACE_RGB, false, 8, 16, 14) | 399 | hex := rgbaToHex(rgba) |
| 210 | if err != nil { | 400 | app.currentColor = rgba |
| 211 | return nil | 401 | app.currentHex = hex |
| 402 | app.hexEntry.SetText(hex) | ||
| 403 | |||
| 404 | app.suppressColorSet = true | ||
| 405 | app.colorButton.SetRGBA(rgba) | ||
| 406 | app.suppressColorSet = false | ||
| 407 | |||
| 408 | if pushHistory { | ||
| 409 | app.pushHistory(hex) | ||
| 212 | } | 410 | } |
| 213 | 411 | ||
| 214 | rgba := gdk.NewRGBA() | 412 | app.config.LastColor = hex |
| 215 | rgba.Parse(hexColor) | 413 | app.config.LastScheme = app.activeSchemeName() |
| 216 | 414 | app.config.Palette = app.activePaletteName() | |
| 217 | r := uint32(rgba.GetRed() * 255) | 415 | app.updateSchemePreview() |
| 218 | g := uint32(rgba.GetGreen() * 255) | 416 | app.updateActionStates() |
| 219 | b := uint32(rgba.GetBlue() * 255) | 417 | app.saveConfig() |
| 220 | 418 | } | |
| 221 | pixels := pixbuf.GetPixels() | 419 | |
| 222 | rowstride := pixbuf.GetRowstride() | 420 | func (app *App) updateSchemePreview() { |
| 223 | nChannels := pixbuf.GetNChannels() | 421 | colors := generateScheme(app.currentColor, app.activeSchemeName()) |
| 224 | 422 | for i := 0; i < len(app.swatchCards); i++ { | |
| 225 | for y := 0; y < 14; y++ { | 423 | if i >= len(colors) { |
| 226 | for x := 0; x < 16; x++ { | 424 | app.swatchCards[i].button.Hide() |
| 227 | offset := y*rowstride + x*nChannels | 425 | continue |
| 228 | pixels[offset] = byte(r) | ||
| 229 | pixels[offset+1] = byte(g) | ||
| 230 | pixels[offset+2] = byte(b) | ||
| 231 | } | 426 | } |
| 427 | app.swatchCards[i].button.Show() | ||
| 428 | hex := rgbaToHex(colors[i]) | ||
| 429 | h, s, v := rgbToHSV(colors[i]) | ||
| 430 | r := int(math.Round(colors[i].GetRed() * 255)) | ||
| 431 | g := int(math.Round(colors[i].GetGreen() * 255)) | ||
| 432 | b := int(math.Round(colors[i].GetBlue() * 255)) | ||
| 433 | app.swatchCards[i].hex = hex | ||
| 434 | app.swatchCards[i].image.SetFromPixbuf(solidPixbuf(hex, 220, 150)) | ||
| 435 | app.swatchCards[i].label.SetText(fmt.Sprintf("%s\nrgb(%d, %d, %d)\nhsv(%d, %d, %d)", hex, r, g, b, int(h), int(s), int(v))) | ||
| 232 | } | 436 | } |
| 233 | |||
| 234 | return pixbuf | ||
| 235 | } | 437 | } |
| 236 | 438 | ||
| 237 | func (app *App) onSelectionChanged(selection *gtk.TreeSelection) { | 439 | func (app *App) applyHexEntry() { |
| 238 | model, iter, ok := selection.GetSelected() | 440 | text, _ := app.hexEntry.GetText() |
| 239 | if !ok { | 441 | text = strings.TrimSpace(text) |
| 240 | app.deleteBtn.SetSensitive(false) | 442 | if text == "" { |
| 241 | app.selectedIter = nil | ||
| 242 | return | 443 | return |
| 243 | } | 444 | } |
| 445 | if !strings.HasPrefix(text, "#") { | ||
| 446 | text = "#" + text | ||
| 447 | } | ||
| 448 | rgba := gdk.NewRGBA() | ||
| 449 | if !rgba.Parse(text) { | ||
| 450 | app.hexEntry.SetText(app.currentHex) | ||
| 451 | return | ||
| 452 | } | ||
| 453 | app.setCurrentColor(rgba, true) | ||
| 454 | } | ||
| 244 | 455 | ||
| 245 | app.selectedIter = iter | 456 | func (app *App) randomizeColor() { |
| 246 | app.deleteBtn.SetSensitive(true) | 457 | r := app.rng.Intn(256) |
| 458 | g := app.rng.Intn(256) | ||
| 459 | b := app.rng.Intn(256) | ||
| 460 | rgba := gdk.NewRGBA() | ||
| 461 | rgba.SetRed(float64(r) / 255.0) | ||
| 462 | rgba.SetGreen(float64(g) / 255.0) | ||
| 463 | rgba.SetBlue(float64(b) / 255.0) | ||
| 464 | rgba.SetAlpha(1) | ||
| 465 | app.setCurrentColor(rgba, true) | ||
| 466 | } | ||
| 247 | 467 | ||
| 248 | value, _ := model.ToTreeModel().GetValue(iter, 1) | 468 | func (app *App) adjustSV(ds, dv float64) { |
| 249 | hexColor, _ := value.GetString() | 469 | h, s, v := rgbToHSV(app.currentColor) |
| 470 | s = clamp(s+ds, 0, 100) | ||
| 471 | v = clamp(v+dv, 0, 100) | ||
| 472 | app.setCurrentColor(hsvToRGBA(h, s, v), true) | ||
| 473 | } | ||
| 250 | 474 | ||
| 475 | func (app *App) pasteColorFromClipboard() { | ||
| 476 | clipboard, _ := gtk.ClipboardGet(gdk.SELECTION_CLIPBOARD) | ||
| 477 | text, err := clipboard.WaitForText() | ||
| 478 | if err != nil { | ||
| 479 | return | ||
| 480 | } | ||
| 481 | text = strings.TrimSpace(text) | ||
| 482 | if text == "" { | ||
| 483 | return | ||
| 484 | } | ||
| 485 | if !strings.HasPrefix(text, "#") { | ||
| 486 | text = "#" + text | ||
| 487 | } | ||
| 251 | rgba := gdk.NewRGBA() | 488 | rgba := gdk.NewRGBA() |
| 252 | rgba.Parse(hexColor) | 489 | if rgba.Parse(text) { |
| 253 | app.colorButton.SetRGBA(rgba) | 490 | app.setCurrentColor(rgba, true) |
| 254 | app.currentColor = rgba | 491 | } |
| 255 | } | 492 | } |
| 256 | 493 | ||
| 257 | func (app *App) onSaveClicked() { | 494 | func (app *App) pushHistory(hex string) { |
| 258 | dialog, _ := gtk.DialogNew() | 495 | if app.historyPos >= 0 && app.historyPos < len(app.history) && app.history[app.historyPos] == hex { |
| 259 | dialog.SetTitle("Save Color") | 496 | return |
| 260 | dialog.SetTransientFor(app.window) | 497 | } |
| 261 | dialog.SetModal(true) | 498 | if app.historyPos+1 < len(app.history) { |
| 262 | dialog.SetDefaultSize(300, -1) | 499 | app.history = app.history[:app.historyPos+1] |
| 263 | 500 | } | |
| 264 | box, _ := dialog.GetContentArea() | 501 | app.history = append(app.history, hex) |
| 265 | box.SetSpacing(10) | 502 | if len(app.history) > maxHistoryLen { |
| 266 | box.SetMarginTop(10) | 503 | over := len(app.history) - maxHistoryLen |
| 267 | box.SetMarginBottom(10) | 504 | app.history = app.history[over:] |
| 268 | box.SetMarginStart(10) | 505 | } |
| 269 | box.SetMarginEnd(10) | 506 | app.historyPos = len(app.history) - 1 |
| 507 | } | ||
| 270 | 508 | ||
| 271 | // get current color | 509 | func (app *App) navigateHistory(step int) { |
| 272 | hexColor := rgbaToHex(app.currentColor) | 510 | if len(app.history) == 0 { |
| 511 | return | ||
| 512 | } | ||
| 513 | next := app.historyPos + step | ||
| 514 | if next < 0 || next >= len(app.history) { | ||
| 515 | return | ||
| 516 | } | ||
| 517 | rgba := gdk.NewRGBA() | ||
| 518 | if !rgba.Parse(app.history[next]) { | ||
| 519 | return | ||
| 520 | } | ||
| 521 | app.historyPos = next | ||
| 522 | app.setCurrentColor(rgba, false) | ||
| 523 | } | ||
| 273 | 524 | ||
| 274 | label, _ := gtk.LabelNew(fmt.Sprintf("Color: %s", hexColor)) | 525 | func (app *App) updateActionStates() { |
| 275 | box.PackStart(label, false, false, 0) | 526 | _, s, v := rgbToHSV(app.currentColor) |
| 527 | app.historyBackBtn.SetSensitive(app.historyPos > 0) | ||
| 528 | app.historyFwdBtn.SetSensitive(app.historyPos >= 0 && app.historyPos < len(app.history)-1) | ||
| 529 | app.lightenBtn.SetSensitive(v < 100) | ||
| 530 | app.darkenBtn.SetSensitive(v > 5) | ||
| 531 | app.saturateBtn.SetSensitive(s < 100) | ||
| 532 | app.desaturateBtn.SetSensitive(s > 5) | ||
| 533 | app.removeFavBtn.SetSensitive(app.selectedIter != nil) | ||
| 534 | app.clearFavBtn.SetSensitive(len(app.savedColors) > 0) | ||
| 535 | } | ||
| 276 | 536 | ||
| 277 | entryLabel, _ := gtk.LabelNew("Color Name:") | 537 | func (app *App) activeSchemeName() string { |
| 278 | entryLabel.SetHAlign(gtk.ALIGN_START) | 538 | idx := app.schemeCombo.GetActive() |
| 279 | box.PackStart(entryLabel, false, false, 0) | 539 | if idx < 0 || idx >= len(schemeNames) { |
| 540 | return schemeNames[0] | ||
| 541 | } | ||
| 542 | return schemeNames[idx] | ||
| 543 | } | ||
| 280 | 544 | ||
| 281 | entry, _ := gtk.EntryNew() | 545 | func (app *App) activePaletteName() string { |
| 282 | entry.SetText("Untitled") | 546 | idx := app.paletteCombo.GetActive() |
| 283 | entry.SetActivatesDefault(true) | 547 | if idx < 0 || idx >= len(paletteNames) { |
| 284 | box.PackStart(entry, false, false, 0) | 548 | return paletteNames[0] |
| 549 | } | ||
| 550 | return paletteNames[idx] | ||
| 551 | } | ||
| 285 | 552 | ||
| 286 | dialog.AddButton("Cancel", gtk.RESPONSE_CANCEL) | 553 | func (app *App) onPaletteSelectionChanged(selection *gtk.TreeSelection) { |
| 287 | okBtn, _ := dialog.AddButton("OK", gtk.RESPONSE_OK) | 554 | model, iter, ok := selection.GetSelected() |
| 288 | okBtn.SetCanDefault(true) | 555 | if !ok { |
| 289 | okBtn.GrabDefault() | 556 | return |
| 557 | } | ||
| 558 | value, _ := model.ToTreeModel().GetValue(iter, 1) | ||
| 559 | hex, _ := value.GetString() | ||
| 560 | rgba := gdk.NewRGBA() | ||
| 561 | if rgba.Parse(hex) { | ||
| 562 | app.setCurrentColor(rgba, true) | ||
| 563 | } | ||
| 564 | } | ||
| 290 | 565 | ||
| 291 | dialog.ShowAll() | 566 | func (app *App) onFavoriteSelectionChanged(selection *gtk.TreeSelection) { |
| 567 | model, iter, ok := selection.GetSelected() | ||
| 568 | if !ok { | ||
| 569 | app.selectedIter = nil | ||
| 570 | app.updateActionStates() | ||
| 571 | return | ||
| 572 | } | ||
| 573 | app.selectedIter = iter | ||
| 574 | value, _ := model.ToTreeModel().GetValue(iter, 1) | ||
| 575 | hex, _ := value.GetString() | ||
| 576 | rgba := gdk.NewRGBA() | ||
| 577 | if rgba.Parse(hex) { | ||
| 578 | app.setCurrentColor(rgba, true) | ||
| 579 | } | ||
| 580 | app.updateActionStates() | ||
| 581 | } | ||
| 292 | 582 | ||
| 293 | response := dialog.Run() | 583 | func (app *App) populatePaletteList() { |
| 294 | if response == gtk.RESPONSE_OK { | 584 | app.paletteStore.Clear() |
| 295 | text, _ := entry.GetText() | 585 | for _, hex := range paletteByName(app.activePaletteName()) { |
| 296 | app.savedColors = append([]SavedColor{{Hex: hexColor, Name: text}}, app.savedColors...) | 586 | iter := app.paletteStore.Append() |
| 297 | app.saveColors() | 587 | app.paletteStore.Set(iter, []int{0, 1}, []interface{}{solidPixbuf(hex, 36, 14), hex}) |
| 298 | app.populateList() | ||
| 299 | } | 588 | } |
| 589 | } | ||
| 300 | 590 | ||
| 301 | dialog.Destroy() | 591 | func (app *App) addCurrentToFavorites() { |
| 592 | for _, item := range app.savedColors { | ||
| 593 | if strings.EqualFold(item.Hex, app.currentHex) { | ||
| 594 | return | ||
| 595 | } | ||
| 596 | } | ||
| 597 | app.savedColors = append([]SavedColor{{Hex: app.currentHex, Name: app.currentHex}}, app.savedColors...) | ||
| 598 | app.refreshFavoritesView() | ||
| 599 | app.saveConfig() | ||
| 302 | } | 600 | } |
| 303 | 601 | ||
| 304 | func (app *App) onDeleteClicked() { | 602 | func (app *App) removeSelectedFavorite() { |
| 305 | if app.selectedIter == nil { | 603 | if app.selectedIter == nil { |
| 306 | return | 604 | return |
| 307 | } | 605 | } |
| 308 | 606 | value, _ := app.favoritesStore.GetValue(app.selectedIter, 1) | |
| 309 | model := app.listStore.ToTreeModel() | 607 | hex, _ := value.GetString() |
| 310 | value, _ := model.GetValue(app.selectedIter, 1) | 608 | for i, c := range app.savedColors { |
| 311 | hexColor, _ := value.GetString() | 609 | if strings.EqualFold(c.Hex, hex) { |
| 312 | |||
| 313 | // remove from saved colors | ||
| 314 | for i, color := range app.savedColors { | ||
| 315 | if color.Hex == hexColor { | ||
| 316 | app.savedColors = append(app.savedColors[:i], app.savedColors[i+1:]...) | 610 | app.savedColors = append(app.savedColors[:i], app.savedColors[i+1:]...) |
| 317 | break | 611 | break |
| 318 | } | 612 | } |
| 319 | } | 613 | } |
| 320 | |||
| 321 | app.saveColors() | ||
| 322 | app.populateList() | ||
| 323 | app.deleteBtn.SetSensitive(false) | ||
| 324 | app.selectedIter = nil | 614 | app.selectedIter = nil |
| 615 | app.refreshFavoritesView() | ||
| 616 | app.saveConfig() | ||
| 325 | } | 617 | } |
| 326 | 618 | ||
| 327 | func (app *App) onCopyClicked() { | 619 | func (app *App) refreshFavoritesView() { |
| 328 | hexColor := rgbaToHex(app.currentColor) | 620 | app.favoritesStore.Clear() |
| 329 | 621 | for _, color := range app.savedColors { | |
| 330 | clipboard, _ := gtk.ClipboardGet(gdk.SELECTION_CLIPBOARD) | 622 | iter := app.favoritesStore.Append() |
| 331 | clipboard.SetText(hexColor) | 623 | app.favoritesStore.Set(iter, []int{0, 1, 2}, []interface{}{solidPixbuf(color.Hex, 16, 14), strings.ToUpper(color.Hex), color.Name}) |
| 624 | } | ||
| 625 | app.updateActionStates() | ||
| 626 | } | ||
| 332 | 627 | ||
| 333 | dialog := gtk.MessageDialogNew(app.window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, | 628 | func (app *App) exportFavoritesGPLToDefaultPath() { |
| 334 | gtk.BUTTONS_OK, fmt.Sprintf("Color %s copied to clipboard!", hexColor)) | 629 | if len(app.savedColors) == 0 { |
| 335 | dialog.Run() | 630 | return |
| 336 | dialog.Destroy() | 631 | } |
| 632 | outPath := filepath.Join(filepath.Dir(app.configFile), "kjagave-favorites.gpl") | ||
| 633 | lines := []string{"GIMP Palette", "Name: kjagave Favorites", "#"} | ||
| 634 | for _, item := range app.savedColors { | ||
| 635 | r, g, b := hexToRGB(item.Hex) | ||
| 636 | lines = append(lines, fmt.Sprintf("%3d %3d %3d\t%s", r, g, b, item.Name)) | ||
| 637 | } | ||
| 638 | _ = os.WriteFile(outPath, []byte(strings.Join(lines, "\n")+"\n"), 0644) | ||
| 337 | } | 639 | } |
| 338 | 640 | ||
| 339 | func (app *App) onAboutClicked() { | 641 | func (app *App) onAboutClicked() { |
| @@ -341,8 +643,8 @@ func (app *App) onAboutClicked() { | |||
| 341 | dialog.SetTransientFor(app.window) | 643 | dialog.SetTransientFor(app.window) |
| 342 | dialog.SetProgramName(appTitle) | 644 | dialog.SetProgramName(appTitle) |
| 343 | dialog.SetVersion(appVersion) | 645 | dialog.SetVersion(appVersion) |
| 344 | dialog.SetComments("a color picker inspired by agave, but only with the features kj_sh604 actually used") | 646 | dialog.SetComments("Agave-inspired GTK color scheme tool") |
| 345 | dialog.SetAuthors([]string{"kj_sh604"}) | 647 | dialog.SetAuthors([]string{"kj_sh604", "Agave inspiration: Jonathon Jongsma"}) |
| 346 | dialog.SetLicense("BSD Zero Clause License (0-clause BSD)") | 648 | dialog.SetLicense("BSD Zero Clause License (0-clause BSD)") |
| 347 | dialog.SetLogoIconName("applications-graphics") | 649 | dialog.SetLogoIconName("applications-graphics") |
| 348 | dialog.Run() | 650 | dialog.Run() |
| @@ -350,63 +652,314 @@ func (app *App) onAboutClicked() { | |||
| 350 | } | 652 | } |
| 351 | 653 | ||
| 352 | func (app *App) pickColorFromScreen() (*gdk.RGBA, error) { | 654 | func (app *App) pickColorFromScreen() (*gdk.RGBA, error) { |
| 353 | // use xcolor for x11 color picking | ||
| 354 | cmd := exec.Command("xcolor", "--format", "hex") | 655 | cmd := exec.Command("xcolor", "--format", "hex") |
| 355 | output, err := cmd.Output() | 656 | output, err := cmd.Output() |
| 356 | if err != nil { | 657 | if err != nil { |
| 357 | // fallback to grabc | ||
| 358 | cmd = exec.Command("grabc") | 658 | cmd = exec.Command("grabc") |
| 359 | output, err = cmd.Output() | 659 | output, err = cmd.Output() |
| 360 | if err != nil { | 660 | if err != nil { |
| 361 | dialog := gtk.MessageDialogNew(app.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, | 661 | dialog := gtk.MessageDialogNew(app.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, |
| 362 | gtk.BUTTONS_OK, "Color picker not found. Please install 'xcolor'") | 662 | gtk.BUTTONS_OK, "Color picker not found. Please install xcolor or grabc") |
| 363 | dialog.Run() | 663 | dialog.Run() |
| 364 | dialog.Destroy() | 664 | dialog.Destroy() |
| 365 | return nil, err | 665 | return nil, err |
| 366 | } | 666 | } |
| 367 | } | 667 | } |
| 368 | 668 | ||
| 369 | hexColor := strings.TrimSpace(string(output)) | 669 | hex := strings.TrimSpace(string(output)) |
| 370 | if !strings.HasPrefix(hexColor, "#") { | 670 | if !strings.HasPrefix(hex, "#") { |
| 371 | hexColor = "#" + hexColor | 671 | hex = "#" + hex |
| 372 | } | 672 | } |
| 373 | |||
| 374 | rgba := gdk.NewRGBA() | 673 | rgba := gdk.NewRGBA() |
| 375 | if !rgba.Parse(hexColor) { | 674 | if !rgba.Parse(hex) { |
| 376 | return nil, fmt.Errorf("invalid color format: %s", hexColor) | 675 | return nil, fmt.Errorf("invalid color format: %s", hex) |
| 377 | } | 676 | } |
| 378 | |||
| 379 | return rgba, nil | 677 | return rgba, nil |
| 380 | } | 678 | } |
| 381 | 679 | ||
| 382 | func (app *App) loadColors() { | 680 | func (app *App) loadConfig() { |
| 383 | data, err := os.ReadFile(app.configFile) | 681 | data, err := os.ReadFile(app.configFile) |
| 384 | if err != nil { | 682 | if err != nil { |
| 385 | app.savedColors = []SavedColor{} | 683 | app.savedColors = []SavedColor{} |
| 684 | app.config = AppConfig{Favorites: []SavedColor{}, LastColor: "#0066FF", LastScheme: "Triads", Palette: "Web-safe colors"} | ||
| 386 | return | 685 | return |
| 387 | } | 686 | } |
| 388 | 687 | ||
| 389 | if err := json.Unmarshal(data, &app.savedColors); err != nil { | 688 | var cfg AppConfig |
| 390 | log.Printf("Error loading colors: %v", err) | 689 | if err := json.Unmarshal(data, &cfg); err == nil && (cfg.LastColor != "" || len(cfg.Favorites) > 0) { |
| 391 | app.savedColors = []SavedColor{} | 690 | app.config = cfg |
| 691 | app.savedColors = append([]SavedColor(nil), cfg.Favorites...) | ||
| 692 | if app.savedColors == nil { | ||
| 693 | app.savedColors = []SavedColor{} | ||
| 694 | } | ||
| 695 | if app.config.LastColor == "" { | ||
| 696 | app.config.LastColor = "#0066FF" | ||
| 697 | } | ||
| 698 | if app.config.LastScheme == "" { | ||
| 699 | app.config.LastScheme = "Triads" | ||
| 700 | } | ||
| 701 | if app.config.Palette == "" { | ||
| 702 | app.config.Palette = "Web-safe colors" | ||
| 703 | } | ||
| 704 | return | ||
| 705 | } | ||
| 706 | |||
| 707 | var legacy []SavedColor | ||
| 708 | if err := json.Unmarshal(data, &legacy); err == nil { | ||
| 709 | app.savedColors = legacy | ||
| 710 | app.config = AppConfig{Favorites: legacy, LastColor: "#0066FF", LastScheme: "Triads", Palette: "Web-safe colors"} | ||
| 711 | return | ||
| 392 | } | 712 | } |
| 713 | |||
| 714 | app.savedColors = []SavedColor{} | ||
| 715 | app.config = AppConfig{Favorites: []SavedColor{}, LastColor: "#0066FF", LastScheme: "Triads", Palette: "Web-safe colors"} | ||
| 393 | } | 716 | } |
| 394 | 717 | ||
| 395 | func (app *App) saveColors() { | 718 | func (app *App) saveConfig() { |
| 396 | data, err := json.MarshalIndent(app.savedColors, "", " ") | 719 | app.config.Favorites = app.savedColors |
| 720 | if app.currentHex != "" { | ||
| 721 | app.config.LastColor = app.currentHex | ||
| 722 | } | ||
| 723 | if app.schemeCombo != nil { | ||
| 724 | app.config.LastScheme = app.activeSchemeName() | ||
| 725 | } | ||
| 726 | if app.paletteCombo != nil { | ||
| 727 | app.config.Palette = app.activePaletteName() | ||
| 728 | } | ||
| 729 | |||
| 730 | data, err := json.MarshalIndent(app.config, "", " ") | ||
| 397 | if err != nil { | 731 | if err != nil { |
| 398 | log.Printf("Error marshaling colors: %v", err) | 732 | log.Printf("Error marshaling config: %v", err) |
| 399 | return | 733 | return |
| 400 | } | 734 | } |
| 401 | |||
| 402 | if err := os.WriteFile(app.configFile, data, 0644); err != nil { | 735 | if err := os.WriteFile(app.configFile, data, 0644); err != nil { |
| 403 | log.Printf("Error saving colors: %v", err) | 736 | log.Printf("Error saving config: %v", err) |
| 404 | } | 737 | } |
| 405 | } | 738 | } |
| 406 | 739 | ||
| 740 | func (app *App) restoreStartupState() { | ||
| 741 | rgba := gdk.NewRGBA() | ||
| 742 | if app.config.LastColor != "" && rgba.Parse(app.config.LastColor) { | ||
| 743 | app.currentColor = rgba | ||
| 744 | } | ||
| 745 | |||
| 746 | for i, name := range schemeNames { | ||
| 747 | if name == app.config.LastScheme { | ||
| 748 | app.schemeCombo.SetActive(i) | ||
| 749 | break | ||
| 750 | } | ||
| 751 | } | ||
| 752 | for i, name := range paletteNames { | ||
| 753 | if name == app.config.Palette { | ||
| 754 | app.paletteCombo.SetActive(i) | ||
| 755 | break | ||
| 756 | } | ||
| 757 | } | ||
| 758 | |||
| 759 | app.setCurrentColor(app.currentColor, true) | ||
| 760 | } | ||
| 761 | |||
| 762 | func generateScheme(base *gdk.RGBA, schemeName string) []*gdk.RGBA { | ||
| 763 | h, s, v := rgbToHSV(base) | ||
| 764 | mk := func(hue float64) *gdk.RGBA { | ||
| 765 | return hsvToRGBA(wrapHue(hue), s, v) | ||
| 766 | } | ||
| 767 | |||
| 768 | switch schemeName { | ||
| 769 | case "Complements": | ||
| 770 | return []*gdk.RGBA{hsvToRGBA(h, s, v), mk(h + 180)} | ||
| 771 | case "Split Complements": | ||
| 772 | offset := 360.0 / 15.0 | ||
| 773 | return []*gdk.RGBA{hsvToRGBA(h, s, v), mk(h + 180 - offset), mk(h + 180 + offset)} | ||
| 774 | case "Tetrads": | ||
| 775 | offset := 90.0 | ||
| 776 | return []*gdk.RGBA{hsvToRGBA(h, s, v), mk(h + offset), mk(h + 180), mk(h + 180 + offset)} | ||
| 777 | case "Analogous": | ||
| 778 | offset := 360.0 / 12.0 | ||
| 779 | return []*gdk.RGBA{mk(h - offset), hsvToRGBA(h, s, v), mk(h + offset)} | ||
| 780 | case "Monochromatic": | ||
| 781 | c0 := hsvToRGBA(h, s, v) | ||
| 782 | c1 := hsvToRGBA(h, s, v) | ||
| 783 | c2 := hsvToRGBA(h, s, v) | ||
| 784 | if s < 10 { | ||
| 785 | c1 = hsvToRGBA(h, math.Mod(s+33, 100), v) | ||
| 786 | c2 = hsvToRGBA(h, math.Mod(s+66, 100), v) | ||
| 787 | } else { | ||
| 788 | c1 = hsvToRGBA(h, s, math.Mod(v+33, 100)) | ||
| 789 | c2 = hsvToRGBA(h, s, math.Mod(v+66, 100)) | ||
| 790 | } | ||
| 791 | out := []*gdk.RGBA{c0, c1, c2} | ||
| 792 | sort.Slice(out, func(i, j int) bool { return luminance(out[i]) < luminance(out[j]) }) | ||
| 793 | return out | ||
| 794 | case "Triads": | ||
| 795 | fallthrough | ||
| 796 | default: | ||
| 797 | offset := 120.0 | ||
| 798 | return []*gdk.RGBA{hsvToRGBA(h, s, v), mk(h + offset), mk(h - offset)} | ||
| 799 | } | ||
| 800 | } | ||
| 801 | |||
| 802 | func paletteByName(name string) []string { | ||
| 803 | switch name { | ||
| 804 | case "Tango": | ||
| 805 | return []string{ | ||
| 806 | "#2E3436", "#555753", "#888A85", "#BABDB6", "#D3D7CF", "#EEEEEC", | ||
| 807 | "#FCE94F", "#EDD400", "#C4A000", "#8AE234", "#73D216", "#4E9A06", | ||
| 808 | "#729FCF", "#3465A4", "#204A87", "#AD7FA8", "#75507B", "#5C3566", | ||
| 809 | "#EF2929", "#CC0000", "#A40000", "#FCAF3E", "#F57900", "#CE5C00", | ||
| 810 | } | ||
| 811 | case "Visibone Core": | ||
| 812 | return []string{ | ||
| 813 | "#000000", "#333333", "#666666", "#999999", "#CCCCCC", "#FFFFFF", | ||
| 814 | "#FF0000", "#FF9900", "#FFFF00", "#00FF00", "#00FFFF", "#0000FF", | ||
| 815 | "#9900FF", "#FF00FF", "#FF0066", "#663300", "#CC6633", "#99CC33", | ||
| 816 | "#6699CC", "#CC33CC", "#CC9999", "#33CCCC", "#336699", "#9966CC", | ||
| 817 | } | ||
| 818 | default: | ||
| 819 | vals := []int{0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF} | ||
| 820 | colors := make([]string, 0, 216) | ||
| 821 | for _, r := range vals { | ||
| 822 | for _, g := range vals { | ||
| 823 | for _, b := range vals { | ||
| 824 | colors = append(colors, fmt.Sprintf("#%02X%02X%02X", r, g, b)) | ||
| 825 | } | ||
| 826 | } | ||
| 827 | } | ||
| 828 | return colors | ||
| 829 | } | ||
| 830 | } | ||
| 831 | |||
| 832 | func solidPixbuf(hex string, width, height int) *gdk.Pixbuf { | ||
| 833 | pb, err := gdk.PixbufNew(gdk.COLORSPACE_RGB, false, 8, width, height) | ||
| 834 | if err != nil { | ||
| 835 | return nil | ||
| 836 | } | ||
| 837 | rgba := gdk.NewRGBA() | ||
| 838 | rgba.Parse(hex) | ||
| 839 | r := byte(rgba.GetRed() * 255) | ||
| 840 | g := byte(rgba.GetGreen() * 255) | ||
| 841 | b := byte(rgba.GetBlue() * 255) | ||
| 842 | |||
| 843 | pixels := pb.GetPixels() | ||
| 844 | rowstride := pb.GetRowstride() | ||
| 845 | channels := pb.GetNChannels() | ||
| 846 | for y := 0; y < height; y++ { | ||
| 847 | for x := 0; x < width; x++ { | ||
| 848 | off := y*rowstride + x*channels | ||
| 849 | pixels[off] = r | ||
| 850 | pixels[off+1] = g | ||
| 851 | pixels[off+2] = b | ||
| 852 | } | ||
| 853 | } | ||
| 854 | return pb | ||
| 855 | } | ||
| 856 | |||
| 857 | func rgbToHSV(rgba *gdk.RGBA) (float64, float64, float64) { | ||
| 858 | r := rgba.GetRed() | ||
| 859 | g := rgba.GetGreen() | ||
| 860 | b := rgba.GetBlue() | ||
| 861 | |||
| 862 | mx := math.Max(r, math.Max(g, b)) | ||
| 863 | mn := math.Min(r, math.Min(g, b)) | ||
| 864 | delta := mx - mn | ||
| 865 | |||
| 866 | h := 0.0 | ||
| 867 | if delta > 0 { | ||
| 868 | switch mx { | ||
| 869 | case r: | ||
| 870 | h = 60 * math.Mod((g-b)/delta, 6) | ||
| 871 | case g: | ||
| 872 | h = 60 * ((b-r)/delta + 2) | ||
| 873 | case b: | ||
| 874 | h = 60 * ((r-g)/delta + 4) | ||
| 875 | } | ||
| 876 | } | ||
| 877 | if h < 0 { | ||
| 878 | h += 360 | ||
| 879 | } | ||
| 880 | |||
| 881 | s := 0.0 | ||
| 882 | if mx > 0 { | ||
| 883 | s = (delta / mx) * 100 | ||
| 884 | } | ||
| 885 | v := mx * 100 | ||
| 886 | return wrapHue(h), clamp(s, 0, 100), clamp(v, 0, 100) | ||
| 887 | } | ||
| 888 | |||
| 889 | func hsvToRGBA(h, s, v float64) *gdk.RGBA { | ||
| 890 | h = wrapHue(h) | ||
| 891 | s = clamp(s, 0, 100) / 100 | ||
| 892 | v = clamp(v, 0, 100) / 100 | ||
| 893 | |||
| 894 | c := v * s | ||
| 895 | x := c * (1 - math.Abs(math.Mod(h/60, 2)-1)) | ||
| 896 | m := v - c | ||
| 897 | |||
| 898 | var r1, g1, b1 float64 | ||
| 899 | switch { | ||
| 900 | case h < 60: | ||
| 901 | r1, g1, b1 = c, x, 0 | ||
| 902 | case h < 120: | ||
| 903 | r1, g1, b1 = x, c, 0 | ||
| 904 | case h < 180: | ||
| 905 | r1, g1, b1 = 0, c, x | ||
| 906 | case h < 240: | ||
| 907 | r1, g1, b1 = 0, x, c | ||
| 908 | case h < 300: | ||
| 909 | r1, g1, b1 = x, 0, c | ||
| 910 | default: | ||
| 911 | r1, g1, b1 = c, 0, x | ||
| 912 | } | ||
| 913 | |||
| 914 | rgba := gdk.NewRGBA() | ||
| 915 | rgba.SetRed(r1 + m) | ||
| 916 | rgba.SetGreen(g1 + m) | ||
| 917 | rgba.SetBlue(b1 + m) | ||
| 918 | rgba.SetAlpha(1) | ||
| 919 | return rgba | ||
| 920 | } | ||
| 921 | |||
| 407 | func rgbaToHex(rgba *gdk.RGBA) string { | 922 | func rgbaToHex(rgba *gdk.RGBA) string { |
| 408 | r := uint8(rgba.GetRed() * 255) | 923 | r := int(math.Round(rgba.GetRed() * 255)) |
| 409 | g := uint8(rgba.GetGreen() * 255) | 924 | g := int(math.Round(rgba.GetGreen() * 255)) |
| 410 | b := uint8(rgba.GetBlue() * 255) | 925 | b := int(math.Round(rgba.GetBlue() * 255)) |
| 411 | return fmt.Sprintf("#%02X%02X%02X", r, g, b) | 926 | return fmt.Sprintf("#%02X%02X%02X", r, g, b) |
| 412 | } | 927 | } |
| 928 | |||
| 929 | func hexToRGB(hex string) (int, int, int) { | ||
| 930 | text := strings.TrimPrefix(strings.TrimSpace(hex), "#") | ||
| 931 | if len(text) != 6 { | ||
| 932 | return 0, 0, 0 | ||
| 933 | } | ||
| 934 | var r, g, b int | ||
| 935 | _, err := fmt.Sscanf(text, "%02x%02x%02x", &r, &g, &b) | ||
| 936 | if err != nil { | ||
| 937 | return 0, 0, 0 | ||
| 938 | } | ||
| 939 | return r, g, b | ||
| 940 | } | ||
| 941 | |||
| 942 | func wrapHue(h float64) float64 { | ||
| 943 | v := math.Mod(h, 360) | ||
| 944 | if v < 0 { | ||
| 945 | v += 360 | ||
| 946 | } | ||
| 947 | return v | ||
| 948 | } | ||
| 949 | |||
| 950 | func clamp(v, lo, hi float64) float64 { | ||
| 951 | if v < lo { | ||
| 952 | return lo | ||
| 953 | } | ||
| 954 | if v > hi { | ||
| 955 | return hi | ||
| 956 | } | ||
| 957 | return v | ||
| 958 | } | ||
| 959 | |||
| 960 | func luminance(rgba *gdk.RGBA) float64 { | ||
| 961 | r := rgba.GetRed() | ||
| 962 | g := rgba.GetGreen() | ||
| 963 | b := rgba.GetBlue() | ||
| 964 | return 0.2126*r + 0.7152*g + 0.0722*b | ||
| 965 | } | ||
