gui.py 12.4 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1 2 3 4 5 6 7 8 9 10 11 12 13
# 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/>.
14 15 16 17 18

'''
GUI classes related to plug-in management.

:author: Mateusz Biliński <mateusz@bilinski.it>
Mateusz Biliński's avatar
Mateusz Biliński committed
19
:since: 6th June 2008
20 21 22 23
:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it>
:license: GPL
'''

Philipp Hörist's avatar
Philipp Hörist committed
24 25 26
import os
from enum import IntEnum
from enum import unique
27

28 29
from gi.repository import Gtk
from gi.repository import GdkPixbuf
30
from gi.repository import Gdk
31

32
from gajim.common import app
33
from gajim.common import ged
Philipp Hörist's avatar
Philipp Hörist committed
34
from gajim.common.exceptions import PluginsystemError
35
from gajim.common.helpers import open_uri
36
from gajim.common.nec import EventHelper
Philipp Hörist's avatar
Philipp Hörist committed
37

André's avatar
André committed
38 39
from gajim.plugins.helpers import GajimPluginActivateException
from gajim.plugins.plugins_i18n import _
Philipp Hörist's avatar
Philipp Hörist committed
40 41

from gajim.gtk.dialogs import WarningDialog
42 43
from gajim.gtk.dialogs import DialogButton
from gajim.gtk.dialogs import NewConfirmationDialog
Philipp Hörist's avatar
Philipp Hörist committed
44 45 46
from gajim.gtk.filechoosers import ArchiveChooserDialog
from gajim.gtk.util import get_builder
from gajim.gtk.util import load_icon
47

48

49
@unique
50 51 52 53 54 55
class Column(IntEnum):
    PLUGIN = 0
    NAME = 1
    ACTIVE = 2
    ACTIVATABLE = 3
    ICON = 4
Dicson's avatar
Dicson committed
56 57


58
class PluginsWindow(Gtk.ApplicationWindow, EventHelper):
59
    def __init__(self):
60
        Gtk.ApplicationWindow.__init__(self)
61 62
        EventHelper.__init__(self)

63 64 65 66 67
        self.set_application(app.app)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.set_default_size(650, 500)
        self.set_show_menubar(False)
        self.set_title(_('Plugins'))
68

69 70
        self._ui = get_builder('plugins_window.ui')
        self.add(self._ui.plugins_notebook)
71 72 73

        # Disable 'Install from ZIP' for Flatpak installs
        if app.is_flatpak():
74
            self._ui.install_plugin_button.set_tooltip_text(
75 76
                _('Click to view Gajim\'s wiki page on how to install plugins '
                  'in Flatpak.'))
77

Dicson's avatar
Dicson committed
78
        self.installed_plugins_model = Gtk.ListStore(object, str, bool, bool,
79
                                                     GdkPixbuf.Pixbuf)
80 81
        self._ui.installed_plugins_treeview.set_model(
            self.installed_plugins_model)
82

83
        renderer = Gtk.CellRendererText()
84
        col = Gtk.TreeViewColumn(_('Plugin'))  # , renderer, text=Column.NAME)
85
        cell = Gtk.CellRendererPixbuf()
Dicson's avatar
Dicson committed
86
        col.pack_start(cell, False)
87
        col.add_attribute(cell, 'pixbuf', Column.ICON)
Dicson's avatar
Dicson committed
88
        col.pack_start(renderer, True)
89
        col.add_attribute(renderer, 'text', Column.NAME)
90
        col.set_property('expand', True)
91
        self._ui.installed_plugins_treeview.append_column(col)
92

93
        renderer = Gtk.CellRendererToggle()
94
        renderer.connect('toggled', self._installed_plugin_toggled)
95
        col = Gtk.TreeViewColumn(_('Active'), renderer, active=Column.ACTIVE,
96
                                 activatable=Column.ACTIVATABLE)
97
        self._ui.installed_plugins_treeview.append_column(col)
98

99
        self.def_icon = load_icon('preferences-desktop', self, pixbuf=True)
100

101
        # connect signal for selection change
102 103
        selection = self._ui.installed_plugins_treeview.get_selection()
        selection.connect(
104
            'changed', self._installed_plugins_treeview_selection_changed)
105
        selection.set_mode(Gtk.SelectionMode.SINGLE)
106

107
        self._clear_installed_plugin_info()
108

109
        self._fill_installed_plugins_model()
Dicson's avatar
Dicson committed
110 111
        root_iter = self.installed_plugins_model.get_iter_first()
        if root_iter:
112
            selection.select_iter(root_iter)
113

114 115 116
        self.connect('destroy', self._on_destroy)
        self.connect('key-press-event', self._on_key_press)
        self._ui.connect_signals(self)
117

118
        self._ui.plugins_notebook.set_current_page(0)
119

120 121
        # Adding GUI extension point for Plugins that want to hook
        # the Plugin Window
122
        app.plugin_manager.gui_extension_point('plugin_window', self)
123

124 125 126 127 128
        self.register_events([
            ('plugin-removed', ged.GUI1, self._on_plugin_removed),
            ('plugin-added', ged.GUI1, self._on_plugin_added),
        ])

129
        self.show_all()
130

131 132
    def get_notebook(self):
        # Used by plugins
133
        return self._ui.plugins_notebook
134

135
    def _on_key_press(self, _widget, event):
136
        if event.keyval == Gdk.KEY_Escape:
137 138 139 140 141
            self.destroy()

    def _on_destroy(self, *args):
        self.unregister_events()
        app.plugin_manager.remove_gui_extension_point('plugin_window', self)
142

143
    def _installed_plugins_treeview_selection_changed(self, treeview_selection):
144 145 146
        model, iter_ = treeview_selection.get_selected()
        if iter_:
            plugin = model.get_value(iter_, Column.PLUGIN)
147 148 149 150 151
            self._display_installed_plugin_info(plugin)
        else:
            self._clear_installed_plugin_info()

    def _display_installed_plugin_info(self, plugin):
152 153 154
        self._ui.plugin_name_label.set_text(plugin.name)
        self._ui.plugin_version_label.set_text(plugin.version)
        self._ui.plugin_authors_label.set_text(plugin.authors)
Sophie Herold's avatar
Sophie Herold committed
155
        markup = '<a href="%s">%s</a>' % (plugin.homepage, plugin.homepage)
156
        self._ui.plugin_homepage_linkbutton.set_markup(markup)
Sophie Herold's avatar
Sophie Herold committed
157

158
        if plugin.available_text:
Sophie Herold's avatar
Sophie Herold committed
159
            text = _('Warning: %s') % plugin.available_text
160 161
            self._ui.available_text_label.set_text(text)
            self._ui.available_text.show()
Sophie Herold's avatar
Sophie Herold committed
162
            # Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=710888
163
            self._ui.available_text.queue_resize()
Sophie Herold's avatar
Sophie Herold committed
164
        else:
165
            self._ui.available_text.hide()
Sophie Herold's avatar
Sophie Herold committed
166

167
        self._ui.description.set_text(plugin.description)
Sophie Herold's avatar
Sophie Herold committed
168

169
        self._ui.uninstall_plugin_button.set_sensitive(True)
170
        self._ui.configure_plugin_button.set_sensitive(
171
            plugin.config_dialog is not None and plugin.active)
172 173

    def _clear_installed_plugin_info(self):
174 175 176 177
        self._ui.plugin_name_label.set_text('')
        self._ui.plugin_version_label.set_text('')
        self._ui.plugin_authors_label.set_text('')
        self._ui.plugin_homepage_linkbutton.set_markup('')
178

179 180 181
        self._ui.description.set_text('')
        self._ui.uninstall_plugin_button.set_sensitive(False)
        self._ui.configure_plugin_button.set_sensitive(False)
182

183
    def _fill_installed_plugins_model(self):
184
        pm = app.plugin_manager
185
        self.installed_plugins_model.clear()
186 187
        self.installed_plugins_model.set_sort_column_id(1,
                                                        Gtk.SortType.ASCENDING)
188 189

        for plugin in pm.plugins:
190 191 192 193 194 195 196 197 198
            icon = self._get_plugin_icon(plugin)
            self.installed_plugins_model.append(
                [plugin,
                 plugin.name,
                 plugin.active and plugin.activatable,
                 plugin.activatable,
                 icon])

    def _get_plugin_icon(self, plugin):
199
        icon_file = os.path.join(plugin.__path__, os.path.split(
200
                                 plugin.__path__)[1]) + '.png'
201 202
        icon = self.def_icon
        if os.path.isfile(icon_file):
203
            icon = GdkPixbuf.Pixbuf.new_from_file_at_size(icon_file, 16, 16)
204
        return icon
205

206
    def _installed_plugin_toggled(self, _cell, path):
207 208
        is_active = self.installed_plugins_model[path][Column.ACTIVE]
        plugin = self.installed_plugins_model[path][Column.PLUGIN]
209 210

        if is_active:
211
            app.plugin_manager.deactivate_plugin(plugin)
212
        else:
213
            try:
214
                app.plugin_manager.activate_plugin(plugin)
Yann Leboulanger's avatar
Yann Leboulanger committed
215
            except GajimPluginActivateException as e:
216
                WarningDialog(_('Plugin failed'), str(e),
217
                              transient_for=self)
218
                return
219

220
        self._ui.configure_plugin_button.set_sensitive(not is_active)
221
        self.installed_plugins_model[path][Column.ACTIVE] = not is_active
222

223
    def _on_configure_plugin(self, _widget):
224
        selection = self._ui.installed_plugins_treeview.get_selection()
225 226 227
        model, iter_ = selection.get_selected()
        if iter_:
            plugin = model.get_value(iter_, Column.PLUGIN)
228

229
            if isinstance(plugin.config_dialog, GajimPluginConfigDialog):
230
                plugin.config_dialog.run(self)
231
            else:
232
                plugin.config_dialog(self)
233 234 235 236 237 238 239

        else:
            # No plugin selected. this should never be reached. As configure
            # plugin button should only be clickable when plugin is selected.
            # XXX: maybe throw exception here?
            pass

240
    def _on_uninstall_plugin(self, _widget):
241
        selection = self._ui.installed_plugins_treeview.get_selection()
242 243 244
        model, iter_ = selection.get_selected()
        if iter_:
            plugin = model.get_value(iter_, Column.PLUGIN)
245
            try:
246
                app.plugin_manager.uninstall_plugin(plugin)
Yann Leboulanger's avatar
Yann Leboulanger committed
247
            except PluginsystemError as e:
248
                WarningDialog(_('Unable to properly remove the plugin'),
249
                              str(e), self)
250
                return
251 252 253 254 255 256 257 258

    def _on_plugin_removed(self, event):
        for row in self.installed_plugins_model:
            if row[Column.PLUGIN] == event.plugin:
                self.installed_plugins_model.remove(row.iter)
                break

    def _on_plugin_added(self, event):
259
        icon = self._get_plugin_icon(event.plugin)
260 261 262 263 264
        self.installed_plugins_model.append([event.plugin,
                                             event.plugin.name,
                                             False,
                                             event.plugin.activatable,
                                             icon])
265

266
    def _on_install_plugin(self, _widget):
267
        if app.is_flatpak():
268
            open_uri('https://dev.gajim.org/gajim/gajim/wikis/help/flathub')
269 270
            return

271
        def _show_warn_dialog():
272
            text = _('Archive is malformed')
273
            dialog = WarningDialog(text, '', transient_for=self)
274 275 276
            dialog.set_modal(False)
            dialog.popup()

277
        def _on_plugin_exists(zip_filename):
278
            def _on_yes():
279
                plugin = app.plugin_manager.install_from_zip(zip_filename,
Emmanuel Gil Peyrot's avatar
Emmanuel Gil Peyrot committed
280
                                                             overwrite=True)
281
                if not plugin:
282
                    _show_warn_dialog()
283
                    return
284

285 286 287 288 289 290 291 292
            NewConfirmationDialog(
                _('Overwrite Plugin?'),
                _('Plugin already exists'),
                _('Do you want to overwrite the currently installed version?'),
                [DialogButton.make('Cancel'),
                 DialogButton.make('Remove',
                                   text=_('_Overwrite'),
                                   callback=_on_yes)],
293
                transient_for=self).show()
294 295 296

        def _try_install(zip_filename):
            try:
297
                plugin = app.plugin_manager.install_from_zip(zip_filename)
Yann Leboulanger's avatar
Yann Leboulanger committed
298
            except PluginsystemError as er_type:
299 300 301 302 303
                error_text = str(er_type)
                if error_text == _('Plugin already exists'):
                    _on_plugin_exists(zip_filename)
                    return

304
                WarningDialog(error_text, '"%s"' % zip_filename, self)
305 306
                return
            if not plugin:
307
                _show_warn_dialog()
308 309
                return

310
        ArchiveChooserDialog(_try_install, transient_for=self)
311 312


313
class GajimPluginConfigDialog(Gtk.Dialog):
314
    def __init__(self, plugin, **kwargs):
315 316
        Gtk.Dialog.__init__(self, title='%s %s' % (plugin.name,
                            _('Configuration')), **kwargs)
317
        self.plugin = plugin
Dicson's avatar
Dicson committed
318 319
        button = self.add_button('gtk-close', Gtk.ResponseType.CLOSE)
        button.connect('clicked', self.on_close_button_clicked)
320

321
        self.get_child().set_spacing(3)
322 323 324

        self.init()

325 326 327 328
    def on_close_dialog(self, widget, data):
        self.hide()
        return True

Dicson's avatar
Dicson committed
329 330 331
    def on_close_button_clicked(self, widget):
        self.hide()

332 333 334 335
    def run(self, parent=None):
        self.set_transient_for(parent)
        self.on_run()
        self.show_all()
336
        self.connect('delete-event', self.on_close_dialog)
337
        result = super(GajimPluginConfigDialog, self)
338 339 340 341 342 343 344
        return result

    def init(self):
        pass

    def on_run(self):
        pass