diff --git a/lib/wibox/widget/calendar.lua b/lib/wibox/widget/calendar.lua new file mode 100644 index 000000000..aabf35016 --- /dev/null +++ b/lib/wibox/widget/calendar.lua @@ -0,0 +1,385 @@ +--------------------------------------------------------------------------- +-- A calendar widget +-- +-- This module defines two widgets: a month calendar and a year calendar +-- +-- The two widgets have a `date` property, in the form of +-- a table {day=[number|nil], month=[number|nil], year=[number]}. +-- +-- The `year` widget displays the whole specified year, e.g. {year=2006}. +-- +-- The `month` widget displays the calendar for the specified month, e.g. {month=12, year=2006}, +-- highlighting the specified day if the day is provided in the date, e.g. {day=22, month=12, year=2006}. +-- +-- Cell and container styles can be overridden using the `fn_embed` callback function +-- which is called before adding the widgets to the layouts. The `fn_embed` function +-- takes three arguments, the original widget, the flag (`string` used to identified the widget) +-- and the date (`table`). +-- It returns another widget, embedding (and modifying) the original widget. +-- +--@DOC_wibox_widget_defaults_calendar_EXAMPLE@ +-- +-- @author getzze +-- @copyright 2017 getzze +-- @classmod wibox.widget.calendar +--------------------------------------------------------------------------- + + +local setmetatable = setmetatable +local string = string +local gtable = require("gears.table") +local vertical = require("wibox.layout.fixed").vertical +local grid = require("wibox.layout.grid") +local textbox = require("wibox.widget.textbox") +local bgcontainer = require("wibox.container.background") +local base = require("wibox.widget.base") +local beautiful = require("beautiful") + +local calendar = { mt = {} } + +local properties = { "date", "font", "spacing", "week_numbers", "start_sunday", "long_weekdays", "fn_embed" } + + +--- The calendar font. +-- @beautiful beautiful.calendar_font +-- @tparam string font Font of the calendar + +--- The calendar spacing. +-- @beautiful beautiful.calendar_spacing +-- @tparam number spacing Spacing of the grid (twice this value for inter-month spacing) + +--- Display the calendar week numbers. +-- @beautiful beautiful.calendar_week_numbers +-- @param boolean Display week numbers + +--- Start the week on Sunday. +-- @beautiful beautiful.calendar_start_sunday +-- @param boolean Start the week on Sunday + +--- Format the weekdays with three characters instead of two +-- @beautiful beautiful.calendar_long_weekdays +-- @param boolean Use three characters for the weekdays instead of two + +--- The calendar date. +-- +-- A table representing the date {day=[number|nil], month=[number|nil], year=[number]}. +-- +-- E.g.. {day=21, month=2, year=2005}, {month=2, year=2005}, {year=2005} +-- @tparam date table Date table. +-- @tparam number date.year Date year +-- @tparam number|nil date.month Date month +-- @tparam number|nil date.day Date day +-- @property date + +--- The calendar font. +-- +-- Choose a monospace font for a better rendering. +--@DOC_wibox_widget_calendar_font_EXAMPLE@ +-- @param[opt="Monospace 10"] string Font of the calendar +-- @property font + +--- The calendar spacing. +-- +-- The spacing between cells in the month. +-- The spacing between months in a year calendar is twice this value. +-- @param[opt=5] number Spacing of the grid +-- @property spacing + +--- Display the calendar week numbers. +-- +--@DOC_wibox_widget_calendar_week_numbers_EXAMPLE@ +-- @param[opt=false] boolean Display week numbers +-- @property week_numbers + +--- Start the week on Sunday. +-- +--@DOC_wibox_widget_calendar_start_sunday_EXAMPLE@ +-- @param[opt=false] boolean Start the week on Sunday +-- @property start_sunday + +--- Format the weekdays with three characters instead of two +-- +--@DOC_wibox_widget_calendar_long_weekdays_EXAMPLE@ +-- @param[opt=false] boolean Use three characters for the weekdays instead of two +-- @property long_weekdays + +--- The widget encapsulating function. +-- +-- Function that takes a widget, flag (`string`) and date (`table`) as argument +-- and returns a widget encapsulating the input widget. +-- +-- Default value: function (widget, flag, date) return widget end +-- +-- It is used to add a container to the grid layout and to the cells: +-- +--@DOC_wibox_widget_calendar_fn_embed_cell_EXAMPLE@ +-- @param function Function to embed the widget depending on its flag +-- @property fn_embed + + +--- Make a textbox +-- @tparam string text Text of the textbox +-- @tparam string font Font of the text +-- @tparam boolean center Center the text horizontally +-- @treturn wibox.widget.textbox +local function make_cell(text, font, center) + return base.make_widget_declarative { + markup = text, + align = center and "center" or "right", + valign = 'center', + font = font, + widget = textbox + } +end + +--- Create a grid layout with the month calendar +-- @tparam table props Table of calendar properties +-- @tparam table date Date table +-- @tparam number date.year Date year +-- @tparam number date.month Date month +-- @tparam number|nil date.day Date day +-- @treturn widget Grid layout +local function create_month(props, date) + local num_rows = 8 + local num_columns = props.week_numbers and 8 or 7 + + -- Create grid layout + local layout = base.make_widget_declarative{ + layout = grid, + expand = true, + homogeneous = true, + spacing = props.spacing, + forced_num_rows = num_rows, + forced_num_cols = num_columns, + } + + local start_row = 3 + local start_column = num_columns - 6 + local week_start = props.start_sunday and 1 or 2 + local last_day = os.date("*t", os.time{year=date.year, month=date.month+1, day=0}) + local month_days = last_day.day + local column_fday = (last_day.wday - month_days + 1 - week_start ) % 7 + + --local flags = {"header", "weekdays", "weeknumber", "normal", "focus"} + local cell_date, t, i, j, w, flag, text + + -- Header + flag = "header" + t = os.time{year=date.year, month=date.month, day=1} + if props.subtype=="monthheader" then + flag = "monthheader" + text = os.date("%B", t) + else + text = os.date("%B %Y", t) + end + w = props.fn_embed(make_cell(text, props.font, true), flag, date) + layout:add_widget_at(w, 1, 1, 1, num_columns) + + -- Days + i = start_row + j = column_fday + start_column + local current_week = nil + local drawn_weekdays = 0 + for d=1, month_days do + cell_date = {year=date.year, month=date.month, day=d} + t = os.time(cell_date) + -- Week number + if props.week_numbers then + text = os.date("%V", t) + if tonumber(text) ~= current_week then + flag = "weeknumber" + w = props.fn_embed(make_cell(text, props.font), flag, cell_date) + layout:add_widget_at(w, i, 1, 1, 1) + current_week = tonumber(text) + end + end + -- Week days + if drawn_weekdays < 7 then + flag = "weekday" + text = os.date("%a", t) + if not props.long_weekdays then + text = string.sub(text, 1, 2) + end + w = props.fn_embed(make_cell(text, props.font), flag, cell_date) + layout:add_widget_at(w, 2, j, 1, 1) + drawn_weekdays = drawn_weekdays +1 + end + -- Normal day + flag = "normal" + text = string.format("%2d", d) + -- Focus day + if date.day == d then + flag = "focus" + text = ""..text.."" + end + w = props.fn_embed(make_cell(text, props.font), flag, cell_date) + layout:add_widget_at(w, i, j, 1, 1) + + -- find next cell + i,j = layout:get_next_empty(i,j) + if j < start_column then j = start_column end + end + return props.fn_embed(layout, "month", date) +end + + +--- Create a grid layout for the year calendar +-- @tparam table props Table of year calendar properties +-- @param date Year to display (number or string) +-- @treturn widget Grid layout +local function create_year(props, date) + -- Create a grid widget with the 12 months + local in_layout = base.make_widget_declarative{ + layout = grid, + expand = true, + homogeneous = true, + spacing = 2*props.spacing, + forced_num_cols = 4, + forced_num_rows = 3, + } + + local month_date + local current_date = os.date("*t") + + for month=1,12 do + if date.year == current_date.year and month == current_date.month then + month_date = {day=current_date.day, month=current_date.month, year=current_date.year} + else + month_date = {month=month, year=date.year} + end + in_layout:add(create_month(props, month_date)) + end + + -- Create a vertical layout + local flag, text = "yearheader", string.format("%s", date.year) + local year_header = props.fn_embed(make_cell(text, props.font, true), flag, date) + local out_layout = base.make_widget_declarative{ + year_header, + in_layout, + spacing = 2*props.spacing, -- separate header from calendar grid + layout = vertical + } + return props.fn_embed(out_layout, "year", date) +end + + +--- Set the container to the current date +-- @param self Widget to update +local function fill_container(self) + local date = self._private.date + if date then + -- Create calendar grid + if self._private.type == "month" then + self._private.container:set_widget(create_month(self._private, date)) + elseif self._private.type == "year" then + self._private.container:set_widget(create_year(self._private, date)) + end + else + self._private.container:set_widget(nil) + end + self:emit_signal("widget::layout_changed") +end + + +-- Set the calendar date +function calendar:set_date(date) + if date ~= self._private.date then + self._private.date = date + -- (Re)create calendar grid + fill_container(self) + end +end + + +-- Build properties function +for _, prop in ipairs(properties) do + -- setter + if not calendar["set_" .. prop] then + calendar["set_" .. prop] = function(self, value) + if (string.sub(prop,1,3)=="fn_" and type(value) == "function") or self._private[prop] ~= value then + self._private[prop] = value + -- (Re)create calendar grid + fill_container(self) + end + end + end + -- getter + if not calendar["get_" .. prop] then + calendar["get_" .. prop] = function(self) + return self._private[prop] + end + end +end + + +--- Return a new calendar widget by type. +-- +-- @tparam string type Type of the calendar, `year` or `month` +-- @tparam table date Date of the calendar +-- @tparam number date.year Date year +-- @tparam number|nil date.month Date month +-- @tparam number|nil date.day Date day +-- @tparam[opt="Monospace 10"] string font Font of the calendar +-- @treturn widget The calendar widget +local function get_calendar(type, date, font) + local ct = bgcontainer() + local ret = base.make_widget(ct, "calendar", {enable_properties = true}) + gtable.crush(ret, calendar, true) + + ret._private.type = type + ret._private.container = ct + + -- default values + ret._private.date = date + ret._private.font = font or beautiful.calendar_font or "Monospace 10" + + ret._private.spacing = beautiful.calendar_spacing or 5 + ret._private.week_numbers = beautiful.calendar_week_numbers or false + ret._private.start_sunday = beautiful.calendar_start_sunday or false + ret._private.long_weekdays = beautiful.calendar_long_weekdays or false + ret._private.fn_embed = function (w, _) return w end + + -- header specific + ret._private.subtype = type=="year" and "monthheader" or "fullheader" + + fill_container(ret) + return ret +end + +--- A month calendar widget. +-- +-- A calendar widget is a grid containing the calendar for one month. +-- If the day is specified in the date, its cell is highlighted. +-- +--@DOC_wibox_widget_calendar_month_EXAMPLE@ +-- @tparam table date Date of the calendar +-- @tparam number date.year Date year +-- @tparam number date.month Date month +-- @tparam number|nil date.day Date day +-- @tparam[opt="Monospace 10"] string font Font of the calendar +-- @treturn widget The month calendar widget +-- @function wibox.widget.calendar.month +function calendar.month(date, font) + return get_calendar("month", date, font) +end + +--- A year calendar widget. +-- +-- A calendar widget is a grid containing the calendar for one year. +-- +--@DOC_wibox_widget_calendar_year_EXAMPLE@ +-- @tparam table date Date of the calendar +-- @tparam number date.year Date year +-- @tparam number|nil date.month Date month +-- @tparam number|nil date.day Date day +-- @tparam[opt="Monospace 10"] string font Font of the calendar +-- @treturn widget The year calendar widget +-- @function wibox.widget.calendar.year +function calendar.year(date, font) + return get_calendar("year", date, font) +end + + +return setmetatable(calendar, calendar.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/widget/init.lua b/lib/wibox/widget/init.lua index f33ce3451..04950b498 100644 --- a/lib/wibox/widget/init.lua +++ b/lib/wibox/widget/init.lua @@ -19,6 +19,7 @@ local widget = { checkbox = require("wibox.widget.checkbox"); piechart = require("wibox.widget.piechart"); slider = require("wibox.widget.slider"); + calendar = require("wibox.widget.calendar"); } setmetatable(widget, {