Commit c38db84e authored by Philipp Hörist's avatar Philipp Hörist

Refactor FileChooserDialogs

Use GtkFileChooserDialog only when we need previews, default to
NativeFileChooser otherwise.

GtkFileChooserDialogs have a long list of issues, so lets only use it
if we gain something from it.

Flatpak should only run NativeFileChoosers because its sandboxed and
this is needed for security purposes. As a result of that, Flatpak Users
dont have image previews in the FileOpenDialogs

Refactor all FileChoosers for a more simple approach when we use them

Add a new SendFileDialog, so we dont have to put widgets into the FileChooser
which forces non-native Dialogs.
parent bb33e055
......@@ -155,6 +155,7 @@ _dependencies = {
'PYCURL': False,
'GSPELL': False,
'IDLE': False,
'RUN_AS_FLATPAK': False,
}
......@@ -167,6 +168,9 @@ def is_installed(dependency):
return _dependencies['AVAHI'] or _dependencies['PYBONJOUR']
return _dependencies[dependency]
def is_flatpak():
return _dependencies['RUN_AS_FLATPAK']
def disable_dependency(dependency):
_dependencies[dependency] = False
......
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.21.0 -->
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.12"/>
<object class="GtkMenu" id="context_menu">
......@@ -22,70 +22,6 @@
</object>
</child>
</object>
<object class="GtkFileChooserDialog" id="filechooserdialog">
<property name="can_focus">False</property>
<property name="type_hint">dialog</property>
<property name="action">save</property>
<child internal-child="vbox">
<object class="GtkBox" id="dialog-vbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">24</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="dialog-action_area1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">False</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="save_button">
<property name="label">gtk-save</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="has_default">True</property>
<property name="receives_default">False</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
</object>
</child>
<action-widgets>
<action-widget response="-6">cancel_button</action-widget>
<action-widget response="-5">save_button</action-widget>
</action-widgets>
<child>
<placeholder/>
</child>
</object>
<object class="GtkImage" id="image1">
<property name="visible">True</property>
<property name="can_focus">False</property>
......@@ -98,6 +34,9 @@
<property name="default_width">650</property>
<property name="default_height">500</property>
<signal name="delete-event" handler="on_history_manager_window_delete_event" swapped="no"/>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox" id="vbox">
<property name="visible">True</property>
......@@ -306,8 +245,5 @@ If you plan to do massive deletions, please make sure Gajim is not running. Gene
</child>
</object>
</child>
<child>
<placeholder/>
</child>
</object>
</interface>
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkGrid" id="send_file_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">6</property>
<property name="column_spacing">6</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="hscrollbar_policy">never</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkListBox" id="listbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="vexpand">True</property>
<property name="selection_mode">none</property>
<property name="activate_on_single_click">False</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Description:</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="height_request">40</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="hscrollbar_policy">never</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="description">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wrap_mode">word-char</property>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Select Files</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="halign">start</property>
<signal name="clicked" handler="_select_files" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Send</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="halign">end</property>
<signal name="clicked" handler="_send" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Files:</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="width">2</property>
</packing>
</child>
</object>
</interface>
......@@ -99,3 +99,7 @@ popover#EmoticonPopover flowboxchild { padding-top: 5px; padding-bottom: 5px; }
/*MessageWindow Notebook*/
.notebook-tab-label {min-width: 80px}
/*SendFileDialog*/
#SendFileDialog grid {padding: 12px}
#SendFileDialog grid list { background-color: @theme_bg_color}
......@@ -1426,74 +1426,6 @@ class HigDialog(Gtk.MessageDialog):
vb.grab_focus()
self.show_all()
class FileChooserDialog(Gtk.FileChooserDialog):
"""
Non-blocking FileChooser Dialog around Gtk.FileChooserDialog
"""
def __init__(self, title_text, action, buttons, default_response,
select_multiple=False, current_folder=None, on_response_ok=None,
on_response_cancel=None, preview=False, transient_for=None):
Gtk.FileChooserDialog.__init__(self, title=title_text,
parent=transient_for, action=action)
self.add_button(buttons[0],buttons[1])
if len(buttons) ==4:
self.add_button(buttons[2],buttons[3])
self.set_default_response(default_response)
self.set_select_multiple(select_multiple)
if current_folder and os.path.isdir(current_folder):
self.set_current_folder(current_folder)
else:
self.set_current_folder(os.path.expanduser('~'))
self.response_ok, self.response_cancel = \
on_response_ok, on_response_cancel
# in gtk+-2.10 clicked signal on some of the buttons in a dialog
# is emitted twice, so we cannot rely on 'clicked' signal
self.connect('response', self.on_dialog_response)
if preview:
self.set_use_preview_label(False)
self.set_preview_widget(Gtk.Image())
self.connect('selection-changed', self.update_preview)
self.show_all()
def on_dialog_response(self, dialog, response):
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.CLOSE):
if self.response_cancel:
if isinstance(self.response_cancel, tuple):
self.response_cancel[0](dialog, *self.response_cancel[1:])
else:
self.response_cancel(dialog)
else:
self.just_destroy(dialog)
elif response == Gtk.ResponseType.OK:
if self.response_ok:
if isinstance(self.response_ok, tuple):
self.response_ok[0](dialog, *self.response_ok[1:])
else:
self.response_ok(dialog)
else:
self.just_destroy(dialog)
def update_preview(self, widget):
path_to_file = widget.get_preview_filename()
preview = widget.get_preview_widget()
if path_to_file is None or os.path.isdir(path_to_file):
# nothing to preview or directory
# make sure you clean image do show nothing
preview.clear()
return
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(path_to_file, 200, 200)
except GObject.GError:
preview.clear()
return
widget.get_preview_widget().set_from_pixbuf(pixbuf)
def just_destroy(self, widget):
self.destroy()
class AspellDictError:
def __init__(self, lang):
ErrorDialog(
......@@ -4773,139 +4705,6 @@ class ProgressDialog:
def on_progress_dialog_delete_event(self, widget, event):
return True # WM's X button or Escape key should not destroy the window
class ImageChooserDialog(FileChooserDialog):
def __init__(self, path_to_file='', on_response_ok=None,
on_response_cancel=None):
"""
Optionally accepts path_to_snd_file so it has that as selected
"""
def on_ok(widget, callback):
'''check if file exists and call callback'''
path_to_file = self.get_filename()
if not path_to_file:
return
if os.path.exists(path_to_file):
if isinstance(callback, tuple):
callback[0](widget, path_to_file, *callback[1:])
else:
callback(widget, path_to_file)
path = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES)
FileChooserDialog.__init__(self,
title_text = _('Choose Image'),
action = Gtk.FileChooserAction.OPEN,
buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN, Gtk.ResponseType.OK),
default_response = Gtk.ResponseType.OK,
current_folder = path,
on_response_ok = (on_ok, on_response_ok),
on_response_cancel = on_response_cancel)
if on_response_cancel:
self.connect('destroy', on_response_cancel)
filter_ = Gtk.FileFilter()
filter_.set_name(_('All files'))
filter_.add_pattern('*')
self.add_filter(filter_)
filter_ = Gtk.FileFilter()
filter_.set_name(_('Images'))
filter_.add_mime_type('image/png')
filter_.add_mime_type('image/jpeg')
filter_.add_mime_type('image/gif')
filter_.add_mime_type('image/tiff')
filter_.add_mime_type('image/svg+xml')
filter_.add_mime_type('image/x-xpixmap') # xpm
self.add_filter(filter_)
self.set_filter(filter_)
if path_to_file:
self.set_filename(path_to_file)
self.set_use_preview_label(False)
self.set_preview_widget(Gtk.Image())
self.connect('selection-changed', self.update_preview)
def update_preview(self, widget):
path_to_file = widget.get_preview_filename()
if path_to_file is None or os.path.isdir(path_to_file):
# nothing to preview or directory
# make sure you clean image do show nothing
preview = widget.get_preview_widget()
preview.clear()
return
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(path_to_file, 100, 100)
except GObject.GError:
return
widget.get_preview_widget().set_from_pixbuf(pixbuf)
class AvatarChooserDialog(ImageChooserDialog):
def __init__(self, path_to_file='', on_response_ok=None,
on_response_cancel=None, on_response_clear=None):
ImageChooserDialog.__init__(self, path_to_file, on_response_ok,
on_response_cancel)
button = Gtk.Button(None, Gtk.STOCK_CLEAR)
self.response_clear = on_response_clear
if on_response_clear:
button.connect('clicked', self.on_clear)
button.show_all()
action_area = self.get_action_area()
action_area.pack_start(button, True, True, 0)
action_area.reorder_child(button, 0)
def on_clear(self, widget):
if isinstance(self.response_clear, tuple):
self.response_clear[0](widget, *self.response_clear[1:])
else:
self.response_clear(widget)
class ArchiveChooserDialog(FileChooserDialog):
def __init__(self, on_response_ok=None, on_response_cancel=None,
transient_for=None):
def on_ok(widget, callback):
'''check if file exists and call callback'''
path_to_file = self.get_filename()
if not path_to_file:
return
if os.path.exists(path_to_file):
if isinstance(callback, tuple):
callback[0](path_to_file, *callback[1:])
else:
callback(path_to_file)
self.destroy()
path = os.path.expanduser('~')
FileChooserDialog.__init__(self,
title_text=_('Choose Archive'),
action=Gtk.FileChooserAction.OPEN,
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN, Gtk.ResponseType.OK),
default_response=Gtk.ResponseType.OK,
current_folder=path,
on_response_ok=(on_ok, on_response_ok),
on_response_cancel=on_response_cancel,
transient_for=transient_for)
if on_response_cancel:
self.connect('destroy', on_response_cancel)
filter_ = Gtk.FileFilter()
filter_.set_name(_('All files'))
filter_.add_pattern('*')
self.add_filter(filter_)
filter_ = Gtk.FileFilter()
filter_.set_name(_('Zip files'))
filter_.add_pattern('*.zip')
self.add_filter(filter_)
self.set_filter(filter_)
class TransformChatToMUC:
# Keep a reference on windows so garbage collector don't restroy them
instances = []
......
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# 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, either version 3 of the License, or
# (at your option) any later version.
#
# 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 os
from pathlib import Path
from collections import namedtuple
from gi.repository import Gtk
from gi.repository import GdkPixbuf
from gi.repository import GObject
from gajim.common import app
Filter = namedtuple('Filter', 'name pattern default')
# Notes: Adding mime types to Gtk.FileFilter forces non-native dialogs
class BaseFileChooser:
def _on_response(self, dialog, response, accept_cb, cancel_cb):
if response == Gtk.ResponseType.ACCEPT:
if self.get_select_multiple():
accept_cb(dialog.get_filenames())
else:
accept_cb(dialog.get_filename())
if response in (Gtk.ResponseType.CANCEL,
Gtk.ResponseType.DELETE_EVENT):
if cancel_cb is not None:
cancel_cb()
def _add_filters(self, filters):
for filterinfo in filters:
filter_ = Gtk.FileFilter()
filter_.set_name(filterinfo.name)
if isinstance(filterinfo.pattern, list):
for mime_type in filterinfo.pattern:
filter_.add_mime_type(mime_type)
else:
filter_.add_pattern(filterinfo.pattern)
self.add_filter(filter_)
if filterinfo.default:
self.set_filter(filter_)
def _update_preview(self, filechooser):
path_to_file = filechooser.get_preview_filename()
preview = filechooser.get_preview_widget()
if path_to_file is None or os.path.isdir(path_to_file):
# nothing to preview
preview.clear()
return
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
path_to_file, *self._preivew_size)
except GObject.GError:
preview.clear()
return
filechooser.get_preview_widget().set_from_pixbuf(pixbuf)
class BaseFileOpenDialog:
_title = _('Choose File to Send…')
_filters = [Filter(_('All files'), '*', True)]
class BaseAvatarChooserDialog:
_title = _('Choose Avatar…')
_preivew_size = (100, 100)
if app.is_flatpak():
_filters = [Filter(_('PNG files'), '*.png', True),
Filter(_('JPEG files'), '*.jp*g', False),
Filter(_('SVG files'), '*.svg', False)]
else:
_filters = [Filter(_('Images'), ['image/png',
'image/jpeg',
'image/svg+xml'], True)]
class NativeFileChooserDialog(Gtk.FileChooserNative, BaseFileChooser):
_title = ''
_filters = []
_action = Gtk.FileChooserAction.OPEN
def __init__(self, accept_cb, cancel_cb=None, transient_for=None,
path=None, file_name=None, select_multiple=False,
modal=False):
Gtk.FileChooserNative.__init__(self,
title=self._title,
action=self._action,
transient_for=transient_for)
self.set_current_folder(path or str(Path.home()))
if file_name is not None:
self.set_current_name(file_name)
self.set_select_multiple(select_multiple)
self.set_do_overwrite_confirmation(True)
self.set_modal(modal)
self._add_filters(self._filters)
self.connect('response', self._on_response, accept_cb, cancel_cb)
self.show()
class ArchiveChooserDialog(NativeFileChooserDialog):
_title = _('Choose Archive')
_filters = [Filter(_('All files'), '*', False),
Filter(_('ZIP files'), '*.zip', True)]
class FileSaveDialog(NativeFileChooserDialog):
_title = _('Save File as…')
_filters = [Filter(_('All files'), '*', True)]
_action = Gtk.FileChooserAction.SAVE
class AvatarSaveDialog(FileSaveDialog):
if os.name == 'nt':
_filters = [Filter(_('Images'), '*.png;*.jpg;*.jpeg;*.svg', True)]
class NativeFileOpenDialog(BaseFileOpenDialog, NativeFileChooserDialog):
pass
class NativeAvatarChooserDialog(BaseAvatarChooserDialog, NativeFileChooserDialog):
pass
class GtkFileChooserDialog(Gtk.FileChooserDialog, BaseFileChooser):
_title = ''
_filters = []
_action = Gtk.FileChooserAction.OPEN
_preivew_size = (200, 200)
def __init__(self, accept_cb, cancel_cb=None, transient_for=None,
path=None, file_name=None, select_multiple=False,
preview=True, modal=False):
Gtk.FileChooserDialog.__init__(
self,
title=self._title,
action=self._action,
buttons=[Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT],
transient_for=transient_for)
self.set_current_folder(path or str(Path.home()))
if file_name is not None:
self.set_current_name(file_name)
self.set_select_multiple(select_multiple)
self.set_do_overwrite_confirmation(True)
self.set_modal(modal)
self._add_filters(self._filters)
if preview:
self.set_use_preview_label(False)
self.set_preview_widget(Gtk.Image())
self.connect('selection-changed', self._update_preview)
self.connect('response', self._on_response, accept_cb, cancel_cb)
self.show()
def _on_response(self, *args):
super()._on_response(*args)
self.destroy()
class GtkFileOpenDialog(BaseFileOpenDialog, GtkFileChooserDialog):
pass
class GtkAvatarChooserDialog(BaseAvatarChooserDialog, GtkFileChooserDialog):
pass
def FileChooserDialog(*args, **kwargs):
if app.is_flatpak():
return NativeFileOpenDialog(*args, **kwargs)
else:
return GtkFileOpenDialog(*args, **kwargs)
def AvatarChooserDialog(*args, **kwargs):
if app.is_flatpak():
return NativeAvatarChooserDialog(*args, **kwargs)
else:
return GtkAvatarChooserDialog(*args, **kwargs)
......@@ -28,6 +28,8 @@ from gi.repository import GLib
from gi.repository import Pango
import os
import time
from functools import partial
from pathlib import Path
from enum import IntEnum, unique
from datetime import datetime
......@@ -41,6 +43,7 @@ from gajim.common import helpers
from gajim.common.file_props import FilesProp
from gajim.common.protocol.bytestream import (is_transfer_active, is_transfer_paused,
is_transfer_stopped)
from gajim.filechoosers import FileSaveDialog, FileChooserDialog
from nbxmpp.protocol import NS_JINGLE_FILE_TRANSFER_5
import logging
log = logging.getLogger('gajim.filetransfer_window')
......@@ -322,51 +325,8 @@ class FileTransfersWindow:
account), type_=Gtk.MessageType.ERROR)
def show_file_send_request(self, account, contact):
win = Gtk.ScrolledWindow()
win.set_shadow_type(Gtk.ShadowType.IN)
win.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)
from gajim.message_textview import MessageTextView
desc_entry = MessageTextView()
win.add(desc_entry)
def on_ok(widget):
file_dir = None
files_path_list = dialog.get_filenames()
desc = desc_entry.get_text()
for file_path in files_path_list:
if self.send_file(account, contact, file_path, desc) \
and file_dir is None:
file_dir = os.path.dirname(file_path)
if file_dir:
app.config.set('last_send_dir', file_dir)
dialog.destroy()
dialog = dialogs.FileChooserDialog(_('Choose File to Send…'),
Gtk.FileChooserAction.OPEN, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL),
Gtk.ResponseType.OK,
True, # select multiple true as we can select many files to send
app.config.get('last_send_dir'),
on_response_ok=on_ok,
on_response_cancel=lambda e:dialog.destroy(),
preview=True,
transient_for=app.interface.roster.window
)
btn = Gtk.Button.new_with_mnemonic(_('_Send'))
btn.set_property('can-default', True)