diff --git a/boot.py b/boot.py index 4cfb2c029..09691f6af 100644 --- a/boot.py +++ b/boot.py @@ -24,6 +24,7 @@ from .plugin.core.panels import LspUpdateLogPanelCommand from .plugin.core.panels import WindowPanelListener from .plugin.core.protocol import Error +from .plugin.color_picker import CloseColorPickerOnBlur from .plugin.core.protocol import Location from .plugin.core.protocol import LocationLink from .plugin.core.registry import LspRecheckSessionsCommand @@ -39,6 +40,7 @@ from .plugin.core.transports import kill_all_subprocesses from .plugin.core.typing import Any, Optional, List, Type, Dict, Union from .plugin.core.views import get_uri_and_position_from_location +from .plugin.color_picker import LspChooseColorPicker from .plugin.core.views import LspRunTextCommandHelperCommand from .plugin.document_link import LspOpenLinkCommand from .plugin.documents import DocumentSyncListener diff --git a/plugin/color_picker/__init__.py b/plugin/color_picker/__init__.py new file mode 100644 index 000000000..a5028909c --- /dev/null +++ b/plugin/color_picker/__init__.py @@ -0,0 +1,242 @@ +from ..core.protocol import Color, TextEdit, ColorInformation +from ..core.typing import Callable, Optional, List, Any +from ..core.views import lsp_color_to_hex +from ..formatting import apply_text_edits_to_view +from abc import ABCMeta +import os +import sublime +import sublime_plugin +import subprocess +import sys +import threading + +OnPickCallback = Callable[[Optional[Color]], None] + + +class ColorPickerPlugin(metaclass=ABCMeta): + def pick(self, on_pick: OnPickCallback, preselect_color: Optional[Color] = None) -> None: + ... + + def normalize_color(self, color: Any) -> Optional[Color]: + ... + + def close(self) -> None: + ... + + +if sublime.platform() == 'linux': + class LinuxColorPicker(ColorPickerPlugin): + process = None # type: Optional[subprocess.Popen] + + def pick(self, on_pick: OnPickCallback, preselect_color: Optional[Color] = None) -> None: + self.close() + t = threading.Thread(target=self._open_picker, args=(on_pick, preselect_color)) + t.start() + + def _open_picker(self, on_pick: OnPickCallback, color: Optional[Color] = None) -> None: + preselect_color_arg = "" + if color: + preselect_color_arg = "{},{},{},{}".format( + color['red'], color['green'], color['blue'], color['alpha'] + ) + picker_cmd = [ + os.path.join(sublime.packages_path(), "LSP", "plugin", "color_picker", "linux_executable.py"), + preselect_color_arg + ] + self.process = subprocess.Popen(picker_cmd, stdout=subprocess.PIPE) + output = self.process.communicate()[0].strip().decode('utf-8') + on_pick(self.normalize_color(output)) + + def normalize_color(self, color: Any) -> Optional[Color]: + if color and isinstance(color, str): + r, g, b, a = map(float, color.split(',')) + return { + "red": r, + "green": g, + "blue": b, + "alpha": a, + } + return None + + def close(self) -> None: + if self.process: + try: + self.process.kill() + except: + pass + self.process = None + + +if sys.platform == "win32": + import ctypes + + class CHOOSECOLOR(ctypes.Structure): + _fields_ = [ + ("lStructSize", ctypes.c_uint32), + ("hwndOwner", ctypes.c_void_p), + ("hInstance", ctypes.c_void_p), + ("rgbResult", ctypes.c_uint32), + ("lpCustColors", ctypes.POINTER(ctypes.c_uint32)), + ("Flags", ctypes.c_uint32), + ("lCustData", ctypes.c_void_p), + ("lpfnHook", ctypes.c_void_p), + ("lpTemplateName", ctypes.c_wchar_p)] + + CC_SOLIDCOLOR = 0x80 + CC_RGBINIT = 0x01 + CC_FULLOPEN = 0x02 + ChooseColorW = ctypes.windll.Comdlg32.ChooseColorW + ChooseColorW.argtypes = [ctypes.POINTER(CHOOSECOLOR)] + ChooseColorW.restype = ctypes.c_int32 + + + class WindowsColorPicker(ColorPickerPlugin): + def pick(self, on_pick: OnPickCallback, preselect_color: Optional[Color] = None) -> None: + self.close() + default_color = (255 << 16) | (255 << 8) | (255) + if preselect_color: + default_color = (round(255*preselect_color['blue']) << 16) | (round(255*preselect_color['green']) << 8) | round(255*preselect_color['red']) + cc = CHOOSECOLOR() + ctypes.memset(ctypes.byref(cc), 0, ctypes.sizeof(cc)) + cc.lStructSize = ctypes.sizeof(cc) + cc.hwndOwner = sublime.active_window().hwnd() + CustomColors = ctypes.c_uint32 * 16 + cc.lpCustColors = CustomColors() # uses 0 (black) for all 16 predefined custom colors + cc.rgbResult = ctypes.c_uint32(default_color) + cc.Flags = CC_SOLIDCOLOR | CC_FULLOPEN | CC_RGBINIT + + # ST window will become unresponsive until color picker dialog is closed + output = ChooseColorW(ctypes.byref(cc)) + + if output == 1: # user clicked OK + on_pick(self.normalize_color(cc.rgbResult)) + else: + on_pick(None) + + def normalize_color(self, bgr_color: Any) -> Optional[Color]: + + def bgr2color(bgr) -> Color: + # 0x00BBGGRR + byte_table = list(["{0:02X}".format(b) for b in range(256)]) + b_hex = byte_table[(bgr >> 16) & 0xff] + g_hex = byte_table[(bgr >> 8) & 0xff] + r_hex = byte_table[(bgr) & 0xff] + + r = int(r_hex, 16) / 255 + g = int(g_hex, 16) / 255 + b = int(b_hex, 16) / 255 + return { + "red": r, + "green": g, + "blue": b, + "alpha": 1 # Windows color picker doesn't support alpha, so fallback to 1 + } + + if bgr_color: + return bgr2color(bgr_color) + return None + + def close(self) -> None: + pass # on windows, the color picker will block until a color is choosen + + +if sublime.platform() == 'osx': + class OsxColorPicker(ColorPickerPlugin): + process = None # type: Optional[subprocess.Popen] + script = ''' + function run(argv) {{ + var app = Application.currentApplication(); + app.includeStandardAdditions = true; + var color = app.chooseColor({{defaultColor: [{red}, {green}, {blue}]}}); + console.log(JSON.stringify(color)); + }} + ''' + + def pick(self, on_pick: OnPickCallback, preselect_color: Optional[Color] = None) -> None: + self.close() + t = threading.Thread(target=self._open_picker, args=(on_pick, preselect_color)) + t.start() + + def _open_picker(self, on_pick: OnPickCallback, color: Optional[Color] = None) -> None: + script = self.script.format(red=0, green=0, blue=0) + if color: + script = self.script.format(red=color['red'], green=color['green'], blue=color['blue']) + picker_cmd = ["/usr/bin/osascript", "-l", "JavaScript", "-e", script] + self.process = subprocess.Popen(picker_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + output = self.process.communicate()[0].strip().decode('ascii') + on_pick(self.normalize_color(output)) + + def normalize_color(self, color: Any) -> Optional[Color]: + print('OSX output contains a warning:', color) + if color and isinstance(color, list): + r, g, b = 1, 1, 1 # hardcoded + return { + "red": r, + "green": g, + "blue": b, + "alpha": 1, # OSX color picker doesn't support alpha, so fallback to 1 + } + return None + + def close(self) -> None: + if self.process: + try: + self.process.kill() + except: + pass + self.process = None + + +def get_color_picker() -> Optional[ColorPickerPlugin]: + if sublime.platform() == "linux": + return LinuxColorPicker() + if sublime.platform() == "windows": + return WindowsColorPicker() + if sublime.platform() == "osx": + return OsxColorPicker() + return None + + +color_picker = get_color_picker() + + +class CloseColorPickerOnBlur(sublime_plugin.EventListener): + def on_activated(self, view: sublime.View) -> None: + if color_picker: + color_picker.close() + + def on_exit(self) -> None: + if color_picker: + color_picker.close() + + +class LspChooseColorPicker(sublime_plugin.TextCommand): + def run(self, edit: sublime.Edit, color_information: ColorInformation, file_name: str) -> None: + if not color_picker: + sublime.status_message('Your platform does not support a ColorPicker yet.') + return + window = self.view.window() + if not window: + return + + def on_select(color: Optional[Color]) -> None: + self.on_pick_color(color, color_information, file_name) + + color_picker.pick(on_select, color_information['color']) + + def on_pick_color( + self, selected_color: Optional[Color], color_information: ColorInformation, file_name: str + ) -> None: + if not selected_color: + return + window = self.view.window() + if not window: + return + view = window.find_open_file(file_name) + new_text = lsp_color_to_hex(selected_color) + text_edits = [{ + "newText": new_text, + "range": color_information['range'] + }] # type: List[TextEdit] + if view: + apply_text_edits_to_view(text_edits, view) diff --git a/plugin/color_picker/linux_executable.py b/plugin/color_picker/linux_executable.py new file mode 100755 index 000000000..4d863b3e8 --- /dev/null +++ b/plugin/color_picker/linux_executable.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +import gi +import sys +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +from gi.repository import Gdk + +color_chooser_dialog = Gtk.ColorChooserDialog(show_editor=True) +color_chooser_dialog.set_title('LSP Color Picker') + +if len(sys.argv) > 1: # sys.argv[1] looks like '1,0.2,1,0.5' + r, g, b, a = map(float, sys.argv[1].split(',')) + preselect_color = Gdk.RGBA(r, g, b, a) + color_chooser_dialog.set_rgba(preselect_color) + +def on_select_color(): + color = color_chooser_dialog.get_rgba() + print('{},{},{},{}'.format(color.red, color.green, color.blue, color.alpha)) + +if color_chooser_dialog.run() == Gtk.ResponseType.OK: + on_select_color() + +color_chooser_dialog.destroy() diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index a21880987..a8e7a0100 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -166,6 +166,18 @@ class SemanticTokenModifiers: 'end': Position }) +Color = TypedDict('Color', { + 'red': float, + 'green': float, + 'blue': float, + 'alpha': float +}, total=True) + +ColorInformation = TypedDict('ColorInformation', { + 'range': RangeLsp, + 'color': Color +}, total=True) + TextDocumentIdentifier = TypedDict('TextDocumentIdentifier', { 'uri': DocumentUri, }, total=True) diff --git a/plugin/core/views.py b/plugin/core/views.py index 79d83853b..999e950e5 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -1,8 +1,8 @@ from .css import css as lsp_css -from .protocol import CodeAction from .protocol import CodeActionContext from .protocol import CodeActionParams from .protocol import CodeActionTriggerKind +from .protocol import CodeAction, Color, ColorInformation from .protocol import Command from .protocol import CompletionItem from .protocol import CompletionItemKind @@ -763,28 +763,46 @@ def run(self, view_id: int, command: str, args: Optional[Dict[str, Any]] = None) COLOR_BOX_HTML = """ - + -
-
+   """ -def lsp_color_to_html(color_info: Dict[str, Any]) -> str: - color = color_info['color'] - red = color['red'] * 255 - green = color['green'] * 255 - blue = color['blue'] * 255 - alpha = color['alpha'] - return COLOR_BOX_HTML.format(red, green, blue, alpha) +def lsp_color_to_hex(color: Color) -> str: + red = int(color['red'] * 255) + green = int(color['green'] * 255) + blue = int(color['blue'] * 255) + alpha = int(color['alpha'] * 255) + if color['alpha'] < 1: + return "#{:02x}{:02x}{:02x}{:02x}".format(red, green, blue, alpha) + return "#{:02x}{:02x}{:02x}".format(red, green, blue) -def lsp_color_to_phantom(view: sublime.View, color_info: Dict[str, Any]) -> sublime.Phantom: - region = range_to_region(Range.from_lsp(color_info['range']), view) - return sublime.Phantom(region, lsp_color_to_html(color_info), sublime.LAYOUT_INLINE) + +def lsp_color_to_html(view: sublime.View, color_information: ColorInformation) -> str: + color = lsp_color_to_hex(color_information['color']) + command = sublime.command_url('lsp_choose_color_picker', { + 'color_information': color_information, + 'file_name': view.file_name() + }) + return COLOR_BOX_HTML.format(command=command, backgroundColor=color) + + +def lsp_color_to_phantom(view: sublime.View, color_information: ColorInformation) -> sublime.Phantom: + region = range_to_region(Range.from_lsp(color_information['range']), view) + return sublime.Phantom(region, lsp_color_to_html(view, color_information), sublime.LAYOUT_INLINE) def document_color_params(view: sublime.View) -> Dict[str, Any]: diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index faad70479..c9a329f00 100644 --- a/plugin/session_buffer.py +++ b/plugin/session_buffer.py @@ -1,3 +1,4 @@ +from .core.protocol import ColorInformation from .core.protocol import Diagnostic from .core.protocol import DiagnosticSeverity from .core.protocol import DocumentLink @@ -351,10 +352,9 @@ def _do_color_boxes_async(self, view: sublime.View, version: int) -> None: Request.documentColor(document_color_params(view), view), self._if_view_unchanged(self._on_color_boxes_async, version) ) - - def _on_color_boxes_async(self, view: sublime.View, response: Any) -> None: + def _on_color_boxes_async(self, view: sublime.View, response: List[ColorInformation]) -> None: color_infos = response if response else [] - self.color_phantoms.update([lsp_color_to_phantom(view, color_info) for color_info in color_infos]) + self.color_phantoms.update([lsp_color_to_phantom(view, color_information) for color_information in color_infos]) # --- textDocument/documentLink ------------------------------------------------------------------------------------