#!/usr/bin/env python3
import concurrent.futures
import sys
import threading
from datetime import datetime
import gi
import pytz
from matplotlib import rcParams
from halo.API import API, APIError, RateLimitReached, NotFound
from halo.DataStore import DataStore
from halo.Icon import Icon
from halo.Place import PlaceDialog
from halo.Preference import PreferenceDialog
from halo.SummaryView import SummaryView
from halo.settings import BASE, VERSION
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GdkPixbuf, Gdk, GObject # noqa: E402
rcParams['font.family'] = 'sans-serif'
rcParams['font.sans-serif'] = ['Lato', 'DejaVu Sans']
[docs]class MainWindow(Gtk.ApplicationWindow):
def __init__(self, application):
"""
Initialises the main window
"""
super().__init__(application=application)
self.api = API()
self.store = DataStore()
self.city = None
self.city_tz = "UTC"
self.currentWeather = None
self.forecastWeather = []
self.chartData = []
self.historyWeather = []
self.historyChartData = []
self.LH = 0
self.LW = 0
# Background image
self.overlay = Gtk.Overlay()
self.add(self.overlay)
self.bg = Gtk.Image()
scrollable_wrapper = Gtk.ScrolledWindow()
scrollable_wrapper.add(self.bg)
scrollable_wrapper.set_size_request(700, 550)
self.overlay.add(scrollable_wrapper)
# Header
header = Gtk.HeaderBar()
header.set_show_close_button(True)
header.props.title = "Halo"
self.set_titlebar(header)
change_place = Gtk.Button(label="Change City")
change_place.connect("clicked", self.switch_city)
refresh_btn = Gtk.Button(label=None, image=Gtk.Image(icon_name='view-refresh-symbolic',
icon_size=Gtk.IconSize.BUTTON))
refresh_btn.connect("clicked", self.refresh)
header.pack_end(change_place)
header.pack_end(refresh_btn)
# Menu
menu = Gtk.MenuBar()
file = Gtk.MenuItem("File")
file_dropdown = Gtk.Menu()
file_refresh = Gtk.MenuItem("Refresh")
file_preference = Gtk.MenuItem("Preference")
file_exit = Gtk.MenuItem("Exit")
file.set_submenu(file_dropdown)
file_dropdown.append(file_refresh)
file_dropdown.append(file_preference)
file_dropdown.append(Gtk.SeparatorMenuItem())
file_dropdown.append(file_exit)
help_menu = Gtk.MenuItem("Help")
help_dropdown = Gtk.Menu()
help_about = Gtk.MenuItem("About")
help_menu.set_submenu(help_dropdown)
help_dropdown.append(help_about)
file_refresh.connect('activate', self.refresh)
file_preference.connect('activate', self.show_preference)
file_exit.connect('activate', lambda w: application.quit())
help_about.connect('activate', self.show_about)
menu.append(file)
menu.append(help_menu)
header.pack_start(menu)
# Weather Panel
tm = Gtk.Box(spacing=0)
left = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
right = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
top = Gtk.Box(spacing=10)
view = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
left.set_homogeneous(False)
right.set_homogeneous(False)
self.icon = Gtk.Image()
self.place = Gtk.Label()
self.status = Gtk.Label()
self.temperature = Gtk.Label()
self.time = Gtk.Label()
self.t_follow = Gtk.Label()
self.date = Gtk.Label()
self.time.set_alignment(0, 1)
self.t_follow.set_alignment(0, 1)
self.date.set_alignment(0, 0)
self.icon.set_alignment(1, 0)
self.status.set_alignment(1, 0)
self.place.set_alignment(1, 0)
self.temperature.set_alignment(1, 0)
self.time.set_name("time")
self.t_follow.set_name("t_follow")
self.date.set_name("date")
self.status.set_name("status")
self.place.set_name("place")
self.temperature.set_name("temperature")
view.set_name("box")
tm.pack_start(self.time, False, False, 2)
tm.pack_start(self.t_follow, False, False, 2)
left.pack_start(tm, False, False, 0)
left.pack_start(self.date, False, False, 0)
right.pack_start(self.icon, False, False, 0)
right.pack_start(self.status, False, False, 0)
right.pack_start(self.place, False, False, 0)
right.pack_start(self.temperature, False, False, 10)
top.pack_start(left, True, True, 20)
top.pack_start(right, True, True, 20)
# Summary and Trend View
switcher = Gtk.StackSwitcher()
stack_area = Gtk.Stack()
self.forecastArea = SummaryView()
self.historyArea = SummaryView(True)
stack_area.add_titled(self.historyArea.get_view(), "history", "History")
stack_area.add_titled(self.forecastArea.get_view(), "forecast", "Forecast")
stack_area.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
stack_area.set_transition_duration(150)
stack_area.set_homogeneous(True)
switcher.set_name("toggle")
switcher.set_opacity(0.92)
switcher.set_stack(stack_area)
sw_b = Gtk.Box(spacing=10)
sw_b.pack_start(switcher, False, False, 25)
view.pack_start(top, True, True, 10)
view.pack_start(sw_b, False, False, 0)
view.pack_start(stack_area, False, False, 0)
self.overlay.add_overlay(view)
# Styling
screen = Gdk.Screen().get_default()
css_provider = Gtk.CssProvider()
css_provider.load_from_path(BASE + '/style.css')
context = Gtk.StyleContext()
context.add_provider_for_screen(screen, css_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self.set_default_size(DataStore.get_width(), DataStore.get_height())
self.set_position(Gtk.WindowPosition.CENTER)
self.connect('check-resize', lambda w: self.check_resize())
self.set_icon_from_file(BASE + "/assets/halo.svg")
self.show_all()
GObject.timeout_add_seconds(2, self.update_time)
GObject.idle_add(self.refresh)
stack_area.set_visible_child_name("forecast")
[docs] def check_resize(self):
"""
Resize the background image when window is resized
and store new screen size to db.
"""
screen = self.get_size()
if self.LW is not screen.width or self.LH is not screen.height:
self.bg.set_size_request(screen.width, screen.height)
buff = GdkPixbuf.Pixbuf.new_from_file(DataStore.get_bg_file())
buff = buff.scale_simple(screen.width, screen.height, GdkPixbuf.InterpType.BILINEAR)
self.bg.set_from_pixbuf(buff)
self.store.screen(screen.width, screen.height)
self.LW = screen.width
self.LH = screen.height
# noinspection PyUnusedLocal
[docs] def switch_city(self, widget):
"""Change the city for which weather data is displayed"""
dialog = PlaceDialog(self)
response = dialog.run()
if response == Gtk.ResponseType.OK:
if dialog.get_city() != "":
self.city = dialog.get_city()
self.refresh()
dialog.destroy()
[docs] def show_preference(self, w):
"""
Shows the Preference dialog.
:param w: Widget
"""
preference = PreferenceDialog(self)
preference.run()
self.LH = 0 # This will force redraw of background on window
preference.save_preference()
preference.destroy()
[docs] def show_about(self, w):
"""
Shows the about dialog.
:param w: Widget
"""
about = Gtk.AboutDialog(transient_for=self, modal=True)
buff = GdkPixbuf.Pixbuf.new_from_file(BASE + "/assets/halo.svg")
buff = buff.scale_simple(75, 75, GdkPixbuf.InterpType.BILINEAR)
about.set_logo(buff)
author = ["Cijo Saju"]
about.set_program_name("Halo")
about.set_license_type(Gtk.License.MIT_X11)
about.set_copyright("Copyrights © {} Cijo Saju".format(datetime.now().strftime("%Y")))
about.set_authors(author)
about.set_website("https://github.com/cijo7/Halo")
about.set_website_label("View on Github")
about.set_version(VERSION)
about.run()
about.destroy()
[docs] def fetch_weather(self, city=None, widget=None):
"""
Fetch the weather data from online endpoints and update the ui.
:param city: City for which weather is fetched.
:param widget: The GUI widget that triggered search.
"""
# If no city is specified, then detect location based on user ip.
query = "ip=auto" if city is None else "city=" + city
def not_found(err):
"""
Display an error message when the weather data for the
searched city is not found
:param err: Exception
"""
error = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR,
Gtk.ButtonsType.CLOSE, str(err))
self.clear_cursor(widget)
error.run()
error.destroy()
self.city = None
def api_error(err):
"""
Display an error when something goes wrong with
api call, like no internet connection and ask whether we should retry.
:param err: Exception
"""
error = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR,
(Gtk.STOCK_QUIT, Gtk.ResponseType.CLOSE,
Gtk.STOCK_CLEAR, Gtk.ResponseType.CANCEL,
"Try Again", Gtk.ResponseType.OK), str(err))
self.clear_cursor(widget)
r = error.run()
error.destroy()
if r == Gtk.ResponseType.OK:
self.refresh()
elif r == Gtk.ResponseType.CANCEL:
pass
else:
exit(0)
try:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as exe:
# Current weather
current = exe.submit(self.api.get_current_weather, query)
# Forecast
forecast = exe.submit(self.api.get_forecast_weather, query)
forecast_chart = exe.submit(self.api.get_forecast_weather_chart, query)
self.city, self.city_tz, self.currentWeather = current.result()
# Historic data fetched with tz returned from previous call
history = exe.submit(self.api.get_weather_history, query, self.city_tz)
history_chart = exe.submit(self.api.get_weather_history_chart, query, self.city_tz)
self.forecastWeather = forecast.result()
self.chartData = forecast_chart.result()
# Render current weather
GObject.idle_add(self.render_weather)
self.historyWeather = history.result()
self.historyChartData = history_chart.result()
# Render rest of the data
GObject.idle_add(self.forecastArea.render, self.forecastWeather, self.chartData)
GObject.idle_add(self.historyArea.render, self.historyWeather, self.historyChartData)
GObject.idle_add(self.clear_cursor, widget)
except NotFound as e:
GObject.idle_add(not_found, e)
except (APIError, RateLimitReached) as e:
GObject.idle_add(api_error, e)
[docs] def render_weather(self):
"""Update the current weather info of currently chosen city"""
if self.currentWeather is None:
return
self.icon.set_from_pixbuf(Icon.get_icon(self.currentWeather['code'], 60))
self.place.set_text(self.city)
self.status.set_text(self.currentWeather['status'].title())
self.temperature.set_text(str(int(self.currentWeather['temp'])) + "°")
self.update_time()
[docs] def refresh(self, widget=None):
"""Fetch the latest data into the ui"""
if widget is not None:
widget.set_sensitive(False)
self.busy_cursor()
thread = threading.Thread(target=self.fetch_weather, args=(self.city, widget))
thread.start()
# noinspection PyUnusedLocal
[docs] def busy_cursor(self, w=None):
watch_cursor = Gdk.Cursor(Gdk.CursorType.WATCH)
win = self.get_window()
if win:
win.set_cursor(watch_cursor)
[docs] def clear_cursor(self, widget=None):
arrow_cursor = Gdk.Cursor(Gdk.CursorType.ARROW)
win = self.get_window()
if win:
win.set_cursor(arrow_cursor)
if widget is not None:
widget.set_sensitive(True)
[docs] def update_time(self):
"""Updates the time shown as per the timezone of currently chosen city"""
dt = datetime.now(pytz.utc).astimezone(pytz.timezone(self.city_tz))
self.time.set_text(dt.strftime("%I:%M "))
self.t_follow.set_text(dt.strftime("%p"))
self.date.set_text(dt.strftime("%A, %d %B %Y"))
return True
[docs]class Halo(Gtk.Application):
def __init__(self):
Gtk.Application.__init__(self)
[docs] def do_activate(self):
MainWindow(self)
[docs] def do_startup(self):
Gtk.Application.do_startup(self)
if __name__ == '__main__':
app = Halo()
exit_status = app.run(sys.argv)
sys.exit(exit_status)