#!/usr/bin/env python # -*- coding: utf-8 -*- # # king_phisher/client/widget/managers.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import collections import datetime import functools from king_phisher import utilities from king_phisher.client import gui_utilities from gi.repository import Gdk from gi.repository import Gtk class ButtonGroupManager(object): """ Manage a set of buttons. The buttons should all be of the same type (such as "checkbutton" or "radiobutton") and include a common group name prefix. The intent is to make managing buttons of similar functionality easier by grouping them together. """ def __init__(self, glade_gobject, widget_type, group_name): """ :param glade_gobject: The gobject which has the radio buttons set. :type glade_gobject: :py:class:`.GladeGObject` :param str group_name: The name of the group of buttons. """ utilities.assert_arg_type(glade_gobject, gui_utilities.GladeGObject) self.group_name = group_name name_prefix = widget_type + '_' + self.group_name + '_' self.buttons = utilities.FreezableDict() for gobj_name in glade_gobject.dependencies.children: if not gobj_name.startswith(name_prefix): continue button_name = gobj_name[len(name_prefix):] self.buttons[button_name] = glade_gobject.gobjects[gobj_name] if not len(self.buttons): raise ValueError('found no ' + widget_type + ' of group: ' + self.group_name) self.buttons.freeze() def __repr__(self): return "<{0} group_name={1!r} active={2!r} >".format(self.__class__.__name__, self.group_name, self.__str__()) class RadioButtonGroupManager(ButtonGroupManager): """ Manage a group of :py:class:`Gtk.RadioButton` objects together to allow the active one to be easily set and identified. The buttons are retrieved from a :py:class:`.GladeGObject` instance and must be correctly named in the :py:attr:`.dependencies` attribute as 'radiobutton_group_name_button_name'. """ def __init__(self, glade_gobject, group_name): """ :param glade_gobject: The gobject which has the radio buttons set. :type glade_gobject: :py:class:`.GladeGObject` :param str group_name: The name of the group of buttons. """ super(RadioButtonGroupManager, self).__init__(glade_gobject, 'radiobutton', group_name) def __str__(self): return self.get_active() or '' def get_active(self): """ Return the name of the active button if one in the group is active. If no button in the group is active, None is returned. :return: The name of the active button. :rtype: str """ for name, button in self.buttons.items(): if button.get_active(): return name return def set_active(self, button): """ Set a button in the group as active. :param str button: The name of the button to set as active. """ button = self.buttons[button] button.set_active(True) button.toggled() class ToggleButtonGroupManager(ButtonGroupManager): """ Manage a mapping of button names to a boolean value indicating whether they are active or not. """ def __str__(self): return ', '.join(name for name, active in self.get_active().items() if active) def get_active(self): """ Get the button names and whether or not they are active. :return: A mapping of button names to whether or not they are active. :rtype: dict """ return {name: button.get_active() for name, button in self.buttons.items()} def set_active(self, buttons): """ Set the specified buttons to active or not. :param dict buttons: A mapping of button names to boolean values. """ for name, active in buttons.items(): button = self.buttons.get(name) if button is None: raise ValueError('invalid button name: ' + name) button.set_active(active) class MenuManager(object): """ A class that wraps :py:class:`Gtk.Menu` objects and facilitates managing their respective items. """ __slots__ = ('menu', 'items') def __init__(self, menu=None): """ :param menu: An optional menu to start with. If a menu is specified it is used as is, otherwise a new instance is used and is set to be visible using :py:meth:`~Gtk.Widget.show`. :type menu: :py:class:`Gtk.Menu` """ if menu is None: menu = Gtk.Menu() menu.show() self.menu = menu self.items = collections.OrderedDict() def __getitem__(self, label): return self.items[label] def __setitem__(self, label, menu_item): return self.append_item(menu_item, set_show=False) def append(self, label, activate=None, activate_args=()): """ Create and append a new :py:class:`Gtk.MenuItem` with the specified label to the menu. :param str label: The label for the new menu item. :param activate: An optional callback function to connect to the new menu item's ``activate`` signal. :return: Returns the newly created and added menu item. :rtype: :py:class:`Gtk.MenuItem` """ if label in self.items: raise RuntimeError('label already exists in menu items') menu_item = Gtk.MenuItem.new_with_label(label) self.items[label] = menu_item self.append_item(menu_item) if activate: menu_item.connect('activate', activate, *activate_args) return menu_item def append_item(self, menu_item, set_show=True): """ Append the specified menu item to the menu. :param menu_item: The item to append to the menu. :type menu_item: :py:class:`Gtk.MenuItem` :param bool set_show: Whether to set the item to being visible or leave it as is. """ if set_show: menu_item.show() self.menu.append(menu_item) return menu_item def append_submenu(self, label): """ Create and append a submenu item, then return a new menu manager instance for it. :param str label: The label for the new menu item. :return: Returns the newly created and added menu item. :rtype: :py:class:`Gtk.MenuManager` """ submenu = self.__class__() submenu_item = Gtk.MenuItem.new_with_label(label) submenu_item.set_submenu(submenu.menu) self.append_item(submenu_item) return submenu class TreeViewManager(object): """ A class that wraps :py:class:`Gtk.TreeView` objects that use `Gtk.ListStore` models with additional functions for conveniently displaying text data. If *cb_delete* is specified, the callback will be called with the treeview instance, and the selection as the parameters. If *cb_refresh* is specified, the callback will be called without any parameters. """ def __init__(self, treeview, selection_mode=None, cb_delete=None, cb_refresh=None): """ :param treeview: The treeview to wrap and manage. :type treeview: :py:class:`Gtk.TreeView` :param selection_mode: The selection mode to set for the treeview. :type selection_mode: :py:class:`Gtk.SelectionMode` :param cb_delete: An optional callback that can be used to delete entries. :type cb_delete: function """ self.treeview = treeview """The :py:class:`Gtk.TreeView` instance being managed.""" self.cb_delete = cb_delete """An optional callback for deleting entries from the treeview's model.""" self.cb_refresh = cb_refresh """An optional callback for refreshing the data in the treeview's model.""" self.column_titles = collections.OrderedDict() """An ordered dictionary of storage data columns keyed by their respective column titles.""" self.column_views = {} """A dictionary of column treeview's keyed by their column titles.""" self.treeview.connect('key-press-event', self.signal_key_press_event) if selection_mode is None: selection_mode = Gtk.SelectionMode.SINGLE treeview.get_selection().set_mode(selection_mode) self._menu_items = {} def _call_cb_delete(self): if not self.cb_delete: return selection = self.treeview.get_selection() if not selection.count_selected_rows(): return self.cb_delete(self.treeview, selection) def get_popup_menu(self, handle_button_press=True): """ Create a :py:class:`Gtk.Menu` with entries for copying and optionally delete cell data from within the treeview. The delete option will only be available if a delete callback was previously set. :param bool handle_button_press: Whether or not to connect a handler for displaying the popup menu. :return: The populated popup menu. :rtype: :py:class:`Gtk.Menu` """ popup_copy_submenu = self.get_popup_copy_submenu() popup_menu = Gtk.Menu.new() menu_item = Gtk.MenuItem.new_with_label('Copy') menu_item.set_submenu(popup_copy_submenu) popup_menu.append(menu_item) self._menu_items['Copy'] = menu_item if self.cb_delete: menu_item = Gtk.SeparatorMenuItem() popup_menu.append(menu_item) menu_item = Gtk.MenuItem.new_with_label('Delete') menu_item.connect('activate', self.signal_activate_popup_menu_delete) popup_menu.append(menu_item) self._menu_items['Delete'] = menu_item popup_menu.show_all() if handle_button_press: self.treeview.connect('button-press-event', self.signal_button_pressed, popup_menu) return popup_menu def get_popup_copy_submenu(self): """ Create a :py:class:`Gtk.Menu` with entries for copying cell data from the treeview. :return: The populated copy popup menu. :rtype: :py:class:`Gtk.Menu` """ copy_menu = Gtk.Menu.new() for column_title, store_id in self.column_titles.items(): menu_item = Gtk.MenuItem.new_with_label(column_title) menu_item.connect('activate', self.signal_activate_popup_menu_copy, store_id) copy_menu.append(menu_item) if len(self.column_titles) > 1: menu_item = Gtk.SeparatorMenuItem() copy_menu.append(menu_item) menu_item = Gtk.MenuItem.new_with_label('All') menu_item.connect('activate', self.signal_activate_popup_menu_copy, self.column_titles.values()) copy_menu.append(menu_item) return copy_menu def set_column_color(self, background=None, foreground=None, column_titles=None): """ Set a column in the model to be used as either the background or foreground RGBA color for a cell. :param int background: The column id of the model to use as the background color. :param int foreground: The column id of the model to use as the foreground color. :param column_titles: The columns to set the color for, if None is specified all columns will be set. :type column_titles: str, tuple """ if background is None and foreground is None: raise RuntimeError('either background of foreground must be set') if column_titles is None: column_titles = self.column_titles.keys() elif isinstance(column_titles, str): column_titles = (column_titles,) for column_title in column_titles: column = self.column_views[column_title] renderer = column.get_cells()[0] if background is not None: column.add_attribute(renderer, 'background-rgba', background) column.add_attribute(renderer, 'background-set', True) if foreground is not None: column.add_attribute(renderer, 'foreground-rgba', foreground) column.add_attribute(renderer, 'foreground-set', True) def set_column_titles(self, column_titles, column_offset=0, renderers=None): """ Populate the column names of a GTK TreeView and set their sort IDs. This also populates the :py:attr:`.column_titles` attribute. :param list column_titles: The titles of the columns. :param int column_offset: The offset to start setting column names at. :param list renderers: A list containing custom renderers to use for each column. :return: A dict of all the :py:class:`Gtk.TreeViewColumn` objects keyed by their column id. :rtype: dict """ self.column_titles.update((v, k) for (k, v) in enumerate(column_titles, column_offset)) columns = gui_utilities.gtk_treeview_set_column_titles(self.treeview, column_titles, column_offset=column_offset, renderers=renderers) for store_id, column_title in enumerate(column_titles, column_offset): self.column_views[column_title] = columns[store_id] return columns def signal_button_pressed(self, treeview, event, popup_menu): if not (event.type == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY): return selection = treeview.get_selection() sensitive = bool(selection.count_selected_rows()) for menu_item in self._menu_items.values(): menu_item.set_sensitive(sensitive) popup_menu.popup(None, None, functools.partial(gui_utilities.gtk_menu_position, event), None, event.button, event.time) return True def signal_key_press_event(self, treeview, event): if event.type != Gdk.EventType.KEY_PRESS: return keyval = event.get_keyval()[1] if event.get_state() == Gdk.ModifierType.CONTROL_MASK: if keyval == Gdk.KEY_c and self.column_titles: gui_utilities.gtk_treeview_selection_to_clipboard(treeview, list(self.column_titles.values())[0]) elif keyval == Gdk.KEY_F5 and self.cb_refresh: self.cb_refresh() elif keyval == Gdk.KEY_Delete: self._call_cb_delete() def signal_activate_popup_menu_copy(self, menuitem, column_ids): gui_utilities.gtk_treeview_selection_to_clipboard(self.treeview, column_ids) def signal_activate_popup_menu_delete(self, menuitem): self._call_cb_delete() class _TimeSelector(gui_utilities.GladeGObject): """ This is the TimeSelector :py:class:`~Gtk.Popover` object containing the :py:class:`~Gtk.SpinButton` widgets. This class should be treated as private, as it is created by the :py:class:`~TimeSelectorButtonManager` class. It should not be used directly. """ dependencies = gui_utilities.GladeDependencies( children=( 'spinbutton_hour', 'spinbutton_minute' ), top_level=( 'ClockHourAdjustment', 'ClockMinuteAdjustment' ), name='TimeSelector' ) top_gobject = 'popover' def signal_spinbutton_output(self, spinbutton): adjustment = spinbutton.get_adjustment() value = adjustment.get_value() spinbutton.set_text("{0:02.0f}".format(value)) return True class TimeSelectorButtonManager(object): """ A manager class to convert a :py:class:`~Gtk.ToggleButton` to be used for showing a time selector py:class:`~.Gtk.Popover` object with inputs for setting the hour and minutes. This then exposes the selected time through the :py:attr:`.time` attribute. """ def __init__(self, application, button, value=None): """ :param button: The button used for activation. :type button: :py:class:`Gtk.ToggleButton` :param application: The application instance which owns this object. :param value: The present datetime value (defaults to 00:00). :type value: :py:class:`datetime.time` """ self.popover = _TimeSelector(application) self.button = button self.application = application self._time_format = "{time.hour:02}:{time.minute:02}" self._hour_spin = self.popover.gobjects['spinbutton_hour'] self._hour_spin.connect('value-changed', lambda _: self.button.set_label(self._time_format.format(time=self.time))) self._minute_spin = self.popover.gobjects['spinbutton_minute'] self._minute_spin.connect('value-changed', lambda _: self.button.set_label(self._time_format.format(time=self.time))) self.time = value or datetime.time(0, 0) self.popover.popover.set_relative_to(self.button) self.popover.popover.connect('closed', lambda _: self.button.set_active(False)) self.button.connect('toggled', self.signal_button_toggled) def __repr__(self): return "<{0} time='{1:%H:%M}' >".format(self.__class__.__name__, self.time) def signal_button_toggled(self, _): if self.button.get_active(): self.popover.popover.popup() @property def time(self): """ This property represents the current time value and when set, updates the associated button. :return: The current time value. :rtype: :py:class:`datetime.time` """ return datetime.time(self._hour_spin.get_value_as_int(), self._minute_spin.get_value_as_int()) @time.setter def time(self, value): """ :param value: value from self.popover.gobjects['spinbutton_xx'] :return: The new time value to self.time """ if not isinstance(value, datetime.time): raise TypeError('argument 1 must be a datetime.time instance') self._hour_spin.set_value(value.hour) self._minute_spin.set_value(value.minute) self.button.set_label(self._time_format.format(time=value))