」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > RGFW 底層:XDrag &#n Drop

RGFW 底層:XDrag &#n Drop

發佈於2024-09-03
瀏覽:867

RGFW Under the Hood: XDrag

Introduction

To handle Drag 'n Drop events with X11, you must use the XDnD protocol. Although the XDnD protocol is significantly more complicated than other Drag 'n Drop APIs, it's still relatively simple in theory. However, implementing it is tedious because it requires properly communicating with the X11 server and the source window.

This tutorial explains how to handle the XDnD protocol and manage X11 Drag 'n Drop events. The code is based on RGFW's source code.

Overview

A detailed overview of the steps required:

First, X11 Atoms will be initialized. X11 Atoms are used to ask for or send specific data or properties through X11.
Then, the window's properties will be changed, allowing it to be aware of XDND (X Drag 'n Drop) events.
When a drag happens, the window will receive a ClientMessage Event which includes an XdndEnter message telling the target window that the drag has started.
While the drag is in progress, the source window sends updates about the drag to the target window via ClientMessage events. Each time the target window gets an update, it must confirm it received the update; otherwise, the interaction will end.
Once the drop happens, the source window will send an XdndDrop message. Then the target window will convert the drop selection via X11 and will receive an SelectionNotify event to get the converted data.
The target window will handle this event, convert the data to a readable string, and finally send a ClientMessage with the XdndFinished atom to tell the source window that the interaction is done.

A quick overview of the steps required:

1) Define X11 Atoms
2) Enable XDnD events for the window
3) Handle XDnD events via ClientMessage
4) Get the XDnD drop data via ClientMessage and end the interaction

Step 1 (Define X11 Atoms)

To handle XDnD events, XDnD atoms must be initialized via XInternAtom. Atoms are used when sending or requesting specific data or actions.

XdndTypeList is used when the target window wants to know the data types the source window supports.\
XdndSelection is used to examine the data selection after a drop and to retrieve the data after it is converted.

const Atom XdndTypeList = XInternAtom(display, "XdndTypeList", False);
const Atom XdndSelection = XInternAtom(display, "XdndSelection", False);

These generic Xdnd atoms are messages sent by the source window except for XdndStatus.

XdndEnter, is used when the drop has entered the target window.\
XdndPosition is used to update the target window on the position of the drop.\
XdndStatus is used to tell the source window that the target has received the message.\
XdndLeave is used when the drop has left the target window.\
XdndDrop is used when the drop has been dropped into the target window.\
XdndFinished is used when the drop has been finished.\

const Atom XdndEnter = XInternAtom(display, "XdndEnter", False);
const Atom XdndPosition = XInternAtom(display, "XdndPosition", False);
const Atom XdndStatus = XInternAtom(display, "XdndStatus", False);
const Atom XdndLeave = XInternAtom(display, "XdndLeave", False);    
const Atom XdndDrop = XInternAtom(display, "XdndDrop", False);  
const Atom XdndFinished = XInternAtom(display, "XdndFinished", False);

Xdnd Actions are actions the target window wants to make with the drag data.

XdndActionCopy is used when the target window wants to copy the drag data.

const Atom XdndActionCopy = XInternAtom(display, "XdndActionCopy", False);

The text/uri-list and text/plain atoms are needed to check the format of the drop data.

const Atom XtextUriList = XInternAtom((Display*) display, "text/uri-list", False); 
const Atom XtextPlain = XInternAtom((Display*) display, "text/plain", False);

Step 2 (Enable XDnD events for the window)

To receive XDnD events, the window must enable the XDndAware atom. This atom tells the window manager and the source window that the window wants to receive XDnD events.

This can be done by creating an XdndAware atom and using XChangeProperty to change the window's XdndAware property.

You also must set the XDnD version using a pointer, version 5 should be used as it is the newest version of the XDnD protocol.

const Atom XdndAware = XInternAtom(display, "XdndAware", False);
const char myversion = 5;

XChangeProperty(display, window, XdndAware, 4, 32, PropModeReplace, &myversion, 1);

Step 3 (Handle XDnD events via ClientMessage)

Before any events are handled, some variables must be defined.
These variables are given to us by the source window and are used across multiple instances.

These variables are the source window, the XDnD Protocall version used, and the format of the drop data.

int64_t source, version;
int32_t format;

Now the ClientMessage event can be handled.

case ClientMessage:

First, I will create a generic XEvent structure to reply to XDnD events. This is optional, but in using it we will have to do less work.

This will send the event to the source window and include our window (the target) in the data.

XEvent reply = { ClientMessage };
reply.xclient.window = source;
reply.xclient.format = 32;
reply.xclient.data.l[0] = (long) window;
reply.xclient.data.l[1] = 0;
reply.xclient.data.l[2] = None;

The ClientMessage event structure can be accessed via XEvent.xclient.

message_type is an attribute in the structure, it holds what the message type is. We will use it to check if the message type is an XDnD message.

There are 3 XDnD events we will handle, XdndEnter, XdndPosition, and XdndDrop.

Step 3.1 (XdndEnter)

XdndEnter is sent when the drop enters the target window.

if (E.xclient.message_type == XdndEnter) {

First, RGFW inits the required variables.

  • count: number of formats in the the format list,
  • formats: the list of supported formats and
  • real_formats: this is used here to avoid running malloc for each drop
    unsigned long count;
    Atom* formats;
    Atom real_formats[6];

We can also create a bool to check if the supported formats are a list or if there is only one format.

This can be done by using the xclient's data attribute. Data is a list of data about the event.

the first item is the source window.

The second item of the data includes two values, if the format is a list or not and the version of XDnD used.

To get the bool value, you can check the first bit, the version is stored 24 bits after (the final 40 bits).

The format should be set to None for now, also make sure the version is less than or equal to 5. Otherwise, there's probably an issue because 5 is the newest version.

    Bool list = E.xclient.data.l[1] & 1;

    source = E.xclient.data.l[0];
    version = E.xclient.data.l[1] >> 24;
    format = None;

    if (version > 5)
        break;

If the format is a list, we'll have to get the format list from the source window's XDndTypeList value using XGetWindowProperty

    if (list) {
        Atom actualType;
        int32_t actualFormat;
        unsigned long bytesAfter;

        XGetWindowProperty((Display*) display,
            source,
            XdndTypeList,
            0,
            LONG_MAX,
            False,
            4,
            &actualType,
            &actualFormat,
            &count,
            &bytesAfter,
            (unsigned char**) &formats);
    } 

Otherwise, the format can be found using the leftover xclient values (2 - 4)

    else {
        count = 0;

        if (E.xclient.data.l[2] != None)
            real_formats[count  ] = E.xclient.data.l[2];
        if (E.xclient.data.l[3] != None)
            real_formats[count  ] = E.xclient.data.l[3];
        if (E.xclient.data.l[4] != None)
            real_formats[count  ] = E.xclient.data.l[4];

        formats = real_formats;
    }

Now that we have the format array, we can check if the format matches any of the formats we're looking for.

The list should also be freed using XFree if it was received using XGetWindowProperty.

    unsigned long i;
    for (i = 0; i 



Step 3.2 (XdndPosition)

XdndPosition is used when the drop position is updated.

Before we handle the event, make sure the version is correct.

if (E.xclient.message_type == XdndPosition && version 



The absolute X and Y can be found using the second item of the data list.

The X = the last 32 bits.
The Y = the first 32 bits.

    const int32_t xabs = (E.xclient.data.l[2] >> 16) & 0xffff;
    const int32_t yabs = (E.xclient.data.l[2]) & 0xffff;

The absolute X and Y can be translated to the actual X and Y coordinates of the drop position using XTranslateCoordinates.

    Window dummy;
    int32_t xpos, ypos;

    XTranslateCoordinates((Display*) display,
        XDefaultRootWindow((Display*) display),
        (Window) window,
        xabs, yabs,
        &xpos, &ypos,
        &dummy);

    printf("File drop starting at %i %i\n", xpos, ypos);

A response must be sent back to the source window. The response uses XdndStatus to tell the window it has received the message.

We should also tell the source the action accepted with the data. (XdndActionCopy)

The message can be sent out via XSendEvent make sure you also send out XFlush to make sure the event is pushed out.

    reply.xclient.message_type = XdndStatus;

    if (format) {
        reply.xclient.data.l[1] = 1;
        if (version >= 2)
            reply.xclient.data.l[4] = XdndActionCopy;
    }

    XSendEvent((Display*) display, source, False, NoEventMask, &reply);
    XFlush((Display*) display);
    break;
}

Step 3.3 (XdndDrop)

Before we handle the event, make sure the version is correct.

XdndDrop occurs when the item has been dropped.

if (E.xclient.message_type = XdndDrop && version 



First, we should make sure we registered a valid format earlier.

    if (format) {

We can use XConvertSection to request that the selection be converted to the format.

We will get the result in an SelectionNotify event.

        // newer versions of xDnD require us to tell the source our time 
        Time time = CurrentTime;
        if (version >= 1)
            time = E.xclient.data.l[2];

        XConvertSelection((Display*) display,
            XdndSelection,
            format,
            XdndSelection,
            (Window) window,
            time);
    } 

Otherwise, there is no drop data and the drop has ended. XDnD versions 2 and older require the target to tell the source when the drop has ended.

This can be done by sending out a ClientMessage event with the XdndFinished message type.

    else if (version >= 2) {
        reply.xclient.message_type = XdndFinished;

        XSendEvent((Display*) display, source,
            False, NoEventMask, &reply);
        XFlush((Display*) display);
    }
}

Step 4 (Get the XDnD drop data via ClientMessage and end the interaction)

Now we can receive the converted selection from the SlectionNotify event

case SelectionNotify: {

To do this, first, ensure the property is the XdndSelection.

/* this is only for checking for drops */

if (E.xselection.property != XdndSelection)
    break;

XGetWindowpropery can be used to get the selection data.

char* data;
unsigned long result;

Atom actualType;
int32_t actualFormat;
unsigned long bytesAfter;

XGetWindowProperty((Display*) display, E.xselection.requestor, E.xselection.property, \
                                    0, LONG_MAX, False, E.xselection.target, &actualType, 
                                    &actualFormat, &result, &bytesAfter, 
                                    (unsigned char**) &data);

if (result == 0)
    break;

printf("File dropped: %s\n", data);

This is the raw string data for the drop. If there are multiple drops, it will include the files separated by a '\n'. If you'd prefer an array of strings, you'd have to parse the data into an array.

The data should also be freed once you're done using it.

If you want to use the data after the event has been processed, you should allocate a separate buffer and copy the data over.

if (data)
    XFree(data);

the drop has ended and XDnD versions 2 and older require the target to tell the source when the drop has ended.
This can be done by sending out a ClientMessage event with the XdndFinished message type.

It will also include the action we did with the data and the result to tell the source wether or not we actually got the data.

if (version >= 2) {
    reply.xclient.message_type = XdndFinished;
    reply.xclient.data.l[1] = result;
    reply.xclient.data.l[2] = XdndActionCopy;

    XSendEvent((Display*) display, source, False, NoEventMask, &reply);
    XFlush((Display*) display);
}

Full code example

// This compiles with
// gcc example.c -lX11

#include 
#include 

#include 
#include 

int main(void) {
    Display* display = XOpenDisplay(NULL);

    Window window = XCreateSimpleWindow(display, 
                                        RootWindow(display, DefaultScreen(display)), 
                                        10, 10, 200, 200, 1,
                                        BlackPixel(display, DefaultScreen(display)), WhitePixel(display, DefaultScreen(display)));

    XSelectInput(display, window, ExposureMask | KeyPressMask);

    const Atom wm_delete_window = XInternAtom((Display*) display, "WM_DELETE_WINDOW", False);

    /* Xdnd code */

    /* fetching data */
    const Atom XdndTypeList = XInternAtom(display, "XdndTypeList", False);
    const Atom XdndSelection = XInternAtom(display, "XdndSelection", False);

    /* client messages */
    const Atom XdndEnter = XInternAtom(display, "XdndEnter", False);
    const Atom XdndPosition = XInternAtom(display, "XdndPosition", False);
    const Atom XdndStatus = XInternAtom(display, "XdndStatus", False);
    const Atom XdndLeave = XInternAtom(display, "XdndLeave", False);    
    const Atom XdndDrop = XInternAtom(display, "XdndDrop", False);  
    const Atom XdndFinished = XInternAtom(display, "XdndFinished", False);

    /* actions */
    const Atom XdndActionCopy = XInternAtom(display, "XdndActionCopy", False);
    const Atom XdndActionMove = XInternAtom(display, "XdndActionMove", False);
    const Atom XdndActionLink = XInternAtom(display, "XdndActionLink", False);
    const Atom XdndActionAsk = XInternAtom(display, "XdndActionAsk", False);
    const Atom XdndActionPrivate = XInternAtom(display, "XdndActionPrivate", False);

    const Atom XtextUriList = XInternAtom((Display*) display, "text/uri-list", False); 
    const Atom XtextPlain = XInternAtom((Display*) display, "text/plain", False);

    const Atom XdndAware = XInternAtom(display, "XdndAware", False);
    const char myVersion = 5;
    XChangeProperty(display, window, XdndAware, 4, 32, PropModeReplace, &myVersion, 1);

    XMapWindow(display, window);

    XEvent E;
    Bool running = True;

    int64_t source, version;
    int32_t format;

    while (running) {
        XNextEvent(display, &E);

        switch (E.type) {
            case KeyPress: running = False; break;
            case ClientMessage:
                if (E.xclient.data.l[0] == (int64_t) wm_delete_window) {
                    running = False;
                    break;
                }

                XEvent reply = { ClientMessage };
                reply.xclient.window = source;
                reply.xclient.format = 32;
                reply.xclient.data.l[0] = (long) window;
                reply.xclient.data.l[2] = 0;
                reply.xclient.data.l[3] = 0;


                if (E.xclient.message_type == XdndEnter) {
                    unsigned long count;
                    Atom* formats;
                    Atom real_formats[6];

                    Bool list = E.xclient.data.l[1] & 1;

                    source = E.xclient.data.l[0];
                    version = E.xclient.data.l[1] >> 24;
                    format = None;

                    if (version > 5)
                        break;

                    if (list) {
                        Atom actualType;
                        int32_t actualFormat;
                        unsigned long bytesAfter;

                        XGetWindowProperty((Display*) display,
                            source,
                            XdndTypeList,
                            0,
                            LONG_MAX,
                            False,
                            4,
                            &actualType,
                            &actualFormat,
                            &count,
                            &bytesAfter,
                            (unsigned char**) &formats);
                    } else {
                        count = 0;

                        if (E.xclient.data.l[2] != None)
                            real_formats[count  ] = E.xclient.data.l[2];
                        if (E.xclient.data.l[3] != None)
                            real_formats[count  ] = E.xclient.data.l[3];
                        if (E.xclient.data.l[4] != None)
                            real_formats[count  ] = E.xclient.data.l[4];

                        formats = real_formats;
                    }

                    unsigned long i;
                    for (i = 0; i > 16) & 0xffff;
                    const int32_t yabs = (E.xclient.data.l[2]) & 0xffff;
                    Window dummy;
                    int32_t xpos, ypos;

                    if (version > 5)
                        break;

                    XTranslateCoordinates((Display*) display,
                        XDefaultRootWindow((Display*) display),
                        (Window) window,
                        xabs, yabs,
                        &xpos, &ypos,
                        &dummy);

                    printf("File drop starting at %i %i\n", xpos, ypos);

                    reply.xclient.message_type = XdndStatus;

                    if (format) {
                        reply.xclient.data.l[1] = 1;
                        if (version >= 2)
                            reply.xclient.data.l[4] = XdndActionCopy;
                    }

                    XSendEvent((Display*) display, source, False, NoEventMask, &reply);
                    XFlush((Display*) display);
                    break;
                }

                if (E.xclient.message_type = XdndDrop && version = 1)
                            time = E.xclient.data.l[2];

                        XConvertSelection((Display*) display,
                            XdndSelection,
                            format,
                            XdndSelection,
                            (Window) window,
                            time);
                    } else if (version >= 2) {
                        reply.xclient.message_type = XdndFinished;

                        XSendEvent((Display*) display, source,
                            False, NoEventMask, &reply);
                        XFlush((Display*) display);
                    }
                }
                break;
        case SelectionNotify: {
            /* this is only for checking for drops */
            if (E.xselection.property != XdndSelection)
                break;

            char* data;
            unsigned long result;

            Atom actualType;
            int32_t actualFormat;
            unsigned long bytesAfter;

            XGetWindowProperty((Display*) display, 
                                            E.xselection.requestor, E.xselection.property, 
                                            0, LONG_MAX, False, E.xselection.target, 
                                            &actualType, &actualFormat, &result, &bytesAfter, 
                                            (unsigned char**) &data);

            if (result == 0)
                break;

            printf("File(s) dropped: %s\n", data);

            if (data)
                XFree(data);

            if (version >= 2) {
                reply.xclient.message_type = XdndFinished;
                reply.xclient.data.l[1] = result;
                reply.xclient.data.l[2] = XdndActionCopy;

                XSendEvent((Display*) display, source, False, NoEventMask, &reply);
                XFlush((Display*) display);
            }

            break;
        }

            default: break;
        }
    }

    XCloseDisplay(display);
}
版本聲明 本文轉載於:https://dev.to/colleagueriley/rgfw-under-the-hood-x11-drag-n-drop-1ldj如有侵犯,請聯絡[email protected]刪除
最新教學 更多>
  • VLONE Clothing:重新定義都市時尚的街頭服飾品牌
    VLONE Clothing:重新定義都市時尚的街頭服飾品牌
    VLONE 是少数几个在快速变化的市场中取得超越街头服饰行业所能想象的成就的品牌之一。 VLONE 由 A$AP Mob 集体的电影制片人之一 A$AP Bari 创立,现已发展成为一个小众项目,有时甚至成为都市时尚界的国际知名品牌。 VLONE 凭借大胆的图案、深厚的文化联系和限量版发售,在时尚界...
    程式設計 發佈於2024-11-07
  • 如何使用PDO查詢單行中的單列?
    如何使用PDO查詢單行中的單列?
    使用 PDO 查詢單行中的單列處理針對單行中特定列的 SQL 查詢時,通常需要檢索直接取值,無需循環。要使用 PDO 完成此操作,fetchColumn() 方法就派上用場了。 fetchColumn() 的語法為:$col_value = $stmt->fetchColumn([column...
    程式設計 發佈於2024-11-07
  • 我如何建立 PeerSplit:一個免費的點對點費用分攤應用程式 — 從構思到發布僅需數週時間
    我如何建立 PeerSplit:一個免費的點對點費用分攤應用程式 — 從構思到發布僅需數週時間
    我构建了 PeerSplit——一个免费的、点对点的 Splitwise 替代品——从想法到发布仅用了两周时间! PeerSplit 是一款本地优先的应用程序,用于分配团体费用。它可以离线工作,100% 免费且私密,不需要注册或任何个人数据。 以下是我如何构建它以及我在此过程中学到的一切。 ...
    程式設計 發佈於2024-11-07
  • 如何在 PHP 中解析子網域的根網域?
    如何在 PHP 中解析子網域的根網域?
    在 PHP 中從子域解析網域名稱在 PHP 中,從子域中提取根網域是一項常見任務。當您需要識別與子網域關聯的主網站時,這非常有用。為了實現這一目標,讓我們探索一個解決方案。 提供的程式碼片段利用 parse_url 函數將 URL 分解為其元件,包括網域名稱。隨後,它使用正規表示式來隔離根域,而忽略...
    程式設計 發佈於2024-11-07
  • 使用 Socket.io 建立即時應用程式
    使用 Socket.io 建立即時應用程式
    介紹 Socket.io 是一個 JavaScript 函式庫,可讓 Web 用戶端和伺服器之間進行即時通訊。它支援創建互動式動態應用程序,例如聊天室、多人遊戲和直播。憑藉其易於使用的 API 和跨平台相容性,Socket.io 已成為建立即時應用程式的熱門選擇。在本文中,我們將探...
    程式設計 發佈於2024-11-07
  • 重寫 `hashCode()` 和 `equals()` 如何影響 HashMap 效能?
    重寫 `hashCode()` 和 `equals()` 如何影響 HashMap 效能?
    了解equals 和hashCode 在HashMap 中的工作原理Java 中的HashMap 使用hashCode() 和equals() 方法的組合來有效地儲存和檢索鍵值對。當新增新的鍵值對時,首先計算鍵的hashCode()方法,以確定該條目將放置在哪個雜湊桶中。然後使用 equals() ...
    程式設計 發佈於2024-11-07
  • 使用 Google Apps 腳本和 Leaflet.js 建立互動式 XY 圖像圖
    使用 Google Apps 腳本和 Leaflet.js 建立互動式 XY 圖像圖
    Google Maps has a ton of features for plotting points on a map, but what if you want to plot points on an image? These XY Image Plot maps are commonly...
    程式設計 發佈於2024-11-07
  • 理解 React 中的狀態變數:原因和方法
    理解 React 中的狀態變數:原因和方法
    在深入研究狀態變數之前,讓我們先來分析一下 React 元件的工作原理吧! 什麼是 React 元件? 在 React 中,元件是一段可重複使用的程式碼,代表使用者介面 (UI) 的一部分。它可以像 HTML 按鈕一樣簡單,也可以像完整的頁面一樣複雜。 React...
    程式設計 發佈於2024-11-07
  • Miva 的日子:第 4 天
    Miva 的日子:第 4 天
    這是 100 天 Miva 編碼挑戰的第四天。我跳過了第三天的報告,因為我被困在我的網頁設計專案中,需要改變節奏。這就是為什麼我今天決定深入研究 JavaScript。 JavaScript JavaScript 就像是系統和網站的行為元件。它為網站增加了互動性和回應能力,使其成為網頁設計和開發...
    程式設計 發佈於2024-11-07
  • TailGrids React:+ Tailwind CSS React UI 元件
    TailGrids React:+ Tailwind CSS React UI 元件
    我們很高興推出 TailGrids React,這是您的新首選工具包,可用於輕鬆建立令人驚嘆的響應式 Web 介面。 TailGrids React 提供了超過 600 免費和高級 React UI 元件、區塊、部分和模板的大量集合 - 所有這些都是用 Tailwind CSS 精心製作的。 無論...
    程式設計 發佈於2024-11-07
  • 如何用列表值反轉字典?
    如何用列表值反轉字典?
    使用列表值反轉字典:解決方案在本文中,我們探討了使用列表值反轉字典的挑戰。給定一個索引字典,其中鍵是檔案名,值是這些檔案中出現的單字列表,我們的目標是建立一個倒排字典,其中單字是鍵,值是檔案名稱列表。 提供的反轉函數 invert_dict,不適用於以列表值作為鍵的字典,因為它會失敗並顯示“Type...
    程式設計 發佈於2024-11-07
  • 現代 Web 開發框架:比較流行的框架及其用例
    現代 Web 開發框架:比較流行的框架及其用例
    在快速發展的 Web 開發領域,選擇正確的框架可以顯著影響專案的成功。本文深入研究了一些最受歡迎的 Web 開發框架,比較了它們的優勢和理想用例,以幫助開發人員做出明智的決策。 反應 概述 React 由 Facebook 開發和維護,是一個用於建立使用者介面的 J...
    程式設計 發佈於2024-11-07
  • 如何在 Go 1.18 中安全地使用泛型類型解組 JSON?
    如何在 Go 1.18 中安全地使用泛型類型解組 JSON?
    Unmarshal 中的泛型使用(Go 1.18)在Go 1.18 中使用泛型時,例如創建一個容器來保存各種報告類型,可能會出現類型限制。考慮以下設定:由結構表示的多種報告類型具有通用類型參數的ReportContainer 包裝器可報告,約束為實作可報告介面鑑別器ReportType 在解組過程中...
    程式設計 發佈於2024-11-07
  • 了解 Effect-TS 中的選項排序
    了解 Effect-TS 中的選項排序
    範例 1:使用 O.andThen 忽略第一個值 概念 O.andThen 函數可讓您執行兩個選項的序列,其中結果僅由第二個選項決定。當第一個選項達到目的,但後續操作中不需要它的值時,這很有用。 程式碼 function sequencing_ex...
    程式設計 發佈於2024-11-07
  • React 初學者指南:基礎知識入門
    React 初學者指南:基礎知識入門
    React 已成为现代 Web 开发的基石,以其高效、灵活性和强大的生态系统而闻名。 React 由 Facebook 开发,允许开发人员创建可重用的 UI 组件,从而简化了构建交互式用户界面的过程。 无论您是想构建复杂的单页应用程序还是只是想提高您的 Web 开发技能,掌握 React 都是一笔...
    程式設計 發佈於2024-11-07

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3