# gdlint: ignore=max-public-methods extends Node # Presets for creating a new palette enum NewPalettePresetType { EMPTY = 0, FROM_CURRENT_PALETTE = 1, FROM_CURRENT_SPRITE = 2, FROM_CURRENT_SELECTION = 3 } # Color options when user creates a new palette from current sprite or selection enum GetColorsFrom { CURRENT_FRAME = 0, CURRENT_CEL = 1, ALL_FRAMES = 2 } const DEFAULT_PALETTE_NAME := "Default" var palettes_write_path := Global.home_data_directory.path_join("Palettes") # All available palettes var palettes := {} # Currently displayed palette var current_palette: Palette = null # Indexes of colors that are selected in palette # by left and right mouse button var left_selected_color := -1 var right_selected_color := -1 func _ready() -> void: _load_palettes() func get_palettes() -> Dictionary: return palettes func get_current_palette() -> Palette: return current_palette func does_palette_exist(palette_name: String) -> bool: for palette_path in palettes.keys(): var file_name = palette_path.get_basename().get_file() var stripped_palette_name = Palette.strip_unvalid_characters(palette_name) if file_name == stripped_palette_name: return true return false func select_palette(palette_path: String) -> void: current_palette = palettes.get(palette_path) _clear_selected_colors() Global.config_cache.set_value("data", "last_palette", current_palette.name) func is_any_palette_selected() -> bool: if self.current_palette: return true return false func _current_palette_save() -> String: var save_path := "" if current_palette: save_path = _save_palette(self.current_palette) return save_path func _ensure_palette_directory_exists() -> void: var dir := DirAccess.open(Global.home_data_directory) if is_instance_valid(dir) and not dir.dir_exists(palettes_write_path): dir.make_dir(palettes_write_path) func _save_palette(palette: Palette) -> String: _ensure_palette_directory_exists() # Save old resource name and set new resource name var old_resource_name := palette.resource_name palette.set_resource_name(palette.name) # If resource name changed remove the old palette file if old_resource_name != palette.resource_name: var old_palette := palettes_write_path.path_join(old_resource_name) + ".tres" _delete_palette(old_palette) # Save palette var save_path := palettes_write_path.path_join(palette.resource_name) + ".tres" palette.resource_path = save_path var err := ResourceSaver.save(palette, save_path) if err != OK: Global.notification_label("Failed to save palette") return save_path func create_new_palette( preset: int, palette_name: String, comment: String, width: int, height: int, add_alpha_colors: bool, get_colors_from: int ) -> void: _check_palette_settings_values(palette_name, width, height) match preset: NewPalettePresetType.EMPTY: _create_new_empty_palette(palette_name, comment, width, height) NewPalettePresetType.FROM_CURRENT_PALETTE: _create_new_palette_from_current_palette(palette_name, comment) NewPalettePresetType.FROM_CURRENT_SPRITE: _create_new_palette_from_current_sprite( palette_name, comment, width, height, add_alpha_colors, get_colors_from ) NewPalettePresetType.FROM_CURRENT_SELECTION: _create_new_palette_from_current_selection( palette_name, comment, width, height, add_alpha_colors, get_colors_from ) func _create_new_empty_palette( palette_name: String, comment: String, width: int, height: int ) -> void: var new_palette: Palette = Palette.new(palette_name, width, height, comment) var palette_path := _save_palette(new_palette) palettes[palette_path] = new_palette select_palette(palette_path) func _create_new_palette_from_current_palette(palette_name: String, comment: String) -> void: if !current_palette: return var new_palette := current_palette.duplicate() as Palette new_palette.name = palette_name new_palette.comment = comment new_palette.set_resource_name(palette_name) var palette_path := _save_palette(new_palette) palettes[palette_path] = new_palette select_palette(palette_path) func _create_new_palette_from_current_selection( palette_name: String, comment: String, width: int, height: int, add_alpha_colors: bool, get_colors_from: int ): var new_palette := Palette.new(palette_name, width, height, comment) var current_project := Global.current_project var pixels: Array[Vector2i] = [] for x in current_project.size.x: for y in current_project.size.y: var pos := Vector2i(x, y) if current_project.selection_map.is_pixel_selected(pos): pixels.append(pos) _fill_new_palette_with_colors(pixels, new_palette, add_alpha_colors, get_colors_from) func _create_new_palette_from_current_sprite( palette_name: String, comment: String, width: int, height: int, add_alpha_colors: bool, get_colors_from: int ): var new_palette := Palette.new(palette_name, width, height, comment) var current_project := Global.current_project var pixels: Array[Vector2i] = [] for x in current_project.size.x: for y in current_project.size.y: pixels.append(Vector2i(x, y)) _fill_new_palette_with_colors(pixels, new_palette, add_alpha_colors, get_colors_from) func _fill_new_palette_with_colors( pixels: Array[Vector2i], new_palette: Palette, add_alpha_colors: bool, get_colors_from: int ): var current_project := Global.current_project var cels: Array[BaseCel] = [] match get_colors_from: GetColorsFrom.CURRENT_CEL: for cel_index in current_project.selected_cels: var cel := current_project.frames[cel_index[0]].cels[cel_index[1]] cels.append(cel) GetColorsFrom.CURRENT_FRAME: for cel in current_project.frames[current_project.current_frame].cels: cels.append(cel) GetColorsFrom.ALL_FRAMES: for frame in current_project.frames: for cel in frame.cels: cels.append(cel) for cel in cels: var cel_image := Image.new() cel_image.copy_from(cel.get_image()) if cel_image.is_invisible(): continue for i in pixels: var color := cel_image.get_pixelv(i) if color.a > 0: if not add_alpha_colors: color.a = 1 if not new_palette.has_theme_color(color): new_palette.add_color(color) var palette_path := _save_palette(new_palette) palettes[palette_path] = new_palette select_palette(palette_path) func current_palette_edit(palette_name: String, comment: String, width: int, height: int) -> void: _check_palette_settings_values(palette_name, width, height) current_palette.edit(palette_name, width, height, comment) var palette_path := _current_palette_save() palettes[palette_path] = current_palette func _delete_palette(path: String, permanent := true) -> void: if permanent: DirAccess.remove_absolute(path) else: OS.move_to_trash(path) palettes.erase(path) func current_palete_delete(permanent := true) -> void: _delete_palette(current_palette.resource_path, permanent) if palettes.size() > 0: select_palette(palettes.keys()[0]) else: current_palette = null func current_palette_add_color(mouse_button: int, start_index: int = 0) -> void: if ( not current_palette.is_full() and (mouse_button == MOUSE_BUTTON_LEFT or mouse_button == MOUSE_BUTTON_RIGHT) ): # Get color on left or right tool var color := Tools.get_assigned_color(mouse_button) current_palette.add_color(color, start_index) _current_palette_save() func current_palette_get_color(index: int) -> Color: return current_palette.get_color(index) func current_palette_set_color(index: int, color: Color) -> void: current_palette.set_color(index, color) _current_palette_save() func current_palette_delete_color(index: int) -> void: current_palette.remove_color(index) _current_palette_save() func current_palette_swap_colors(source_index: int, target_index: int) -> void: current_palette.swap_colors(source_index, target_index) _select_color(MOUSE_BUTTON_LEFT, target_index) _current_palette_save() func current_palette_copy_colors(from: int, to: int) -> void: current_palette.copy_colors(from, to) _current_palette_save() func current_palette_insert_color(from: int, to: int) -> void: var from_color = current_palette.colors[from] current_palette.remove_color(from) current_palette.insert_color(to, from_color.color) _current_palette_save() func current_palette_get_selected_color_index(mouse_button: int) -> int: match mouse_button: MOUSE_BUTTON_LEFT: return left_selected_color MOUSE_BUTTON_RIGHT: return right_selected_color _: return -1 func current_palette_select_color(mouse_button: int, index: int) -> void: var color := current_palette_get_color(index) if color == null: return match mouse_button: MOUSE_BUTTON_LEFT: Tools.assign_color(color, mouse_button) MOUSE_BUTTON_RIGHT: Tools.assign_color(color, mouse_button) _select_color(mouse_button, index) func _select_color(mouse_button: int, index: int) -> void: match mouse_button: MOUSE_BUTTON_LEFT: left_selected_color = index MOUSE_BUTTON_RIGHT: right_selected_color = index func _clear_selected_colors() -> void: left_selected_color = -1 right_selected_color = -1 func current_palette_is_empty() -> bool: return current_palette.is_empty() func current_palette_is_full() -> bool: return current_palette.is_full() func _check_palette_settings_values(palette_name: String, width: int, height: int) -> bool: # Just in case. These values should be not allowed in gui. if palette_name.length() <= 0 or width <= 0 or height <= 0: printerr("Palette width, height and name length must be greater than 0!") return false return true func _load_palettes() -> void: _ensure_palette_directory_exists() var search_locations := Global.path_join_array(Global.data_directories, "Palettes") var priority_ordered_files := _get_palette_priority_file_map(search_locations) # Iterate backwards, so any palettes defined in default files # get overwritten by those of the same name in user files search_locations.reverse() priority_ordered_files.reverse() var default_palette_name = Global.config_cache.get_value( "data", "last_palette", DEFAULT_PALETTE_NAME ) for i in range(search_locations.size()): # If palette is not in palettes write path - make its copy in the write path var make_copy := false if search_locations[i] != palettes_write_path: make_copy = true var base_directory := search_locations[i] var palette_files: Array = priority_ordered_files[i] for file_name in palette_files: var palette := load(base_directory.path_join(file_name)) as Palette if palette: if make_copy: _save_palette(palette) # Makes a copy of the palette palette.resource_name = palette.resource_path.get_file().trim_suffix(".tres") # On Windows for some reason paths can contain "res://" in front of them which breaks saving palette.resource_path = palette.resource_path.trim_prefix("res://") palettes[palette.resource_path] = palette # Store index of the default palette if palette.name == default_palette_name: select_palette(palette.resource_path) if not current_palette && palettes.size() > 0: select_palette(palettes.keys()[0]) # This returns an array of arrays, with priorities. # In particular, it takes an array of paths to look for # arrays in, in order of file and palette override priority # such that the files in the first directory override the # second, third, etc. ^.^ # It returns an array of arrays, where each output array # corresponds to the given input array at the same index, and # contains the (relative to the given directory) palette files # to load, excluding all ones already existing in higher-priority # directories. nya # in particular, this also means you can run backwards on the result # so that palettes with the given palette name in the higher priority # directories override those set in lower priority directories :) func _get_palette_priority_file_map(looking_paths: Array) -> Array: var final_list := [] # Holds pattern files already found var working_file_set: Dictionary = {} for search_directory in looking_paths: var to_add_files := [] var files = _get_palette_files(search_directory) # files to check for maybe_to_add in files: if not maybe_to_add in working_file_set: to_add_files.append(maybe_to_add) working_file_set[maybe_to_add] = true final_list.append(to_add_files) return final_list # Get the palette files in a single directory. # if it does not exist, return [] func _get_palette_files(path: String) -> Array: var dir := DirAccess.open(path) var results := [] if not is_instance_valid(dir) or not dir.dir_exists(path): return [] dir.list_dir_begin() while true: var file_name := dir.get_next() if file_name == "": break elif ( (not file_name.begins_with(".")) && file_name.to_lower().ends_with("tres") && not dir.current_is_dir() ): results.append(file_name) dir.list_dir_end() return results # Locate the highest priority palette by the given relative filename # If none is found in the directories, then do nothing and return null func _get_best_palette_file_location(looking_paths: Array, fname: String): # -> String: var priority_fmap: Array = _get_palette_priority_file_map(looking_paths) for i in range(looking_paths.size()): var base_path: String = looking_paths[i] var the_files: Array = priority_fmap[i] if the_files.has(fname): return base_path.path_join(fname) return null func import_palette_from_path(path: String) -> void: if does_palette_exist(path.get_basename().get_file()): # If there is a palette with same name ignore import for now return var palette: Palette = null match path.to_lower().get_extension(): "gpl": if FileAccess.file_exists(path): var text := FileAccess.open(path, FileAccess.READ).get_as_text() palette = _import_gpl(path, text) "pal": if FileAccess.file_exists(path): var text := FileAccess.open(path, FileAccess.READ).get_as_text() palette = _import_pal_palette(path, text) "png", "bmp", "hdr", "jpg", "jpeg", "svg", "tga", "webp": var image := Image.new() var err := image.load(path) if !err: palette = _import_image_palette(path, image) "json": if FileAccess.file_exists(path): var text := FileAccess.open(path, FileAccess.READ).get_as_text() palette = _import_json_palette(text) import_palette(palette, path.get_file()) func import_palette(palette: Palette, file_name: String) -> void: if does_palette_exist(file_name.get_basename()): # If there is a palette with same name ignore import for now return if palette: var palette_path := _save_palette(palette) palettes[palette_path] = palette select_palette(palette_path) Global.palette_panel.setup_palettes_selector() Global.palette_panel.select_palette(palette_path) else: Global.error_dialog.set_text( tr("Can't load file '%s'.\nThis is not a valid palette file.") % [file_name] ) Global.error_dialog.popup_centered() Global.dialog_open(true) func _import_gpl(path: String, text: String) -> Palette: # Refer to app/core/gimppalette-load.c of the GIMP for the "living spec" var result: Palette = null var lines := text.split("\n") var line_number := 0 var palette_name := path.get_basename().get_file() var comments := "" var colors := PackedColorArray() for line in lines: # Check if valid Gimp Palette Library file if line_number == 0: if not "GIMP Palette" in line: return result # Comments if line.begins_with("#"): comments += line.trim_prefix("#") + "\n" # Some programs output palette name in a comment for old format if line.begins_with("#Palette Name: "): palette_name = line.replace("#Palette Name: ", "") elif line.begins_with("Name: "): palette_name = line.replace("Name: ", "") elif line.begins_with("Columns: "): # Number of colors in this palette. Unnecessary and often wrong continue elif line_number > 0 && line.length() >= 9: line = line.replace("\t", " ") var color_data: PackedStringArray = line.split(" ", false, 4) var red: float = color_data[0].to_float() / 255.0 var green: float = color_data[1].to_float() / 255.0 var blue: float = color_data[2].to_float() / 255.0 var color := Color(red, green, blue) if color_data.size() >= 4: # Ignore color name for now - result.add_color(color, color_data[3]) colors.append(color) # else: colors.append(color) line_number += 1 if line_number > 0: var height := ceili(colors.size() / 8.0) result = Palette.new(palette_name, 8, height, comments) for color in colors: result.add_color(color) return result func _import_pal_palette(path: String, text: String) -> Palette: var result: Palette = null var colors := PackedColorArray() var lines := text.split("\n") if not "JASC-PAL" in lines[0] or not "0100" in lines[1]: return result var num_colors := int(lines[2]) for i in range(3, num_colors + 3): var color_data = lines[i].split(" ") var red: float = color_data[0].to_float() / 255.0 var green: float = color_data[1].to_float() / 255.0 var blue: float = color_data[2].to_float() / 255.0 var color := Color(red, green, blue) colors.append(color) var height := ceili(colors.size() / 8.0) result = Palette.new(path.get_basename().get_file(), 8, height) for color in colors: result.add_color(color) return result func _import_image_palette(path: String, image: Image) -> Palette: var colors := [] var height := image.get_height() var width := image.get_width() # Iterate all pixels and store unique colors to palette for y in range(0, height): for x in range(0, width): var color := image.get_pixel(x, y) if !colors.has(color): colors.append(color) var palette_height := ceili(colors.size() / 8.0) var result: Palette = Palette.new(path.get_basename().get_file(), 8, palette_height) for color in colors: result.add_color(color) return result ## Import of deprecated older json palette format func _import_json_palette(text: String) -> Palette: var result := Palette.new() var test_json_conv := JSON.new() test_json_conv.parse(text) var result_json = test_json_conv.get_data() if result_json.error != OK: # If parse has errors printerr("JSON palette import error") printerr("Error: ", result_json.error) printerr("Error Line: ", result_json.error_line) printerr("Error String: ", result_json.error_string) result = null else: # If parse OK var data = result_json.result if data.has("name"): # If data is 'valid' palette file result.name = data.name if data.has("comments"): result.comment = data.comments if data.has("colors"): for color_data in data.colors: var color := Color(color_data.data) result.add_color(color) return result