diff --git a/gajim/common/app.py b/gajim/common/app.py index 6a39c1a8e..2506a921e 100644 --- a/gajim/common/app.py +++ b/gajim/common/app.py @@ -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 diff --git a/gajim/data/gui/history_manager.ui b/gajim/data/gui/history_manager.ui index a303efb58..5142d4204 100644 --- a/gajim/data/gui/history_manager.ui +++ b/gajim/data/gui/history_manager.ui @@ -1,5 +1,5 @@ - + @@ -22,70 +22,6 @@ - - False - dialog - save - - - True - False - vertical - 24 - - - True - False - end - - - gtk-cancel - True - True - True - False - True - - - False - False - 0 - - - - - gtk-save - True - True - True - True - False - True - - - False - False - 1 - - - - - False - False - end - 0 - - - - - - cancel_button - save_button - - - - - True False @@ -98,6 +34,9 @@ 650 500 + + + True @@ -306,8 +245,5 @@ If you plan to do massive deletions, please make sure Gajim is not running. Gene - - - diff --git a/gajim/data/gui/profile_window.ui b/gajim/data/gui/profile_window.ui index 0a6fe161e..7893928d9 100644 --- a/gajim/data/gui/profile_window.ui +++ b/gajim/data/gui/profile_window.ui @@ -1,5 +1,5 @@ - + @@ -8,6 +8,9 @@ dialog + + + True @@ -30,10 +33,10 @@ True False - 0 - 0 <b>Name:</b> True + 0 + 0 0 @@ -54,10 +57,10 @@ True False - 1 - 0 <b>Nickname:</b> True + 1 + 0 2 @@ -89,10 +92,10 @@ True False - 1 - 0 <b>Family:</b> True + 1 + 0 0 @@ -113,10 +116,10 @@ True False - 1 - 0 <b>Given:</b> True + 1 + 0 2 @@ -137,10 +140,10 @@ True False - 1 - 0 <b>Middle:</b> True + 1 + 0 0 @@ -151,10 +154,10 @@ True False - 1 - 0 <b>Prefix:</b> True + 1 + 0 0 @@ -185,10 +188,10 @@ True False - 1 - 0 <b>Suffix:</b> True + 1 + 0 2 @@ -217,10 +220,10 @@ True False - 0 - 0 <b>Full Name</b> True + 0 + 0 @@ -245,10 +248,10 @@ True False - 1 - 0 <b>Street:</b> True + 1 + 0 0 @@ -269,10 +272,10 @@ True False - 1 - 0 <b>Extra Address:</b> True + 1 + 0 2 @@ -293,10 +296,10 @@ True False - 1 - 0 <b>City:</b> True + 1 + 0 0 @@ -317,10 +320,10 @@ True False - 1 - 0 <b>Postal Code:</b> True + 1 + 0 2 @@ -341,10 +344,10 @@ True False - 1 - 0 <b>State:</b> True + 1 + 0 0 @@ -365,10 +368,10 @@ True False - 1 - 0 <b>Country:</b> True + 1 + 0 2 @@ -391,10 +394,10 @@ True False - 0 - 0 <b>Address</b> True + 0 + 0 @@ -408,10 +411,10 @@ True False - 1 - 0 <b>Homepage:</b> True + 1 + 0 0 @@ -433,9 +436,9 @@ True False - 1 <b>E-Mail:</b> True + 1 0 @@ -457,10 +460,10 @@ True False - 1 - 0 <b>Phone No.:</b> True + 1 + 0 0 @@ -492,10 +495,10 @@ True False - 1 - 0 <b>Avatar:</b> True + 1 + 0 0 @@ -506,12 +509,11 @@ True False + 6 True False - 0 - 0 @@ -522,7 +524,6 @@ True False - 0 gtk-missing-image @@ -543,8 +544,6 @@ False True True - 0 - 0 @@ -553,10 +552,31 @@ 1 + + + Remove Avatar + True + True + True + Clear Avatar + start + end + + + + + False + True + 2 + + 1 6 + 3 @@ -569,10 +589,10 @@ True False - 1 - 0 <b>Birthday:</b> True + 1 + 0 @@ -581,21 +601,15 @@ 5 - - - - - - True False + Personal Info 0 0 - Personal Info False @@ -612,10 +626,10 @@ True False - 1 - 0 <b>Company:</b> True + 1 + 0 0 @@ -636,10 +650,10 @@ True False - 1 - 0 <b>Department:</b> True + 1 + 0 2 @@ -660,10 +674,10 @@ True False - 1 - 0 <b>Position:</b> True + 1 + 0 0 @@ -684,10 +698,10 @@ True False - 1 - 0 <b>Role:</b> True + 1 + 0 2 @@ -719,10 +733,10 @@ True False - 1 - 0 <b>Street:</b> True + 1 + 0 0 @@ -743,10 +757,10 @@ True False - 1 - 0 <b>Extra Address:</b> True + 1 + 0 2 @@ -767,10 +781,10 @@ True False - 1 - 0 <b>City:</b> True + 1 + 0 0 @@ -791,10 +805,10 @@ True False - 1 - 0 <b>Postal Code:</b> True + 1 + 0 2 @@ -815,10 +829,10 @@ True False - 1 - 0 <b>State:</b> True + 1 + 0 0 @@ -839,10 +853,10 @@ True False - 1 - 0 <b>Country:</b> True + 1 + 0 2 @@ -865,10 +879,10 @@ True False - 1 - 0 <b>Address</b> True + 1 + 0 @@ -882,10 +896,10 @@ True False - 1 - 0 <b>E-Mail:</b> True + 1 + 0 0 @@ -907,10 +921,10 @@ True False - 1 - 0 <b>Phone No.:</b> True + 1 + 0 0 @@ -942,9 +956,9 @@ True False + Work 0 0 - Work 1 @@ -974,9 +988,9 @@ True False + About 0 0 - About 2 diff --git a/gajim/data/gui/send_file_dialog.ui b/gajim/data/gui/send_file_dialog.ui new file mode 100644 index 000000000..48c7dc24a --- /dev/null +++ b/gajim/data/gui/send_file_dialog.ui @@ -0,0 +1,116 @@ + + + + + + True + False + 6 + 6 + + + True + True + True + never + in + + + True + False + + + True + False + True + none + False + + + + + + + 0 + 1 + 2 + + + + + True + False + Description: + 0 + + + 0 + 2 + 2 + + + + + 40 + True + True + True + never + in + + + True + True + word-char + + + + + 0 + 3 + 2 + + + + + Select Files + True + True + True + start + + + + 0 + 4 + + + + + Send + True + True + True + end + + + + 1 + 4 + + + + + True + False + Files: + 0 + + + 0 + 0 + 2 + + + + diff --git a/gajim/data/style/gajim.css b/gajim/data/style/gajim.css index 57e28c170..50f349476 100644 --- a/gajim/data/style/gajim.css +++ b/gajim/data/style/gajim.css @@ -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} diff --git a/gajim/dialogs.py b/gajim/dialogs.py index 00dad6d8f..577563cef 100644 --- a/gajim/dialogs.py +++ b/gajim/dialogs.py @@ -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 = [] diff --git a/gajim/filechoosers.py b/gajim/filechoosers.py new file mode 100644 index 000000000..ea79bb411 --- /dev/null +++ b/gajim/filechoosers.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2018 Philipp Hörist +# +# 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 . + +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) diff --git a/gajim/filetransfers_window.py b/gajim/filetransfers_window.py index ef1a0b75a..11744c758 100644 --- a/gajim/filetransfers_window.py +++ b/gajim/filetransfers_window.py @@ -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) - # FIXME: add send icon to this button (JUMP_TO) - dialog.add_action_widget(btn, Gtk.ResponseType.OK) - dialog.set_default_response(Gtk.ResponseType.OK) - - desc_hbox = Gtk.HBox(homogeneous=False, spacing=5) - desc_hbox.pack_start(Gtk.Label.new(_('Description: ')), False, False, 0) - desc_hbox.pack_start(win, True, True, 0) - - dialog.vbox.pack_start(desc_hbox, False, False, 0) - - btn.show() - desc_hbox.show_all() + send_callback = partial(self.send_file, account, contact) + SendFileDialog(send_callback, self.window) def send_file(self, account, contact, file_path, file_desc=''): """ @@ -411,9 +371,9 @@ class FileTransfersWindow: app.connections[account].send_file_approval(file_props) def on_file_request_accepted(self, account, contact, file_props): - def on_ok(widget, account, contact, file_props): - file_path = dialog2.get_filename() + def on_ok(account, contact, file_props, file_path): if os.path.exists(file_path): + app.config.set('last_save_dir', os.path.dirname(file_path)) # check if we have write permissions if not os.access(file_path, os.W_OK): file_name = GLib.markup_escape_text(os.path.basename( @@ -433,13 +393,11 @@ class FileTransfersWindow: return elif response == 100: file_props.offset = dl_size - dialog2.destroy() self._start_receive(file_path, account, contact, file_props) dialog = dialogs.FTOverwriteConfirmationDialog( _('This file already exists'), _('What do you want to do?'), - propose_resume=not dl_finished, on_response=on_response, - transient_for=dialog2) + propose_resume=not dl_finished, on_response=on_response) dialog.set_destroy_with_parent(True) return else: @@ -452,26 +410,15 @@ class FileTransfersWindow: dirname, _('You do not have permission to create files ' 'in this directory.')) return - dialog2.destroy() self._start_receive(file_path, account, contact, file_props) - def on_cancel(widget, account, contact, file_props): - dialog2.destroy() - app.connections[account].send_file_rejection(file_props) - - dialog2 = dialogs.FileChooserDialog( - title_text=_('Save File 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, account, contact, file_props), - on_response_cancel=(on_cancel, account, contact, file_props)) - - dialog2.set_current_name(file_props.name) - dialog2.connect('delete-event', lambda widget, event: - on_cancel(widget, account, contact, file_props)) + con = app.connections[account] + accept_cb = partial(on_ok, account, contact, file_props) + cancel_cb = partial(con.send_file_rejection, file_props) + FileSaveDialog(accept_cb, + cancel_cb, + path=app.config.get('last_save_dir'), + file_name=file_props.name) def show_file_request(self, account, contact, file_props): """ @@ -1048,3 +995,78 @@ class FileTransfersWindow: def on_file_transfers_window_key_press_event(self, widget, event): if event.keyval == Gdk.KEY_Escape: # ESCAPE self.window.hide() + + +class SendFileDialog(Gtk.ApplicationWindow): + def __init__(self, send_callback, transient_for): + active_window = app.app.get_active_window() + Gtk.ApplicationWindow.__init__(self) + self.set_name('SendFileDialog') + self.set_application(app.app) + self.set_show_menubar(False) + self.set_resizable(True) + self.set_default_size(400, 250) + self.set_type_hint(Gdk.WindowTypeHint.DIALOG) + self.set_transient_for(active_window) + self.set_title(_('Choose a File to Send…')) + self.set_destroy_with_parent(True) + + self._send_callback = send_callback + + xml = gtkgui_helpers.get_gtk_builder('send_file_dialog.ui') + grid = xml.get_object('send_file_grid') + + self._filebox = xml.get_object('listbox') + self._description = xml.get_object('description') + + self.add(grid) + self.connect('key-press-event', self._key_press_event) + + xml.connect_signals(self) + self.show_all() + + def _send(self, button): + for file in self._filebox.get_children(): + self._send_callback(str(file.path), self._get_description()) + self.destroy() + + def _select_files(self, button): + FileChooserDialog(self._set_files, + select_multiple=True, + transient_for=self, + path=app.config.get('last_send_dir')) + + def _set_files(self, filenames): + # Clear the ListBox + self._filebox.foreach(self._remove_widget, None) + + for file in filenames: + row = FileRow(file) + if row.path.is_dir(): + continue + last_dir = row.path.parent + self._filebox.add(row) + self._filebox.show_all() + app.config.set('last_send_dir', str(last_dir)) + + def _remove_widget(self, widget, data): + self._filebox.remove(widget) + + def _get_description(self): + buffer_ = self._description.get_buffer() + start, end = buffer_.get_bounds() + return buffer_.get_text(start, end, False) + + def _key_press_event(self, widget, event): + if event.keyval == Gdk.KEY_Escape: + self.destroy() + + +class FileRow(Gtk.ListBoxRow): + def __init__(self, path): + Gtk.ListBoxRow.__init__(self) + self.path = Path(path) + label = Gtk.Label(self.path.name) + label.set_ellipsize(Pango.EllipsizeMode.END) + label.set_xalign(0) + self.add(label) diff --git a/gajim/gtkgui_helpers.py b/gajim/gtkgui_helpers.py index ea056d007..3a6fd0c2b 100644 --- a/gajim/gtkgui_helpers.py +++ b/gajim/gtkgui_helpers.py @@ -51,6 +51,7 @@ from gajim.common import i18n from gajim.common import app from gajim.common import pep from gajim.common import configpaths +from gajim.filechoosers import AvatarSaveDialog gtk_icon_theme = Gtk.IconTheme.get_default() gtk_icon_theme.append_search_path(configpaths.get('ICONS')) @@ -450,6 +451,7 @@ def on_avatar_save_as_menuitem_activate(widget, avatar, default_name=''): if response < 0: return + app.config.set('last_save_dir', os.path.dirname(file_path)) if isinstance(avatar, str): # We got a SHA pixbuf = app.interface.get_avatar(avatar) @@ -459,10 +461,8 @@ def on_avatar_save_as_menuitem_activate(widget, avatar, default_name=''): extension = os.path.splitext(file_path)[1] if not extension: # Silently save as Jpeg image - image_format = 'jpeg' - file_path += '.jpeg' - elif extension == 'jpg': - image_format = 'jpeg' + image_format = 'png' + file_path += '.png' else: image_format = extension[1:] # remove leading dot @@ -473,19 +473,16 @@ def on_avatar_save_as_menuitem_activate(widget, avatar, default_name=''): log.error('Error saving avatar: %s' % str(e)) if os.path.exists(file_path): os.remove(file_path) - new_file_path = '.'.join(file_path.split('.')[:-1]) + '.jpeg' + new_file_path = '.'.join(file_path.split('.')[:-1]) + '.png' def on_ok(file_path, pixbuf): - pixbuf.savev(file_path, 'jpeg', [], []) + pixbuf.savev(file_path, 'png', [], []) dialogs.ConfirmationDialog(_('Extension not supported'), _('Image cannot be saved in %(type)s format. Save as ' '%(new_filename)s?') % {'type': image_format, 'new_filename': new_file_path}, on_response_ok = (on_ok, new_file_path, pixbuf)) - else: - dialog.destroy() - def on_ok(widget): - file_path = dialog.get_filename() + def on_ok(file_path): if os.path.exists(file_path): # check if we have write permissions if not os.access(file_path, os.W_OK): @@ -496,8 +493,7 @@ def on_avatar_save_as_menuitem_activate(widget, avatar, default_name=''): return dialog2 = dialogs.FTOverwriteConfirmationDialog( _('This file already exists'), _('What do you want to do?'), - propose_resume=False, on_response=(on_continue, file_path), - transient_for=dialog) + propose_resume=False, on_response=(on_continue, file_path)) dialog2.set_destroy_with_parent(True) else: dirname = os.path.dirname(file_path) @@ -509,19 +505,11 @@ def on_avatar_save_as_menuitem_activate(widget, avatar, default_name=''): on_continue(0, file_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(default_name + '.jpeg') - dialog.connect('delete-event', lambda widget, event: - on_cancel(widget)) + transient = app.app.get_active_window() + AvatarSaveDialog(on_ok, + path=app.config.get('last_save_dir'), + file_name='%s.png' % default_name, + transient_for=transient) def create_combobox(value_list, selected_value = None): """ diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py index 6afc52988..539e22503 100644 --- a/gajim/gui_interface.py +++ b/gajim/gui_interface.py @@ -39,6 +39,7 @@ import sys import re import time import hashlib +from functools import partial from gi.repository import Gtk from gi.repository import GdkPixbuf @@ -66,7 +67,7 @@ from gajim import notify from gajim import message_control from gajim.dialog_messages import get_dialog from gajim.dialogs import ProgressWindow -from gajim.dialogs import FileChooserDialog +from gajim.filechoosers import FileChooserDialog from gajim.chat_control_base import ChatControlBase from gajim.chat_control import ChatControl @@ -1147,22 +1148,13 @@ class Interface: ProgressWindow(file) def send_httpupload(self, chat_control): - FileChooserDialog( - on_response_ok=lambda widget: self.on_file_dialog_ok(widget, - chat_control), - title_text=_('Choose file to send'), - action=Gtk.FileChooserAction.OPEN, - buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OPEN, Gtk.ResponseType.OK), - select_multiple=True, - default_response=Gtk.ResponseType.OK, - preview=True, - transient_for=chat_control.parent_win.window) + accept_cb = partial(self.on_file_dialog_ok, chat_control) + FileChooserDialog(accept_cb, + select_multiple=True, + transient_for=chat_control.parent_win.window) @staticmethod - def on_file_dialog_ok(widget, chat_control): - paths = widget.get_filenames() - widget.destroy() + def on_file_dialog_ok(chat_control, paths): con = app.connections[chat_control.account] groupchat = chat_control.type_id == message_control.TYPE_GC for path in paths: diff --git a/gajim/history_manager.py b/gajim/history_manager.py index bb15aafb2..ae8b72ef9 100644 --- a/gajim/history_manager.py +++ b/gajim/history_manager.py @@ -83,10 +83,11 @@ if is_standalone(): configpaths.init() from gajim.common import app -from gajim import gtkgui_helpers from gajim.common.const import JIDConstant, KindConstant from gajim.common import helpers from gajim import dialogs +from gajim.filechoosers import FileSaveDialog +from gajim import gtkgui_helpers @unique @@ -465,24 +466,14 @@ class HistoryManager: return True def on_export_menuitem_activate(self, widget): - xml = gtkgui_helpers.get_gtk_builder('history_manager.ui', - 'filechooserdialog') - xml.connect_signals(self) + FileSaveDialog(self._on_export, + transient_for=self.window, + modal=True) - dlg = xml.get_object('filechooserdialog') - dlg.set_title(_('Exporting History Logs…')) - dlg.set_current_folder(configpaths.get('HOME')) - dlg.props.do_overwrite_confirmation = True - response = dlg.run() - - if response == Gtk.ResponseType.OK: # user want us to export ;) - liststore, list_of_paths = self.jids_listview.get_selection()\ - .get_selected_rows() - path_to_file = dlg.get_filename() - self._export_jids_logs_to_file(liststore, list_of_paths, - path_to_file) - - dlg.destroy() + def _on_export(self, filename): + liststore, list_of_paths = self.jids_listview.get_selection()\ + .get_selected_rows() + self._export_jids_logs_to_file(liststore, list_of_paths, filename) def on_delete_menuitem_activate(self, widget, listview): widget_name = Gtk.Buildable.get_name(listview) diff --git a/gajim/plugins/gui.py b/gajim/plugins/gui.py index 349c64936..9f01ffe63 100644 --- a/gajim/plugins/gui.py +++ b/gajim/plugins/gui.py @@ -35,7 +35,8 @@ import os from enum import IntEnum, unique from gajim import gtkgui_helpers -from gajim.dialogs import WarningDialog, YesNoDialog, ArchiveChooserDialog +from gajim.dialogs import WarningDialog, YesNoDialog +from gajim.filechoosers import ArchiveChooserDialog from gajim.htmltextview import HtmlTextView from gajim.common import app from gajim.common import configpaths @@ -309,8 +310,7 @@ class PluginsWindow(object): sel = self.installed_plugins_treeview.get_selection() sel.select_iter(iter_) - self.dialog = ArchiveChooserDialog( - on_response_ok=_try_install, transient_for=self.window) + ArchiveChooserDialog(_try_install, transient_for=self.window) class GajimPluginConfigDialog(Gtk.Dialog): diff --git a/gajim/profile_window.py b/gajim/profile_window.py index 462a75735..7469ccd2c 100644 --- a/gajim/profile_window.py +++ b/gajim/profile_window.py @@ -33,6 +33,7 @@ import hashlib from gajim import gtkgui_helpers from gajim import dialogs +from gajim.filechoosers import AvatarChooserDialog from gajim.common.const import AvatarSize from gajim.common import app @@ -107,7 +108,7 @@ class ProfileWindow: if event.keyval == Gdk.KEY_Escape: self.window.destroy() - def on_clear_button_clicked(self, widget): + def _clear_photo(self, widget): # empty the image button = self.xml.get_object('PHOTO_button') image = button.get_image() @@ -115,12 +116,14 @@ class ProfileWindow: button.hide() text_button = self.xml.get_object('NOPHOTO_button') text_button.show() + remove_avatar = self.xml.get_object('remove_avatar') + remove_avatar.hide() self.avatar_encoded = None self.avatar_sha = None self.avatar_mime_type = None def on_set_avatar_button_clicked(self, widget): - def on_ok(widget, path_to_file): + def on_ok(path_to_file): with open(path_to_file, 'rb') as file: data = file.read() sha = app.interface.save_avatar(data, publish=True) @@ -129,9 +132,6 @@ class ProfileWindow: _('Could not load image'), transient_for=self.window) return - self.dialog.destroy() - self.dialog = None - scale = self.window.get_scale_factor() surface = app.interface.get_avatar(sha, AvatarSize.VCARD, scale) @@ -142,26 +142,15 @@ class ProfileWindow: text_button = self.xml.get_object('NOPHOTO_button') text_button.hide() + remove_avatar = self.xml.get_object('remove_avatar') + remove_avatar.show() + self.avatar_sha = sha publish = app.interface.get_avatar(sha, publish=True) self.avatar_encoded = base64.b64encode(publish).decode('utf-8') - self.avatar_mime_type = 'image/jpeg' + self.avatar_mime_type = 'image/png' - def on_clear(widget): - self.dialog.destroy() - self.dialog = None - self.on_clear_button_clicked(widget) - - def on_cancel(widget): - self.dialog.destroy() - self.dialog = None - - if self.dialog: - self.dialog.present() - else: - self.dialog = dialogs.AvatarChooserDialog( - on_response_ok=on_ok, on_response_cancel=on_cancel, - on_response_clear=on_clear) + AvatarChooserDialog(on_ok, transient_for=self.window) def on_PHOTO_button_press_event(self, widget, event): """ @@ -218,11 +207,13 @@ class ProfileWindow: button = self.xml.get_object('PHOTO_button') image = button.get_image() text_button = self.xml.get_object('NOPHOTO_button') + remove_avatar = self.xml.get_object('remove_avatar') if not 'PHOTO' in vcard_: # set default image image.set_from_pixbuf(None) button.hide() text_button.show() + remove_avatar.hide() for i in vcard_.keys(): if i == 'PHOTO': photo_encoded = vcard_[i]['BINVAL'] @@ -241,6 +232,7 @@ class ProfileWindow: GdkPixbuf.InterpType.BILINEAR) image.set_from_pixbuf(pixbuf) button.show() + remove_avatar.show() text_button.hide() continue if i == 'ADR' or i == 'TEL' or i == 'EMAIL':