[preview] Port to Gtk3
This commit is contained in:
@@ -1 +1,2 @@
|
||||
# simple redirect
|
||||
from .url_image_preview import UrlImagePreviewPlugin
|
||||
|
||||
@@ -1,55 +1,187 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk+" version="2.16"/>
|
||||
<!-- interface-naming-policy toplevel-contextual -->
|
||||
<requires lib="gtk+" version="3.0"/>
|
||||
<object class="GtkListStore" id="liststore1">
|
||||
<columns>
|
||||
<!-- column-name Text -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">256 KiB</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">512 KiB</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">1 MiB</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">5 MiB</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">10 MiB</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkListStore" id="liststore2">
|
||||
<columns>
|
||||
<!-- column-name Text -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Open</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Save as</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Copy Link Location</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Open Link in Browser</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Open Downloaded File in Browser</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkWindow" id="window1">
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vbox1">
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="border_width">9</property>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hbox2">
|
||||
<object class="GtkFrame" id="frame1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">none</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="preview_size_lebel">
|
||||
<property name="width_request">133</property>
|
||||
<object class="GtkTable" id="table1">
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">0.029999999329447746</property>
|
||||
<property name="label" translatable="yes">Preview size</property>
|
||||
<property name="ellipsize">start</property>
|
||||
<property name="single_line_mode">True</property>
|
||||
<property name="track_visited_links">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="n_rows">3</property>
|
||||
<property name="n_columns">2</property>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="preview_size">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="invisible_char">●</property>
|
||||
<property name="width_chars">6</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="primary_icon_sensitive">True</property>
|
||||
<property name="secondary_icon_sensitive">True</property>
|
||||
<property name="snap_to_ticks">True</property>
|
||||
<property name="numeric">True</property>
|
||||
<signal name="value-changed" handler="preview_size_value_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">2</property>
|
||||
<property name="y_options"/>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="max_size_combobox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">liststore1</property>
|
||||
<signal name="changed" handler="max_size_value_changed" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="cellrenderertext1"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="y_options">GTK_EXPAND</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="leftclick_action_combobox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">liststore2</property>
|
||||
<signal name="changed" handler="leftclick_action_changed" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="cellrenderertext2"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">2</property>
|
||||
<property name="top_attach">2</property>
|
||||
<property name="bottom_attach">3</property>
|
||||
<property name="y_options">GTK_EXPAND</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="max_size_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="xpad">13</property>
|
||||
<property name="label" translatable="yes">Accept files smaller then</property>
|
||||
<property name="track_visited_links">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="y_options">GTK_EXPAND</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="preview_size_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="xpad">12</property>
|
||||
<property name="label" translatable="yes">Preview size</property>
|
||||
<property name="track_visited_links">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="y_options">GTK_EXPAND</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="leftclick_action_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="xpad">12</property>
|
||||
<property name="label" translatable="yes">Left click action</property>
|
||||
<property name="track_visited_links">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="y_options">GTK_EXPAND</property>
|
||||
<property name="top_attach">2</property>
|
||||
<property name="bottom_attach">3</property>
|
||||
<property name="y_options">GTK_EXPAND</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="preview_size">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="has_tooltip">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Preview size(10-512)</property>
|
||||
<property name="invisible_char">●</property>
|
||||
<property name="width_chars">6</property>
|
||||
<property name="snap_to_ticks">True</property>
|
||||
<property name="numeric">True</property>
|
||||
<signal name="value_changed" handler="preview_size_value_changed"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="padding">6</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
99
url_image_preview/context_menu.ui
Normal file
99
url_image_preview/context_menu.ui
Normal file
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.0"/>
|
||||
<object class="GtkImage" id="image1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-open</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="image2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-save-as</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="image3">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-copy</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="image4">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-jump-to</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="image5">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-jump-to</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="context_menu">
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="open_menuitem">
|
||||
<property name="label" translatable="yes">_Open</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="image">image1</property>
|
||||
<property name="use_stock">False</property>
|
||||
<property name="always_show_image">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="save_as_menuitem">
|
||||
<property name="label" translatable="yes">_Save as</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="image">image2</property>
|
||||
<property name="use_stock">False</property>
|
||||
<property name="always_show_image">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="encryption_separator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="copy_link_location_menuitem">
|
||||
<property name="label" translatable="yes">_Copy Link Location</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="image">image3</property>
|
||||
<property name="use_stock">False</property>
|
||||
<property name="always_show_image">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="open_link_in_browser_menuitem">
|
||||
<property name="label" translatable="yes">Open Link in _Browser</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="image">image4</property>
|
||||
<property name="use_stock">False</property>
|
||||
<property name="always_show_image">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="extras_separator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="open_file_in_browser_menuitem">
|
||||
<property name="label" translatable="yes">Open _Downloaded File in Browser</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="image">image5</property>
|
||||
<property name="use_stock">False</property>
|
||||
<property name="always_show_image">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
222
url_image_preview/http_functions.py
Normal file
222
url_image_preview/http_functions.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##
|
||||
## This file is part of Gajim.
|
||||
##
|
||||
## Gajim is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published
|
||||
## by the Free Software Foundation; version 3 only.
|
||||
##
|
||||
## Gajim is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License
|
||||
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
||||
##
|
||||
|
||||
import urllib.request as urllib2
|
||||
import socket
|
||||
import re
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import helpers
|
||||
import logging
|
||||
|
||||
import os
|
||||
if os.name == 'nt':
|
||||
import certifi
|
||||
|
||||
if app.HAVE_PYCURL:
|
||||
import pycurl
|
||||
from io import StringIO
|
||||
|
||||
|
||||
log = logging.getLogger('gajim.plugin_system.url_image_preview.http_functions')
|
||||
|
||||
def get_http_head(account, url):
|
||||
# Check if proxy is used
|
||||
proxy = helpers.get_proxy_info(account)
|
||||
if proxy and proxy['type'] in ('http', 'socks5'):
|
||||
return _get_http_head_proxy(url, proxy)
|
||||
return _get_http_head_direct(url)
|
||||
|
||||
def get_http_file(account, attrs):
|
||||
# Check if proxy is used
|
||||
proxy = helpers.get_proxy_info(account)
|
||||
if proxy and proxy['type'] in ('http', 'socks5'):
|
||||
return _get_http_proxy(attrs, proxy)
|
||||
else:
|
||||
return _get_http_direct(attrs)
|
||||
|
||||
def _get_http_head_direct(url):
|
||||
log.debug('Head request direct for URL: %s' % url)
|
||||
try:
|
||||
req = urllib2.Request(url)
|
||||
req.get_method = lambda: 'HEAD'
|
||||
req.add_header('User-Agent', 'Gajim %s' % app.version)
|
||||
if os.name == 'nt':
|
||||
f = urllib2.urlopen(req, cafile=certifi.where())
|
||||
else:
|
||||
f = urllib2.urlopen(req)
|
||||
except Exception as ex:
|
||||
log.debug('Could not get head response for URL: %s' % url)
|
||||
log.debug("%s" % str(ex))
|
||||
return ('', 0)
|
||||
ctype = f.headers['Content-Type']
|
||||
clen = f.headers['Content-Length']
|
||||
try:
|
||||
clen = int(clen)
|
||||
except ValueError:
|
||||
pass
|
||||
return (ctype, clen)
|
||||
|
||||
def _get_http_head_proxy(url, proxy):
|
||||
log.debug('Head request with proxy for URL: %s' % url)
|
||||
if not app.HAVE_PYCURL:
|
||||
log.error('PYCURL not installed')
|
||||
return ('', 0)
|
||||
|
||||
headers = ''
|
||||
try:
|
||||
b = StringIO()
|
||||
c = pycurl.Curl()
|
||||
c.setopt(pycurl.URL, url.encode('utf-8'))
|
||||
c.setopt(pycurl.FOLLOWLOCATION, 1)
|
||||
# Make a HEAD request:
|
||||
c.setopt(pycurl.CUSTOMREQUEST, 'HEAD')
|
||||
c.setopt(pycurl.NOBODY, 1)
|
||||
c.setopt(pycurl.HEADER, 1)
|
||||
|
||||
c.setopt(pycurl.MAXFILESIZE, 2000000)
|
||||
c.setopt(pycurl.WRITEFUNCTION, b.write)
|
||||
c.setopt(pycurl.USERAGENT, 'Gajim ' + app.version)
|
||||
|
||||
# set proxy
|
||||
c.setopt(pycurl.PROXY, proxy['host'].encode('utf-8'))
|
||||
c.setopt(pycurl.PROXYPORT, proxy['port'])
|
||||
if proxy['useauth']:
|
||||
c.setopt(pycurl.PROXYUSERPWD, proxy['user'].encode('utf-8') +
|
||||
':' + proxy['pass'].encode('utf-8'))
|
||||
c.setopt(pycurl.PROXYAUTH, pycurl.HTTPAUTH_ANY)
|
||||
if proxy['type'] == 'http':
|
||||
c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_HTTP)
|
||||
elif proxy['type'] == 'socks5':
|
||||
c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS5)
|
||||
x = c.perform()
|
||||
c.close()
|
||||
headers = b.getvalue()
|
||||
except pycurl.error as ex:
|
||||
log.debug('Could not get head response for URL: %s' % url)
|
||||
log.debug("%s" % str(ex))
|
||||
return ('', 0)
|
||||
|
||||
ctype = ''
|
||||
searchObj = re.search(r'^Content-Type: (.*)$', headers, re.M | re.I)
|
||||
if searchObj:
|
||||
ctype = searchObj.group(1).strip()
|
||||
clen = 0
|
||||
searchObj = re.search(r'^Content-Length: (.*)$', headers, re.M | re.I)
|
||||
if searchObj:
|
||||
try:
|
||||
clen = int(searchObj.group(1).strip())
|
||||
except ValueError:
|
||||
pass
|
||||
return (ctype, clen)
|
||||
|
||||
def _get_http_direct(attrs):
|
||||
"""
|
||||
Download a file. This function should
|
||||
be launched in a separated thread.
|
||||
"""
|
||||
log.debug('Get request direct for URL: %s' % attrs['src'])
|
||||
mem, alt, max_size = b'', '', 2 * 1024 * 1024
|
||||
if 'max_size' in attrs:
|
||||
max_size = attrs['max_size']
|
||||
try:
|
||||
req = urllib2.Request(attrs['src'])
|
||||
req.add_header('User-Agent', 'Gajim ' + app.version)
|
||||
if os.name == 'nt':
|
||||
f = urllib2.urlopen(req, cafile=certifi.where())
|
||||
else:
|
||||
f = urllib2.urlopen(req)
|
||||
except Exception as ex:
|
||||
log.debug('Error loading file %s '
|
||||
% attrs['src'] + str(ex))
|
||||
pixbuf = None
|
||||
alt = attrs.get('alt', 'Broken image')
|
||||
else:
|
||||
while True:
|
||||
try:
|
||||
temp = f.read(100)
|
||||
except socket.timeout as ex:
|
||||
log.debug('Timeout loading image %s '
|
||||
% attrs['src'] + str(ex))
|
||||
alt = attrs.get('alt', '')
|
||||
if alt:
|
||||
alt += '\n'
|
||||
alt += _('Timeout loading image')
|
||||
break
|
||||
if temp:
|
||||
mem += temp
|
||||
else:
|
||||
break
|
||||
if len(mem) > max_size:
|
||||
alt = attrs.get('alt', '')
|
||||
if alt:
|
||||
alt += '\n'
|
||||
alt += _('Image is too big')
|
||||
break
|
||||
return (mem, alt)
|
||||
|
||||
def _get_http_proxy(attrs, proxy):
|
||||
"""
|
||||
Download an image through a proxy.
|
||||
This function should be launched in a
|
||||
separated thread.
|
||||
"""
|
||||
log.debug('Get request with proxy for URL: %s' % attrs['src'])
|
||||
if not app.HAVE_PYCURL:
|
||||
log.error('PYCURL not installed')
|
||||
return '', _('PyCURL is not installed')
|
||||
mem, alt, max_size = '', '', 2 * 1024 * 1024
|
||||
if 'max_size' in attrs:
|
||||
max_size = attrs['max_size']
|
||||
try:
|
||||
b = StringIO()
|
||||
c = pycurl.Curl()
|
||||
c.setopt(pycurl.URL, attrs['src'].encode('utf-8'))
|
||||
c.setopt(pycurl.FOLLOWLOCATION, 1)
|
||||
c.setopt(pycurl.MAXFILESIZE, max_size)
|
||||
c.setopt(pycurl.WRITEFUNCTION, b.write)
|
||||
c.setopt(pycurl.USERAGENT, 'Gajim ' + app.version)
|
||||
# set proxy
|
||||
c.setopt(pycurl.PROXY, proxy['host'].encode('utf-8'))
|
||||
c.setopt(pycurl.PROXYPORT, proxy['port'])
|
||||
if proxy['useauth']:
|
||||
c.setopt(pycurl.PROXYUSERPWD, proxy['user'].encode('utf-8') +
|
||||
':' + proxy['pass'].encode('utf-8'))
|
||||
c.setopt(pycurl.PROXYAUTH, pycurl.HTTPAUTH_ANY)
|
||||
if proxy['type'] == 'http':
|
||||
c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_HTTP)
|
||||
elif proxy['type'] == 'socks5':
|
||||
c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS5)
|
||||
x = c.perform()
|
||||
c.close()
|
||||
t = b.getvalue()
|
||||
return (t, attrs.get('alt', ''))
|
||||
except pycurl.error as ex:
|
||||
alt = attrs.get('alt', '')
|
||||
if alt:
|
||||
alt += '\n'
|
||||
if ex[0] == pycurl.E_FILESIZE_EXCEEDED:
|
||||
alt += _('Image is too big')
|
||||
elif ex[0] == pycurl.E_OPERATION_TIMEOUTED:
|
||||
alt += _('Timeout loading image')
|
||||
else:
|
||||
alt += _('Error loading image')
|
||||
except Exception as ex:
|
||||
log.debug('Error loading file %s ' % attrs['src'] + str(ex))
|
||||
pixbuf = None
|
||||
alt = attrs.get('alt', 'Broken image')
|
||||
return ('', alt)
|
||||
@@ -2,8 +2,11 @@
|
||||
name: Url image preview
|
||||
short_name: url_image_preview
|
||||
version: 0.5.6
|
||||
description: Url image preview in chatbox.
|
||||
description: Displays a preview of links to images
|
||||
authors = Denis Fomin <fominde@gmail.com>
|
||||
Yann Leboulanger <asterix@lagaule.org>
|
||||
homepage = http://trac-plugins.gajim.org/wiki/UrlImagePreviewPlugin
|
||||
Anders Sandblad <runeson@gmail.com>
|
||||
Thilo Molitor <thilo@eightysoft.de>
|
||||
Philipp Hoerist <philipp@hoerist.com>
|
||||
homepage = https://dev.gajim.org/gajim/gajim-plugins/wikis/UrlImagePreviewPlugin
|
||||
min_gajim_version: 0.16.11
|
||||
|
||||
@@ -1,116 +1,442 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##
|
||||
## This file is part of Gajim.
|
||||
##
|
||||
## Gajim is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published
|
||||
## by the Free Software Foundation; version 3 only.
|
||||
##
|
||||
## Gajim is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License
|
||||
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
||||
##
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GdkPixbuf
|
||||
import re
|
||||
from gi.repository import Gtk, Gdk, GLib, GObject, GdkPixbuf, Gio
|
||||
import os
|
||||
import hashlib
|
||||
import binascii
|
||||
from urllib.parse import urlparse
|
||||
from io import BytesIO
|
||||
import shutil
|
||||
|
||||
import logging
|
||||
import nbxmpp
|
||||
from gajim.common import app
|
||||
from gajim.common import ged
|
||||
from gajim.common import helpers
|
||||
from gajim.common import configpaths
|
||||
from gajim import dialogs
|
||||
from gajim.plugins import GajimPlugin
|
||||
from gajim.plugins.helpers import log_calls
|
||||
from gajim.plugins.gui import GajimPluginConfigDialog
|
||||
from gajim.conversation_textview import TextViewImage
|
||||
from .http_functions import get_http_head, get_http_file
|
||||
|
||||
EXTENSIONS = ('.png','.jpg','.jpeg','.gif','.raw','.svg')
|
||||
log = logging.getLogger('gajim.plugin_system.url_image_preview')
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except:
|
||||
log.debug('Pillow not available')
|
||||
|
||||
try:
|
||||
if os.name == 'nt':
|
||||
from cryptography.hazmat.backends.openssl import backend
|
||||
else:
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms
|
||||
from cryptography.hazmat.primitives.ciphers.modes import GCM
|
||||
decryption_available = True
|
||||
except Exception as e:
|
||||
DEP_MSG = 'For preview of encrypted images, ' \
|
||||
'please install python-cryptography!'
|
||||
log.debug('Cryptography Import Error: ' + str(e))
|
||||
log.info('Decryption/Encryption disabled due to errors')
|
||||
decryption_available = False
|
||||
|
||||
ACCEPTED_MIME_TYPES = ('image/png', 'image/jpeg', 'image/gif', 'image/raw',
|
||||
'image/svg+xml', 'image/x-ms-bmp')
|
||||
|
||||
|
||||
class UrlImagePreviewPlugin(GajimPlugin):
|
||||
@log_calls('UrlImagePreviewPlugin')
|
||||
def init(self):
|
||||
self.description = _('Url image preview in chatbox.\n'
|
||||
'Based on patch in ticket #5300:\n'
|
||||
'http://trac.gajim.org/attachment/ticket/5300.')
|
||||
if not decryption_available:
|
||||
self.available_text = DEP_MSG
|
||||
self.config_dialog = UrlImagePreviewPluginConfigDialog(self)
|
||||
self.events_handlers = {}
|
||||
self.events_handlers['message-received'] = (
|
||||
ged.PRECORE, self.handle_message_received)
|
||||
self.gui_extension_points = {
|
||||
'chat_control_base': (self.connect_with_chat_control,
|
||||
self.disconnect_from_chat_control),
|
||||
'print_special_text': (self.print_special_text,
|
||||
self.print_special_text1),}
|
||||
'chat_control_base': (self.connect_with_chat_control,
|
||||
self.disconnect_from_chat_control),
|
||||
'print_special_text': (self.print_special_text, None), }
|
||||
self.config_default_values = {
|
||||
'PREVIEW_SIZE': (150, 'Preview size(10-512)'),}
|
||||
self.chat_control = None
|
||||
self.controls = []
|
||||
'PREVIEW_SIZE': (150, 'Preview size(10-512)'),
|
||||
'MAX_FILE_SIZE': (524288, 'Max file size for image preview'),
|
||||
'LEFTCLICK_ACTION': ('open_menuitem', 'Open')}
|
||||
self.controls = {}
|
||||
|
||||
# remove oob tag if oob url == message text
|
||||
def handle_message_received(self, event):
|
||||
oob_node = event.stanza.getTag('x', namespace=nbxmpp.NS_X_OOB)
|
||||
oob_url = None
|
||||
oob_desc = None
|
||||
if oob_node:
|
||||
oob_url = oob_node.getTagData('url')
|
||||
oob_desc = oob_node.getTagData('desc')
|
||||
if oob_url and oob_url == event.msgtxt and \
|
||||
(not oob_desc or oob_desc == ""):
|
||||
log.debug("Detected oob tag containing same"
|
||||
"url as the message text, deleting oob tag...")
|
||||
event.stanza.delChild(oob_node)
|
||||
|
||||
@log_calls('UrlImagePreviewPlugin')
|
||||
def connect_with_chat_control(self, chat_control):
|
||||
|
||||
self.chat_control = chat_control
|
||||
control = Base(self, self.chat_control)
|
||||
self.controls.append(control)
|
||||
account = chat_control.contact.account.name
|
||||
jid = chat_control.contact.jid
|
||||
if account not in self.controls:
|
||||
self.controls[account] = {}
|
||||
self.controls[account][jid] = Base(self, chat_control)
|
||||
|
||||
@log_calls('UrlImagePreviewPlugin')
|
||||
def disconnect_from_chat_control(self, chat_control):
|
||||
for control in self.controls:
|
||||
control.disconnect_from_chat_control()
|
||||
self.controls = []
|
||||
account = chat_control.contact.account.name
|
||||
jid = chat_control.contact.jid
|
||||
self.controls[account][jid].deinit()
|
||||
del self.controls[account][jid]
|
||||
|
||||
def print_special_text(self, tv, special_text, other_tags, graphics=True,
|
||||
additional_data={}):
|
||||
for control in self.controls:
|
||||
if control.chat_control.conv_textview != tv:
|
||||
additional_data=None, iter_=None):
|
||||
account = tv.account
|
||||
for jid in self.controls[account]:
|
||||
if self.controls[account][jid].chat_control.conv_textview != tv:
|
||||
continue
|
||||
control.print_special_text(special_text, other_tags, graphics=True)
|
||||
self.controls[account][jid].print_special_text(
|
||||
special_text, other_tags, graphics=graphics,
|
||||
additional_data=additional_data, iter_=iter_)
|
||||
return
|
||||
|
||||
def print_special_text1(self, chat_control, special_text, other_tags=None,
|
||||
graphics=True, additional_data={}):
|
||||
for control in self.controls:
|
||||
if control.chat_control == chat_control:
|
||||
control.disconnect_from_chat_control()
|
||||
self.controls.remove(control)
|
||||
|
||||
class Base(object):
|
||||
def __init__(self, plugin, chat_control):
|
||||
self.plugin = plugin
|
||||
self.chat_control = chat_control
|
||||
self.textview = self.chat_control.conv_textview
|
||||
self.handlers = {}
|
||||
|
||||
def print_special_text(self, special_text, other_tags, graphics=True):
|
||||
if not app.interface.basic_pattern_re.match(special_text):
|
||||
return
|
||||
self.directory = os.path.join(configpaths.gajimpaths['MY_DATA'],
|
||||
'downloads')
|
||||
self.thumbpath = os.path.join(configpaths.gajimpaths['MY_CACHE'],
|
||||
'downloads.thumb')
|
||||
|
||||
try:
|
||||
self._create_path(self.directory)
|
||||
self._create_path(self.thumbpath)
|
||||
except Exception as e:
|
||||
log.error("Error creating download and/or thumbnail folder!")
|
||||
raise
|
||||
|
||||
def deinit(self):
|
||||
# remove all register handlers on wigets, created by self.xml
|
||||
# to prevent circular references among objects
|
||||
for i in list(self.handlers.keys()):
|
||||
if self.handlers[i].handler_is_connected(i):
|
||||
self.handlers[i].disconnect(i)
|
||||
del self.handlers[i]
|
||||
|
||||
def print_special_text(self, special_text, other_tags, graphics=True,
|
||||
additional_data=None, iter_=None):
|
||||
# remove qip bbcode
|
||||
special_text = special_text.rsplit('[/img]')[0]
|
||||
|
||||
name, extension = os.path.splitext(special_text)
|
||||
if extension.lower() not in EXTENSIONS:
|
||||
return
|
||||
if not special_text.startswith('http://') and \
|
||||
special_text.startswith('www.'):
|
||||
if special_text.startswith('www.'):
|
||||
special_text = 'http://' + special_text
|
||||
if not special_text.startswith('ftp://') and \
|
||||
special_text.startswith('ftp.'):
|
||||
if special_text.startswith('ftp.'):
|
||||
special_text = 'ftp://' + special_text
|
||||
|
||||
# show pics preview
|
||||
buffer_ = self.textview.tv.get_buffer()
|
||||
iter_ = buffer_.get_end_iter()
|
||||
mark = buffer_.create_mark(None, iter_, True)
|
||||
# start downloading image
|
||||
app.thread_interface(helpers.download_image, [
|
||||
self.textview.account, {'src': special_text}], self._update_img,
|
||||
[mark])
|
||||
urlparts = urlparse(special_text)
|
||||
if urlparts.scheme not in ["https", "http", "ftp", "ftps", 'aesgcm'] or \
|
||||
not urlparts.netloc:
|
||||
log.info("Not accepting URL for image preview: %s" % special_text)
|
||||
return
|
||||
|
||||
def _update_img(self, mem_alt, mark):
|
||||
mem, alt = mem_alt
|
||||
if mem:
|
||||
# Don't print the URL in the message window (in the calling function)
|
||||
self.textview.plugin_modified = True
|
||||
|
||||
buffer_ = self.textview.tv.get_buffer()
|
||||
if not iter_:
|
||||
iter_ = buffer_.get_end_iter()
|
||||
|
||||
# Show URL, until image is loaded (if ever)
|
||||
ttt = buffer_.get_tag_table()
|
||||
repl_start = buffer_.create_mark(None, iter_, True)
|
||||
buffer_.insert_with_tags(iter_, special_text,
|
||||
*[(ttt.lookup(t) if isinstance(t, str) else t) for t in ["url"]])
|
||||
repl_end = buffer_.create_mark(None, iter_, True)
|
||||
|
||||
filename = os.path.basename(urlparts.path)
|
||||
ext = os.path.splitext(filename)[1]
|
||||
name = os.path.splitext(filename)[0]
|
||||
namehash = hashlib.sha1(special_text.encode('utf-8')).hexdigest()
|
||||
newfilename = name + '_' + namehash + ext
|
||||
thumbfilename = name + '_' + namehash + '_thumb_' \
|
||||
+ str(self.plugin.config['PREVIEW_SIZE']) + ext
|
||||
|
||||
filepath = os.path.join(self.directory, newfilename)
|
||||
thumbpath = os.path.join(self.thumbpath, thumbfilename)
|
||||
filepaths = [filepath, thumbpath]
|
||||
|
||||
key = ''
|
||||
iv = ''
|
||||
encrypted = False
|
||||
if urlparts.fragment:
|
||||
fragment = binascii.unhexlify(urlparts.fragment)
|
||||
key = fragment[16:]
|
||||
iv = fragment[:16]
|
||||
if len(key) == 32 and len(iv) == 16:
|
||||
encrypted = True
|
||||
if not encrypted:
|
||||
key = fragment[12:]
|
||||
iv = fragment[:12]
|
||||
if len(key) == 32 and len(iv) == 12:
|
||||
encrypted = True
|
||||
|
||||
# file exists but thumbnail got deleted
|
||||
if os.path.exists(filepath) and not os.path.exists(thumbpath):
|
||||
with open(filepath, 'rb') as f:
|
||||
mem = f.read()
|
||||
f.closed
|
||||
app.thread_interface(
|
||||
self._save_thumbnail, [thumbpath, (mem, '')],
|
||||
self._update_img, [special_text, repl_start,
|
||||
repl_end, filepath, encrypted])
|
||||
|
||||
# display thumbnail if already downloadeded
|
||||
# (but only if file also exists)
|
||||
elif os.path.exists(filepath) and os.path.exists(thumbpath):
|
||||
app.thread_interface(
|
||||
self._load_thumbnail, [thumbpath],
|
||||
self._update_img, [special_text, repl_start,
|
||||
repl_end, filepath, encrypted])
|
||||
|
||||
# or download file, calculate thumbnail and finally display it
|
||||
else:
|
||||
if encrypted and not decryption_available:
|
||||
log.debug('Please install Crytography to decrypt pictures')
|
||||
else:
|
||||
# First get the http head request
|
||||
# which does not fetch data, just headers
|
||||
# then check the mime type and filesize
|
||||
if urlparts.scheme == 'aesgcm':
|
||||
special_text = 'https://' + special_text[9:]
|
||||
app.thread_interface(
|
||||
get_http_head, [self.textview.account, special_text],
|
||||
self._check_mime_size, [special_text, repl_start, repl_end,
|
||||
filepaths, key, iv, encrypted])
|
||||
|
||||
def _save_thumbnail(self, thumbpath, tuple_arg):
|
||||
mem, alt = tuple_arg
|
||||
size = self.plugin.config['PREVIEW_SIZE']
|
||||
use_Gtk = False
|
||||
output = None
|
||||
|
||||
try:
|
||||
output = BytesIO()
|
||||
im = Image.open(BytesIO(mem))
|
||||
im.thumbnail((size, size), Image.ANTIALIAS)
|
||||
im.save(output, "jpeg", quality=100, optimize=True)
|
||||
mem = output.getvalue()
|
||||
output.close()
|
||||
except Exception as e:
|
||||
if output:
|
||||
output.close()
|
||||
log.info("Failed to load image using pillow, "
|
||||
"falling back to gdk pixbuf.")
|
||||
log.debug(e)
|
||||
use_Gtk = True
|
||||
|
||||
if use_Gtk:
|
||||
log.info("Pillow not available or file corrupt, "
|
||||
"trying to load using gdk pixbuf.")
|
||||
try:
|
||||
loader = GdkPixbuf.PixbufLoader()
|
||||
loader.write(mem)
|
||||
loader.close()
|
||||
pixbuf = loader.get_pixbuf()
|
||||
pixbuf, w, h = self.get_pixbuf_of_size(pixbuf,
|
||||
self.plugin.config['PREVIEW_SIZE'])
|
||||
buffer_ = mark.get_buffer()
|
||||
end_iter = buffer_.get_iter_at_mark(mark)
|
||||
anchor = buffer_.create_child_anchor(end_iter)
|
||||
img = TextViewImage(anchor, alt)
|
||||
img.set_from_pixbuf(pixbuf)
|
||||
img.show()
|
||||
self.textview.tv.add_child_at_anchor(img, anchor)
|
||||
except Exception:
|
||||
pass
|
||||
pixbuf, w, h = self._get_pixbuf_of_size(pixbuf, size)
|
||||
|
||||
ok, mem = pixbuf.save_to_bufferv("jpeg", ["quality"], ["100"])
|
||||
except Exception as e:
|
||||
log.info("Failed to load image using gdk pixbuf, "
|
||||
"ignoring image.")
|
||||
log.debug(e)
|
||||
return ('', '')
|
||||
|
||||
def get_pixbuf_of_size(self, pixbuf, size):
|
||||
try:
|
||||
self._create_path(os.path.dirname(thumbpath))
|
||||
self._write_file(thumbpath, mem)
|
||||
except Exception as e:
|
||||
dialogs.ErrorDialog(
|
||||
_('Could not save file'),
|
||||
_('Exception raised while saving thumbnail '
|
||||
'for image file (see error log for more '
|
||||
'information)'),
|
||||
transient_for=self.chat_control.parent_win.window)
|
||||
log.error(str(e))
|
||||
return (mem, alt)
|
||||
|
||||
def _load_thumbnail(self, thumbpath):
|
||||
with open(thumbpath, 'rb') as f:
|
||||
mem = f.read()
|
||||
f.closed
|
||||
return (mem, '')
|
||||
|
||||
def _write_file(self, path, data):
|
||||
log.info("Writing '%s' of size %d..." % (path, len(data)))
|
||||
try:
|
||||
with open(path, "wb") as output_file:
|
||||
output_file.write(data)
|
||||
output_file.closed
|
||||
except Exception as e:
|
||||
log.error("Failed to write file '%s'!" % path)
|
||||
raise
|
||||
|
||||
def _update_img(self, tuple_arg, url, repl_start, repl_end,
|
||||
filepath, encrypted):
|
||||
mem, alt = tuple_arg
|
||||
if mem:
|
||||
try:
|
||||
urlparts = urlparse(url)
|
||||
filename = os.path.basename(urlparts.path)
|
||||
eb = Gtk.EventBox()
|
||||
eb.connect('button-press-event', self.on_button_press_event,
|
||||
filepath, filename, url, encrypted)
|
||||
eb.connect('enter-notify-event', self.on_enter_event)
|
||||
eb.connect('leave-notify-event', self.on_leave_event)
|
||||
|
||||
# this is threadsafe
|
||||
# (Gtk textview is NOT threadsafe by itself!!)
|
||||
def add_to_textview():
|
||||
try: # textview closed in the meantime etc.
|
||||
buffer_ = repl_start.get_buffer()
|
||||
iter_ = buffer_.get_iter_at_mark(repl_start)
|
||||
buffer_.insert(iter_, "\n")
|
||||
anchor = buffer_.create_child_anchor(iter_)
|
||||
|
||||
# Use url as tooltip for image
|
||||
img = TextViewImage(anchor, url)
|
||||
loader = GdkPixbuf.PixbufLoader()
|
||||
loader.write(mem)
|
||||
loader.close()
|
||||
pixbuf = loader.get_pixbuf()
|
||||
img.set_from_pixbuf(pixbuf)
|
||||
|
||||
eb.add(img)
|
||||
eb.show_all()
|
||||
self.textview.tv.add_child_at_anchor(eb, anchor)
|
||||
buffer_.delete(iter_,
|
||||
buffer_.get_iter_at_mark(repl_end))
|
||||
except Exception as ex:
|
||||
log.warn("Exception while loading %s: %s" % (str(url), str(ex)))
|
||||
return False
|
||||
# add to mainloop --> make call threadsafe
|
||||
GObject.idle_add(add_to_textview)
|
||||
except Exception:
|
||||
# URL is already displayed
|
||||
log.error('Could not display image for URL: %s'
|
||||
% url)
|
||||
raise
|
||||
else:
|
||||
# If image could not be downloaded, URL is already displayed
|
||||
log.error('Could not download image for URL: %s -- %s'
|
||||
% (url, alt))
|
||||
|
||||
def _check_mime_size(self, tuple_arg,
|
||||
url, repl_start, repl_end, filepaths,
|
||||
key, iv, encrypted):
|
||||
file_mime, file_size = tuple_arg
|
||||
# Check if mime type is acceptable
|
||||
if file_mime == '' and file_size == 0:
|
||||
log.info("Failed to load HEAD Request for URL: '%s'"
|
||||
"(see debug log for more info)" % url)
|
||||
# URL is already displayed
|
||||
return
|
||||
if file_mime.lower() not in ACCEPTED_MIME_TYPES:
|
||||
log.info("Not accepted mime type '%s' for URL: '%s'"
|
||||
% (file_mime.lower(), url))
|
||||
# URL is already displayed
|
||||
return
|
||||
# Check if file size is acceptable
|
||||
if file_size > self.plugin.config['MAX_FILE_SIZE'] or file_size == 0:
|
||||
log.info("File size (%s) too big or unknown (zero) for URL: '%s'"
|
||||
% (str(file_size), url))
|
||||
# URL is already displayed
|
||||
return
|
||||
|
||||
attributes = {'src': url,
|
||||
'max_size': self.plugin.config['MAX_FILE_SIZE'],
|
||||
'filepaths': filepaths,
|
||||
'key': key,
|
||||
'iv': iv}
|
||||
|
||||
app.thread_interface(
|
||||
self._download_image, [self.textview.account,
|
||||
attributes, encrypted],
|
||||
self._update_img, [url, repl_start, repl_end,
|
||||
filepaths[0], encrypted])
|
||||
|
||||
def _download_image(self, account, attributes, encrypted):
|
||||
filepath = attributes['filepaths'][0]
|
||||
thumbpath = attributes['filepaths'][1]
|
||||
key = attributes['key']
|
||||
iv = attributes['iv']
|
||||
mem, alt = get_http_file(account, attributes)
|
||||
|
||||
# Decrypt file if necessary
|
||||
if encrypted:
|
||||
mem = self._aes_decrypt_fast(key, iv, mem)
|
||||
|
||||
try:
|
||||
# Write file to harddisk
|
||||
self._write_file(filepath, mem)
|
||||
except Exception as e:
|
||||
dialogs.ErrorDialog(
|
||||
_('Could not save file'),
|
||||
_('Exception raised while saving image file'
|
||||
' (see error log for more information)'),
|
||||
transient_for=self.chat_control.parent_win.window)
|
||||
log.error(str(e))
|
||||
|
||||
# Create thumbnail, write it to harddisk and return it
|
||||
return self._save_thumbnail(thumbpath, (mem, alt))
|
||||
|
||||
def _create_path(self, folder):
|
||||
if os.path.exists(folder):
|
||||
return
|
||||
log.debug("creating folder '%s'" % folder)
|
||||
os.mkdir(folder, 0o700)
|
||||
|
||||
def _aes_decrypt_fast(self, key, iv, payload):
|
||||
# Use AES128 GCM with the given key and iv to decrypt the payload.
|
||||
if os.name == 'nt':
|
||||
be = backend
|
||||
else:
|
||||
be = default_backend()
|
||||
data = payload[:-16]
|
||||
tag = payload[-16:]
|
||||
decryptor = Cipher(
|
||||
algorithms.AES(key),
|
||||
GCM(iv, tag=tag),
|
||||
backend=be).decryptor()
|
||||
return decryptor.update(data) + decryptor.finalize()
|
||||
|
||||
def _get_pixbuf_of_size(self, pixbuf, size):
|
||||
# Creates a pixbuf that fits in the specified square of sizexsize
|
||||
# while preserving the aspect ratio
|
||||
# Returns tuple: (scaled_pixbuf, actual_width, actual_height)
|
||||
@@ -127,28 +453,198 @@ class Base(object):
|
||||
image_height = int(size)
|
||||
|
||||
crop_pixbuf = pixbuf.scale_simple(image_width, image_height,
|
||||
GdkPixbuf.InterpType.BILINEAR)
|
||||
GdkPixbuf.InterpType.BILINEAR)
|
||||
return (crop_pixbuf, image_width, image_height)
|
||||
|
||||
def make_rightclick_menu(self, event, data):
|
||||
xml = Gtk.Builder()
|
||||
xml.set_translation_domain('gajim_plugins')
|
||||
xml.add_from_file(self.plugin.local_file_path('context_menu.ui'))
|
||||
menu = xml.get_object('context_menu')
|
||||
|
||||
open_menuitem = xml.get_object('open_menuitem')
|
||||
save_as_menuitem = xml.get_object('save_as_menuitem')
|
||||
copy_link_location_menuitem = \
|
||||
xml.get_object('copy_link_location_menuitem')
|
||||
open_link_in_browser_menuitem = \
|
||||
xml.get_object('open_link_in_browser_menuitem')
|
||||
open_file_in_browser_menuitem = \
|
||||
xml.get_object('open_file_in_browser_menuitem')
|
||||
extras_separator = \
|
||||
xml.get_object('extras_separator')
|
||||
|
||||
if data["encrypted"]:
|
||||
open_link_in_browser_menuitem.hide()
|
||||
if app.config.get('autodetect_browser_mailer') \
|
||||
or app.config.get('custombrowser') == '':
|
||||
extras_separator.hide()
|
||||
open_file_in_browser_menuitem.hide()
|
||||
|
||||
id_ = open_menuitem.connect(
|
||||
'activate', self.on_open_menuitem_activate, data)
|
||||
self.handlers[id_] = open_menuitem
|
||||
id_ = save_as_menuitem.connect(
|
||||
'activate', self.on_save_as_menuitem_activate, data)
|
||||
self.handlers[id_] = save_as_menuitem
|
||||
id_ = copy_link_location_menuitem.connect(
|
||||
'activate', self.on_copy_link_location_menuitem_activate, data)
|
||||
self.handlers[id_] = copy_link_location_menuitem
|
||||
id_ = open_link_in_browser_menuitem.connect(
|
||||
'activate', self.on_open_link_in_browser_menuitem_activate, data)
|
||||
self.handlers[id_] = open_link_in_browser_menuitem
|
||||
id_ = open_file_in_browser_menuitem.connect(
|
||||
'activate', self.on_open_file_in_browser_menuitem_activate, data)
|
||||
self.handlers[id_] = open_file_in_browser_menuitem
|
||||
|
||||
return menu
|
||||
|
||||
def on_open_menuitem_activate(self, menu, data):
|
||||
filepath = data["filepath"]
|
||||
helpers.launch_file_manager(filepath)
|
||||
|
||||
def on_save_as_menuitem_activate(self, menu, data):
|
||||
filepath = data["filepath"]
|
||||
original_filename = data["original_filename"]
|
||||
def on_continue(response, target_path):
|
||||
if response < 0:
|
||||
return
|
||||
shutil.copy(filepath, target_path)
|
||||
dialog.destroy()
|
||||
|
||||
def on_ok(widget):
|
||||
target_path = dialog.get_filename()
|
||||
if os.path.exists(target_path):
|
||||
# check if we have write permissions
|
||||
if not os.access(target_path, os.W_OK):
|
||||
file_name = os.path.basename(target_path)
|
||||
dialogs.ErrorDialog(
|
||||
_('Cannot overwrite existing file "%s"') % file_name,
|
||||
_('A file with this name already exists and you do '
|
||||
'not have permission to overwrite it.'))
|
||||
return
|
||||
dialog2 = dialogs.FTOverwriteConfirmationDialog(
|
||||
_('This file already exists'),
|
||||
_('What do you want to do?'),
|
||||
propose_resume=False,
|
||||
on_response=(on_continue, target_path),
|
||||
transient_for=dialog)
|
||||
dialog2.set_destroy_with_parent(True)
|
||||
else:
|
||||
dirname = os.path.dirname(target_path)
|
||||
if not os.access(dirname, os.W_OK):
|
||||
dialogs.ErrorDialog(
|
||||
_('Directory "%s" is not writable') % dirname,
|
||||
_('You do not have permission to '
|
||||
'create files in this directory.'))
|
||||
return
|
||||
on_continue(0, target_path)
|
||||
|
||||
def on_cancel(widget):
|
||||
dialog.destroy()
|
||||
|
||||
dialog = dialogs.FileChooserDialog(
|
||||
title_text=_('Save Image as...'),
|
||||
action=Gtk.FileChooserAction.SAVE,
|
||||
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
||||
Gtk.STOCK_SAVE, Gtk.ResponseType.OK),
|
||||
default_response=Gtk.ResponseType.OK,
|
||||
current_folder=app.config.get('last_save_dir'),
|
||||
on_response_ok=on_ok,
|
||||
on_response_cancel=on_cancel)
|
||||
|
||||
dialog.set_current_name(original_filename)
|
||||
dialog.connect('delete-event', lambda widget, event:
|
||||
on_cancel(widget))
|
||||
|
||||
def on_copy_link_location_menuitem_activate(self, menu, data):
|
||||
url = data["url"]
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
clipboard.set_text(url, -1)
|
||||
clipboard.store()
|
||||
|
||||
def on_open_link_in_browser_menuitem_activate(self, menu, data):
|
||||
url = data["url"]
|
||||
if data["encrypted"]:
|
||||
dialogs.ErrorDialog(
|
||||
_('Encrypted file'),
|
||||
_('You cannot open encrypted files in your '
|
||||
'browser directly. Try "Open Downloaded File '
|
||||
'in Browser" instead.'),
|
||||
transient_for=self.chat_control.parent_win.window)
|
||||
else:
|
||||
helpers.launch_browser_mailer('url', url)
|
||||
|
||||
def on_open_file_in_browser_menuitem_activate(self, menu, data):
|
||||
if os.name == "nt":
|
||||
filepath = "file://" + os.path.abspath(data["filepath"])
|
||||
else:
|
||||
filepath = "file://" + data["filepath"]
|
||||
if app.config.get('autodetect_browser_mailer') \
|
||||
or app.config.get('custombrowser') == '':
|
||||
dialogs.ErrorDialog(
|
||||
_('Cannot open downloaded file in browser'),
|
||||
_('You have to set a custom browser executable '
|
||||
'in your gajim settings for this to work.'),
|
||||
transient_for=self.chat_control.parent_win.window)
|
||||
return
|
||||
command = app.config.get('custombrowser')
|
||||
command = helpers.build_command(command, filepath)
|
||||
try:
|
||||
helpers.exec_command(command)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Change mouse pointer to HAND2 when
|
||||
# mouse enter the eventbox with the image
|
||||
def on_enter_event(self, eb, event):
|
||||
self.textview.tv.get_window(
|
||||
Gtk.TextWindowType.TEXT).set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2))
|
||||
|
||||
# Change mouse pointer to default when mouse leaves the eventbox
|
||||
def on_leave_event(self, eb, event):
|
||||
self.textview.tv.get_window(
|
||||
Gtk.TextWindowType.TEXT).set_cursor(Gdk.Cursor(Gdk.CursorType.XTERM))
|
||||
|
||||
def on_button_press_event(self, eb, event, filepath,
|
||||
original_filename, url, encrypted):
|
||||
data = {"filepath": filepath,
|
||||
"original_filename": original_filename,
|
||||
"url": url,
|
||||
"encrypted": encrypted}
|
||||
# left click
|
||||
if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 1:
|
||||
method = getattr(self, "on_"
|
||||
+ self.plugin.config['LEFTCLICK_ACTION']
|
||||
+ "_activate")
|
||||
method(event, data)
|
||||
# right klick
|
||||
elif event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:
|
||||
menu = self.make_rightclick_menu(event, data)
|
||||
#menu.attach_to_widget(self.tv, None)
|
||||
#menu.popup(None, None, None, event.button, event.time)
|
||||
menu.popup_at_pointer(event)
|
||||
|
||||
def disconnect_from_chat_control(self):
|
||||
pass
|
||||
|
||||
|
||||
class UrlImagePreviewPluginConfigDialog(GajimPluginConfigDialog):
|
||||
max_file_size = [262144, 524288, 1048576, 5242880, 10485760]
|
||||
leftclick_action = ['open_menuitem', 'save_as_menuitem', 'copy_link_location_menuitem',
|
||||
'open_link_in_browser_menuitem', 'open_file_in_browser_menuitem']
|
||||
|
||||
def init(self):
|
||||
self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path(
|
||||
'config_dialog.ui')
|
||||
self.xml = Gtk.Builder()
|
||||
self.xml.set_translation_domain('gajim_plugins')
|
||||
self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH, ['vbox1'])
|
||||
self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH, [
|
||||
'vbox1', 'liststore1', 'liststore2'])
|
||||
self.preview_size_spinbutton = self.xml.get_object('preview_size')
|
||||
adjustment = Gtk.Adjustment(value=20,
|
||||
lower=10,
|
||||
upper=512,
|
||||
step_increment=1,
|
||||
page_increment=10,
|
||||
page_size=0)
|
||||
self.preview_size_spinbutton.set_adjustment(adjustment)
|
||||
self.preview_size_spinbutton.get_adjustment().configure(20, 10, 512, 1,
|
||||
10, 0)
|
||||
self.max_size_combobox = self.xml.get_object('max_size_combobox')
|
||||
self.leftclick_action_combobox = self.xml.get_object('leftclick_action_combobox')
|
||||
vbox = self.xml.get_object('vbox1')
|
||||
self.get_child().pack_start(vbox, True, True, 0)
|
||||
|
||||
@@ -157,7 +653,37 @@ class UrlImagePreviewPluginConfigDialog(GajimPluginConfigDialog):
|
||||
def on_run(self):
|
||||
self.preview_size_spinbutton.set_value(self.plugin.config[
|
||||
'PREVIEW_SIZE'])
|
||||
|
||||
value = self.plugin.config['MAX_FILE_SIZE']
|
||||
if value:
|
||||
# this fails if we upgrade from an old version
|
||||
# which has other file size values than we have now
|
||||
try:
|
||||
self.max_size_combobox.set_active(
|
||||
self.max_file_size.index(value))
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
self.max_size_combobox.set_active(-1)
|
||||
|
||||
value = self.plugin.config['LEFTCLICK_ACTION']
|
||||
if value:
|
||||
# this fails if we upgrade from an old version
|
||||
# which has other file size values than we have now
|
||||
try:
|
||||
self.leftclick_action_combobox.set_active(
|
||||
self.leftclick_action.index(value))
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
self.leftclick_action_combobox.set_active(0)
|
||||
|
||||
def preview_size_value_changed(self, spinbutton):
|
||||
self.plugin.config['PREVIEW_SIZE'] = spinbutton.get_value()
|
||||
|
||||
def max_size_value_changed(self, widget):
|
||||
self.plugin.config['MAX_FILE_SIZE'] = self.max_file_size[
|
||||
self.max_size_combobox.get_active()]
|
||||
|
||||
def leftclick_action_changed(self, widget):
|
||||
self.plugin.config['LEFTCLICK_ACTION'] = self.leftclick_action[
|
||||
self.leftclick_action_combobox.get_active()]
|
||||
|
||||
Reference in New Issue
Block a user