[preview] Add preview widget

This commit is contained in:
Daniel Brötzmann
2020-03-08 12:44:33 +01:00
committed by Philipp Hörist
parent de16fa6f9a
commit dda1e91b35
6 changed files with 534 additions and 101 deletions

View File

@@ -1,9 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 --> <!-- Generated with glade 3.22.2 -->
<interface> <interface>
<requires lib="gtk+" version="3.20"/> <requires lib="gtk+" version="3.20"/>
<object class="GtkMenu" id="context_menu"> <object class="GtkMenu" id="context_menu">
<property name="can_focus">False</property> <property name="can_focus">False</property>
<child>
<object class="GtkMenuItem" id="download">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Download</property>
<property name="use_underline">True</property>
</object>
</child>
<child> <child>
<object class="GtkMenuItem" id="open"> <object class="GtkMenuItem" id="open">
<property name="visible">True</property> <property name="visible">True</property>

View File

@@ -0,0 +1,103 @@
# This is an excerpt of Media Types from
# https://www.iana.org/assignments/media-types/media-types.xhtml
# plus some additions
MIME_TYPES = (
# application/
'application/calendar+json',
'application/calendar+xml',
'application/epub+zip',
'application/json',
'application/mp4',
'application/msword',
'application/octet-stream',
'application/ogg',
'application/pdf',
'application/pgp-encrypted',
'application/pgp-signature',
'application/postscript',
'application/rtf',
'application/vcard+json',
'application/vcard+xml',
'application/vnd.amazon.mobi8-ebook',
'application/vnd.google-earth.kml+xml',
'application/vnd.google-earth.kmz',
# Start office
'application/vnd.ms-access',
'application/vnd.ms-excel',
'application/vnd.ms-excel.addin.macroEnabled.12',
'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
'application/vnd.ms-excel.sheet.macroEnabled.12',
'application/vnd.ms-excel.template.macroEnabled.12',
'application/vnd.ms-powerpoint',
'application/vnd.ms-powerpoint.addin.macroEnabled.12',
'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
'application/vnd.ms-powerpoint.template.macroEnabled.12',
'application/vnd.ms-word.document.macroEnabled.12',
'application/vnd.ms-word.template.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
'application/vnd.openxmlformats-officedocument.presentationml.template',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'application/vnd.openxmlformats-officedocument.vmlDrawing',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
# End office
'application/vnd.sqlite3',
'application/zip',
# audio/*
'audio/aac',
'audio/ac3',
'audio/flac',
'audio/mp4',
'audio/mpeg',
'audio/ogg',
'audio/opus',
'audio/wav',
'audio/x-flac',
'audio/x-matroska',
# font/*
'font/ttf',
'font/woff',
'font/woff2',
# image/*
'image/bmp',
'image/x-bmp',
'image/x-ms-bmp',
'image/gif',
'image/heic',
'image/heif',
'image/jpeg',
'image/png',
'image/svg+xml',
'image/tiff',
'image/vnd.adobe.photoshop',
'image/vnd.dwg',
'image/vnd.dxf',
'image/vnd.microsoft.icon',
'image/x-icon',
'image/x-xcf',
# model/*
'model/mtl',
'model/obj',
'model/stl',
# text/*
'text/calendar',
'text/csv',
'text/markdown',
'text/rtf',
'text/vcard',
# video/*
'video/H264',
'video/H265',
'video/mp4',
'video/mpeg4-generic',
'video/ogg',
'video/quicktime',
'video/vc1',
'video/VP8',
'video/webm',
'video/x-matroska',
'video/x-msvideo',
)

View File

@@ -0,0 +1,11 @@
.preview-box {
border: 1px solid;
border-color: @borders;
border-radius: 5px;
padding: 10px;
margin: 5px;
background-color: @theme_unfocused_base_color;
}
.preview-button {
border: 1px solid @borders;
}

View File

@@ -0,0 +1,166 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="preview_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">3</property>
<child>
<object class="GtkEventBox" id="event_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="button_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="file_name">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="selectable">True</property>
<property name="ellipsize">end</property>
<property name="single_line_mode">True</property>
<property name="xalign">0</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="file_size">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="single_line_mode">True</property>
<property name="xalign">0</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="save_as_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Save as...</property>
<property name="valign">end</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-save-as-symbolic</property>
</object>
</child>
<style>
<class name="preview-button"/>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="open_folder_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Open folder</property>
<property name="valign">end</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">folder-symbolic</property>
</object>
</child>
<style>
<class name="preview-button"/>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="download_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Download</property>
<property name="valign">end</property>
<property name="relief">none</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">folder-download-symbolic</property>
</object>
</child>
<style>
<class name="preview-button"/>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="preview-box"/>
</style>
</object>
</interface>

View File

@@ -17,6 +17,7 @@
import os import os
import logging import logging
import shutil import shutil
import mimetypes
from pathlib import Path from pathlib import Path
from functools import partial from functools import partial
from urllib.parse import urlparse from urllib.parse import urlparse
@@ -24,9 +25,10 @@ from urllib.parse import unquote
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import Gdk from gi.repository import Gdk
from gi.repository import GdkPixbuf
from gi.repository import Gio
from gi.repository import GLib from gi.repository import GLib
from gi.repository import Soup from gi.repository import Soup
from gi.repository import GdkPixbuf
from gajim.common import app from gajim.common import app
from gajim.common import configpaths from gajim.common import configpaths
@@ -38,15 +40,16 @@ from gajim.common.helpers import get_tls_error_phrase
from gajim.common.helpers import get_user_proxy from gajim.common.helpers import get_user_proxy
from gajim.gtk.dialogs import ErrorDialog from gajim.gtk.dialogs import ErrorDialog
from gajim.gtk.filechoosers import FileSaveDialog from gajim.gtk.filechoosers import FileSaveDialog
from gajim.gtk.util import load_icon from gajim.gtk.util import get_cursor
from gajim.gtk.util import get_monitor_scale_factor from gajim.gtk.util import get_monitor_scale_factor
from gajim.gtk.util import load_icon
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.helpers import get_builder from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from url_image_preview.config_dialog import UrlImagePreviewConfigDialog from url_image_preview.config_dialog import UrlImagePreviewConfigDialog
from url_image_preview.mime_types import MIME_TYPES
log = logging.getLogger('gajim.p.preview') log = logging.getLogger('gajim.p.preview')
@@ -72,26 +75,29 @@ if ERROR_MSG is None:
from url_image_preview.utils import parse_fragment from url_image_preview.utils import parse_fragment
from url_image_preview.utils import create_thumbnail from url_image_preview.utils import create_thumbnail
from url_image_preview.utils import pixbuf_from_data from url_image_preview.utils import pixbuf_from_data
from url_image_preview.utils import create_clickable_image
from url_image_preview.utils import filename_from_uri from url_image_preview.utils import filename_from_uri
# pylint: enable=ungrouped-imports # pylint: enable=ungrouped-imports
def get_accepted_mime_types():
accepted_mime_types = set() def get_previewable_mime_types():
previewable_mime_types = set()
for fmt in GdkPixbuf.Pixbuf.get_formats(): for fmt in GdkPixbuf.Pixbuf.get_formats():
for mime_type in fmt.get_mime_types(): for mime_type in fmt.get_mime_types():
accepted_mime_types.add(mime_type.lower()) previewable_mime_types.add(mime_type.lower())
if Image is not None: if Image is not None:
Image.init() Image.init()
for mime_type in Image.MIME.values(): for mime_type in Image.MIME.values():
accepted_mime_types.add(mime_type.lower()) previewable_mime_types.add(mime_type.lower())
return tuple(filter( return tuple(filter(
lambda mime_type: mime_type.startswith('image'), lambda mime_type: mime_type.startswith('image'),
accepted_mime_types previewable_mime_types
)) ))
ACCEPTED_MIME_TYPES = get_accepted_mime_types()
PREVIEWABLE_MIME_TYPES = get_previewable_mime_types()
mime_types = set(MIME_TYPES)
# Merge both: if its a previewable image, it should be allowed
ALLOWED_MIME_TYPES = mime_types.union(PREVIEWABLE_MIME_TYPES)
class UrlImagePreviewPlugin(GajimPlugin): class UrlImagePreviewPlugin(GajimPlugin):
def init(self): def init(self):
@@ -109,15 +115,17 @@ class UrlImagePreviewPlugin(GajimPlugin):
self._on_disconnect_chat_control_base), self._on_disconnect_chat_control_base),
'history_window': (self._on_connect_history_window, 'history_window': (self._on_connect_history_window,
self._on_disconnect_history_window), self._on_disconnect_history_window),
'print_real_text': (self._print_real_text, None), } 'print_real_text': (self._print_real_text, None),
}
self.config_default_values = { self.config_default_values = {
'PREVIEW_SIZE': (150, 'Preview size (100-1000)'), 'PREVIEW_SIZE': (150, 'Preview size (100-1000)'),
'MAX_FILE_SIZE': (5242880, 'Max file size for image preview'), 'MAX_FILE_SIZE': ('10485760', 'Max file size for image preview'),
'ALLOW_ALL_IMAGES': (False, ''), 'ALLOW_ALL_IMAGES': (False, ''),
'LEFTCLICK_ACTION': ('open_menuitem', 'Open'), 'LEFTCLICK_ACTION': ('open_menuitem', 'Open'),
'ANONYMOUS_MUC': (False, ''), 'ANONYMOUS_MUC': (False, ''),
'VERIFY': (True, ''),} 'VERIFY': (True, ''),
}
self._textviews = {} self._textviews = {}
self._sessions = {} self._sessions = {}
@@ -131,13 +139,37 @@ class UrlImagePreviewPlugin(GajimPlugin):
if GLib.mkdir_with_parents(str(self._thumb_dir), 0o700) != 0: if GLib.mkdir_with_parents(str(self._thumb_dir), 0o700) != 0:
log.error('Failed to create: %s', self._thumb_dir) log.error('Failed to create: %s', self._thumb_dir)
if app.config.get('use_kib_mib'):
self._units = GLib.FormatSizeFlags.IEC_UNITS
else:
self._units = GLib.FormatSizeFlags.DEFAULT
self._migrate_config() self._migrate_config()
self._load_css()
def _migrate_config(self): def _migrate_config(self):
action = self.config['LEFTCLICK_ACTION'] action = self.config['LEFTCLICK_ACTION']
if action.endswith('_menuitem'): if action.endswith('_menuitem'):
self.config['LEFTCLICK_ACTION'] = action[:-9] self.config['LEFTCLICK_ACTION'] = action[:-9]
@staticmethod
def _load_css():
path = Path(__file__).parent / 'preview.css'
try:
with path.open('r') as file:
css = file.read()
except Exception as exc:
log.error('Error loading css: %s', exc)
return
try:
provider = Gtk.CssProvider()
provider.load_from_data(bytes(css.encode('utf-8')))
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
provider, 610)
except Exception:
log.exception('Error loading application css')
def _on_connect_chat_control_base(self, chat_control): def _on_connect_chat_control_base(self, chat_control):
account = chat_control.account account = chat_control.account
if account not in self._sessions: if account not in self._sessions:
@@ -161,7 +193,8 @@ class UrlImagePreviewPlugin(GajimPlugin):
if textview == textview_: if textview == textview_:
return control_id return control_id
def _create_session(self, account): @staticmethod
def _create_session(account):
session = Soup.Session() session = Soup.Session()
session.add_feature_by_type(Soup.ContentSniffer) session.add_feature_by_type(Soup.ContentSniffer)
session.props.https_aliases = ['aesgcm'] session.props.https_aliases = ['aesgcm']
@@ -211,7 +244,7 @@ class UrlImagePreviewPlugin(GajimPlugin):
size=preview.size, size=preview.size,
scale=get_monitor_scale_factor(), scale=get_monitor_scale_factor(),
pixbuf=True) pixbuf=True)
self._update_textview(pixbuf, preview) self._update_textview(preview, pixbuf)
return return
preview = self._process_web_uri(uri, preview = self._process_web_uri(uri,
@@ -287,9 +320,9 @@ class UrlImagePreviewPlugin(GajimPlugin):
account): account):
try: try:
split_geo_uri(uri) split_geo_uri(uri)
except Exception as error: except Exception as err:
log.error(uri) log.error(uri)
log.error(error) log.error(err)
return return
return Preview(uri, return Preview(uri,
@@ -331,11 +364,16 @@ class UrlImagePreviewPlugin(GajimPlugin):
log.error('%s: %s', preview.orig_path.name, error) log.error('%s: %s', preview.orig_path.name, error)
return return
preview.mime_type = self._guess_mime_type(preview.orig_path)
preview.file_size = os.path.getsize(preview.orig_path)
if preview.is_previewable:
if preview.create_thumbnail(data): if preview.create_thumbnail(data):
write_file_async(preview.thumb_path, write_file_async(preview.thumb_path,
preview.thumbnail, preview.thumbnail,
self._on_thumb_write_finished, self._on_thumb_write_finished,
preview) preview)
else:
self._update_textview(preview, None)
def _on_thumb_load_finished(self, data, error, preview): def _on_thumb_load_finished(self, data, error, preview):
if data is None: if data is None:
@@ -343,6 +381,8 @@ class UrlImagePreviewPlugin(GajimPlugin):
return return
preview.thumbnail = data preview.thumbnail = data
preview.mime_type = self._guess_mime_type(preview.orig_path)
preview.file_size = os.path.getsize(preview.orig_path)
try: try:
pixbuf = pixbuf_from_data(preview.thumbnail) pixbuf = pixbuf_from_data(preview.thumbnail)
@@ -351,9 +391,9 @@ class UrlImagePreviewPlugin(GajimPlugin):
preview.thumb_path.name, preview.thumb_path.name,
err) err)
return return
self._update_textview(pixbuf, preview) self._update_textview(preview, pixbuf)
def _download_content(self, preview): def _download_content(self, preview, force=False):
if preview.account is None: if preview.account is None:
# History Window can be opened without account context # History Window can be opened without account context
# This means we can not apply proxy settings # This means we can not apply proxy settings
@@ -361,7 +401,8 @@ class UrlImagePreviewPlugin(GajimPlugin):
log.info('Start downloading: %s', preview.request_uri) log.info('Start downloading: %s', preview.request_uri)
message = Soup.Message.new('GET', preview.request_uri) message = Soup.Message.new('GET', preview.request_uri)
message.connect('starting', self._check_certificate, preview) message.connect('starting', self._check_certificate, preview)
message.connect('content-sniffed', self._on_content_sniffed, preview) message.connect(
'content-sniffed', self._on_content_sniffed, preview, force)
session = self._get_session(preview.account) session = self._get_session(preview.account)
session.queue_message(message, self._on_finished, preview) session.queue_message(message, self._on_finished, preview)
@@ -379,20 +420,25 @@ class UrlImagePreviewPlugin(GajimPlugin):
session.cancel_message(message, Soup.Status.CANCELLED) session.cancel_message(message, Soup.Status.CANCELLED)
return return
def _on_content_sniffed(self, message, type_, _params, preview): def _on_content_sniffed(self, message, type_, _params, preview, force):
size = message.props.response_headers.get_content_length() file_size = message.props.response_headers.get_content_length()
uri = message.props.uri.to_string(False) uri = message.props.uri.to_string(False)
session = self._get_session(preview.account) session = self._get_session(preview.account)
if type_ not in ACCEPTED_MIME_TYPES: preview.mime_type = type_
log.info('Not allowed content type: %s, %s', type_, uri) preview.file_size = file_size
if type_ not in ALLOWED_MIME_TYPES:
log.info('Not an allowed content type: %s, %s', type_, uri)
session.cancel_message(message, Soup.Status.CANCELLED) session.cancel_message(message, Soup.Status.CANCELLED)
return return
if size == 0 or size > int(self.config['MAX_FILE_SIZE']): if file_size == 0 or file_size > int(self.config['MAX_FILE_SIZE']):
log.info('File size (%s) too big or unknown (zero) for URL: \'%s\'', log.info('File size (%s) too big or unknown (zero) for URL: \'%s\'',
size, uri) file_size, uri)
if not force:
session.cancel_message(message, Soup.Status.CANCELLED) session.cancel_message(message, Soup.Status.CANCELLED)
return
self._update_textview(preview, None)
def _on_finished(self, _session, message, preview): def _on_finished(self, _session, message, preview):
if message.status_code != Soup.Status.OK: if message.status_code != Soup.Status.OK:
@@ -412,11 +458,14 @@ class UrlImagePreviewPlugin(GajimPlugin):
self._on_orig_write_finished, self._on_orig_write_finished,
preview) preview)
if preview.is_previewable:
if preview.create_thumbnail(data): if preview.create_thumbnail(data):
write_file_async(preview.thumb_path, write_file_async(preview.thumb_path,
preview.thumbnail, preview.thumbnail,
self._on_thumb_write_finished, self._on_thumb_write_finished,
preview) preview)
else:
self._update_textview(preview, None)
@staticmethod @staticmethod
def _on_orig_write_finished(_result, error, preview): def _on_orig_write_finished(_result, error, preview):
@@ -425,6 +474,7 @@ class UrlImagePreviewPlugin(GajimPlugin):
return return
log.info('File stored: %s', preview.orig_path.name) log.info('File stored: %s', preview.orig_path.name)
preview.file_size = os.path.getsize(preview.orig_path)
def _on_thumb_write_finished(self, _result, error, preview): def _on_thumb_write_finished(self, _result, error, preview):
if error is not None: if error is not None:
@@ -435,14 +485,29 @@ class UrlImagePreviewPlugin(GajimPlugin):
try: try:
pixbuf = pixbuf_from_data(preview.thumbnail) pixbuf = pixbuf_from_data(preview.thumbnail)
except Exception as err: except Exception as error:
log.error('Unable to load: %s, %s', log.error('Unable to load: %s, %s',
preview.thumb_path.name, preview.thumb_path.name,
err) error)
return return
self._update_textview(pixbuf, preview) self._update_textview(preview, pixbuf)
def _update_textview(self, pixbuf, preview): @staticmethod
def _guess_mime_type(data):
mime_type, _ = mimetypes.MimeTypes().guess_type(data)
if mime_type is None:
# Try to guess MIME type by file name
mime_type, _ = Gio.content_type_guess(str(data), None)
log.debug('Guessed MIME type: %s', str(mime_type))
return mime_type
@staticmethod
def _get_icon_for_mime_type(mime_type):
if mime_type is None:
return Gio.Icon.new_for_string('mail-attachment')
return Gio.content_type_get_icon(mime_type)
def _update_textview(self, preview, data):
textview = self._textviews.get(preview.control_id) textview = self._textviews.get(preview.control_id)
if textview is None: if textview is None:
# Control closed # Control closed
@@ -454,30 +519,108 @@ class UrlImagePreviewPlugin(GajimPlugin):
anchor = buffer_.create_child_anchor(iter_) anchor = buffer_.create_child_anchor(iter_)
anchor.plaintext = preview.uri anchor.plaintext = preview.uri
image = create_clickable_image(pixbuf, preview) preview_widget = self._create_preview_widget(preview, data)
textview.tv.add_child_at_anchor(image, anchor) textview.tv.add_child_at_anchor(preview_widget, anchor)
buffer_.delete(iter_, buffer_.delete(iter_,
buffer_.get_iter_at_mark(preview.end_mark)) buffer_.get_iter_at_mark(preview.end_mark))
image.connect('button-press-event',
self._on_button_press_event,
preview)
if textview.autoscroll: if textview.autoscroll:
textview.scroll_to_end() textview.scroll_to_end()
def _get_context_menu(self, preview): def _create_preview_widget(self, preview, data):
path = self.local_file_path('context_menu.ui') if isinstance(data, GdkPixbuf.PixbufAnimation):
image = Gtk.Image.new_from_animation(data)
elif isinstance(data, GdkPixbuf.Pixbuf):
image = Gtk.Image.new_from_pixbuf(data)
else:
icon = self._get_icon_for_mime_type(preview.mime_type)
image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.DIALOG)
def _on_realize(box):
box.get_window().set_cursor(get_cursor('pointer'))
def _on_enter_leave(button, event):
if event.type == Gdk.EventType.ENTER_NOTIFY:
button.get_window().set_cursor(get_cursor('default'))
else:
button.get_window().set_cursor(get_cursor('text'))
path = self.local_file_path('preview.ui')
ui = get_builder(path) ui = get_builder(path)
if preview.is_aes_encrypted:
ui.open_link_in_browser.hide() ui.download_button.set_no_show_all(True)
ui.download_button.connect('enter-notify-event', _on_enter_leave)
ui.download_button.connect('leave-notify-event', _on_enter_leave)
ui.download_button.connect('clicked', self._on_download, preview)
ui.save_as_button.set_no_show_all(True)
ui.save_as_button.connect('enter-notify-event', _on_enter_leave)
ui.save_as_button.connect('leave-notify-event', _on_enter_leave)
ui.save_as_button.connect('clicked', self._on_save_as, preview)
ui.open_folder_button.set_no_show_all(True)
ui.open_folder_button.connect('enter-notify-event', _on_enter_leave)
ui.open_folder_button.connect('leave-notify-event', _on_enter_leave)
ui.open_folder_button.connect('clicked', self._on_open_folder, preview)
ui.event_box.set_tooltip_text(preview.filename)
ui.event_box.add(image)
ui.event_box.connect('realize', _on_realize)
ui.event_box.connect('button-press-event',
self._on_button_press_event,
preview)
ui.preview_box.show_all()
if preview.is_geo_uri: if preview.is_geo_uri:
ui.open_link_in_browser.hide() ui.file_name.set_text(_('Click to view location'))
ui.save_as.hide() ui.save_as_button.hide()
ui.open_folder.hide() ui.open_folder_button.hide()
ui.download_button.hide()
location = split_geo_uri(preview.uri)
ui.file_size.set_text(_('Lat: %s Lon: %s') % (
location.lat, location.lon))
ui.event_box.set_tooltip_text(_('Location at Lat: %s Lon: %s') % (
location.lat, location.lon))
ui.event_box.set_halign(Gtk.Align.CENTER)
ui.preview_box.set_size_request(160, -1)
return ui.preview_box
if preview.is_previewable and preview.orig_exists():
ui.event_box.set_halign(Gtk.Align.CENTER)
else:
image.set_property('pixel-size', 64)
if preview.orig_exists():
ui.download_button.hide()
else:
ui.save_as_button.hide()
ui.open_folder_button.hide()
file_size_string = _('File size unknown')
if preview.file_size != 0:
file_size_string = GLib.format_size_full(
preview.file_size, self._units)
ui.file_size.set_text(file_size_string)
ui.preview_box.set_size_request(300, -1)
ui.file_name.set_text(preview.filename)
ui.file_name.set_tooltip_text(preview.filename)
return ui.preview_box
def _get_context_menu(self, preview):
def destroy(menu, _pspec):
visible = menu.get_property('visible')
if not visible:
GLib.idle_add(menu.destroy)
path = self.local_file_path('context_menu.ui')
ui = get_builder(path)
ui.download.connect(
'activate', self._on_download, preview)
ui.open.connect( ui.open.connect(
'activate', self._on_open, preview) 'activate', self._on_open, preview)
ui.save_as.connect( ui.save_as.connect(
@@ -488,24 +631,43 @@ class UrlImagePreviewPlugin(GajimPlugin):
'activate', self._on_open_link_in_browser, preview) 'activate', self._on_open_link_in_browser, preview)
ui.copy_link_location.connect( ui.copy_link_location.connect(
'activate', self._on_copy_link_location, preview) 'activate', self._on_copy_link_location, preview)
def destroy(menu, _pspec):
visible = menu.get_property('visible')
if not visible:
GLib.idle_add(menu.destroy)
ui.context_menu.connect('notify::visible', destroy) ui.context_menu.connect('notify::visible', destroy)
if preview.is_aes_encrypted:
ui.open_link_in_browser.hide()
if preview.is_geo_uri:
ui.download.hide()
ui.open_link_in_browser.hide()
ui.save_as.hide()
ui.open_folder.hide()
return ui.context_menu return ui.context_menu
@staticmethod if preview.orig_exists():
def _on_open(_menu, preview): ui.download.hide()
else:
ui.open.hide()
ui.save_as.hide()
ui.open_folder.hide()
return ui.context_menu
def _on_download(self, _menu, preview):
if not preview.orig_exists():
self._download_content(preview, force=True)
def _on_open(self, _menu, preview):
if preview.is_geo_uri: if preview.is_geo_uri:
open_uri(preview.uri) open_uri(preview.uri)
return return
if not preview.orig_exists():
self._download_content(preview, force=True)
return
open_file(preview.orig_path) open_file(preview.orig_path)
@staticmethod def _on_save_as(self, _menu, preview):
def _on_save_as(_menu, preview):
def on_ok(target_path): def on_ok(target_path):
dirname = Path(target_path).parent dirname = Path(target_path).parent
if not os.access(dirname, os.W_OK): if not os.access(dirname, os.W_OK):
@@ -517,13 +679,19 @@ class UrlImagePreviewPlugin(GajimPlugin):
return return
shutil.copyfile(str(preview.orig_path), target_path) shutil.copyfile(str(preview.orig_path), target_path)
if not preview.orig_exists():
self._download_content(preview, force=True)
return
FileSaveDialog(on_ok, FileSaveDialog(on_ok,
path=app.config.get('last_save_dir'), path=app.config.get('last_save_dir'),
file_name=preview.filename, file_name=preview.filename,
transient_for=app.app.get_active_window()) transient_for=app.app.get_active_window())
@staticmethod def _on_open_folder(self, _menu, preview):
def _on_open_folder(_menu, preview): if not preview.orig_exists():
self._download_content(preview, force=True)
return
open_file(preview.orig_path.parent) open_file(preview.orig_path.parent)
@staticmethod @staticmethod
@@ -560,15 +728,19 @@ class Preview:
self._uri = uri self._uri = uri
self._urlparts = urlparts self._urlparts = urlparts
self._filename = filename_from_uri(self._uri) self._filename = filename_from_uri(self._uri)
self.size = size self.size = size
self.control_id = control_id self.control_id = control_id
self.orig_path = orig_path self.orig_path = orig_path
self.thumb_path = thumb_path self.thumb_path = thumb_path
self.start_mark = start_mark self.start_mark = start_mark
self.end_mark = end_mark self.end_mark = end_mark
self.thumbnail = None
self.account = account self.account = account
self.thumbnail = None
self.mime_type = None
self.file_size = 0
self.key, self.iv = None, None self.key, self.iv = None, None
if self.is_aes_encrypted: if self.is_aes_encrypted:
self.key, self.iv = parse_fragment(urlparts.fragment) self.key, self.iv = parse_fragment(urlparts.fragment)
@@ -581,6 +753,10 @@ class Preview:
def is_web_uri(self): def is_web_uri(self):
return not self.is_geo_uri return not self.is_geo_uri
@property
def is_previewable(self):
return self.mime_type in PREVIEWABLE_MIME_TYPES
@property @property
def uri(self): def uri(self):
return self._uri return self._uri
@@ -612,6 +788,6 @@ class Preview:
def create_thumbnail(self, data): def create_thumbnail(self, data):
self.thumbnail = create_thumbnail(data, self.size) self.thumbnail = create_thumbnail(data, self.size)
if self.thumbnail is None: if self.thumbnail is None:
log.warning('creating thumbnail failed for: %s', self.orig_path) log.warning('Creating thumbnail failed for: %s', self.orig_path)
return False return False
return True return True

View File

@@ -26,7 +26,6 @@ from urllib.parse import unquote
from gi.repository import GdkPixbuf from gi.repository import GdkPixbuf
from gi.repository import GLib from gi.repository import GLib
from gi.repository import Gtk
from PIL import Image from PIL import Image
@@ -35,7 +34,6 @@ from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers.modes import GCM from cryptography.hazmat.primitives.ciphers.modes import GCM
from gajim.gtk.util import get_cursor
log = logging.getLogger('gajim.p.preview.utils') log = logging.getLogger('gajim.p.preview.utils')
@@ -237,35 +235,6 @@ def pixbuf_from_data(data):
return loader.get_pixbuf() return loader.get_pixbuf()
def create_clickable_image(pixbuf, preview):
if isinstance(pixbuf, GdkPixbuf.PixbufAnimation):
image = Gtk.Image.new_from_animation(pixbuf)
else:
image = Gtk.Image.new_from_pixbuf(pixbuf)
css = '''#Preview {
box-shadow: 0px 0px 3px 0px alpha(@theme_text_color, 0.2);
margin: 5px 10px 5px 10px; }'''
provider = Gtk.CssProvider()
provider.load_from_data(bytes(css.encode()))
context = image.get_style_context()
context.add_provider(provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
image.set_name('Preview')
def _on_realize(box):
box.get_window().set_cursor(get_cursor('pointer'))
event_box = Gtk.EventBox()
event_box.connect('realize', _on_realize)
event_box.set_tooltip_text(preview.uri)
event_box.add(image)
event_box.show_all()
return event_box
def parse_fragment(fragment): def parse_fragment(fragment):
if not fragment: if not fragment:
raise ValueError('Invalid fragment') raise ValueError('Invalid fragment')