# widgets.py
#
# Copyright 2020 Fabio
#
# This program 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.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
from os import path
import datetime
import html
from dateutil.tz import UTC
from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
from gi.repository import GdkPixbuf
from gi.repository import Handy
from .config import FMT_DAY_LIST
class ListRowHeader(Gtk.VBox):
def __init__(self, text):
super().__init__(margin=16)
label = Gtk.Label("", halign=Gtk.Align.END)
label.set_markup("<b>{}</b>".format(text))
sep = Gtk.Separator()
self.pack_start(label, True, True, 2)
self.pack_start(sep, False, False, 2)
self.show_all()
class WebLinkActionRow(Handy.ActionRow):
def __init__(self, title, url, icontype=None, **kwargs):
opts = dict(activatable=True, selectable=False)
opts.update(kwargs)
super().__init__(**opts)
self.url = url
if title is None:
title = path.basename(url)
self.set_title(html.unescape(title))
self.set_subtitle(url)
#if icontype is None:
normurl = url.split("#")[0].split("?")[0]
ftype, _ = Gio.content_type_guess(normurl)
fmaj, fmin = ftype.split("/")
icontype = "text/html"
if fmaj in ('text','video', 'audio', 'image'):
icontype = ftype
if fmin == 'pdf' or 'document' in fmin:
icontype = ftype
self.content_type = icontype
ficon = Gio.content_type_get_icon(icontype)
# I have to manage icon by hand.. ouf
image = Gtk.Image.new_from_gicon(ficon, Gtk.IconSize.DIALOG)
self.add_prefix(image)
def weblinkactionrowactivated(listbox, actionrow):
appinfo = Gio.app_info_get_default_for_type(actionrow.content_type, True)
if appinfo is not None:
appinfo.launch_uris([actionrow.url], None)
else:
Gio.app_info_launch_default_for_uri(actionrow.url, None)
class EventActionRow(Handy.ActionRow):
def __init__(self, obj):
super().__init__(activatable=True, selectable=False)
subtitle = "{}-{}".format(
obj.start_in_tz().strftime("%a %H:%M"),
obj.end_in_tz().strftime("%H:%M"))
if obj.room:
subtitle = "{} ({})".format(
subtitle,
obj.room)
if obj.track:
subtitle = "{} '{}'".format(
subtitle,
obj.track)
ps = list(obj.persons())
if len(ps) > 0:
subtitle = "{} - {}".format(
subtitle,
", ".join([str(p) for p in ps]))
self.set_title(html.unescape(obj.title))
self.set_subtitle(html.unescape(subtitle))
self.obj = obj
obj.connect('update', self.on_update)
icon_name = "starred-symbolic" if obj.starred else "non-starred-symbolic"
self.toggle_btn = Gtk.Button.new_from_icon_name(icon_name, Gtk.IconSize.BUTTON)
self.toggle_btn.connect("clicked", self.toggle_starred)
self.add(self.toggle_btn)
Gio.Application.get_default().connect(
"tick", lambda *_: self.queue_draw()
)
self.connect("draw", self._on_draw)
def _on_draw(self, widget, cr):
now = datetime.datetime.now(UTC).replace(tzinfo=None)
obj = self.obj
h = self.get_allocated_height()
dur = (obj.end.timestamp() - obj.start.timestamp())
pos = (now.timestamp() - obj.start.timestamp())
if dur > 0:
prc = min(max(pos/dur, 0.0), 1.0)
else:
# not a valid event duration
# set prc to 0 if the event is in the future or 1 if in the past
prc = int(pos >= 0)
y = h * prc
cr.set_source_rgb(0, 0, 0)
cr.move_to(1, 0)
cr.line_to(1, y)
cr.stroke()
def on_update(self, *args):
icon_name = "starred-symbolic" if self.obj.starred else "non-starred-symbolic"
self.toggle_btn.get_image().set_from_icon_name(icon_name, Gtk.IconSize.BUTTON)
def toggle_starred(self, button):
self.obj.set_star(not self.obj.starred)
class MenuBoxRow(Gtk.ListBoxRow):
def __init__(self, title, icon):
super().__init__()
box = Gtk.HBox(margin=16)
lbl = Gtk.Label(title)
icn = Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.LARGE_TOOLBAR )
box.pack_start(icn, False, True, 0)
box.pack_start(lbl, False, True, 8)
self.add(box)
class PlaceholderWidget(Gtk.VBox):
def __init__(self, title=None, text=None, icon_name='net.kirgroup.confy'):
super().__init__(valign = Gtk.Align.CENTER)
img = Gtk.Image.new_from_pixbuf(
Gtk.IconTheme.get_default().load_icon(
icon_name, 128, 0))
self.pack_start(img, False, False, 16)
LBL_PROPS = {
'justify': Gtk.Justification.CENTER,
'halign' : Gtk.Align.CENTER,
'wrap' : True,
}
if title is not None:
label = Gtk.Label(**LBL_PROPS)
label.set_markup(_("<b>{}</b>").format(title))
self.pack_start(label, False, False, 8)
if text is not None:
label = Gtk.Label(**LBL_PROPS)
label.set_markup("<small>{}</small>".format(text))
label.get_style_context().add_class("dim-label")
self.pack_start(label, False, False, 8)
self.show_all()
class LoadingWidget(Handy.Clamp):
def __init__(self, fetcher):
super().__init__(maximum_size=400)
box = Gtk.VBox(valign = Gtk.Align.CENTER)
img = Gtk.Image.new_from_pixbuf(
Gtk.IconTheme.get_default().load_icon(
'net.kirgroup.confy', 256, 0))
self.pb = pb = Gtk.ProgressBar()
label = Gtk.Label()
label.set_markup(_("<b>Loading…</b>"))
button = Gtk.Button("Cancel")
fetcher.connect("notify", lambda *args: pb.set_fraction(fetcher.props.fraction))
button.connect("clicked", lambda *args: fetcher.cancel())
box.pack_start(img, False, False, 16)
box.pack_start(label, False, False, 8)
box.pack_start(pb, False, False, 8)
box.pack_start(button, False, False, 16)
self.add(box)
GLib.timeout_add(200, self._do_pulse)
def _do_pulse(self,*args):
if self.pb.get_fraction() > 0:
return False
self.pb.pulse()
return True
class ErrorWidget(Gtk.VBox):
def __init__(self, msg, error):
super().__init__()
self.set_valign(Gtk.Align.CENTER)
img = Gtk.Image.new_from_icon_name('dialog-warning-symbolic', Gtk.IconSize.DIALOG )
lbl1 = Gtk.Label()
lbl1.set_markup("<b>{}</b>".format(msg))
lbl2 = Gtk.Label(str(error))
lbl2.get_style_context().add_class("dim-label")
self.pack_start(img, False, False, 16)
self.pack_start(lbl1, False, False, 0)
self.pack_start(lbl2, False, False, 0)
class NotificationOverlay(Gtk.Revealer):
def __init__(self):
super().__init__(halign=Gtk.Align.CENTER, valign=Gtk.Align.START)
box = Gtk.HBox(spacing=20)
box.get_style_context().add_class("app-notification")
label = Gtk.Label("")
button = Gtk.Button.new_from_icon_name("window-close-symbolic", Gtk.IconSize.BUTTON)
box.pack_start(label,False, True, 0)
box.pack_start(button, False, False, 0)
self.add(box)
button.connect("clicked", self.close)
self.label = label
self.show_all()
def show(self, text):
self.label.set_markup(text)
self.set_reveal_child(True)
def close(self, *args):
self.set_reveal_child(False)
class LoadingOverlay(Gtk.Revealer):
def __init__(self):
super().__init__(halign=Gtk.Align.CENTER, valign=Gtk.Align.START)
box = Gtk.HBox(spacing=20)
box.get_style_context().add_class("app-notification")
spinner = Gtk.Spinner.new()
spinner.start()
label = Gtk.Label(_("Loading…"), margin_right=16)
button = Gtk.Button.new_from_icon_name("process-stop-symbolic", Gtk.IconSize.BUTTON)
box.pack_start(spinner,False, False, 0)
box.pack_start(label,False, True, 0)
box.pack_start(button, False, False, 0)
self.add(box)
button.connect("clicked", self.cancel)
self.cancellable = None
self.show_all()
def show(self,cancellable):
self.cancellable = cancellable
if cancellable is not None:
self.cancellable.connect("done", self.close)
self.cancellable.connect("error", self.close)
self.set_reveal_child(True)
def cancel(self, *args):
if self.cancellable is not None:
self.cancellable.cancel()
self.close()
def close(self, *args):
self.cancellable = None
self.set_reveal_child(False)
class SearchBarOverlay(Handy.SearchBar):
def __init__(self, **kwargs):
opts = dict(
halign = Gtk.Align.FILL,
valign = Gtk.Align.START,
show_close_button = True
)
opts.update(kwargs)
super().__init__(**opts)
clamp = Handy.Clamp()
self.entry = Gtk.SearchEntry(can_focus=True)
clamp.add(self.entry)
self.add(clamp)
self.show_all()
self.connect('notify::search-mode-enabled', self._on_search_mode_changed)
def _on_search_mode_changed(self, *args):
if self.get_search_mode():
self.entry.set_text("")
self.entry.grab_focus()
class OkCancelDialog(Gtk.Dialog):
CANCEL = 0
OK = 1
title = "Dialog"
def __init__(self, parent, **kwargs):
opts = dict(
transient_for=parent,
can_focus=False, modal=True,
use_header_bar=True,
default_height=20,
destroy_with_parent=True
)
opts.update(kwargs)
super().__init__(**opts)
self._btn_cancel = cancel = Gtk.Button(name="cancel_button", label=_("Cancel"), receives_default=True)
self._btn_ok = ok = Gtk.Button(name="done_button", label=_("Ok"), receives_default=True)
ok.get_style_context().add_class("suggested-action")
cancel.connect("clicked", self._do_cancel)
ok.connect("clicked", self._do_ok)
hb = self.get_header_bar()
hb.pack_start(cancel)
hb.pack_end(ok)
hb.set_show_close_button(False)
hb.set_title(self.title)
self.setup_ui()
self.show_all()
def _do_cancel(self, *args):
self.response(self.CANCEL)
def _do_ok(self, *args):
self.response(self.OK)
def setup_ui(self):
...
class InputDialog(OkCancelDialog):
title = _("Schedule URL")
def setup_ui(self):
self._input = Gtk.Entry()
self.get_content_area().add(self._input)
def get_text(self):
return self._input.get_text()
ACTION_ROW_PROPS = dict(
selectable=False,
activatable=True,
)
ACTION_ROW_ENTRY_PROPS = dict(
valign=Gtk.Align.CENTER,
halign=Gtk.Align.FILL,
vexpand=False,
hexpand=True,
)
class ConferenceEditDialog(OkCancelDialog):
DELETE=3
title = _("Edit Conference")
def __init__(self, parent, conf, can_cancel=False, can_delete=False):
self._err = set()
self.conf = conf
self._can_delete = can_delete
super().__init__(parent, default_height=355)
if not can_cancel:
self._btn_cancel.destroy()
def get_conf(self):
return self.conf
def _do_delete(self, *args):
self.response(self.DELETE)
def _do_ok(self, *args):
self.conf.url = self._entry_url.get_text()
self.conf.title = self._entry_title.get_text()
self.conf.start = self._entry_start.get_date()
self.conf.end = self._entry_end.get_date()
if self._entry_icn:
if self._entry_icn.get_text() != "":
self.conf.metadata['icon'] = self._entry_icn.get_text()
elif 'icon' in self.conf.metadata:
del self.conf.metadata['icon']
self.response(self.OK)
def _validate_entry_required(self, entry):
if len(entry.get_text()) > 0:
entry.get_style_context().remove_class("error")
if entry in self._err:
self._err.remove(entry)
else:
entry.get_style_context().add_class("error")
self._err.add(entry)
self._update_ok()
def _validate_date_order(self, entry):
if self._entry_start.get_date() > self._entry_end.get_date():
entry.get_style_context().add_class("error")
self._err.add(entry)
else:
for e in (self._entry_start, self._entry_end):
e.get_style_context().remove_class("error")
if e in self._err:
self._err.remove(e)
self._update_ok()
def _update_ok(self):
self._btn_ok.set_sensitive(len(self._err) == 0)
def setup_ui(self):
scroll = Gtk.ScrolledWindow(
hexpand=True,
vexpand=True,
kinetic_scrolling=True,
hscrollbar_policy=Gtk.PolicyType.NEVER)
column = Handy.Clamp(
margin=16,
maximum_size=600,
tightening_threshold=400)
box = Gtk.VBox(valign=Gtk.Align.START, spacing=8)
listbox = Gtk.ListBox()
listbox.get_style_context().add_class("content")
self._entry_url = entry = Gtk.Entry(text=self.conf.url, editable=False, **ACTION_ROW_ENTRY_PROPS)
entry.connect("changed", self._validate_entry_required)
self._validate_entry_required(entry)
row = Handy.ActionRow(title=_("URL"), **ACTION_ROW_PROPS)
row.add(entry)
row.set_activatable_widget(entry)
listbox.add(row)
self._entry_title = entry = Gtk.Entry(text=self.conf.title, **ACTION_ROW_ENTRY_PROPS)
entry.connect("changed", self._validate_entry_required)
self._validate_entry_required(entry)
row = Handy.ActionRow(title=_("Title"), **ACTION_ROW_PROPS)
row.add(entry)
row.set_activatable_widget(entry)
listbox.add(row)
self._entry_start = entry = DatePickerButton(date=self.conf.start, **ACTION_ROW_ENTRY_PROPS)
entry.get_style_context().add_class("list-button")
entry.connect("changed", self._validate_date_order)
row = Handy.ActionRow(title=_("Start"), **ACTION_ROW_PROPS)
row.add(entry)
row.set_activatable_widget(entry)
listbox.add(row)
self._entry_end = entry = DatePickerButton(date=self.conf.end, **ACTION_ROW_ENTRY_PROPS)
entry.get_style_context().add_class("list-button")
entry.connect("changed", self._validate_date_order)
self._validate_date_order(entry)
row = Handy.ActionRow(title=_("End"), **ACTION_ROW_PROPS)
row.add(entry)
row.set_activatable_widget(entry)
listbox.add(row)
if self.conf.metadata is not None:
self._entry_icn = entry = Gtk.Entry(text=self.conf.metadata.get('icon',''), **ACTION_ROW_ENTRY_PROPS)
row = Handy.ActionRow(title=_("Icon URL"), **ACTION_ROW_PROPS)
row.add(entry)
row.set_activatable_widget(entry)
listbox.add(row)
else:
self._entry_icn = None
box.add(listbox)
if self._can_delete:
btn_delete = Gtk.Button(
name="delete_button", label=_("Delete"),
valign=Gtk.Align.START,
halign=Gtk.Align.END)
btn_delete.get_style_context().add_class("destructive-action")
btn_delete.connect("clicked", self._do_delete)
box.add(btn_delete)
column.add(box)
scroll.add(column)
self.get_content_area().add(scroll)
class DatePickerButton(Gtk.Button):
@GObject.Signal
def changed(self):
...
def __init__(self, date, **kwargs):
super().__init__(**kwargs)
self._is_popup = False
self.pop = Gtk.Popover.new(self)
self.cal = Gtk.Calendar()
self.cal.show_all()
self.pop.add(self.cal)
self.pop.connect("closed", self._cal_closed)
self.connect("clicked", self._toggle_cal)
self.set_date(date)
def get_date(self):
return self.date
def get_datetime(self):
return datetime.datetime.combine(self.date, datetime.time.min)
def set_date(self, date):
self.date = date
if date is None:
lbl = ""
date = datetime.date.today()
else:
lbl = str(date)
self.cal.select_month(date.month-1, date.year)
self.cal.select_day(date.day)
self.set_label(lbl)
self.emit("changed")
def _cal_closed(self, *args):
date = self.cal.get_date()
date = datetime.date(year=date.year, month=date.month+1, day=date.day)
self.set_date(date)
def _toggle_cal(self, *args):
if self._is_popup:
self.pop.popdown()
else:
self.pop.popup()
self._is_popup = not self._is_popup
class SimpleScalableImage(Gtk.Image):
@GObject.property(type=float)
def scale(self):
return self._scale
@scale.setter
def scale(self, value):
self._scale = value
pixbuf = self._originalpb.scale_simple(
self._w * value,
self._h * value,
GdkPixbuf.InterpType.BILINEAR)
super().set_from_pixbuf(pixbuf)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.gesture = Gtk.GestureZoom.new(self)
self.gesture.set_propagation_phase(Gtk.PropagationPhase.BUBBLE)
self.gesture.connect("scale-changed", self.on_scale_changed)
def on_scale_changed(self, gesture, scale):
self.scale_by(scale)
def scale_by(self, scale):
self.scale = self.scale * scale
def set_from_pixbuf(self, pixbuf):
self._scale = 1
self._w = pixbuf.get_width()
self._h = pixbuf.get_height()
self._originalpb = pixbuf
super().set_from_pixbuf(pixbuf)
class MaxSizedImage(Gtk.Image):
def __init__(self, max_width, max_height, **kwargs):
super().__init__(**kwargs)
self.max_width = max_width
self.max_height = max_height
def set_from_pixbuf(self, pixbuf):
w = pixbuf.get_width()
h = pixbuf.get_height()
r = w/h
if r>=1 and w > self.max_width:
w = self.max_width
h = w / r
pixbuf = pixbuf.scale_simple(
w,
h,
GdkPixbuf.InterpType.BILINEAR)
if r<1 and h > self.max_height:
h = self.max_height
w = h * r
pixbuf = pixbuf.scale_simple(
w,
h,
GdkPixbuf.InterpType.BILINEAR)
super().set_from_pixbuf(pixbuf)