wxPython и Drag’n’Drop файлов в Linux

wxPython и Drag'n'Drop файлов в Linux

В промежутках между работой и сном постепенно доделываю OutWiker, очередную версию которого собираюсь в скором времени выложить. Одним из изменений в этой версии будет то, что теперь можно будет перетаскивать прикрепленные файлы из окна OutWiker’а в другие приложения. Но когда я только начинал добавлять эту возможность, то не думал, что потрачу на нее столько сил и нервов, и все из-за одной мерзопакостной ошибки в wxWidgets.

Началось все хорошо. Сидя под Windows, реализация возможности перетаскивать файлы в другие приложения заняла считанные строки:

def onBeginDrag(self, event):
	"""
	Начало перетаскивания файлов
	"""
	data = wx.FileDataObject()

	for fname in self.getSelectedFiles():
		# Добавляем вложенные файлы в список.
		data.AddFile (fname)

	dragSource = wx.DropSource (self)
	dragSource.SetData(data)

	result = dragSource.DoDragDrop ()

Под Windows все заработало с первого раза, никаких неприятностей от этих нескольких строчек я не ожидал. На следующий день перезагружаюсь под Linux, чтобы убедиться, что перетаскивание работает и там, забираю изменения из репозитория, запускаю. И, как вы уже догадываетесь, перетаскивание не работает. Причем, не работает очень интересным образом: обрезаются пути до перетаскиваемых файлов. Путь до файлов обрезается только если в пути есть русские буквы, то есть налицо какие-то проблемы с кодировками.

Например, если изменить приведенный выше код на синтетический, который передает на перетаскивание всегда один и тот же файл:

def onBeginDrag(self, event):
	data = wx.FileDataObject()
	# Добавим один файл на перетаскивание
	data.AddFile (u"/tmp/тест/Путь с русскими буквами/__attach/readme.txt") 

	# Выведем список перетаскиваемых файлов
	for fname in data.GetFilenames():
		print fname 

	dragSource = wx.DropSource (self)
	dragSource.SetData(data)
	result = dragSource.DoDragDrop (True)

то в результате код выведет на экран правильный путь до файла, а это означает, что по крайней мере добавили мы его правильно. А вот в приемник этого файла дойдет только строка «/tmp/тест/Путь с русскими буквам».

А если мы перетаскиваем несколько файлов, то имя обрезается у последнего. Например, передаем вот такой список файлов:

/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/CurrentPagePanel.py

/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/guicontroller.py

/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/HtmlTextEditor.py

/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/htmlview.py

А приемник получает вот такой:

/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/CurrentPagePanel.py

/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/guicontroller.py

/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/HtmlTextEditor.py

/media/Storage/Workdir/outwiker/Прим

Долго я промучался с тем, чтобы понять кто виноват, и что делать. Уже готов был пойти на то, чтобы просто отключить эту возможность в версии под Linux, но в результате этот глюк все-таки удалось побороть.

Я не буду рассказывать все шаманства, которые я делал, чтобы побороть проблему, расскажу только суть. А суть оказалась в том, что в версии под Linux для обеспечения работы перетаскивания файлов из этого списка нужно сделать строку следующего вида:

«file:/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/CurrentPagePanel.py\r\n

file:/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/guicontroller.py\r\n

file:/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/HtmlTextEditor.py\r\n

file:/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/htmlview.py»

По крайней мере так поступает класс wx.FileDataObject. Так как wx.FileDataObject — это всего лишь обертка над wxFileDataObject из wxWidgets, то пришлось лезть в изначальные исходники, а там есть такие строки:

bool wxFileDataObject::GetDataHere(void *buf) const
{
    wxString filenames;

    for (size_t i = 0; i < m_filenames.GetCount(); i++)
    {
        filenames += wxT("file:");
        filenames += m_filenames[i];
        filenames += wxT("\r\n");
    }

    memcpy( buf, filenames.mbc_str(), filenames.length() + 1 );

    return true;
}

size_t wxFileDataObject::GetDataSize() const
{
    size_t res = 0;

    for (size_t i = 0; i < m_filenames.GetCount(); i++)
    {
        // This is junk in UTF-8
        res += m_filenames[i].length();
        res += 5 + 2; // "file:" (5) + "\r\n" (2)
    }

    return res + 1;
}

Первая функция копирует подготовленыне данные в буфер, вторая вычисляет размер данных. Я не могу сказать на 100%, что ошибка именно здесь, но в методе wxFileDataObject::GetDataSize() есть строка

res += m_filenames[i].length();

которая помечена подозрительным комментарием. И мне кажется, что в данном случае нужно все-таки считать не размер строки в символах, а размер строки в байтах. Смущает правда, то, что GetDataSize() не используется в GetDataHere(), которая по идее должна возвращать результирующую строку.

В классе wxPython wx.FileDataObject есть свой аналог GetDataHere(), который тоже возвращает результирующую строку. Как оказалось, этот метод как раз и возвращает обрезанную строку.

К счастью, в wxWidgets/wxPython можно создавать свои классы для перетаскиваемых объектов. В данном случае, чтобы обойти проблему, нужно просто написать свой аналог wx.FileDataObject, который делает то же самое, но правильно. Для этого надо сделать класс, производный от wx.PyDataObjectSimple и переопределить в нем методы GetDataSize(), GetDataHere() и SetData(). Честно говоря, я пока немножко схалявил и не стал переопределять метод SetData(), т.к. сам я его вызывать не буду (мне удобнее добавлять по одному файлу), а снаружи, как я понял, этот метод не вызывается.

В результате в меня получился вот такой класс:

class GtkFileDataObject (wx.PyDataObjectSimple):
	"""
	Класс данных для перетаскивания файлов. Использовать вместо wx.FileDataObject, который по сути не работает с Unicode
	"""
	def __init__ (self):
		wx.PyDataObjectSimple.__init__ (self, wx.DataFormat (wx.DF_FILENAME))
		self._fnames = []

	def AddFile (self, fname):
		self._fnames.append (fname)

	def GetDataHere (self):
		result = ""
		for fname in self._fnames:
			result += u"file:%s\r\n" % (fname)

		# Преобразуем unicode в строку
		return result.strip().encode("utf8")

	def GetDataSize (self):
		return len (self.GetDataHere())

Главное здесь было не забыть преобразовать объект unicode в string с помощью метода result.strip().encode(«utf8»).

В итоге в коде добавилась еще одна зависимость кода от ОС: в версии под Windows используется исходный класс wx.FileDataObject (там Drag & Drop реализуется намного сложнее), а под Linux используется класс GtkFileDataObject.

Когда я написал на форуме по wxWidgets про эту проблему, то мне кинули ссылку на багтрекер, где уже зарегистрировали такую проблему год назад, но до сих пор ее еще не решили.

И с заключении отдельно хотелось бы сказать про справку по WxPython/wxWidgets. В справке до сих пор встречаются противоречащие друг другу фразы о том, работает ли класс wx.FileDataObject где-то кроме Windows. Вот, например, одна из цитат:

Warning: Under all non-Windows platforms this class is currently «input-only», i.e. you can receive the files from another application, but copying (or dragging) file(s) from a wxWidgets application is not currently supported. PS: GTK2 should work as well.

Где-то в другом месте писали, что этот класс работает только под Windows, хотя уже прошло несколько лет с того момента, как появилась реализация и под wxGTK.

В общем, хочется верить, что когда-нибудь этот баг когда-нибудь закроют, а еще хочется верить, что когда-нибудь в wxPython появится официальный компонент для работы с WebKit (сейчас он есть, но только под Mac).

PS. Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.

Пожалуйста, оцените запись

УжасноПлохоТак себеХорошоОтлично (Нет оценок)
Загрузка...

Leave a comment

Subscribe without commenting