From 6b45e72e1bf9cad1c852099d049db5e6bd042225 Mon Sep 17 00:00:00 2001 From: Saeed Rasooli Date: Tue, 29 Dec 2015 21:00:00 +0330 Subject: [PATCH] version 3.0.0 --- .gitignore | 27 + ChangeLog | 177 + README | 52 + about | 4 + authors | 27 + authors-dialog | 6 + branch | 1 + conf/defaults/en_US.UTF-8/core.json | 63 + conf/defaults/en_US.UTF-8/hijri.json | 4 + conf/defaults/en_US.UTF-8/jalali.json | 3 + conf/defaults/en_US.UTF-8/locale.json | 3 + conf/defaults/en_US.UTF-8/pray_times.json | 13 + conf/defaults/fa_IR.UTF-8/core.json | 72 + conf/defaults/fa_IR.UTF-8/hijri.json | 4 + conf/defaults/fa_IR.UTF-8/jalali.json | 3 + conf/defaults/fa_IR.UTF-8/locale.json | 3 + conf/defaults/fa_IR.UTF-8/pray_times.json | 13 + conf/lang/ar.UTF-8.json | 18 + conf/lang/default | 1 + conf/lang/en_US.UTF-8.json | 8 + conf/lang/fa_IR.UTF-8.json | 11 + conf/logging-system.conf | 33 + conf/logging-user.conf | 56 + copyright | 674 +++ donate | 12 + icons/hicolor/16x16/apps/starcal.png | Bin 0 -> 823 bytes icons/hicolor/22x22/apps/starcal.png | Bin 0 -> 1337 bytes icons/hicolor/24x24/apps/starcal.png | Bin 0 -> 1259 bytes icons/hicolor/32x32/apps/starcal.png | Bin 0 -> 2152 bytes icons/hicolor/48x48/apps/starcal.png | Bin 0 -> 3387 bytes install | 225 + install-archlinux | 66 + install-debian | 94 + install-fedora | 98 + install-suse | 111 + install-ubuntu | 5 + license | 7 + locale.d/compile | 6 + locale.d/countries.po | 589 +++ locale.d/fa.mo | Bin 0 -> 60937 bytes locale.d/fa.po | 4001 +++++++++++++++ locale.d/gtk20.fa.po | 2044 ++++++++ locale.d/install | 14 + locale.d/make-template | 15 + natz/LICENSE | 18 + natz/__init__.py | 79 + natz/directory.py | 13 + natz/exceptions.py | 34 + natz/local.py | 99 + natz/tree.py | 59 + natz/tzfile.py | 137 + natz/tzinfo.py | 559 +++ natz/utc.py | 48 + pixmaps/README | 5 + pixmaps/applications-graphics.png | Bin 0 -> 1523 bytes pixmaps/applications-system.png | Bin 0 -> 1403 bytes pixmaps/arrow-down.png | Bin 0 -> 892 bytes pixmaps/arrow-left.png | Bin 0 -> 907 bytes pixmaps/arrow-right.png | Bin 0 -> 940 bytes pixmaps/arrow-up.png | Bin 0 -> 929 bytes pixmaps/computer.png | Bin 0 -> 1038 bytes pixmaps/empty.png | Bin 0 -> 737 bytes pixmaps/event/alarm.png | Bin 0 -> 1246 bytes pixmaps/event/birthday.png | Bin 0 -> 1150 bytes pixmaps/event/birthday2.png | Bin 0 -> 429 bytes pixmaps/event/business.png | Bin 0 -> 680 bytes pixmaps/event/education.png | Bin 0 -> 1404 bytes pixmaps/event/favorite.png | Bin 0 -> 1208 bytes pixmaps/event/holiday.png | Bin 0 -> 691 bytes pixmaps/event/important.png | Bin 0 -> 1056 bytes pixmaps/event/marriage.png | Bin 0 -> 900 bytes pixmaps/event/note.png | Bin 0 -> 806 bytes pixmaps/event/obituary.png | Bin 0 -> 909 bytes pixmaps/event/phone_call.png | Bin 0 -> 600 bytes pixmaps/event/task.png | Bin 0 -> 639 bytes pixmaps/event/university.png | Bin 0 -> 1499 bytes pixmaps/evolution-18.png | Bin 0 -> 1154 bytes pixmaps/exit.png | Bin 0 -> 1134 bytes pixmaps/firefox-18.png | Bin 0 -> 1140 bytes pixmaps/flags/ar.png | Bin 0 -> 316 bytes pixmaps/flags/flag-iq.png | Bin 0 -> 1134 bytes pixmaps/flags/flag-ir.png | Bin 0 -> 1313 bytes pixmaps/flags/flag-lb.png | Bin 0 -> 1036 bytes pixmaps/flags/flag-us.png | Bin 0 -> 1241 bytes pixmaps/gnome-web-browser-16.png | Bin 0 -> 982 bytes pixmaps/home.png | Bin 0 -> 935 bytes pixmaps/ical-32.png | Bin 0 -> 1306 bytes pixmaps/konqueror-16.png | Bin 0 -> 947 bytes pixmaps/preferences-desktop-theme.png | Bin 0 -> 752 bytes pixmaps/preferences-other.png | Bin 0 -> 1390 bytes pixmaps/preferences-plugin.png | Bin 0 -> 1133 bytes pixmaps/resize-small.png | Bin 0 -> 661 bytes pixmaps/resize.png | Bin 0 -> 679 bytes pixmaps/starcal-24.png | Bin 0 -> 1503 bytes pixmaps/starcal-dark.png | Bin 0 -> 2875 bytes pixmaps/starcal-inactive.png | Bin 0 -> 2471 bytes pixmaps/starcal.png | Bin 0 -> 3387 bytes pixmaps/sunbird-18.png | Bin 0 -> 1238 bytes pixmaps/timeline-18.png | Bin 0 -> 988 bytes pixmaps/timeline-48.png | Bin 0 -> 4412 bytes pixmaps/trash.png | Bin 0 -> 1149 bytes pixmaps/web-browser.png | Bin 0 -> 1319 bytes pixmaps/web-settings.png | Bin 0 -> 1800 bytes pixmaps/weekcal-18.png | Bin 0 -> 728 bytes pixmaps/wm/button-close-focus.png | Bin 0 -> 1228 bytes pixmaps/wm/button-close.png | Bin 0 -> 1210 bytes pixmaps/wm/button-inactive.png | Bin 0 -> 806 bytes pixmaps/wm/button-max-focus.png | Bin 0 -> 1249 bytes pixmaps/wm/button-max.png | Bin 0 -> 1220 bytes pixmaps/wm/button-min-focus.png | Bin 0 -> 1043 bytes pixmaps/wm/button-min.png | Bin 0 -> 1203 bytes plugins/README | 7 + plugins/__init__.py | 0 plugins/holidays-iran.hol | 53 + plugins/holidays-iran.json | 122 + plugins/iran-ancient-data.txt | 76 + plugins/iran-ancient.json | 16 + plugins/iran-ancient.spg | 18 + plugins/iran-gregorian-2-data.txt | 41 + plugins/iran-gregorian-2.json | 17 + plugins/iran-gregorian-2.spg | 21 + plugins/iran-gregorian-data.txt | 12 + plugins/iran-gregorian.json | 17 + plugins/iran-gregorian.spg | 20 + plugins/iran-hijri-2-data.txt | 32 + plugins/iran-hijri-2.json | 15 + plugins/iran-hijri-2.spg | 19 + plugins/iran-hijri-data.txt | 78 + plugins/iran-hijri.json | 17 + plugins/iran-hijri.spg | 22 + plugins/iran-jalali-2-data.txt | 63 + plugins/iran-jalali-2.json | 17 + plugins/iran-jalali-2.spg | 21 + plugins/iran-jalali-data.txt | 137 + plugins/iran-jalali.json | 17 + plugins/iran-jalali.spg | 20 + plugins/pray_times.json | 16 + plugins/pray_times_files/__init__.py | 0 plugins/pray_times_files/locations.txt | 4189 ++++++++++++++++ plugins/pray_times_files/pray_times.py | 352 ++ .../pray_times_files/pray_times_backend.py | 315 ++ plugins/pray_times_files/pray_times_gtk.py | 491 ++ plugins/pray_times_files/pray_times_utils.py | 35 + requirements.txt | 0 scal3/__init__.py | 1 + scal3/account/__init__.py | 0 scal3/account/google.py | 554 +++ scal3/bin_heap.py | 166 + scal3/cal_types/__init__.py | 138 + scal3/cal_types/ethiopian.py | 98 + scal3/cal_types/gregorian.py | 98 + scal3/cal_types/gregorian_proleptic.py | 141 + scal3/cal_types/hijri-monthes.json | 19 + scal3/cal_types/hijri.py | 243 + scal3/cal_types/indian_national.py | 186 + scal3/cal_types/jalali.py | 210 + scal3/cal_types/julian.py | 91 + scal3/cal_types/modules.list | 6 + scal3/color_utils.py | 74 + scal3/core.py | 611 +++ scal3/date_utils.py | 81 + scal3/event_diff.py | 86 + scal3/event_lib.py | 4364 +++++++++++++++++ scal3/event_search_tree.py | 325 ++ scal3/export.py | 205 + scal3/format_time.py | 381 ++ scal3/get_version.py | 13 + scal3/graph_utils.py | 43 + scal3/ics.py | 157 + scal3/import_config_2to3.py | 455 ++ scal3/interval_utils.py | 164 + scal3/json_utils.py | 122 + scal3/lib/__init__.py | 5 + scal3/locale_man.py | 435 ++ scal3/lockfile.py | 73 + scal3/monthcal.py | 172 + scal3/mywidgets/__init__.py | 0 scal3/mywidgets/multi_spin.py | 212 + scal3/os_utils.py | 149 + scal3/path.py | 77 + scal3/plugin_api.py | 54 + scal3/plugin_man.py | 703 +++ scal3/s_object.py | 278 ++ scal3/season.py | 53 + scal3/show_object.py | 22 + scal3/startup.py | 63 + scal3/time_line_tree.py | 209 + scal3/time_utils.py | 278 ++ scal3/timeline.py | 372 ++ scal3/timeline_box.py | 233 + scal3/ui.py | 976 ++++ scal3/ui_gtk/__init__.py | 46 + scal3/ui_gtk/about.py | 29 + scal3/ui_gtk/adjust_dtime.py | 236 + scal3/ui_gtk/app_info.py | 34 + scal3/ui_gtk/arch-enable-locale.py | 41 + scal3/ui_gtk/buffer.py | 16 + scal3/ui_gtk/cal_base.py | 175 + scal3/ui_gtk/color_utils.py | 17 + scal3/ui_gtk/customize.py | 144 + scal3/ui_gtk/customize_dialog.py | 251 + scal3/ui_gtk/day_info.py | 136 + scal3/ui_gtk/decorators.py | 12 + scal3/ui_gtk/desktop.py | 133 + scal3/ui_gtk/dnd.py | 38 + scal3/ui_gtk/drawing.py | 351 ++ scal3/ui_gtk/event/__init__.py | 47 + scal3/ui_gtk/event/account/__init__.py | 138 + scal3/ui_gtk/event/account/google.py | 26 + scal3/ui_gtk/event/account_op.py | 81 + scal3/ui_gtk/event/bulk_edit.py | 191 + scal3/ui_gtk/event/bulk_save_timezone.py | 96 + scal3/ui_gtk/event/common.py | 526 ++ scal3/ui_gtk/event/editor.py | 130 + scal3/ui_gtk/event/event/__init__.py | 13 + scal3/ui_gtk/event/event/allDayTask.py | 119 + scal3/ui_gtk/event/event/custom.py | 184 + scal3/ui_gtk/event/event/dailyNote.py | 39 + scal3/ui_gtk/event/event/largeScale.py | 91 + scal3/ui_gtk/event/event/lifeTime.py | 89 + scal3/ui_gtk/event/event/monthly.py | 119 + scal3/ui_gtk/event/event/task.py | 128 + scal3/ui_gtk/event/event/universityClass.py | 199 + scal3/ui_gtk/event/event/universityExam.py | 177 + scal3/ui_gtk/event/event/weekly.py | 118 + scal3/ui_gtk/event/event/yearly.py | 110 + scal3/ui_gtk/event/export.py | 170 + scal3/ui_gtk/event/group/__init__.py | 0 scal3/ui_gtk/event/group/base.py | 166 + scal3/ui_gtk/event/group/editor.py | 90 + scal3/ui_gtk/event/group/group.py | 90 + scal3/ui_gtk/event/group/largeScale.py | 57 + scal3/ui_gtk/event/group/lifeTime.py | 25 + scal3/ui_gtk/event/group/noteBook.py | 9 + scal3/ui_gtk/event/group/taskList.py | 31 + scal3/ui_gtk/event/group/universityTerm.py | 480 ++ scal3/ui_gtk/event/group/vcs.py | 45 + scal3/ui_gtk/event/group/vcsBase.py | 44 + scal3/ui_gtk/event/group/vcsDailyStat.py | 12 + scal3/ui_gtk/event/group/vcsEpochBase.py | 27 + scal3/ui_gtk/event/group/vcsTag.py | 33 + scal3/ui_gtk/event/group/yearly.py | 31 + scal3/ui_gtk/event/group_op.py | 94 + scal3/ui_gtk/event/import_event.py | 141 + scal3/ui_gtk/event/manager.py | 1354 +++++ scal3/ui_gtk/event/notifier/__init__.py | 0 scal3/ui_gtk/event/notifier/alarm.py | 47 + scal3/ui_gtk/event/notifier/command.py | 3 + scal3/ui_gtk/event/notifier/floatingMsg.py | 73 + scal3/ui_gtk/event/notifier/windowMsg.py | 54 + scal3/ui_gtk/event/occurrence_view.py | 309 ++ scal3/ui_gtk/event/rule/__init__.py | 34 + scal3/ui_gtk/event/rule/cycleDays.py | 2 + scal3/ui_gtk/event/rule/cycleLen.py | 32 + scal3/ui_gtk/event/rule/cycleWeeks.py | 2 + scal3/ui_gtk/event/rule/date.py | 22 + scal3/ui_gtk/event/rule/dateTime.py | 34 + scal3/ui_gtk/event/rule/day.py | 19 + scal3/ui_gtk/event/rule/dayTime.py | 19 + scal3/ui_gtk/event/rule/dayTimeRange.py | 30 + scal3/ui_gtk/event/rule/duration.py | 19 + scal3/ui_gtk/event/rule/end.py | 1 + scal3/ui_gtk/event/rule/ex_dates.py | 171 + scal3/ui_gtk/event/rule/ex_day.py | 1 + scal3/ui_gtk/event/rule/ex_month.py | 1 + scal3/ui_gtk/event/rule/ex_year.py | 1 + scal3/ui_gtk/event/rule/month.py | 41 + scal3/ui_gtk/event/rule/numberSimpleRule.py | 17 + scal3/ui_gtk/event/rule/start.py | 1 + scal3/ui_gtk/event/rule/weekDay.py | 41 + scal3/ui_gtk/event/rule/weekMonth.py | 45 + scal3/ui_gtk/event/rule/weekNumMode.py | 24 + scal3/ui_gtk/event/rule/year.py | 22 + scal3/ui_gtk/event/search_events.py | 560 +++ scal3/ui_gtk/event/tags.py | 269 + scal3/ui_gtk/event/trash.py | 60 + scal3/ui_gtk/event/utils.py | 39 + scal3/ui_gtk/export.py | 223 + scal3/ui_gtk/font_utils.py | 39 + scal3/ui_gtk/gtk_ud.py | 262 + scal3/ui_gtk/hijri.py | 259 + scal3/ui_gtk/import_config_2to3.py | 126 + scal3/ui_gtk/listener.py | 49 + scal3/ui_gtk/mainwin_items/__init__.py | 15 + scal3/ui_gtk/mainwin_items/dayCal.py | 317 ++ scal3/ui_gtk/mainwin_items/eventDayView.py | 35 + scal3/ui_gtk/mainwin_items/labelBox.py | 482 ++ scal3/ui_gtk/mainwin_items/monthCal.py | 586 +++ scal3/ui_gtk/mainwin_items/pluginsText.py | 73 + scal3/ui_gtk/mainwin_items/seasonPBar.py | 35 + scal3/ui_gtk/mainwin_items/statusBar.py | 48 + scal3/ui_gtk/mainwin_items/toolbar.py | 34 + scal3/ui_gtk/mainwin_items/weekCal.py | 976 ++++ scal3/ui_gtk/mainwin_items/winContronller.py | 150 + scal3/ui_gtk/mywidgets/__init__.py | 167 + scal3/ui_gtk/mywidgets/button.py | 57 + scal3/ui_gtk/mywidgets/cal_type_combo.py | 19 + scal3/ui_gtk/mywidgets/clock.py | 136 + scal3/ui_gtk/mywidgets/datelabel.py | 42 + scal3/ui_gtk/mywidgets/dialog.py | 27 + scal3/ui_gtk/mywidgets/direction_combo.py | 25 + scal3/ui_gtk/mywidgets/floatingMsg.py | 247 + scal3/ui_gtk/mywidgets/font_family_combo.py | 54 + scal3/ui_gtk/mywidgets/icon.py | 88 + scal3/ui_gtk/mywidgets/month_combo.py | 38 + scal3/ui_gtk/mywidgets/multi_spin/__init__.py | 263 + scal3/ui_gtk/mywidgets/multi_spin/date.py | 31 + .../ui_gtk/mywidgets/multi_spin/date_time.py | 41 + scal3/ui_gtk/mywidgets/multi_spin/day.py | 11 + .../ui_gtk/mywidgets/multi_spin/float_num.py | 13 + .../mywidgets/multi_spin/hour_minute.py | 27 + scal3/ui_gtk/mywidgets/multi_spin/integer.py | 16 + .../multi_spin/option_box/__init__.py | 64 + .../mywidgets/multi_spin/option_box/date.py | 24 + .../multi_spin/option_box/hour_minute.py | 27 + scal3/ui_gtk/mywidgets/multi_spin/tests.py | 38 + scal3/ui_gtk/mywidgets/multi_spin/time_b.py | 30 + scal3/ui_gtk/mywidgets/multi_spin/timer.py | 56 + scal3/ui_gtk/mywidgets/multi_spin/year.py | 11 + .../ui_gtk/mywidgets/multi_spin/year_month.py | 22 + scal3/ui_gtk/mywidgets/num_ranges_entry.py | 183 + scal3/ui_gtk/mywidgets/resize_button.py | 33 + scal3/ui_gtk/mywidgets/text_widgets.py | 78 + scal3/ui_gtk/mywidgets/tz_combo.py | 70 + scal3/ui_gtk/mywidgets/weekday_combo.py | 22 + scal3/ui_gtk/mywidgets/ymd.py | 68 + scal3/ui_gtk/player.py | 388 ++ scal3/ui_gtk/pref_utils.py | 755 +++ scal3/ui_gtk/preferences.py | 1224 +++++ scal3/ui_gtk/selectdate.py | 187 + scal3/ui_gtk/simplemonthcal.py | 0 scal3/ui_gtk/starcal-static.py | 1 + scal3/ui_gtk/starcal.py | 1054 ++++ scal3/ui_gtk/starcal_appindicator.py | 105 + scal3/ui_gtk/timeline.py | 573 +++ scal3/ui_gtk/timeline_box.py | 138 + scal3/ui_gtk/tinycal.py | 138 + scal3/ui_gtk/toolbar.py | 207 + scal3/ui_gtk/tree_utils.py | 7 + scal3/ui_gtk/utils.py | 245 + scal3/ui_gtk/windows.py | 10 + scal3/ui_gtk/wizard.py | 53 + scal3/ui_gtk/year_wheel.py | 316 ++ scal3/utils.py | 331 ++ scal3/vcs_modules/__init__.py | 45 + scal3/vcs_modules/bzr.py | 201 + scal3/vcs_modules/git.py | 212 + scal3/vcs_modules/hg.py | 142 + scal3/weekcal.py | 58 + scal3/windows.py | 19 + scal3/xml_utils.py | 31 + scripts/assert_python3 | 14 + scripts/bson_to_json.py | 16 + scripts/compact_json.py | 8 + scripts/compact_json_all | 5 + scripts/json_to_bson.py | 17 + scripts/load-zoneinfo-tree.py | 20 + scripts/post_install.py | 52 + scripts/pre_remove.py | 21 + scripts/pretty_json.py | 8 + scripts/pretty_json_all | 5 + scripts/py2json.py | 31 + scripts/run | 12 + scripts/run.pyw | 7 + scripts/run_abs | 10 + setup.py | 10 + starcal | 2 + starcal.pyw | 3 + status-icons/ambiance-green.svg | 88 + status-icons/ambiance-red.svg | 88 + status-icons/ambiance.svg | 79 + status-icons/black-circle.svg | 150 + status-icons/dark-blue.svg | 184 + status-icons/dark-green.svg | 184 + status-icons/dark-red.svg | 251 + status-icons/radiance-green.svg | 88 + status-icons/radiance-red.svg | 88 + status-icons/radiance.svg | 79 + status-icons/ubuntu-dark.svg | 131 + status-icons/ubuntu-light.svg | 131 + status-icons/white-blue.svg | 178 + status-icons/white-green.svg | 181 + status-icons/white-red.svg | 178 + svg/color-check.svg | 65 + svg/dnd-date.svg | 51 + svg/dnd-font.svg | 72 + tools/kalzium-elements-discovery.py | 73 + tools/wikipedia-fa-events/1-download.py | 40 + tools/wikipedia-fa-events/2-parse.py | 104 + .../wikipedia-fa-events/3-starcal2-import.py | 72 + uninstall | 82 + update-perm | 33 + wsgi.py | 307 ++ zoneinfo-tree.json | 569 +++ 394 files changed, 54422 insertions(+) create mode 100644 .gitignore create mode 100644 ChangeLog create mode 100644 README create mode 100644 about create mode 100644 authors create mode 100644 authors-dialog create mode 100644 branch create mode 100644 conf/defaults/en_US.UTF-8/core.json create mode 100644 conf/defaults/en_US.UTF-8/hijri.json create mode 100644 conf/defaults/en_US.UTF-8/jalali.json create mode 100644 conf/defaults/en_US.UTF-8/locale.json create mode 100644 conf/defaults/en_US.UTF-8/pray_times.json create mode 100644 conf/defaults/fa_IR.UTF-8/core.json create mode 100644 conf/defaults/fa_IR.UTF-8/hijri.json create mode 100644 conf/defaults/fa_IR.UTF-8/jalali.json create mode 100644 conf/defaults/fa_IR.UTF-8/locale.json create mode 100644 conf/defaults/fa_IR.UTF-8/pray_times.json create mode 100644 conf/lang/ar.UTF-8.json create mode 100644 conf/lang/default create mode 100644 conf/lang/en_US.UTF-8.json create mode 100644 conf/lang/fa_IR.UTF-8.json create mode 100644 conf/logging-system.conf create mode 100644 conf/logging-user.conf create mode 100644 copyright create mode 100644 donate create mode 100644 icons/hicolor/16x16/apps/starcal.png create mode 100644 icons/hicolor/22x22/apps/starcal.png create mode 100644 icons/hicolor/24x24/apps/starcal.png create mode 100644 icons/hicolor/32x32/apps/starcal.png create mode 100644 icons/hicolor/48x48/apps/starcal.png create mode 100755 install create mode 100755 install-archlinux create mode 100755 install-debian create mode 100755 install-fedora create mode 100755 install-suse create mode 100755 install-ubuntu create mode 100644 license create mode 100755 locale.d/compile create mode 100644 locale.d/countries.po create mode 100644 locale.d/fa.mo create mode 100644 locale.d/fa.po create mode 100644 locale.d/gtk20.fa.po create mode 100755 locale.d/install create mode 100755 locale.d/make-template create mode 100644 natz/LICENSE create mode 100644 natz/__init__.py create mode 100644 natz/directory.py create mode 100644 natz/exceptions.py create mode 100644 natz/local.py create mode 100644 natz/tree.py create mode 100644 natz/tzfile.py create mode 100644 natz/tzinfo.py create mode 100644 natz/utc.py create mode 100644 pixmaps/README create mode 100644 pixmaps/applications-graphics.png create mode 100644 pixmaps/applications-system.png create mode 100644 pixmaps/arrow-down.png create mode 100644 pixmaps/arrow-left.png create mode 100644 pixmaps/arrow-right.png create mode 100644 pixmaps/arrow-up.png create mode 100644 pixmaps/computer.png create mode 100644 pixmaps/empty.png create mode 100644 pixmaps/event/alarm.png create mode 100644 pixmaps/event/birthday.png create mode 100644 pixmaps/event/birthday2.png create mode 100644 pixmaps/event/business.png create mode 100644 pixmaps/event/education.png create mode 100644 pixmaps/event/favorite.png create mode 100644 pixmaps/event/holiday.png create mode 100644 pixmaps/event/important.png create mode 100644 pixmaps/event/marriage.png create mode 100644 pixmaps/event/note.png create mode 100644 pixmaps/event/obituary.png create mode 100644 pixmaps/event/phone_call.png create mode 100644 pixmaps/event/task.png create mode 100644 pixmaps/event/university.png create mode 100644 pixmaps/evolution-18.png create mode 100644 pixmaps/exit.png create mode 100644 pixmaps/firefox-18.png create mode 100644 pixmaps/flags/ar.png create mode 100644 pixmaps/flags/flag-iq.png create mode 100644 pixmaps/flags/flag-ir.png create mode 100644 pixmaps/flags/flag-lb.png create mode 100644 pixmaps/flags/flag-us.png create mode 100644 pixmaps/gnome-web-browser-16.png create mode 100644 pixmaps/home.png create mode 100644 pixmaps/ical-32.png create mode 100644 pixmaps/konqueror-16.png create mode 100644 pixmaps/preferences-desktop-theme.png create mode 100644 pixmaps/preferences-other.png create mode 100644 pixmaps/preferences-plugin.png create mode 100644 pixmaps/resize-small.png create mode 100644 pixmaps/resize.png create mode 100644 pixmaps/starcal-24.png create mode 100644 pixmaps/starcal-dark.png create mode 100644 pixmaps/starcal-inactive.png create mode 100644 pixmaps/starcal.png create mode 100644 pixmaps/sunbird-18.png create mode 100644 pixmaps/timeline-18.png create mode 100644 pixmaps/timeline-48.png create mode 100644 pixmaps/trash.png create mode 100644 pixmaps/web-browser.png create mode 100644 pixmaps/web-settings.png create mode 100644 pixmaps/weekcal-18.png create mode 100644 pixmaps/wm/button-close-focus.png create mode 100644 pixmaps/wm/button-close.png create mode 100644 pixmaps/wm/button-inactive.png create mode 100644 pixmaps/wm/button-max-focus.png create mode 100644 pixmaps/wm/button-max.png create mode 100644 pixmaps/wm/button-min-focus.png create mode 100644 pixmaps/wm/button-min.png create mode 100644 plugins/README create mode 100644 plugins/__init__.py create mode 100644 plugins/holidays-iran.hol create mode 100644 plugins/holidays-iran.json create mode 100644 plugins/iran-ancient-data.txt create mode 100644 plugins/iran-ancient.json create mode 100644 plugins/iran-ancient.spg create mode 100644 plugins/iran-gregorian-2-data.txt create mode 100644 plugins/iran-gregorian-2.json create mode 100644 plugins/iran-gregorian-2.spg create mode 100644 plugins/iran-gregorian-data.txt create mode 100644 plugins/iran-gregorian.json create mode 100644 plugins/iran-gregorian.spg create mode 100644 plugins/iran-hijri-2-data.txt create mode 100644 plugins/iran-hijri-2.json create mode 100644 plugins/iran-hijri-2.spg create mode 100644 plugins/iran-hijri-data.txt create mode 100644 plugins/iran-hijri.json create mode 100644 plugins/iran-hijri.spg create mode 100644 plugins/iran-jalali-2-data.txt create mode 100644 plugins/iran-jalali-2.json create mode 100644 plugins/iran-jalali-2.spg create mode 100644 plugins/iran-jalali-data.txt create mode 100644 plugins/iran-jalali.json create mode 100644 plugins/iran-jalali.spg create mode 100644 plugins/pray_times.json create mode 100644 plugins/pray_times_files/__init__.py create mode 100644 plugins/pray_times_files/locations.txt create mode 100644 plugins/pray_times_files/pray_times.py create mode 100644 plugins/pray_times_files/pray_times_backend.py create mode 100644 plugins/pray_times_files/pray_times_gtk.py create mode 100644 plugins/pray_times_files/pray_times_utils.py create mode 100644 requirements.txt create mode 100644 scal3/__init__.py create mode 100644 scal3/account/__init__.py create mode 100644 scal3/account/google.py create mode 100644 scal3/bin_heap.py create mode 100644 scal3/cal_types/__init__.py create mode 100644 scal3/cal_types/ethiopian.py create mode 100644 scal3/cal_types/gregorian.py create mode 100644 scal3/cal_types/gregorian_proleptic.py create mode 100644 scal3/cal_types/hijri-monthes.json create mode 100644 scal3/cal_types/hijri.py create mode 100644 scal3/cal_types/indian_national.py create mode 100644 scal3/cal_types/jalali.py create mode 100644 scal3/cal_types/julian.py create mode 100644 scal3/cal_types/modules.list create mode 100644 scal3/color_utils.py create mode 100644 scal3/core.py create mode 100644 scal3/date_utils.py create mode 100644 scal3/event_diff.py create mode 100644 scal3/event_lib.py create mode 100644 scal3/event_search_tree.py create mode 100644 scal3/export.py create mode 100644 scal3/format_time.py create mode 100755 scal3/get_version.py create mode 100644 scal3/graph_utils.py create mode 100644 scal3/ics.py create mode 100755 scal3/import_config_2to3.py create mode 100644 scal3/interval_utils.py create mode 100644 scal3/json_utils.py create mode 100644 scal3/lib/__init__.py create mode 100644 scal3/locale_man.py create mode 100644 scal3/lockfile.py create mode 100644 scal3/monthcal.py create mode 100644 scal3/mywidgets/__init__.py create mode 100644 scal3/mywidgets/multi_spin.py create mode 100644 scal3/os_utils.py create mode 100644 scal3/path.py create mode 100644 scal3/plugin_api.py create mode 100644 scal3/plugin_man.py create mode 100644 scal3/s_object.py create mode 100644 scal3/season.py create mode 100644 scal3/show_object.py create mode 100644 scal3/startup.py create mode 100644 scal3/time_line_tree.py create mode 100644 scal3/time_utils.py create mode 100644 scal3/timeline.py create mode 100644 scal3/timeline_box.py create mode 100644 scal3/ui.py create mode 100644 scal3/ui_gtk/__init__.py create mode 100644 scal3/ui_gtk/about.py create mode 100755 scal3/ui_gtk/adjust_dtime.py create mode 100644 scal3/ui_gtk/app_info.py create mode 100755 scal3/ui_gtk/arch-enable-locale.py create mode 100644 scal3/ui_gtk/buffer.py create mode 100644 scal3/ui_gtk/cal_base.py create mode 100644 scal3/ui_gtk/color_utils.py create mode 100644 scal3/ui_gtk/customize.py create mode 100644 scal3/ui_gtk/customize_dialog.py create mode 100644 scal3/ui_gtk/day_info.py create mode 100644 scal3/ui_gtk/decorators.py create mode 100644 scal3/ui_gtk/desktop.py create mode 100644 scal3/ui_gtk/dnd.py create mode 100644 scal3/ui_gtk/drawing.py create mode 100644 scal3/ui_gtk/event/__init__.py create mode 100644 scal3/ui_gtk/event/account/__init__.py create mode 100644 scal3/ui_gtk/event/account/google.py create mode 100644 scal3/ui_gtk/event/account_op.py create mode 100644 scal3/ui_gtk/event/bulk_edit.py create mode 100644 scal3/ui_gtk/event/bulk_save_timezone.py create mode 100644 scal3/ui_gtk/event/common.py create mode 100644 scal3/ui_gtk/event/editor.py create mode 100644 scal3/ui_gtk/event/event/__init__.py create mode 100644 scal3/ui_gtk/event/event/allDayTask.py create mode 100644 scal3/ui_gtk/event/event/custom.py create mode 100644 scal3/ui_gtk/event/event/dailyNote.py create mode 100644 scal3/ui_gtk/event/event/largeScale.py create mode 100644 scal3/ui_gtk/event/event/lifeTime.py create mode 100644 scal3/ui_gtk/event/event/monthly.py create mode 100644 scal3/ui_gtk/event/event/task.py create mode 100644 scal3/ui_gtk/event/event/universityClass.py create mode 100644 scal3/ui_gtk/event/event/universityExam.py create mode 100644 scal3/ui_gtk/event/event/weekly.py create mode 100644 scal3/ui_gtk/event/event/yearly.py create mode 100644 scal3/ui_gtk/event/export.py create mode 100644 scal3/ui_gtk/event/group/__init__.py create mode 100644 scal3/ui_gtk/event/group/base.py create mode 100644 scal3/ui_gtk/event/group/editor.py create mode 100644 scal3/ui_gtk/event/group/group.py create mode 100644 scal3/ui_gtk/event/group/largeScale.py create mode 100644 scal3/ui_gtk/event/group/lifeTime.py create mode 100644 scal3/ui_gtk/event/group/noteBook.py create mode 100644 scal3/ui_gtk/event/group/taskList.py create mode 100644 scal3/ui_gtk/event/group/universityTerm.py create mode 100644 scal3/ui_gtk/event/group/vcs.py create mode 100644 scal3/ui_gtk/event/group/vcsBase.py create mode 100644 scal3/ui_gtk/event/group/vcsDailyStat.py create mode 100644 scal3/ui_gtk/event/group/vcsEpochBase.py create mode 100644 scal3/ui_gtk/event/group/vcsTag.py create mode 100644 scal3/ui_gtk/event/group/yearly.py create mode 100644 scal3/ui_gtk/event/group_op.py create mode 100644 scal3/ui_gtk/event/import_event.py create mode 100644 scal3/ui_gtk/event/manager.py create mode 100644 scal3/ui_gtk/event/notifier/__init__.py create mode 100644 scal3/ui_gtk/event/notifier/alarm.py create mode 100644 scal3/ui_gtk/event/notifier/command.py create mode 100644 scal3/ui_gtk/event/notifier/floatingMsg.py create mode 100644 scal3/ui_gtk/event/notifier/windowMsg.py create mode 100644 scal3/ui_gtk/event/occurrence_view.py create mode 100644 scal3/ui_gtk/event/rule/__init__.py create mode 100644 scal3/ui_gtk/event/rule/cycleDays.py create mode 100644 scal3/ui_gtk/event/rule/cycleLen.py create mode 100644 scal3/ui_gtk/event/rule/cycleWeeks.py create mode 100644 scal3/ui_gtk/event/rule/date.py create mode 100644 scal3/ui_gtk/event/rule/dateTime.py create mode 100644 scal3/ui_gtk/event/rule/day.py create mode 100644 scal3/ui_gtk/event/rule/dayTime.py create mode 100644 scal3/ui_gtk/event/rule/dayTimeRange.py create mode 100644 scal3/ui_gtk/event/rule/duration.py create mode 100644 scal3/ui_gtk/event/rule/end.py create mode 100644 scal3/ui_gtk/event/rule/ex_dates.py create mode 100644 scal3/ui_gtk/event/rule/ex_day.py create mode 100644 scal3/ui_gtk/event/rule/ex_month.py create mode 100644 scal3/ui_gtk/event/rule/ex_year.py create mode 100644 scal3/ui_gtk/event/rule/month.py create mode 100644 scal3/ui_gtk/event/rule/numberSimpleRule.py create mode 100644 scal3/ui_gtk/event/rule/start.py create mode 100644 scal3/ui_gtk/event/rule/weekDay.py create mode 100644 scal3/ui_gtk/event/rule/weekMonth.py create mode 100644 scal3/ui_gtk/event/rule/weekNumMode.py create mode 100644 scal3/ui_gtk/event/rule/year.py create mode 100644 scal3/ui_gtk/event/search_events.py create mode 100644 scal3/ui_gtk/event/tags.py create mode 100644 scal3/ui_gtk/event/trash.py create mode 100644 scal3/ui_gtk/event/utils.py create mode 100644 scal3/ui_gtk/export.py create mode 100644 scal3/ui_gtk/font_utils.py create mode 100644 scal3/ui_gtk/gtk_ud.py create mode 100644 scal3/ui_gtk/hijri.py create mode 100755 scal3/ui_gtk/import_config_2to3.py create mode 100644 scal3/ui_gtk/listener.py create mode 100644 scal3/ui_gtk/mainwin_items/__init__.py create mode 100644 scal3/ui_gtk/mainwin_items/dayCal.py create mode 100644 scal3/ui_gtk/mainwin_items/eventDayView.py create mode 100644 scal3/ui_gtk/mainwin_items/labelBox.py create mode 100644 scal3/ui_gtk/mainwin_items/monthCal.py create mode 100644 scal3/ui_gtk/mainwin_items/pluginsText.py create mode 100644 scal3/ui_gtk/mainwin_items/seasonPBar.py create mode 100644 scal3/ui_gtk/mainwin_items/statusBar.py create mode 100644 scal3/ui_gtk/mainwin_items/toolbar.py create mode 100644 scal3/ui_gtk/mainwin_items/weekCal.py create mode 100644 scal3/ui_gtk/mainwin_items/winContronller.py create mode 100644 scal3/ui_gtk/mywidgets/__init__.py create mode 100644 scal3/ui_gtk/mywidgets/button.py create mode 100644 scal3/ui_gtk/mywidgets/cal_type_combo.py create mode 100644 scal3/ui_gtk/mywidgets/clock.py create mode 100644 scal3/ui_gtk/mywidgets/datelabel.py create mode 100644 scal3/ui_gtk/mywidgets/dialog.py create mode 100644 scal3/ui_gtk/mywidgets/direction_combo.py create mode 100644 scal3/ui_gtk/mywidgets/floatingMsg.py create mode 100644 scal3/ui_gtk/mywidgets/font_family_combo.py create mode 100644 scal3/ui_gtk/mywidgets/icon.py create mode 100644 scal3/ui_gtk/mywidgets/month_combo.py create mode 100644 scal3/ui_gtk/mywidgets/multi_spin/__init__.py create mode 100644 scal3/ui_gtk/mywidgets/multi_spin/date.py create mode 100644 scal3/ui_gtk/mywidgets/multi_spin/date_time.py create mode 100644 scal3/ui_gtk/mywidgets/multi_spin/day.py create mode 100644 scal3/ui_gtk/mywidgets/multi_spin/float_num.py create mode 100644 scal3/ui_gtk/mywidgets/multi_spin/hour_minute.py create mode 100644 scal3/ui_gtk/mywidgets/multi_spin/integer.py create mode 100644 scal3/ui_gtk/mywidgets/multi_spin/option_box/__init__.py create mode 100644 scal3/ui_gtk/mywidgets/multi_spin/option_box/date.py create mode 100644 scal3/ui_gtk/mywidgets/multi_spin/option_box/hour_minute.py create mode 100644 scal3/ui_gtk/mywidgets/multi_spin/tests.py create mode 100644 scal3/ui_gtk/mywidgets/multi_spin/time_b.py create mode 100644 scal3/ui_gtk/mywidgets/multi_spin/timer.py create mode 100644 scal3/ui_gtk/mywidgets/multi_spin/year.py create mode 100644 scal3/ui_gtk/mywidgets/multi_spin/year_month.py create mode 100644 scal3/ui_gtk/mywidgets/num_ranges_entry.py create mode 100644 scal3/ui_gtk/mywidgets/resize_button.py create mode 100644 scal3/ui_gtk/mywidgets/text_widgets.py create mode 100644 scal3/ui_gtk/mywidgets/tz_combo.py create mode 100644 scal3/ui_gtk/mywidgets/weekday_combo.py create mode 100644 scal3/ui_gtk/mywidgets/ymd.py create mode 100644 scal3/ui_gtk/player.py create mode 100644 scal3/ui_gtk/pref_utils.py create mode 100644 scal3/ui_gtk/preferences.py create mode 100644 scal3/ui_gtk/selectdate.py create mode 100644 scal3/ui_gtk/simplemonthcal.py create mode 100644 scal3/ui_gtk/starcal-static.py create mode 100755 scal3/ui_gtk/starcal.py create mode 100644 scal3/ui_gtk/starcal_appindicator.py create mode 100644 scal3/ui_gtk/timeline.py create mode 100644 scal3/ui_gtk/timeline_box.py create mode 100644 scal3/ui_gtk/tinycal.py create mode 100644 scal3/ui_gtk/toolbar.py create mode 100644 scal3/ui_gtk/tree_utils.py create mode 100644 scal3/ui_gtk/utils.py create mode 100644 scal3/ui_gtk/windows.py create mode 100644 scal3/ui_gtk/wizard.py create mode 100644 scal3/ui_gtk/year_wheel.py create mode 100644 scal3/utils.py create mode 100644 scal3/vcs_modules/__init__.py create mode 100644 scal3/vcs_modules/bzr.py create mode 100644 scal3/vcs_modules/git.py create mode 100644 scal3/vcs_modules/hg.py create mode 100644 scal3/weekcal.py create mode 100644 scal3/windows.py create mode 100644 scal3/xml_utils.py create mode 100755 scripts/assert_python3 create mode 100755 scripts/bson_to_json.py create mode 100755 scripts/compact_json.py create mode 100755 scripts/compact_json_all create mode 100755 scripts/json_to_bson.py create mode 100755 scripts/load-zoneinfo-tree.py create mode 100755 scripts/post_install.py create mode 100755 scripts/pre_remove.py create mode 100755 scripts/pretty_json.py create mode 100755 scripts/pretty_json_all create mode 100755 scripts/py2json.py create mode 100755 scripts/run create mode 100755 scripts/run.pyw create mode 100755 scripts/run_abs create mode 100644 setup.py create mode 100755 starcal create mode 100644 starcal.pyw create mode 100644 status-icons/ambiance-green.svg create mode 100644 status-icons/ambiance-red.svg create mode 100644 status-icons/ambiance.svg create mode 100644 status-icons/black-circle.svg create mode 100644 status-icons/dark-blue.svg create mode 100644 status-icons/dark-green.svg create mode 100644 status-icons/dark-red.svg create mode 100644 status-icons/radiance-green.svg create mode 100644 status-icons/radiance-red.svg create mode 100644 status-icons/radiance.svg create mode 100644 status-icons/ubuntu-dark.svg create mode 100644 status-icons/ubuntu-light.svg create mode 100644 status-icons/white-blue.svg create mode 100644 status-icons/white-green.svg create mode 100644 status-icons/white-red.svg create mode 100644 svg/color-check.svg create mode 100644 svg/dnd-date.svg create mode 100644 svg/dnd-font.svg create mode 100644 tools/kalzium-elements-discovery.py create mode 100644 tools/wikipedia-fa-events/1-download.py create mode 100644 tools/wikipedia-fa-events/2-parse.py create mode 100644 tools/wikipedia-fa-events/3-starcal2-import.py create mode 100755 uninstall create mode 100755 update-perm create mode 100644 wsgi.py create mode 100644 zoneinfo-tree.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..dd317725a --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +*~ +*.py[oc] +*.pywc +.cache +.cdtproject +.cproject +.project +.pydevproject +.metadata +.settings +build +dist +debian +.hidden +*.deb +*.rpm +*.spec +*.pkg.tar.xz +PKGBUILD +ui_qt +pixmaps_qt +starcal2-qt +starcal2-qt.pyw +plugins/pray_times_files/pray_times_qt.py +google-api-python-client +oauth2client +.[Tt]rash* diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 000000000..5dd9b29ce --- /dev/null +++ b/ChangeLog @@ -0,0 +1,177 @@ +All dates are in format YYYY/MM/DD + +2011/04/?? Saeed Rasooli + * Releasing StarCalendar version 1.9.0 + * Moving to github.com + * Install scripts for Debian(-based) and ArchLinux, that builds distribution package and installs it + * Install and uninstall scripts (works for all Linux distributions, independent of packaging system) + * Changing program Unix name (short name) to starcal2, applied in package name, git repo name, filenames, ... + * Config importer (from 1.5.*) / language selector dialog on first run (that ~/.starcal2 does not exist) + * Implementing strftime with pure python codes for better locale-support and improvements + * Adding Islamic Pray Times plugin that works for every location on the earth and supports many calculating methods + * Fixing many bugs + * Adding Customize dialog and changing all main window items for it + * PyQt interface almost completed + * Better method for handling locales, default config per language, config importer from 1.5.* + * Fully internationalized, and localizable for every locale/language (localizing in progress) + * Separating config file (preferences) into several files accourding to program layers, breaking compatibily + * Too much cleaning codes, moving codes, seperating layers, new classes... + + +2010/05/07 Saeed Rasooli + * Releasing StarCalendar version 1.5.3 + * Fixing few bugs and minor changes + +2010/03/12 Saeed Rasooli + * Releasing StarCalendar version 1.5.2 + * Fixing few bugs + +2010/02/24 Saeed Rasooli + * Releasing StarCalendar version 1.5.0 + * Fixing many bugs + * Updating Persian events plugins using 1389 Iranian official calendar + * Updating Hijri (Islamic) month lengh database using 1389 Iranian official calendar + * Add item "Adjust System Time" to the tray menu + * Add support for Julain calendar + * Seperate calulation codes as seperate modules for each Jalali, Gregorian and Hijri calendar + * Add Plasmoid (Plasma Applet) as a seperate package. This is still too BUGGY + * Add Gnome Applet (temprory in the main package until version 2.0 ...) + * Support for multiple holidays in week, see Preferences -> Advanced + +2009/09/?? Saeed Rasooli + * Releasing StarCalendar version 1.4.3 + * Fixing few bugs + * Releasing StarCalendar version 1.4.2 + * Fixing few bugs + * Releasing StarCalendar version 1.4.1 + * Fixing few bugs + +2009/09/06 Saeed Rasooli + * Releasing StarCalendar version 1.4.0 + * Add support for .ics (iCalendar) files as plugins. Currently a WEEK and minial support for ics + * Change Home Page to SourceForce.net (starcal.sourceforge.net) + * Implement Holidays (and currently Iranian holidays) as Plugin + * Change setting file ~/.starcal to a folder containing file ~/.starcal/pref for prefrences and ~/.starcal/plugins for user plugins + * Add support for real PLUGINs, including external plugins(python programs or binary modules .so) and plugins with many builtin types + * Add inline window controllers on top of main window + * Ability to show date/time with custom format in the Tray (Notification Area) + * Change tray icon object from GtkStatusIcon to EggTrayIcon (GtkStatusIcon will still used if EggTrayIcon's python module not found) + * Correct week numbers(in year), dependent to the first week day (Sunday or Saturday or ...) + * Open day in Evolution(like GNOME clock) by double clicking on day(like choosing from right click menu) + * Add item "Open In Sunbird" to right click menu (if file /usr/bin/sunbird was exists) + * Add item "Open In Evolution" to right click menu (if file /usr/bin/evolution was exists) + * Drag & drop of days(between starcal and gnome-clock or gedit or every text field) + * Drag & drop fonts between buttons + * Drag & drop color from any ColorButton to the calendar(to set as background color) + * Show starcal logo in about dialog + * Save being "On Top" or no + * Shift + F10 -> like right-click + * Middle click on tray icon to copy today's date string.(like right-click on try and select "Copy Date") + * On Apply preferences, check that if one of preferences needs to restart to apply, open a dialog to restart starcal + * On right click on year/month labes, show a menu to choose year/month + * Update desktop background on calendar, when window moved (on metacity! on compize and kwin, program does not recieve any events when moving window!) + * Seperated extarday text (buttom of main window) as a new window, that synchronize it's position an size with the main window when "configure-event" received + + + +2009/06/07 Saeed Rasooli + * Releasing StarCalendar version 1.3.6 + * Enhance calculation and updating width request of year & month labels + * Enhance position of tray menu, when right click on the tray icon + * Enhance all popup menus (fixing space before items icons, adding underline accelerators) + * Add clock label in the corner of main window + * Enhance coordinate method and positions of day numbers in calendar, and make them relative to the centers of cells + * مختصات نسبی شمارهٔ روزها نسبت به مرکز(نطقهٔ وسط) باشند. تمام مختصات‌ها تغییر کرد + + +2009/05/04 Saeed Rasooli + * Releasing StarCalendar version 1.3.3 + * Fixing many TROUBLE BUGS (that was added in version 1.3.x), and many other changes + * Adding many extraday databases + + +2009/04/27 Saeed Rasooli + * Releasing StarCalendar version 1.3.0 + * Fixing many bugs, some enhancing, and many other changes + * Adding Database Manager to preferences to manage(enable, disable and move) extraday databses + * Adding database of Owghat(Islamic Pray times) for Tehran (1388 jalali) + * Previous/Next Year/Month Buttons - Go successive while mouse button is down + * Some other changes to add capability of making the program more look-like to a "Desktop Widget" + * Capability of making the calendar Transparent on GNOME desktop(not KDE yet) + * Porting CustomDayDialog from Glade to Python Code + * Writing two custom interactive GtkWidgets "TimeBox" and "DateBox", Replacing with ComboBoxEntry in the SelectDateDialog + * Completing support for Non-Jalali (Gregorian and Hijri) Calendars. Now the default calendar can be Gregorian or Hijri + * Fixing the bug of incorrect dates in tooltip of tray icon, for non-defalut calendars, when curser is not on today + * Adding font chooser to Preferences(for this application, select a custom font rather than sysntem font) + * Adding tooltip for Gregorian month label in english + +2009/03/31 Saeed Rasooli + * Releasing StarCalendar version 1.2.0 + * Fixing many bugs, some enhancing, and many other changes + * Enhancing positions of days number to expand in the calendar, for more well-looking of program + * Adding compatibility with old PyGTK versions(2.10 or even 2.8), for facility of running in Debian Etch + * Changing main dialog's toolbar from a HBox of Buttons, to a real gtk.Toolbar, for more well-looking of program + * Adding many appearance-related preferences, such as background color, border color, text colors, + * Seperating preferences to many tabs + * Adding "Export to HTML" for the selected month + * Adding 33 year algorithm for Jalali calculation. and setting as default jalali algorithm, instead of previous(2820 year) algorithm + * Seperating holidays for Jalali and Hijri (now not need to add holidays for every new year!). Checking(and some correcting) with the Iranian official calendar + * Skip taskbar for Preferences, CustomDay, SelectDate and About dialogs. + +2009/03/19 Saeed Rasooli + * Releasing StarCalendar version 1.1.0 + * Fixing many bugs, some enhancing and cleaning codes, and some other changes + * Changing and enhancing algorithms of some part, for example using cache for days information and statics for many months (that are viewing by user) + * Editing and completing extradays databases (src/extra-*.xml) using Iranian official calendar(شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران) + * Show/don't show main dialog on start + * Adding Select Date Dialog(manually select year, month and day) + * Allow the user to change icon of "Next" and "Previous" buttons + * Adding resize icon (StatusBar icon) at the right low corner, for easy resizing of window(in vertical) + * Change(and save) height of calendar, Save width of window + * Select text of custom day (below calendar) (changing from Treeview to HBox). Now the text is selectable + * Mouse scroll for previous/next week + * Defining some keys such as PageUp(previous month), PageDown(next month), End(last day of month), MenuKey(beside right Ctrl, for popup menu) + * Popup menu when right click on a day -> items "Edit Custom Day" and "Remove Custom Day" when the selected day is defined as CustomDay + * Close About Dilaog + * Changing default language to Persian + * Auto translating defaults words(defaults buttons, tray menu, ...) + * Auto setting Right to Left (RTL) for arabic and persian + +2008/12/01 to 2009/03/12 Saeed Rasooli + * Releasing StarCalendar version 1.0.0 (first release) + * Some enhancing and cleaning codes, and also many other changes + * Adding translation(locale) support, and translating to Persian(Farsi). Arabic translation will be added later + * Removing old preferences items(of jalali applet) that were not needed. Adding new preferences + * Seperating extradays database for Jalai, Hijri and Gregorian + * Adding jalali-1388 holiday + * Adding support for working with keyboard in the calendar. Arrow keys, Space(goto today), F1, +, Q, + * Changing colors and sizes + * Adding support for multiple calendars + * Adding Hijri (Islamic) calendar + * Changing from applet to windowed on tray + * Relicensing from GPLv2+ to GPLv3+ + * Renaming from "Jalali Calendar Applet" to "StarCalendar" (with unix name "starcal") + * Taking version 1.6.5 of "Jalali Calendar Applet" (applet for Gnome) + +2008/03/28 Mola Pahnadayan + * Add 1387 holiday + * Add 1387 day's name + * Correct tooltip + +2007/05/25 Mola Pahnadayan + * Correct some holiday + * Fix bug to work with debian etch + * Change to /usr/share/jalali-calendar directory + +2007/05/01 Mola Pahnadayan + * Release 1.6.2 + * Add the license into all of source files + * Remove incorrect holiday + +2007/03/31 Mola Pahnadayan + * Release 1.6.1 + * Add preferences dialog + * (widget calendar) Height detect + +2007/03/04 Mola Pahnadayan + * First release diff --git a/README b/README new file mode 100644 index 000000000..732a6dd5f --- /dev/null +++ b/README @@ -0,0 +1,52 @@ +Download latest codes via git: + $ git clone git://github.com/ilius/starcal.git + +Download latest snapshot (without git): + Open one of these links with your browser: + http://github.com/ilius/starcal/tarball/master + +Installation/uninstallation on Ubuntu: + $ sudo bash ./install-ubuntu + $ sudo apt-get remove starcal3 + +Installation/uninstallation on Debian or other Debian-based distributions: + $ sudo bash ./install-debian + $ sudo apt-get remove starcal3 + +Installation/uninstallation on ArchLinux: + $ bash ./install-archlinux + $ sudo pacman -R starcal3 + OR + $ yaourt starcal-git + $ sudo pacman -R starcal-git + +Installation/uninstallation on openSUSE: + $ sudo bash ./install-suse + $ sudo zypper remove starcal3 + +Installation/uninstallation on Fedora: + $ sudo bash ./install-fedora + $ sudo yum remove starcal3 + +Installation/uninstallation on other distributions: + $ sudo bash ./install + $ sudo /usr/share/starcal3/uninstall + +Running on Windows: + 1- Install Python 2.6 or 2.7 + 2- Install PyGTK + 3- Copy starcal source folder somewhere you want to keep + 4- Send a shortcut from file starcal.pyw (inside source folder) to you desktop + + +Home Pages: + http://ilius.github.io/starcal + +Follow Us: + To get notified on new releases, subscribe to: + https://github.com/ilius/starcal/releases.atom + + + + + diff --git a/about b/about new file mode 100644 index 000000000..5c7a90a7c --- /dev/null +++ b/about @@ -0,0 +1,4 @@ +A Perfect Calendar Program Writen in Python +Copyleft © 2008-2015 Saeed Rasooli +StarCalendar is distributed under the GNU General Public License +"I fight for the users!" (Quote from TRON) diff --git a/authors b/authors new file mode 100644 index 000000000..28bcc590b --- /dev/null +++ b/authors @@ -0,0 +1,27 @@ +Saeed Rasooli + Developer, Maintainer, Packager and Supporter + +Mola Pahnadayan + Program "Jalali Calendar Applet" + +Mehdi Bayazee + Program "Calverter.py" + +Stuart Bishop + Program pytz + +Calendar Center Council, Institute of Geophysics, University of Tehran + Iranian Event Plugins, Hijri Calculations + +Reza Moradi Ghiasabadi (www.ghiasabadi.com) + Iranian Ancient Festivals + + +Some icons taken from Oxygen Icon Theme. +Some icons taken from T-ish-Ubuntulooks Metacity Theme +Some icons taken from Gnome Evolution. +Some icons taken from crystalproject. +Some icons taken from http://183amir.github.io/persian-calendar + +Firefox and Sunbird logos are trademarks of the Mozilla Fondation. + diff --git a/authors-dialog b/authors-dialog new file mode 100644 index 000000000..49918d01a --- /dev/null +++ b/authors-dialog @@ -0,0 +1,6 @@ +Saeed Rasooli +Mola Pahnadayan +Mehdi Bayazee +Stuart Bishop +Calendar Center Council, Institute of Geophysics, University of Tehran +Reza Moradi Ghiasabadi (www.ghiasabadi.com) diff --git a/branch b/branch new file mode 100644 index 000000000..de84bc6bd --- /dev/null +++ b/branch @@ -0,0 +1 @@ +py3 diff --git a/conf/defaults/en_US.UTF-8/core.json b/conf/defaults/en_US.UTF-8/core.json new file mode 100644 index 000000000..32d8c6ba9 --- /dev/null +++ b/conf/defaults/en_US.UTF-8/core.json @@ -0,0 +1,63 @@ +{ + "allPlugList": [ + { + "enable": false, + "_file": "pray_times.py", + "show_date": false + }, + { + "enable": false, + "_file": "iran-holidays.hol", + "show_date": false + }, + { + "enable": false, + "_file": "iran-jalali.spg", + "show_date": false + }, + { + "enable": false, + "_file": "iran-gregorian.spg", + "show_date": false + }, + { + "enable": false, + "_file": "iran-hijri.spg", + "show_date": false + }, + { + "enable": false, + "_file": "iran-ancient.spg", + "show_date": false + }, + { + "enable": false, + "_file": "iran-jalali-2.spg", + "show_date": false + }, + { + "enable": false, + "_file": "iran-gregorian-2.spg", + "show_date": false + }, + { + "enable": false, + "_file": "iran-hijri-2.spg", + "show_date": false + } + ], + "plugIndex": [ + 0 + ], + "activeCalTypes": [ + "gregorian" + ], + "holidayWeekDays": [ + 0 + ], + "firstWeekDayAuto": false, + "firstWeekDay": 0, + "weekNumberModeAuto": false, + "weekNumberMode": 4, + "version": "3.0.0" +} \ No newline at end of file diff --git a/conf/defaults/en_US.UTF-8/hijri.json b/conf/defaults/en_US.UTF-8/hijri.json new file mode 100644 index 000000000..c2521aed0 --- /dev/null +++ b/conf/defaults/en_US.UTF-8/hijri.json @@ -0,0 +1,4 @@ +{ + "hijriAlg": 0, + "hijriUseDB": false +} \ No newline at end of file diff --git a/conf/defaults/en_US.UTF-8/jalali.json b/conf/defaults/en_US.UTF-8/jalali.json new file mode 100644 index 000000000..150060806 --- /dev/null +++ b/conf/defaults/en_US.UTF-8/jalali.json @@ -0,0 +1,3 @@ +{ + "jalaliAlg": 0 +} \ No newline at end of file diff --git a/conf/defaults/en_US.UTF-8/locale.json b/conf/defaults/en_US.UTF-8/locale.json new file mode 100644 index 000000000..97eede7c7 --- /dev/null +++ b/conf/defaults/en_US.UTF-8/locale.json @@ -0,0 +1,3 @@ +{ + "lang": "en_US.UTF-8" +} \ No newline at end of file diff --git a/conf/defaults/en_US.UTF-8/pray_times.json b/conf/defaults/en_US.UTF-8/pray_times.json new file mode 100644 index 000000000..b77d3769b --- /dev/null +++ b/conf/defaults/en_US.UTF-8/pray_times.json @@ -0,0 +1,13 @@ +{ + "locName": "United States/Washington", + "lat": 41.283, + "lng": -91.667, + "method": "MWL", + "shownTimeNames": [ + "fajr", + "sunrise", + "maghrib", + "midnight" + ], + "imsak": 10 +} \ No newline at end of file diff --git a/conf/defaults/fa_IR.UTF-8/core.json b/conf/defaults/fa_IR.UTF-8/core.json new file mode 100644 index 000000000..ae7c59ba3 --- /dev/null +++ b/conf/defaults/fa_IR.UTF-8/core.json @@ -0,0 +1,72 @@ +{ + "allPlugList": [ + { + "enable": true, + "show_date": false, + "_file": "iran-holidays.hol" + }, + { + "enable": true, + "show_date": false, + "_file": "iran-jalali.spg" + }, + { + "enable": true, + "show_date": false, + "_file": "iran-gregorian.spg" + }, + { + "enable": true, + "show_date": false, + "_file": "iran-hijri.spg" + }, + { + "enable": false, + "show_date": false, + "_file": "iran-ancient.spg" + }, + { + "enable": false, + "show_date": false, + "_file": "iran-jalali-2.spg" + }, + { + "enable": false, + "show_date": false, + "_file": "iran-gregorian-2.spg" + }, + { + "enable": false, + "show_date": false, + "_file": "iran-hijri-2.spg" + }, + { + "enable": false, + "show_date": false, + "_file": "pray_times.py" + } + ], + "plugIndex": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "activeCalTypes": [ + "jalali", + "gregorian" + ], + "holidayWeekDays": [ + 5 + ], + "firstWeekDayAuto": false, + "firstWeekDay": 6, + "weekNumberModeAuto": false, + "weekNumberMode": 7, + "version": "3.0.0" +} \ No newline at end of file diff --git a/conf/defaults/fa_IR.UTF-8/hijri.json b/conf/defaults/fa_IR.UTF-8/hijri.json new file mode 100644 index 000000000..805020aca --- /dev/null +++ b/conf/defaults/fa_IR.UTF-8/hijri.json @@ -0,0 +1,4 @@ +{ + "hijriAlg": 0, + "hijriUseDB": true +} \ No newline at end of file diff --git a/conf/defaults/fa_IR.UTF-8/jalali.json b/conf/defaults/fa_IR.UTF-8/jalali.json new file mode 100644 index 000000000..150060806 --- /dev/null +++ b/conf/defaults/fa_IR.UTF-8/jalali.json @@ -0,0 +1,3 @@ +{ + "jalaliAlg": 0 +} \ No newline at end of file diff --git a/conf/defaults/fa_IR.UTF-8/locale.json b/conf/defaults/fa_IR.UTF-8/locale.json new file mode 100644 index 000000000..0a0c9d9ec --- /dev/null +++ b/conf/defaults/fa_IR.UTF-8/locale.json @@ -0,0 +1,3 @@ +{ + "lang": "fa_IR.UTF-8" +} \ No newline at end of file diff --git a/conf/defaults/fa_IR.UTF-8/pray_times.json b/conf/defaults/fa_IR.UTF-8/pray_times.json new file mode 100644 index 000000000..177acf723 --- /dev/null +++ b/conf/defaults/fa_IR.UTF-8/pray_times.json @@ -0,0 +1,13 @@ +{ + "locName": "Iran/Tehran", + "lat": 35.705, + "lng": 51.4216, + "method": "Tehran", + "shownTimeNames": [ + "fajr", + "sunrise", + "maghrib", + "midnight" + ], + "imsak": 10 +} \ No newline at end of file diff --git a/conf/lang/ar.UTF-8.json b/conf/lang/ar.UTF-8.json new file mode 100644 index 000000000..e01947f57 --- /dev/null +++ b/conf/lang/ar.UTF-8.json @@ -0,0 +1,18 @@ +{ + "code": "ar.UTF-8", + "name": "Arabic", + "nativeName": "العربيه", + "fileName": "", + "flag": "ar.png", + "timeZoneList": [ + "Asia/Riyadh", + "Asia/Dubai", + "Asia/Baghdad", + "Asia/Qatar", + "Asia/Kuwait", + "Asia/Muscat", + "Asia/Damascus", + "Asia/Beirut" + ], + "rtl": true +} diff --git a/conf/lang/default b/conf/lang/default new file mode 100644 index 000000000..927508f3f --- /dev/null +++ b/conf/lang/default @@ -0,0 +1 @@ +en_US.UTF-8 diff --git a/conf/lang/en_US.UTF-8.json b/conf/lang/en_US.UTF-8.json new file mode 100644 index 000000000..b563f19c3 --- /dev/null +++ b/conf/lang/en_US.UTF-8.json @@ -0,0 +1,8 @@ +{ + "code": "en_US.UTF-8", + "name": "English", + "nativeName": "English", + "fileName": "", + "flag": "flag-us.png", + "rtl": false +} diff --git a/conf/lang/fa_IR.UTF-8.json b/conf/lang/fa_IR.UTF-8.json new file mode 100644 index 000000000..ae6ecc5b4 --- /dev/null +++ b/conf/lang/fa_IR.UTF-8.json @@ -0,0 +1,11 @@ +{ + "code": "fa_IR.UTF-8", + "name": "Persian", + "nativeName": "فارسی", + "fileName": "fa", + "flag": "flag-ir.png", + "timeZoneList": [ + "Asia/Tehran" + ], + "rtl": true +} diff --git a/conf/logging-system.conf b/conf/logging-system.conf new file mode 100644 index 000000000..e55c6cba6 --- /dev/null +++ b/conf/logging-system.conf @@ -0,0 +1,33 @@ +[loggers] +keys=root + +[handlers] +keys=file_debug,file_error + +[formatters] +keys=file + +[logger_root] +level=DEBUG +handlers= + +[handler_file_debug] +class=logging.handlers.RotatingFileHandler +level=DEBUG +formatter=file +maxBytes=2048 +backupCount=20 +args=('/var/log/starcal2/debug',) + +[handler_file_error] +class=logging.handlers.RotatingFileHandler +level=ERROR +formatter=file +maxBytes=1024 +backupCount=20 +args=('/var/log/starcal2/error',) + +[formatter_file] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s +datefmt=%Y/%m/%d %H:%M:%S + diff --git a/conf/logging-user.conf b/conf/logging-user.conf new file mode 100644 index 000000000..9a1c4ec78 --- /dev/null +++ b/conf/logging-user.conf @@ -0,0 +1,56 @@ +[loggers] +keys=root,starcal2 + +[handlers] +keys=console,console_error,file_debug,file_error + +[formatters] +keys=console,console_error,file + +[logger_root] +level=DEBUG +handlers= + +[logger_starcal2] +qualname=starcal2 +level=DEBUG +handlers=console,file_debug,file_error + +[handler_console] +class=StreamHandler +level=DEBUG +formatter=console +args=(sys.stdout,) + +[handler_console_error] +class=StreamHandler +level=ERROR +formatter=console_error +args=(sys.stderr,) + +[handler_file_debug] +class=logging.handlers.RotatingFileHandler +level=DEBUG +formatter=file +maxBytes=2048 +backupCount=20 +args=('confDir/log/debug',) + +[handler_file_error] +class=logging.handlers.RotatingFileHandler +level=ERROR +formatter=file +maxBytes=1024 +backupCount=20 +args=('confDir/log/error',) + +[formatter_console] +format=%(message)s + +[formatter_console_error] +format=ERROR: %(message)s + +[formatter_file] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s +datefmt=%Y/%m/%d %H:%M:%S + diff --git a/copyright b/copyright new file mode 100644 index 000000000..94a9ed024 --- /dev/null +++ b/copyright @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/donate b/donate new file mode 100644 index 000000000..77b712cf0 --- /dev/null +++ b/donate @@ -0,0 +1,12 @@ +Iran, Mellat Bank, Account Number: ‪3249590307 +Card Number: 6104 3370 3338 9055 +Account Owner: Saeed Rasooli + + + +ایران، بانک ملت، شمارهٔ حساب: +3249590307 +شمارهٔ کارت: +6104 3370 3338 9055 +به نام: سعید رسولی + diff --git a/icons/hicolor/16x16/apps/starcal.png b/icons/hicolor/16x16/apps/starcal.png new file mode 100644 index 0000000000000000000000000000000000000000..3938f80878c916a579308b6173b10f936fda1b92 GIT binary patch literal 823 zcmV-71IYY|P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L00hne00hnfXSg4400007bV*G`2iXT0 z6aXV87ZWwn*ZjN$l8?C`yz_@p8^|p&CyA)4=auy57=!Q z!qg{wx90q-$JC2v?D$?_BoKA}^buvgqnMR=p3BRhU-R1!mmE&|nA$QpP5JTrUpV-z zzu!oK8#$uB`}Lc8>COGcKP$uv@lix^w?zcP)F&OrtnW6M&eB#Lqc4t_zg=>Cc0iA~ zQ(r%SHoLyQ8J?Y;^Qjbo0}6$;_J84(0%I+&U%e`{Qp(;J5fCN#p3BK-Kpcm-jzilC zn@vO87+md9RTag04G8*Ev>-&0$MCqv`T2;Z*)lj7P^?RgsR+Xer2^6<`40g4-s}ef z@z$`&~DCTazw8; zK*XT+9rJmPh~OxLi0r3py$2|z@O>XoYl1+NCNV+K0|qG7lEhomG(vC)La3V-Yb~SE z2nWD%9F$UYoxxh6+8N4fhm|eHl$2$IwF{;pvvLblKGovsun?_=qP@ zV%oOl#fxh+Y9C{_s}T{J+H&>gmbMX^rljBRlPy(SS#_-plFIle(I%CjjKdpE#;m6yLomi^` z9I+O;|15+sjO_HwsdQcATwPsB+q4`E`c%8>-M@^1Z>-73BT4`O002ovPDHLkV1nHe BgDwC7 literal 0 HcmV?d00001 diff --git a/icons/hicolor/22x22/apps/starcal.png b/icons/hicolor/22x22/apps/starcal.png new file mode 100644 index 0000000000000000000000000000000000000000..20ca90557ed18b2adc0881ec4b270c2b4e780245 GIT binary patch literal 1337 zcmV-91;+Y`P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L00hne00hnfXSg4400007bV*G`2iXT0 z6agDnIkzSN00g;7L_t(I%UzVqjvL1nhQCvn?k1bV7Kao)w?q~k8<7kc2JBT9$;#Wj zN;ZCjAjmook$u+5E=c4>00VLCSeM9%lFVo{9Er^#yV;kzWWn)Z33i7 z;b-qLy;p!l&|oKqzH_3iNdh)vGs{nYc8~IWLs$2F{L2UAcQU;5{P*wYY}YL(A005- zAF#ftXx1&|>59ekB}yy0ts}TVlt_};Aiw?d=WS`q#I=r>j~9dxIC?ne?3)V)MTYM^ zHZ_P=?4OJYA@cmue<%+2Xsedua0~*Q%bMB!2}x=h#Zf1}`Tggsr{_<{$0x^#5(Mx* zptORxOD7U{!T1OfOrnS(B1(2^fG8n`$l2K`@6PU*=Hjofn}7Z7-{a$7+@tsYI$MR0 zfCx$nMk_=sG?Cx}y$^U7fp7zch+wScyTAO8;YZ`fs6?SN4HiW0P>V#A64KPLKOHk3 z4=_ffm7;fn5H*|imc9$vB!w8cTCdsGH9AwMM2WdgVvN^}Dq%1*jQ3Iw-Wj8HKx-i% z<*Zg~j852WN{qGa?TxOj)AU_;!?U>!w^E8cA5s)KlgW_r*fO8*k){!?1#K*&ToHl= zru5Drq-+~U-}i4llpA4DiaSNYY&KQi4{(_x&oZ5mC2< zMM#o_G)c%(OP(91Gs|E&L?j0Wh_sAGj_tN*IxVPbq3aCUAf@db-g~CgeFlSUCv2^~ zX}jxtoL4kWM^$w@tddTM4QZON*=$J? zjS|h9HzjQ+tjd~t3r!u=M zuqI`_?&*8s&9Y*(Y7rR#Fr7{*ih^g)o?)%UAUiu^jQHS*0(~E->z1~y$kG8K9;G24 z=B!o~U}zc$A>riYN2F;=nxPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L00hne00hnfXSg4400007bV*G`2iXT0 z6b32LuW(@i00e7EL_t(Y$E}smZyQAv$3L?>H)@+ep!5>K0jW16DkOUA zAHjh$5*H*6NE}crE*y~p672yl9N`}*>7lfUmX?OJYU|iZlhm;{-nDmkb~rdBjT+h> z`lOXsZ#3WU`@T2d8S%sQFF(7!x$*^uR~|~)+FI?bf8P7OkBNX3$N&Mtdif7A1ibjc z(hwtoyam161KWzS$STgbSUv~`nkyR!@#5-!yL4m!U+fhGLk!`9Jp)Lv9Lv$7dazMv zIp2Ns^C7Tr&R^!j*(ENVUE0sRbN(~{%a{H*lz%#;Be9BhF%!-~^2bkp;q?3S0DS%7 z_W)#r?3v9!efldG&MvXL9r5=0w*YwW<982{i2%g6%U^Z>Hn+=IuYSnnAZxAh#`49> zr_P)?^GdO=T)8rA6TJ9<+R{QP8!3k}bDfi>Icch2NF<-TD!?xln4O(ua%vpcQ3xT> zxj}145<@b`(K#4n==WnDKi=f=#>OFnBNYfs5CjfE*ahvz@=EMqLhm!%H? zk5(XUo3cNGUs6m>&M`5m@H`i#%7BLu8s8U87?8G4rK;)e=G5z>bi)HfmV+{Bn)1|D zb#SqC!w`ev(W6I%-7aA7)#3sQAtY&~5e9*h1B@{Y1~JA+DwQ#$%7`LEvFH=WJ>obHSXR2 zhib*+%~NwUTN`+;Ll6Ws8XNR_37t-#_D)2pRAY0qN4wo6OH<}fp2W88|M7tk1VKQO z7>*x*9oMypqMU-NpmV{_PKM`=VoQS|qS?C7_I8Uxq0G$848HH<`##sMU1NHB8o4*Y zLkc>b4m$I>as4mKfy3hBX&yefhg3H6b0@jGvPv@0bi){(SKaO>7B!fqF1 z3?R6>au@lZ(bgIb)*6kMeoz+PSP090i7@PzyWI|Xp5r(Ug?f!-kPyW&=gyt${|jka V00;>^eI@_^002ovPDHLkV1kcnN6!EN literal 0 HcmV?d00001 diff --git a/icons/hicolor/32x32/apps/starcal.png b/icons/hicolor/32x32/apps/starcal.png new file mode 100644 index 0000000000000000000000000000000000000000..c2fbe228ccc08443f20ae6f9c9b5c4efdb207554 GIT binary patch literal 2152 zcmV-u2$%PXP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L00hne00hnfXSg4400007bV*G`2iXT0 z6bB0()BwZ)00-zvL_t(o!@ZW@Yh_mz$3J`T^ZT5eo108BnM|~?t)+#Qwm4YYhY_n- z1RN_A(DfbEP~=TJf-Ly2&LYSbVA=Q@ACf9iQY`|XE1 z#|~(r-PNRbXJ7C(>#G6<={+Ifa1MU=!*l%e4{!0%qxbR9*Y3wThY;e}fP{eUVb0T! z{f26<<~z@SlX~17?kBfJM9Vj%SAu?3Q?{k>d=*%K%Bl{U036#Zdp!E%FR^_8BENm| z92d_ISw3@`JI*e1>B1^g3mJER?sm@q>hFv;#(eSdPw~uGpWz!n{R;h)JzhWeHpAC8 zm|DoV=aG*eQ36Li6Vtjz$^hRIFmc|kFF)}aUU~Lk{N-ma@#$}UlI1g}dHAshP)hQ* zU;l&We)MOA5Ujks#>0<2fN2foZbdqiP>&nJBse^GOwIuCy}+qQzJB)cqAolc%fszI z{p37Y=r8n`zkQa!KK(LVZ|~r&C7KFRf#ROe-@)+JtK9m)5;*WblJ zH81?)HT=k9us8(*TnT>S8S&J)?~N|*U(A#*KjgC(ALxdyAPM5eo9~@8*Jd}tIkeRu z*wFv9q(chHTsx<|@a=f@xhMbF_kEwUXV0RwexFvvJ2LS8*QJ#ehF7j|=FBJdyo1{Z zZFXF{xk*zCDUed2ltc)D5MYhPSckO^XB`4@Y$6ZsO#FE7)!*0S>v+HqBw^^|dlJu+ z2zda&p^c?&Ep@GF8r^|7k~0MI2A`fgfW+~Lr4q!kPZ%hcK5`1BAc;ey6hv`MF)k5O zp;W+lTr(b5l%-~>H+5K?rmk6AUuTr(H`#yB40O&VN#ct$J2A!P#u~M15I6>d zlxx>^vBna`J+`*CD2tY&)FeqvQB-)IA`C*ZEPXeC<7(ub!#PJ3DWb?DiWPC}F_=kd znvf_8kP>EQV)8sd8%vhPjB*d0!x~G!pD`X6I43(gKcXz&Q8fy}kW&i_^rw3a1}RY_ zi6co63VNAh;pA=jfkX|#aajNi2g_b086J& zqm9K`P1~02?rx)$VSX;<)WQki-#D=;Ql}tOtXch%gKY!vrBBoKpy4iK0o3 zWx?!hpD@%!QHv0cUN1$J7AZa4Uf0sfQ>1Bv=ldKv00KXtsw$ecMkz_%R8++nYb;86 z2oV4gLL@+d5I$0RNGT|aG5h;DJ3HGbB`L~+x~^#3wkv^yBuSZ>nZffsJm;Kn&Jic^ zbw(8eAw;KWgupq6opgn@T@)OOl27-P)wJRksTEx1l0 zSqpV-D60moHB#1ip29f;7_4om>zcaO=+>Z3_v)SL?ph0;64XsIk!g-73Xgw;P6&b^ z!Ve;>g*3^C6Ga&MlvRZj0*63JiM1B3o5?H?%+Ah}q?TT5KkNr~_K zEZ=emFP?wlXaE8ziV~$9Sr)RD@6xP}$a(>H-MP&6_6}*1qC7jistI#kj^NVQd5=9>QUO`c6oRw^D?vUpdRoT$CEi;1wgTa8dZTa`>uj70E(EzM- z^m-X#7}2(l>FGJ<=jR9mNmW}0gHw3ELKeMW_(9C&l_9;}3~l4M>#loA)AS~qAB{$=t`4!*VvIQ&K+`sd7qrpT zwWTOD#yB=M_t@GR5%?tthwo*qt!-m1#Ia^?ucB^p{2(QcW2EfNTWbMWSy`cJT7-Zw z437pdE()?N!}kD(vc=J6F~xHWl7Un>bfD%D~i%mRr`GOmgPg|2b1;k z<;#plBed2CAyG=<;ZQe@VSu;Zd=qOdagxyM^~lnce!ou`g(#^A!USV9Wl^xUw!>&N zBG31!s+u%SDa(?Dh4~4YIjpq|hgZ0Ib)B*-an53`MIi7j9e}bZ85d)G-^b|=@$T*p zyE{8LT*q8)BF_+5J7KwrlY<^tN-;Y-OBhBI%_z8Z=@J`PuTGqI65bfY#^wgzfqWD~ zaN)v*52d!3mX=(WrYtR;7FAWTy?u>5A5j!Vmv$-fg}_*g=XtbkZ9FNt_~3&No{&NTb%IH#iNuxS2C;;2*R{C2uE`e{7pou)7>)9Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L00hne00hnfXSg4400007bV*G`2iXG| z0s{vnnU{PuYe*XEF>fpcmXAn^MJ(*FBqaAgoKblavrdsEQCn}1Urf%qin)@XS1HY z%uIJrS69_{wdo7ugT6O8Jug?EF=lswAeAVKc-~F>c`rfr4{fP@+VFAsD ze-mJ|@}D{Ju`lbZ`}H5#ZLi7;)twKZj_!p5tvlqh`-JW{-QGEeMq|BeFL;jb-{n@^ zm#M9E|0{arcXNBS9n`F8%(_PC7A2zlwLUl&;=MgE?OdDQ#Kker58&&+^b!ES|MGKi z=z4SzM=*P*!tOXGrzXpKVkgSsFau3`=IG$xf8~e#>tFvDlgm>8o`2=J_vQJ$OB25J zwb!`xgDb=kkBqmN7|}lGg|jaql4FqreGsG8%d=1_t%QnNYhjj-&&;bYzRoXy^(jt$ z>;&S_)W2}{rS}Bp)fZpq{+~I`ufFgFl$P&#BC_QSS362yYOSP>$QpXbe|Y-io4oqs z>pb|m`*{4BN9eB=ym0oV1&Fy<=Eq6T@YBEeAiwatpW%hGFY)ltpW%OA``-oFtvBD{ zi(mQt5#w)k?`?BfwH{EYC^aeo$J_<*r9VB(=YRWWDR*c5!yo?3kuZAxmFE`6KlfXo zCL5$QQ_FY%=?9DZKl#N+Z|kX>-Fv%D8;z1W%2cE+Puu{2kb;kV;vOD*`eF7ij*kRm zIXYmi*Z=kne&uUV@#}x_?2-HLef;E6k3P1^fBxh5@h;r<-fWO?_59wAt(PDSXra7T zQU$Z5Z^$ll_hy5H&;9177S})i)Ijxw#=m{zzZXe-vwJ`L8=vBvzw^(y+8_6#X_-+;9mJo%_Q#XA$8YyV%Kw`!)-A2B zFRholm-M1sJL;YfeIf25F!ia|8^xx*>aH*o<%iyIk2{E>nDUtX=HvnX@iuCet((m?JG>D z71RA0b?rHI>Zca6RBMG0l3uTeHu|;z-m~#(qDfPQNffIqeVpBIb3jX^XhUeYI-sui z3BC;>>l}He386vBNSf*v=;{)Cdy`|&TV7tiyE%}U&iGW5Wtt>u&zF`8_V*21OO%pG zB@n=1kTDu31RrodU~FJ%DJRPg!VDnK6RgdcPLFnmQpmdpz!*iEs@CYF-RGGmNi}Jv zSXxR+Q@sEIQ1ldoenQju)&n|_B#JE62q4nZYmz96BBv~`Yi)1S0_s~cj3Unq)`mm! zB4;qjDT)MR1Zg5MMxeDINd< z`zU2lCPgZR6apzVWw}oXmb$7Lk1K|2C%ACo0(Ir6DoZ)DoH_FlLIn2qc5n`akR--n zj5%UcUgXTm*_{E%vy5I*FfB`@5({=&Sy^Jomn5m8*H7s86S7Q_Bn4TPVN8OQa*^~9 zkg_7iKnMY21WBU0yijNx&`RKB>%ACT&%N(Lri~^^Q)X3(lpqx-CFm6?qmiV@4ZR|v zC=5xGAZ13DrD&ZXy5x&7AcaCo1qghIZP*!wcWq)PrcHV&K}kW7HwWOIM=C||{tf_W z-7<q&|tMJa{WrfZkXp}FyZ zKsOI(?pskVw;FklHqVj(?qKJ#w%V~mUfcl4m^+~ZKFIMkMTaC z7ZQ@B02CLtF0irQBlsMpmMQWAA?7nb2LWJ#fDnopp|KVLT)ldQvTPH0I&C<8>OLZo z-Mw9$gQkfjNlKm<2jSxb!{HirT~XH!b=@Ejt-p~M`E?X5j`Q$Bp&e+KR_`2InL>1} zojP>hCZ{;EN=0`~w0b5URW-pMBtnRR7$fWJYxH`3l$3z5f18SgtV);g3@$WEEwz7h*TjIn!I zJNVpyrtvh^Q#Uogp!MyHpI6fmBU0&Z@W{%_D!qO}mL(`9(+gq)SC@p2#u(^4f@p#5`T4QYl1kQQJ<1wNe zCruL8*Vb^(y$dzk#ai1%wL(lvL1X=*$cd?CS_VdYB~{&Ev_|VT$(ELu*x4D;G;Isn zG#=+7)yy)h8tU5NY+yF4sb=+}-Q3@w(liZH2zo`q`uh696X(vIyE6ch*fJ4wcg95< z*wh{&4*Fi(L?)A(+01p-T6BuG$7E7>;605El(U9e)zH))??CI6?RTy+t1L|u2)<>e zG)dUl*dWhywANHr#opc?&bdW>xHW(n5mGW3^gFF1@gAInhaP?uty?*5w7|N^#)(s{ z++3_mcn`B#&F1E5c6LTwzPv-z%y2H!G>)aERq`xH8-r4cJkRO%dI%x# z-gEIMKf#B<>gvk%FOO~_oQz~L*+*-I)|w*E5xs;r-~1kFs>$<&qDcAZ182ClbA>cD zq=`Yuwzti*oawa0IghoDx_0DwkMrj*FdUx1+JJYkwsw*eCr(1gI@f<Fe{P~LqYeFt?&&ia% z(Uj$7XlhRgf;7$AC7By6ox~W~-rlCJDx9-j3!2^D1C6z0@u;r#!Q+Fbs#{*4SL5B? zeP*RYY0)XnR*H;9Gp3~_b|K_kV1H6k%^a7n>`*s>UT;9ZKR_yVqyvi!0`ctb?ou}m z-Uoy@&}p~zK+`nG^hsUU1m7}=_W|b=J3AA~nLV_y)k=}wy?x5kcGWgwoo8>fPu&Er zT^-?kAkT|~*Rql7uu(DT;zT%g{!XCMm`klvb^d>Eu`lo~Ev;n;L5^RaG&a zmQ+=H?wqIB>rvM=LI{S#)kO<>*!$bt@36DGL)p!K2(2KChXkOGheBvp&T!r@)*xGf zHs|>U+cfM?Chza<%Awgc#?b5aNYiY=G;_MVbmMH$ykEUsuPWKs&Ml_A3u4`=5 zEDEU-ZC?@(_3B-}(%dqMKZtF8V|_9hEYs^1?C(zomo8mGD~)xwQ=Eb<%UYU@kw_~+ zg7^5)GN9I)XP$Yc%5(Ye;^mjW@i$c!o)vZXn%1cVT=3yXODO1U_EU)QrrDN6)(o~!@(!qc07^?#U));!TB Rl#KuY002ovPDHLkV1lxVi7Eg9 literal 0 HcmV?d00001 diff --git a/install b/install new file mode 100755 index 000000000..27ffa0446 --- /dev/null +++ b/install @@ -0,0 +1,225 @@ +#!/bin/bash +UI_QT_ENABLE= + +function printUsage { + ## echo "Usage: $0 [TERGET_DIR] [--for-pkg|--portable] [--prefix=/usr/local]" + U='\033[4m' ## start Underline + E='\033[0;0;0m' ## End formatting + echo -e "Usage: $0 [${U}TERGET_DIR${E}] [--for-pkg|--portable] [--prefix=${U}/usr/local${E}]" +} + +function clean { + for EXP in '.hidden' '*~' '*.pyc' '*.pyo' '*.tar.xz' '*.deb' '*.rpm' '*.spec'; do + find . -name "$EXP" -exec rm '{}' \; + done 2>/dev/null + find . -name '__pycache__' -exec rm -R '{}' \; 2>/dev/null + find scal3 -type d -empty -delete +} + +myPath="$0" +if [ "${myPath:0:2}" == "./" ] ; then + myPath=$PWD${myPath:1} +elif [ "${myPath:0:1}" != "/" ] ; then + myPath=$PWD/$myPath +fi + +pkgName=starcal3 +sourceDir="`dirname \"$myPath\"`" +version=`$sourceDir/scal3/get_version.py` + +PY=python3 +#"$sourceDir/scripts/assert_python3" + +"$sourceDir/update-perm" ## FIXME + + +options=`getopt -o 'h' --long 'help,for-pkg,portable,prefix:' -n "$0" -- "$@"` +if [ $? != 0 ] ; then + printUsage + exit 1 +fi +eval set -- "$options" ## Note the quotes around $options are essential! +options="" + + +prefix="" +installType="" + +while true ; do + case "$1" in + --help | -h) printUsage ; exit 0 ;; + --for-pkg) installType="for-pkg" ; shift ;; + --portable) installType="portable" ; shift ;; + --prefix) prefix="$2" ; shift 2 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +targetDir="$1" ; shift +if [ -n "$1" ] ; then ## extra arguments + printUsage + exit 1 +fi + +if [ -n "$targetDir" ] ; then ## non-Root directory + n=${#targetDir} + if [ ${targetDir:n-1:1} = / ] ; then + targetDir=${targetDir::-1} + fi + mkdir -p "${targetDir}" +fi + +## do not f*** the system if a variable was empty amiss! +if [ -z $pkgName ] ; then + echo "Internal Error! pkgName=''" + exit 1 +fi +if [ -z "$prefix" ] ; then ## prefix is empty (not been set) + if [ "$installType" = "for-pkg" ] ; then + prefix=/usr + elif [ "$installType" = "portable" ] ; then + prefix=/usr/local ## FIXME + else + prefix=/usr/local + fi +else + n=${#prefix} + if [ ${prefix:n-1:1} = / ] ; then + prefix=${prefix::-1} + fi +fi + +#echo "prefix: $prefix" +#echo "installType: $installType" +#echo "targetDir: $targetDir" +#exit 0 + +targetPrefix="${targetDir}${prefix}" +shareDir="${targetPrefix}/share" + + + + + + + +mkdir -p "${shareDir}/doc" +mkdir -p "${shareDir}/applications" +mkdir -p "${shareDir}/icons" +mkdir -p "${shareDir}/pixmaps" +mkdir -p "${shareDir}/doc/$pkgName" +mkdir -p "${targetPrefix}/bin" +mkdir -p "${targetDir}/var/log/$pkgName" +mkdir -p "${targetDir}/etc" + + + +if [ -L "${shareDir}/$pkgName" ] ; then ## a symbiloc link + rm -f "${shareDir}/$pkgName" +elif [ -d "${shareDir}/$pkgName" ] ; then + rm -Rf "${shareDir}/$pkgName" +fi + +cp -Rf "$sourceDir/" "${shareDir}/$pkgName" ### PUT SLASH after $sourceDir to copy whole folder, not just a link (if was a link) + +"${shareDir}/$pkgName/update-perm" ## FIXME + +for docFile in copyright authors ChangeLog ; do + mv -f "${shareDir}/$pkgName/$docFile" "${shareDir}/doc/$pkgName/" +done + +cp -f "${shareDir}/$pkgName/donate" "${shareDir}/doc/$pkgName/" + + + +## "$sourceDir/config" is not used yet, and will not created by git yet +#if [ -e "${targetDir}/etc/$pkgName" ] ; then +# rm -Rf "${targetDir}/etc/$pkgName" ## descard old configuration? FIXME +#fi +#cp -Rf "$sourceDir/config" "${targetDir}/etc/$pkgName" + + +mkdir -p "${shareDir}/icons/hicolor" +for SZ in 16 22 24 32 48 ; do + relDir="icons/hicolor/${SZ}x${SZ}/apps" + mkdir -p "${shareDir}/$relDir" + mv -f "${shareDir}/$pkgName/$relDir/starcal.png" "${shareDir}/$relDir/$pkgName.png" +done +rm -R "${shareDir}/$pkgName/icons" + + +cp -f "$sourceDir/pixmaps/starcal.png" "${shareDir}/pixmaps/$pkgName.png" + + +if [ "$installType" = "for-pkg" ] ; then + runDirStr="/usr/share/$pkgName" +elif [ "$installType" = "portable" ] ; then + runDirStr="\"\`dirname \\\"\$0\\\"\`/../share/$pkgName" +else + runDirStr="${shareDir}/$pkgName" +fi + + +echo "#!/bin/bash +$PY $runDirStr/scal3/ui_gtk/starcal.py \"\$@\"" > "${targetPrefix}/bin/$pkgName" +chmod 755 "${targetPrefix}/bin/$pkgName" + +if [ $UI_QT_ENABLE ] ; then + echo "#!/bin/bash + $PY $runDirStr/scal3/ui_qt/starcal.py \"\$@\"" > "${targetPrefix}/bin/$pkgName-qt" + chmod 755 "${targetPrefix}/bin/$pkgName-qt" +fi + + + + + + +echo "[Desktop Entry] +Encoding=UTF-8 +Name=StarCalendar $version +GenericName=StarCalendar +Comment=A Perfect Calendar Program +Comment[fa]=یک برنامهٔ کامل تقویم +Exec=$pkgName +Icon=$pkgName +Type=Application +Terminal=false +Categories=GTK;Utility;Accessibility;Office;Calendar; +StartupNotify=true" > "${shareDir}/applications/$pkgName.desktop" + + +if [ $UI_QT_ENABLE ] ; then + echo "[Desktop Entry] +Encoding=UTF-8 +Name=StarCalendar $version (Qt) +Comment=A Perfect Calendar Program (Qt Interface) +Comment[fa]=یک برنامهٔ کامل تقویم +Exec=$pkgName-qt +Icon=$pkgName +Type=Application +Terminal=false +Categories=GTK;Utility;Accessibility;Office;Calendar; +StartupNotify=true" > "${shareDir}/applications/$pkgName-qt.desktop" +fi + + +"$sourceDir/locale.d/install" "${targetPrefix}" ## FIXME + + +## Cleaning useless files from targetDir +cd "${shareDir}/$pkgName" +rm -Rf .git .gitignore .Trash* 2>/dev/null +rm -Rf google-api-python-client/.git google-api-python-client/.hg* 2>/dev/null +clean +cd - > /dev/null + +cd "$targetDir" +clean +cd - > /dev/null + +## lib/ --> starcal3-platform-spec +## locale/ --> starcal3-region-* + + diff --git a/install-archlinux b/install-archlinux new file mode 100755 index 000000000..5631cdf0a --- /dev/null +++ b/install-archlinux @@ -0,0 +1,66 @@ +#!/bin/bash +## makes PKGUILD and builds it (without root access), then installs it (prompts for password if necessary) + +initPwd=$PWD + +myPath="$0" +if [ "${myPath:0:2}" == "./" ] ; then + myPath=$initPwd${myPath:1} +elif [ "${myPath:0:1}" != "/" ] ; then + myPath=$initPwd/$myPath +fi + + +pkgName=starcal3 +sourceDir="`dirname \"$myPath\"`" +#"$sourceDir/scripts/assert_python3" +version=`$sourceDir/scal3/get_version.py` + +tmpDir=/tmp/$pkgName-install-arch +mkdir -p $tmpDir +cd $tmpDir + +depends=('python>=3.2') +depends+=('python-gobject') ## The new gobject introspection +#depends+=('python-gflags') +depends+=('python-httplib2') +depends+=('python-psutil') +depends+=('python-dateutil') +depends+=('python-bson') + +optdepends=() +optdepends+=('python-dateutil') +optdepends+=('python-igraph') +#optdepends+=('python-gnomevfs') + + +depends_str=$(printf " '%s'" "${depends[@]}") ; depends_str=${depends_str:1} +optdepends_str=$(printf " '%s'" "${optdepends[@]}") ; optdepends_str=${optdepends_str:1} + +echo "# Contributor: Saeed Rasooli +# This is a local PKGBUILD +sourceDir='$sourceDir' +pkgname=$pkgName +pkgver=$version +pkgrel=1 +pkgdesc='A full-featured international calendar writen in Python' +arch=('any') +url=(http://ilius.github.io/starcal) +license=('GPLv3') +depends=($depends_str) +optdepends=($optdepends_str) +makedepends=() +conflicts=('starcal-git') +source=() +md5sums=() +package() { + \"\$sourceDir/install\" \"\$pkgdir\" --for-pkg +}" > PKGBUILD + +makepkg -sif + +cp $pkgName*.pkg.tar.?z "$initPwd" +echo "Package installed and copied into $initPwd directory" +cd "$initPwd" +rm -Rf $tmpDir + diff --git a/install-debian b/install-debian new file mode 100755 index 000000000..bbcbf9861 --- /dev/null +++ b/install-debian @@ -0,0 +1,94 @@ +#!/bin/bash +## This script builds DEB package on a debian-based distribution +## (like debian, ubuntu, mint, ...) and installs the package + +function getDirTotalSize(){ + du -ks "$1" | python3 -c "import sys;print(input().split('\t')[0])" +} + +if [ "$UID" != "0" ] ; then + echo "Run this script as root" + exit 1 +fi + + +myPath="$0" +if [ "${myPath:0:2}" == "./" ] ; then + myPath=$PWD${myPath:1} +elif [ "${myPath:0:1}" != "/" ] ; then + myPath=$PWD/$myPath +fi + + + +pkgName=starcal3 +sourceDir="`dirname \"$myPath\"`" +#"$sourceDir/scripts/assert_python3" +version=`$sourceDir/scal3/get_version.py` + + +tmpDir=/tmp/$pkgName-install-deb +mkdir -p $tmpDir +mkdir -p "$tmpDir/DEBIAN" + +"$sourceDir/install" "$tmpDir" "--for-pkg" +chown -R root "$tmpDir" +installedSize=`getDirTotalSize "$tmpDir"` ## only /usr ? FIXME + +#getDirTotalSize "$tmpDir" +#getDirTotalSize "$tmpDir/usr" + + +depends=('python3(>=3.2)') +depends+=('python3-gi(>=3.8)') ## The new gobject introspection +depends+=('python3-gi-cairo(>=3.8)') +## it's "python-gobject-cairo" in ubuntu FIXME +#depends+=('python3-gflags') ## FIXME +depends+=('python3-httplib2') +depends+=('python3-psutil') +depends+=('python3-bson') + +recommends=() +recommends+=('python3-dateutil') +recommends+=('python3-igraph') ## FIXME +recommends+=('python3-gnomevfs') + + +depends_str=$(printf ", %s" "${depends[@]}") ; depends_str=${depends_str:2} +recommends_str=$(printf ", %s" "${recommends[@]}") ; recommends_str=${recommends_str:2} + +echo "Package: $pkgName +Version: $version +Architecture: all +Maintainer: Saeed Rasooli +Installed-Size: $installedSize +Depends: $depends_str +Recommends: $recommends_str +Section: Utilities +Priority: optional +Homepage: http://ilius.github.io/starcal +Description: A full-featured international calendar writen in Python + StarCalendar is a full-featured international calendar writen in Python, + with both PyGTK and PyQt interfaces, that supports Jalai(Iranian), + Hijri(Islamic), and Indian National calendars, as well as common + english(Gregorian) calendar + Homepage: http://ilius.github.io/starcal +" > "$tmpDir/DEBIAN/control" + +#echo "/usr/share/$pkgName/scripts/assert_python3" > "$tmpDir/DEBIAN/postinst" +chmod 755 "$tmpDir/DEBIAN/postinst" + +pkgFile=${pkgName}_${version}-1_all.deb +dpkg-deb -b "$tmpDir" "$pkgFile" +if [ "$?" = "0" ] ; then + echo "Package file $pkgFile created, installing..." + if [ -f /usr/bin/gdebi ] ; then + /usr/bin/gdebi "$pkgFile" + else + apt-get install python3-httplib2 + dpkg -i "$pkgFile" + fi +fi + +rm -Rf "$tmpDir" + diff --git a/install-fedora b/install-fedora new file mode 100755 index 000000000..c67bd45a3 --- /dev/null +++ b/install-fedora @@ -0,0 +1,98 @@ +#!/bin/bash +## makes rpm package and installs it using yum + +## yum install @development-tools +## yum install rpm-build rpmdevtools rpmlint mock + + +if [ "$UID" != "0" ] ; then + echo "Run this script as root" + exit 1 +fi + + +myPath="$0" +if [ "${myPath:0:2}" == "./" ] ; then + myPath=$PWD${myPath:1} +elif [ "${myPath:0:1}" != "/" ] ; then + myPath=$PWD/$myPath +fi + + +pkgName=starcal3 +sourceDir="`dirname \"$myPath\"`" +#"$sourceDir/scripts/assert_python3" +version=`$sourceDir/scal3/get_version.py` + +#echo "myPath=$myPath" +#echo "sourceDir=$sourceDir" +#echo version=$version + +#%post +#/usr/share/$pkgName/scripts/assert_python3 + +requires=('python3(>=3.2)') +requires+=('python3-gobject') ## The new gobject introspection +#requires+=('python3-gflags') +requires+=('python3-httplib2') +requires+=('python3-psutil') +requires+=('python3-bson') + +#recommends=() +requires+=('python3-dateutil') +#requires+=('python3-igraph') +#requires+=('python3-gnomevfs') + + +requires_str=$(printf " %s" "${requires[@]}") ; requires_str=${requires_str:2} +#recommends_str=$(printf ", %s" "${recommends[@]}") ; recommends_str=${recommends_str:2} + + + +echo "Name: $pkgName +Version: $version +Release: 1 +Summary: A full-featured international calendar writen in Python + +Group: User Interface/Desktops +License: GPLv3+ +URL: http://ilius.github.io/starcal +Requires: $requires_str +BuildArch: noarch + +%description +StarCalendar is a full-featured international calendar writen in Python, +with both PyGTK and PyQt interfaces, that supports Jalai(Iranian), +Hijri(Islamic), and Indian National calendars, as well as common +english(Gregorian) calendar + +%install +\"$sourceDir/install\" \"%{buildroot}\" --for-pkg --prefix=%{_prefix} + +%files +%defattr(-,root,root,-) +%{_prefix}/share/$pkgName/* +%{_prefix}/bin/$pkgName* +%{_prefix}/share/applications/$pkgName* +%{_prefix}/share/doc/$pkgName/* +%{_prefix}/share/pixmaps/$pkgName.png +%{_prefix}/share/icons/hicolor/*/apps/$pkgName.png +%{_prefix}/share/locale/*/LC_MESSAGES/$pkgName.mo +" > $pkgName.spec + +pkgPath=`rpmbuild -bb $pkgName.spec | grep -o /usr/src/packages/RPMS/.*rpm` + +if [ -z $pkgPath ] ; then + exit 1 +fi + +if [ ! -f $pkgPath ] ; then + echo "Package file $pkgPath does not exit" + exit 1 +fi + +echo "Package created in \"$pkgPath\", installing" +yum remove -y $pkgName >/dev/null 2>&1 +yum install --nogpgcheck "$pkgPath" ## disable gpgcheck in /etc/yum.conf +#rpm -U --force "$pkgPath" ## its OK when requiered packages are installed! + diff --git a/install-suse b/install-suse new file mode 100755 index 000000000..f11eb26b2 --- /dev/null +++ b/install-suse @@ -0,0 +1,111 @@ +#!/bin/bash +## makes rpm package and installs it using zypper + +## rpmbuild command is provided by package "rpm" that is a base and essential package is SUSE + + +if [ "$UID" != "0" ] ; then + echo "Run this script as root" + exit 1 +fi + + +myPath="$0" +if [ "${myPath:0:2}" == "./" ] ; then + myPath=$PWD${myPath:1} +elif [ "${myPath:0:1}" != "/" ] ; then + myPath=$PWD/$myPath +fi + + +pkgName=starcal3 +sourceDir="`dirname \"$myPath\"`" +#"$sourceDir/scripts/assert_python3" +version=`$sourceDir/scal3/get_version.py` + +#echo "myPath=$myPath" +#echo "sourceDir=$sourceDir" +#echo version=$version + + +requires=('python >= 2.6' 'python < 3.0') +requires+=('python3-gobject') ## The new gobject introspection +#requires+=('python3-gflags') +requires+=('python3-httplib2') +requires+=('python3-psutil') +requires+=('python3-bson') + + +recommends=() +recommends+=('python3-dateutil') +#recommends+=('python3-igraph') + +requires_str=$(printf "Requires: %s\n" "${requires[@]}") +recommends_str=$(printf "Recommends: %s\n" "${recommends[@]}") + +#echo "$requires_str"; exit + + + +echo "Name: $pkgName +Version: $version +Release: 1 +Summary: A full-featured international calendar writen in Python + +Group: User Interface/Desktops +License: GPLv3+ +URL: http://ilius.github.io/starcal + +$requires_str +$recommends_str + +BuildArch: noarch + +%description +StarCalendar is a full-featured international calendar writen in Python, +with both PyGTK and PyQt interfaces, that supports Jalai(Iranian), +Hijri(Islamic), and Indian National calendars, as well as common +english(Gregorian) calendar + +%install +\"$sourceDir/install\" \"%{buildroot}\" --for-pkg --prefix=%{_prefix} + +%files +%defattr(-,root,root,-) +%{_prefix}/share/$pkgName/* +%{_prefix}/bin/$pkgName* +%{_prefix}/share/applications/$pkgName.desktop +%{_prefix}/share/doc/$pkgName/* +%{_prefix}/share/pixmaps/$pkgName.png +%{_prefix}/share/icons/hicolor/*/apps/$pkgName.png +%{_prefix}/share/locale/*/LC_MESSAGES/$pkgName.mo +" > $pkgName.spec + +#less $pkgName.spec ; exit 0 + +rpmbuild -bb $pkgName.spec +pkgPath="`ls /usr/src/packages/RPMS/noarch/$pkgName*$version*.rpm`" +echo "pkgPath=$pkgPath" + +if [ -z "$pkgPath" ] ; then + echo "Package build failed" + exit 1 +fi +if [ ! -f "$pkgPath" ] ; then + echo "Package file $pkgPath does not exit" + exit 1 +fi + +echo "Package created in \"$pkgPath\", installing" + +zypper install -f "$pkgPath" +## Problem: nothing provides /usr needed by $pkgName-1.9.0-3.noarch +## Fixed with defining /usr as Provides + +#if [ -f /usr/bin/yum ] ; then +# yum remove -y $pkgName >/dev/null 2>&1 +# yum install --nogpgcheck "$pkgPath" +#fi + +#rpm -U --force "$pkgPath" ## its OK when requiered packages are installed! + diff --git a/install-ubuntu b/install-ubuntu new file mode 100755 index 000000000..500234a76 --- /dev/null +++ b/install-ubuntu @@ -0,0 +1,5 @@ +#!/bin/bash +apt-get install 'gir1.2-appindicator3*' +"`dirname \"$0\"`/install-debian" + + diff --git a/license b/license new file mode 100644 index 000000000..e8e8eb9cf --- /dev/null +++ b/license @@ -0,0 +1,7 @@ +StarCalendar - A Perfect Calendar Program Writen in Python +Copyright © 2008-2015 Saeed Rasooli +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. Or on Debian systems, from /usr/share/common-licenses/GPL. If not, see . diff --git a/locale.d/compile b/locale.d/compile new file mode 100755 index 000000000..fc26c1f7b --- /dev/null +++ b/locale.d/compile @@ -0,0 +1,6 @@ +#!/bin/bash +myDir="`dirname "$0"`" +for LANG in fa ; do + msgfmt "$myDir/$LANG.po" -o "$myDir/$LANG.mo" +done + diff --git a/locale.d/countries.po b/locale.d/countries.po new file mode 100644 index 000000000..5558fe5d5 --- /dev/null +++ b/locale.d/countries.po @@ -0,0 +1,589 @@ +################################################################################ +############################### Country Names ################################## + +msgid "Afghanistan" +msgstr "" + +msgid "Albania" +msgstr "" + +msgid "Algeria" +msgstr "" + +msgid "Andorra" +msgstr "" + +msgid "Angola" +msgstr "" + +msgid "Antigua & Barbuda" +msgstr "" + +msgid "Argentina" +msgstr "" + +msgid "Armenia" +msgstr "" + +msgid "Australia" +msgstr "" + +msgid "Austria" +msgstr "" + +msgid "Azerbaijan" +msgstr "" + +msgid "Bahamas" +msgstr "" + +msgid "Bahrain" +msgstr "" + +msgid "Bangladesh" +msgstr "" + +msgid "Barbados" +msgstr "" + +msgid "Belarus" +msgstr "" + +msgid "Belgium" +msgstr "" + +msgid "Belize" +msgstr "" + +msgid "Benin" +msgstr "" + +msgid "Bhutan" +msgstr "" + +msgid "Bolivia" +msgstr "" + +msgid "Bosnia & Herzegovina" +msgstr "" + +msgid "Botswana" +msgstr "" + +msgid "Brazil" +msgstr "" + +msgid "Brunei" +msgstr "" + +msgid "Bulgaria" +msgstr "" + +msgid "Burkina Faso" +msgstr "" + +msgid "Burundi" +msgstr "" + +msgid "Cambodia" +msgstr "" + +msgid "Cameroon" +msgstr "" + +msgid "Canada" +msgstr "" + +msgid "Cape Verde" +msgstr "" + +msgid "Central African Republic" +msgstr "" + +msgid "Chad" +msgstr "" + +msgid "Chile" +msgstr "" + +msgid "China" +msgstr "" + +msgid "Colombia" +msgstr "" + +msgid "Comoros" +msgstr "" + +msgid "Congo, Democratic Republic of the" +msgstr "" + +msgid "Congo, Republic of the" +msgstr "" + +msgid "Costa Rica" +msgstr "" + +msgid "Cote d'Ivoire" +msgstr "" + +msgid "Croatia" +msgstr "" + +msgid "Cuba" +msgstr "" + +msgid "Cyprus" +msgstr "" + +msgid "Czech Republic" +msgstr "" + +msgid "Denmark" +msgstr "" + +msgid "Djibouti" +msgstr "" + +msgid "Dominica" +msgstr "" + +msgid "Dominican Republic" +msgstr "" + +msgid "East Timor" +msgstr "" + +msgid "Ecuador" +msgstr "" + +msgid "Egypt" +msgstr "" + +msgid "El Salvador" +msgstr "" + +msgid "Equatorial Guinea" +msgstr "" + +msgid "Eritrea" +msgstr "" + +msgid "Estonia" +msgstr "" + +msgid "Ethiopia" +msgstr "" + +msgid "Fiji" +msgstr "" + +msgid "Finland" +msgstr "" + +msgid "France" +msgstr "" + +msgid "Gabon" +msgstr "" + +msgid "Gambia" +msgstr "" + +msgid "Georgia" +msgstr "" + +msgid "Germany" +msgstr "" + +msgid "Ghana" +msgstr "" + +msgid "Greece" +msgstr "" + +msgid "Grenada" +msgstr "" + +msgid "Guatemala" +msgstr "" + +msgid "Guinea" +msgstr "" + +msgid "Guinea Bissau" +msgstr "" + +msgid "Guyana" +msgstr "" + +msgid "Haiti" +msgstr "" + +msgid "Honduras" +msgstr "" + +msgid "Hungary" +msgstr "" + +msgid "Iceland" +msgstr "" + +msgid "India" +msgstr "" + +msgid "Indonesia" +msgstr "" + +msgid "Iran" +msgstr "" + +msgid "Iraq" +msgstr "" + +msgid "Ireland" +msgstr "" + +msgid "Israel" +msgstr "" + +msgid "Italy" +msgstr "" + +msgid "Jamaica" +msgstr "" + +msgid "Japan" +msgstr "" + +msgid "Jordan" +msgstr "" + +msgid "Kazakhstan" +msgstr "" + +msgid "Kenya" +msgstr "" + +msgid "Kiribati" +msgstr "" + +msgid "Kuwait" +msgstr "" + +msgid "Kyrgyzstan" +msgstr "" + +msgid "Laos" +msgstr "" + +msgid "Latvia" +msgstr "" + +msgid "Lebanon" +msgstr "" + +msgid "Lesotho" +msgstr "" + +msgid "Liberia" +msgstr "" + +msgid "Libya" +msgstr "" + +msgid "Liechtenstein" +msgstr "" + +msgid "Lithuania" +msgstr "" + +msgid "Luxembourg" +msgstr "" + +msgid "Macedonia" +msgstr "" + +msgid "Madagascar" +msgstr "" + +msgid "Malawi" +msgstr "" + +msgid "Malaysia" +msgstr "" + +msgid "Maldives" +msgstr "" + +msgid "Mali" +msgstr "" + +msgid "Malta" +msgstr "" + +msgid "Marshall Islands" +msgstr "" + +msgid "Mauritania" +msgstr "" + +msgid "Mauritius" +msgstr "" + +msgid "Mexico" +msgstr "" + +msgid "Micronesia" +msgstr "" + +msgid "Moldova" +msgstr "" + +msgid "Monaco" +msgstr "" + +msgid "Mongolia" +msgstr "" + +msgid "Morocco" +msgstr "" + +msgid "Mozambique" +msgstr "" + +msgid "Myanmar" +msgstr "" + +msgid "Namibia" +msgstr "" + +msgid "Nauru" +msgstr "" + +msgid "Nepal" +msgstr "" + +msgid "Netherlands" +msgstr "" + +msgid "New Zealand" +msgstr "" + +msgid "Nicaragua" +msgstr "" + +msgid "Niger" +msgstr "" + +msgid "Nigeria" +msgstr "" + +msgid "North Korea" +msgstr "" + +msgid "Norway" +msgstr "" + +msgid "Oman" +msgstr "" + +msgid "Pakistan" +msgstr "" + +msgid "Palau" +msgstr "" + +msgid "Palestine" +msgstr "" + +msgid "Panama" +msgstr "" + +msgid "Papua New Guinea" +msgstr "" + +msgid "Paraguay" +msgstr "" + +msgid "Peru" +msgstr "" + +msgid "Philippines" +msgstr "" + +msgid "Poland" +msgstr "" + +msgid "Portugal" +msgstr "" + +msgid "Qatar" +msgstr "" + +msgid "Romania" +msgstr "" + +msgid "Russia" +msgstr "" + +msgid "Rwanda" +msgstr "" + +msgid "Samoa" +msgstr "" + +msgid "San Marino" +msgstr "" + +msgid "Sao Tome & Principe" +msgstr "" + +msgid "Saudi Arabia" +msgstr "" + +msgid "Senegal" +msgstr "" + +msgid "Serbia" +msgstr "" + +msgid "Seychelles" +msgstr "" + +msgid "Sierra Leone" +msgstr "" + +msgid "Singapore" +msgstr "" + +msgid "Slovakia" +msgstr "" + +msgid "Slovenia" +msgstr "" + +msgid "Solomon Islands" +msgstr "" + +msgid "Somalia" +msgstr "" + +msgid "South Africa" +msgstr "" + +msgid "South Korea" +msgstr "" + +msgid "Spain" +msgstr "" + +msgid "Sri Lanka" +msgstr "" + +msgid "St Kitts & Nevis" +msgstr "" + +msgid "St Lucia" +msgstr "" + +msgid "St Vincent" +msgstr "" + +msgid "Sudan" +msgstr "" + +msgid "Suriname" +msgstr "" + +msgid "Swaziland" +msgstr "" + +msgid "Sweden" +msgstr "" + +msgid "Switzerland" +msgstr "" + +msgid "Syria" +msgstr "" + +msgid "Taiwan" +msgstr "" + +msgid "Tajikistan" +msgstr "" + +msgid "Tanzania" +msgstr "" + +msgid "Thailand" +msgstr "" + +msgid "Togo" +msgstr "" + +msgid "Tonga" +msgstr "" + +msgid "Trinidad" +msgstr "" + +msgid "Tunisia" +msgstr "" + +msgid "Turkey" +msgstr "" + +msgid "Turkmenistan" +msgstr "" + +msgid "Tuvalu" +msgstr "" + +msgid "Uganda" +msgstr "" + +msgid "Ukraine" +msgstr "" + +msgid "United Arab Emirates" +msgstr "" + +msgid "United Kingdom" +msgstr "" + +msgid "United States of America" +msgstr "" + +msgid "Uruguay" +msgstr "" + +msgid "Uzbekistan" +msgstr "" + +msgid "Vanuatu" +msgstr "" + +msgid "Vatican City" +msgstr "" + +msgid "Venezuela" +msgstr "" + +msgid "Vietnam" +msgstr "" + +msgid "Western Sahara" +msgstr "" + +msgid "Yemen" +msgstr "" + +msgid "Zambia" +msgstr "" + +msgid "Zimbabwe" +msgstr "" + + diff --git a/locale.d/fa.mo b/locale.d/fa.mo new file mode 100644 index 0000000000000000000000000000000000000000..855dd9cbc1ba35a0985ff6cce0ea56103c4487e5 GIT binary patch literal 60937 zcmbT92Yi%O+P6ogD=2ocJ)j7vL5c;@Rp~Xf1Q1t^Pm&=SlFWpe3?W$?C}k0QuPf*R zB1MV?R4mxr+D%}sYhAl*UCVyg|D1a!6L8<}{l5A6%yrIv+I`x6$}_>Yc5iWYgirq# zk;nn?v(Ayo(w33P1A`=uL{2>|62Tjp0$af%*al8DCQLpPD&BdJtRiz@9$Wx-fcHVA z+X!33&E|g=w#EM{JQ%)Z^55WI_*?(cr$5kmFx&%qXV?)Q19ybOpz;|BcZP*f`krMh zgVHMwrDw`mXW{3;U69X(yTdCjdPL_wNE#UI3N8 zE8Gd@nY=fYzQ;o4bBg&#z+Led!1k~Nwu955;++k5gL6!N4OD#>L6yG}s@(NZ_1s{5 z2uhE~jn5ljgVN(osQP~dmHt~OeSfm>U!lrxeTL6xH>mRVh3bccq4eqvrT;)Ey(U2E zai*~pD*tIv@e)vaooAc}rO&12zaA?7BB=70n13~te(Rz1xX<_y+z0;?Q2D$CmH)?Z zKll|q6t+9l=X(@XyrZG=ISFb!4Kev}sPZR3^}}SSb}BZ10xEqyRJ;pK{%0tCuY#)o z%~0`fgNnBjD!;W*@g9U~zb7pG4Jf_;4i)cxDE+=P`S(!u{|$D4I~Mu&>q%pR46@)pyEfN>Klj3=RBx> zn`i#3j5k5Wza1V6S3>3cK2*DY3e}GPhKjdK=-YQMC_8Z^l>B5k6b^@)x0gbtzZq(r zuYhXT2cY6VVf-6Zc@4(5q59=RsPtb$#g7y-*FamSaohzezZ0Rx(@>~%qoDGeWIO}P zj+R2{Q4cjg7C_~@*mx&Y`>i)_gv$3}^FI#N4$qqZP2)eH+VvCT_fYM3{SJYOcet^Su|HJ%4TS2yaj-KCp~m$EQ1!V1D&AdCdTxShkIlyCq1xjO<9kr~ zeQy5WjBQJOK6^sx)dk9K^)Ma-RlfmH?KK8U@5#m@*aClawdxy#Ifji-U7-~Iv%)%R>^7{ZPpMOD(i+@AKZyWXXY!4N_FO;4KL+RNawuHT) z$~zXefP;*~q3W9trQZ~&bS1DQtbi(KI#l{JRQ|J}^tj06H$dgH6l$EUgBoX>q4eJh zmF^R${`&?hzcy37e=n%=kASMjF;L+rLX|TDO0QF)`t>ZR@+zS8tA(xLpP}?L$%jgunjDQieCkle+p{co&#IMOD+6bC_Qh6(r+0&7~TU_@7G}` z_!U(D?i^!`!Ol?mM4|Mkf^A_9R5|BD<$D=ayc=N~cpGdBS3sq^2daJ#LdAa+O7G{P z=JhL3{rx^vyM6_w=dV!xyz?}lE`ZAK2=n)WivI_w^dpUv;5hunQ2DGg`6I9${^z0e zX@G6vn^5_F1lz$c%>NTq{*emL_E7V?6IA-1Q0e=c{|`|0A7~t9EQHGU45)gSL#2-! zQ&8M=D!Fk{*_SebUjr6EQf0Uhm9}81Mt5OrOytPKA)YT((MgZUN^WiJkre+DYwm!Rs~ z2-R-yTlnX27yREs#oM9Evn^CU9ih_g12r$YK}oyH@*p#?tQ3yzl199JM;ev^YHH!_u;*v@;eSH z|C6BVKOA;|*hiLFIFbahP$8aUxW? zr$faLjnhn?f|^$|q4GH!svR$Yihrr`I;e6NLB(4N)lRFR#^pWce*`Mt(@^pLX5p_x z<@c_I{}Za;zJkih%3F(}k;yBiONN_QkwImbZde~QV6LX|Vt{F9*aD>8W*RJypy z>!9+RWn2K&K377u!*#GDya~!)t%B0?NvL?QnSTq^y74j8IQbPykJdH5{0_#wq3U%I zR6d8Byr1zDsQL`I@G(&Remc~Cwh}7;3!wDA7^*(k7#Ep*1ysDdP5uy6J3M22*|-HN zpZB2Re+s4FcTn})A?@qY%Ge$%pFN?{9boJV6~8Bx{(Vh;JXAXjH2Daq^kXf2GTaUS zSy1(uZaf<*-aM%GztrT5p!8e;)h_oy*@Y*d`s+2Qbd6BwfOnzv+G&Po8>n_~Z`>cM z9zCG)=>w(j0H}P1TlhGWPci>l=8r+?T@BT4f3om-P~}_()gHG%>3h5R*F%-F5vo3q zK*fI+9s(Po%KsIrUM*{V{I%P~;;41~&e3{-iiL$yZa`Fm-)o`rTLP7Ct%Yxd(&KTs2YkW8-!u6?q4fI~R5?FE*B>*zybV-- z9gKUM{2-|E4u!I(y`a*KwD7SOJ`qaK(EOE9@n)L*9H?^V8?S(BhnviQn{g$Sp7%oO z^Pu@3hpN}JQ2D+MmHvGw{XT_i-yh81vd)Khgo?i}RQU%&^-DMN9}ShyNl@)K1nvpP zK$Tx&tg!Hu`Ok(L4;MmLFQ{^^fXe4uliv!}Z_A?#p?n!e|tdX)89DA!bd};D}X9@iiJm^^p2Z61*PYC#syIM zUkx>HZi4EE6()Zesy&~C%I8&+H$v(04pe@hK()iSQ04FRC(ed&SJ(?qfr`HXD&Cb) z?XuW-mxXVFs`ryn{qqu3y&9p)e;Z2Q_s#z)R6l)X@^6hlTlkJ=`*K?wcZ14*Kd5{U zfhwnm$$J}5fU5TpsPZO2={p7P1<{|`{*{RWj^ zt8;w1J)z{Cq57c*R6fT;<$p3%ev^!6K-HrZs$Jty_N*2v-HlN37eU2aX7aUA{dFHy zy3J7i{Tx*Mcc9{b1eMR%Q1O0%(l2tZ?}s)}?cN?LydzZoJ3*!E2Gw7A=I;ZQe?O@H zKN+h2W6Xa#lpYmO@y>%w;9LvOJI{}+6QRl(4%JQ*q4J5so-hTKelb+}cR=aA3aVcB z7$1g;_Y9Q&FB!K$wa-7y|Ap}fsB$Cc`|{dD`S*vr!|qV|oM0RbRbD<+`IC)BQ011v zqhK5=-7Qe^rBLZs!A|f#sCoYiRQtRSRql^454N1;*X_Pgdp6LG{x^P~|@kmG28s5V+d96a;W$zsB-JfKimBCVGHD!L+Nu3 zRKB;Ge5v_Y7}vp_2;T^m?N}|QyXQQgZV+sRe>7|f3*e4$iuq56 z%C{IQ-!hX=hpq9~Lg{fHlwH37YFu6qHNKZX<+~oLoQ+WWJ_42Q8Q2=W05v{dH~%M4 z@xO*{zM8+~e6PnYPN|Fux%ErLqF94g*j#=D{H>PDz?UoihxsQ6z(rT@r;B~~E>Q9JG=C>3J-e8{w}l@M z)lMfv=`#ZE0`sBrKMfuVtDy3~0xI5(Q0?{?sCHNmH4g4EJ_A+$TTt`oeW>&wnfxoL ze)`_Rf41=7pvrB1iLXZ|sCZqV%IjwAW8r^*gBb`z%)j7LU!Nuv#T#TCZQ&E4(iOw~VbXXBRQ%<}bx`phfaBnE=0ETXf6tc( z6@LuW{${fIYoY3MK9oKSVS9KzRQ;Ah+4o0`Z$Q=SbK@>o`glh`t;<8;0q_iCJ=`7t zwNUvlhidi?9RoH=*kD8Qcy20%gD3U**#u2G#C)Q1N>~<$tnqv~day zkWYoG$2=(eeY5%3L+SAhRQtSUdpj=|2T3 zeGICc8SpST3#wkrOuhm3!v7d_>orump4WJu0F{0?)VwHwD(_UN^2(sftA-un`R2dE zxEM;$wNU-B0V>{eQ0d=-D(@qxc;7;m_p|w1UF-X|JyiUz(CG(Nj}uHj8Y$(QccAL`2~_?+LXFF|*ZF+*fhzxSsPc}3YS)40k3r>A zV?4*?bB&k7y$HVvDxbAb@gFk(Q^wa!{vMP*pF)-QZ)3~refr&?=EZ?f`kn|CFW!9d7a51uFeMP~nHcli;ziJ50j^ z;4M)7d_PqFFF@u08dUm^Ec|<@@zVA$e*AQT3O^ny{&3h8o(7fA`B3$k2c_pNP~&(7 zR6dWw{oyN6_4^XazW)N%er;~`dUu4K@OOqP=R|lA90QfE3hoE%q1x{%sCHNeRnK)$ z_53^R41a>kfB!{(-X99JU+4j)Pac&1BcSphXFLNc-)T^Kq@m)?Gx;S@5>`3$Olt#9|? z9iZmx{!snh*Ekre+==Et%ly-!$~znOf|o$$_YhS1PeAFt1*-f{pxXC)sCI1eS3eFq zLHUn1j)VdJv*5*W7F7AWF7f$yf=A=;3}xphL4{WsXTYxbXF-jdr7(bZS@=s(<^A3K z-xz;|8kcRBdVRaVR``#C8t2DCwdX+C4^DtR;YCpWy%DPaKZ28B+hsnVv!L{=f@+VM zQ0+ets{Iy2wa^%@LS z&q+}EOo!d!MNsXy9;!VagR0*vP~&|IR6bwBcCh6NulH_H{?4!sJkod!+!_B#Q0Yd( zUErBe@hhP6odIQsE{4);1yp-J2{m5cfok_wD}6ic2bEuMsPcwD#UBGzpE8rrf_?BW zfGY1nlRpJjua}_aRRdK2d<9kS4y(ML2f`ik9}ZPc9&7=Rh3c;ppz<9JcY%|QrBM1# zhaKRBusysI9s(Dc{BfxCuR`hbHdMYJLZ$x!s@!&~eY`!42SM549#G{Dg=&uisQ6`2 z?NDv<1#nmVx4_A86;wH`?(*^Xfog{UN}qnlK~Q>+foh-AOae{~mBEycDY3?_o!{!&=rRg+uwrL8UK-(z6zJfS1DE;4M)4 ztcEK09w>b_L&bj%w%>tsDSQ_HH|u2nk84IhFk|7&C9UY~wf|+wDpRa<_V=a_ko1yf1 z5vrU<^M4GL&)4Sv#n|#bFYf^NA$&imcI^$7&nT#QR0x%R3RL|{p~|g+Dt8W)-d8}y zyWV&^RJ_$t@g9cC_jwC{-NN5B|2I(i{0i0nEjIZ4_J%6AGgLmkjVD2;Csa9wQ0b#k z?NkY+&m~alu7`@h#JC#nj{iQW_Ieg7-WCi02rB(oQ2G80)t@_V^!?ctY8{vi6)y$% zgXcoo$?ISzc(2J{hRWwNsQCYad%@qJ^w{%$FYgAWM;|DCPln2OI8?sHFb~dx(t9;j zyiKqZd=hHhzXw&$4^Z)Te89^)LbdN<#(q%cjWkX+c`1~BGobQc2$kQ}Q1Ndy|4Q@U zXM7wg{>#R#Q0d+`|Ch!eq4L?`LC+3Q<8XiT_cfjZm0vzoy(U5RcLh|wXTxrA9#lK6 zh8nj|L#2Dc!ry>u|Nj_UZSw8V-gq!ne!ZdE@kH}ahSIaxn1IUnY$!eEK&89J{EMLa zb1l@keH_XjyapBT6L`vofA zPLF!-0@dDo8M{KoI|iy9PJ}9Vr1?*W+V@O@Dt87{zs-Tl_d2NZ7eTexDyVU`0V>~j zq2hfE)qYGiq4Imz z{Qrc~<2$JF@{5J<^_b7+AgFpD0Tq5clztPS#$zQ^`R7B8i_4+PUkO#tM)Pkrz6e#1 zH_iW<@h4-e$Njw711jI1Q0;v(RDR>3(w_!ZUd-fkpz3jng3rgSP%s&vez&`@Efn%V;C!4<*YW!3{*}qv(9o{zXvnYt4Ty?2rE%sP=ow_!iWB z_!O!gelz)Q&v^fVP@J_Qy374yFX)nA{Q z|35Im-;Pb8(j5WY!9K7J{Db+2Lgh2wYsnYLtx}NAOBETv;*~rYVS**_wklPwe!PJNiRqr35^p5<^*L!EEe%cpyfQOs>cqsjcLd8Gbtg)Lt4?X@R79{*9W zCyc>i@H*HHz5~@hZC>`}?h1Fq-wCRIM?m#=e+wUG;S(&p1gia$Q2lWURQx4y9$W`i zpA%m3^&Sel;2#TBuO!rXSqk@nkHC)bHK_7Gf@-htjsLOmHm~|}J3+-i7IuZhq2kA( z+MyQi0p}TSgqo+Tq2fIU3*c)|diHqD^Ju8@Plhew7^r*-jpfF(p~|_;{I?p{L$$+G zQ2D$D)voVDmHQp+41a^tJ9yofI~4APf4uq2j7g|^o&$G=mqO)#BitP>gUa`QsPY~& z|0_`C{vE2HKZnZyTd4Hy-th8$pz42+u?LhrJ=(&LH=Y99BOe9>I1%m*XF$!ri=pd( zsQlK!otVFy;9uaQ4ct*6FW%zYZ5dSi-Un5`O;GiC5vrU?1?>>A#sP^ds)vx`G!=d{BRH*coQ2U2E zDE+U4ihm1~{aOW8-pf#WZG|f5L-YRx)h{jH^zFZw@c^j)JH*%r%AOB^nl}|t=`MkX z!M{N1^8{47SD^HN5B7#XLbb!;Z+U%=f=b^H25329(d!1tbamJ}8KO3rF7eb}G9jgCV zLgoJ`JQO|+)h=H^>DTdHAAS&&zbjPveT;*k`lApkpCaQ_sQjwn{_qNTI9z4njZpc$ zWB$+O-+_4qrQZ(!@Zs%@`#_a*C{*}S(2XZ3eTJKS3e@^p3^m@P@Bo-H|7B3~>lUc- zb2rrdf6BsNg>CV_XY$WrYy3Y##gDx2+iedhd3PvzFR1bc!A@{IJP1}=_(dka1uFjy zP~~p6@V}Y-J(GV1HBNqns$c65e0}$Y>bLIZ{{vKh`B3^yhHB>`DEqMhs=l{D>A4ZA z+~!_X#aEj4JpGV8NxaPG5yICY z`zx}CcsAi5hrgr6d7OySy$*kO!bZR@Y6SbZ@r7Gn%J=XiuIRkGu~P_g|ECH+-CD zAhJA*C-moefM;*Z>wQamk?FcK@`=c8+@l9hLTxIT%@IJV|hLwtJpYzD)OY#Jf zr!DS&y+=Q$kti}*Lg5riKJ_rR~ue|T=izwKu)WYq{~!xt?4 z9{hj6y&67Z=|{m~$cFITX0inCzftaO#5)MtsXTFH0se1zmJoIyyaQ%FKNDv^{LjIA z;Kjt(=MDT{!K3m2#blEI4R;aocF2)wEo7?{DMfx9&tteZS@_B1a|zF<$i9X@@$|y2 z&&NDB6YmM<^Y41h85u^rGYRXV=wg+Ka>lVYKk)4KnGvNo~ zZiV|l#MfsuPaOFI;^iZ|gXaa@`h1K0e4ZDPJ?}-??+cMV!*e`OlILmS=~IncpFbN{ zlh=5{u0-}5?)mV0!VXjv`rNd!P6^KkM@&ai4!W}0q&2TC8{(lp8Bl)z(zaGBE^O2>!2KOsG`W(&k7->7ggLtO# zJVV$Z*x%xuK)O3EZ9cMxiN7QHjwjBp7Uyi-AK;!$yuD2JHtu}nnNJ+YRiqtg;n(4Q z$pU}H{T^=Z7w^UW5OIDZeP64Oq8)&HgX!@-+@Co2!(EUcX7bs_OO5A{ZeQZvh5u6A zC-OXjdn&TqD5ovY*E~b}~lRV%))4Oc{Ig=v*~{=PA-2P1qDAvd?CCHS&j%ze3y&@Mf6#3?{6B=R{;n;2PM@ z^1loJEj*_nOOoI*lg~i56OTTRBKr&BU*JB`!rAIauIKq9aT}UeaZ;CmgjG{cZWB_E8!u8cY+7-R3V!Wi^!`M_qRNK3IB}eX=I%V zyH8ZzhDs?@E=CH zKM;Nca(!M#HijpKtP}oAaG#M=wth3v1(`loxa&zb&(huq-y`fXi$BXpbKh-{7vh=# z-!=LB#tVsm3Ta>E(VakFlM5&Fj3><~o||~|IfdsF-2a3lm5%fW@EmV#a0l|CJZB=) z=TqW-LA-By?#11J{4Cu1EaypByrm}F5&y5mdqfe)^DVGeZa!s%O($MA!lM>n@+*<= z#}gns-{ij#N1y+|DahIp{<_I}7+)v+1j35V-;VMwC!Ri+AnyYIU}2}j^IV8OyUKYe z&vfEOq^Tl2fM>!xdGvXbXArW_oez6X`pXGBm}dsE z+b!Pl#A(Z;&j#Y8Odf$P&E13WH*jA;_**>sv^L&^dl%d-;r{4;73@v?PoO^Ya6e`F zr}3|_baP<>|G#-ovT|A=d!45NSx53Z0zOFE8!i1=@L$O8HGc=<9>G(B|3J%wVE1_! z**C~`<|*OnL%hsqD(<kAH9Uusr#^qe{gI{robtXj_fN=ITlp=BdprK!;c@Wy zK8ui#ChfT23q*D^VZF$&6M5`yagHz@o*?W~(r+ca0xrTooH&2s*^4+mbISY~SvB!) zBhFVm11zt{aUX2%Hp-VYbCDgy^EvK)kR3<5-v~Mq&O$x`{u}C3&+`%S3h@5{_gOss z@Q;D|9E<#F?~K%u?r`$!4E0%0yf=9Cxzi)E5B@KC29xePc%;eyW@+9={uT0b@OOpB z@=QYZERR0t;NNI*YrH73%y=zfna^|_(@AqX9L6&O_gd1egl7=18_zn6ygTk5xF-{~ z$Yeu^y9>`d$PS0oc=Y)v{2AGsq#46=4DPFtJ%GC$_e4d)KM8-4r8x}wQ2bxOR9v}4fqmiFM}5o2a)^ihI<-uD+%j{dk@^_6Ec?P z2xM(}cCg4jjJFc+NZf7UbHqDVkA>lMpGOJP=Q7fd#yy{W59WE>i{0;bdm?`p*}lkL zDF3=aBKk~FgegkRuA^xor^7#~h9ejzf zfAVy2FHTW+>ek}YAwno<8`s*!Zd*IH8ClR(Ue3>WnIf`W>}go z$adzLEg_$)k(ctEjx3KnVx;{Z`Khp)XC03|=ZeALkbjFQ7@kNcL4GWru8HC(h?XSc zr8p)>qZOdk#Y#*KB3%k&Rng$IL_FFhs7@xTqsf}OAeIW^(b~$oAS^A7mIgJ6AUY!& zuSxL^v}-CDR2imlrh*|e!zyt~G^8{fnOLcyESX4Gr-E2jbs||q7O8YeNi>z3ny##@ zQ_NURw8|x^h*c)4qBYTEDwvu~R0UmA7`~p7KK=Ui3hKx=2rJ7H$yiN!Ritm;=uso$p*g*btV)ZfDVKt}6pa$7Kai<-zJd`WAeAdEMPI&#s3V+fXsl8ZOM_u* zYv&phtqrp35+Z+7Xr`hBOh{W=y0$n<_2W)kLe$rB%9~nN9>!y-8nxWiWUM5N3~=pN zJ|nD;45$o~ReaN~F#;;fqDjYCNW0^Ym8H>L@hbBekU?R{Ka@D2Dp463puFRy)G@`s zr6^abTs;&oO(c_K6)#Iv3N>UA28RU$!(=geMFvz?SE58sED;ZeCe#857@zS{hGQZ{ zLsVBMV)2?P*E-e77)4D_hQ*Nq$&hjw6~T-!7#O3qq(!ncELId)p&BO3NEeG!PLdo{ zmy{~Pm#kpcB#X{tOdN3(j>gRfw4dD{Fz zepptXjM4~_7chz`&pFe@w0XV8Sba2E9LA=Rc;K*LP@*!Cj0_BmX(RU?6cA@%SW@A> zG^xo#{@O6v1*#S{bX40_IF)V0qeAkQO9u ztg=!Aw0rMfXCB|f%;LaUlGdfMV#%6v>~3UWVp=?{)=N~zW-vAeqGmDQ$*|saLn2uk zC9gy>JuONBjxKtNfytB@9!=Iq%Mvp*Tn8p(Xfi=% z2hkT;D^L=x1l7@CQj~h*Ohs9R;}lH!cx&jwB$Y#NOi18uY&@6{txgwX>9PT`aLnAA z@?ZdE$POz>IvHbGA5@N!;;TH+oTF{a!%7A!h?h}@13g2RObx4Im`kjPNn=xkXjQdZ zw>(->;ks{ZvYHf`A%Z-v#Z(be7=1ak;PB0G^Bv>%0d6&! zYFdhGYNL#7g3A&|27{wjiISv@X0~P3?U>w5j7)>)h#4OYJsVG6tnVFPKP-mJnT|}~Uv0Q#`<}(_~vp($xgRyb8WQ3mtemlV38)Gga;0);9|EQ6((tP@61^<3Zuy?x7RC;PGxlr zQfy4D@l%=2e_bN2gHsUFOq`UCa>t}0rD?zUiwr5Nt41|_$FR4Ta}suBakU}O6^jD@q`kSQ`ZkB z7+c~uwWj|iWJWT%hq}!iZnSe#r&=3y9jR4N)Ljq|P#oj(h9>X}BXDOnWA?~4RC_j6 z*rihmY0fE;p=|xQM;Xc*%R{vQ+kv6%&r#N$IERJBnEYYvfte)3xDZIuT*Gu&+Pqwc zQ8{ZjFQ0}ZUYw{AG`jtgz?M8&rgYfTFkTm78nUhAY+lU_EmLZ@8Zbdeg)`VT3`-=+ z=)Ym4QeZO5x+>^y$P3)YdlMmT^B)rbAm5XO^kVf2M%%bgXYGR%&M4w?tUOvA87K zlbrsCEU*|@lkDILno==jP%1W!@#DU=@p6Ydo#MDK(v;;&&xLg#A$uGvL2Zu28f(TB zj(8~>Asp#AXIVlXzq2MXg2R=jtTTH0^3!JoQ{& z+;j%DI?8B`R*SeLT+^MA?;GSumyZs)YObgb&aBPU)S8PC>oy8*BT(XxSu{mc0sMb+ zToxS7Nt*B~g#jnMc#7VVG9{VwMR#fvq@u2$k^%P<6*Y+*$7yb2y$p3NGF8tePH}@af`ehv49;S1 zpNFrgARRA`F$72W3(aWO;ns%{Y(7Ioe8)MlC>3KKKh3YpG|uN5aaen*A5F^t|8QO( zp@XTuH1|cnbmN+i*9Hf&h>fgZ91Fj*+%$$CY&oLLMMNf~AW;&F`hD0K7OSR{_lQ(7 zpW+ZSlKXoqFJBr^k+Y zq=p*feq=~P@y)47`{j{gZT(}jA|qqT1UGIYxmrD@{h|djAX&Cg*IQrm-MjK#i0=;O>uuGVSQLp?r!vOM!7DEjEctVLcV3i z4eE=?D6S$Zc;sLkow>mC{e%yNOC`nJxt&Q>p zRTxbzdCJ(rX{Px(u)QP|xRt{K3KI!#!j+H>guZkDjZKYlyX$=0ZXQOXa!tB4+MnPW z-2jdb-`#K{lXrBK3w74i(b1{ofg0F)=f;Fga0Y#JG{rV85g8rhOsSNyVw!(+j5Bo& zmrFHKrHD=SyOfOf&HY*3F~DheT|GKsCzH`!nq)Lf&*a>SMJ$Vqe(cV8>C@N8BKaY& zS=`<@pRrXIrdXj6`YUF~STX)`x2=J`FY-goaDFtNCJ2{Z1?7ho60lh)b*NSLm8|{B z5z0p3DCM#&C5H>jcxyKuoqR|f@dTgBe5oJ-k^vaAQJ zURA_&XRvJ1(V4N51kyUT3)BhL#~2DYxRQ+IV+cyqOm;ccTCoz&P|ot^vn|xf)?mdQ zcjGGGZm#{CJYE~Rr19ppp{P5yCDgIFXk= zVb7}SW;IV#mL_JPD?^;~2-E1rK6ZP8h@|Gr& zpXTg{{lbx|j8z4b6UoZbV01JrQy{gkVt$NaKDeta9Ng!P38#fq-2{={$6JLN;5(*G z^q6qEwtxEzlr{dm`#Kf4o> z^Ry_Fis>>&M?-xDzJ;CBo!p#D-A`~`GFC4zg^JS%ScsT)Y08P|#Ol7KE|(4Q$e2Ws zIew?ynGLfyhOx#Mivn6XFr0ZW-F!LqzESiOdFgmk2RO*-i-T^WLN4s6Stsw|?%s

kPdh8_PU#OX*l{vUDRime;C0c8DLVSwEI7 zd902e7|SM=7mU#wxoGd2bjlyG@n+0DCu-dQ6-1LWVjM=svg8y;by-fLW>@KBlj$m# z>$v>LINFhZ9~V}5>xjd@WF1EbrIA&Hv zS+#^f+qrL>2;*oIx9!3~*6ev>)z##k!mVBKAPi(=Tr9hN8po2!v>b=|4g70DoejNz zTr!%sjf^JrZ*Wq9yK%rn&P;cuVc(Q4qxW+*UfX7ojJka}mkY$gx&>oL1-zEf7Cy6Y zE_FAa*;^k@Xk^a$g$c{Och13s>C5Jt75~5G517 zeQt(~+kF4yf8nYoGCt&23~qFd*E+;Lc|2EIlGmpAs^_9OGCslbGahR)KEa({d1QQA zJ9~iHo`CTVyRl0%M;{zhnN+&En1H1y=G&hcA)BSDM15oeC*OFDE-@zf%QwFjk%t#c zyv~Sn3d0g|J!8K9=IxsDv(U%cM(SD?6S!TV>hHvrEdN1`3yl*G8SD+s4g=l{*a+jCpTanYOluF>)-hc+xQ##fCV_N-XIf-p_%4Inl( z;Z*}8rXZ|EA?-)`E=To90d|_VD7=N^HDLdY*A8qJNcZr*fht6)ZXDGTWp0)YRPEf) zfdIpoqb&m_b4j3gzGa-6^-c1Ot+&^`~TAE9Mx=o)lo6YAy;#6dPtc#`I6IKw&)tDN^<-u<{>KPnL z7E^CJha2B=ai~F3TPuaj8TJe=reQFmhD}-CfE0U{*pbXQeh$MB@dGJ)`aUuk&o3N^ zZ^Kg#>Qv6_8TTq7>(bqd8*f2Dx{7@c1J^~$dD-Lzxi5?usfv{OF`9YYoi~GMM4Wp} zeiy@)rmXCd-VnP%!REmyEz*mv%-bxN9t&%1+9+lOIpZu7K8Q_v zoEHmvXYI_4c3mzJi@&Z}akHuL-!k_Fu_*gM+Em+~0(w8>l3Ol!2VD@u9z_Z&+45Fs zPUCPtF)CnpRGHmPaPr{Ic07AvDZl_~Ck&(9!DrV>ewasFsWO>KvhBoDt?GJ`kXu)7 zD&cf)iVIkgeUG8O+-@;+H^#a~s&>mFA?}w3{s&*)c=_S7ZqSFxsxy8O(ymmE9lQO z?8LNEQFJyy%WlqyM$Uzc&m;Rrj564&hmpdtSUdK@5VKRrF@uGxFm$7?Fq{^%4Q63j z!3xB;HzS2%Ra!~-xs}#g_X|Du;@SPwpfIe{*0L~4bJ%ZW3ZoUVsv3Nf%%Gxf)2A>0 za>DuDMXbL&@&4i{D}>)VyQsx%8o2j!H@Ai59EQ~@g_s%lZEqp8yf6@5OnQ{pgwcRq zDz`GJ>S&3(S&{vT)v$cH?(P{(Vb{o;s$$-L*7+$ySg^0MyX(%>^!M+m3AH*mt3pp- z)s#QIWEb=q>Jr`{`yb=_rBUx0G4O@uY?Gywza%IuPb1=IBboy;i|#8Jt;37F?l;BW zMPJE+JE^;Wm-!lCzYVq8*6NKMKPThf&VEU&?T7o#L{36=PMogdEst_jW;jx;gFr8L z3KRV2G)k1o8RvMc*HpO580B_TSIB{$-wSeg$jUnWxn{L+pW`U1a;@JGJFv<4lr zI(k7r*mJ)=D}4HTtoB51IoZQbtk4f8qmhYKRWw-Mc+RvrzhGmD3@a+QZPi|iRYAMP zU`Q2fwtn(r!cmy#QjQU3Ak&jE;6!;cfVTbY>T_px;NMD~OnqxBYmT@{Ov1gzkC!-1*Kyuff`-)yXa+_y8+ zxZ-g?4+t_>;%LlGfPS-(8OA+GN8MyaQ~t$yG%!oT+<9*NIA``=YchsAnq=wWMu|muaydJ~ zfFEqR*GcYN>R)L48&-@mvz8y+VXsZXG?axeW}QJoUXjVX7U7HKn&#n$I;XNaRBm2g z8;zXmZuA+GQ^FK43#Nqir4d(#zF6A*x-~k)zNcJ3R!v37crtg*k||n+ejHSTgE!&J=)upZDtH|i=$#fj5%-=7(~~iT_M7@rZ;~<4J%f?_IxR@LpJSEqyFcEr$8F)4 zflB#Ry%$be*X|XC699ru%cEGe`QKV>qe*a_$><6wz{74@WXCU_*1n1(yrbstT zBpT#@EL!A$Fj+*u`QOJ9qTd<1Z@>F4YWe{l8)5fju%hg*qasE6*^>YE8$H~5|A&$A zzceZG^+fFItBKP!6)Eyc+0T$7MfL`ePd?JT2#P%_b+woZ|4WK;E175DS#2==Zzq2K$m{P>#IXqBYrX%3$`cUEqJ zNhI@1?5CT~@{&Tob>n}p#%u4#X3czVVdTjr%v&zU8D>u|;i?pJq}Lw^`rjqVACW(#sgAvS_G%~VMtA3F$?WfLrH;b3$Koewv%?Q(YyL2?aA;n?rg&;; z?i=%l#7lT#5ijc>^y5OfT_k8&(>T9jEnL^Iv|&TTa$Kt$XK!8DIJ%xZBNLM$`YgiiKxQ9fu6t-S6N!FoQt5r|swTvQAay|u6%ZB9v)nD4MreU=!O>q?}flGG> zb-$3!-*y4NK{K*W%9Eus$2yurQE< zG#oW>t*>qg8WuOsrRx!)#|E`hK;JUZX!cCb3>NCOG?IyNRKrHX)k2vB>LpqrBZ<%n zb7?ucY)*FU(b%M7{4~x{o85&ru2G$X9#a>&rf^y1dT6wqpk@QLc?jvLKLf8>{LGMR zF3nUfXjsv}j8S!S3&|vMBaIGnBPgq0kcsQNEjMA~Y$~=v)1`Ta8XF|b^uX^1GGm$6 zw6x29Bj0n?U?NRM?HJ$6U*T?`0mcB+ID(3Ew@`=0%u9x;dQh=0q;_l7C~E5sjTbUH zG>}N*>Wkma8;#MmOlstsTh3U_k&W>7g@KK-kP## zh(}q=O!$!HR@P$1ryGC7TSjb#BLhb>02_d6Yj9vulpzi2RJqWSXz(z~W~&4gl~P1i zgD+PZZvLzHH@GOvlmRiN=VCb;=lXoq+vuiYf_Sy&Ro0_6aIM|2X3KSQHlVizM5Vl( z{*WzHBMN+5n=W>voEpuQ=2&IM!rgwLyPlLDsN-54flrS`qbNF~foVwNP$#+F1ZUFt zrcm;1G^N8!to3rPR0xGNG8~X$OMM$U<4U8dnH$~kb7iW#S0QueoZ_}iFJqCh#MyK` zBQDWuKTMi?8aHW>Q8Xiwj!=U$>}VJUhHAf5^W8V2a|8|dH4C6G8#YoTeRemBF%Y&- zaW`$eQaRt1F&xgyQs@ewHzQ{Y7LBz|U8Yu3rz}(7nuES_^I*0L%891|vyA_VjT+F| zeYF|w<>g8(vC9UvwcgQ*#31`MO*0gMhKQokA%55_leJq$cc{%bI6c-i2{gXes^fD- zTB+v*w6&{7PMDhu^D<4E$&_J$#rEa;au{6d{`uAd))a0&YSE@sq@q;Uw`QzV(#+R@ z;K8@`YMC2ynd_rU!Fj&Qu7-H5Yu6~f2C4QL>l$a#AFG4zeo~Q{3+|!h7jC)EFJL(R zNTCXuR-(^Um`Zm2Nk6&zyGel}^sC=fxCm<+R;a34XJlb;ERzIHh^exWk~4Eb9vVaa zO$~9Z&5RcID-G+Vw+kUD8DyQ=e5esR2F%Jv2D0``DkN9ybqLf<8WD;?m>Y0Z9O)9T z!u*U~q3#Uy++iavr5WGrv)dywxvbejN9k_Ju>qo`rKt4SqFoV1Yw@Pv8ku-b?uy1t zd#~>(G+5WLOl!GQMiapeWO`kh1k%HHk5okJ(B`hqv}jNpNhk$FqEHoS3(8#$t5k)? zdF)rHVL+by0KbBIoK(!KWbQJjyE~-F)Cc9RE2i9 zN``K&7NjkAD7rI=)FpRpGO2TtXvoY~T{JJHt!sHjQ6ZS?Y_MdvqsKZ|4vWDOT2-T) z9^P^XwQlN)>%VJbsihJrUXsSOiVX^=D6Nn;TCFX ztZw!m4RI7vm(JCC17{IJH8;_*_~{hCZ)DR;%V}Gv%geyByi#G;~nusGz!gKihiQoaNe9Y9Qq} zF@xlz26Y2Z9p1E6kbc=j>JTiLRu7ugr=xrorNwVXck6P1o4)k`_hP7SsOqH>ey6}lVsG;#G>ElX8rgY?jeVj z%1+jW_M=%*#ckiI8|&V3odlgdpb=;m)I%j=sxen9kt>e4lQ2=pUGuF6P?tQ~#4 z00XpLj7;9+1X12>2p)@3?JBT-uci=;`gQa$fxoJkv2Y{dj_y;{SH*mTbSU_8}J zu4raKq=>qyc{CcS&V0P=TQzUz=}_d_PCN724fkpQx_yz%cs6Jb*-@gjb-Uf1EtMwf z_QCAA{l1gID@9Z|<*?2)3t7*kzkBOK@^y`fy>*kJQh)ZAB+OBvQl}y`btfy(K=Vvp$1xhnG8s@@@pBy zseP?=i?b@)iD*S55i{Op$(|XBYS}a%Wp+tRDLJyt96$j-uA2fiCYyYj0pwLAAhYI~ z(pYa=NA^jBGgB3{GV`xd>uP2mxCt)9>MHAcO;!$lwHPo|G9ps>8xt)*j2d^vv_gGM z7H$SLWx^nqG2~85drq}gwp{-Q%)Ycny413lQ~6kt8schNI%xaF)i>*UwSl&<_b3MG z7-s%%CnsPICA)n_#%ga9BhUz!JzR|f?3-l^S2o0?O ziVmbLv8it5V&|bssY(nma--Y%W|YX-u)OfSzM02gn`VO4fvPRHYdHy;72G@|b5GP< z>e|dTVzXSag^^w)YE*SE7Z3alu+%P(<<8z{oLqk zFzw;}rK&naJ+s{dt^hNWKQq#Kw~+m_clS0y zGt_2{3(NlH`=j^M)9lDu49JWQ(Ghi)#-KUX1ZsEhWjRtaE_St{eeFI-i7d!(xs{DN zNCR6bHTmbO8=84?x=*3>noMuQiU_UZ7OI?{@Bz8jg{-6}x%=g?X<5=_LQLaoATX84 zeO@yI;X?cgNP|X;X-?<4zr1JU$y#d>_ls|{T-jk!geIkQa5<2GEqSwyvI}2M=8@cz zydEnUDRb*tRZg;ZRUW1(02D$G;g1+R>U?O+7()#t%4XthT&~#9aYw7u>U5j^T z_biM&v~AarW)z?JY$Ejd3a=5ba_MI zY$~8_hg%7pQ&L4)q0%F{Np<sLb9M7b@y?9_UV57c09VOJ6n#+h!Qsq}pEUhb@WqwIvb&`C2^xhcsq zYDjBF$lW=zJMSj@q(XSG7|mv*`jTs3LN=%;+}=(*Ih{kczsmMMe})Emv>7#2=oV$c>WuTQAvioxF^ZKsjk^N87j)CnMHgLTkPdraR{g8!@jcO~X~Q zR6~|cjIKqTbE7(m2uR!(z$qsEkh3Laz|w`vOd{=6wH0tnsghHIPI7WeihXihnXL=E z``wEwzja{`7pQm|sI(F`o8c>qvjHoO1d-3By$XC~I*jfbthkuj8r$<|ga2I^!iIW+qmZN^&BMTMLYN(#vJTI3Tq~t}5R1 z|D$ZLt-G1Z?cx8qn8tZ3PPL9uI#-S;%1YqwbJ1a&A5$RLHGbyCU9(In8rW8Emw0=L z242HjI#Sz!4Gs5d*2+*KB(j<)M@}m;cnMSgyE|37L4BH;+A9e&qrk3Eku^z}>^M*y zKTf}8%aN-SP*%p7NZ9V*IcPO-(Wi@01#%S2Y0O4bQ*TUH>*(cj)4Sm9e_lhNtb}V( z`hWy}i!UqY(lX`b3@}pIxNS;7pi#H1=@#4VJ=8n%Nkt&vyd=sw(`g0xT|Hb*O&c#M zqJn89-x_8y+-qPTYP!AdoU1z(j2)C?>)nuW%xbZtiGPpyRaplo_RK6?509j7)igXy)HLL8t$(mt1Zr(lJt8 zqtZFGjmdhb)I4u+6*k*Knon9L+6J13MN`YL`yq!`F5Z;8tQdYA&9o3{MErgfEVbqU z!I^WSo5R#g>oucM-lo@4iY8^X5XlfpU2Pk50WUQKX)VWHjr7_Iw}86~GWJE<18Kk! zojCt%Lz$C`o{$NW&C2c%rD9ILS!eggjDdAEV8_G7H5O5cRRL8m;(w;1Nr<}6!+Rv@ z$&8>dSy~2>ysV@+sF?e48~ST}SzKQuVySwnoc4Ndd!sd&n(`3#J97JBB1Kcg60&Q& zfa^1C)I2&)2X36ItWIjwhbqh0_%2eJNr^}5FT}55orIXv0>$ecuUs_}|B%GE}^%aM#FxL$-d>Oyy zss@J(v}$FtQGT|7x#fnwKtPk~b*x)Bm6P^FydlextmBo#1^BhDvBA>IxOtSLJ7?D> zSX3t^3daz=Rn?D#)@GZ+brG3JoIc?;(714*w6%Ivr zMotQeWarB&j^?-q-vo#S;nqVuQ z%%5()$6);_-|0eWQcSUUo3`LVUs7u!SGx+$*_KW~Yuyo>1&(1RsAXpC?=cRNA0PB^ z8U2FpByUn;Zw7~YgwJ?crKWi+xL9nOaR{Dqhb4PAU}ch)fK@mB&cM6M+GLSfdSC_sA<- z$)Q7Wg&eIwwee>iarvxX5C{-z&7et*CCur6S_-4@MvygBwK-A9m1tkK?h!nF`reYw z8Nt!Tm}k06`7i*^CCoTX+o{uP)Es0206OCHhokEK@e|;NR?rAk9c`7VP*pK@@asdZ zJ&gkH=j1}3x`ffPgUG{erZw|IggEnDB>=Jj8-t#aulJHV_ij)8@y-B%g(^?w?S-Dg zq)3KDXdrX0s-n0i96?YN>C$3&@c_8im=!X-tfT}Poy%}gw_PEIdP0C!>{lyN#dWaq zHGRGKe`rWCbXJTVtY*$!e?Q#26QmBhzibdjgd!d;vPrCIMVc5KSlNOyoTc1o7Qr|u zLelt-t4vDY?TP>tWJSUFh!X~lt(w7pqUH1)L-06I{t(gf7Y)Rg9Yu!?A}HS&%Tb~a z-+^Yuj4pXpk1k}v4#2XFSIh*0EpWZGL3yYAQQA7TRZGsnLkTb0lLW~~@ydJVZf>>08 z+}pvEV@Lbs77V+D*jjZ}JGXrhi!M?@Jldi)yNptUBeY3{<HdN2$?@h&Q5_Y6iLI9u7?1Opv09Zfqz}AkZY$m)s_@% z0uX(h2j`9-E!{JLm9vaS36_lrC3iRjhHMpVMd~H6Tjqz(puN_olsp9t30P$9n|5;rOnEWF}wID3XEH1(JnB!!N^R1ed-mq(&VZ8)qBK8;HbV8N@b zd1FWPiW+TLsO5r9X%%n_Kl1oo6uhGl?vcBlrCqTo=;?$fxa1Fv5=;zy1V>!~dv2!CwCx=%A ziu;?P$^v+ib4MQ#s~HjVwkO0qBxnT9>eL*gcKDD2aMJ7vwl1~ZvP0gQh>ig831w1W+{P3Xxph$<|sEH~%VwOb-4R`02wmjil2Z|qedcO?i@I4T4Vo47KH z&X>~&luD}ZEAc~ZiMCH%k`G^>;2LA!thacUq?!n_93E7mDAz_(gLIb+_=u>@3M=VD zKNIl#9)GqH)|nxM^-+vEQ8-fHx#mGLJb+fAGv4iMIn+d>^BiP}q_Cc{CzN#^XZ_|5 zJuZKbh_DR^C4Gq&+;4iY91Qhad!PbgiyYcKC1ST0v#dL`RRkIqUwQ-s3sHlbDu+$z zF=4)+%f0g|#fL%?U}qhY#iCH%;cTGUJ@*$z@)V*rG_ry{c$NeXjpE7g#72AV$4J+r z;?8tkm*AP6EedF8nq%APh4OR3Xh)&)?ow2C&!GU!K49VkVY#@uZ`-e-R z#vI7KbT{=vA0yHi`MI{355-LNhRM8CcRRm>>^gUve)`(Mtdwygr}&*MPEFo!0VLKJ zL^Zk4l#<_+P!Mgb(QZ5Z`R>^vs^Dsej&M14SYD_q;;;e-YX)+4qGZ@(*8W4b8MG~= zYJZ>zpEZwo5o?``+*$$?d3YjQhc-{U%bO&)E~7HbsswxzWJ`Z-;K2L=5{#Z8#UB7< zL@ivPVWOgN?zZdNXwD5A7@^16<^qHx!SIq4%R1E@n2gre)2+JF zBYY46OmEa9-Qy-{gT2e~4oL7hLFDHEPHq(x9 z`^Onyf05bqAAFl!46jF6fx;1_7SC~s7!hyQ(3sseKiQBLhs)SVrQLeo&a6rE9LeIL zu@3Zu_YyrmMAaD$6CsT%J_3AHP&Z~$aL=0A`U523^-b_;*UzV4*&>S^w@7LR7s;#x z56Qk7h~JeX;;Cs%Q07PNi;PtBabebIq~z!U>B?L&a&!$eTSh>UICW7--A=ihZUX=o zd`y|Xwj$O6(F**V%2f%EAUM-OlHn`&vG8$cX;}p19E9bYEvaIYo!>wd2jtQQX&a1u zV*Zx5FP%I7rV`lEz; zZvMoZ>OYc4I}tiJ8g+j|QiMMC_OW;da@ z(M|lEiD-+u6#~|*DG%m5*?MxSu;mhyPO>d$^tw!-h`*5s2har>vSKL;6^8`bHvYSSyG`Lf7w8elV)3^LtcwKw6gJJBQ{YqNg;mYC5>mgI3WG@irTdrgO^tz zeELFCSB#=uGl_>M2PuMM`18G_xJdw2>>qB8V_+JBs6KeSrc9~+%wdWltRO2SDk``` z1I~le#0d8Wph>SWaRN8$x4DR<7q!$G(GdFE2P2}lECkD%UD%Wy!WNN8lH#+AT1-G8 zPSFGI*;opP1x%a6%t?vk2?OA9>3pj6tk7fBW3~?^e_>#JEfa0iWPU=z6l_wC7B^3?qExkBnO>I640JY3xl zcxD;9*tw^h)`#&OF{W89RV|7D+szHQ)R{=@6w0EAmmIzgyTb& z^c|=s!|s{h3y=e2O@!uJTWu{ofI@Z;;IeA+$bU#plf0qCf1+RB7{TKD8z6Y)lg%2v zRznp$iE-AsJd8rfQR1SWx6LL;X?#3&QZm~1AJ-uD)EVY7tIEnQzX99GV1XGCAKKCZ zztj94b>CXYx_}|vw)BC=;(S^!w|F5POsDN#dYl+ z;``?a{0;?UuSv=K)(E7TbD#T@{Tloj6*m{^U?<-diN#LxAsX#Tp=uov8o1rrmxXgt zHDT{HZByzXpu{k71~s}s$e;_NT1uvVvszR_VWWq@BoU)MFabgP9!A8F_c$L0hSFwhQi2Z z9?uF_0WsKZRHZ))KDQt9p$!;H9mv~#(lSpW5hIRfNKV`!qZ7{f%Qx(9a&g3QDH&aFcv7bC%)nKJBgH=G(DVX`aZCg?Uadc0OXZI zpaS4h0|NQszkas$YyS7$wfuN!l7Eq1RlJI!n|$s0U$5*DwYz<1R+UsKI$uI|y~<7s zQ_Hdd1%D)6%d2m{^PA<{zx?@I@fY9&S?`fqU($^v;^mY9#2}dBwFUF@WNx>9hP%4T z&|WA|MBT6c`d7br=dFd|eC}*#no!M(`R4xZg_l+=p+D!;piZm+YQe{e7_*F}^FkEC z9fPkHKYsk>jRppVFs5~mb3u&OkXc@Z#jBwi zR@XA6*BOV83Fg?v3Q5m;f)#F^X-uV^&gbS$03mjGRaU~_Qrt@U!jaKHgqAf>(eRE=#S+LrYI%+0;6 zVuAZ}9*FEpgk*+MzlhgU2)9dSk+_Isc;2^>B25;mY+dS$7f_zfOO=mMLLwe8z z)Zo~2BNGm-b=xw-2bD^pvb5>3`^t|We}44Q2k#&GkAeO6h<}=o&(U9NU%LO`2*-Sd zdB-wln5eqVT-jx5Br%<_O6XK7tZp#4?leG#?ev!Ex|eZ1NXf%zTre?~^2m?R`0+nY CVJq7J literal 0 HcmV?d00001 diff --git a/locale.d/fa.po b/locale.d/fa.po new file mode 100644 index 000000000..73ddfe39f --- /dev/null +++ b/locale.d/fa.po @@ -0,0 +1,4001 @@ +msgid "" +msgstr "" +"Project-Id-Version: 2.2.6\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-11-02 00:00+0330\n" +"PO-Revision-Date: 2014-01-02 00:00+0330\n" +"Last-Translator: Saeed Rasooli \n" +"Language-Team: Saeed Rasooli \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "_About" +msgstr "" +"_درباره" + +msgid "_Add" +msgstr "" +"_افزودن" + +msgid "_Add to" +msgstr "" +"_افزودن به" + +msgid "_Apply" +msgstr "" +"_اعمال" + +msgid "Cancel" +msgstr "" +"انصراف" + + +msgid "_Cancel" +msgstr "" +"_انصراف" + +msgid "_Close" +msgstr "" +"_بستن" + +msgid "Close" +msgstr "" +"بستن" + +msgid "_Open" +msgstr "" +"_باز کردن" + +msgid "_Revert" +msgstr "" +"برگرداندن" + +msgid "_Reset to Defaults" +msgstr "" +"بازگشت به حالت پیش‌فرض" + +msgid "C_redits" +msgstr "" +"_دست‌اندرکاران" + +msgid "_Delete" +msgstr "" +"_حذف" + +msgid "_Edit" +msgstr "" +"_ویرایش" + +msgid "Edit" +msgstr "" +"ویرایش" + +msgid "Edit " +msgstr "" +"ویرایش " + +msgid "_View" +msgstr "" +"_نما" + +msgid "Clear" +msgstr "" +"پاک کردن" + + +msgid "_License" +msgstr "" +"_مجوز" + +msgid "Written by" +msgstr "" +"نوشتهٔ" + +msgid "Documented by" +msgstr "" +"مستندسازی از" + +msgid "Translated by" +msgstr "" +"ترجمه از" + + +msgid "_OK" +msgstr "" +"_تأیید" + +msgid "_Preferences" +msgstr "" +"تر_جیحات" + +msgid "_Quit" +msgstr "" +"_خروج" + +msgid "_Save" +msgstr "" +"ذ_خیره" +msgid "_File" +msgstr "" +"_فایل" + + + +msgid "From" +msgstr "" +"از" + +msgid "To" +msgstr "" +"تا" + +msgid "Main Window" +msgstr "" +"پنجرهٔ اصلی" + + +msgid "Preferences" +msgstr "" +"ترجیحات" + +msgid "Configuration" +msgstr "تنظیمات" + +msgid "Settings" +msgstr "تنظیمات" + + +msgid "About StarCalendar" +msgstr "" +"دربارهٔ StarCalendar" + +msgid "About" +msgstr "" +"درباره" + + +msgid "About " +msgstr "" +"دربارهٔ " + + +msgid "Quit" +msgstr "" +"خروج" + +msgid "Credits" +msgstr "" +"دست‌اندرکاران" + + +msgid "Calendar" +msgstr "" +"تقویم" + +msgid "Apply and Close" +msgstr "" +"اعمال و بستن" + +msgid "Delete" +msgstr "" +"حذف" + +msgid "Add" +msgstr "" +"افزودن" + +msgid "Add " +msgstr "" +"افزودن " + + +msgid "Name" +msgstr "نام" + +msgid "And" +msgstr "و" + +msgid "and" +msgstr "و" + +msgid "or" +msgstr "یا" + +msgid "to" +msgstr "تا" + +msgid "," +msgstr "،" + +msgid "_" +msgstr "ـ" + +msgid "." +msgstr "٫" + +msgid "%" +msgstr "٪" + +msgid "Gregorian" +msgstr "" +"گریگوری(میلادی)" + +msgid "Jalali(Iranian)" +msgstr "" +"جلالی(هجری شمسی)" + +msgid "Jalali" +msgstr "" +"جلالی" + +msgid "Hijri(Islamic)" +msgstr "" +"هجری قمری" + +msgid "Hijri" +msgstr "" +"هجری قمری" + +msgid "Julian" +msgstr "" +"جولی" + +msgid "Indian National" +msgstr "" +"ملی هند" + +msgid "Hebrew(Jewish)" +msgstr "" +"عبری(یهودی)" + +msgid "Ethiopian" +msgstr "" +"اتیوپیایی" + + +msgid "Language" +msgstr "" +"زبان" + + +msgid "Numbers Localization" +msgstr "" +"بومی‌سازی اعداد" + +msgid "AM" +msgstr "ق‍.ظ" + +msgid "PM" +msgstr "ب‍.ظ" + +msgid "Year" +msgstr "" +"سال" + +msgid "Month" +msgstr "" +"ماه" + +msgid "Day" +msgstr "" +"روز" + + +msgid "Years" +msgstr "" +"سال" + +msgid "%s Years" +msgstr "%s سال" + +msgid "Centuries" +msgstr "" +"قرن" + +msgid "Thousand Years" +msgstr "" +"هزار سال" + +msgid "Million Years" +msgstr "" +"میلیون سال" + +msgid "Billion (10^9) Years" +msgstr "" +"میلیارد سال" + + + +msgid "Sunday" +msgstr "" +"یک‌شنبه" + +msgid "Monday" +msgstr "" +"دوشنبه" + +msgid "Tuesday" +msgstr "" +"سه‌شنبه" + +msgid "Wednesday" +msgstr "" +"چهارشنبه" + +msgid "Thursday" +msgstr "" +"پنج‌شنبه" + +msgid "Friday" +msgstr "" +"جمعه" + +msgid "Saturday" +msgstr "" +"شنبه" + + +msgid "Sun" +msgstr "" +"۱ ش" + +msgid "Mon" +msgstr "" +"۲ ش" + +msgid "Tue" +msgstr "" +"۳ ش" + +msgid "Wed" +msgstr "" +"۴ ش" + +msgid "Thu" +msgstr "" +"۵ ش" + +msgid "Fri" +msgstr "" +"ج" + +msgid "Sat" +msgstr "" +"ش" + + +msgid "January" +msgstr "" +"ژانویه" + +msgid "February" +msgstr "" +"فوریه" # "فبریه" + +msgid "March" +msgstr "" +"مارس" + +msgid "April" +msgstr "" +"آوریل" # "آپریل" + + +msgid "May" +msgstr "" +"می" # "مه" + +msgid "June" +msgstr "" +"ژوئن" # "جوئن" + +msgid "July" +msgstr "" +"جولای" # "ژوئیه" + +msgid "August" +msgstr "" +"آگوست" # "اوت" + +msgid "September" +msgstr "" +"سپتامبر" + +msgid "October" +msgstr "" +"اکتبر" + +msgid "November" +msgstr "" +"نوامبر" + +msgid "December" +msgstr "" +"دسامبر" + + +msgid "Farvardin" +msgstr "" +"فروردین" + +msgid "Ordibehesht" +msgstr "" +"اردیبهشت" + +msgid "Khordad" +msgstr "" +"خرداد" + +msgid "Teer" +msgstr "" +"تیر" + +msgid "Mordad" +msgstr "" +"مرداد" + +msgid "Shahrivar" +msgstr "" +"شهریور" + +msgid "Mehr" +msgstr "" +"مهر" + +msgid "Aban" +msgstr "" +"آبان" + +msgid "Azar" +msgstr "" +"آذر" + +msgid "Dey" +msgstr "" +"دی" + +msgid "Bahman" +msgstr "" +"بهمن" + +msgid "Esfand" +msgstr "" +"اسفند" + + + +msgid "Xakelêwe" +msgstr "خاکەلێوە" + +msgid "Gullan" +msgstr "گوڵان" + +msgid "Cozerdan" +msgstr "جۆزەردان" + +msgid "Pûşper" +msgstr "پووشپەڕ" + +msgid "Gelawêj" +msgstr "گەلاوێژ" + +msgid "Xermanan" +msgstr "خەرمانان" + +msgid "Rezber" +msgstr "ڕەزبەر" + +msgid "Gelarêzan" +msgstr "گەڵاڕێزان" + +msgid "Sermawez" +msgstr "سەرماوەز" + +msgid "Befranbar" +msgstr "بەفرانبار" + +msgid "Rêbendan" +msgstr "ڕێبەندان" + +msgid "Reşeme" +msgstr "ڕەشەمە" + + + + +msgid "Hamal" +msgstr "حمل" + +msgid "Sawr" +msgstr "ثور" + +msgid "Jawzā" +msgstr "جوزا" + +msgid "Saratān" +msgstr "سرطان" + +msgid "Asad" +msgstr "اسد" + +msgid "Sonbola" +msgstr "سنبله" + +msgid "Mizān" +msgstr "میزان" + +msgid "Aqrab" +msgstr "عقرب" + +msgid "Qaws" +msgstr "قوس" + +msgid "Jadi" +msgstr "جدی" + +msgid "Dalvæ" +msgstr "دلو" + +msgid "Hūt" +msgstr "حوت" + + + + +msgid "Wray" +msgstr "وری" + +msgid "Ǧwayay" +msgstr "غويی" + +msgid "Ǧbargolay" +msgstr "غبرګولی" + +msgid "Čungāx̌" +msgstr "چنګاښ" + +msgid "Zmaray" +msgstr "زمری" + +msgid "Waǵay" +msgstr "وږی" + +msgid "Təla" +msgstr "تله" + +msgid "Laṛam" +msgstr "لړم" + +msgid "Līndəi" +msgstr "ليندۍ" + +msgid "Marǧūmay" +msgstr "مرغومی" + +msgid "Salwāǧa" +msgstr "سلواغه" + +msgid "Kab" +msgstr "كب" + + + +msgid "Muharram" +msgstr "" +"محرم" + +msgid "Safar" +msgstr "" +"صفر" + +msgid "Rabia' 1" +msgstr "" +"ربیع‌الاول" + +msgid "Rabia' 2" +msgstr "" +"ربیع‌الثانی" + +msgid "Jumada 1" +msgstr "" +"جمادی‌الاول" + +msgid "Jumada 2" +msgstr "" +"جمادی‌الثانی" + +msgid "Rajab" +msgstr "" +"رجب" + +msgid "Sha'aban" +msgstr "" +"شعبان" + +msgid "Ramadan" +msgstr "" +"رمضان" + +msgid "Shawwal" +msgstr "" +"شوال" + +msgid "Dhu'l Qidah" +msgstr "" +"ذو‌القعده" + +msgid "Dhu'l Hijjah" +msgstr "" +"ذو‌الحجه" + + +msgid "Meskerem" +msgstr "مسکرم" + +msgid "Tekimt" +msgstr "تکیمت" + +msgid "Hidar" +msgstr "هیدار" + +msgid "Tahsas" +msgstr "طه ساز" + +msgid "Ter" +msgstr "تر" + +msgid "Yekoutit" +msgstr "یکوتیت" + +msgid "Meyabit" +msgstr "مگابیت" + +msgid "Meyaziya" +msgstr "میازیا" + +msgid "Genbot" +msgstr "گین بوت" + +msgid "Sene" +msgstr "سنه" + +msgid "Hamle" +msgstr "حمله" + +msgid "Nahse" +msgstr "نحسه" + +msgid "Pagume" +msgstr "پاگومه" + + +msgid "Tishri" +msgstr "تیشری" + +msgid "Cheshvan" +msgstr "حشوان" + +msgid "Kislev" +msgstr "كیسلو" + +msgid "Tevet" +msgstr "طوت" + +msgid "Shevat" +msgstr "شواط" + +msgid "Adar" +msgstr "آذار" + +msgid "Adar II" +msgstr "آذار ۲" + +msgid "Nisan" +msgstr "نیسان" + +msgid "Iyyar" +msgstr "ایار" + +msgid "Siwan" +msgstr "سیوان" + +msgid "Tamuz" +msgstr "تموز" + +msgid "Av" +msgstr "آو" + +msgid "Elul" +msgstr "ایلول" + + + +msgid "Previous Year" +msgstr "" +"سال قبل" + +msgid "Next Year" +msgstr "" +"سال بعد" + +msgid "Previous Month" +msgstr "" +"ماه قبل" + +msgid "Next Month" +msgstr "" +"ماه بعد" + +msgid "Previous Week" +msgstr "" +"هفتهٔ قبل" + +msgid "Next Week" +msgstr "" +"هفتهٔ بعد" + + +msgid "None" +msgstr "" +"هیچ" + +msgid "Birthday" +msgstr "" +"تولد" + +msgid "Marriage" +msgstr "" +"ازدواج" + +msgid "Obituary" +msgstr "" +"وفات" + +msgid "Note" +msgstr "" +"یادداشت" + + +msgid "Today" +msgstr "" +"امروز" + +msgid "Use Hijri month length data (Iranian official calendar)" +msgstr "" +"استفاده از دادهٔ طول ماه‌های قمری (تقویم رسمی ایران)" + +msgid "Tune Hijri Monthes" +msgstr "" +"تنظیم ماه‌های قمری" + +msgid "Equals to" +msgstr "" +"معادل" + +msgid "Month Length" +msgstr "" +"طول ماه" + +msgid "End Date" +msgstr "" +"تاریخ پایان" + +msgid "Calendars" +msgstr "" +"تقویم‌ها" + +msgid "Calendar Types" +msgstr "" +"نوع تقویم‌ها" + +msgid "Active" +msgstr "" +"فعال" + +msgid "Inactive" +msgstr "" +"غیرفعال" + +msgid "Activate/Inactivate" +msgstr "" +"فعال/غیرفعال کردن" + +msgid "Jalali Month Names" +msgstr "" +"نام ماه‌های جلالی" + +msgid "Iranian" +msgstr "ایرانی" + +msgid "Kurdish" +msgstr "کردی" + +msgid "Dari" +msgstr "دری" + +msgid "Pashto" +msgstr "پشتو" + +msgid "Jalali Calculation Algorithm" +msgstr "" +"الگوریتم محاسبهٔ تاریخ جلالی" + +msgid "33 year algorithm" +msgstr "" +"الگوریتم ۳۳ ساله" + +msgid "2820 year algorithm" +msgstr "" +"الگوریتم ۲۸۲۰ ساله" + +msgid "Hijri Calculation Algorithm" +msgstr "" +"الگوریتم محاسبهٔ تاریخ هجری قمری" + +msgid "Enable/Disable" +msgstr "" +"فعال/غیرفعال کردن" + +msgid "position" +msgstr "" +"مکان" + +msgid "Time" +msgstr "" +"زمان" + +msgid "time" +msgstr "" +"زمان" + + +msgid "Alarm" +msgstr "" +"زنگ" + +msgid "Event" +msgstr "" +"رویداد" + +msgid "Event ID: %s" +msgstr "" +"شمارهٔ رویداد: %s" + +msgid "Group ID: %s" +msgstr "" +"شمارهٔ گروه: %s" + + +msgid "Task" +msgstr "" +"کار" + +msgid "All-Day Task" +msgstr "" +"کار تمام‌روز" + +msgid "Task List" +msgstr "" +"فهرست کارها" + +msgid "Daily Note" +msgstr "" +"یادداشت روزانه" + +msgid "Note Book" +msgstr "" +"دفترچهٔ یادداشت" + +msgid "Yearly Event" +msgstr "" +"رویداد سالیانه" + +msgid "Yearly Events Group" +msgstr "" +"گروه رویدادهای سالیانه" + +msgid "Monthly Event" +msgstr "" +"رویداد ماهیانه" + +msgid "Weekly Event" +msgstr "" +"رویداد هفتگی" + +msgid "Repeat Every " +msgstr "" +"تکرار هر " + + +msgid "Life Time Event" +msgstr "" +"رویداد زیست زمان" + +msgid "Life Time Events Group" +msgstr "" +"گروه رویدادهای زیست زمان" + +msgid "Show Seperated Inputs for Year, Month and Day" +msgstr "" +"نمایش ورودی‌های مجزا برای سال، ماه و روز" + +msgid "Large Scale Event" +msgstr "" +"رویداد مقیاس‌بالا" + +msgid "Large Scale Events Group" +msgstr "" +"گروه رویدادهای مقیاس‌بالا" + +msgid "Custom Event" +msgstr "" +"رویداد دلخواه" + +msgid "Events" +msgstr "" +"رویدادها" + +msgid "Events of Day" +msgstr "" +"رویدادهای روز" +msgid "_Event Manager" +msgstr "" + +"مدیریت _رویدادها" + +msgid "Event Manager" +msgstr "" +"مدیریت رویدادها" + +msgid "Day of Month" +msgstr "" +"روز در ماه" + +msgid "Week Number" +msgstr "" +"شمارهٔ هفته" + +msgid "Every Week" +msgstr "" +"هر هفته" + +msgid "Even Weeks" +msgstr "" +"هفته‌های زوج" + +msgid "Odd Weeks" +msgstr "" +"هفته‌های فرد" + +msgid "Even" +msgstr "" +"زوج" + +msgid "Odd" +msgstr "" +"فرد" + +msgid "Current Week Only" +msgstr "" +"فقط هفتهٔ جاری" + +msgid "Day of Week" +msgstr "" +"روز هفته" + + +msgid "Week-Month" +msgstr "" +"هفته-ماه" + +msgid "Every Month" +msgstr "" +"هر ماه" + + +msgid "First" +msgstr "" +"اولین" + +msgid "Second" +msgstr "" +"دومین" + +msgid "Third" +msgstr "" +"سومین" + +msgid "Fourth" +msgstr "" +"چهارمین" + +msgid "Last" +msgstr "" +"آخرین" + +msgid " of " +msgstr "" +" از " + +msgid "Time in Day" +msgstr "" +"زمان در روز" + +msgid "Day Time Range" +msgstr "" +"بازهٔ زمانی در روز" + +msgid "Start" +msgstr "" +"شروع" + +msgid "start" +msgstr "" +"شروع" + +msgid "Start Year" +msgstr "" +"سال شروع" + +msgid "End" +msgstr "" +"پایان" + +msgid "end" +msgstr "" +"پایان" + + +msgid "Cycle (Days)" +msgstr "" +"طول دوره (روزها)" + +msgid "Cycle (Weeks)" +msgstr "" +"طول دوره (هفته‌ها)" + +msgid "Cycle (Days & Time)" +msgstr "" +"طول دوره (روزها و زمان)" + + +########## + + +msgid "Repeat: Every %s Days" +msgstr "" +"تکرار: هر %s روز" + +msgid "Repeat: Every %s Weeks" +msgstr "" +"تکرار: هر %s هفته" + + +msgid "Repeat: Every %s Days and %s" +msgstr "" +"تکرار: هر %s روز و %s" + +msgid "Event Type" +msgstr "" +"نوع رویداد" + +msgid "File" +msgstr "" +"فایل" + +msgid "Add File" +msgstr "" +"افزودن فایل" + +msgid "_Add File" +msgstr "" +"_افزودن فایل" + + +msgid "Edit Event" +msgstr "" +"ویرایش رویداد" + +msgid "_Edit Event" +msgstr "" +"_ویرایش رویداد" + + +msgid "Add Event" +msgstr "" +"افزودن رویداد" + + +msgid "Trash" +msgstr "" +"زباله‌دان" + +msgid "Empty Trash" +msgstr "" +"خالی کردن زباله‌دان" + +msgid "Edit Trash" +msgstr "" +"ویرایش زباله‌دان" + +msgid "Move to %s" +msgstr "" +"جابجایی به %s" + +msgid "Day Info" +msgstr "" +"اطلاعات روز" + +msgid "Copy" +msgstr "" +"کپی" + +msgid "_Copy" +msgstr "" +"_کپی" + +msgid "Copy _All" +msgstr "" +"کپی _همه" + +msgid "_Copy Date" +msgstr "" +"_کپی تاریخ" + +msgid "Copy _Date" +msgstr "" +"کپی _تاریخ" + +msgid "Copy _Time" +msgstr "" +"کپی _زمان" + +msgid "Copy to %s" +msgstr "" +"کپی به %s" + +msgid "Copy as %s to..." +msgstr "" +"کپی بعنوان %s به..." + +msgid "Cut" +msgstr "" +"بریدن" + +msgid "Cu_t" +msgstr "" +"_بریدن" + +msgid "Paste" +msgstr "" +"چسباندن" + +msgid "_Paste" +msgstr "" +"_چسباندن" + +msgid "Duplicate" +msgstr "" +"دوتا کردن" +msgid "_Duplicate" +msgstr "" +"_دوتا کردن" + +msgid "Duplicate with All Events" +msgstr "" +"دوتا کردن با تمام رویدادها" + +msgid "Convert to %s" +msgstr "" +"تبدیل به %s" + +msgid "Paste Event" +msgstr "" +"چسباندن رویداد" + +msgid "Event Group" +msgstr "" +"گروه رویدادها" + +msgid "Group" +msgstr "" +"گروه" + +msgid "Expand" +msgstr "" +"گستردن" + +msgid "Expand All" +msgstr "" +"گستردن همه" + +msgid "Collapse All" +msgstr "" +"جمع کردن همه" + +msgid "Show _Description" +msgstr "" +"نمایش _توضیح" + + +msgid "Week" +msgstr "" +"هفته" + +msgid "Week Day" +msgstr "" +"روز هفته" + +msgid "University Term" +msgstr "" +"ترم دانشگاه" + +msgid "Edit University Term and define some Courses before you add a Class/Exam" +msgstr "" +"قبل از اضافه کردن کلاس/امتحان، ترم دانشگاه را ویرایش کنید و چند درس تعریف کنید" + + + +msgid "Course" +msgstr "" +"درس" + +msgid "Deleted Course" +msgstr "" +"درس حذف‌شده" + +msgid "Course List" +msgstr "" +"لیست درس‌ها" + +msgid "Course Name" +msgstr "" +"نام درس" + +msgid "Course Units" +msgstr "" +"واحدهای درس" + +msgid "Units" +msgstr "" +"واحدها" + + +msgid "New Course" +msgstr "" +"درس جدید" + +#msgid "University Class" +#msgstr "" +#"کلاس دانشگاه" + +msgid "Class" +msgstr "" +"کلاس" + +msgid "%s Class" +msgstr "" +"کلاس %s" + +msgid "Class Time Bounds" +msgstr "" +"مرز زمان کلاس‌ها" + +msgid "Exam" +msgstr "" +"امتحان" + +msgid "%s Exam" +msgstr "" +"امتحان %s" + +msgid "View Weekly Schedule" +msgstr "" +"مشاهدهٔ برنامهٔ هفتگی" + + +msgid "Scale" +msgstr "" +"مقیاس" + +msgid "Add Group" +msgstr "" +"افزودن گروه" + +msgid "Add New Group" +msgstr "" +"افزودن گروه تازه" + +msgid "Delete Group" +msgstr "" +"حذف گروه" + +msgid "Edit Group" +msgstr "" +"ویرایش گروه" + +msgid "Group Type" +msgstr "" +"نوع گروه" + +msgid "Title" +msgstr "" +"عنوان" + +msgid "Group Title" +msgstr "" +"عنوان گروه" + +msgid "Imported Events" +msgstr "" +"رویدادهای وارد شده" + +msgid "Color" +msgstr "" +"رنگ" + +msgid "Default Icon" +msgstr "" +"نماد پیش‌فرض" + +msgid "Default Calendar Type" +msgstr "" +"نوع تقویم پیش‌فرض" + +msgid "Event Cache Size" +msgstr "" +"اندازهٔ cache رویدادها" + +msgid "Event Text Seperator" +msgstr "" +"جداکنندهٔ متن رویداد" + +msgid "Using to seperate Summary and Description when displaying event" +msgstr "" +"برای جدا کردن خلاصه و توضیح رویداد هنگاه نمایش رویداد استفاده می‌شود" + + + + +msgid "Move Up" +msgstr "" +"جابجایی به بالا" + +msgid "Move Down" +msgstr "" +"جابجایی به پایین" + +msgid "Press OK if you want to delete group \"%s\" and move its %s events to trash" +msgstr "" +"اگر می‌خواهید گروه «%s» حذف شده و %s رویدادش به زباله‌دان منتقل شوند، دکمهٔ تائید را فشار دهید" + + +msgid "Press OK if you want to move event \"%s\" to trash" +msgstr "" +"اگر می‌خواهید رویداد «%s» به زباله‌دان منتقل شود، دکمهٔ تائید را فشار دهید" + + +msgid "Sort Events" +msgstr "" +"مرتب کردن رویدادها" + +msgid "Sort events of group \"%s\"" +msgstr "" +"مرتب کردن رویدادهای گروه «%s»" + +msgid "Based on" +msgstr "" +"بر اساس" + +msgid "Descending" +msgstr "" +"نزولی" + +msgid "Convert Calendar Type" +msgstr "" +"تبدیل نوع تقویم" + + +msgid "This is going to convert calendar types of all events inside group \"%s\" to a specific type. This operation does not work for Yearly events and also some of Custom events. You have to edit those events manually to change calendar type." +msgstr "" +"اینجا نوع تقویم تمام رویدادهای داخل گروه «%s» قرار است به یک نوع خاص تبدیل شود. این عملیات برای رویدادهای سالیانه و همینطور برای بعضی از رویدادهای دلخواه کار نمی‌کند. شما برای تغییر نوع تقویم، باید آن رویدادها را دستی ویرایش کنید." + +msgid "Bulk Edit Events" +msgstr "" +"ویرایش گروهیِ رویدادها" + +msgid "Here you are going to modify all events inside group \"%s\" at once." +msgstr "" +"اینجا شما در حال تغییر دادن همهٔ رویدادهای گروه «%s» به یکباره هستید." + +msgid "Here you are going to modify these %s events at once." +msgstr "" +"اینجا شما در حال تغییر دادن این %s رویداد به یکباره هستید." + + +msgid "You better make a backup from you events before doing this. Just right click on group and select \"Export\" (or a full backup: menu File -> Export)" +msgstr "" +"بهتر است قبل از این کار، یک پشتیبان از رویدادهای خود بگیرید. روی گروه راست کلیک کرده و «صادر کردن» را بزنید (یا یک پشتیبان کامل: منوی فایل -> صادر کردن)" + + +msgid "Change" +msgstr "" +"تغییر" + +msgid "Change if empty" +msgstr "" +"تغییر در صورت خالی بودن" + +msgid "Add to beginning" +msgstr "" +"افزودن به ابتدا" + +msgid "Add to end" +msgstr "" +"افزودن به انتها" + +msgid "Replace text" +msgstr "" +"جایگزینی متن" + +msgid "with" +msgstr "" +"با" + + +msgid "For" +msgstr "" +"برای" + +msgid "Until" +msgstr "" +"تا" + +msgid "Duration" +msgstr "" +"مدت" + +msgid "Default Task Duration" +msgstr "" +"مدت‌زمان پیش‌فرض کار" + + +msgid " Seconds" +msgstr "" +" ثانیه" + +msgid "Show Seconds" +msgstr "" +"نمایش ثانیه" + +msgid " Minutes" +msgstr "" +" دقیقه" + +msgid " Hours" +msgstr "" +" ساعت" + +msgid " Days" +msgstr "" +" روز" + +msgid " Weeks" +msgstr "" +" هفته" + +msgid "Group type \"%s\" can not contain event type \"%s\"" +msgstr "" +"نوع گروه «%s» نمی‌تواند شامل نوع رویداد «%s» باشد" + +msgid "Events Count" +msgstr "" +"تعداد رویدادها" + +msgid "%s events" +msgstr "%s رویداد" + +msgid "contains %s events" +msgstr "" +"شامل %s رویداد" + + +msgid "contains %s events and %s occurences" +msgstr "" +"شامل %s رویداد و %s رخداد" + +msgid "Last Occurrence Time" +msgstr "" +"زمان آخرین رخداد" + +msgid "First Occurrence Time" +msgstr "" +"زمان اولین رخداد" + + + +msgid "Time Line" +msgstr "" +"خط زمان" + +msgid "Week Calendar" +msgstr "" +"تقویم هفتگی" + +msgid "Switch to Week Calendar" +msgstr "" +"تغییر به تقویم هفتگی" + +msgid "Switch to Month Calendar" +msgstr "" +"تغییر به تقویم ماه" + + +msgid "Week Days" +msgstr "" +"روزهای هفته" + +msgid "Events Text" +msgstr "" +"متن رویدادها" + +msgid "Events Icon" +msgstr "" +"نماد رویدادها" + +msgid "Events Box" +msgstr "" +"جعبهٔ رویدادها" + +msgid "Days of Month" +msgstr "" +"روزهای ماه" + +msgid "Direction" +msgstr "" +"جهت" + +msgid "Left to Right" +msgstr "" +"چپ به راست" + +msgid "Right to Left" +msgstr "" +"راست به چپ" + +msgid "Auto" +msgstr "" +"خودکار" + + + +msgid "aboutText" +msgstr "" + +"یک برنامهٔ کامل تقویم که با زبان پایتون نوشته شده است\n" +"کپی‌لفت © ۲۰۰۸-۲۰۱۵ سعید رسولی\n" +"برنامهٔ StarCalendar تحت مجوز همگانی عمومی گنو (GNU GPL) منتشر شده است\n" +"«من برای کاربران می‌جنگم!» (نقل قول از TRON)" + + +msgid "licenseText" +msgstr "" +"‫StarCalendar - یک برنامهٔ کامل تقویم که با زبان پایتون نوشته شده است\n" +"کپی‌رایت © ۲۰۰۸-۲۰۱۵ سعید رسولی\n" +"این یک نرم‌افزار آزاد است؛ شما می‌توانید آن را مجدداً توزیع کنید و/یا تغییر دهید، به شرطی که تحت بندهای مجوز همگانی عمومی گنو (GNU GPL) که توسط بنیاد نرم‌افزار آزاد منتشر شده است، باشد؛ چه نسخهٔ ۳ یا (بنا به انتخاب خود) هر نسخهٔ بعدی این مجوز.\n" +"\n" +"این برنامه منتشر شده است با این امید که مفید واقع شود، اما بدون هیچ گونه ضمانتی؛ حتی بدون ضمانت قابل ارائهٔ تجاری بودنِ کار و یا مناسب بودن برای یک منظور خاص. برای جزئیّات بیشتر، مجوز همگانی عمومی گنو را ببینید.\n" +"\n" +"شما می‌توانید یک کپی از مجوز همگانی عمومی گنو را به همراه این برنامه، یا در سیستم‌های دبیان در فایل\n" +"‎/usr/share/common-licenses/GPL\n" +"مشاهده کنید؛ در غیر این‌صورت به آدرس\n" +"‎http://www.gnu.org/licenses/gpl.txt\n" +"مراجعه کنید." + + + +msgid "Select Today" +msgstr "" +"انتخاب امروز" + +msgid "Resize" +msgstr "" +"تغییر اندازه" + +msgid "Menu" +msgstr "" +"منو" + +msgid "Main Menu" +msgstr "" +"منوی اصلی" + +msgid "More" +msgstr "" +"بیشتر" + +msgid "Date" +msgstr "" +"تاریخ" + +msgid "Date..." +msgstr "" +"تاریخ..." + +msgid "Dates" +msgstr "" +"تاریخ‌ها" + + +msgid "Comment" +msgstr "" +"توضیح" + +msgid "Show in" +msgstr "" +"نمایش در" + + +msgid "Show in Calendar" +msgstr "" +"نمایش در تقویم" + + +msgid "Show in Status Icon (for today)" +msgstr "" +"نمایش در نماد وضعیت (برای امروز)" + + +msgid "Show in applet (for today)" +msgstr "" +"نمایش در اپلت (برای امروز)" + + +msgid "Enable" +msgstr "" +"فعال" + +msgid "Description" +msgstr "" +"توضیح" + +msgid "Show Description" +msgstr "" +"نمایش توضیح" + +msgid "Colorize" +msgstr "" +"رنگی کردن" + +msgid "Show Date" +msgstr "" +"نمایش تاریخ" + +msgid "Show Date in Event Summary" +msgstr "" +"نمایش تاریخ در خلاصهٔ رویداد" + +msgid "Move up" +msgstr "" +"جابجایی به بالا" + +msgid "Move down" +msgstr "" +"جابجایی به پایین" + +msgid "Move to top" +msgstr "" +"جابجایی به ابتدا" + +msgid "Move to bottom" +msgstr "" +"جابجایی به انتها" + +msgid "Add Plugin" +msgstr "" +"افزودن افزونه" + +msgid "_About Plugin" +msgstr "" +"_دربارهٔ افزونه" + +msgid "About Plugin" +msgstr "" +"دربارهٔ افزونه" + +msgid "_Configure" +msgstr "" +"_تنظیم" + +msgid "C_onfigure Plugin" +msgstr "" +"_تنظیم افزونه" + + +msgid "Calendar height" +msgstr "" +"ارتفاع تقویم" + +msgid "Window in Taskbar" +msgstr "" +"پنجره در نواروظیفه" + +msgid "Use AppIndicator" +msgstr "" +"استفاده از AppIndicator" + + + +msgid "Window Controller" +msgstr "" +"کنترل‌کنندهٔ پنجره" + + +msgid "Seperator" +msgstr "" +"جداکننده" + +msgid "Show Digital Clock:" +msgstr "" +"نمایش ساعت رقمی:" + +msgid "On Toolbar" +msgstr "" +"روی نوارابزار" + +msgid "Status Icon" +msgstr "" +"نماد وضعیت" + +msgid "On Status Icon" +msgstr "" +"در نماد وضعیت" + +msgid "On Applet" +msgstr "" +"در اپلت" + + +msgid "Digital Clock Format" +msgstr "" +"فرمت ساعت رقمی" + + + +msgid "Previous" +msgstr "" +"قبل" + +msgid "Next" +msgstr "" +"بعد" + +msgid "Up" +msgstr "" +"بالا" + +msgid "Down" +msgstr "" +"پایین" + +msgid "Back" +msgstr "" +"عقب" + +msgid "Backward" +msgstr "" +"عقب" + +msgid "Forward" +msgstr "" +"جلو" + +msgid "Backward 4 Weeks" +msgstr "" +"۴ هفته به عقب" + +msgid "Forward 4 Weeks" +msgstr "" +"۴ هفته به جلو" + + +msgid "Plus" +msgstr "" +"بعلاوه" + +msgid "Minus" +msgstr "" +"منها" + +msgid "Copy Date" +msgstr "" +"کپی تاریخ" + +msgid "Select Date..." +msgstr "" +"انتخاب تاریخ..." + + +msgid "Style" +msgstr "" +"سَبک" + +msgid "Icon" +msgstr "" +"نماد" + +msgid "Text" +msgstr "" +"متن" + +msgid "Text below Icon" +msgstr "" +"متن زیر نماد" + +msgid "Text beside Icon" +msgstr "" +"متن کنار نماد" + + + +msgid "In E_volution" +msgstr "" +"در ا_وولوشن" + +msgid "In _Sunbird" +msgstr "" +"در _سان‌بِرد" + +msgid "Date Mode" +msgstr "" +"نوع تاریخ" + +msgid "Calendar Type" +msgstr "" +"نوع تقویم" + +msgid "Type" +msgstr "" +"نوع" + +msgid "_General" +msgstr "" +"ع_مومی" + +msgid "A_ppearance" +msgstr "" +"_ظاهر" + +msgid "_Plugins" +msgstr "" +"_افزونه‌ها" + +msgid "A_dvanced" +msgstr "" +"_پیشرفته" + +msgid "Account" +msgstr "" +"حساب" + + +msgid "Accounts" +msgstr "" +"حساب‌ها" + +msgid "Edit Account" +msgstr "" +"ویرایش حساب" + +msgid "Account must be enabled before editing" +msgstr "" +"برای ویرایش حساب، ابتدا باید آن را فعال کنید" + +msgid "Add New Account" +msgstr "" +"افزودن حساب جدید" + +msgid "Account Type" +msgstr "" +"نوع حساب" + + +msgid "Google" +msgstr "" +"گوگل" + +msgid "Email" +msgstr "" +"ایمیل" + +msgid "Online Service" +msgstr "" +"سرویس برخط" + + +msgid "Remote Group" +msgstr "" +"گروه راه‌دور" + +msgid "Fetch" +msgstr "" +"واکشی" + + +msgid "No account selected" +msgstr "" +"هیچ حسابی انتخاب نشده" + +msgid "Fatching" +msgstr "" +"در حال واکشی" + +msgid "Error in fetching remote groups" +msgstr "" +"خطا در واکشی گروه‌های راه‌دور" + +msgid "Synchronize" +msgstr "" +"همگام‌سازی" + +msgid "Error in synchronizing group \"%(group)s\" with account \"%(account)s\"" +msgstr "" +"خطا در همگام‌سازی گروه «%(group)s» با حساب «%(account)s»" + +msgid "Successful synchronizing of group \"%(group)s\" with account \"%(account)s\"" +msgstr "" +"همگام‌سازی موفق گروه «%(group)s» با حساب «%(account)s»" + +msgid "HTTP Error" +msgstr "" +"خطای HTTP" + +msgid "Error Code" +msgstr "" +"کد خطا" + +msgid "Error Message" +msgstr "" +"پیام خطا" + +msgid "Forbidden" +msgstr "" +"ممنوع" +msgid "Authentication request was rejected." +msgstr "" +"درخواست احراز هویت، رد شده است." + +msgid "Authentication has failed" +msgstr "" +"احراز هویت ناموفق بوده است" + +msgid "This service is not available from your country" +msgstr "" +"این سرویس در کشور شما قابل دسترسی نیست" + +#msgid "Failed to find \"code\" in the query parameters of the redirect." + + +msgid "Show main window on start" +msgstr "" +"در هنگام شروع، پنجرهٔ اصلی را نمایش بده" + + +msgid "Run on session startup" +msgstr "" +"در شروعِ نشست اجرا شو" + + +msgid "First day of week" +msgstr "" +"اولین روز هفته" + +msgid "Automatic" +msgstr "" +"خودکار" + + +msgid "Holidays" +msgstr "" +"روزهای تعطیل" + + +msgid "First week of year containts" +msgstr "" +"اولین هفتهٔ سال شامل" + +msgid "First %s of year" +msgstr "" +"اولین %sٔ سال" + + +msgid "First day of year" +msgstr "" +"اولین روز سال" + + + +msgid "Use Desktop Background" +msgstr "" +"استفاده از تصویر زمینهٔ میزکار" + +msgid "If you want to have a transparent calendar (and see your desktop), change the opacity of calendar background color!" +msgstr "" +"اگر می‌خواهید یک تقویم شفاف داشته باشید (و تصویر میزکار دیده‌شود)، مقدار شفافیت رنگ زمینهٔ تقویم را تغییر دهید!" + +msgid "Colors" +msgstr "" +"رنگ‌ها" + +msgid "Font Colors" +msgstr "" +"رنگ نوشته‌ها" + +msgid "Background" +msgstr "" +"زمینه" + +msgid "Border" +msgstr "" +"حاشیه" + +msgid "Normal" +msgstr "" +"عادی" + +msgid "Holiday" +msgstr "" +"روز‌ تعطیل" + +msgid "Inactive Day" +msgstr "" +"روز‌ غیرفعال" + +msgid "Left Margin" +msgstr "" +"حاشیهٔ راست" + +msgid "Top" +msgstr "" +"بالا" + +msgid "Cursor" +msgstr "" +"مکان‌نما" + +msgid "Cursor BG" +msgstr "" +"زمینهٔ مکان‌نما" + +msgid "Diameter Factor" +msgstr "" +"ضریب ضخامت" + + +msgid "Rounding Factor" +msgstr "" +"ضریب گردی" + +msgid "Normal Days" +msgstr "" +"روزهای عادی" + +msgid "Change font family to" +msgstr "" +"تغییر خانوادهٔ فونت به" + +msgid "Fixed Size" +msgstr "" +"اندازهٔ ثابت" + +msgid "Width" +msgstr "" +"عرض" + +msgid "Height" +msgstr "" +"ارتفاع" +msgid "Maximum Height" +msgstr "" +"حداکثر ارتفاع" + + +msgid "Days maximum cache size" +msgstr "" +"حداکثر اندازهٔ cache روزها" + +msgid "Date Format" +msgstr "" +"فرمت تاریخ" + +msgid "Grid" +msgstr "" +"جدول‌بندی" + +msgid "Font" +msgstr "" +"فونت" + +msgid "Application Font" +msgstr "" +"فونت برنامه" + +msgid "Status Icon Font" +msgstr +"فونت نماد وضعیت" + +msgid "Font Family" +msgstr "" +"خانوادهٔ فونت" + +msgid "Text Size Scale" +msgstr "" +"مقیاس اندازهٔ متن" + +msgid "Export" +msgstr "" +"صادر کردن" + +msgid "_Export" +msgstr "" +"_صادر کردن" + + +msgid "Export to %s" +msgstr "" +"صادر کردن به %s" + +msgid "_Export to %s" +msgstr "" +"_صادر کردن به %s" +msgid "Export Group" +msgstr "" +"صادر کردن گروه" + +msgid "Select File" +msgstr "انتخاب فایل" + +msgid "Export: Select File" +msgstr "" +"صادر کردن: انتخاب فایل" + + +msgid "Import" +msgstr "" +"وارد کردن" + +msgid "_Import" +msgstr "" +"_وارد کردن" + +msgid "Import Events" +msgstr "" +"وارد کردن رویدادها" + +msgid "Import from %s" +msgstr "" +"وارد کردن از %s" + +msgid "Import: Select File" +msgstr "" +"وارد کردن: انتخاب فایل" + +msgid "%s groups imported successfully" +msgstr "%s گروه با موفقیت وارد شد" + +msgid "_Search" +msgstr "_جستجو" + +msgid "Search Events" +msgstr "" +"جستجوی رویدادها" + +msgid "_Search Events" +msgstr "" +"_جستجوی رویدادها" + +msgid "Case Sensitive" +msgstr "" +"حساس به حروف بزرگ و کوچک" + +msgid "Modified From" +msgstr "" +"تغییر کرده از" + +msgid "Last Modified" +msgstr "" +"آخرین تغییرات" + +msgid "Search Results" +msgstr "" +"نتایج جستجو" + +msgid "Found %s events" +msgstr "%s رویداد پیدا شد" + +msgid "Columns" +msgstr "" +"ستون‌ها" + +msgid "Error in reading file" +msgstr "" +"خطا در خواندن فایل" + +msgid "Error in loading JSON data" +msgstr "" +"خطا در بارگذاری دادهٔ JSON" + +msgid "Error in importing events" +msgstr "" +"خطا در وارد کردن رویدادها" + +msgid "Format" +msgstr "" +"فرمت" + +#msgid "Compact JSON (StarCalendar)" +#msgstr "JSON فشرده (StarCalendar)" + +#msgid "Pretty JSON (StarCalendar)" +#msgstr "JSON زیبا (StarCalendar)" + + +msgid "Month Range" +msgstr "" +"محدودهٔ ماه‌ها" + +msgid "Current Month" +msgstr "" +"ماه جاری" + +msgid "Whole Current Year" +msgstr "" +"کل سال جاری" + +msgid "Custom" +msgstr "" +"دلخواه" + +msgid "from month" +msgstr "" +"از ماه" + +msgid "to month" +msgstr "" +"تا ماه" + + +msgid "Generated by" +msgstr "" +"ایجاد شده به وسیلهٔ" + +msgid "version" +msgstr "" +"نسخهٔ" + + +msgid "Check for Orphan Events" +msgstr "" +"بررسی رویدادهای یتیم" + +msgid "Orphan Events" +msgstr "" +"رویدادهای یتیم" + + +#msgid "_General" +#msgstr "" +#"_عمومی" + +#msgid "_Appearance" +#msgstr "" +#"_ظاهر" + +#msgid "_Advanced" +#msgstr "" +#"_پیشرفته" + + + +msgid "Select _Today" +msgstr "" +"انتخاب ا_مروز" + +msgid "_Resize" +msgstr "" +"ت_غییر اندازه" + +msgid "_On Top" +msgstr "" +"در _بالا" + +msgid "_Sticky" +msgstr "" +"_چسبناک" + + +msgid "Ad_just System Time" +msgstr "" +"تن_ظیم زمان سیستم" + + + +msgid "Select _Date..." +msgstr "" +"انتخاب _تاریخ..." + + +msgid "Need Restart StarCalendar" +msgstr "" +"نیاز به شروع دوباره" + +msgid "Some preferences need for restart StarCalendar to apply." +msgstr "" +"بعضی از تنظیمات برای اعمال شدن نیاز دارند که StarCalendar دوباره راه‌اندازی شود" + +msgid "_Restart" +msgstr "" +"شروع _دوباره" + +msgid "Seems that you are using a Unity desktop and StarCalendar is not allowed to use Status Icon. Press OK to add StarCalendar to Unity's white list and then restart Unity" +msgstr "" +"به نظر می‌رسد که شما از یک میزکار Unity استفاده می‌کنید و StarCalendar اجازهٔ استفاده از نماد وضعیت را ندارد. دکمهٔ تائید را فشار دهید تا StarCalendar به لیست سفید Unity اضافه شده و سپس Unity دوباره راه‌اندازی شود" + + + +msgid "Error" +msgstr "خطا" + + + + +msgid "Minimize Window" +msgstr "" +"کوچک کردن پنجره" + +msgid "Maximize Window" +msgstr "" +"بزرگ‌ترین حالت پنجره" + +msgid "Close Window" +msgstr "" +"بستن پنجره" + +msgid "Customize" +msgstr "" +"شخصی‌سازی" + +msgid "_Customize" +msgstr "" +"_شخصی‌سازی" + +msgid "Toolbar" +msgstr "" +"نوارابزار" + +msgid "Year & Month Labels" +msgstr "" +"برچسب‌های سال و ماه" + +msgid "Month Calendar" +msgstr "" +"تقویم ماه" +msgid "Day Calendar" +msgstr "" +"تقویم روز" + +msgid "Status Bar" +msgstr "" +"نوار وضعیت" + +msgid "Plugins Text" +msgstr "" +"متن افزونه‌ها" + +msgid "Season Progress Bar" +msgstr "" +"نوار پیشروی فصل‌ها" + +msgid "Spring" +msgstr "" +"بهار" + +msgid "Summer" +msgstr "" +"تابستان" + +msgid "Autumn" +msgstr "" +"پاییز" + +msgid "Winter" +msgstr "" +"زمستان" + + + +msgid "Icon Size" +msgstr "" +"اندازهٔ نماد" + +msgid "Small Toolbar" +msgstr "" +"نوارابزار کوچک" + +msgid "Button" +msgstr "" +"دکمه" + +msgid "Large Toolbar" +msgstr "" +"نوارابزار بزرگ" + +msgid "DND" +msgstr "" +"DND" + +msgid "Dialog" +msgstr "" +"دیالوگ" + +msgid "Buttons Border" +msgstr "" +"حاشیهٔ دکمه‌ها" + + +msgid "Inside Expander" +msgstr "" +"درون expander" + + +msgid "Summary" +msgstr "" +"خلاصه" + +msgid "Rules" +msgstr "" +"قواعد" + +msgid "Add Rule" +msgstr "" +"افزودن قاعده" + + +msgid "Remove" +msgstr "" +"حذف" + +msgid "_Remove" +msgstr "" +"_حذف" + +msgid "Exception" +msgstr "" +"استثناء" + +msgid "%s items" +msgstr "%s مورد" + +msgid "Notifiers" +msgstr "" +"یادآورها" + +msgid "Notification" +msgstr "" +"یادآوری" + +msgid "Notify" +msgstr "" +"یادآوری" + +msgid "before event" +msgstr "" +"قبل از رویداد" + +msgid "Floating Message" +msgstr "" +"پیام شناور" + +msgid "Fill Width" +msgstr "" +"پرکردن عرض" + +msgid "Speed" +msgstr "" +"سرعت" + +msgid "BG Color" +msgstr "" +"رنگ زمینه" + +msgid "Text Color" +msgstr "" +"رنگ متن" + + +msgid "Message Window" +msgstr "" +"پیام پنجره‌ای" + +msgid "needs" +msgstr "" +"نیاز دارد به" + +msgid "Conflict between" +msgstr "" +"تضاد بین" + + +msgid " days" +msgstr "" +" روز" + +msgid "days and" +msgstr "" +"روز و" + +msgid "Tags" +msgstr "" +"برچسب‌ها" + + +msgid "Category" +msgstr "" +"دسته" + +msgid "Business" +msgstr "" +"کسب‌وکار" + +msgid "Personal" +msgstr "" +"شخصی" + +msgid "Favorite" +msgstr "" +"مورد علاقه" + +msgid "Important" +msgstr "" +"مهم" + +msgid "Travel" +msgstr "" +"مسافرت" + +msgid "Appointment" +msgstr "" +"قرار ملاقات" + +msgid "Meeting" +msgstr "" +"جلسه" + +msgid "Phone Call" +msgstr "" +"تماس تلفنی" + +msgid "Education" +msgstr "" +"آموزش" + +msgid "School" +msgstr "" +"مدرسه" + +msgid "University" +msgstr "" +"دانشگاه" + +msgid "College" +msgstr "" +"دانشکده" + + +################################################################################ + +msgid "Islamic Pray Times" +msgstr "اوقات شرعی" + +msgid "Pray Times" +msgstr "اوقات شرعی" + +msgid "Islamic Pray Times Plugin" +msgstr "افزونهٔ اوقات شرعی" + +msgid "Location" +msgstr "مکان" + +msgid "Calculation Method" +msgstr "روش محاسبه" + +msgid "Muslim World League" +msgstr "اتحادیهٔ جهان اسلام" + +msgid "Islamic Society of North America" +msgstr "انجمن اسلامی امریکای شمالی" + +msgid "Egyptian General Authority of Survey" +msgstr "سازمان نقشه‌برداری جامع مصر" + +msgid "Umm Al-Qura University, Makkah" +msgstr "دانشگاه ام‌القری، مکه" + +msgid "University of Islamic Sciences, Karachi" +msgstr "دانشگاه علوم اسلامی، کراچی" + +msgid "Shia Ithna-Ashari, Leva Research Institute, Qum" +msgstr "شیعهٔ دوازده‌امامی، مؤسسهٔ تحقیقاتی لِوا، قم" + +msgid "Institute of Geophysics, University of Tehran" +msgstr "مؤسسهٔ ژئوفیزیک دانشگاه تهران" + +msgid "Calendar Center Council, Institute of Geophysics, University of Tehran" +msgstr "شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران" + + +msgid "Shown Times" +msgstr "اوقات نمایش‌داده شده" + + +msgid "Imsak" +msgstr "امساک" + +msgid "Fajr" +msgstr "اذان صبح" + +msgid "Azan Subh" +msgstr "اذان صبح" + +msgid "Sun Rise" +msgstr "طلوع آفتاب" + +msgid "Sunrise" +msgstr "طلوع آفتاب" + +msgid "Sun Rise (Shorook)" +msgstr "طلوع آفتاب" + +msgid "Azan Dhuhr" +msgstr "اذان ظهر" + +msgid "Dhuhr" +msgstr "اذان ظهر" + +msgid "Asr" +msgstr "نماز عصر" + +msgid "Sun Set" +msgstr "غروب آفتاب" + +msgid "Sunset" +msgstr "غروب آفتاب" + + +msgid "Maghrib" +msgstr "اذان مغرب" + +msgid "Maghreb" +msgstr "اذان مغرب" + +msgid "Azan Maghreb" +msgstr "اذان مغرب" + +msgid "Isha" +msgstr "نماز عشا" + +msgid "Midnight" +msgstr "نیمه‌شب" + +msgid "minutes before fajr" +msgstr "دقیقه قبل از اذان صبح" + +msgid "Azan" +msgstr "اذان" + +msgid "Azan Sound" +msgstr "صوت اذان" + +msgid "Play Azan Sound" +msgstr "پخش صوت اذان" + +msgid "Play Pre-Azan Sound" +msgstr "پخش صوت پیش-اذان" + +msgid "Pre-Azan Sound" +msgstr "صوت پیش-اذان" + + +msgid "File..." +msgstr "فایل..." + +msgid "minutes before azan" +msgstr "دقیقه قبل از اذان" + +msgid "Player" +msgstr "پخش‌کننده" + + + +msgid "Search Cities:" +msgstr "جستجوی شهرها:" + +msgid "City" +msgstr "شهر" + +msgid "Edit Manually" +msgstr "ویرایش دستی" + +msgid "Name:" +msgstr "نام:" + +msgid "Latitude:" +msgstr "عرض جغرافیائی:" + +msgid "Longitude:" +msgstr "طول جغرافیائی:" + +msgid "Calculate Nearest City" +msgstr "محاسبهٔ نزدیک‌ترین شهر" + +msgid "%s kilometers from %s" +msgstr "%s کیلومتر تا %s" + +############################### + +msgid "Time Zone" +msgstr "منطقهٔ زمانی" + +msgid "Timezone" +msgstr "منطقهٔ زمانی" + +msgid "Daylight Saving" +msgstr "صرفه‌جویی نور روز" + +msgid "Recent..." +msgstr "اخیر..." + +msgid "For input times of event" +msgstr "" +"برای زمان‌های ورودی رویداد" + +msgid "\"Time Zone\" property is newly added to events" +msgstr "" +"ویژگی «منطقهٔ زمانی» به‌تازگی به رویدادها اضافه شده است" + + +msgid "But this property needs to be saved for current events" +msgstr "" +"اما این ویژگی باید برای رویدادهای فعلی ذخیره شود" + +msgid "Select the time zone for your current location" +msgstr "" +"منطقهٔ زمانی موقعیت فعلی خود را انتخاب کنید" + +msgid "If you have been in a different time zone while adding some of your event, you need to edit those events manually and change the time zone" +msgstr "" +"اگر هنگام اضافه کردن بعضی از رویدادهای خود، در منطقهٔ زمانی دیگری بوده‌اید، باید آن رویدادها را بصورت دستی ویرایش کنید و منطقهٔ زمانی را تغییر دهید" + +msgid "Time zone for All-Day events will be disabled by default" +msgstr "" +"منطقهٔ زمانی برای رویدادهای تمام-روز غیرفعال خواهد بود" + + +msgid "Time zone is invalid" +msgstr "" +"منطقهٔ زمانی نامعتبر است" + + +msgid "Events are Read-Only because they are locked by another StarCalendar 3.x process" +msgstr "" +"رویدادها قابل تغییر نیستند چون توسط یک پروسهٔ دیگر از StarCalendar 3 قفل شده‌اند" + + +msgid "This event is outside of date range specified in it's group. You probably need to edit group \"%s\" and change \"Start\" or \"End\" values" +msgstr "" +"این رویداد خارج از بازهٔ زمانی است که در گروهش مشخص شده. احتمالاً نیاز دارید که گروه «%s» را ویرایش کرده و مقادیر «شروع» یا «پایان» را تغییر دهید" + + + +############################################################################ +############################################################################ +############################################################################ + + +msgid "Africa" +msgstr "آفریقا" + +msgid "Asia" +msgstr "آسیا" + +msgid "Australasia and Oceania" +msgstr "استرالیا و اقیانوسیه" + +msgid "Central and South America" +msgstr "امریکای مرکزی و شمالی" + +msgid "Europe" +msgstr "اروپا" + +msgid "Middle East" +msgstr "خاورمیانه" + +msgid "North America" +msgstr "امریکای شمالی" + +############################################################################ +############################################################################ + + +msgid "Abadan" +msgstr "آبادان" + +msgid "Abadeh" +msgstr "آباده" + +msgid "Abhar" +msgstr "ابهر" + +msgid "Abyek" +msgstr "آبیک" + +msgid "Ahar" +msgstr "اهر" + +msgid "Ahvaz" +msgstr "اهواز" + +msgid "Aliabad" +msgstr "علی آباد" + +msgid "Aligudarz" +msgstr "الیگودرز" + +msgid "Amol" +msgstr "آمل" + +msgid "Andimeshk" +msgstr "اندیمشک" + +msgid "Andisheh" +msgstr "اندیشه" + +msgid "Arak" +msgstr "اراک" + +msgid "Aran va Bid Gol" +msgstr "آران و بیدگل" + +msgid "Ardabil" +msgstr "اردبیل" + +msgid "Ardakan" +msgstr "اردکان" + +msgid "Asadabad" +msgstr "اسداباد" + +msgid "Astara" +msgstr "آستارا" + +msgid "Azadshahr" +msgstr "آزادشهر" + +msgid "Babol" +msgstr "بابل" + +msgid "Babol Sar" +msgstr "بابلسر" + +msgid "Baft" +msgstr "بافت" + +msgid "Baharestan" +msgstr "بهارستان" + +msgid "Bam" +msgstr "بم" + +msgid "Bandar-e Abbas" +msgstr "بندرعباس" + +msgid "Bandar-e Anzali" +msgstr "بندرانزلی" + +msgid "Bandar-e Emam Khomeyni" +msgstr "بندرامام خمینی" + +msgid "Bandar-e-Gonaveh" +msgstr "بندرگناوه" + +msgid "Bandar-e Mahshahr" +msgstr "بندرماهشهر" + +msgid "Bandar-e Torkeman" +msgstr "بندرترکمن" + +msgid "Baneh" +msgstr "بانه" + +msgid "Behbahan" +msgstr "بهبهان" + +msgid "Behshahr" +msgstr "بهشهر" + +msgid "Bijar" +msgstr "بیجار" + +msgid "Birjand" +msgstr "بیرجند" + +msgid "Bojnurd" +msgstr "بجنورد" + +msgid "Bonab" +msgstr "بناب" + +msgid "Borazjan" +msgstr "برازجان" + +msgid "Borujen" +msgstr "بروجن" + +msgid "Borujerd" +msgstr "بروجرد" + +msgid "Bukan" +msgstr "بوکان" + +msgid "Bumahen" +msgstr "بومهن" + +msgid "Bushehr" +msgstr "بوشهر" + +msgid "Chah Bahar" +msgstr "چاه بهار" + +msgid "Chalus" +msgstr "چالوس" + +msgid "Chenaran" +msgstr "چناران" + +msgid "Damavand" +msgstr "دماوند" + +msgid "Damghan" +msgstr "دامغان" + +msgid "Darab" +msgstr "داراب" + +msgid "Darcheh Piaz" +msgstr "درچه پیاز" + +msgid "Deh Dasht" +msgstr "دهدشت" + +msgid "Dezful" +msgstr "دزفول" + +msgid "Dorud" +msgstr "دورود" + +msgid "Dow Gonbadan" +msgstr "دوگنبدان" + +msgid "Eqlid" +msgstr "اقلید" + +msgid "Esfahan" +msgstr "اصفهان" + +msgid "Esfarayen" +msgstr "اسفراین" + +msgid "Eslamabad-e Gharb" +msgstr "اسلام آباد غرب" + +msgid "Eslamshahr" +msgstr "اسلام شهر" + +msgid "Fasa" +msgstr "فسا" + +msgid "Felavarjan" +msgstr "فلاورجان" + +msgid "Firuzabad" +msgstr "فیروزاباد" + +msgid "Fulad Shahr" +msgstr "فولادشهر" + +msgid "Garmsar" +msgstr "گرمسار" + +msgid "Golpayegan" +msgstr "گلپایگان" + +msgid "Gonbad-e Kavus" +msgstr "گنبدکاووس" + +msgid "Gorgan" +msgstr "گرگان" + +msgid "Hamadan" +msgstr "همدان" + +msgid "Harsin" +msgstr "هرسین" + +msgid "Hashtgerd" +msgstr "هشتگرد" + +msgid "Hashtpar" +msgstr "هشتپر" + +msgid "Ilam" +msgstr "ایلام" + +msgid "Iranshahr" +msgstr "ایرانشهر" + +msgid "Izeh" +msgstr "ایذه" + +msgid "Jahrom" +msgstr "جهرم" + +msgid "Javanrud" +msgstr "جوانرود" + +msgid "Jiroft" +msgstr "جیرفت" + +msgid "Kahnuj" +msgstr "کهنوج" + +msgid "Kamal Shahr" +msgstr "کمال شهر" + +msgid "Kamyaran" +msgstr "کامیاران" + +msgid "Kangavar" +msgstr "کنگاور" + +msgid "Karaj" +msgstr "کرج" + +msgid "Kashan" +msgstr "کاشان" + +msgid "Kashmar" +msgstr "کاشمر" + +msgid "Kazerun" +msgstr "کازرون" + +msgid "Kerman" +msgstr "کرمان" + +msgid "Kermanshah" +msgstr "کرمانشاه" + +msgid "Khalkhal" +msgstr "خلخال" + +msgid "Khash" +msgstr "خاش" + +msgid "Khomeynishahr" +msgstr "خمینی شهر" + +msgid "Khorramabad" +msgstr "خرم آباد" + +msgid "Khorramdareh" +msgstr "خرمدره" + +msgid "Khorramshahr" +msgstr "خرمشهر" + +msgid "Khowmeyn" +msgstr "خمین" + +msgid "Khvorasgan" +msgstr "خوراسگان" + +msgid "Khvoy" +msgstr "خوی" + +msgid "Kuhdasht" +msgstr "کوهدشت" + +msgid "Lahijan" +msgstr "لاهیجان" + +msgid "Langerud" +msgstr "لنگرود" + +msgid "Lar" +msgstr "لار" + +msgid "Mahabad" +msgstr "مهاباد" + +msgid "Mahdasht" +msgstr "ماهدشت" + +msgid "Maku" +msgstr "ماکو" + +msgid "Malard" +msgstr "ملارد" + +msgid "Malayer" +msgstr "ملایر" + +msgid "Maragheh" +msgstr "مراغه" + +msgid "Marand" +msgstr "مرند" + +msgid "Marivan" +msgstr "مریوان" + +msgid "Marv Dasht" +msgstr "مرودشت" + +msgid "Mashhad" +msgstr "مشهد" + +msgid "Masjed-e Soleyman" +msgstr "مسجدسلیمان" + +msgid "Meshgin Shahr" +msgstr "مشگین شهر" + +msgid "Meshkin Dasht" +msgstr "مشکین دشت" + +msgid "Meybod" +msgstr "میبد" + +msgid "Mianduab" +msgstr "میاندواب" + +msgid "Mianeh" +msgstr "میانه" + +msgid "Minab" +msgstr "میناب" + +msgid "Mobarakeh" +msgstr "مبارکه" + +msgid "Nahavand" +msgstr "نهاوند" + +msgid "Najafabad" +msgstr "نجف‌آباد" + +msgid "Naqadeh" +msgstr "نقده" + +msgid "Naz̨arabad" +msgstr "نظراباد" + +msgid "Neka" +msgstr "نکا" + +msgid "Neyriz" +msgstr "نی ریز" + +msgid "Neyshabur" +msgstr "نیشابور" + +msgid "Nurabad" +msgstr "نور آباد" + +msgid "Nushahr" +msgstr "نوشهر" + +msgid "Omidiyeh" +msgstr "امیدیه" + +msgid "Orumiyeh" +msgstr "ارومیه" + +msgid "Pakdasht" +msgstr "پاکدشت" + +msgid "Parsabad" +msgstr "پارس آباد" + +msgid "Piranshahr" +msgstr "پیرانشهر" + +msgid "Pishva" +msgstr "پیشوا" + +msgid "Qaemshahr" +msgstr "قائم شهر" + +msgid "Qarchak" +msgstr "قرچک" + +msgid "Qazvin" +msgstr "قزوین" + +msgid "Qods" +msgstr "قدس" + +msgid "Qom" +msgstr "قم" + +msgid "Qorveh" +msgstr "قروه" + +msgid "Quchan" +msgstr "قوچان" + +msgid "Rafsanjan" +msgstr "رفسنجان" + +msgid "Ramhormoz" +msgstr "رامهرمز" + +msgid "Resht" +msgstr "رشت" + +msgid "Robaţ Karim" +msgstr "رباط کریم" + +msgid "Sabzevar" +msgstr "سبزوار" + +msgid "Salmas" +msgstr "سلماس" + +msgid "Sanandaj" +msgstr "سنندج" + +msgid "Saqqez" +msgstr "سقز" + +msgid "Sarab" +msgstr "سراب" + +msgid "Saravan" +msgstr "سراوان" + +msgid "Sar Dasht" +msgstr "سردشت" + +msgid "Sari" +msgstr "ساری" + +msgid "Saveh" +msgstr "ساوه" + +msgid "Semnan" +msgstr "سمنان" + +msgid "Shadegan" +msgstr "شادگان" + +msgid "Shahin Shahr" +msgstr "شاهین شهر" + +msgid "Shahrak-e Golestan" +msgstr "شهرک گلستان" + +msgid "Shahr-e Babak" +msgstr "شهربابک" + +msgid "Shahr-e Kord" +msgstr "شهرکرد" + +msgid "Shahreza" +msgstr "شهرضا" + +msgid "Shahriar" +msgstr "شهریار" + +msgid "Shahrud" +msgstr "شاهرود" + +msgid "Shiravan" +msgstr "شیروان" + +msgid "Shiraz" +msgstr "شیراز" + +msgid "Shush" +msgstr "شوش" + +msgid "Shushtar" +msgstr "شوشتر" + +msgid "Sirjan" +msgstr "سیرجان" + +msgid "Sonqor" +msgstr "سنقر" + +msgid "Sowmaeh Sara" +msgstr "صومعه سرا" + +msgid "Susangerd" +msgstr "سوسنگرد" + +msgid "Tabriz" +msgstr "تبریز" + +msgid "Takab" +msgstr "تکاب" + +msgid "Takestan" +msgstr "تاکستان" + +msgid "Taybad" +msgstr "تایباد" + +msgid "Tehran" +msgstr "تهران" + +msgid "Tonekabon" +msgstr "تنکابن" + +msgid "Torbat-e H̨eydariyeh" +msgstr "تربت حیدریه" + +msgid "Torbat-e Jam" +msgstr "تربت جام" + +msgid "Tuyserkan" +msgstr "تویسرکان" + +msgid "Varamin" +msgstr "ورامین" + +msgid "Yasuj" +msgstr "یاسوج" + +msgid "Yazd" +msgstr "یزد" + +msgid "Zabol" +msgstr "زابل" + +msgid "Zahedan" +msgstr "زاهدان" + +msgid "Zanjan" +msgstr "زنجان" + +msgid "Zarand" +msgstr "زرند" + +msgid "Zarrin Shahr" +msgstr "زرین شهر" + +################################################################################ +################################################################################ + +msgid "Afghanistan" +msgstr "افغانستان" + +msgid "Albania" +msgstr "آلبانی" + +msgid "Algeria" +msgstr "الجزایر" + +msgid "Andorra" +msgstr "آندورا" + +msgid "Angola" +msgstr "آنگولا" + +msgid "Antigua & Barbuda" +msgstr "آنتیگوآ و باربودا" + +msgid "Argentina" +msgstr "آرژانتین" + +msgid "Armenia" +msgstr "ارمنستان" + +msgid "Australia" +msgstr "استرالیا" + +msgid "Austria" +msgstr "اتریش" + +msgid "Azerbaijan" +msgstr "آذربایجان" + +msgid "Bahamas" +msgstr "باهاما" + +msgid "Bahrain" +msgstr "بحرین" + +msgid "Bangladesh" +msgstr "بنگلادش" + +msgid "Barbados" +msgstr "باربادوس" + +msgid "Belarus" +msgstr "روسیهٔ سفید (بیلاروس)" + +msgid "Belgium" +msgstr "بلژیک" + +msgid "Belize" +msgstr "بلیز" + +msgid "Benin" +msgstr "بنین" + +msgid "Bhutan" +msgstr "بوتان" + +msgid "Bolivia" +msgstr "بولیوی" + +msgid "Bosnia & Herzegovina" +msgstr "بوسنی و هرزگورین" + +msgid "Botswana" +msgstr "بوتسوانا" + +msgid "Brazil" +msgstr "برزیل" + +msgid "Brunei" +msgstr "برونئی" + +msgid "Bulgaria" +msgstr "بلغارستان" + +msgid "Burkina Faso" +msgstr "بورکینا فاسو" + +msgid "Burundi" +msgstr "بوروندی" + +msgid "Cambodia" +msgstr "کامبوج" + +msgid "Cameroon" +msgstr "کامرون" + +msgid "Canada" +msgstr "کانادا" + +msgid "Cape Verde" +msgstr "کیپ ورد" + +msgid "Central African Republic" +msgstr "افریقای مرکزی" + +msgid "Chad" +msgstr "چاد" + +msgid "Chile" +msgstr "شیلی" + +msgid "China" +msgstr "چین" + +msgid "Colombia" +msgstr "کلمبیا" + +msgid "Comoros" +msgstr "کومور" + +msgid "Congo, Democratic Republic of the" +msgstr "جمهوری دموکرات کنگو (کنشاسا، زئیر)" + +msgid "Congo, Republic of the" +msgstr "جمهوری کنگو (برازاویل)" + +msgid "Costa Rica" +msgstr "کستا ریکا" + +msgid "Cote d'Ivoire" +msgstr "ساحل عاج (کوت دیووآر)" + +msgid "Croatia" +msgstr "کروآسی" + +msgid "Cuba" +msgstr "کوبا" + +msgid "Cyprus" +msgstr "قبرس" + +msgid "Czech Republic" +msgstr "چک" + +msgid "Denmark" +msgstr "دانمارک" + +msgid "Djibouti" +msgstr "جیبوتی" + +msgid "Dominica" +msgstr "دومینیکا" + +msgid "Dominican Republic" +msgstr "دومینیکن" + +msgid "East Timor" +msgstr "تیمور شرقی" + +msgid "Ecuador" +msgstr "اکوادور" + +msgid "Egypt" +msgstr "مصر" + +msgid "El Salvador" +msgstr "السالوادور" + +msgid "Equatorial Guinea" +msgstr "گینهٔ استوایی" + +msgid "Eritrea" +msgstr "اریتره" + +msgid "Estonia" +msgstr "استونی" + +msgid "Ethiopia" +msgstr "اتیوپی" + +msgid "Fiji" +msgstr "فیجی" + +msgid "Finland" +msgstr "فنلاند" + +msgid "France" +msgstr "فرانسه" + +msgid "Gabon" +msgstr "گابون" + +msgid "Gambia" +msgstr "گامبیا" + +msgid "Georgia" +msgstr "گرجستان" + +msgid "Germany" +msgstr "آلمان" + +msgid "Ghana" +msgstr "غنا" + +msgid "Greece" +msgstr "یونان" + +msgid "Grenada" +msgstr "گرنادا" + +msgid "Guatemala" +msgstr "گواتمالا" + +msgid "Guinea" +msgstr "گینه" + +msgid "Guinea Bissau" +msgstr "گینهٔ بیسائو" + +msgid "Guyana" +msgstr "گویان" + +msgid "Haiti" +msgstr "هائیتی | ها ئی تی" + +msgid "Honduras" +msgstr "هندوراس" + +msgid "Hungary" +msgstr "مجارستان" + +msgid "Iceland" +msgstr "ایسلند" + +msgid "India" +msgstr "هند" + +msgid "Indonesia" +msgstr "اندونزی" + +msgid "Iran" +msgstr "ایران" + +msgid "Iraq" +msgstr "عراق" + +msgid "Ireland" +msgstr "ایرلند" + +msgid "Israel" +msgstr "اسرائیل" + +msgid "Italy" +msgstr "ایتالیا" + +msgid "Jamaica" +msgstr "جامائیکا" + +msgid "Japan" +msgstr "ژاپن" + +msgid "Jordan" +msgstr "اردن" + +msgid "Kazakhstan" +msgstr "قزاقستان" + +msgid "Kenya" +msgstr "کنیا" + +msgid "Kiribati" +msgstr "کیریباتی (کیریباس)" + +msgid "Kuwait" +msgstr "کویت" + +msgid "Kyrgyzstan" +msgstr "قرقیزستان" + +msgid "Laos" +msgstr "لائوس" + +msgid "Latvia" +msgstr "لاتویا (لتونی)" + +msgid "Lebanon" +msgstr "لبنان" + +msgid "Lesotho" +msgstr "لسوتو" + +msgid "Liberia" +msgstr "لیبریا" + +msgid "Libya" +msgstr "لیبی" + +msgid "Liechtenstein" +msgstr "لیختن‌اشتاین" + +msgid "Lithuania" +msgstr "لیتوانی" + +msgid "Luxembourg" +msgstr "لوگزامبورگ" + +msgid "Macedonia" +msgstr "مقدونیه" + +msgid "Madagascar" +msgstr "ماداگاسکار" + +msgid "Malawi" +msgstr "مالاوی" + +msgid "Malaysia" +msgstr "مالزیا" + +msgid "Maldives" +msgstr "مالدیو" + +msgid "Mali" +msgstr "مالی" + +msgid "Malta" +msgstr "مالت" + +msgid "Marshall Islands" +msgstr "جزایر مارشال" + +msgid "Mauritania" +msgstr "موریتانی" + +msgid "Mauritius" +msgstr "موریس" + +msgid "Mexico" +msgstr "مکزیک" + +msgid "Micronesia" +msgstr "میکرونزی" + +msgid "Moldova" +msgstr "مولداوی" + +msgid "Monaco" +msgstr "موناکو" + +msgid "Mongolia" +msgstr "مغولستان" + +msgid "Morocco" +msgstr "مغرب (مراکش)" + +msgid "Mozambique" +msgstr "موزامبیک" + +msgid "Myanmar" +msgstr "میانمار (برمه)" + +msgid "Namibia" +msgstr "نامیبیا" + +msgid "Nauru" +msgstr "نائورو" + +msgid "Nepal" +msgstr "نپال" + +msgid "Netherlands" +msgstr "هلند" + +msgid "New Zealand" +msgstr "زلاند نو" + +msgid "Nicaragua" +msgstr "نیکاراگوآ" + +msgid "Niger" +msgstr "نیجر" + +msgid "Nigeria" +msgstr "نیجریه" + +msgid "North Korea" +msgstr "کرهٔ شمالی" + +msgid "Norway" +msgstr "نروژ" + +msgid "Oman" +msgstr "عمان" + +msgid "Pakistan" +msgstr "پاکستان" + +msgid "Palau" +msgstr "پالائو" + +msgid "Palestine" +msgstr "فلسطین" + +msgid "Panama" +msgstr "پاناما" + +msgid "Papua New Guinea" +msgstr "پاپوا گینهٔ نو" + +msgid "Paraguay" +msgstr "پاراگوئه" + +msgid "Peru" +msgstr "پرو" + +msgid "Philippines" +msgstr "فیلیپین" + +msgid "Poland" +msgstr "لهستان" + +msgid "Portugal" +msgstr "پرتغال" + +msgid "Qatar" +msgstr "قطر" + +msgid "Romania" +msgstr "رومانی" + +msgid "Russia" +msgstr "روسیه" + +msgid "Rwanda" +msgstr "روآندا" + +msgid "Samoa" +msgstr "ساموآ" + +msgid "San Marino" +msgstr "سان مازینو" + +msgid "Sao Tome & Principe" +msgstr "سائوتومه و پرنسیپ" + +msgid "Saudi Arabia" +msgstr "عربستان صعودی" + +msgid "Senegal" +msgstr "سنگال" + +msgid "Serbia" +msgstr "صربستان و مونته‌نگرو (یوگوسلاوی)" + +msgid "Seychelles" +msgstr "سیشل" + +msgid "Sierra Leone" +msgstr "سیرالئون" + +msgid "Singapore" +msgstr "سنگاپور" + +msgid "Slovakia" +msgstr "اسلوواکی" + +msgid "Slovenia" +msgstr "اسلوونی" + +msgid "Solomon Islands" +msgstr "جزایر سلیمان" + +msgid "Somalia" +msgstr "سومالی" + +msgid "South Africa" +msgstr "افریقای جنوبی" + +msgid "South Korea" +msgstr "کرهٔ جنوبی" + +msgid "Spain" +msgstr "اسپانیا" + +msgid "Sri Lanka" +msgstr "سری لانکا" + +msgid "St Kitts & Nevis" +msgstr "سنت کیتس و نویس" + +msgid "St Lucia" +msgstr "سنت لوسیا" + +msgid "St Vincent" +msgstr "سنت وینسنت و گرنادین" + +msgid "Sudan" +msgstr "سودان" + +msgid "Suriname" +msgstr "سوینام" + +msgid "Swaziland" +msgstr "سوازیلند" + +msgid "Sweden" +msgstr "سوئد" + +msgid "Switzerland" +msgstr "سوئیس" + +msgid "Syria" +msgstr "سوریه" + +msgid "Taiwan" +msgstr "تایوان" + +msgid "Tajikistan" +msgstr "تاجیکستان" + +msgid "Tanzania" +msgstr "تانزانیا" + +msgid "Thailand" +msgstr "تایلند" + +msgid "Togo" +msgstr "توگو" + +msgid "Tonga" +msgstr "تونگا" + +msgid "Trinidad" +msgstr "ترینیدا و توباگو" + +msgid "Tunisia" +msgstr "تونس" + +msgid "Turkey" +msgstr "ترکیه" + +msgid "Turkmenistan" +msgstr "ترکمنستان" + +msgid "Tuvalu" +msgstr "توولا" + +msgid "Uganda" +msgstr "اوگاندا" + +msgid "Ukraine" +msgstr "اوکراین" + +msgid "United Arab Emirates" +msgstr "امارات متحدهٔ عربی" + +msgid "United Kingdom" +msgstr "انگلستان" + +msgid "United States of America" +msgstr "ایالات متحدهٔ امریکا" + +msgid "Uruguay" +msgstr "اوروگوئه" + +msgid "Uzbekistan" +msgstr "ازبکستان" + +msgid "Vanuatu" +msgstr "وانوآتو" + +msgid "Vatican City" +msgstr "واتیکان" + +msgid "Venezuela" +msgstr "ونزوئلا" + +msgid "Vietnam" +msgstr "ویتنام" + +msgid "Western Sahara" +msgstr "صحرا" + +msgid "Yemen" +msgstr "یمن" + +msgid "Zambia" +msgstr "زامبیا" + +msgid "Zimbabwe" +msgstr "زیمبابوه" + +################################################################################ +################################################################################ + +msgid "Saeed Rasooli " +msgstr "" +"سعید رسولی " + +msgid "Mola Pahnadayan " +msgstr "" +"مولا پهنادایان " + +msgid "Mehdi Bayazee " +msgstr "" +"مهدی بیاضی " + +msgid "Hamid Zarrabi-Zadeh " +msgstr "" +"حمید ضرابی‌زاده " + +msgid "Reza Moradi Ghiasabadi (www.ghiasabadi.com)" +msgstr "" +"رضا مرادی قیاس آبادی (www.ghiasabadi.com)" + +msgid "translator-credits" +msgstr "Saeed Rasooli " diff --git a/locale.d/gtk20.fa.po b/locale.d/gtk20.fa.po new file mode 100644 index 000000000..c68b12400 --- /dev/null +++ b/locale.d/gtk20.fa.po @@ -0,0 +1,2044 @@ +# +msgid "" +msgstr "" +"Project-Id-Version: gtk+ 2.14\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2008-10-17 00:37-0400\n" +"PO-Revision-Date: 2009-06-07 15:35+0430\n" +"Last-Translator: Roozbeh Pournader \n" +"Language-Team: Persian , \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;" + + +#msgid "Yesterday at %d:%d" #????????????????? +#msgstr "دیروز در %d:%d" + + +msgid "_Play" +msgstr "" +"_پخش" + + +## ????????????????? +msgid "_Stop" +msgstr "" +"_توقف" + + +msgid "input method menu|System" +msgstr "" +"سیستم" + + +msgid "Simple" +msgstr "" +"ساده" + + +msgid "Multipress" +msgstr "" +"مالتی‌پرس" + + +### ????????????????????????????????????????????????????????? +msgid "SCIM Bridge Input Method" +msgstr "" +"روش ورودی پل SCIM" + + +msgid "input method menu|SCIM Bridge Input Method" +msgstr "" +"روش ورودی پل SCIM" + + +msgid "SCIM Input Method" +msgstr "" +"روش ورودی SCIM" + + +msgid "input method menu|SCIM Input Method" +msgstr "" +"روش ورودی SCIM" +### ????????????????????????????????????????????????????????? + + +msgid "Thai-Lao" +msgstr "" +"تایلندی-لائوسی" + + +msgid "\"Deepness\" of the color." +msgstr "" +"«عمق» رنگ." + + +msgid "(Empty)" +msgstr "" +"(خالی)" + + +msgid "(None)" +msgstr "" +"(هیچ‌کدام)" + + +msgid "(disabled)" +msgstr "" +"(غیرفعال‌شده)" + + +msgid "(unknown)" +msgstr "" +"(ناشناخته)" + + +msgid "About %s" +msgstr "" +"دربارهٔ %s" + + +msgid "Add the current folder to the bookmarks" +msgstr "" +"اضافه کردن پوشهٔ فعلی به چوب‌الف‌ها" + + +msgid "Add the folder '%s' to the bookmarks" +msgstr "" +"اضافه کردن پوشهٔ «%s» به چوب‌الف‌ها" + + +msgid "Add the selected folders to the bookmarks" +msgstr "" +"اضافه کردن پوشه‌های انتخاب‌شده به چوب‌الف‌ها" + + +msgid "Amharic (EZ+)" +msgstr "" +"امهری (EZ+‎)" + + +msgid "Amount of blue light in the color." +msgstr "" +"میزان نور آبی در رنگ." + + +msgid "Amount of green light in the color." +msgstr "" +"میزان نور سبز در رنگ." + + +msgid "Amount of red light in the color." +msgstr "" +"میزان نور قرمز در رنگ." + + +msgid "Artwork by" +msgstr "" +"طرح‌ها و تصاویر از" + + +msgid "BMP image has bogus header data" +msgstr "" +"داده‌های سرصفحهٔ تصویر BMP جعلی است" + + +msgid "BMP image has unsupported header size" +msgstr "" +"اندازهٔ سرصفحهٔ تصویر BMP پشتیبانی نمی‌شود" + + +msgid "Bad code encountered" +msgstr "" +"برخورد با کد بد" + + +msgid "Best _Fit" +msgstr "" +"بهترین اندازه" + + +msgid "Bits per channel of PNG image is invalid." +msgstr "" +"بیت بر کانال تصویر PNG نامعتبر است." + + +msgid "Bits per channel of transformed PNG is not 8." +msgstr "" +"بیت بر کانال PNG تبدیل‌شده، ۸ نیست." + + +msgid "Brightness of the color." +msgstr "" +"درخشندگی رنگ." + + +msgid "CLASS" +msgstr "" +"رده" + + +msgid "COLORS" +msgstr "" +"رنگ‌ها" + + +msgid "C_reate" +msgstr "" +"_ایجاد" + + +msgid "Cannot allocate TGA header memory" +msgstr "" +"نمی‌توان حافظهٔ سرصفحهٔ TGA را تخصیص داد" + + +msgid "Cannot allocate colormap entries" +msgstr "" +"نمی‌توان مدخلهای نقشه‌رنگ را تخصیص داد" + + +msgid "Cannot allocate colormap structure" +msgstr "" +"نمی‌توان ساختار نقشه‌رنگ تخصیص داد" + + +msgid "Cannot allocate memory for IOBuffer data" +msgstr "" +"نمی‌توان برای داده‌های IOBuffer حافظه تخصیص داد" + + +msgid "Cannot allocate memory for IOBuffer struct" +msgstr "" +"نمی‌توان برای ساختار IOBuffer حافظه تخصیص داد" + + +msgid "Cannot allocate memory for TGA context struct" +msgstr "" +"نمی‌توان برای ساختار زمینهٔ TGA حافظه تخصیص داد" + + +msgid "Cannot allocate memory for loading PNM image" +msgstr "" +"نمی‌توان برای بار کردن تصویر PNM حافظه تخصیص داد" + + +msgid "Cannot allocate memory for loading XPM image" +msgstr "" +"نمی‌توان برای بار کردن تصویر XPM حافظه تخصیص داد" + + +msgid "Cannot allocate new pixbuf" +msgstr "" +"نمی‌توان pixbuf جدید تخصیص داد" + + +msgid "Cannot allocate temporary IOBuffer data" +msgstr "" +"نمی‌توان داده‌های موقت IOBuffer را تخصیص داد" + + +msgid "Cannot change to folder because it is not local" +msgstr "" +"نمی‌توان به پوشه رفت چون محلی نیست" + + +msgid "Cannot read XPM colormap" +msgstr "" +"نمی‌توان نقشه‌رنگ XPM را خواند" + + +msgid "Cannot realloc IOBuffer data" +msgstr "" +"نمی‌توان داده‌های IOBuffer را بازتخصیص کرد" + + +msgid "Cedilla" +msgstr "" +"سدیلا" + + +msgid "Circular table entry in GIF file" +msgstr "" +"مدخل دوری جدول در پروندهٔ GIF" + + +msgid "" +"Click the eyedropper, then click a color anywhere on your screen to select " +"that color." +msgstr "" +"روی قطره‌چکان کلیک کنید، سپس روی رنگی در هر جای صفحه‌تان کلیک کنید تا آن رنگ " +"انتخاب شود" + + +msgid "" +"Click this palette entry to make it the current color. To change this entry, " +"drag a color swatch here or right-click it and select \"Save color here.\"" +msgstr "" +"روی این مدخل تخته‌رنگ کلیک کنید تا رنگ فعلی شود. برای تغییر این مدخل، یک " +"نمونهٔ رنگ را به اینجا بکشید یا روی آن کلیک راست کنید و «ذخیرهٔ رنگ در اینجا» " +"را انتخاب کنید." + + +msgid "Color Selection" +msgstr "" +"انتخاب رنگ" + + +msgid "Color Wheel" +msgstr "" +"چرخ رنگ" + + +msgid "Compressed icons are not supported" +msgstr "" +"شمایل‌های فشرده‌شده پشتیبانی نمی‌شوند" + + +msgid "Could not add a bookmark" +msgstr "" +"نمی‌توان چوب‌الفی اضافه کرد" + + +msgid "" +"Could not find the icon '%s'. The '%s' theme\n" +"was not found either, perhaps you need to install it.\n" +"You can get a copy from:\n" +"\t%s" +msgstr "" +"نمی‌توان شمایل «%s» را یافت. تم «%s»\n" +"را نیز نمی‌توان یافت، شاید لازم باشد نصبش کنید.\n" +"می‌توانید یک نسخه از نشانی زیر بگیرید:\n" +"\t%s" + + +msgid "Could not get image height (bad TIFF file)" +msgstr "" +"نمی‌توان ارتفاع تصویر را گرفت (پروندهٔ TIFF خراب)" + + +msgid "Could not get image width (bad TIFF file)" +msgstr "" +"نمی‌توان عرض تصویر را گرفت (پروندهٔ TIFF خراب)" + + +msgid "Could not get information for file '%s': %s" +msgstr "" +"نمی‌توان برای پروندهٔ «%s» اطلاعات گرفت: %s" + + +msgid "Could not mount %s" +msgstr "" +"نمی‌توان %s را سوار کرد" + + +msgid "Could not remove bookmark" +msgstr "" +"نمی‌توان چوب‌الف را حذف کرد" + + +msgid "Could not retrieve information about the file" +msgstr "" +"نمی‌توان اطلاعاتی دربارهٔ پرونده بازیابی کرد" + + +msgid "Couldn't allocate memory for context buffer" +msgstr "" +"نمی‌توان برای میانگیر زمینه حافظه تخصیص داد" + + +msgid "Couldn't allocate memory for header" +msgstr "" +"نمی‌توان برای سرصفحه حافظه تخصیص داد" + + +msgid "Couldn't allocate memory for line data" +msgstr "" +"نمی‌توان برای داده‌های خط حافظه تخصیص داد" + + +msgid "Couldn't allocate memory for loading JPEG file" +msgstr "" +"نمی‌توان برای بار کردن پروندهٔ JPEG حافظه تخصیص داد" + + +msgid "Couldn't allocate memory for paletted data" +msgstr "" +"نمی‌توان برای داده‌های تخته‌رنگ‌شده حافظه تخصیص داد" + + +msgid "Couldn't convert filename" +msgstr "" +"نمی‌توان نام پرونده را تبدیل کرد" + + +msgid "Couldn't create new pixbuf" +msgstr "" +"نمی‌توان pixbuf ایجاد کرد" + + +msgid "Couldn't recognize the image file format for file '%s'" +msgstr "" +"قالب پروندهٔ تصویر برای پروندهٔ '%s' تشخیص داده نشد" + + +msgid "Couldn't save the rest" +msgstr "" +"نمی‌توان بقیه را ذخیره کرد" + + +msgid "Create Fo_lder" +msgstr "" +"ایجاد پو_شه" + + +msgid "Create in _folder:" +msgstr "" +"ایجاد در _پوشهٔ:" + + +msgid "Credits" +msgstr "" +"دست‌اندرکاران" + + +msgid "C_redits" +msgstr "" +"_دست‌اندرکاران" + + +msgid "Cu_t" +msgstr "" +"_برش" + + +msgid "Cursor hotspot outside image" +msgstr "" +"کانون مکان‌نما خارج از تصویر است" + + +msgid "Cyrillic (Transliterated)" +msgstr "" +"سیریلی (حرف‌نگاری‌شده)" + + +msgid "DISPLAY" +msgstr "" +"نمایش" + + +msgid "De_lete File" +msgstr "" +"_حذف پرونده" + + +msgid "Decrease Indent" +msgstr "" +"کاهش تورفتگی" + + +msgid "Delete File" +msgstr "" +"حذف پرونده" + + +msgid "Desktop" +msgstr "" +"رومیزی" + + +msgid "Didn't get all lines of PCX image" +msgstr "" +"همهٔ خطهای تصویر PCX گرفته نشد" + + +msgid "Dimensions of TIFF image too large" +msgstr "" +"ابعاد تصویر TIFF خیلی بزرگ است" + + +msgid "Disabled" +msgstr "" +"غیرفعال شده" + + +msgid "Documented by" +msgstr "" +"مستندسازی توسط" + + +msgid "Don't batch GDI requests" +msgstr "" +"درخواست‌های GDI دسته نشوند" + + +msgid "Empty" +msgstr "" +"خالی" + + +msgid "Error" +msgstr "" +"خطا" + + +msgid "Error interpreting JPEG image file (%s)" +msgstr "" +"خطا در تفسیر پروندهٔ JPEG ‏(%s)" + + +msgid "Error loading icon: %s" +msgstr "" +"خطا در بار کردن شمایل: %s" + + +msgid "Error renaming file \"%s\" to \"%s\": %s" +msgstr "" +"خطا در تغییر نام پروندهٔ «%s» به «%s»: %s" + + +msgid "Error writing to image file: %s" +msgstr "" +"خطا در نوشتن در پروندهٔ تصویر: %s" + + +msgid "Excess data in file" +msgstr "" +"داده‌های اضافی در پرونده" + + +msgid "FLAGS" +msgstr "" +"پرچم‌ها" + + +msgid "" +"Failed to close '%s' while writing image, all data may not have been saved: %" +"s" +msgstr "" +"بستن «%s» هنگام نوشتن تصویر شکست خورد، ممکن است همهٔ داده‌ها ذخیره نشده باشند: " +"%s" + + +msgid "Failed to load RGB data from TIFF file" +msgstr "" +"نمی‌توان داده‌های RGB را از پروندهٔ TIFF بار کرد" + + +msgid "Failed to load TIFF image" +msgstr "" +"بار کردن پروندهٔ TIFF شکست خورد" + + +msgid "" +"Failed to load animation '%s': reason not known, probably a corrupt " +"animation file" +msgstr "" +"بار کردن پویانمایی '%s' شکست خورد: دلیل آن معلوم نیست، احتمالاً پروندهٔ " +"پویانمایی خراب است" + + +msgid "Failed to load image '%s': %s" +msgstr "" +"بار کردن تصویر '%s' شکست خورد: %s" + + +msgid "" +"Failed to load image '%s': reason not known, probably a corrupt image file" +msgstr "" +"بار کردن تصویر '%s' شکست خورد: دلیل آن معلوم نیست، احتمالاً پروندهٔ تصویری " +"خراب است" + + +msgid "Failed to open '%s' for writing: %s" +msgstr "" +"نتوانست «%s» را برای نوشتن باز کند: %s" + + +msgid "Failed to open TIFF image" +msgstr "" +"نتوانست پروندهٔ TIFF را باز کند" + + +msgid "Failed to open file '%s': %s" +msgstr "" +"نتوانست پروندهٔ '%s' را باز کند: %s" + + +msgid "Failed to open temporary file" +msgstr "" +"نتوانست پروندهٔ موقت را باز کند" + + +msgid "Failed to read from temporary file" +msgstr "" +"نتوانست از پروندهٔ موقت بخواند" + + +msgid "Failed to write to temporary file when loading XBM image" +msgstr "" +"هنگام بار کردن تصویر XNM نتوانست روی پروندهٔ موقت بنویسد" + + +msgid "Failed to write to temporary file when loading XPM image" +msgstr "" +"هنگام بار کردن تصویر XPM نتوانست روی پروندهٔ موقت بنویسد" + + +msgid "Failure reading GIF: %s" +msgstr "" +"خواندن GIF ناموفق بود: %s" + + +msgid "Fatal error in PNG image file: %s" +msgstr "" +"خطای وخیم در پروندهٔ تصویری PNG‏: %s" + + +msgid "Fatal error reading PNG image file" +msgstr "" +"خطای وخیم در خواندن پروندهٔ تصویری PNG" + + +msgid "Fatal error reading PNG image file: %s" +msgstr "" +"خطای وخیم در خواندن پروندهٔ تصویری PNG‏: %s" + + +msgid "File does not appear to be a GIF file" +msgstr "" +"پرونده به‌نظر نمی‌رسد که یک پروندهٔ GIF باشد" + + +msgid "Files" +msgstr "" +"پرونده‌ها" + + +msgid "Find and _Replace" +msgstr "" +"یافتن و _جای‌گزینی" + + +msgid "Fol_ders" +msgstr "" +"پو_شه‌ها" + + +msgid "Folder unreadable: %s" +msgstr "" +"پوشه غیر قابل خواندن است: %s" + + +msgid "Folders" +msgstr "" +"پوشه‌ها" + + +msgid "Font" +msgstr "" +"قلم" + + +msgid "Font Selection" +msgstr "" +"انتخاب قلم" + + +msgid "GIF file was missing some data (perhaps it was truncated somehow?)" +msgstr "" +"بعضی از داده‌های پروندهٔ GIF مفقود شده‌اند (ممکن است پرونده به طریقی قطع شده باشد؟)‏" + + +msgid "" +"GIF image has no global colormap, and a frame inside it has no local " +"colormap." +msgstr "" +"تصویر GIF نقشه‌رنگ سراسری‌ای ندارد، و یکی از چارچوب‌های داخل آن نقشه‌رنگ محلی " +"ندارد." + + +msgid "GIF image is corrupt (incorrect LZW compression)" +msgstr "" +"تصویر GIF خراب است (فشرده‌سازی LZW غلط)" + + +msgid "GIF image loader cannot understand this image." +msgstr "" +"بارکنندهٔ تصویر GIF نمی‌تواند این تصویر را بفهمد." + + +msgid "GIF image was truncated or incomplete." +msgstr "" +"تصویر GIF قطع شده یا ناقص است." + + +msgid "GTK+ Options" +msgstr "" +"گزینه‌های GTK+" + + +msgid "GTK+ debugging flags to set" +msgstr "" +"پرچمهای اشکال‌زدایی GTK+ که باید یک شوند" + + +msgid "GTK+ debugging flags to unset" +msgstr "" +"پرچمهای اشکال‌زدایی GTK+ که باید صفر شوند" + + +msgid "Gamma" +msgstr "" +"گاما" + + +msgid "Gdk debugging flags to set" +msgstr "" +"پرچم‌های اشکال‌زدایی Gdk که باید یک شوند" + + +msgid "Gdk debugging flags to unset" +msgstr "" +"پرچم‌های اشکال‌زدایی Gdk که باید صفر شوند" + + +msgid "IPA" +msgstr "" +"الفبای فونتیک بین‌المللی" + + +msgid "Icon '%s' not present in theme" +msgstr "" +"شمایل «%s» در تم وجود ندارد" + + +msgid "Icon has zero height" +msgstr "" +"ارتفاع شمایل صفر است" + + +msgid "Icon has zero width" +msgstr "" +"عرض شمایل صفر است" + + +msgid "Image file '%s' contains no data" +msgstr "" +"پروندهٔ تصویری '%s' هیچ داده‌ای ندارد" + + +msgid "Image format unknown" +msgstr "" +"قالب تصویر ناشناخته است" + + +msgid "Image has invalid width and/or height" +msgstr "" +"ارتفاع و/یا عرض تصویر نامعتبر است" + + +msgid "Image has unsupported bpp" +msgstr "" +"‏bpp تصویر پشتیبانی نمی‌شود" + + +msgid "Image has zero height" +msgstr "" +"ارتفاع تصویر صفر است" + + +msgid "Image has zero width" +msgstr "" +"عرض تصویر صفر است" + + +msgid "Image header corrupt" +msgstr "" +"سرصفحهٔ تصویر خراب است" + + +msgid "Image pixel data corrupt" +msgstr "" +"داده‌های نقطه‌ای تصویر خراب است" + + +msgid "Image too large to be saved as ICO" +msgstr "" +"تصویر برای ذخیره شدن به‌عنوان ICO خیلی بزرگ است" + + +msgid "Image type '%s' is not supported" +msgstr "" +"تصویر نوع '%s' پشتیبانی نمی‌شود" + + +msgid "" +"Image-loading module %s does not export the proper interface; perhaps it's " +"from a different GTK version?" +msgstr "" +"پیمانهٔ بار کردن پرونده %s رابط مناسب را صادر نمی‌کند؛ شاید از نسخهٔ دیگری از " +"GTK است؟" + + +msgid "Increase Indent" +msgstr "" +"افزایش تورفتگی" + + +msgid "Incremental loading of image type '%s' is not supported" +msgstr "" +"بار کردن افزایشی تصویر نوع «%s» پشتیبانی نمی‌شود" + + +msgid "Information" +msgstr "" +"اطلاعات" + + +msgid "Input" +msgstr "" +"ورودی" + + +msgid "Input _Methods" +msgstr "" +"روش‌های ورودی" + + +msgid "Insufficient memory to load PNG file" +msgstr "" +"حافظه برای بار کردن پروندهٔ PNG کافی نیست" + + +msgid "Insufficient memory to load PNM context struct" +msgstr "" +"حافظه برای بار کردن ساختار زمینهٔ PNM کافی نیست" + + +msgid "Insufficient memory to load PNM file" +msgstr "" +"حافظه برای بار کردن پروندهٔ PNM کافی نیست" + + +msgid "Insufficient memory to load XBM image file" +msgstr "" +"حافظه برای بار کردن پروندهٔ تصویری XBM کافی نیست" + + +msgid "" +"Insufficient memory to load image, try exiting some applications to free " +"memory" +msgstr "" +"حافظه برای بار کردن تصویر کافی نیست، برای آزاد کردن حافظه خروج از بعضی " +"برنامه‌ها را امتحان کنید" + + +msgid "Insufficient memory to open TIFF file" +msgstr "" +"حافظه برای بار کردن پروندهٔ TIFF کافی نیست" + + +msgid "Insufficient memory to save image into a buffer" +msgstr "" +"حافظه برای ذخیره کردن تصویر در میان‌گیر کافی نیست" + + +msgid "Internal error in the GIF loader (%s)" +msgstr "" +"خطای داخلی در بارگذار GIF (%s)" + + +msgid "Inuktitut (Transliterated)" +msgstr "" +"اینوکتیتوت (حرف‌نگاری‌شده)" + + +msgid "Invalid UTF-8" +msgstr "" +"‏UTF-8 نامعتبر" + + +msgid "Invalid XBM file" +msgstr "" +"پروندهٔ XBM نامعتبر" + + +msgid "Invalid file name" +msgstr "" +"نام پروندهٔ نامعتبر" + + +msgid "Invalid header in animation" +msgstr "" +"سرصفحهٔ نامعتبر در پویانمایی" + + +msgid "Invalid header in icon" +msgstr "" +"سرصفحهٔ نامعتبر در شمایل" + + +msgid "" +"JPEG quality must be a value between 0 and 100; value '%s' could not be " +"parsed." +msgstr "" +"کیفیت JPEG باید مقداری بین ۰ و ۱۰۰ باشد؛ مقدار «%s» قابل درک نیست." + + +msgid "Keys for PNG text chunks must be ASCII characters." +msgstr "" +"کلیدهای تکه‌متنهای PNG باید نویسه‌های اَسکی باشند." + + +msgid "" +"Keys for PNG text chunks must have at least 1 and at most 79 characters." +msgstr "" +"کلیدهای تکه‌متنهای PNG باید حداقل یک و حداکثر ۷۹ نویسه داشته باشند." + + +msgid "LRE Left-to-right _embedding" +msgstr "" +"زیرمتن چپ‌به‌راست_" + + +msgid "LRM _Left-to-right mark" +msgstr "" +"نشانهٔ _چپ‌به‌راست" + + +msgid "LRO Left-to-right _override" +msgstr "" +"زیرمتن ا_کیداً چپ‌به‌راست" + + +msgid "License" +msgstr "" +"مجوز" + + +msgid "Load additional GTK+ modules" +msgstr "" +"پیمانه‌های GTK+ بیشتری بار شود" + + +msgid "MODULES" +msgstr "" +"پیمانه‌ها" + + +msgid "Make X calls synchronous" +msgstr "" +"تماسهای با X همگام شوند" + + +msgid "Make all warnings fatal" +msgstr "" +"همهٔ اخطارها وخیم شوند" + + +msgid "Malformed chunk in animation" +msgstr "" +"تکهٔ معیوب در پویانمایی" + + +msgid "Maximum color value in PNM file is 0" +msgstr "" +"حداکثر مقدار رنگ در پروندهٔ PNM صفر است" + + +msgid "Maximum color value in PNM file is too large" +msgstr "" +"مقدار حداکثر رنگ در پروندهٔ PNM خیلی بزرگ است" + + +msgid "Modified" +msgstr "" +"تغییریافته" + + +msgid "NAME" +msgstr "" +"نام" + + +msgid "Name" +msgstr "" +"_نام:" + + +msgid "Name too long" +msgstr "" +"نام خیلی بلند است" + + +msgid "New Folder" +msgstr "" +"پوشهٔ جدید" + + +msgid "No XPM header found" +msgstr "" +"سرصفحهٔ XPM یافت نشد" + + +msgid "No extended input devices" +msgstr "" +"بدون دستگاه ورودی گسترش‌یافته" + + +msgid "No palette found at end of PCX data" +msgstr "" +"در انتهای داده‌های PCX تخته‌رنگی یافته نشد" + + +msgid "Not enough memory to load GIF file" +msgstr "" +"حافظه برای بار کردن پروندهٔ GIF کافی نیست" + + +msgid "Not enough memory to load ICO file" +msgstr "" +"حافظه برای بار کردن پروندهٔ ICO کافی نیست" + + +msgid "Not enough memory to load RAS image" +msgstr "" +"حافظه برای بار کردن تصویر RAS کافی نیست" + + +msgid "Not enough memory to load animation" +msgstr "" +"حافظه برای بار کردن پویانمایی کافی نیست" + + +msgid "Not enough memory to load bitmap image" +msgstr "" +"حافظه برای بار کردن تصویر نقشه‌بیتی کافی نیست" + + +msgid "Not enough memory to load icon" +msgstr "" +"حافظه برای بار کردن شمایل کافی نیست" + + +msgid "Not enough memory to load image" +msgstr "" +"حافظه برای بار کردن تصویر کافی نیست" + + +msgid "Other..." +msgstr "" +"غیره..." + + +msgid "PDF _Pop directional formatting" +msgstr "" +"پایان زیرمتن_" + + +msgid "PNM file has an image height of 0" +msgstr "" +"ارتفاع تصویر پروندهٔ PNM صفر است" + + +msgid "PNM file has an image width of 0" +msgstr "" +"عرض تصویر پروندهٔ PNM صفر است" + + +msgid "PNM file has an incorrect initial byte" +msgstr "" +"بایت ابتدایی پروندهٔ PNM نادرست است" + + +msgid "PNM file is not in a recognized PNM subformat" +msgstr "" +"پروندهٔ PNM در زیرقالب شناخته‌شده‌ای از PNM نیست" + + +msgid "PNM image loader does not support this PNM subformat" +msgstr "" +"بارکنندهٔ تصویر PNM از این زیرقالب پشتیبانی نمی‌کند" + + +msgid "PNM loader expected to find an integer, but didn't" +msgstr "" +"بارکنندهٔ PNM انتظار داشت یک عدد صحیح ببیند، ولی ندید" + + +msgid "Pick a Color" +msgstr "" +"یک رنگ بردارید" + + +msgid "Pick a Font" +msgstr "" +"یک قلم بردارید" + + +msgid "Position on the color wheel." +msgstr "" +"موقعیت روی چرخ رنگ." + + +msgid "Premature end-of-file encountered" +msgstr "" +"پیش از موقع به پایان پرونده برخورد شد" + + +msgid "Print Pre_view" +msgstr "" +"پی_ش‌نمایش چاپ" + + +msgid "Program class as used by the window manager" +msgstr "" +"ردهٔ برنامه، به شکل مورد استفادهٔ مدیر پنجره‌ها" + + +msgid "Program name as used by the window manager" +msgstr "" +"نام برنامه، به شکل مورد استفادهٔ مدیر پنجره‌ها" + + +msgid "Question" +msgstr "" +"سؤال" + + +msgid "RAS image has bogus header data" +msgstr "" +"داده‌های سرصفحه‌ای تصویر RAS جعلی است" + + +msgid "RAS image has unknown type" +msgstr "" +"نوع تصویر RAS ناشناخته است" + + +msgid "RLE Right-to-left e_mbedding" +msgstr "" +"ز_یرمتن راست‌به‌چپ" + + +msgid "RLM _Right-to-left mark" +msgstr "" +"نشانهٔ _راست‌به‌چپ" + + +msgid "RLO Right-to-left o_verride" +msgstr "" +"زیرمتن اکی_داً راست‌به‌چپ" + + +msgid "Raw PNM formats require exactly one whitespace before sample data" +msgstr "" +"قالبهای PNM خام به دقیقاً یک فاصلهٔ خالی قبل از داده‌های نمونه نیاز دارند" + + +msgid "Raw PNM image type is invalid" +msgstr "" +"نوع تصویر PNM خام نامعتبر است" + + +msgid "Really delete file \"%s\"?" +msgstr "" +"پروندهٔ «%s» واقعاً حذف شود؟" + + +msgid "Received invalid color data\n" +msgstr "" +"اطلاعات رنگی نامعتبر دریافت شد\n" + + +msgid "Remove the bookmark '%s'" +msgstr "" +"حذف چوب‌الف «%s»" + + +msgid "Remove the selected bookmark" +msgstr "" +"حذف پوشه‌های انتخاب‌شده" + + +msgid "Rename File" +msgstr "" +"تغییر نام پرونده" + + +msgid "Rename file \"%s\" to:" +msgstr "" +"تغییر نام پروندهٔ «%s» به:" + + +msgid "SCREEN" +msgstr "" +"صفحه" + + +msgid "Same as --no-wintab" +msgstr "" +"مانند --no-wintab" + + +msgid "Save _As" +msgstr "" +"ذخیره ب_عنوان" + + +msgid "Save in _folder:" +msgstr "" +"ذخیره در _پوشهٔ:" + + +msgid "Screen" +msgstr "" +"صفحه‌نمایش" + + +msgid "Select A File" +msgstr "" +"یک پرونده انتخاب کنید" + + +msgid "Select _All" +msgstr "" +"انتخاب _همه" + + +msgid "" +"Select the color you want from the outer ring. Select the darkness or " +"lightness of that color using the inner triangle." +msgstr "" +"رنگی را که می‌خواهید از حلقهٔ خارجی انتخاب کنید. تیرگی یا روشنی آن رنگ را با " +"استفاده از مثلث داخلی انتخاب کنید." + + +msgid "Select which types of files are shown" +msgstr "" +"انتخاب این که کدام نوع پرونده‌ها نمایش داده شوند" + + +msgid "Shortcut %s does not exist" +msgstr "" +"میانبر %s وجود ندارد" + + +msgid "Show GTK+ Options" +msgstr "" +"نشان دادن گزینه‌های GTK+" + + +msgid "Show _Hidden Files" +msgstr "" +"نمایش پرونده‌های _مخفی" + + +msgid "Si_ze:" +msgstr "" +"_اندازه:" + + +msgid "Size" +msgstr "" +"اندازه" + + +msgid "Size of the palette in 8 bit mode" +msgstr "" +"اندازهٔ تخته‌رنگ در حالت ۸ بیتی" + + +msgid "Stack overflow" +msgstr "" +"سرریزی پشته" + + +msgid "TGA image has invalid dimensions" +msgstr "" +"ابعاد پروندهٔ TGA نامعتبر است" + + +msgid "TGA image type not supported" +msgstr "" +"تصویر نوع TGA پشتیبانی نمی‌شود" + + +msgid "TIFFClose operation failed" +msgstr "" +"عملیات TIFFClose ناموفق بود" + + +msgid "The ANI image format" +msgstr "" +"قالب تصویر ANI" + + +msgid "The BMP image format" +msgstr "" +"قالب تصویر BMP" + + +msgid "The GIF image format" +msgstr "" +"قالب تصویر GIF" + + +msgid "The ICO image format" +msgstr "" +"قالب تصویر ICO" + + +msgid "The JPEG image format" +msgstr "" +"قالب تصویر JPEG" + + +msgid "The PCX image format" +msgstr "" +"قالب تصویر PCX" + + +msgid "The PNG image format" +msgstr "" +"قالب تصویر PNG" + + +msgid "The PNM/PBM/PGM/PPM image format family" +msgstr "" +"قالب تصویر خانوادهٔ PNM/PBM/PGM/PPM" + + +msgid "The TIFF image format" +msgstr "" +"قالب تصویر TIFF" + + +msgid "The Targa image format" +msgstr "" +"قالب تصویر تارگا" + + +msgid "The WBMP image format" +msgstr "" +"قالب تصویری WBMP" + + +msgid "The XBM image format" +msgstr "" +"قالب تصویر XBM" + + +msgid "The XPM image format" +msgstr "" +"قالب تصویر XPM" + + +msgid "" +"The color you've chosen. You can drag this color to a palette entry to save " +"it for use in the future." +msgstr "" +"رنگی که انتخاب کرده‌اید. می‌توانید این رنگ را به یک مدخل تخته‌رنگ بکشید تا برای " +"استفاده در آینده ذخیره شود." + + +msgid "" +"The file \"%s\" resides on another machine (called %s) and may not be " +"available to this program.\n" +"Are you sure that you want to select it?" +msgstr "" +"پروندهٔ «%s» روی دستگاه دیگری (به نام %s) است و ممکن است برای این برنامه قابل " +"دسترسی نباشد.\n" +"آیا مطمئنید می‌خواهید انتخابش کنید؟" + + +msgid "The filename \"%s\" contains symbols that are not allowed in filenames" +msgstr "" +"نام پروندهٔ «%s» نمادهایی دارد که در نام پرونده‌ها مجاز نیستند" + + +msgid "" +"The filename \"%s\" couldn't be converted to UTF-8. (try setting the " +"environment variable G_FILENAME_ENCODING): %s" +msgstr "" +"نام پروندهٔ «%s» نمی‌تواند به UTF-8 تبدیل شود. (تنظیم متغیر محیطی " +"G_FILENAME_ENCODING را امتحان کنید): %s" + + +msgid "The folder contents could not be displayed" +msgstr "" +"محتویات پوشه نمی‌تواند نمایش داده شود" + + +msgid "The folder could not be created" +msgstr "" +"پوشه نمی‌تواند ایجاد شود" + + +msgid "" +"The folder name \"%s\" contains symbols that are not allowed in filenames" +msgstr "" +"نام پوشهٔ «%s» نمادهایی دارد که در نام پرونده‌ها مجاز نیستند" + + +msgid "The license of the program" +msgstr "" +"مجوز برنامه" + + +msgid "" +"The previously-selected color, for comparison to the color you're selecting " +"now. You can drag this color to a palette entry, or select this color as " +"current by dragging it to the other color swatch alongside." +msgstr "" +"رنگی که پیش‌تر انتخاب شده بود، برای مقایسه با رنگی که حالا دارید انتخاب " +"می‌کنید. می‌توانید این رنگ را تا یک مدخل تخته‌رنگ بکشید، یا با کشیدنش به نمونهٔ " +"دیگر در کنار آن، این رنگ را به عنوان رنگ فعلی انتخاب کنید." + + +msgid "This build of gdk-pixbuf does not support saving the image format: %s" +msgstr "" +"این ساخت gdk-pixbuf از ذخیرهٔ این قالب تصویری پشتیبانی نمی‌کند: %s" + + +msgid "Tigrigna-Eritrean (EZ+)" +msgstr "" +"تیگرینیایی-اریتره‌ای (EZ+‎)" + + +msgid "Tigrigna-Ethiopian (EZ+)" +msgstr "" +"تیگرینیایی-اریتره‌ای (EZ+‎)" + + +msgid "Topdown BMP images cannot be compressed" +msgstr "" +"تصاویر BMP ازبالا‌به‌پایین نمی‌توانند فشرده باشند" + + +msgid "Transformed PNG has unsupported number of channels, must be 3 or 4." +msgstr "" +"تعداد کانالهای PNG تبدیل‌شده پشتیبانی نمی‌شود، باید ۳ یا ۴ تا باشد." + + +msgid "Transformed PNG has zero width or height." +msgstr "" +"عرض یا ارتفاع PNG تبدیل‌شده صفر است." + + +msgid "Transformed PNG not RGB or RGBA." +msgstr "" +"PNG تبدیل‌شده RGB یا RGBA نیست." + + +msgid "Translated by" +msgstr "" +"ترجمه از" + + +msgid "Transparency of the color." +msgstr "" +"شفافیت رنگ" + + +msgid "Type name of new folder" +msgstr "" +"نام پوشهٔ جدید را وارد کنید" + + +msgid "Unable to find include file: \"%s\"" +msgstr "" +"نمی‌تواند پروندهٔ درجی را بیابد: «%s»" + + +msgid "Unable to load image-loading module: %s: %s" +msgstr "" +"نمی‌تواند پیمانهٔ تصویربارکن را بار کند: %s: %s" + + +msgid "Unable to locate image file in pixmap_path: \"%s\"" +msgstr "" +"نمی‌توانو پروندهٔ تصویر را در pixmap_path بیابد: «%s»" + + +msgid "Unable to locate theme engine in module_path: \"%s\"," +msgstr "" +"نمی‌تواند موتور تم را در module_path بیابد: «%s»،" + + +msgid "Unexpected bitdepth for colormap entries" +msgstr "" +"عمق بیتی غیرمنتظره برای مدخلهای نقشه‌رنگ" + + +msgid "Unexpected end of PNM image data" +msgstr "" +"پایان غیرمنتظرهٔ داده‌های تصویر PNM" + + +msgid "Unexpected icon chunk in animation" +msgstr "" +"تکه ‌شمایل غیرمنتظره در پویانمایی" + + +msgid "Unknown" +msgstr "" +"ناشناخته" + + +msgid "Unrecognized image file format" +msgstr "" +"قالب پروندهٔ تصویری ناشناخته" + + +msgid "Unsupported JPEG color space (%s)" +msgstr "" +"فضای رنگ JPEG پشتیبانی نمی‌شود (%s)" + + +msgid "Unsupported animation type" +msgstr "" +"پویانمایی از نوع پشتیبانی‌نشده" + + +msgid "Unsupported icon type" +msgstr "" +"شمایل از نوع پشتیانی‌نشده" + + +msgid "Value for PNG text chunk %s cannot be converted to ISO-8859-1 encoding." +msgstr "" +"مقدار تکه‌متن PNG «%s» را نمی‌توان به کدگذاری ISO-8859-1 تبدیل کرد." + + +msgid "Version %s of the GIF file format is not supported" +msgstr "" +"نسخهٔ %s از قالب پروندهٔ GIF پشتیبانی نمی‌شود" + + +msgid "Vietnamese (VIQR)" +msgstr "" +"ویتنامی (VIQR)" + + +msgid "Warning" +msgstr "" +"اخطار" + + +msgid "Width or height of TIFF image is zero" +msgstr "" +"عرض یا ارتفاع تصویر TIFF صفر است" + + +msgid "Window" +msgstr "" +"پنجره" + + +msgid "Written by" +msgstr "" +"نوشتهٔ" + + +msgid "X Input Method" +msgstr "" +"شیوهٔ ورودی X" + + +msgid "X display to use" +msgstr "" +"نمایش‌گر X مورد استفاده" + + +msgid "X screen to use" +msgstr "" +"صفحهٔ X مورد استفاده" + + +msgid "XPM file has image height <= 0" +msgstr "" +"ارتفاع تصویر پروندهٔ XPM کمتر یا مساوی صفر است" + + +msgid "XPM file has image width <= 0" +msgstr "" +"عرض تصویر پروندهٔ XPM کمتر یا مساوی صفر است" + + +msgid "XPM file has invalid number of colors" +msgstr "" +"تعداد رنگهای پروندهٔ XPM نامعتبر است" + + +msgid "XPM has invalid number of chars per pixel" +msgstr "" +"تعداد نویسه بر نقطهٔ XPM نامعتبر است" + + +msgid "" +"You can enter an HTML-style hexadecimal color value, or simply a color name " +"such as 'orange' in this entry." +msgstr "" +"می‌توانید یک مقدار رنگ شانزده‌شانزدهی به سبک HTML وارد کنید، یا نام انگلیسی یک " +"رنگ مثل «orange» برای نارنجی را وارد کنید." + + +msgid "ZWJ Zero width _joiner" +msgstr "" +"_اتصال مجازی ZWJ" + + +msgid "ZWNJ Zero width _non-joiner" +msgstr "" +"_فاصلهٔ مجازی ZWNJ" + + +msgid "ZWS _Zero width space" +msgstr "" +"فاصلهٔ _بی‌عرض ZWS" + + +msgid "Zoom _In" +msgstr "" +"بزرگنمایی به _داخل" + + +msgid "Zoom _Out" +msgstr "" +"بزرگنمایی به _خارج" + + +msgid "_About" +msgstr "" +"_درباره" + + +msgid "_Add" +msgstr "" +"_افزودن" + + +msgid "_Apply" +msgstr "" +"اِ_عمال" + + +msgid "_Ascending" +msgstr "" +"_صعودی" + + + +msgid "_Blue:" +msgstr "" +"_آبی:" + + +msgid "_Bold" +msgstr "" +"_سیاه" + + +msgid "_Browse for other folders" +msgstr "" +"_مرور برای سایر پوشه‌ها" + + +msgid "_CD-Rom" +msgstr "" +"_سی‌دی-رام" + + +msgid "_Cancel" +msgstr "" +"ان_صراف" + + +msgid "_Clear" +msgstr "" +"_پاک کردن" + + +msgid "_Close" +msgstr "" +"_بستن" + + +msgid "_Color" +msgstr "" +"_رنگ" + + +msgid "_Convert" +msgstr "" +"_تبدیل" + + +msgid "_Copy" +msgstr "" +"_نسخه‌برداری" + + +msgid "_Delete" +msgstr "" +"_حذف" + + +msgid "_Descending" +msgstr "" +"_نزولی" + + +msgid "_Device:" +msgstr "" +"_دستگاه:" + + +msgid "_Edit" +msgstr "" +"_ویرایش" + + +msgid "_Execute" +msgstr "" +"ا_جرا" + + +msgid "_Family:" +msgstr "" +"_خانواده:" + + +msgid "_Files" +msgstr "" +"_پرونده‌ها" + + +msgid "_Find" +msgstr "" +"_یافتن" + + +msgid "_Floppy" +msgstr "" +"دیسک _نرم" + + +msgid "_Folder name:" +msgstr "" +"نام _پوشه:" + + +msgid "_Font" +msgstr "" +"_قلم" + + +msgid "_Gamma value" +msgstr "" +"مقدار _گاما" + + +msgid "_Green:" +msgstr "" +"_سبز:" + + +msgid "_Harddisk" +msgstr "" +"دیسک _سخت" + + +msgid "_Help" +msgstr "" +"_راهنما" + + +msgid "_Home" +msgstr "" +"آ_غازه" + + +msgid "_Hue:" +msgstr "" +"_پرده:" + + +msgid "_Index" +msgstr "" +"_نمایه" + + +msgid "_Insert Unicode Control Character" +msgstr "" +"_درج نویسهٔ کنترلی یونی‌کد" + + +msgid "_Italic" +msgstr "" +"ای_تالیک" + + +msgid "_Jump to" +msgstr "" +"_پرش به" + + +msgid "_License" +msgstr "" +"_مجوز" + + +msgid "_Name:" +msgstr "" +"_نام:" + + +msgid "_Network" +msgstr "" +"_شبکه" + + +msgid "_New" +msgstr "" +"_جدید" + + +msgid "_New Folder" +msgstr "" +"پوشهٔ _جدید" + + +msgid "_No" +msgstr "" +"_نه" + + +msgid "_Normal Size" +msgstr "" +"اندازهٔ _عادی" + + +msgid "_OK" +msgstr "" +"_تأیید" + + +msgid "_Open" +msgstr "" +"_باز کردن" + + +msgid "_Paste" +msgstr "" +"_چسباندن" + + +msgid "_Preferences" +msgstr "" +"_ترجیحات" + + +msgid "_Preview:" +msgstr "" +"_پیش‌نمایش:" + + +msgid "_Print" +msgstr "" +"_چاپ" + + +msgid "_Properties" +msgstr "" +"_ویژگی‌ها" + + +msgid "_Quit" +msgstr "" +"_خروج" + + +msgid "_Red:" +msgstr "" +"_قرمز:" + + +msgid "_Redo" +msgstr "" +"_دوباره" + + +msgid "_Refresh" +msgstr "" +"_نوسازی" + + +msgid "_Remove" +msgstr "" +"_حذف" + + +msgid "_Rename" +msgstr "" +"_تغییر نام" + + +msgid "_Rename File" +msgstr "" +"_تغییر نام پرونده" + + +msgid "_Revert" +msgstr "" +"باز_گشت" + + +msgid "_Saturation:" +msgstr "" +"_غلظت:" + + +msgid "_Save" +msgstr "" +"_ذخیره" + + +msgid "_Save color here" +msgstr "" +"_ذخیرهٔ رنگ در این‌جا" + + +msgid "_Selection: " +msgstr "" +"_انتخاب: " + + +msgid "_Spell Check" +msgstr "" +"_غلطیابی املایی" + + +msgid "_Strikethrough" +msgstr "" +"_خط‌خورده" + + +msgid "_Style:" +msgstr "" +"_سبک:" + + +msgid "_Undelete" +msgstr "" +"احیا" + + +msgid "_Underline" +msgstr "" +"_زیرخط‌دار" + + +msgid "_Undo" +msgstr "" +"_برگشت حرکت" + + +msgid "_Value:" +msgstr "" +"_روشنایی:" + + +msgid "_Yes" +msgstr "" +"_بله" + + +## ??????????????????????????????????? +#msgid "abcdefghijk ABCDEFGHIJK" +#msgstr "abcdefghijk ABCDEFGHIJK" + + + +msgid "calendar:week_start:0" +msgstr "calendar:week_start:6" + + +msgid "default:LTR" +msgstr "" +"default:RTL" + + +msgid "none" +msgstr "" +"هیچ‌کدام" + + +msgid "unsupported RAS image variation" +msgstr "" +"اینگونه تصاویر RAS پشتیبانی نمی‌شوند" + +#, c-format + +msgid "failed to allocate image buffer of %u byte" + +msgid_plural "failed to allocate image buffer of %u bytes" +msgstr[0] "" +"تخصیص یک میانگیر تصویر %Iu بایتی شکست خورد" + +#, c-format + +msgid "Unsupported depth for ICO file: %d" +msgstr "" +"عمق پشتیبانی‌نشده برای پروندهٔ ICO: %Id" + +#, c-format + +msgid "" +"JPEG quality must be a value between 0 and 100; value '%d' is not allowed." +msgstr "" +"کیفیت JPEG باید مقداری بین ۰ و ۱۰۰ باشد؛ مقدار «%Id» مجاز نیست." + +#, c-format + +msgid "Image has unsupported number of %d-bit planes" +msgstr "" +"تعداد صفحه‌های %Idبیتی تصویر پشتیبانی نمی‌شود" + +#, c-format + +msgid "" +"Insufficient memory to store a %ld by %ld image; try exiting some " +"applications to reduce memory usage" +msgstr "" +"حافظه برای ذخیرهٔ یک تصویر %Ild در %Ild کافی نیست؛ سعی کنید از بعضی برنامه‌ها خارج شوید تا مصرف حافظه کمتر شود" + + +#, c-format + +msgid "progress bar label|%d %%" +msgstr "" +"‪٪%Id‬" + +#, c-format + +msgid "Page %u" +msgstr "" +"صفحهٔ %Iu" + +#, c-format + +msgid "Unexpected start tag '%s' on line %d char %d" +msgstr "" +"برچسب شروع غیرمنتظره «%s» در سطر %Id نویسهٔ %Id" + +#, c-format + +msgid "Unexpected character data on line %d char %d" +msgstr "" +"دادهٔ نویسه‌ای غیرمنتظره در سطر %Id نویسهٔ %Id" + diff --git a/locale.d/install b/locale.d/install new file mode 100755 index 000000000..31d7e721e --- /dev/null +++ b/locale.d/install @@ -0,0 +1,14 @@ +#!/bin/bash +myDir="`dirname "$0"`" +if [ -n "$1" ] ; then + targetPrefix="$1" ## no slash at the end +else + targetPrefix="/usr" +fi + +for LANG in fa ; do + msgfmt "$myDir/$LANG.po" -o "$myDir/$LANG.mo" + mkdir -p "$targetPrefix/share/locale/$LANG/LC_MESSAGES" + sudo cp -f "$myDir/$LANG.mo" "$targetPrefix/share/locale/$LANG/LC_MESSAGES/starcal3.mo" +done + diff --git a/locale.d/make-template b/locale.d/make-template new file mode 100755 index 000000000..16218bd89 --- /dev/null +++ b/locale.d/make-template @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +import sys + +ipath = sys.argv[1] + +for line in open(ipath): + if line.startswith('msgid'): + i = line.index('"') + st = line[i+1:-2] + print('msgid "%s"\nmsgstr "%s"\n\n'%(st, st)) + + + + + diff --git a/natz/LICENSE b/natz/LICENSE new file mode 100644 index 000000000..68f85f98a --- /dev/null +++ b/natz/LICENSE @@ -0,0 +1,18 @@ +Copyright (c) 2014 Saeed Rasooli +Copyright (c) 2003-2009 Stuart Bishop + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2 of the License, or (at your option) any later version. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +Also avalable in /usr/share/common-licenses/LGPL on Debian systems +or /usr/share/licenses/common/LGPL/license.txt on ArchLinux + diff --git a/natz/__init__.py b/natz/__init__.py new file mode 100644 index 000000000..85c0dc725 --- /dev/null +++ b/natz/__init__.py @@ -0,0 +1,79 @@ +import os +import os.path +import datetime +from .exceptions import * +from .directory import infoDir + + +_tzinfo_cache = {} + +__all__ = [ + 'timezone', + 'infoDir', +] + + +def timezone(name): + r''' Return a datetime.tzinfo implementation for the given timezone + + >>> from datetime import datetime, timedelta + >>> utc = timezone('UTC') + >>> eastern = timezone('US/Eastern') + >>> eastern.zone + 'US/Eastern' + >>> timezone(unicode('US/Eastern')) is eastern + True + >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc) + >>> loc_dt = utc_dt.astimezone(eastern) + >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' + >>> loc_dt.strftime(fmt) + '2002-10-27 01:00:00 EST (-0500)' + >>> (loc_dt - timedelta(minutes=10)).strftime(fmt) + '2002-10-27 00:50:00 EST (-0500)' + >>> eastern.normalize(loc_dt - timedelta(minutes=10)).strftime(fmt) + '2002-10-27 01:50:00 EDT (-0400)' + >>> (loc_dt + timedelta(minutes=10)).strftime(fmt) + '2002-10-27 01:10:00 EST (-0500)' + + Raises UnknownTimeZoneError if passed an unknown zone. + + >>> try: + ... timezone('Asia/Shangri-La') + ... except UnknownTimeZoneError: + ... print('Unknown') + Unknown + + >>> try: + ... timezone(unicode('\N{TRADE MARK SIGN}')) + ... except UnknownTimeZoneError: + ... print('Unknown') + Unknown + + ''' + from .tzfile import build_tzinfo + from .exceptions import UnknownTimeZoneError + name = str(name) + + if name.upper() == 'UTC': + from .utc import UTC + return UTC() + + if name not in _tzinfo_cache: + name_parts = name.lstrip('/').split('/') + for part in name_parts: + if part == os.path.pardir or os.path.sep in part: + raise ValueError('Bad path segment: %r' % part) + try: + fp = open(os.path.join(infoDir, *name_parts), 'rb') + except IOError: + raise UnknownTimeZoneError(name) + try: + _tzinfo_cache[name] = build_tzinfo(name, fp) + finally: + fp.close() + + return _tzinfo_cache[name] + + + + diff --git a/natz/directory.py b/natz/directory.py new file mode 100644 index 000000000..5c44bc70d --- /dev/null +++ b/natz/directory.py @@ -0,0 +1,13 @@ +import os +import os.path + +infoDir = '/usr/share/zoneinfo' + +if not os.path.isdir(infoDir): + import pytz + pytz_infoDir = os.path.join(os.path.dirname(pytz.__file__), 'zoneinfo') + if os.path.isdir(pytz_infoDir): + infoDir = pytz_infoDir + else: + raise IOError('zoneinfo directory not found, neither "%s" or "%s"'%(infoDir, infoDir_pytz)) + diff --git a/natz/exceptions.py b/natz/exceptions.py new file mode 100644 index 000000000..c0fc5ff68 --- /dev/null +++ b/natz/exceptions.py @@ -0,0 +1,34 @@ +__all__ = [ + 'UnknownTimeZoneError', + 'InvalidTimeError', + 'AmbiguousTimeError', + 'NonExistentTimeError', +] + +class UnknownTimeZoneError(KeyError): + pass + + +class InvalidTimeError(Exception): + pass + +class AmbiguousTimeError(InvalidTimeError): + '''Exception raised when attempting to create an ambiguous wallclock time. + + At the end of a DST transition period, a particular wallclock time will + occur twice (once before the clocks are set back, once after). Both + possibilities may be correct, unless further information is supplied. + + See DstTzInfo.normalize() for more info + ''' + pass + + +class NonExistentTimeError(InvalidTimeError): + '''Exception raised when attempting to create a wallclock time that + cannot exist. + + At the start of a DST transition period, the wallclock time jumps forward. + The instants jumped over never occur. + ''' + diff --git a/natz/local.py b/natz/local.py new file mode 100644 index 000000000..48901014f --- /dev/null +++ b/natz/local.py @@ -0,0 +1,99 @@ +import os +import re +import natz +from .exceptions import UnknownTimeZoneError +from .tzfile import build_tzinfo + +def _tz_from_env(tzenv): + if tzenv[0] == ':': + tzenv = tzenv[1:] + + # TZ specifies a file + if os.path.exists(tzenv): + with open(tzenv, 'rb') as tzFp: + return build_tzinfo('local', tzFp) + + # TZ specifies a zoneinfo zone. + try: + tz = natz.timezone(tzenv) + # That worked, so we return this: + return tz + except UnknownTimeZoneError: + raise UnknownTimeZoneError( + "We don't support non-zoneinfo timezones like %s. \n" + "Please use a timezone in the form of Continent/City") + +def get_localzone(_root='/'): + """Tries to find the local timezone configuration. + + This method prefers finding the timezone name and passing that to natz, + over passing in the localtime file, as in the later case the zoneinfo + name is unknown. + + The parameter _root makes the function look for files like /etc/localtime + beneath the _root directory. This is primarily used by the tests. + In normal usage you call the function without parameters.""" + + tzenv = os.environ.get('TZ') + if tzenv: + return _tz_from_env(tzenv) + + # Now look for distribution specific configuration files + # that contain the timezone name. + tzpath = os.path.join(_root, 'etc/timezone') + if os.path.exists(tzpath): + with open(tzpath, 'rb') as tzFp: + data = tzFp.read() + + # Issue #3 was that /etc/timezone was a zoneinfo file. + # That's a misconfiguration, but we need to handle it gracefully: + if data[:5] != 'TZif2': + etctz = data.strip().decode() + # Get rid of host definitions and comments: + if ' ' in etctz: + etctz, dummy = etctz.split(' ', 1) + if '#' in etctz: + etctz, dummy = etctz.split('#', 1) + return natz.timezone(etctz.replace(' ', '_')) + + # CentOS has a ZONE setting in /etc/sysconfig/clock, + # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and + # Gentoo has a TIMEZONE setting in /etc/conf.d/clock + # We look through these files for a timezone: + + zone_re = re.compile('\s*ZONE\s*=\s*\"') + timezone_re = re.compile('\s*TIMEZONE\s*=\s*\"') + end_re = re.compile('\"') + + for filename in ('etc/sysconfig/clock', 'etc/conf.d/clock'): + tzpath = os.path.join(_root, filename) + if not os.path.exists(tzpath): + continue + with open(tzpath, 'rt') as tzFp: + data = tzFp.readlines() + + for line in data: + # Look for the ZONE= setting. + match = zone_re.match(line) + if match is None: + # No ZONE= setting. Look for the TIMEZONE= setting. + match = timezone_re.match(line) + if match is not None: + # Some setting existed + line = line[match.end():] + etctz = line[:end_re.search(line).start()] + + # We found a timezone + return natz.timezone(etctz.replace(' ', '_')) + + # No explicit setting existed. Use localtime + for filename in ('etc/localtime', 'usr/local/etc/localtime'): + tzpath = os.path.join(_root, filename) + + if not os.path.exists(tzpath): + continue + with open(tzpath, 'rb') as tzFp: + return build_tzinfo('local', tzFp) + + raise UnknownTimeZoneError('Can not find any timezone configuration') + diff --git a/natz/tree.py b/natz/tree.py new file mode 100644 index 000000000..61339d3fc --- /dev/null +++ b/natz/tree.py @@ -0,0 +1,59 @@ +#!/usr/bin/python + +import os +import os.path +from collections import OrderedDict +from .directory import infoDir + +infoDirL = list(os.path.split(infoDir)) + +def _addZoneNode(parentDict, zone, zoneNamesLevel): + path = '/' + os.path.join(*tuple(infoDirL + zone)) + name = zone[-1] + zoneNamesLevel[len(zone)].append(name) + if os.path.isfile(path): + parentDict[name] = '' + elif os.path.isdir(path): + parentDict[name] = OrderedDict() + for chName in sorted(os.listdir(path)): + _addZoneNode( + parentDict[name], + zone + [chName], + zoneNamesLevel, + ) + else: + print('invalid path =', path) + + +def getZoneInfoTree(): + zoneTree = OrderedDict() + zoneNamesLevel = [[] for i in range(4)] + for group in [ + 'Etc', + 'Africa', + 'America', + 'Antarctica', + 'Arctic', + 'Asia', + 'Atlantic', + 'Australia', + 'Brazil', + 'Canada', + 'Chile', + 'Europe', + 'Indian', + 'Mexico', + 'Mideast', + 'Pacific', + ]: + _addZoneNode( + zoneTree, + [group], + zoneNamesLevel, + ) + #zoneNamesList = [] + #for levelNames in zoneNamesLevel: + # zoneNamesList += sorted(levelNames) + #from pprint import pprint ; pprint(zoneTree) + return zoneTree + diff --git a/natz/tzfile.py b/natz/tzfile.py new file mode 100644 index 000000000..101da6d2d --- /dev/null +++ b/natz/tzfile.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +''' +$Id: tzfile.py,v 1.8 2004/06/03 00:15:24 zenzen Exp $ +''' + +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO +from datetime import datetime, timedelta +from struct import unpack, calcsize + +from .tzinfo import StaticTzInfo, DstTzInfo, memorized_ttinfo +from .tzinfo import memorized_datetime, memorized_timedelta + +def _std_string(s): + """Cast a string or byte string to an ASCII string.""" + return str(s.decode('US-ASCII')) + +def build_tzinfo(zone, fp): + head_fmt = '>4s c 15x 6l' + head_size = calcsize(head_fmt) + (magic, format, ttisgmtcnt, ttisstdcnt,leapcnt, timecnt, + typecnt, charcnt) = unpack(head_fmt, fp.read(head_size)) + + # Make sure it is a tzfile(5) file + assert magic == b'TZif', 'Got magic %s' % repr(magic) + + # Read out the transition times, localtime indices and ttinfo structures. + data_fmt = '>%(timecnt)dl %(timecnt)dB %(ttinfo)s %(charcnt)ds' % dict( + timecnt=timecnt, ttinfo='lBB'*typecnt, charcnt=charcnt) + data_size = calcsize(data_fmt) + data = unpack(data_fmt, fp.read(data_size)) + + # make sure we unpacked the right number of values + assert len(data) == 2 * timecnt + 3 * typecnt + 1 + transitions = [memorized_datetime(trans) + for trans in data[:timecnt]] + lindexes = list(data[timecnt:2 * timecnt]) + ttinfo_raw = data[2 * timecnt:-1] + tznames_raw = data[-1] + del data + + # Process ttinfo into separate structs + ttinfo = [] + tznames = {} + i = 0 + while i < len(ttinfo_raw): + # have we looked up this timezone name yet? + tzname_offset = ttinfo_raw[i+2] + if tzname_offset not in tznames: + nul = tznames_raw.find(b'\x00', tzname_offset) + if nul < 0: + nul = len(tznames_raw) + tznames[tzname_offset] = _std_string( + tznames_raw[tzname_offset:nul] + ) + ttinfo.append((ttinfo_raw[i], + bool(ttinfo_raw[i+1]), + tznames[tzname_offset])) + i += 3 + + # Now build the timezone object + if len(transitions) == 0: + ttinfo[0][0], ttinfo[0][2] + cls = type(zone, (StaticTzInfo,), dict( + zone=zone, + _utcoffset=memorized_timedelta(ttinfo[0][0]), + _tzname=ttinfo[0][2])) + else: + # Early dates use the first standard time ttinfo + i = 0 + while ttinfo[i][1]: + i += 1 + if ttinfo[i] == ttinfo[lindexes[0]]: + transitions[0] = datetime.min + else: + transitions.insert(0, datetime.min) + lindexes.insert(0, i) + + # calculate transition info + transition_info = [] + for i in range(len(transitions)): + inf = ttinfo[lindexes[i]] + utcoffset = inf[0] + if not inf[1]: + dst = 0 + else: + for j in range(i-1, -1, -1): + prev_inf = ttinfo[lindexes[j]] + if not prev_inf[1]: + break + dst = inf[0] - prev_inf[0] # dst offset + + # Bad dst? Look further. DST > 24 hours happens when + # a timzone has moved across the international dateline. + if dst <= 0 or dst > 3600*3: + for j in range(i+1, len(transitions)): + stdinf = ttinfo[lindexes[j]] + if not stdinf[1]: + dst = inf[0] - stdinf[0] + if dst > 0: + break # Found a useful std time. + + tzname = inf[2] + + # Round utcoffset and dst to the nearest minute or the + # datetime library will complain. Conversions to these timezones + # might be up to plus or minus 30 seconds out, but it is + # the best we can do. + utcoffset = int((utcoffset + 30) // 60) * 60 + dst = int((dst + 30) // 60) * 60 + transition_info.append(memorized_ttinfo(utcoffset, dst, tzname)) + + cls = type(zone, (DstTzInfo,), dict( + zone=zone, + _utc_transition_times=transitions, + _transition_info=transition_info)) + + return cls() + +if __name__ == '__main__': + import os.path + from pprint import pprint + from .directory import infoDir + tz = build_tzinfo( + 'Australia/Melbourne', + open(os.path.join(infoDir, 'Australia', 'Melbourne'), 'rb') + ) + tz = build_tzinfo( + 'US/Eastern', + open(os.path.join(infoDir, 'US', 'Eastern'), 'rb') + ) + pprint(tz._utc_transition_times) + #print(tz.asPython(4)) + #print(tz.transitions_mapping) + diff --git a/natz/tzinfo.py b/natz/tzinfo.py new file mode 100644 index 000000000..b55260703 --- /dev/null +++ b/natz/tzinfo.py @@ -0,0 +1,559 @@ +'''Base classes and helpers for building zone specific tzinfo classes''' + +from datetime import datetime, timedelta, tzinfo +from bisect import bisect_right + +from .exceptions import AmbiguousTimeError, NonExistentTimeError + +__all__ = [] + +_timedelta_cache = {} +def memorized_timedelta(seconds): + '''Create only one instance of each distinct timedelta''' + try: + return _timedelta_cache[seconds] + except KeyError: + delta = timedelta(seconds=seconds) + _timedelta_cache[seconds] = delta + return delta + +_epoch = datetime.utcfromtimestamp(0) +_datetime_cache = {0: _epoch} +def memorized_datetime(seconds): + '''Create only one instance of each distinct datetime''' + try: + return _datetime_cache[seconds] + except KeyError: + # NB. We can't just do datetime.utcfromtimestamp(seconds) as this + # fails with negative values under Windows (Bug #90096) + dt = _epoch + timedelta(seconds=seconds) + _datetime_cache[seconds] = dt + return dt + +_ttinfo_cache = {} +def memorized_ttinfo(*args): + '''Create only one instance of each distinct tuple''' + try: + return _ttinfo_cache[args] + except KeyError: + ttinfo = ( + memorized_timedelta(args[0]), + memorized_timedelta(args[1]), + args[2] + ) + _ttinfo_cache[args] = ttinfo + return ttinfo + +_notime = memorized_timedelta(0) + +def _to_seconds(td): + '''Convert a timedelta to seconds''' + return td.seconds + td.days * 24 * 60 * 60 + + +class BaseTzInfo(tzinfo): + # Overridden in subclass + _utcoffset = None + _tzname = None + zone = None + + def __str__(self): + return self.zone + + +class StaticTzInfo(BaseTzInfo): + '''A timezone that has a constant offset from UTC + + These timezones are rare, as most locations have changed their + offset at some point in their history + ''' + def fromutc(self, dt): + '''See datetime.tzinfo.fromutc''' + if dt.tzinfo is not None and dt.tzinfo is not self: + raise ValueError('fromutc: dt.tzinfo is not self') + return (dt + self._utcoffset).replace(tzinfo=self) + + def utcoffset(self, dt, is_dst=None): + '''See datetime.tzinfo.utcoffset + + is_dst is ignored for StaticTzInfo, and exists only to + retain compatibility with DstTzInfo. + ''' + return self._utcoffset + + def dst(self, dt, is_dst=None): + '''See datetime.tzinfo.dst + + is_dst is ignored for StaticTzInfo, and exists only to + retain compatibility with DstTzInfo. + ''' + return _notime + + def tzname(self, dt, is_dst=None): + '''See datetime.tzinfo.tzname + + is_dst is ignored for StaticTzInfo, and exists only to + retain compatibility with DstTzInfo. + ''' + return self._tzname + + def localize(self, dt, is_dst=False): + '''Convert naive time to local time''' + if dt.tzinfo is not None: + raise ValueError('Not naive datetime (tzinfo is already set)') + return dt.replace(tzinfo=self) + + def normalize(self, dt, is_dst=False): + '''Correct the timezone information on the given datetime. + + This is normally a no-op, as StaticTzInfo timezones never have + ambiguous cases to correct: + + >>> from natz import timezone + >>> gmt = timezone('GMT') + >>> isinstance(gmt, StaticTzInfo) + True + >>> dt = datetime(2011, 5, 8, 1, 2, 3, tzinfo=gmt) + >>> gmt.normalize(dt) is dt + True + + The supported method of converting between timezones is to use + datetime.astimezone(). Currently normalize() also works: + + >>> la = timezone('America/Los_Angeles') + >>> dt = la.localize(datetime(2011, 5, 7, 1, 2, 3)) + >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' + >>> gmt.normalize(dt).strftime(fmt) + '2011-05-07 08:02:03 GMT (+0000)' + ''' + if dt.tzinfo is self: + return dt + if dt.tzinfo is None: + raise ValueError('Naive time - no tzinfo set') + return dt.astimezone(self) + + def __repr__(self): + return '' % (self.zone,) + + def __reduce__(self): + # Special pickle to zone remains a singleton and to cope with + # database changes. + return unpickler, (self.zone,) + + +class DstTzInfo(BaseTzInfo): + '''A timezone that has a variable offset from UTC + + The offset might change if daylight saving time comes into effect, + or at a point in history when the region decides to change their + timezone definition. + ''' + # Overridden in subclass + _utc_transition_times = None # Sorted list of DST transition times in UTC + _transition_info = None # [(utcoffset, dstoffset, tzname)] corresponding + # to _utc_transition_times entries + zone = None + + # Set in __init__ + _tzinfos = None + _dst = None # DST offset + + def __init__(self, _inf=None, _tzinfos=None): + if _inf: + self._tzinfos = _tzinfos + self._utcoffset, self._dst, self._tzname = _inf + else: + _tzinfos = {} + self._tzinfos = _tzinfos + self._utcoffset, self._dst, self._tzname = self._transition_info[0] + _tzinfos[self._transition_info[0]] = self + for inf in self._transition_info[1:]: + if inf not in _tzinfos: + _tzinfos[inf] = self.__class__(inf, _tzinfos) + + def fromutc(self, dt): + '''See datetime.tzinfo.fromutc''' + if (dt.tzinfo is not None + and getattr(dt.tzinfo, '_tzinfos', None) is not self._tzinfos): + raise ValueError('fromutc: dt.tzinfo is not self') + dt = dt.replace(tzinfo=None) + idx = max(0, bisect_right(self._utc_transition_times, dt) - 1) + inf = self._transition_info[idx] + return (dt + inf[0]).replace(tzinfo=self._tzinfos[inf]) + + def normalize(self, dt): + '''Correct the timezone information on the given datetime + + If date arithmetic crosses DST boundaries, the tzinfo + is not magically adjusted. This method normalizes the + tzinfo to the correct one. + + To test, first we need to do some setup + + >>> from natz import timezone + >>> utc = timezone('UTC') + >>> eastern = timezone('US/Eastern') + >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' + + We next create a datetime right on an end-of-DST transition point, + the instant when the wallclocks are wound back one hour. + + >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc) + >>> loc_dt = utc_dt.astimezone(eastern) + >>> loc_dt.strftime(fmt) + '2002-10-27 01:00:00 EST (-0500)' + + Now, if we subtract a few minutes from it, note that the timezone + information has not changed. + + >>> before = loc_dt - timedelta(minutes=10) + >>> before.strftime(fmt) + '2002-10-27 00:50:00 EST (-0500)' + + But we can fix that by calling the normalize method + + >>> before = eastern.normalize(before) + >>> before.strftime(fmt) + '2002-10-27 01:50:00 EDT (-0400)' + + The supported method of converting between timezones is to use + datetime.astimezone(). Currently, normalize() also works: + + >>> th = timezone('Asia/Bangkok') + >>> am = timezone('Europe/Amsterdam') + >>> dt = th.localize(datetime(2011, 5, 7, 1, 2, 3)) + >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' + >>> am.normalize(dt).strftime(fmt) + '2011-05-06 20:02:03 CEST (+0200)' + ''' + if dt.tzinfo is None: + raise ValueError('Naive time - no tzinfo set') + + # Convert dt in localtime to UTC + offset = dt.tzinfo._utcoffset + dt = dt.replace(tzinfo=None) + dt = dt - offset + # convert it back, and return it + return self.fromutc(dt) + + def localize(self, dt, is_dst=False): + '''Convert naive time to local time. + + This method should be used to construct localtimes, rather + than passing a tzinfo argument to a datetime constructor. + + is_dst is used to determine the correct timezone in the ambigous + period at the end of daylight saving time. + + >>> from natz import timezone + >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' + >>> amdam = timezone('Europe/Amsterdam') + >>> dt = datetime(2004, 10, 31, 2, 0, 0) + >>> loc_dt1 = amdam.localize(dt, is_dst=True) + >>> loc_dt2 = amdam.localize(dt, is_dst=False) + >>> loc_dt1.strftime(fmt) + '2004-10-31 02:00:00 CEST (+0200)' + >>> loc_dt2.strftime(fmt) + '2004-10-31 02:00:00 CET (+0100)' + >>> str(loc_dt2 - loc_dt1) + '1:00:00' + + Use is_dst=None to raise an AmbiguousTimeError for ambiguous + times at the end of daylight saving time + + >>> try: + ... loc_dt1 = amdam.localize(dt, is_dst=None) + ... except AmbiguousTimeError: + ... print('Ambiguous') + Ambiguous + + is_dst defaults to False + + >>> amdam.localize(dt) == amdam.localize(dt, False) + True + + is_dst is also used to determine the correct timezone in the + wallclock times jumped over at the start of daylight saving time. + + >>> pacific = timezone('US/Pacific') + >>> dt = datetime(2008, 3, 9, 2, 0, 0) + >>> ploc_dt1 = pacific.localize(dt, is_dst=True) + >>> ploc_dt2 = pacific.localize(dt, is_dst=False) + >>> ploc_dt1.strftime(fmt) + '2008-03-09 02:00:00 PDT (-0700)' + >>> ploc_dt2.strftime(fmt) + '2008-03-09 02:00:00 PST (-0800)' + >>> str(ploc_dt2 - ploc_dt1) + '1:00:00' + + Use is_dst=None to raise a NonExistentTimeError for these skipped + times. + + >>> try: + ... loc_dt1 = pacific.localize(dt, is_dst=None) + ... except NonExistentTimeError: + ... print('Non-existent') + Non-existent + ''' + if dt.tzinfo is not None: + raise ValueError('Not naive datetime (tzinfo is already set)') + + # Find the two best possibilities. + possible_loc_dt = set() + for delta in [timedelta(days=-1), timedelta(days=1)]: + loc_dt = dt + delta + idx = max(0, bisect_right( + self._utc_transition_times, loc_dt) - 1) + inf = self._transition_info[idx] + tzinfo = self._tzinfos[inf] + loc_dt = tzinfo.normalize(dt.replace(tzinfo=tzinfo)) + if loc_dt.replace(tzinfo=None) == dt: + possible_loc_dt.add(loc_dt) + + if len(possible_loc_dt) == 1: + return possible_loc_dt.pop() + + # If there are no possibly correct timezones, we are attempting + # to convert a time that never happened - the time period jumped + # during the start-of-DST transition period. + if len(possible_loc_dt) == 0: + # If we refuse to guess, raise an exception. + if is_dst is None: + raise NonExistentTimeError(dt) + + # If we are forcing the pre-DST side of the DST transition, we + # obtain the correct timezone by winding the clock forward a few + # hours. + elif is_dst: + return self.localize( + dt + timedelta(hours=6), is_dst=True) - timedelta(hours=6) + + # If we are forcing the post-DST side of the DST transition, we + # obtain the correct timezone by winding the clock back. + else: + return self.localize( + dt - timedelta(hours=6), is_dst=False) + timedelta(hours=6) + + + # If we get this far, we have multiple possible timezones - this + # is an ambiguous case occuring during the end-of-DST transition. + + # If told to be strict, raise an exception since we have an + # ambiguous case + if is_dst is None: + raise AmbiguousTimeError(dt) + + # Filter out the possiblilities that don't match the requested + # is_dst + filtered_possible_loc_dt = [ + p for p in possible_loc_dt + if bool(p.tzinfo._dst) == is_dst + ] + + # Hopefully we only have one possibility left. Return it. + if len(filtered_possible_loc_dt) == 1: + return filtered_possible_loc_dt[0] + + if len(filtered_possible_loc_dt) == 0: + filtered_possible_loc_dt = list(possible_loc_dt) + + # If we get this far, we have in a wierd timezone transition + # where the clocks have been wound back but is_dst is the same + # in both (eg. Europe/Warsaw 1915 when they switched to CET). + # At this point, we just have to guess unless we allow more + # hints to be passed in (such as the UTC offset or abbreviation), + # but that is just getting silly. + # + # Choose the earliest (by UTC) applicable timezone. + sorting_keys = {} + for local_dt in filtered_possible_loc_dt: + key = local_dt.replace(tzinfo=None) - local_dt.tzinfo._utcoffset + sorting_keys[key] = local_dt + first_key = sorted(sorting_keys)[0] + return sorting_keys[first_key] + + def utcoffset(self, dt, is_dst=None): + '''See datetime.tzinfo.utcoffset + + The is_dst parameter may be used to remove ambiguity during DST + transitions. + + >>> from natz import timezone + >>> tz = timezone('America/St_Johns') + >>> ambiguous = datetime(2009, 10, 31, 23, 30) + + >>> tz.utcoffset(ambiguous, is_dst=False) + datetime.timedelta(-1, 73800) + + >>> tz.utcoffset(ambiguous, is_dst=True) + datetime.timedelta(-1, 77400) + + >>> try: + ... tz.utcoffset(ambiguous) + ... except AmbiguousTimeError: + ... print('Ambiguous') + Ambiguous + + ''' + if dt is None: + return None + elif dt.tzinfo is not self: + dt = self.localize(dt, is_dst) + return dt.tzinfo._utcoffset + else: + return self._utcoffset + + def dst(self, dt, is_dst=None): + '''See datetime.tzinfo.dst + + The is_dst parameter may be used to remove ambiguity during DST + transitions. + + >>> from natz import timezone + >>> tz = timezone('America/St_Johns') + + >>> normal = datetime(2009, 9, 1) + + >>> tz.dst(normal) + datetime.timedelta(0, 3600) + >>> tz.dst(normal, is_dst=False) + datetime.timedelta(0, 3600) + >>> tz.dst(normal, is_dst=True) + datetime.timedelta(0, 3600) + + >>> ambiguous = datetime(2009, 10, 31, 23, 30) + + >>> tz.dst(ambiguous, is_dst=False) + datetime.timedelta(0) + >>> tz.dst(ambiguous, is_dst=True) + datetime.timedelta(0, 3600) + >>> try: + ... tz.dst(ambiguous) + ... except AmbiguousTimeError: + ... print('Ambiguous') + Ambiguous + + ''' + if dt is None: + return None + elif dt.tzinfo is not self: + dt = self.localize(dt, is_dst) + return dt.tzinfo._dst + else: + return self._dst + + def tzname(self, dt, is_dst=None): + '''See datetime.tzinfo.tzname + + The is_dst parameter may be used to remove ambiguity during DST + transitions. + + >>> from natz import timezone + >>> tz = timezone('America/St_Johns') + + >>> normal = datetime(2009, 9, 1) + + >>> tz.tzname(normal) + 'NDT' + >>> tz.tzname(normal, is_dst=False) + 'NDT' + >>> tz.tzname(normal, is_dst=True) + 'NDT' + + >>> ambiguous = datetime(2009, 10, 31, 23, 30) + + >>> tz.tzname(ambiguous, is_dst=False) + 'NST' + >>> tz.tzname(ambiguous, is_dst=True) + 'NDT' + >>> try: + ... tz.tzname(ambiguous) + ... except AmbiguousTimeError: + ... print('Ambiguous') + Ambiguous + ''' + if dt is None: + return self.zone + elif dt.tzinfo is not self: + dt = self.localize(dt, is_dst) + return dt.tzinfo._tzname + else: + return self._tzname + + def __repr__(self): + if self._dst: + dst = 'DST' + else: + dst = 'STD' + if self._utcoffset > _notime: + return '' % ( + self.zone, self._tzname, self._utcoffset, dst + ) + else: + return '' % ( + self.zone, self._tzname, self._utcoffset, dst + ) + + def __reduce__(self): + # Special pickle to zone remains a singleton and to cope with + # database changes. + return unpickler, ( + self.zone, + _to_seconds(self._utcoffset), + _to_seconds(self._dst), + self._tzname + ) + + + +def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None): + """Factory function for unpickling natz tzinfo instances. + + This is shared for both StaticTzInfo and DstTzInfo instances, because + database changes could cause a zones implementation to switch between + these two base classes and we can't break pickles on a natz version + upgrade. + """ + import natz + # Raises a KeyError if zone no longer exists, which should never happen + # and would be a bug. + tz = natz.timezone(zone) + + # A StaticTzInfo - just return it + if utcoffset is None: + return tz + + # This pickle was created from a DstTzInfo. We need to + # determine which of the list of tzinfo instances for this zone + # to use in order to restore the state of any datetime instances using + # it correctly. + utcoffset = memorized_timedelta(utcoffset) + dstoffset = memorized_timedelta(dstoffset) + try: + return tz._tzinfos[(utcoffset, dstoffset, tzname)] + except KeyError: + # The particular state requested in this timezone no longer exists. + # This indicates a corrupt pickle, or the timezone database has been + # corrected violently enough to make this particular + # (utcoffset,dstoffset) no longer exist in the zone, or the + # abbreviation has been changed. + pass + + # See if we can find an entry differing only by tzname. Abbreviations + # get changed from the initial guess by the database maintainers to + # match reality when this information is discovered. + for localized_tz in tz._tzinfos.values(): + if (localized_tz._utcoffset == utcoffset + and localized_tz._dst == dstoffset): + return localized_tz + + # This (utcoffset, dstoffset) information has been removed from the + # zone. Add it back. This might occur when the database maintainers have + # corrected incorrect information. datetime instances using this + # incorrect information will continue to do so, exactly as they were + # before being pickled. This is purely an overly paranoid safety net - I + # doubt this will ever been needed in real life. + inf = (utcoffset, dstoffset, tzname) + tz._tzinfos[inf] = tz.__class__(inf, tz._tzinfos) + return tz._tzinfos[inf] + diff --git a/natz/utc.py b/natz/utc.py new file mode 100644 index 000000000..96cf5b54e --- /dev/null +++ b/natz/utc.py @@ -0,0 +1,48 @@ +import datetime + +ZERO = datetime.timedelta(0) + +class UTC(datetime.tzinfo): + '''UTC + + Optimized UTC implementation. It unpickles using the single module global + instance defined beneath this class declaration. + ''' + zone = 'UTC' + + _utcoffset = ZERO + _dst = ZERO + _tzname = zone + + def fromutc(self, dt): + if dt.tzinfo is None: + return self.localize(dt) + return super(utc.__class__, self).fromutc(dt) + + utcoffset = lambda self, dt: ZERO + + tzname = lambda self, dt: 'UTC' + + dst = lambda self, dt: ZERO + + #def __reduce__(self): + # return _UTC, () + + def localize(self, dt, is_dst=False): + '''Convert naive time to local time''' + if dt.tzinfo is not None: + raise ValueError('Not naive datetime (tzinfo is already set)') + return dt.replace(tzinfo=self) + + def normalize(self, dt, is_dst=False): + '''Correct the timezone information on the given datetime''' + if dt.tzinfo is self: + return dt + if dt.tzinfo is None: + raise ValueError('Naive time - no tzinfo set') + return dt.astimezone(self) + + __repr__ = lambda self: 'natz.timezone(\'UTC\')' + __str__ = lambda self: 'UTC' + + diff --git a/pixmaps/README b/pixmaps/README new file mode 100644 index 000000000..bfbb94ad0 --- /dev/null +++ b/pixmaps/README @@ -0,0 +1,5 @@ + +computer icon from Oxygen icons +#task icon from Evolution + + diff --git a/pixmaps/applications-graphics.png b/pixmaps/applications-graphics.png new file mode 100644 index 0000000000000000000000000000000000000000..162b10f5495be99e9a73f1d0fefc9c20178e6b4f GIT binary patch literal 1523 zcmV37%hI%nmJnJMX2s>bts4OCi zPC!L2VPn!MM@2f3RpcmCiUO9F(^3!wtCmX3O%V#peT#~dt1#y_r$qPPTQ*HJGx8-* z@_f(tKELODKMKG^WYVwwJr<9;)SS*cWqEZ)8L~FDj5negG=87Y2Y}S1#|;vjyVoNg z*GZ$%q)0-g-asIR^uEN$4u( zqFu>_rsXmUa}&Wia|-;kOsF!Me{?HK$Yb`RwE5r{9bTxDTh&>c5JMnfQ_DVdOW`?`rucH+0&D7HrzumI zql#-1w6->)s{9rjWHIQijK^?eItJ89=qZaKdNMS3g&1u~Cv8c?qgnwTs}rDU6QjE% z0tLK-&m1U9=MA`8?ANTyJcf=ta#X9zFg!ej?90jH*$mXgp+mvJa2+3mMAtMVVx&og zzS<-_tc->-NguhK>1fDjp;&knHxoVQjYAu+1v;0Cd?1dG#L8u^`24ej=lgp)p9XNr2AGV?i28EYcBfLDJahP z9&*u9^eV%kO!UMFCyRZQD*ZT(WJ?a81~>L&iUX ztVpNl1$>XEX`y=%=w^e1HWr9t1>m%Y^_cp~2@KR-1{joSC*1MXkLNf8@{7)bHT6Z_5k2jf^Kz z!K_yShrVpOUwRIfTQ}niiw$U&g`OONS8SpiIuPh&B0BOU&tD2J9>zV=zk8K& z=u!L(Ewr`ZB;5B|W2MP5aQ%0ab@M<%(9RdMIR+M5KLC1^Hf_HHjGP<%!w`np{??*D@~qR z=4Y!;;5vc1-)wNP z;WS5W`?P-Vf+uN9cn#Icwr$Hlk_)__$uGL&u(KJgwl7C%8gqPm6bWv)7-aj(oKAc2 zt=-P&wwpGYPWXS!U{F(1!W^sfBKD&!`6$ZCt{0LSWN{qeyJyWqV(70y Z{{z|VxS0y$UhMz?002ovPDHLkV1m8X%VGck literal 0 HcmV?d00001 diff --git a/pixmaps/applications-system.png b/pixmaps/applications-system.png new file mode 100644 index 0000000000000000000000000000000000000000..9ab8c04f4ce514c83d3b86d2abbd6a8a0e56b3bf GIT binary patch literal 1403 zcmV->1%&#EP)tN#p37~2FWN=O2B}nB z`59b@fxt<=x%b?j^SSqTe&=`k4ghQ@wD-B(g1<^i9^^3?9xmu_*(hf%$QIO|$jF$8 z3m3xu@qQcXJuM&IesA&2kB z$ERO|+3ZXXK08rdTwb)iY=+rcjSl_KHv``1K_|j5LOai2(Wx~vi26GIUGY<;ItoiKO z^Eb&?zX8*%t+)S_o6B#Rno_~w;0UCqW%*Q>359rCCt?=c04O9@A2rU5~`{mL1tzSczd6toSf)h zYi#gVH#hIm_;{uX`y1r)2`pP5DMKK6If)^*FEQ~(Q9%J;+S%E!9vB#fhY!V&l9En& zd42b6y#wIxe)?Q+aG0sQyb7?4FfcG?!4~`{JDVrAxBv7U0sqk4+@+GsC!xE02wGd8 zLt|qLBqrWm^7ReOSr6>*?{6Ix6lRf1o1vwp3)KsRf!zcyF7!+S_K%HaHl?Q*jPiIQ zeRg&!^!2@f{QN>=XlQuFdf=$2tNUYP<1NF(FQK&bF<>raWE4%Z*}Ru_cKg``{4we` z6f+{?nlvj*0R8=PkjVzjk&)3ebb9!gzjkkNPw;+GnY&!57%@b( zMmgBn*z%;(wuRo_A&^R&@N|hmB&x=;)FP}FpEW>FPcOW9F#@5XVfAFQP~W1?pmwY` zHl5?*653@lIW~T;-re1!_V(@DJqm@wq*N+#hKR>PLPF}f+}!?GuOd0hQqVW5MG!)~ zQ(l3~#roK)XgqrHOm@gAWPH5Z~NK~`i)k_?9txDMb2=3X!cY~Q+?S7nDNwO~_<>VOY- zA9t_#BXNIx@MJXcV|T-{`gv8=RRBaTlXyA-;KQ$BI~OxPC_mSDO#_J}zJ7~gD4%13 zB%knnqPOVLF3UH4(VKiO+dQLgra*XVmxyp7!Qou+KYW1#1|txWUha{5{d6-LR0VF9K^xR`)C1U4h(-^}}<`cMj~SmY%U z;9CpU;ONmxlOE3WcDmd=CxZ2g>J>?hcrG{-FN-MIS~W zq5_VdOj3p-VRTFV;syi*e)t2!i519_#M2UlyB;F)_Gzn?3ZFs+1fVj!v$G#wcRol) zKOn%RYA{$%9OP*WnK<4&xgG`)j({^RG0lE)ADMEnJ2WWpKEo>e`qcKJZG zADy?_ZxT^C?q~^&hJ-OdMBmX*gP%9NfAUP``vEil3jd5LOSa7o%vnDVW8^1;=xT*| SG6r-20000j;;EmaOqG|jHDZgh1o2e9X_zzv(yH6kdqH_fVDvtwNo z67d+$rElP`DxS9W(wZ1y?HgCmu4$Y*1%h+9+RV+24jvhJ*@BWM#*dA+Y+f~sR+g6_ z?-fBXEbeAeF>kv;s0m`k6^+>d2>KqJme8UJc24<#NfnqGcr+p2n+uqfPukAfGP zMdn@(Jm1GfF~?<45Q_VvtRyD(9=*^3E%HN?wp`q5TcmZx+={ZgaVZ$43BxczX=n;U zhL0$vA`2n3Id~9&F&}MLI`H=HSz>-SMkRncTkWV_jnhlv*aUf9rD;i+rlsL3f)h$Y zcm$PFMqnOr1dPGcQGL!0e9O3DL)!$&S=Zz-!HoJ^8$M$IA&A8QOArmG+C;-CXb6VV zAD|>q^aVZ-xajT6;C;_0XwEmVlmQfH%FX-e`BZ@8l)`o#*tSAEL10-S92zR2Kp><- zz$FSr7Uf>_Jmg)Du#rj(gAHiqUUUwo>Q|f}EU@zaTz-*PWW-|j#-wD zCZ?-5qeu|KgoxHsDfoIXY_6TOkdJnMqUy#yRZ;|Pl~vWvGv_QoDpiJLGJ!-quHhC; z)4-_%&Efolm`M;Tq1Q=_t*n4j^47=h>+1+cQ00Q1klf#`f8R3;m#lOq)-@oR(C|b& zs`}*qX0Xi55zkYMpiFlf{ZFs9xr6=l?{!{&^yFbzG!3SXC9a=MsD;r)@e2*Sxcypk zvF!1~uH)Czr^G;iUjzp+@XwSSzj#S$-O@MBM_SdSru7P==#Q@DLmxj7yC$9O{a_3Y hxzGLVyAJ#}`U0KDUItL)W}5&2002ovPDHLkV1gOknz;Y~ literal 0 HcmV?d00001 diff --git a/pixmaps/arrow-right.png b/pixmaps/arrow-right.png new file mode 100644 index 0000000000000000000000000000000000000000..aa7cbb91e667c2f24b5371887943ceec0bd3cf87 GIT binary patch literal 940 zcmV;d15^BoP)>-%*UQ^@((w+ey2zFy3Ku>s8+8vR%1*6lP&biBT)<*^3^mlfv*F+MI)kP1s) ze-`gI&27vC=xA8^_2PT3qV}aNjt~y4mY}2vvPuXY={;ZUKw>P20OCtVF%JK9m9F>8 z&mo*oE+1T_?wWYxwy#>JRffhIWo#miDUKN(4QW*vMZTaQB|Bypim(K{@#;*m@R^!s z0duNha&JX8u(roDZ zZq2v2CAU1Nd3syJQ?pQrHIxRl1q|C-!KTvK@T6uZiGflHTLYx=L~Rfb42?Hc%w93G zdd`X;M=u;w-ef1Hv`Ai<2EnrVPCW0REIR==JZ~(byu!_~aSRWS^3EAVZfp$3&8!uf z8T0GkYH6GV3yTY#H&Fd(4YE~LNT&hMw+!YwXaYEqRHMjAqevo_5>ghp^B4qyR62zy z*5LIa;rf`&BW$ULNE&{CP$*i&iWmei4`!kga!TEMegH3Edp>;MM@1%$NP!ag`xrd0 z`_QId@9#fs!FyYgnO=#?N3zIH%c6o4gn97G@;-?Buux(e$*-p?SzRV0Xuid}>vh2PHng1(;K zR>$?%4gR`+2)C)+@wp@W-o!sBTl*S?VZh0C>C1cv7|HeH)Unn`#>p1P3tJe9vdZA8 z6r>O^7(ze<0v$gb$N9FCXW@pca-Bynp{)A^J7EPxI>c}^f+H<^P5<@oot$!ezUyf6 zz{h4p9){``ynP^D_xxw!{11?EK zK~#9!y^>346;~X_zw?-9?jyN2#^lDNxw*z#Yw9D|YO5h)Q1F4+in?=E5qBzxrC=Au zu5_h{ZqynmlnPq9anaH)3I!J~q?NW=s9;olkbCpEcV^C<)BlVph>tud{bs&7bIu%o z^Zoxbn69=T{=Rkd!AnILwF&%jzmU-$IPUDCUFUJrvg ziD^h+3bn2|xW2p8a%%gQ4_I4E7I*JgmYVsShM)X#`NpghH?Q1Va4dFy%Z6pFS{Xy) z!PP}%Tz-1N&Wq()gH0Tr8Cbid2%}M{R8vPm9cz~tvVi_}!LA{D#^A#HyZ4m4+deI} z7Sg0+m2r$!$5Ev*=V~l0ias6Hj?JjY`FB6*%4Ga2{rxK)V}Ll;P>GdB6sgqt2H@Vv z!&PqQz_rToPybArIJa=CV9?+6_4VeAVF)9QP?_hB$U(A3ey^NoCKx?m95~W8CD<5s zxU1CpaW>ybARvkvB_+okv1T;q>6}7CNDJkT#^$(Q8ERkm1%EBLp)|N6lgWJ9St=6< zDM%X*!z92g7$la^5cG`G9)?Ud3)jMcsy+Vvm0-8C-#0KrbBiTwvK~{pfYO3MHbI}- zkd>CG1q`EO3L_83ap&$RgzdvCUN`E-@#x26Q|zs*-^Nj z3ros`OXX8Ffy57I4uMGz!p%W?O(?X_N6X^UUoR!T`Q3?qZb$!qCM+=F5JW?(TIhMM zXz+d5Hmx(3{%S%9aJLawuILNP54q)GV$#C!Z@1!?z=`U2e$_92%=K^`2fj}wyG{xY z!Vm@~O-TXbN7paLc!eb1Bw-sV1svN(1Hn1xKmrd!h!pG^kY)}xCOJXZ6v#Mxz9!r} zz^vJU9p6D1MX1+Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L00VXa00VXbebs`@00007bV*G`2iXS$ z4-+5=t+L(#00WLmL_t(I%UzVuYh6VY$3JIg?oHmW-r6*zCJV_*AFXj?izOwsOP5tw zQdeUChVHtox~dBo?p#SLxGNMYXcvMh8Yqa?fFwkk_?onNdChCyd%1VcadB^&;Dur4 zF!1?)&yPEDU97{qY>ld<8*g+;@0M-%+1ZYPNzefrg-mh&b?eBf{5_{ z&&=rcdW2(H=FgrxXZ2c5G)9(|mbiH75-Te!{=woR3m4zzoA2-7#_Ny-a1QYvf`2Ih zpumAltI=Tk)e*tWFjMJvyEK|jVn~QZfoeom=?{kdupW4=-Np%sfWsjM&cGqyz^nxO zknr^W9X`4ECIP@ZPm&}Qz*z&l$|t1 zRSAY6B6#nSJZF5jSK8ySmixId7%Jkxd8@$SoRx4d_yWX3;y`=?K)_0u&N(zj`g?m& zImZBm{oJuPh$X}WPVg1vJ5jL&vUH0805lWXos$y;ez0t zAdYGYQ9y?gG?dO&mFN9k(lo_YV-#a#G#X-7Hfnnp(w%}31Ybd&0|{k@;so*Fy%5sK zm?VG^Mxznk?QPN|VPawetB6I6m>GsJ+;{X}C_a>B@xBxhcX;+*h#u6Gl!KZJBxyqX zjW!z_8|6;`Xi>reOg%+zXfz5(2`j2bdMnSBfU2T)q%5XNjE68#iddAfNZEbyr~wgF zAyye^5L5-H5QCrycHlfl#SrXBy&pf`p#S1U85fa;Zjn6OJ~Ey1a?`4r<^>ZlFC>#= zm=&e-(P+eAFhGlfW}5Qd?b}SAnzA5He*W<;Uwt@92niwhqwk7UZ;g}*8&Hn)AAdgL z@6|{EBQJ6@Gx4g7_4|B!^(wzEEy?=&w z`10n>Q{UdY_5Op!#cXeEEZS-adDJx>l>5xc}3=S8BCd zcKXa)$7g3}8&5Vn&(~I0dTVQIy+$^Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2ipS; z5HTB3`H61;00Lo2L_t(I%dM1MOB7)g$A8Z=^X#rJnk(y?nl8OqL{ZR1tAP;FO&1m+ zUD;)yB%-@ML}Wn(8T0|V$&g-%q=+m-vlqgb9J8^VnVr+c%(|}ZZWz;LOMf+1_5HSfuDUe0)5? z`A>5@f;}Bm*Z_lt0@+-S&P;~DaVUl%zxQ`BetM3U0l;qz(i&_FPF|J>X!$<=^fbZh zDuL_rdv=y|u8G3v6>@|7kRWVOqs7nV1y&d4fqN0~`^*f@Jv}%dW(e2U@wCD_vWK?4 zlkn>~NCTeIsIU*L$afpylH>3&Z%7d+**l!c;HDH*^d+mNsXVG|mUfrCpRP;}QC^_OJ2AJBxd6_`rLB_uJaeoSh+ zldKWZAYnC7+s%;|Dn;PUBPF*y_jO8*6egMvpi~HW)j-=MQ79mV)-8ytBT?B{rURu+ zoCpz$H56I~(4Z4&JA=fY)bdTKWOTMXhxou&ptjGcqXzRsi zHESzhS^Dy#0?Z#i%IvEr_#TLq?T$snf7Zr_q@q&m=-x6=9%G)}G5oKXyruaAlt{qV T#TB+9FeGeXVPY4qR9Z}v3WXrmj6fAi4ed4~0W2Rbix#6#^N<*+5JiJBgcRmCv<6dXyiD9Bnq7FX7onoHlhm(#D$ zo;iH$32@WNi@sAwIv1l2fwmnnhpl0cpjaiK@~o^?@yGX~lmZ}Cgg-K{oXKE5>^^$kCH|$$_P$9O(rO8~v5Vg#Nkbpp_8Yra%AQp=ur9^AZYQ91& zWMXTpuMRdgm|fY`GEUV(sU@_k;CdR6Xbok19nbVI45OZ9u~@|OJmLY6+8|f5cGf`+ zp`gzro6T}P)j%ZD!OYAILe#QauUum(eTCuSVfy;|&{~s7BnXGY!~@L~ssc}oSRJ%% zH@)ttXj4-YhH(hfGNZ~Y+XN`;W$N3r6tR?##6f*JQqrR}v}k_r_F z)79055CW|=T5CMdLu-xey4bc&XJ;p^?HydWo*`;@ctWHS%hp$QFgZ3@xH_MEzcuRj zez@!5X-zVj zCO>@n+;XmTIc=Goo3#kUT4--?$MZb$`8|&WnKv6B1t-n?Z z-%fsU;yrtL&H*&w0;M`Aga`s1y~jQqK6q&0qXxejS+`I{%Ozkc8vUSzcK=+=rhmIS zb>`Oj(-S}u$O2`+)>_xb*GB1)jfetG!JfhHZT&+p_4M@g7K$bN?}hoq!u84N^39*- zfOR1EaMxN}|CxvNgAfJ~1VS5l9EbrPP02RQ}kNqM403FE~M}a6=V*mgE07*qo IM6N<$g6Z2#JOBUy literal 0 HcmV?d00001 diff --git a/pixmaps/event/birthday.png b/pixmaps/event/birthday.png new file mode 100644 index 0000000000000000000000000000000000000000..8774fde74fb3b494c22dccd08bde2b78389332d6 GIT binary patch literal 1150 zcmV-^1cCdBP)pXWXAIVb1jvrX4#X}cw@YgpG*=*mPz zQ3nosH>VCn5ZQ%8TEwZ~h2kzYy;c+z9SoFSh@i-1PAhX)Wmp^&rc2vxwQbfUP0}Py zaz5TKuL@Jw`M-UB&yR-({-~6KJ7XL=`_$lW`>l?6ut#VCZ)HPV@f!TS$`=!JGpCI- z)!@}|JRJpW{8p|NK0Nfg5&!_WCHk_<-e2sy|0CX&7&AM%nRaxl-j^L#EystMt846u z`Nx+>5}57V_b3Lt(;PrRv+z^CR>>X99Nm~hr4*cr+MmTovPXKx55&6DDMdlb`r;yZ zyE4pq?nI+-?s+rAzYh;S{7^ZRI#7o|+5jUH0wCZld{QdTUz*T*-aYkfd|=?s(SrxI z*uD`O9~p-5&=5qsO%<(OBP-|6VV|51?@UCb?;;CXwF*Q#0cgqs5~g~`ph4YVCFO|k z?A^V~r+2%1_DZ`X)CM>pm1@#@g4tf9qf)rST8}ZOM zHH7GjgKRZc^R~CRB*jKeNJEzd3qbrPgt3jS1ypPpKoUd-)X>(iJ49Pv1ClgABuwPx zAl2=l%^lZOOMi*RMhn};3YhVj3MM6Z)dHH;DgaT4T7c+*E~PXuRINZ&)FS{lIr#IO zZ?+iqU9VZazCb#uRxL$(g0kfj>8=6e907fhz5>YwntCUazz&V-76r~NRxg5*5)~5& zoL{9bV}*RNl#E)26YIT?^JIdW8bQj|6%r4H(m=QhgfuZgJCsgsO$6E${8dy!66p&B zDJ~EL;EiRe%#vmVJ1ngpjCZ2m+ozF8pQclT_`+67lgM3$y0SoNJ8d*AP5FWdmm%}m zCPVi-^0HDh4#4bu+p#LmCQwbagF^#I6A9w^Ce~Ys9!SMDtH4_o&I!;qbF9{;7j#>` zr2~IE`hao>0DuW_xXk&jT%llNI_uDDk=dh`98RioB1g0l+Vz{{FG#z<>-yBBb>DpGLJ@C$Eg3zBu>7@t6RbHEm?J z?lnUJIMFqsrnfqZ|1Ryvh%eSDJ*9L#W(EV^SN8tPmy=Jwc6`q_P3g<$JN)K6@*VZ QWdHyG07*qoM6N<$f*N2BYXATM literal 0 HcmV?d00001 diff --git a/pixmaps/event/birthday2.png b/pixmaps/event/birthday2.png new file mode 100644 index 0000000000000000000000000000000000000000..eb153967d22a5bf82d087b34c8c68c78f01028f1 GIT binary patch literal 429 zcmV;e0aE^nP)v1-Cl6g^4$Oqk^eJ)D#95o-Z z6KC@i8tel4hbJJSAcVMqykbcZ5bazDX$SN*8i9yFMED+$K}7g43=k1M2Lr5J7e=Q8 zdpb=FOE|EGL$Fc_5@VQ{CNy2gTdRe;N(JxTE^ajq)mkl~;{g*zavTSjs+zDpF-?e~ zh~F^8k7ePZTFra{1OWbGftBlWakSZh$ucCyunvOEz3>L%c3bEd=)~W!o`TjbX&D@*$eCa95IYm*#YPi5cF$}!Utf;iLAk8(q~`^B_frgi X5|hRGVZ-SQ00000NkvXXu0mjfy8f`l literal 0 HcmV?d00001 diff --git a/pixmaps/event/business.png b/pixmaps/event/business.png new file mode 100644 index 0000000000000000000000000000000000000000..9021898963f20279a48ee1c6b6c1cc9b804b6939 GIT binary patch literal 680 zcmV;Z0$2TsP)u}H0006vNklDn-Ms&k`yv4-P}@}WQ@45`;DP~`mhd4RRnj&CZ`kaOo1-6)$l$T**_r4)A;BFcF->4ouse^#8B=u|hJ6~6(Y*eLMBd`@2g O0000Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L05@C!05@C#%g3a-00007bV*G`2ipS` z5CtW^9~eUb00jI=L_t(I%bk_qi(OY0$3JJEeeRjL_vFsKnf#az#;IvYQj;(dkrE>% zDio~tK`L4i>`VJl`UezBK}6}Bk3NV`Dn(E z=bm%+*?X_uhq-89b;0Jlzk9*ji^X@X75(8SuhUvf1Ox(&X?f zmwN9Wt1Cx1f9?W*zy1Rg{p07ae4&4+`=eggZ{p5kBxXjyBdsA`?Bl&fMc}FtLWGJJ z@oi2NMUeLKO@Xuq07?5{yuZ5;NT1i!$++K3{Lx7{mT{gd5r^0Ym39#k6yQyPc#BGU zI9q}9W%(yBH|UvF;S4W~|gnya_|7G8bxk6}0%pdv-06XG~V zYmEY;I0hi#53-YOT(E=#Mp4(idT}LOIrSKulX1|Kd=i!Wn+#WvL}#vCik7ns6>Acm zpp%4HDfB$3AczEfa5(4j-ch#=-aERcp}Uer^>mLSuOdC2Raoaql&3%JlEjKM?T{uN zl!{SG;hX~?RT?J_)mqhiK}GvaSw3k5kHO7qz#)P3tYj7`(Bp9Y0A&btuY` zvMQNXGX}lG^wNx?Dk#b+v$~?HYvMTOi8Gg}O@s9o+mu*SQqC&Wcx-5k5p9)|bq8F2 z{4ytwJ&N~^G)~cRhl?ja!4qdMAtD?f93$;?DC?37YwP@bw2kpCt+zC@5#D=>=>#>Y zL3BcE3P$-JJL4U0zju?%=RU(Tm%hw{(GHU`=lWl+k);`5dGhP@7X~b4Lmpo{$0N(D ztUtO=Ih)Z|1$FU&a%RxQ2%HG5z-KRho{gOird7e)cmKrAJGVIfkuw}09OL?Q48Tu+ z_hatwZQ;G+H*fulldF#rp)j_=i^UqlcvsNFfVBoEf{qg=Rn92i!+S>nqBtebYjidMTTd`EOAyU$E}Jw-X`BTV zk%|xzj5A0OME8SgC-iXZ|C zL;@nG* zAxgV=Qvng)H$*BX$U(CkA2{LeOVyx`W&~|5i*-5TbT3iobrGHY(zoRo*WOUib>C;X zzko3YaR!y8#7Pe`dkBEHkoGz_*8&Pt2u%XDz(SA3C`pp%t{mp&s-XGglWO^^uf2Kk z+0Q-o=ko({Yf&ih2GR^n2`YisK)MLN1*O5v00oIA0Lkr^?`z=N?;r9G(2Td&Ki<6Y zv!@=y53f(K{Abw!59ipPx#32;bRa{vGa>;M1;>;WEiI5hwO00(qQO+^RU0u~G$HBX7$egFUf24YJ`L;(K) z{{a7>y{D4^00cNmL_t(|+NG6SXdG1-$Ny)pd&%x5X`8EQk{S|G(nhL#mwx)`$ zRnX9f_CZ0YZz7@~f)I%y1@Xm8)LM&|&|b zZ6dQY`F(n3_7r!sz1gI_ba&gFfmN}IuC~D9;M;p;RXnN&gLf*SXid1}7CE-95s~U9 zD0&o-86XMx614UJo=G72dq0xXKf#gppUY1BT(kymC1qAE=Per_$;B zvg!HC%@ws^%md(3^~V9QQkigQS_h0%&EaMN3i51=j5EIZc;Ba(8SL-l@#SOkXJZyW zx-+_-*hU`ab{l z&x)<`?t>pKjsEL#>ufFBldz;OT8%r)}lVPR8H7*$wZfPAZb>F-P-@Gh5S7e1q!E+aBa~w>%N)u%2OjZ~f0!$`UMn*o=vz!UQKvU;>0fhoM7lCVjD?V(BN`gUC$jle>@dH4n z$c5;VKPCLb1T@Wmfr6*@#`4ykSSTV&Q2`kOf>ZAS>7<8|Zw!o_HQ?An)IShm4SS*p zX#n_2w%{VnIW_@E{iiVm3%Rz?JetNZ1pXLEVRFbcOoN}Mpiex<<9iX|RFQ#AsXJsf{JoN8VIA{ANweA0@|eF#G=0VWK8jL^Vq%DZ3jJFbg0 z^OmcX6qqKT-?>zl6^o)in*vP*XCCS2t0}0yvB9_HZGfCkCCRs>tSyP01$OO{RF3H2+#QmzWI!< z=@eEw)(3V108m`t&bv+@(O+QeCs zk?L`1sd3{Sn%MAIu0%nx1WD|RgIT9+3aQbb_M9k z4aD-dPf|_9@ca6@qY<^NqN&4kzpZ{Q&zYqGMqz$-z^s^}x@PPG78mBVP~El8hL@iG z)Q-(!QWG&g4t70^1mmYFnmRl!4W8A&N+f%JF`NS;|NC69bjBYzAf+^$1jT^8t+koj zu~~Y%?xCoOIIeu#2{oZ7-ao!NY3O<`QrOLkhyZDTM3RZbs%i3%D@SpiD=Fv2(8ybN zv8z(CWja`x*ZT7IWfUo9p&*2o%wH-1hQxxzq#K4X48n=Ht}RTD5A^puxRsH$JDCCh Zp}!<&?c8P-d29dx002ovPDHLkV1kVkHu?Yn literal 0 HcmV?d00001 diff --git a/pixmaps/event/important.png b/pixmaps/event/important.png new file mode 100644 index 0000000000000000000000000000000000000000..2adca02d5742112751916e64b48e52c8408d3c57 GIT binary patch literal 1056 zcmV+*1mF9KP)nfvO!T{?>JtnQqOjtV~V6c@c%D zPeS`$=pcmA3Q)|n$xIGGZ6A#2OqqTyz9cSd;VAr)ZQNc{`8Y3jCtnlK({24;9!0Fjn}Ki7hi(%JaRzV zQ0+Fl^@q?}r*OLlbKsFSv_~2Zh)fox*Dg%avzGXP&5ToW9Jx|lT!d1BWjqY?u zIu1G=Xjeue5IIqTlgYyj`cV6O)^|J=*9TmSYIhe-HVcu>KxByPLO2q#KNu0a;Q+$- zdEANSH@Lfn0iS~qKr#c5bZoomc3==kTw|hKAwSMN1(WPQ!JinI0|ydZ^fU9wih+??-4zQZ}XPMXe`+bal+1h-i~B1>bNtp4lY zZ6`05nLcM-#lD5ISmIKefE^>3i&p^Vegk|qPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L002q=002q>PB54u00007bV*G`2iXS$ z3p)&8A=pa*00RU`L_t(I%YBniXjE4e#=m>-dv7LhCW%gxv5#qziJ3S`#EV|HQ7wsx;vW&!ZW9WHeVC>@J=Ea$jTf z%+rl?0A>JSB9$72lr6f?_waTmlXGvmD_R4_v(LJloMN?j7XVJDj(uHvy?o7-YnQh2 zYnQi^H~@5XbR3__Y6mXfQ|j@*+Nn?|w6CbP{=0N0SiW(gr@W&{*&mo+RPxP1v@r&Ck!58Dk`bFb4(( zeq5u5?#;IzXLo;F3c&Aml2R(7ltQss^vurARy-aLgb*+cqceW|2O~N(&Ib4FJod|X zd;9AqA%x`>3SV>zePLn28w>_9G&F=DdfecTe>woQa5qVb&V^09@Y>H z293qVMPxFWr}23F@B6o|^FIbVxNTP38rmYy>zNp1e50b_&T}1w!ZK@YY~+tg zM0I3jq?FBOH;CvB5v>r>5)my)DOb|z^e7R%Z$wqGEC4u;!wtir-rimVfVW2S1MmwW z%tRs)uNPJ+RRzERARG=G0H{_mr=|gbg+if&b(55G%UyJJb@6S7e0zPGo0|_X#<*6? zCEZ0N5@9texpa4;(WsS9rxWf!Rfq`HYE=yn4-1$2|L4i;JS65|g zXQv1R0v~%?IF1u%L literal 0 HcmV?d00001 diff --git a/pixmaps/event/note.png b/pixmaps/event/note.png new file mode 100644 index 0000000000000000000000000000000000000000..b7ffa01801df28ab633b0ddd34aae324d8ffe389 GIT binary patch literal 806 zcmV+>1KIqEP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2iXD~ z4+=W2RVoDl00O2-L_t(I%dM2lYg9=Thrd%*_mQ+6lMp*gy*gx7cN+B$Ub;NEMwTwnG4+*k4h;1q=W4j(?& zv3m7yS}bOZQ8e=M#?UW^UcdU|YhZs~5FEfje}7*`F86C^TU$1cB8o|}W_I0*6Ib`V z3bH$xrd+wWo+x9;JIjsjM|9c&*mAk0{OnmvoX-;%3OYM7BAHy%+Wx_#XLAETsK!YH zoaO!Gn4cdT-3^zA9KgTB!|$5Ax?V&thSw)EGg2}_Y2zx>y7pGUhufeHcw&&G8*I)3 z7=WLn=;>Sc-|773u0p)6o5x$W^Ll45O*tP`3;0N#1zvh#41l``3<4d;#~*YqKQKvj zzq#;#2X`k2=zsf=))pTzK_iKk+Ui^dIGE;5p{*&~-Qs+&lOGHBS@!5T6H^}T?d^cA zL(|FH!cYJtXjAsC18F|G|wJz2~*k)Yafl|G~ldHq)$ literal 0 HcmV?d00001 diff --git a/pixmaps/event/obituary.png b/pixmaps/event/obituary.png new file mode 100644 index 0000000000000000000000000000000000000000..b5ceadbf31e6a7b3fe567cf23c8774633115c27d GIT binary patch literal 909 zcmV;819JR{P)Vg~3H4=RTY5o?%msxhUK-ZBFjq zYZ_^)72wPBKjz0;&16kfm8ai{pL+Na_hF?f@yfEYwCK5V$-n>UE8Q0Wo;?|VGcjo% zGX@F)Ow@&5kLBMl`hD_5vp9X|nc&i(ucm!n>1-&2fzs{3jjR5kbx3{EoV6CLgCKyg zD2(k8*4OkaH*>(A$uczymp$DM^b#daAyc-}nze!}!1|hh*H-VbXI(*_^PVW_Lef!mJ5LB0VCR;h3dYEy_Z<1;SoZ@_RT^_q zISe_9@IcyQ53mooV>8nM`oKK!1Mn^IbE}zT05Wn1J4JX>Rk6X)Zzpr81V^pg^4or-i<%$rO7_6`HsQqR9#Oo82@`m@g zvGMTr_#kg)oG$2vIDQ%0gTc3IQv?ys^a9noz9? z<;t)g#o!9E`e)mpFe`ug+4__rj)hVg%2lCK6M{hA1AfahZp9@TRLA~-M6qZu(c`UV zl9*G6JM-5U{EPJ|Ny^n>gU|K*&W64T{0cmjruuoQJOsxvsdKz``uWb=x8Fg%HZJ#< z%d&donm^xaCVta@9C)=pC10pgTVB$STg~M1){?dX|E0!U^fGV+INE9^-;LJ(7hn@| j05}btYBiIOw*0>V5+F?FaCwRI00000NkvXXu0mjfQ_QDY literal 0 HcmV?d00001 diff --git a/pixmaps/event/phone_call.png b/pixmaps/event/phone_call.png new file mode 100644 index 0000000000000000000000000000000000000000..89a8b75d23a600395251602d8d08655b18b7aca7 GIT binary patch literal 600 zcmV-e0;m0nP)-E~kKQa-c)oLZs$?Gkw z9D$%f1hD}~jdY;J0-Md|3u0s@_lfZJ+jj%ye5QM5+5SK3rL^>)RwjEr3OnX_dsu;Bvfo>iTQkvcDoI&@pz0zqk-e`s3#k)D1IIj zkc2nQW8E1f5O?TEpQGtyT+B6iF+l)2Z|Br-{|& mOWWz~B%iQL70(Y)*O%l>?NMQuIx8iM3}{NJLIoz0SZklag8W(&d<$F z%`0ID$TB#1f#v<(VZJ3MCn-3hti1 z0Xi@(<>lpi>6y6&dih1^VxRYm0KJ&x>EamTas2I6L+`^55^eM6^?FP%+8j~vA}GMq z!DFey((Vox!CgPZ-|RopUr<bcw(3 z`#Xz$-{(DkZ1a8_3Hl9-_N7gUbEW3Z^I;LXmRVa zP0aP9n{;aKA2;!xbvA8{*3u0++)G8adn>d! zcy&(sWcqFU`*~cpuP7 z=qWf@{y@xvecodpm+$uPzg`P*YUY0+TJiGSaryZg7jJ52xVP^DCItphS3j3^P6l$JKFi)W+lgVp@6 zA9(xpvkw4l9CIPtdl5QE@<(#K;ty%KwUDegKKQZ}}aQ`P2 z=dOYFs^(%#qGG(gUA25bcfPH+G2%_{*qAO-14iR;@=I*vJpvw`^UUS(z7Do_3v3w6 zOEYUP^w!X4XI@{TOD;k z%&aUd29v_&a@&0SaqiQjEe15ilbD?uw9Bi@svNqcLN)V-kG8O&Cnc>&sFfUe%5e++ zm~G6v4r{>YLF6uhQ25>^@~!&3fzg)4yqBM1mZp7c?Gl%az#1sg@Q=l*zh5_mFIFBa z2LrZA-{gwh3|d9tP|N+=+WFZ+tKim!3x^ISd^M@|GF$1nZQEQ| zcLUw2p_xLhG{Hx{ttc-md)_$nN}*8Xh{a+j%fhbOe0?BKR>}phwEEP^Gv<~jcoUBf zVX+HZu!QvV^l5c^5{pHtyKNh9DS~SozgE|l;YPdb9_7m-xKTgRrr_~*)(!D^`q(AC||3>b1}qIFni&~`WF_yz^n z$!-|gF_N7sZ$kg!^Owr|PQD+sL^DOU!`1X$aq#)#VAZ#8(I-%F9|Wj$>&Qr-Jm#8? zvcbDp=lt!mvauIIG$YBdv$3)EV@a46f)SJpW8}j6NO~Jf2s|at>}C<({RS48oSfF* z2mRQGZ13=VS18-E(0-Gh3JJwNqjz-c0jc2X!(`74%AfYU35@8~7UT~Z&V6)fNE+5q zRPdAzi`og%cX;)o(+TNy4i1xj7SsYz_~0aNyb>id3)#DCrr`Of z2zu^kc2n1{0R~MqoqRsOkeYqObf2T=H(Qv!s|(fjd#i53#LGSNnQ7jdX$fhdsj>du z7n2j4R!KR5hpMb)@0AIC0|V&o>(5z!|=GRz1El$F6W= zgF&}Up@pnf)wZVvxsge|3?y><)DAk_@Z*;>KQuK_0?2d zbAocON6En}9AY=RleAMxCQr`JQd&-KN?C5BA9rwY(4DEIA<)@KSqKheZ7rH0P@SQ zahOB~o2e$i5`)E|&> literal 0 HcmV?d00001 diff --git a/pixmaps/evolution-18.png b/pixmaps/evolution-18.png new file mode 100644 index 0000000000000000000000000000000000000000..dee7b9ab9696eb9fd544984a3038967c8c3711ee GIT binary patch literal 1154 zcmV-|1bzF7P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXP- z4F?g7jj(_K00aU_L_t(I%YBqfY*cj|g}?v*-cH{$9Xgaw8Cr?OBBd0N=s@gLX@Z6{ zDJBhq2{Bepz?hiSg$Pke6Duo1Xre+)A&m2inqhhM{w3 zdhgGTO$;%f{W&>{^Bu*1D-9?F)&UiO3$TF8Kszu7r1&2hKqYV(5KoRF^9xK_P-uwX zrzJZ>2@nTbfF*#QHL8DwxE2-%8v2KZn~UaZ@21)kLaV&8s2G*0V??K>_`M?`=PoDJ ziS`837X83>;5c9#vrV1~1m281i0-Xflj}ZsV7DwQ^QcwJeah#{$K%c*8n_K~8UVihIZLB)>(GlUwfEcBeHa;j)!Y(hGHy~?Q9{o^jLq9e zN!L-9m1^d>4OP83jnh}H?6^fK@S_2w*^cZi@bdCkU(`wIGb`23=7a2N+RM?C=lK5o zT~3|9$DD#ijN7aEDSVsq1xdW=5Xdz+{aexx+%|x@E|;ryeJIDZb#qYd{idDzExRZw zna_sTHd43Ybz*Ucq?Mqzr;CiaRh+qElWoK)%y=yQ_eBjqbi4*omF{vHwr$K8Lp@TL z`zu)cay1t&Tp%wmkKW#1Ow&YbO>S;3{z4xU(G*t(JYag%-N7LSAL7%%@=TY*LVuEl z0h^j-J*<1>RR#tIIdbF(j^j{Nj?zrntC-*4Be0lSu{#2MGp)1cO2L?Ae3K457jyirl7B|BUE; zceava&2FdnxK}=J+AgD`qtevWBqAasBO}t-*eJENwNhVSFX3=l#>OY4=2@hxDNo*8 zlP-qBF#tVd^Ufl_R^MGxtgUOLr>C1pBtltP8K!AcRaHeKa-UQxNoH0KZC~$Z+lnC` zM-^xPa_AbhM-9LdVZCKh5L%+7{dz=gdApH{^2M~Zwb9qthh<4Q0znep7sf$Y;(@greGMUUKD_v7Q*35#uY^+DZ z)TAvZZ`tb5A0}zaJ`tauZk%Op3b+HT_~F;&JSXX}GLR-xo#b+Eww34f0+h-h+cBRx^5wQXO28*w@ UAXSRCk^lez07*qoM6N<$f{@G;w*UYD literal 0 HcmV?d00001 diff --git a/pixmaps/exit.png b/pixmaps/exit.png new file mode 100644 index 0000000000000000000000000000000000000000..ed5f8b2515e72608d2de9d45fb80ff481926066e GIT binary patch literal 1134 zcmV-!1d;oRP)(xC=n00006VoOIv0RI600RN!9r;`8x1N%ut zK~#9!wUS?G990y?zx!uqcQ)CO3Qb9*q)kbepkNXsL>fp%(uUwejD?y8p{)=!q9{uI zgNUFMD`JgLd5}{2)TjDbX(&h)iK5opBqla%!fsPV$ZoQ`*`3+BGvk@tY+BYL;-3Q_ zbGUQA-}(0J;J>IK2tw^3)mP3K37K=}c6+n4r`M;ZdQ>*cgNlLlcl&$x?F+W`_y4@Q zyxhNgd^{yVZgTFam#l;R5QCNuU}|&|FIk9vK+Wf7}ljFCx0I0B)M_DpyEQn_xvs z?A=Q~uEAY2z;11YDwXs*lI}}%*4fvmp>$V!D(@6yRp3GaWzxc#GqBTX3h8TOTW>GW z*$HJAwfv3_Bzk(_c^;HfD5X-`LtYKmiMLikQ{7@wgTlHxF@gKW#vq%T^x3ZNZrqey z_l*xe6hd{cR#q@KIf=k^p*fBN@6N=&lzJ8Aiq_UJcj|qp8>tYY(>JttV*3LfVM;B3 zC5?rTKY+@u!KI`;5`mk~p={fCBo-WNi;EDJ1tVr5Y8ikJBu~AAJo`vlHhy9SSvSW$s^eS~DeuZ?2*i_g*i_>&r1 zfnT}w8N{XYaEdNU#3_a$6~JQA>QxAU0mO$XJP1_;ck)<&>?w2%4eOMoE%>g1C*T}= z1y+rLs0l<2@=5YbUJd5dF-Qrmh-JbbeGLyEAA=A=e|mK{=TQIKd^kYE6N4iNM&E=< zYcmLJgkUoS<2E=DoVWxI6H^51#kUFi93)NQoD*~w{KF$KCq~eaStO95b^qa`sD1ea z4D!%?$Ui4$y5pqA*WX3=v1dY~2FFrALY+K}7`IVtI2yivHHm063ez;ve&lJ~`^HJ_ z6%qtS6?pTYxn++|sdfE(WZyaj=GtK4Y!XuroD(!CJ&Q;rqVF{|H{XJVoJwNa84C=q z!F~Wtj}9aM&1KkEzrylMgK$eW$_gktK%oRUWx%x^{5n1aH8YKkZ@Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXT0 z5&}7_`Ps7o00Z<%L_t(I%XO1mh*Wg|#()2F&Ya!Zx#+q(>y9(qy6QT43B01Ac`Gt1 zjM68?3VJaiEqq9P$smGEB78_g#7rmFTQGy6%mgadvjrnVtKYIdjhc z-;wnheyh5+yn0o9_~4Qi00zL_OX-!Z zOM`sceT%wtgZv;2cCX?7jBebzgU4f#tt&<|H88zgO={5O6|e3JsWZomr-2RukljE1 z$4qZwW8D@wnhsureAXru)F?EE`F`IuYL296OC~1j4E=G3ZH?csb7K;vB$qy$oNWNq z#+}vmvQl+~a0Te5pnY+S^1@g6csNGmkVRcWvZoI_&`SDdFUNDRd)<9xa4MllVuMjQ zG$I4+J=qh2oJl=H~+7}9l^9Gw&& zZ0(;Ex1S4mGdU-v`Hi4I+<=pwWSU=6Zf?PC3t`9_KD)?p+afld-#~S2n5E%S&hF?# zH*CuBGGhbPodV$Jx#mAPhk4QYKKvnrMp+|~bon#xqvD^!ck*ej8;cQ@6e=^x<#*2z zi`ZBxi`0q|8w=gkRMI}+3rMOU@Y?radznI;WE10h@#5*&rlAvj&(Y4a&f z7KPoU@9*4G{P5nf1rHl}z%&IbcLnu>a}1W7jY37AFhyvFPNPp^ni3bVG@)*cMMO_3 z^~r(1ik|~10Q~_-g6n|wdrb|xI(k>Y2>YPPtw`!+iDe6Nn&$LX%=j;Z$&-h!*(U{z z0`4Ol{YMQz1nBg`TnBlCu-sp-H>l7&f0000P000mO1^@s600004b3#c}2nYxW zdZwk;10`Wne2*Mbn*FZKa%#`NfbEgi+9l! zjZiu(;>otgAr6zc#@e_HU>yge2vibcR>gb_O*J8A4$q^Qq|nBjD0<4xf05GX4eW?n zxyLzf@qmtK>kjtB{0v2bP000mO1^@s600001b5ch_0Itp) z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXS$ z5C#{J-xY`e00ZtxL_t(I%cYcGY!p=##(#HaceZTnbSbc1Ex{BE8hL0CX@X7A;v$t) zQz|bKUutNBMyoNrkZ24KBs>@zjiDOTN|ZtxsL2Z2M5xj>-Nr(+(nQ;W=^~hhZ5?*I zf4VcXGuMZf219`dPjYf^?#cbWoRjZ6f(Jx@fB&-4(b3mdtXQ#2*Y$M;1qBsSO6KO~ zu1rr)UocIxYu~Zo|Eg4x+w&YwU3ZewHP$9IvOJb7|mO-;>&UR+E^dpm=JgE)?}B=Eg5uspz# zBS$C=mNGOnG!qVoS1posc6P3;t*xD2=J&H_&#MgoJPd&Ay7L`KiR(BBA#kNcE`TBg zEYy(_DJ4QtIDY(VwuiS9jYgB zI99*kk7=4Xj)SVIi|6f{VHij$(RH2Er%xXe9UUDzw{G3~on={+m6f6EI*xQug}S6t zY{$l{dhe^qTrNi>5}~rPl2|N;l1wHK#^Z4Y1_to?d_?0T?C5=yzcN#cpI$lf76*U) zlo4Z$<)@zI&(!tB+KI96X}jD-&M`SU-o>eFKah2DY}>Yt8#iv?I1V0<$MbX|k-)ZX zR8{4}%b(GE^AfB5rR+HNI=Q?_+RC!)$?g1OCdfD@vsRiHzGTNLp!1RGsnhaV94xNvG3@GiT0tN=i!X!oor<%VP89 z&HUdmO_T2KZq~0~&(P3NLIAY2wMBOC-u-eWo54Qz1-|qw%&QY2n7=I8N;(e0qvDz- zisr+~W>Nfp++-3pP)vSihKprusd@Vy48ve-Y^?ZBGhVN+uV0eZ_Z4-ct<807*qoM6N<$g4MYf AsQ>@~ literal 0 HcmV?d00001 diff --git a/pixmaps/flags/flag-ir.png b/pixmaps/flags/flag-ir.png new file mode 100644 index 0000000000000000000000000000000000000000..41cc30f5481fa4cbd3c96f8e140d18b52c479e76 GIT binary patch literal 1313 zcmV++1>X9JP)P000mO1^@s600001b5ch_0Itp) z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXS$ z4hR&1?dQe-00f~)L_t(I%Y{>2OjK78{^s1f>|OSU-QXG{2FO+_%3n;ZDJcdLvDT() zf6|&*`p~2eNgqrb(h%QBA4|&v##*r^C7~gyL<4CPbxXBq8lhONi>$Im8?nNI2>ZwG z-97hA9{^ip+D>v#<|H%ooo{A7!v9z>7|f4EB29&bg>SfAu7F_}8GYD;f9*%i zq~}o;*vvkMn>0;hP1A{?I431ZDRF0ZitpW@qT13LG}hE%dOrGaY;5f4-o1OB#`^mDVA1*_^qxD-M<)j8!^U@bjnhX`N+>YEtKkg7^3z5-6akn2xW*SR z|3tm-x3kaZqv7G<@Xnn(E3le1FfdSGQ&V$x?b@~W&NC;m=&@+m=31o83~WL}){-C) z7z~@HLn#Ht30d@X+XGI#9KnU#NbLZv1xRVqah1s{{ZeBO_xK6%`ey zPMzX654=T01Tes02oMw)f(XnE2mvrJ9}re=WOioU6ZEY>(I~>c!01&SXjsO{rfM0G3{12aPi0bSR@%pf92DPa*IJv0cF%|aM9SlKKr zRxrb1s6-6>;Ll~M05na5X_}By!tHh=9*-j)j~iM;Lqmbz@2|BiivxiGtpX6yG81Ol z%F3ZRU3k>>1!Uwl55h6e*=<=-9DiuF}%d zL}6heG8vP1?5LyczbIG6VlYG3p?Q6g;zA9`Evx?Ri+5e*}^50BJfjOXBWg4Eb-r%g^rKL$h;p6b^?!XlZG=^aR`P z?(P!}4Gr)4{cAAX^9`#LowUxC&xYY3vgs>Jx3b7nXPvxeLowY9 zkDGTUCo2ygJox()E$!^=#Gyln(B9tue05dTr<*D&w&j#^#BPpZF?1aijUr!3*mWB# zW?E7f%se~vKpA|MRq$>sLp&ONFg`whrlX_dP000mO1^@s600001b5ch_0Itp) z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXS$ z76}?{dkIJY00WFkL_t(I%cWFbOj}hL|DD_0OKH2?QW)uKSp)}J!UHT?6UC4ai{cU+ zMtGaPK|-`*GI{aA2ctfi7^^Qf#(?t=A;c~O3rc5}3`p9988(B?Zo9Zf@xk7%^xBrT z_ug}l4;6xu788FjCpr1P-|u|i`F$V3F<4t$Yuwt}dZD?w`GP2lofeDb3}XzXQt6&7 z%hxNF%KXH{#I<@HBO*A-b3r7$=+Xf>Hk*t{CU?g#Ip=S&C8R(my?;m}EtP2I|xn(?Z_AKJ@xZ?Nwodf{l z@i^=5?nW#YLm&`<*=zmyjn!(m5ZPTg?uqux0?C+dGz%3AeBmi zQxxURbUKao^>x^6wwg)0LGj|++t3UhouU_;nLBkw{C+=nc6LB11;_LJW4T-onx_5T zqNDM}y%q3;M_w=C#)G?f>*i;5MXXjUve_&YMFCeV7Q<$<8D6g!l+v0>N#$4kQQC!J z7zngIfwH0Er`=y_#!Hd}i^Wp4a5Ng_T`reqv)Q1kDtta)&BTrLZG0)kaN*&840s-` z8KvfW z=-wVUs}+W#z~pp5&t!1pp$-hZ@+z{~ERxBjFMb!BO@atf*@e+@~3co975~q(^U}yaB8_q zX5zt2`~kC}s<@fS;qkA&MyuNmNs_R!vC%O;K7Rkm1-Z1eBV&7%8f^LRLErj0000P000mO1^@s600001b5ch_0Itp) z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXS$ z4hSLFUL)lI00db{L_t(I%biqRY?Eaae$MyPeRC`8y5jhmBX*2$K+>U%pAd9RP@@o# zc(nw@3&Xft%(i5SH+m;=nGoDX6TPY7FC#H*NMe8m#)aBYH#1sXkqjBocBA_#v|syu zU%&TwfkLLD#wR&>&((RK^PKY>!oOg6c-S3_#ag{y@7r#-+plTbN@g|;!?=@9r!U#I zeX6akZTO#R&>u-K7{tMY2LXUipFX|A@Atpu@l@=TWm=KRm=HpOBuP*di9McY2n5)+ zjl5w%Ruq_)b!GPc{c~3%k-@I6uJONoa_rbK?Ax~w-QC^G_wL>MWkZAhYO!d_=gS~$e!b3BJ~ufQb6I%_)h@4Kupq~AU>F7`5(!dm8~!a@@F10%8M}6^Z};xqAIIZy z1cSjRorFT69nHHziw@3$01IqtEr_$$JA67^{W=+mm5<62)bU4Xf%mR$;8g$b*RLDJ7i&+965fR zUbuJxZcimfMn<|R7!1C$apQ)eyLYEK^wo9p`Kln3gt~ekCV!oUX*sB=@gSLefKpAt z(j{)Zvu8E566L9@Ote;%i6pB#cH+H0tq^e+k;te8z7_dzf`GSc_MMiC7BUkx+6pO{oTeogCwzjs$7aYi= zM~|Yty&XM0J(~l8z^AKpy~)aEOJ}1|%>OhBBva6Y1DB*Qv!J{XFf|P&_4V)u0;tq= zWV6{sEEYR+p2+8N*X}UAfwZWoDszo$vP2-{o zCO%RlMWVHpCXF`LM5V|?X>|aF!azII85rimyzb0g?)_b8Og-z9@7tUslv3Q9mhQni zDdnqDiia%Q+9eTE2$@7Mxw&b5usGK9-K~9bE1;!&@Svv2laF-n@$KCebTn=a;Bq-g z={chDG+$l_7xYYF)Uxas7sq1Q+E7oZuhvIz0V(f(3dHMnM+mx+c8(C$wntgZEh0S zS%YMr?)_bEJ!b@`B1>-oFG&C)f*49!l3b~KS8TI`1QV%46oORQWi=n zqRIQQ?Rm;(guQJc4LINcIzn3m#k_H|az1e%E71y?JV}oBzm92|csw4!q@ik@fTu(( zSw|{sP*WXrODQ`gu)C_N(sg6i!N!tKb48w`ul1A9=h@iU0Km2&xmJs5S_qM2KDLcY zp9ckdBycU2&XxUUjE0I@`d;Zn*L8GVr(7;0gut=|mwtVkQpx06{BDGR)%6@G<|PpJ zi0|@wy~SH&#sGq8u3>U6?Jjb{+pZZp@^& zu$E5Y_xl;YJV7iLLkNMCl7dlYdbxvOjgP6wVyRFloQs}+c~S!Wer{lVBWsNQb@gZK z&~t|X_<%`2cYOVwbhf+!jAu_>+TJQGeW~)>x zIfgzQvMF8*3^5fG$23P^s|2xw&&pxF!H8!0wN=9cq z9)I`jnQuO;4DD-j`fd+wEc`HMn}z}8fgDg&O4$H^0|d>RnCkRq?f?J)07*qoM6N<$ Ef~xV%!2kdN literal 0 HcmV?d00001 diff --git a/pixmaps/home.png b/pixmaps/home.png new file mode 100644 index 0000000000000000000000000000000000000000..aab6a883e12d68057250d8cddc2f449edfe897f6 GIT binary patch literal 935 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H3?x5i&EW)6%*9TgAsieWw;%dH0CG7CJR*yM zqGce=SbMeU3{X(A#5JNMI6tkVJh3R1p}f3YFEcN@I61K(RWH9NefB#WDFz0{LjgV^ zu0X~A|NjS)L;_PAJ2x*cJrgq{3kwqqD>EA#GaEY#J3AXECnq--H!lx2FCRA_9}gcd zFFzl@06)JVzkrZ{ppbx&s3t#CwI-NlO%mt&4Qn@d2W;yN+7ZdR zJDTxu9Mh3w$Brg49zA~iVwW{re9dJ$mr?!NbQO@bvlf=PzD7fBEvwn>X*?z5Dd()3YwVk~K9A7~u2g-@pHY!M}gX;$^^e!zk_P z;us=vIXU47mxO?it7^_6uB=yHGY%EqTDR`jr=DkKd?yYC&AMk=Y8SOha&M@|p-I2q zrB0nH`mSoBuP#R-C-41hc?ucdxASN>aCb*nTs3cYY@Qvd;1hAgWI?@>h|dRO!B!4# z$(Lnc4meKlTWH`Bam=gql8A~y_NQ;3D&n}#W$KIIrF3u?{bDau>Bc6ocTddE~A(Xyf< zE+G6CK4;eCypf5yV^i~IkIkJqGH-Hpe0Uh1{Vh^Cl_$;%${(JtelF{r G5}E+933O@z literal 0 HcmV?d00001 diff --git a/pixmaps/ical-32.png b/pixmaps/ical-32.png new file mode 100644 index 0000000000000000000000000000000000000000..bbd7c8b3334f872c98029489a1641b77b2bf57eb GIT binary patch literal 1306 zcmV+#1?BpQP)0c#h7{`bkourvTgWOjfncFP2{^sP zxfD`pb10?YOCi0Kkh4pA=%EA}(nBv%$+2G-i#E4hEXz^lLLfSn5;=17VOt-o-I*R% zyIx5vM^0%Uh*|yT492nlu{_A{DIzo%d*U7vpHnh zmi^$b2R~#oncS5tS8N6}(G8^2>5tyu+WC6W1(epbw5F~VPaO3mIy3cUES>)1!i5Vv zgUpxRfa5skpw0oL5DZDlu#^nLOMm`e`gBx??=N4z9Cbx8YAP!j=gg- zk@)S}wQEQI7Xt1=f`||ZrO~yzm^pFctyf1!vkMCg#&Z!Eo16@7_v$l*z|fkl$B!8s z8{_TMr%!&ku<-NJ($c|xetOtVOiZL^TK1WGwF*FY8-~@ZRrjO-+jfhmkqBn=1^%v7 zICSL5>xYJi-#v5o?2or@-Fn>Zujdq;I(6!k4{D9C#wI7(TwkZV^>*CdS6yGnQi`f& zQLgRq#I`|7h9eQoLoe1|K7Rb0*HWp^7Z(?w4n&||$oiSatrr@wwZ>A4hEi-fHcdyd zWw-t)yfXgr?c2APgY))v`lNh4Ca6N z2_XbZDT>7+$z+mRt>*4jYf7aO$z+n99k?%gAmO7ZaF zLmG_+-(9@eQyW4fAf-eIfz}!!1VRX;lpX&8zcma4Aq0kDaNxiJl;hwi#n8|YrfFiD zCXq-4!!Viy5ePc9LGTjf#Wzhjsrle)$+q{+crW7n$4zcU=o^fFJSkhSoy6Z$Y?tOJox*}XzS@E&p zxS%eAbAu^-CV`*^!mJ ze;$B4ckXca?pg5tJTQma=YY1 zUAs2WldL2#V(rNZ^=*BKulCmM~CPN&J`a{enR9*_I)w0`*ywM-_nsQ)9&WHO7- zih!tAs~by8OM^4shgB+-4R+tJuEVY|5{Lm4z(G1R{;x~`J3txO0Q{ox58Zg_l3q4E QrvLx|07*qoM6N<$f~0_T;Q#;t literal 0 HcmV?d00001 diff --git a/pixmaps/konqueror-16.png b/pixmaps/konqueror-16.png new file mode 100644 index 0000000000000000000000000000000000000000..b5649eb5bc12f8da0187095a65b6ca2937559cd0 GIT binary patch literal 947 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLlt<74^)o>FhRsQ*w~R2 zFfuVRva|DmMHqo3GZQ;MzX%VHkc5N`kip3%z`(4?!onjhtt6o0=Irb#Bco<$XzAza zXKwBs6ci$kf8Zrhw@3 z$lA5(ox9@V3UgXFE?BUtuWRD=?FVx*Y8EZoSW?(n-#mTMqK)O{eH+&9oIGR0gb7Qg zPhGZr%f;=xE?&NHW9Rlmj~>0cckk7cCvT?jc)n%x;oG;LGN>PCFgd|sb&A3641?2o zhREk^Vb^&BE(k_ll`VcMmwsC=@2-5>P1U@+YWa873+`$Zzt(MdY|!z|s^p$W-*d0} z8?log#m{{iKkr@gf>$XEzND=9kh<`7>hkw#i{GSg_>!~nOW~^brESNn7Cvp8ajtXb zsh)%1`geWpKly9=v2W9lf1iHg|IF+EXWsZfch}2>XTB}j|7pp&?@KQHTz2>W@;m=m z-TA-f(f_U2e{J7-V*B+!+i(2cdH(yZi?8=weRJT;|AX)TA3JvS#Pk0re*Hi7`2U$l z-_HE}f9BW!%lE$A`1b$K?Z(8Ix{{rLv{EO@Z!1xO&3GxeOU}R!uW#{1H=H?TXk(Jlb(9zX5F}JYx z^78Qw3QtPUEhw#QYH975JayiJm8;im+_H7g{zHcj9ldz*(v|DC?>>0^eR02eeLV)M%dZT*lgtEwfnMaz%2aOxOzs%{0Np8RDTmsfs)dE)v!k6)g|k Q2Ktu4)78&qol`;+0M-k_KzZB){xL~TNwVCN$<^PD$QiKc00(BE)*FmpL`-a9iF|AYDY z`IE_HvKK8-{3CO7bJQ@5rvUtDfmAAAFAxIP?;C_1tZl^@%wm7k;^LbdO-&p2$?_9H zl29y{^>?2>*U9GQ)(^Ma?Kj^FRaM~g`9P^~h%hW+;Fq9bFer+GrR8OeU%mp*izF0X zgD)6HUo4kr5n~hx9Z;7|=pdBs_6bgAD5P~<*4`!NTra8~CUToFtOymtC zBd6e$?d@%dqKIhZeEYNa6;Qq;M z*4EZAF)@J-@Nk_wPNhD7FTg4IF6rv=_c=`GjG}Z(q6d(XIn6<9tRzS{N!t?b8fCjpu z=nTcWign!$kud{rSC>#TFX|)KHXt+X?(U(tb{g^9S7AZW2Kno!kTixeRpI(x14$$j z40_QC8;MOI4p?%sJ^GhDFyt!ui$s{5+3Gj^>h4h#f$S_X=hWU*NEghHYFXUE6( iVIs>iobc>p6+ZzEiYo}nSi<`N0000y{D4^00iJkL_t(|+Le_HOjB1F$G_WKrx9Uu zIH%hZXBJ%+m`+V5FlL-MBZ}fQyjlxI`lh8?pwN~UgtkzMAV?`Ftpx?-;Rd4M1JU7R zb5p0$1rtq{MR8$sZc8L{l-skfH!LR8A^wwJPSeNlJLfz1oWhGlB;Em8+D!QA?vLPd zAE18!<2TqGyJK8N~_5hAkmtGwF3dE8$AcR2g$zH1CXglZ)y6nrB zzat|fBfjsiT_sJ;hemb!oT!3A)6~V)j(wn1DIi%Wpduo};L~+ci@J{=`-@>1R-B<$ zom?NiE*?J*<_nUhF0QJo1Vy?WBvKI_7rzbC#5+`7$2!UA=%{aKXy{?+`yq3}!dF5= zgNM`8dxDyh)Z;k_3Z)FTZQTat6?Q6-9|yM5;{N>y>d2$fN6hUzx5rBDwh#7{mqBe^ zEp@uD_xa-H=4NoaT~J%=1Qcx$B2=blD$%IdER> ztaaEYmA14rLtA?*w6(QDefh}-1+{R>KX@8Yc%6qGlg=63=TInLgS$Zc61-Cr`kK(=w>W*;lhAuc=#UU_ISp5$tes%x{r60 zOW%Go?!aQD_EHxi^6990Z5~uD+Vd3!;Av|Iwlb5~DYi$D%!3k}m2NrG z1a(&qO;K8r#!BHyHIROCy5F8u~ z@b zw*L%g?b;|RHZ~Ui4NY^wFNe8vUjqzVQN2^inf+=2tchARGfS&!5J^NN^5aNvNN9*R ziI+t1eo&T{*}%V%czkhFU4V}nlwqN#vN0waR)4YzG+8_8)U;&mZ^EL?tX)?#)ER`w z=Z(c|iW!$k#iXiwugGREfrB15?Q+#YWo0=Y^D5Yb(q~4cP6umA5p60k;E|PX$<}5* zG?|RVU@(ly6>?^mM$K4D%*n+z3pnd)=$H-b{3`>aQ4e{@|K`TfHFlJzORecjIZ^La zGX(_&UZc_I!<)*By-zH%6hP&^a@yan$jET^$x&q}VA7GO1twYiy}6`D;@T2{hJF>KqCQ?|o%qch|<^oE@Ll^=!DMw8*(b^)JgbF=3U1?ZrELv&q@OW1L1{Y%-0_;EXrS7XwHZ1oXq_=b1zCG{4=enDx>S&wQ#1t7X}8; zL2G-{)Wvus35zVduj})2oLJ;$mUh?;UT_*?*>FdQ8J3&k>7Mji}u)fXqOqv4#B+rV1$j_C&yc0GL02 z-t?fLpjQCkW%fr;VBedN1Wt%%PRL$DxCqTkC-M(%uP!8GIcshJ001I-R9JLVZ)S9N wVRB^v0C?IfFE7{2%*!rLPAo{(%P&d?05;eLSP)anTmS$707*qoM6N<$g80pi0{{R3 literal 0 HcmV?d00001 diff --git a/pixmaps/preferences-plugin.png b/pixmaps/preferences-plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..1d23b911ef87762ec886456ff026a351c0649b68 GIT binary patch literal 1133 zcmV-z1d{uSP)=0wdLacd_@*s z{P^+3HFT2!@9F2hnZ34{|3w%5BHym8@p5T}=Qrl{OFukWd-kiDp>Asg_7EAB1AUNOaaqFki#}em+r(AfE zho#c*ETURPrNrrrpH25eAOOQGLS^pKZ#O&6_|oUoubvsc`2Ib~L4?`|U}3OlNRMX6 zML8@Rs0&|tyRONq-U+s;8kETiD+pPl;B4q(Y4fbzwryPa^w`Mwy~p{X{)fp^F~pBy zq8R2)HNZHrfdL) zwCQQDZgyjnMg%FfXa|$j$dCYPss;8N*0JlbfMZx47{G_503;L~Re_9{?8XtC`(*0T zGapW@Qc|2qSb}P*vI_~Ci9CeOWCq;mAz&S2xf(;+*P5gP*)jC&M~}Sy{Hd4U%vJB` zyuU6ImW66qG-+FafL&w1Ug04k2}Le@T)}3g!UX1YT7bbMrDl8LwFfd|M@HY69Gb*^ z0|!Kk(#QvYBWS|BrX@nSx@_#gQx2r|;RcGAZqydSEx90itPw4FW9!?(oNW&#r^fb= zS%u&Zd|gAyWC-BKi^wq$Co&*a-JOe%N3ldKVF5`&wB>=kyu9+)JAYh5U2LRnDfwv5 zU4g&73Z+9Rjws>?fpVb`0gR>y1lwHTCIK?gP(*nEDjqjvyM=J&Rv`zndbmD8@p=ZE ziUc~ugr=CW&nDIx>-_!ijjtZQa%?Ipgmdd}Er*mGN~I>=9oszlV0K@Obpk~LnFJBF z@w$P*teJ7^u10UNkFx1BuK&99!N%)1U$In8X_*u^HiSC8yj)%JDUlr-NT;OI9BGg- z{%c00IIhiJHX~?%Uk`^U;m^F^>wK5LO3VzZ=j%1r!>(p-fuU1fMfX9PQ}kfVd{7-Py(Pf05=DW4(*0arG$-BLOE= z0CxuEHX^rPWXuR6CKd9*b}*tytJ^zU!jXyz1cnB>fVqvX|DAI~rijtokvLc=U;WwF z!vNV_f0($z?1Xnp>N2k9zi$80H$J3||0nqmuc!zZ{q-Xm00000NkvXXu0mjftE>}~ literal 0 HcmV?d00001 diff --git a/pixmaps/resize-small.png b/pixmaps/resize-small.png new file mode 100644 index 0000000000000000000000000000000000000000..6bc32a80280aefd4093da1464c992d0e0a67d8eb GIT binary patch literal 661 zcmV;G0&4wPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipPy z4muCpY%@gw001FrR9JLFZ*6U5Zgc_CX>@2HRA^-&M@dakZLpUB z0005#NkltG+;h%-Px!xx0Rkuh zg+hVRX!QFw7F#WNEFwFAy|uOVEt}11JRbiF2%Y;tsT zL^_?u^E`qe_^?zY>)gZ`gJoGvCKDXTq1|rdc^*I-c>A|;0FcRK7PL#H(hl$$_%0&J zC`Ex=RlNsb%@5=87}s^}Mx*g52!b-ODIx?xz%sxpgE_!PKA%6Y*XwV3y`C%^6^0>$ z!2rNSMm_`(kwm}Wf0oPTek>)KXbE6Mq@${49l&!{eF@Z}mz9bNe*$6T(}v!Oe-Vb^ z1As5pYV}nZhKLAr`m1KmEf$OK*L7SMB7!kS vhQr|{a4o00000NkvXXu0mjfk2@6= literal 0 HcmV?d00001 diff --git a/pixmaps/resize.png b/pixmaps/resize.png new file mode 100644 index 0000000000000000000000000000000000000000..81288d5bef1a3d3bf3b3c26575c20e06109382e6 GIT binary patch literal 679 zcmV;Y0$BZtP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXV{ z4;?O}-*OoM0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z00060 zNklKtoldW;uCCH-Hv790h9SGVy8td)p7R7|7Hn^Ce@~@SCw)aOItDm1dnd^aBDf)G z7N|sNp5yhI7tZt=0~3Xuvff16%tzYnz4Lhj~28;jz N002ovPDHLkV1htA9_9c5 literal 0 HcmV?d00001 diff --git a/pixmaps/starcal-24.png b/pixmaps/starcal-24.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd58a2bb85f0724cd2d22080f1dce3045e1cae9 GIT binary patch literal 1503 zcmV<51t9u~P)q4yWQQkJMkllnFLP|0|-q?hp7{n&htnvr2V9%Z% ztN`&*ujnnW`t14OeDkzVn`-YM%X5{W5;&8-}hO?q7x?`3pRXG>8c(&M_=7@ zPRWnn|Nf)Bt%EDzIVleapr+qI2h;iUJy&|Xci@-~Ag~&MGB2hJ0_QI-{6ih>cQ4~0 zT(U?T?%w)4y9fK!)+&T6hG@m2&na~0`8cNtS2Tb9<3HKm-zQqNgsYa_gFSBl_Ai9X zmT<{JDfADASG6d?b~Gr@$G^Ex=SqvK-+Yaq-*}J9U%$ZG^-FAiyvtW^yw3giAJbZC zu>a{1Uw!ur%yw$r`stsUTWxaf<{Lb|y$Rw$z_txZo+_o$PNHd7*S~j-xz!fG|JfbZ zzHy10Klu*bCw;tgtpDy22OC}1uCKnh0YnMI!!aMe_cv@sQx*;#>XW++t@XjTzWeqM z@*oTF@c6?m9{p*9w{E_{{O8)-`tfbPeB*WcPluepx(pe zSo&gzh1c4Q4krxuM>H2_0I=4U@{4;vO!k7^uqca@|M>rw#}jD9e0FZEAO7;w{zrH2 z&cE}{x5D} zAOd-ov%S5|csxGgRsS7a(`ZzfZALU_E0pDsvKSCXjx0OIyD{CP9lFPRDCHRp29(ZG zuh&oEoarErV`dt4+U=OeOh{bSbUJmkuA`M8fu>$FAQr6y2E&{*XjGCUefs?ot>zp_GD3*~5xnV`GQO#N%C`?s39sIHJ{Tva-C)?#>R@ z*wX~gIkYioG~-dhOk)0^Fl_k!(my&%R{@%Y7bCKt?+HI~} zzRcp{B7;Gn!^0!?o<1eZa+Fd8LBLV>Q1tTh(lh)u=e;jHz7#|d0-3%Juf8cB@E(tM zo~7mGNob9J`gA97&U+E)oLj(q?~9_4EX(pY-@N+k{{VeMZXZppi1MJuv6ml!!BuH)8(EcE&=4dBfWf> zwBCS2s!&6`ooSXbSbR#|C+ormu9s8bn;2EN-lK9O4(tdLYx@2z`7Lqa;GLWjK9j(^ z$pNPQD%d@EHyWNq++WE%{xUuNwy2_7o)lZ*^I(>j0LiZ?f8#fMK+%Fhj>dn4<+#QC z`SsxxDkbQf8$A>RV!~5O<;-nCdbyq;5P}=Se6$P$t#%#8jK6AYt5BCpKeoLxdWkZ7 z0dy-mT82ucemyY=1L;kuYaP{^kePemTSh^k-Hlm8GBf021|iK@;X8}9yt)dmt*sTC zWb2%vs@W-v)z#OVo9|3Iy1AoaWBcE}eY?*0=FOWV|E~RSefu!N)R!;)|NJ8!Zl7UL z5^GaUa&6?LY4)M*?6B5BE_8gS>(xusx6nP>talF?Z~QI8Z~gVxx}}mrLic^voaCbG zZ50)j9<~k`VV4t34yIWv=g^1`A|s_4494EVL!qYj_DA*g=WPs}tYs{n!hX)YY?h7Q zTg={Mc1GsFZ>Pnn<^~VmI6HeuTlZ*>P}kVV1?oCFSg$^zaryE^m%BvbpYrz`;bk{e z;)BU-gOW|>6Hsh#q4xdF4GN1a?V7c`F-!3xQzCaap6_<;EeU`T7l;xdnXA!<&O)ml zC$ZKx_SV^TfGJ@@n(LBJ$ki!+hq_Yqw^q^S4gH{ ztF*$$%3iFa!RvdLrLLqb9G*>M_pED`qBue7FU;M}=TY|$4|Qh07Ig3M4=+`%N7QIY zrDM!(JNC35kCgI&>K)eYW~ZmaBaV;$Xohcn`}m@9iXvf{Pvo95H9x6i z^DDk4BXKk;Zf>qt5HSc-p~idSZxb19fgvHJ#Q-qYdiWqUH5G%w z=mBnp5uF*GomU`9hQR%RY_ia+ndA-w#R6uJR}ZQ=W1JpLsN<3c2C&@vdf{?cnqI%= z9%Wbb4Yl3t4}{P(Rj8Z<2tZtnj*d>t-0k#;GyJeb{k}Y^r%ma&ckw*Sa66*k0zcvBwXvh+Ixrxv1MMD5jNYeZ2|6L^Tta*Y73=OAx@tR3xadGim42=+%Bv1TU zXko_na?#w}+*>dnzKe(u0)fjNCJ!-0;^I*$0ZiIyZhfE{`_Lp|PJHg-$IJ6C1E}o7 z6=-|(AFaELv*!}r!7lXi^;R%Do_xtLp;b{ZS%>`9*Z&MP+IU-m*=*&7Xz{m)PJsLiEAUl z&(BY}I(0x^Oil8AN5d)72Xv;evRLlD)A()Ijwd5iVeq7xGir5EY}`FYm7Q{xLoM1o zs{!ZfDe{73uhay5ddemM4{{@k)aIS3uc^t-7IjD3nPPIT`b^`Y&!M_tPE?9Pa1gCB zgs~UNi2!b~^8!pJ-ZckV}Se z&@`J>xCoGNvI4|#O++(;iyo>fm0l$(D{+W_CUUqr&08OqxTzy#4Ml)CMZJG2O6(A20SI7yFft)) zOerA`t$c3pw%v`~&{p`9J}TDE9!q9k%j5<~Ts7%S(zS+)e6|RyDQ~vHxX?us5&0j_ zD(CP!(-W34$o7VHyByCtYQDVoBYLCT(9zM+vkbHPIu>O8|jk4Lq%QlLcjSHR6VnvkzSqTbHDXDS9b@;sz09Yk45SUgJb0k@A z!?zM7+3F;?zP`R1qSbUydcfo9-V;y!>h5n~R{+`txJcAH2nfmQl9=HD=iV<01FZlC z;Q?Ne>yt3s+@>p z{eDym1}Yc@au<_hVE_F^V6c{>%>z$a_F3_O^y(kKu*6BZ{Qh z^$y#@Bs3&2Dk=@|nsgf6-r7!`{zxd8&!5uHPqf zJ$u{kO|_+`rwfK?o%SeKdk0jqg{7qkR-_aa&-#tZqwf7%Sv2O`dTkG&Sx%0a!3&(m zx$*BwveCs>WV?m8t$L-scP)&p0lBM^!=)Vqy`-vDewWj~`;Ufaf2zzTOdFY;0Uu8CH?-kWfPZUNIi5Tkm)oE6h3TCsA2h2{ovxiC0Z@ZA{9_ z%BmKYg79n-WxJ1$w1GnFbTz4W&~|(<5~o#ovJZVIOMF&*St3GDZD6`7$R#iAM&?eK z)@e+JA^$KI_~RnxWau(U7^9^X&kUcMpEt>`fU-v~EiDf{4h#y4M?*%%^}$sE87uu^Jf*wzO5<15yIouHTvyl%ZQO(BG5CO|J~bzHLTm5 zX_)j4iShCI*NkDsKmQ{P2!(Tgw^HlAjmZC~YP!+ZFcLRA>+<){$-$?hwkSzh>O&r-cCj*6WFpb9?E7J1Ku@iZYj(cy9u4_9b6fFd;@pQ1 zrr!8}Vx}sKq7VG(p~r(THioH6Q2EH#xsz5tszuis4W_|t$qCY<>;fPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L00hne00hnfXSg4400007bV*G`2igM$ z0tyw03~1^A00}2aL_t(&-ql$Y{;I3`J&*Ge4+dWEja2RpL_{<&Qw$cxTm(T( zE^N4PU|BX0PA-_q!Wy0EUIRtYz)0L+LD0lN6Fs1Fj%S>mp6ThXuBzVPp1U3qjWJwn4bD0K9|q@KA3S*Q&mv9Jzx4ZkzOk{f z>xUi30RS+@?00gUF$TwRV2l9(lw}D3&{~5r2Ca4NI^DO|v9PefqbT~DD2gIXk^}(I zXf)7lHYW`HUuR`$X$b&u>((vnC^DZdh{r6-n{u$S&icuMNvR&Ed*n%WQ@V{yonos z{P9QJzkeS;|NJvfpFWL7qhZraM~W6j=j6$gxOC|fmX?4*c)Vd9O z@Zdqx@An(iGVr|)r4+UORCu0O#&KLr&-Xrv8#w1$L{U_fWmzMP@7-`3tyb$lVq;^Y zbMM~0Il6e_#EI|L0j)J&zI}j4}J>jMxvug%A)zSO+KrBnru3V~llTQwr?Kk!fTrz6K^_xDW#K^YaLT0C5}> zOWG_@2CA7D|BORTN{Qic2q`7H-R}NEh%pA&b>Vp)gb?7I!}on8NrE&@Eu2WACrJWh zjLl9IMd)_B$g&J+nnFs60|yS&0s{b16v1`f9Ub^$d=Q$P^7r_|AU@)+9+jZRunIDE>4QAUqKn9LRBMS@1anNWqV2rW9hhbR5{&>`g zBdCQEjEEnq|6RTBtBK5YUAV4maY^Ll`#uY??;{8Ti+MUu!?EXiD9aL#nhjKTc;Jc^=#QVL2b6h#5YaV&2XMX@EUDQmle5C9B^Ll|S= z`@W6J+S(f0?Y8YuyPeUIT><|>}CQ(MLjz^iy#QV7=tke&R%Ch)$7$%?sZl*F7UQ{XH|YvE4NK! zuaTk`Qkv!fpH@Txu3&33E-GM-*i?a|F<(-tvZXN>L0o{r-{Yi(Iw zN{Kwrs}prgeB*^ot-IZBZFG{6SY}VwZ#5H z2pbsB^AN|e&AQkLuZ%GyNdhS)eBVc_)q*hwoleIJxgZFzxVQ)@rIpv!y#s2CS(ZUc ziDt8jPN##SD3Iql(lo{V{5;Y$#b`9L#%ZnXN;JuecDr4xJF(lMtLk~jaiEl{5s{P< zgTcT$K->_9A(AAi)r&C(QcC+V#vspgtBthQNYm76s~`w4Gc#i|vAVkYWpbKcuC+#< z=e0V~T4OXCK}uQ60OuT|(a09BQp#q6COVvRbh}-9jfz?bf!Wzv%RhOZqu=jCDP{R# zM+iBNW2bhsb!3de;lqb(@nO_-tyarAuw~1jVoTGsHuq(W;mw;ja9!7mxkHBzq2KRYxj|aT z^E_l(R%^SS=b_W-pw((w<7Z}OFgrUtL7;SAmL=Z5e{WGh2oT$G0g=XdR+LhZQr7CD zC<^p?y_!gK90!Ywi}?8QqrFdSjfI5;BuQes2}$$nX5sidyWOs>5t@STn1P}wCU$NJ zv*Z9dlq3nVESpf7X__L-vKn*KN<^>MLs1k6!w^9bY$*zjF&6vO8A+$@n1OLJp_vt> zQELs?b`1BNrl8&p69h1*xX@6uh*LpiPM6(wzh^W%d7*{=CN%C@;rwS0&lmC_L5Uv~w6^%1tp>ZW>r_!lInAQeIXX1KZSA lDPt^mUH9L|k01ZXe*rdRfk?Lzp1%M9002ovPDHLkV1gj9vG4!@ literal 0 HcmV?d00001 diff --git a/pixmaps/starcal.png b/pixmaps/starcal.png new file mode 100644 index 0000000000000000000000000000000000000000..a37cb38c72744127b7af0e266cb3f552411a5413 GIT binary patch literal 3387 zcmV-B4aD+^P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L00hne00hnfXSg4400007bV*G`2iXG| z0s{vnnU{PuYe*XEF>fpcmXAn^MJ(*FBqaAgoKblavrdsEQCn}1Urf%qin)@XS1HY z%uIJrS69_{wdo7ugT6O8Jug?EF=lswAeAVKc-~F>c`rfr4{fP@+VFAsD ze-mJ|@}D{Ju`lbZ`}H5#ZLi7;)twKZj_!p5tvlqh`-JW{-QGEeMq|BeFL;jb-{n@^ zm#M9E|0{arcXNBS9n`F8%(_PC7A2zlwLUl&;=MgE?OdDQ#Kker58&&+^b!ES|MGKi z=z4SzM=*P*!tOXGrzXpKVkgSsFau3`=IG$xf8~e#>tFvDlgm>8o`2=J_vQJ$OB25J zwb!`xgDb=kkBqmN7|}lGg|jaql4FqreGsG8%d=1_t%QnNYhjj-&&;bYzRoXy^(jt$ z>;&S_)W2}{rS}Bp)fZpq{+~I`ufFgFl$P&#BC_QSS362yYOSP>$QpXbe|Y-io4oqs z>pb|m`*{4BN9eB=ym0oV1&Fy<=Eq6T@YBEeAiwatpW%hGFY)ltpW%OA``-oFtvBD{ zi(mQt5#w)k?`?BfwH{EYC^aeo$J_<*r9VB(=YRWWDR*c5!yo?3kuZAxmFE`6KlfXo zCL5$QQ_FY%=?9DZKl#N+Z|kX>-Fv%D8;z1W%2cE+Puu{2kb;kV;vOD*`eF7ij*kRm zIXYmi*Z=kne&uUV@#}x_?2-HLef;E6k3P1^fBxh5@h;r<-fWO?_59wAt(PDSXra7T zQU$Z5Z^$ll_hy5H&;9177S})i)Ijxw#=m{zzZXe-vwJ`L8=vBvzw^(y+8_6#X_-+;9mJo%_Q#XA$8YyV%Kw`!)-A2B zFRholm-M1sJL;YfeIf25F!ia|8^xx*>aH*o<%iyIk2{E>nDUtX=HvnX@iuCet((m?JG>D z71RA0b?rHI>Zca6RBMG0l3uTeHu|;z-m~#(qDfPQNffIqeVpBIb3jX^XhUeYI-sui z3BC;>>l}He386vBNSf*v=;{)Cdy`|&TV7tiyE%}U&iGW5Wtt>u&zF`8_V*21OO%pG zB@n=1kTDu31RrodU~FJ%DJRPg!VDnK6RgdcPLFnmQpmdpz!*iEs@CYF-RGGmNi}Jv zSXxR+Q@sEIQ1ldoenQju)&n|_B#JE62q4nZYmz96BBv~`Yi)1S0_s~cj3Unq)`mm! zB4;qjDT)MR1Zg5MMxeDINd< z`zU2lCPgZR6apzVWw}oXmb$7Lk1K|2C%ACo0(Ir6DoZ)DoH_FlLIn2qc5n`akR--n zj5%UcUgXTm*_{E%vy5I*FfB`@5({=&Sy^Jomn5m8*H7s86S7Q_Bn4TPVN8OQa*^~9 zkg_7iKnMY21WBU0yijNx&`RKB>%ACT&%N(Lri~^^Q)X3(lpqx-CFm6?qmiV@4ZR|v zC=5xGAZ13DrD&ZXy5x&7AcaCo1qghIZP*!wcWq)PrcHV&K}kW7HwWOIM=C||{tf_W z-7<q&|tMJa{WrfZkXp}FyZ zKsOI(?pskVw;FklHqVj(?qKJ#w%V~mUfcl4m^+~ZKFIMkMTaC z7ZQ@B02CLtF0irQBlsMpmMQWAA?7nb2LWJ#fDnopp|KVLT)ldQvTPH0I&C<8>OLZo z-Mw9$gQkfjNlKm<2jSxb!{HirT~XH!b=@Ejt-p~M`E?X5j`Q$Bp&e+KR_`2InL>1} zojP>hCZ{;EN=0`~w0b5URW-pMBtnRR7$fWJYxH`3l$3z5f18SgtV);g3@$WEEwz7h*TjIn!I zJNVpyrtvh^Q#Uogp!MyHpI6fmBU0&Z@W{%_D!qO}mL(`9(+gq)SC@p2#u(^4f@p#5`T4QYl1kQQJ<1wNe zCruL8*Vb^(y$dzk#ai1%wL(lvL1X=*$cd?CS_VdYB~{&Ev_|VT$(ELu*x4D;G;Isn zG#=+7)yy)h8tU5NY+yF4sb=+}-Q3@w(liZH2zo`q`uh696X(vIyE6ch*fJ4wcg95< z*wh{&4*Fi(L?)A(+01p-T6BuG$7E7>;605El(U9e)zH))??CI6?RTy+t1L|u2)<>e zG)dUl*dWhywANHr#opc?&bdW>xHW(n5mGW3^gFF1@gAInhaP?uty?*5w7|N^#)(s{ z++3_mcn`B#&F1E5c6LTwzPv-z%y2H!G>)aERq`xH8-r4cJkRO%dI%x# z-gEIMKf#B<>gvk%FOO~_oQz~L*+*-I)|w*E5xs;r-~1kFs>$<&qDcAZ182ClbA>cD zq=`Yuwzti*oawa0IghoDx_0DwkMrj*FdUx1+JJYkwsw*eCr(1gI@f<Fe{P~LqYeFt?&&ia% z(Uj$7XlhRgf;7$AC7By6ox~W~-rlCJDx9-j3!2^D1C6z0@u;r#!Q+Fbs#{*4SL5B? zeP*RYY0)XnR*H;9Gp3~_b|K_kV1H6k%^a7n>`*s>UT;9ZKR_yVqyvi!0`ctb?ou}m z-Uoy@&}p~zK+`nG^hsUU1m7}=_W|b=J3AA~nLV_y)k=}wy?x5kcGWgwoo8>fPu&Er zT^-?kAkT|~*Rql7uu(DT;zT%g{!XCMm`klvb^d>Eu`lo~Ev;n;L5^RaG&a zmQ+=H?wqIB>rvM=LI{S#)kO<>*!$bt@36DGL)p!K2(2KChXkOGheBvp&T!r@)*xGf zHs|>U+cfM?Chza<%Awgc#?b5aNYiY=G;_MVbmMH$ykEUsuPWKs&Ml_A3u4`=5 zEDEU-ZC?@(_3B-}(%dqMKZtF8V|_9hEYs^1?C(zomo8mGD~)xwQ=Eb<%UYU@kw_~+ zg7^5)GN9I)XP$Yc%5(Ye;^mjW@i$c!o)vZXn%1cVT=3yXODO1U_EU)QrrDN6)(o~!@(!qc07^?#U));!TB Rl#KuY002ovPDHLkV1lxVi7Eg9 literal 0 HcmV?d00001 diff --git a/pixmaps/sunbird-18.png b/pixmaps/sunbird-18.png new file mode 100644 index 0000000000000000000000000000000000000000..a2216422d3019f010cc0b7b24a8a7ada27bedf17 GIT binary patch literal 1238 zcmV;{1S$K8P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2igV_ z6Al!R7#oKG00dS^L_t(I%Vm>YY*S?bhTrde-#P7RySAfi*RgJcZ4B7PkBZ}Gs398? znIQ(aG(_;{La)5RV7MU~jW%8WUnFU`UT34&rO(A1KES7b4 zbo@->N8I^(@tq&G;Gh+SnW;LbMFq`9N&MgA7AeN?Xd^|PG61o9v^GJJy(&bV+VOpTqHd0Ao$B| zv^oZwL;{fjMaRWWF#x{1$CcJ))}0)>aSs5Ysg$wWECY((cJb2K-YYlmeIo!sZ?kr^uQd(L zmr1lIzwU3iEGlIy@H0)cP{!9*+iHuoVOZ#)G6bl;~vErY$qLrmb@ zas;Ak5+`m!Yt zI0v~q8^ooVIK-R}TiYVA0y+i;`jN?&z&U{=5qRY={QeLmRz9qC;)V=i78T>@Y zJwiAvlZlDR$@f3_;w=u?bZu8BmUV2L|1Rn7S-U*f&>p2kvn8liNOC0fQb?CsSaw~L ztEHix2e)uQ{rCO<5Pf~e==LXa{W=xJ4a*s(QVh2g0o8(? zaW!|?5Gj3nJ>S>AqA+}D!2dFEz0`G}3)3g2AB$dieh}v`jNtV4SA`?_>;1Jbeb4Up zuiE{L)}YoX)oq4Cg=zkjI)48`@p3+GpX*@SltTSTW%Kx7QA@l5bJ+!m0B!LG+Dop~ zNxd{m2naQ;1y^LVo)Dc$Df32Kb0bpe`G2>61Hy6xDC|LphX4Qo07*qoM6N<$g73~U ABLDyZ literal 0 HcmV?d00001 diff --git a/pixmaps/timeline-18.png b/pixmaps/timeline-18.png new file mode 100644 index 0000000000000000000000000000000000000000..d825df7b75ed476ab04bc1c46a0d588314387123 GIT binary patch literal 988 zcmV<210(#2P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipr8 z2NyaznTrPi00Ue}L_t(2&t;QcXj@eP$Im(U+;elY+~(FKoxAHCOV=rCLa5vPb_`*4 z&eh7K7JN{|w^4LYia5|0eeqR6Md-s|cTCXhV&C^EV~9uyK`-DkhW)k$AQJLdc7FR=sOoaJjeO7R zbou9(*vcQlN9RNYaILnPpnabIg|4jvz2wvkHu|u5_Fa7OODM@`^g(wxNghZ?Mh@*D z$^Z6)YOj|-9J`(i|0=LwuZX)f`qASKG@s8{J#0Zovh1JFq@H^x4blS2w=N?lo(^ z9|TixVhG@RUw{Aiv*l*-jr-qze&o!h@@yFGLLM1Uo*Iiim5z_?V$oLqhOTd%#B^QN zwBsi*03b{Tp1k+j7n9TJ=vXp(-w&s*%Yy2=u;5cNxF9Jx?vdOV+f%rio?Uh5JHSGV{CD8u~;k?3Wa*TJ~%j-&*!7j=*-N_&2;$il+H9UR=P{+HbN;?ubc7lI(TgZ~XS+Xk#60WI$U0000< KMNUMnLSTaJY~Qp1 literal 0 HcmV?d00001 diff --git a/pixmaps/timeline-48.png b/pixmaps/timeline-48.png new file mode 100644 index 0000000000000000000000000000000000000000..b8f0f9843435cb5501f5f22982ed451bb30d39bf GIT binary patch literal 4412 zcmV-C5yS3@P)Px#32;bRa{vGf6951U69E94oEQKA00(qQO+^RW3mOLwEh0*%*Z=?!&PhZ;R9M5s zSxt;%$8oNz?)M)5e~09doEdWd_Ft>jYFE3fD3&85*_@J#v6dhsf?)?RkW+y87UvM> zkV7s(a&ce>8{`m2kQ@t&WW|;(8=FY}k-d>D?OMCqnIUIL&J4+!;XmJd-Ca4n)o5cW zjtm${ABg4y+5PobRb5rzLuQ8mtD&_-0$#gh-i!nK7;K8G@j?# zwrz~jTE}9s4_$Ln-C^zVG{k!9Xb$;0Uk|heNG35gB6+4i0{0Kp(UwGY1cu z*)TJM_Oy15H*~>gG^n&j0VK#0R&Q^JLINQpF%YqgXVd-NfhRHS*w^3w8Ef`{jn*#s zE_x~ zG?`BK-qkw|WwK7?VPO>cpuiWM9rwn%bMEw{-5uPzk*qArdryyuf!_rlptaUoJB|~0 zfRr+D!*;t}C=`g%Q@$Z(0i>*?8W<?7u(^B|z~GQtPLY-}XMfWkJ435_rs z)RYH(f46Cd(@8m>U_0r^gR6ftyzx`J*)C}#S+md#1ey&Ip!9J{5>c^43k?8Aq!fFb zYl)+065&wz-(CpuXd+2Z2Z>0GEq~G<%l-ZRTrLLy1h!x>WQf^!_fSjP?u34JW2_Ez zqb?e|*xXV5mOzhBeIj=H{8UVmG--bGn)BYA)T0Qs7ozcOhdMvrU5s!pWfEjrm8Ir+`iHO;7=+2k_ zZfVfcmOz3Ckzhv6N(ulHhHNDaNljj8IMAJroS9=QBFe|T*{XW*0xwl%b%m0-5P$ON%03g6g83FXi*gyDpRj7%d8a_($J!I)2blyJUxQ>lz4p>zyVZ@9&WyJc$%rOE&l_oI zlvm65KO_rfP0vfbX8>2xGBZo~`D4PT_TNC3qr(2ET`t9~DYlN;I zPrdO@yZKiCjXm9Ut%+}Wma)hpVnMzz7T<2)7~&v-NdjWf&UiZf`oG*P&RK8#M=s_| z8CIaobb0{bU3@q+w=DDd{MOdi%E}5b2-axWM+Im!cw_Fh2CX}z+rJCHXOOh;{`}e> zztVnz$Y}6cp=3+@?7Lbf9Aj(6o?v8DMi8?h5(Mn=WNWW}EPVn{U3!%t)-Q$xCa`zB_!UK3v=C)prNmV;&_( z`QKRjc+zAna&pmt1`vho`XD_5jH#%cIO#;bl3cFFmlk8I^YLmmu{_@O zIIEbYMsWY>A8zGOPHH#jZI;6J$)GoIeVm>Pk3i+z-emc7-7Di7aZdMGQ3r;GPXkZ6b% zgAK?y)mx;%pmEpM`Zs>sJb7vPN8in)EkZ%sc>cI4XXw$7WeR264#^;^Gc$n5CCkb- z2AF({$$&;cP?p==+d5Tv5Wrl_T)I+xG98ZH;#BF(xx8%~1R30l0Kfnd zh-O4~>~#c-m`qpVOAD#WO87)6Qeg%JSW2AulZ#&@63C*yxm8ZgA>xq6!)`xSOadFN z6#_9#Z4;hq<4Li5VBT)0Uc9;A>_u+hNbQX;eZKZRGQzdAbcbUzX*sc5nal%)vXFv< z5;mxH5U0X~KY!{AnPP4!zL*Ln$=DWHfTq6H&;%NMls=+FbPNEHFb0@d*tW$8M!JZc zN_TpLnPdVOU%l;p>1l7zBQ4`T2;rl2r=J+ye1LYnTuAn;MTPv_juXq&9X&RwzE1=vvF!db@Xh z4K}u?fBFPv6O8C5XJSK_QCLzNVT=$W3c6GK!9R{8XJzD(M97GZ?IHBeef->S|NgtT zZ&iwU3>b>X z!NMYztWn#2--JjxMEXvXkpU1wKo2a5R^P1_^lS=Nllq6>d-Ly&XK88c=no^@*ma-! z*u_UyR^t&HQ5p?Gf&_rs+1YpAd1v+bst?fN=okO0xjsm05WW;j5Evt#bV5$0y4c)mRI6190NHG|)9FY}By>C;+w6}8I)2=sY<0H0U#6#xvmR{#59|Z+PICBHB0~qz@))qm9&P7C3`+Qp3jT9 zT)3Q%&c?C3)1JwO9ourX+HMUib2ES@7IPZA-E^J_G%`yfHb(jCh3EfO2oaRz6ZRJh z#f9q9{rBB(+g3Cd1%S8SdJ7TX?>Y7p!tFG}!Ac&!-5Y z2+UebX|#cCHregAi^W+RV-^gIF@S)822E$Ty}rKYPNtskd#-=`*6knv_;t&+?lv|I z7cN~oe(ZQEl>}fSV&>^|>UrKheHjBI8J>RO$;9G9LLy5OMy1J?3{da7761SvqR$Lq z5wc^ERtgvZ83k-^)@!wz>$>fBdtqUrR4M_0=Xpf*wSWGakRsTanX{Sf=`;7oOh=o2n%!)z83~GfJSJ=+5tgG;c$2`7(}B{W^T9JjYeZU z9&4?4c6Q?NxYoMg@8|RRYPCv4Gcz;EWHRWSEX&&7-oAG2+M&!~j5&Mu?Ck7pBoeuG z>z3zvmSq7zCX-oPTf57UZQIHRO9FsW4grH;07mvqXV|IN>%-x&+wDdo5v_GJ8l9V) z%VaXfm}D{;jYbi%*Xspsu2L$INC3ceI<;+^nZNOkZv=8jkQE}4$occno;!CA z5q;keD(T^HcsF$?SHOP4N`0nQQ_a>{Wt9Kd7 zX0w)Moj7qKI7D!0L=-{<+jKgegM)+dcwDVk)9G|z=;?GC3WZXs)M5W0pkZby1M{nA zo_Qvi2k~{^efL$X)o3(&7)wei&-3uTao2}(x!hnd2nZOD$K&yMGMOwdF9!=D;^yY& zXf#SB58+S=Ow{{Gt9TA@%_US2+=^!)krhx$<9fWag{KtxI@DWy^> zaOA+;Pe1*%ly_S{W6a{>VyRTB)oRzTU$50_wOVa!YpdVy2N=TP@XE>x237}vuYBbz zk3RY+GrO)Ei^T$6=0jz`Uw9bMV~iONhle3XMC?C8;>%gf8vYBjj7LZMLLum4T-JpAY3mWQ5y zFB6A?S`et6PN!C@ZEkMv?Cdm~&FOS{`}XaZUV3Rb99otY1Xr+#QtGLvo_hT8$Cs9t z0u}0B)$rbp-cKF2Z3kN2p_mtV6%hqkd%a%0Uf+AJ;eKMKcdo9J|@zbYI zFD)&dJbCiQjT=|5UJX{$TF=hTUcP*JZf-7!$HP$npi8_Ds8}qv+wE*N8`y3*9JX4m z_4V~;vstg#H#Ro*_xBH%NG6k&N~Kz@E-o&vuCA8L*VhNI)hR& z&OhSbA3Bubji3MINO@sw^d}$xj9)w*!{!&m3ee%jpcY3(0-N2BMwFpS@L_Lge+@fOo^vTlC;TkYlgxI#uT6eh6neu+?r>oZb{Zx5nJ{bk)oS)5WAJJrLo#x za{yvX#0q)2%5v*6BO~|I>$b?V1@@0Wfl?uHWhbUIEZv;zuK@=*lBDNb+0t%SXtdjH z59hFA5MwZ6$-7HfQIsY}c_BeqLrR0Ee12WLK$a&s=g>N&EHlbdLg#300*j@QBv~Xj z!#ahr1~Gy$md45)rVvVFnVtI%-ydLMX$DgY)^4 zoim@GJ1!za+xZ8r)mN_06d(78E?3Sulu`;90O~;cuTeK71`43#oGX3@F$QB=SBZKw P00000NkvXXu0mjfHdq!i literal 0 HcmV?d00001 diff --git a/pixmaps/web-browser.png b/pixmaps/web-browser.png new file mode 100644 index 0000000000000000000000000000000000000000..41254796c26b35fcd001a0bbb49df4bf57f6693e GIT binary patch literal 1319 zcmV+?1=#wDP)O2}(m0nUoG&Ne zFDK_a3;a(GX z93)s(j?bf#v2u)0>J0xe$*)&q6dZTop}rk2-xs`n&w)DEar&2cwe&o(rjxwm;&pVo zI@&nae-eM=5>iH%62+y|r*L-Q8iT{}bCM)&J=C`&aXU-C{UQp^nRTl>dN%b&NNXw5 z(-Rn`#h2gzOnqb-ifa?_%XEfhDkOsqtJ+wxIJ`y(@!6dbE4PeoyWf9rSyy=DQxA8d z={jqcw3C=h)7-X*+R%JtS;flgNZA=oGfUUPRzd+kr6oRQj9hbL_ok{#r;mI!2fTgH zfr!VWez0xx3OX0m(o|PX|95AoZ*Ifu_oFH@ju6PIk8sdOTT3IQr6tI+j7Q|aSH|FQ zym#^1tp_iiJ~9oU0_08Wm6fYHh%{Cq%MwSw{(9IIcidy*%^e zB-2@@4I9I> z(aapCHi-~o&d=9W1&Af4sjsQPqbdLub0U_YU}W>EZsw>^#t5h$Vv|r>-7uR5$04T) z9LGgb{;5#QvS@D(^657}v21k+1jZb&lP}C9r?lXBp@!a;aw6d-7N*jCets0CEJ(p| z$=QD1`sftdyvzEYI(j<7n5KzGk-0u02x}%tMCYW!&RS7r!{FqP7inw`6Hlg)B&mN6*tPSoM~CBtYy6xzeF4{X zXPvS{!Ok+HWtq{lq;-pQ#v(RtlTN46bsf{RSl!*i$e&4o{c{>*XyACfed(r0^DeD& zSq%j}L0O59d_GVAxyvMTelk{`OxDK8=J2~3k&us6I!#)g&+oA*QYqt|!+UogysM=L z&wuf8eaE_&D$4ynug^!RlIO_rQ(T;`!ps#gEt^bsR!OZuPrXaRDj}J6xN>!5_>(uD ze*w_$0t3}{CUb$(hAp;GJW@%3D`OcFhCt8Qm{uOkvQULhYp|4HLkrQ-X`<0<$+Jh_ z-knKLXn+nB?i`yTpb==?{9NCgwN2sn2Nr~=4^~o9UV=we$=L-`dWOVAiqY5vW5d4< zoIJeuUEpR>Lx6K9SOJ=WdZ50gqjz=N()C*c!G>0U**q_@EDOhRt&Ey{D4^00w$VL_t&-87;wkY}Ho*2k`GX=l8n5 z`@6r}-rM%Jx0OO!Su0~Oj4&s!ZOX!ybuo~s7!2;OB^$v`?epH>`*F@uqn{6vQsV6UqZqha!7pAriw8d0ihVCU z#uQrShkkqkgI7)!);!qNo6TyiLQ0I@n4KB9RvC_CZ9z9}GA(LT%0d{q##!SR#QY%Rdp_yWdn&m;2Vsz~`+5%XW_ zt%Y-m>k|zm0Y@bOBPF3+rF=F zKhgY&!b5RJC-q>fMno8AdwW zN@y&1;;s+;^~ycmUJxj04A*bmL+XYybwYwDPSiM6Dd#Uov3Q9ogtCwNOY-7Jwg!XcOgeDpP4dr^?~tLZS1@t&2HNr&?ApEw+m>m7Q^k$B2zE9L$BPI7kj5$X zyojVRU)Q~?;Z4g>7L=ZK9SDPDXUbS!HMDfzkCi36OQKgLE>m#s&Mn+OIgS6~M8o6V zHmcQI7<5~ps0sv2B-0e4nOeHMXyzs>OJAOxsvT6u<^q^fJa%`^Lw+$J^OC~sTu+v- z*#gzFP@kE^*0mbEdJR8%wwbzwpTfez-(NEDoTqW%*n&6}aS>~he2Iy<0=*Ve!Zr`_X@`EcdvIPu{{R1S8 zpy{pPQ2~T1Xvi64dd`KWD;-i!s0!3)GKQgAo*zR533!}>Bm(ob4AvGy?Ag$PSAYEm z&fX5O{woJSn2Cj<^Ei6^I7GGsrL|wgQkr7mdKHlqqQTCX-JgEdpwITLPU3{eJmHAL z1Zfz72m`D63G9EE;L^35IQsq|dOyDxs@VichTQUv;47X&{{AOmSvE|KB91vUl_BLK z9_zo7uxrEZY4ft?ohc!mnofl$EV{9&9|zVQLPPX79C%{{3$1;~7h4f~K17^CCIV(d zJGjtM$WR1c1epqvRVkgGsSoPyvJ06e=ZXuryhjse=-7AwvYHZ7@*D-!?TBvFEXGtUoM! zIucAz&*HUH?QqRaAW?ucOu)kgJW3F_A$ZKuUAAE;R6+EMA^vhc!C>$dtb7UHl84BPk@yi}KSD99Vq;Gec$~=b$we|X zRpXQrN+>yImK#6Jlp2Wgo$ej9+C({{^2D=S#Lh(Ma^yxu{{DmYq-VqDq+PJd-Kq;! zp->fya?ylBfd0!<(y52UHdRgyO_3>|=YITcuUZFH8IQwj^BEM1mYiCOWHsQ>8!c%RZ9)x= zi7>t^y>O?M%bb!zQUWP>5QB9!dyhZR)AEy?k@CyK3+$(F;n=0i<-Hlp*1bUX_{7jE zKEX@?nr+OHLeYk%FiIoe!_xfkOuxF@Qlmk~lT=Y4(iBiN22#w^j{RS<4{u)WKKahM zbogg)VRyNS-L1^=6;y3{#410uCEL__pJLj1E{PDaw2D?NY%?PN(TTo*alb-Q_5Tb0000Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyb$ z0|+Fzfv;Ks0013nR9JLFZ*6U5Zgc_CX>@2HM@dakWG-a~0006n zNkls632f6MC@{pJWxM{7 z7A{(}Gf6~p5sD@WAxMkJQuAvZ%b}TZbo6SYZ;L@ETC@l^^(+sZhjV_LZ|ANF?oU8{uG>iR2_h^CO(8I?adF6n@>u?%p1{qH)W;LGM%_ zLMV6zk9s?b;@A8POb^o4+d*P&87JTX;MWW$zrH6@ev+si;o-{%bWc8`C2|4VwAt9n z)Agc*q4^P-DjQL%f?--~T0r-a$StFY;-f`#DZQP0&;XoL6pv}ET+;K5fovgP8 zhQZ~?C8Bmsk&#;w!)F8-ON;}M%&jmmGf10z9f0Av5dbcpIDf!ocwrR72vTR)78wD= z9T!tK$!)K*non`BzmuN!=Lb!1md3%S(o`7-V0Ukq`RoD#-69r>mQrhUTujZxD|kG9 z{e=6?cPaB9IY@7Gaf}+r#itqoOs8k@3fuTpAF)t<$w*aH8l5;B-WrMAGB+EpBBewK zLE6i(xsk$k8nAQ=fG?RjHh*kUTOL7Ib)=M~JGxTWN||Z%=v*g3BZv@!Z~0Z8PIO~f zAylD~$R%0Lr|6%4g|2E;2dhc0uONg#>_2UKE5mGNjwiGPa#0000< KMNUMnLSTZMK{qJ? literal 0 HcmV?d00001 diff --git a/pixmaps/wm/button-close-focus.png b/pixmaps/wm/button-close-focus.png new file mode 100644 index 0000000000000000000000000000000000000000..d9dba88ad595fadf1f200b5c1e1846fe78acb016 GIT binary patch literal 1228 zcmV;-1T*`IP)bAYe_000McNliru)&mL^H~=l&5=H<303CEi zSad^gZEa<4bO1wgWnpw>WFU8GbZ8({Xk{QrNlj4iWF>9@00b~eL_t(I%e9r?ZxmG& z$G_*!&d<)UzqjpfZHsHFM#TuRRU@sSp!Ra+8~zlh3{9dr!Uxct}C_ zOBfs+q^_u!~q9{oKEkv|t1-t;@isyMZIp=mhpI;mp7~uB>4-XGB$8n;J zv8O$^`eJ#${6hZpslL&vDF8rcM+YA7?mip|$9_^8v>fMr(y}c3UU=x<^7ri76K0%0 z=h*h!DmS`6ve=od07AhGCdQ#OkCVBB-j0Xu#3TI5L_FEoLFq+6<{+gO-Bj zPiag*%_I;jT!VSTM$z$L1q4mg;CWtsyIGdSiO6kwv>kHcBIHaIa%&4%x(%wgu3zSM=T!78FsCWfD+q3a&S(Xc7$6pzqqbj22c+sA`W0tZ;JoC>BA2`9lKJ1H#G%8 z1n343i2zm@FdHC3GW@HTaPCq8k8OPoYN}}}l}atHFzUTR2tk)~x#;}$D;s#J*q7V4 z|D$2TB?kXqY7ngW03A2_GZ;4l~HeDD1)mtx7WP$Yh0U3d5IMC7k(LP7|sFg_k# zD3_k8+V)F!vDo$NH^<&G{6!EEeA&QLy}d`QWb$l7G2djrfaEG%DvMoXcaG( z02oV4OEJb+iijQ&LWEZ&n-@Y{=A0L0S+)V(0^r~4%l~QgD_x@tA=U&zpaFm+NmU|R qSPl5VqiN*=>5g0hVAaX-(Boeiv&1D@h4()I0000bAYe_000McNliru(+Ug~E-@G^3$*|M03CEi zSad^gZEa<4bO1wgWnpw>WFU8GbZ8({Xk{QrNlj4iWF>9@00bUML_t(|+O?HWZxcrt z#-Ew>uJ`}!+GLZ)p%<%3(?UcLB_%;BtyB&bC5Oo00F*WLddSYc5JV`Gd(nsAXW;h#CsZzUj1g?XWn^U;4KAtUBc|_ zEEyXcqqSO%0Dx(l;?A8rd~R+|{4cN&f?U0NRqF5W*9ajNAw=EXZwev8a=F~d<#I70 zM7%M0etw<~4i4&+Qdd!w9smP`5PKI~1+W%H(R0qZ|M20%o$2Xm{#x+-{5%cA(4~|f ziR#T$K_&S3?$YAq^71kOU}R(jAB~P)cUbB-#ZZf!^A*do{QdC6{_-zhzRW1+qqR!q z{H;P^CS{nIIDQX>@l`Av3si{uOae`v8xCEfH z$D(_Sd_GSorMd{ilZ9UkmrnI(FxJzHOdLa9FC)7D0L_(EC{NaLxD(@KPcOzZ1Grtd zeMyAjNlK}n&*w?kjZM>}ilR8J=l&Uc<2jDLlY#!{GHM%-5ji#}2mpat2y}uF`lgQ$ z1x2v&4AHiKMp2X>P1CFch+hF`vssCA?%ZE{oEz2*^!l4nsQ}tl@Tv*O$$wwE(}DHeZyCC-n}iVBB?Tb_$z&3493zzwaC$x1js-oHf@U(vS`vg1blMSG zMg=>njHFh@_ER5r979!AL{Ze;ZkAdgqMVZ(G%(3p(`vp^jN>N=1d z8!B_4I}TKnq1kBT^oh~$Wm&E=#=5}(xT>mtIx}!<-Et8&TA-Q*;W!|+1sEn^Ss=`U zU=C!%Mk8ur-EzPNGK=+kJpj-RCWLSRJ4Rpn_s?=u-#jRlu;ITzLrS2nY7mwIVHoIW zI-0VKt+0xf(j&N2IV8Qlzdg^}*=2P9JOG`6p(BqfZ0)Dvix(2tZ`}CmShIo5&>##q z1xc1r-||r|m9Vt7hSL`=UXi@MKN3!Qb+641G$A2`R9;wctHIXMrtgpYo10_56bfG& z?Hv#Rk!oRLV&Vs@r{}KWrtZpVucY_&J@q`V{<2q)1H4!QptsxY6s6Q7gd7$^uwBV2 zLWn1v^G#WneEY9)H2?qr07*qoM6N<$g4mWML;wH) literal 0 HcmV?d00001 diff --git a/pixmaps/wm/button-inactive.png b/pixmaps/wm/button-inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..71ff1e4dc948e24a3c1fd427bc5e4575d0514679 GIT binary patch literal 806 zcmV+>1KIqEP)E?=m}rbP4Yp8Kb1`Sk z8EhwKE+?5Ushj6C7_ORv`h;Ogp%ZBD5z!{N{m1_RX`0gMbnxCY z9*@ghL0kLtrf+X=IXO8Yj$^_wL@9;$9%mgJXAu#iC_+Rq#?Wjw8I4A4b4qP7#?WXq z*xlU)U}t9sfG`Y+0P7rqQoO#tvcJC%z;rsL?CRdZl~NdE2*VI#43$cS^?Hp`iXaHE z)}oZ+>FEg(dD}%Pe0$4jnzCN634(w)juBC4t-;PA`T<(O##*fNXdR$cNS^0> z{&IuXn!UX}`CkhKI6gj>ySqC-7!1hsoLa3$WFiDu=1Z2V74!Lw^?FUa-7W=}dj3O= zMnkeJ^Y{1n42MHR1Vjk6rdq91sZ{<+lH}7z{ZMi(TN2>ktN9MsLGYGz&LKeg+`pLo k2Q-f35A(n3{i_f44WZOvSa@cg+W-In07*qoM6N<$g8WW&;s5{u literal 0 HcmV?d00001 diff --git a/pixmaps/wm/button-max-focus.png b/pixmaps/wm/button-max-focus.png new file mode 100644 index 0000000000000000000000000000000000000000..66ada469c822e7e7a65da0c89728f77969298f8b GIT binary patch literal 1249 zcmV<71Rnc|P)bAYe_000McNliru)eIdGD;SYn=?4G+03CEi zSad^gZEa<4bO1wgWnpw>WFU8GbZ8({Xk{QrNlj4iWF>9@00c!zL_t(I%e9qVOdM4d z$NzWk+?`=}XLn|I`RY=zg^CnJlrM>Gu!aYTruD17`Cu9!gjNzBjZgNm55@?*82e(Q z2^gyh6=Sfak%R^p3rd!tp{-eXTx!vMb7!UZQF@#HoMu>)Z`opjzl8D z?Ch*&Syr847;W)*yd@HeygV~A0|2P6uSauh^VLvcXwvWXPcX)2>+0$fyWwQ_^#=w9 zRLim&iSCO)uFF?Dr+inuKOIGX*>bH&kumZ*hpbKGLFmrpWyRPKkIqr^_T48 z!jfbx7Mli;f4~wRB!h#4#57H}p3$3cjE?mp1QeYpf^Y;#ii^0^_dYK5U4%gwAyxv1 zJIg^r95+VCdi9LnY?`KfaBz_9nYf{$L69U#)i>g&l2nJ-!tqP&I!fZR_%-dum*4${ zqfhz4?)(7V%wWm81=`}kn9`9*$4|+!e669OAp^kv2UycIYMQ2hA$s?0Dx5-5g^f@} z7AqMYIk*ve!h(@Ev9_hd$Yo(E4#Y|s>&p}i(Yt2}As01GTiF9PO_RD@E@gi8XK2q8 zls%Hee7^s(fE#rkW>s+4q~|A4gi8PZy$x8i(~z*`&dpG@VMhRR(>3!D5h=O zcJ_cPDk_|GI=#_+tPQtPx1oq01mr>l;r9+b4~#iSeB(R#)@p)4g>IS9~+d?CCjqbwskdk zLk~3iZK-6=xzDz3Y1`7m7+E(Xarq7PxD00000 LNkvXXu0mjf@`*V( literal 0 HcmV?d00001 diff --git a/pixmaps/wm/button-max.png b/pixmaps/wm/button-max.png new file mode 100644 index 0000000000000000000000000000000000000000..db24ada7059bc5cf3760d82d221b45a18914f1b9 GIT binary patch literal 1220 zcmV;#1UvhQP)bAYe_000McNliru(+Ug~FbfJ{?brYS03CEi zSad^gZEa<4bO1wgWnpw>WFU8GbZ8({Xk{QrNlj4iWF>9@00byWL_t(|+O?J6YaCS+ z$G>Op+?|=-o!OcF)%>zS`Xg;f>(XXxx9UUDAc2DTqu6JCt4N^#f`5RB&==nX3yoS4 zERp`w2Q?D5(AA~2wXs6lq$b(f-DG!XHZymwFR8{R5)^u#F5JWC-1Fs}?**Pv&~^z@ zN}8IQQf%8M0BAHCa(sL|A|m;}z*0(@o}Olo;}}F_3L$g=V2t@v%GG+k?v+X<@oxpT z1eeQYMc4H>W6U9yFE1Y{m&>nKDir|0!Gi};e75*SHkX}AB~#O!^XkaRNd0j*J>LG=vuEuf2oBe3 zwG)>wU!Jfm3xz@fef@nP0v_C-N2O9hv)ROpV`CrWGMTS9=hsi4KHb)3N(FRo5q!MTq=nRw&WYa*A)H|FN% zZU6`$v6M%L$;nCbJTLCH-Qv|t*Ut0|_aJ{L59$M&d;#;`FL>bH!HPN$HUMM~<$;nCD^x{IHplF(AyQ|B`8_dPPP!E>$3hp#+W4@z9QB)8Cp$O6H zdZ_7ZU_%nknu~gC`M9p@7Yl_#8-Vx+*l`@@d0uMv=Dm~gVFPNe2BEdk@ChL-C@P1P z5;6+W3EL1x2(>2(W1oT9oA*u<(R+^LJlq8KJded zxXM5v1W|yPr$SL(#1s$Vs*PJe-vH-4$vIaxNnzVIeBb9$B1A3?^k$Q=tTq$Nti-)k2wm4f^!Jec1$JFmnx?t7_o*T1S`D_DLpqa!Wu%cz zn9ySy0K%H+LTjr~Rf2*Q=-MLo?A#AkRa-)cXcKsFa4>4MTC2r9CEWGS!%zod#%)-Z ziDbfrp&KAc=tgT$I3OjV+m7IOe#i4K9Qahzv=z>Iv}xP`M4F~G`}dh2iN!QJ-4KRq z!nPdP2@57m!cvn+#S*YnX~c~rx_$_;ltKTF{%<+wbpX*;3nDn@_1y!zKTGA`T>GYa z?wx!#hm4bl!VJWj0e~O`1G-(bJ5Bs{YXPsneCquq>zx7M5z%Ha5lJbfm&^8E*L_u< z7=1hX`r?ImJ4d@1+PMpzcp9prqS0>R?!vFA&OF5Ub8mfU8+{iwmYUtTbz7Q{l#l$H=eTQ~YH5v>650fhg_=(i}q iHsneGV!N;DiN&9L{JAjl;@%bAYe_000McNliru)eIdGEHRv~=d1t#03CEi zSad^gZEa<4bO1wgWnpw>WFU8GbZ8({Xk{QrNlj4iWF>9@00VbPL_t(I%k7j)ZyRM4 zhM#Z7V~;&E9@`VgcG5JoO;a3KBu%-fMJWm(R;*|@EXo2{AR(~;HeDfBS@8>CQMKEO zKTrfB5kgc-)2J$;~SG&}H4I-id zZC9@VEv1zFXYffd4vvj=xt29(I?ii#ckauX@0S1=9zR5+H1tKo_kS#w%fH{7n%bC# z=Dy&QU>q79&85@nGc|YNa@IB(d3A(XY7iwtd2^nd*KV?0EE6jpJMXXmIXQRz`dTMk z?*yMYeL9lXul{@Ilp`~Jveo-n&&8H&Zb(;(o!py3b3&4bk>^yeM?uj)K zaq6>oD4exPB+GpCO_fu_9$F-V3Mmu{5fiBdXpL>&dqZ)s9OLJ2mk<$0DOCZ2PK}jP z%8a`$y3!CyCmDOGn@Bu~VY}!t7u|^yvf>!YBtI=i3A-s$IdI(orIfkbWt|$6JdwjN zOXyC9Pd;=}P7-1X2$_!>9yXw|&ByN^puXc1`D+R9MDLzk(Ee!(sSO+)yIAz=5U~;4 zgLoX}IB1DDS|Wi;IHky`sI_kdeUDL4HZ3iNgdK2Gh-p!(H<&LwBy)X}N+};`?E$y9S+%_G8%OdVOw2Fe=Kkg;^*|>e z1jGQFfDkniHg}3F{&Aaugze zSI$|n5&+G>C7(Ta#kG5`g^jLXEW^6hn}2glDb?CH-9$tKV$EhVX&6Qq&;!KVpG&}M zv)SA*3}XwZbVC0%kK32115puif*>#fbX~8t7gYsbAYe_000McNliru(+Uh00Wb1-u;Ks!03CEi zSad^gZEa<4bO1wgWnpw>WFU8GbZ8({Xk{QrNlj4iWF>9@00b9FL_t(|+O?I>Z&Xzj z$3OS|o|!lA$4sXb+d&BkC~YV|q^*$9g#|0u*d8;jC_Q&;6Wp&pqdR;W?RD&Ou5khKGk$ z&+`O8xm=ce_wJ2_5b}S4rIcc1WJL3Q-x5MNaU7cfs;V}nlygB4GzJC+qNfFTB6w_U zOtma4rK+kgglHE+bgHWA0>p7#ky1`cDeuK`95fn@aND+R@v`8tu`%_p@o}%#YIWBu zm4RS(w*TT!qX#CgUISq5rXF_m^_|Y=^Ow@DJCf1$@m1a3<;UUT@#~*Be%xy|n_Ft7 z(t&Tzo;}i@$q_OWB%Zn`%2lJ#-`%m*blZCJvdO2aBPhAtemM)I#b0_VZ`X9qli!;I)kfIt$=1jar>Rt=_^B({vV~Z(}<< zUu@^%nQxCqmD2Vw3{yixL&}njdwY8o(=`27d3G>AH^b(hB02Rs>huK7ssZBPGct)0 zwHChCWMdc1-3eF-?hjg)^;K_gZyiV;0o%5%L{a42y!GdyS68R$C(Gq=%gv$X97NWGw2!KrAOyrs zsOl&}U?^2gqk_41J-V(}gCIzjbVadPj19x6>>TVlSzo9k%`DQ+f#V`v8zt|e6kLSo zAY2FOy^gL6H%d9mjflv~fa@cC2b>Hz8H8hl z?~|l$DvgL=%Xz$xt_y}?lpl1&BjClUsb_Ne(Y}4}9vi(i&D2bVFxF`ai$u+m$Sf^2 zO^Ct0`6`zurs>~*@K`FhauldPb@G7NZhz@^v*rx19{4~zb^g>xTj~vpYhT88+YnMv ztCzSt`5TwN3mDw>;U|{8YS=WL+d%xEA|$0$rf!_`=c2H?QJWd4RHynb{&4A?Y_D#&K*d3VlNeF%N`5{7gdsgaFDzz7j~5` ظاهر -> رنگ نوشته‌ها -> روز تعطیل''' +authors = [ +'شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران', +'سعید رسولی ', +] +has_config = False + +holidays = { + 'jalali':( + (1, 1), + (1, 2), + (1, 3), + (1, 4), + (1, 12), + (1, 13), + (3, 14), + (3, 15), + (11, 22), + (12, 29) + ), + 'hijri':( + (1, 9), + (1, 10), + (2, 20), + (2, 28), + (2, 30), + (3, 17), + (6, 3), + (7, 13), + (7, 27), + (8, 15), + (9, 21), + #(9, 30),## before fitr FIXME + (10, 1), + (10, 2),## after fitr FIXME + (10, 25), + (12, 10), + (12, 18) + ) +} + +default_enable = False +default_show_date = False + diff --git a/plugins/holidays-iran.json b/plugins/holidays-iran.json new file mode 100644 index 000000000..270f20711 --- /dev/null +++ b/plugins/holidays-iran.json @@ -0,0 +1,122 @@ +{ + "type": "holiday", + "title": "تعطیلات رسمی ایران", + "about": "Official Holidays of Iran, Islamic Republic of\nتعطیلات رسمی جمهوری اسلامی ایران (آخرین تغییرات: ۱۳۸۸)\n\nبرای روزهای تعطیل، رنگ شماره‌ ماه را تغییر می‌دهد\nبرای تنظیم این رنگ مراجعه کنید به:\nترجیحات -> ظاهر -> رنگ نوشته‌ها -> روز تعطیل", + "authors": [ + "شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران", + "سعید رسولی " + ], + "hasConfig": false, + "holidays": { + "jalali": [ + [ + 1, + 1 + ], + [ + 1, + 2 + ], + [ + 1, + 3 + ], + [ + 1, + 4 + ], + [ + 1, + 12 + ], + [ + 1, + 13 + ], + [ + 3, + 14 + ], + [ + 3, + 15 + ], + [ + 11, + 22 + ], + [ + 12, + 29 + ] + ], + "hijri": [ + [ + 1, + 9 + ], + [ + 1, + 10 + ], + [ + 2, + 20 + ], + [ + 2, + 28 + ], + [ + 2, + 30 + ], + [ + 3, + 17 + ], + [ + 6, + 3 + ], + [ + 7, + 13 + ], + [ + 7, + 27 + ], + [ + 8, + 15 + ], + [ + 9, + 21 + ], + [ + 10, + 1 + ], + [ + 10, + 2 + ], + [ + 10, + 25 + ], + [ + 12, + 10 + ], + [ + 12, + 18 + ] + ] + }, + "default_enable": false, + "default_show_date": false +} diff --git a/plugins/iran-ancient-data.txt b/plugins/iran-ancient-data.txt new file mode 100644 index 000000000..96a2b00f8 --- /dev/null +++ b/plugins/iran-ancient-data.txt @@ -0,0 +1,76 @@ +#!/usr/bin/python +## جشن‌های باستانی ایران +## رضا مرادی قیاس آبادی (www.ghiasabadi.com) +## سعید رسولی +01/06 روز امید، روز شادباش‌نویسی +01/10 جشن آبانگاه +01/13 جشن سیزده‌بدر +01/17 سروش‌روز، جشن سروشگان +01/19 فرورین‌روز، جشن فروردینگان +02/02 جشن گیاه‌آوری +02/03 اردیبهشت‌روز، جشن اردیبهشتگان +02/10 جشن چهلم نوروز +02/15 گاهنبار میدیوزَرِم، جشن میانهٔ بهار، جشن بهاربُد/ روز پیام‌آوری زرتشت +03/01 ارغاسوان، جشن گرما +03/06 خردادروز، جشن خردادگان +04/01 جشن آب‌پاشونک، جشن آغاز تابستان/ سال نو در گاهشماری گاهنباری/ دیدار طلوع خورشید در تقویم آفتابی چارتاقی نیاسر +04/06 جشن نیلوفر +04/13 تیرروز، جشن تیرگان +04/15 جشن خام‌خواری +05/07 مردادروز، جشن مردادگان +05/10 جشن چلهٔ تابستان +05/15 گاهنبار میدیوشِم، جشن میانهٔ تابستان +05/18 جشن مَی‌خواره +06/01 فغدیه، جشن خنکی هوا +06/03 جشن کشمین +06/04 شهریورروز، جشن شهریورگان/ زادروز داراب (کوروش)/ عروج مانی +06/08 خزان‌جشن +06/15 بازارجشن +06/31 گاهنبار پَتیَه‌شَهیم، جشن پایان تابستان +07/01 جشن میتراکانا/ سال نو هخامنشی +07/12 آیین قالیشویان اردهال، بازماندی از تیرگان +07/13 تیرروز، جشن تیرروزی +07/16 مهرروز، جشن مهرگان +07/21 رام‌روز، جشن رام‌روزی/ جشن پیروزی کاوه و فریدون +08/10 آبان‌روز، جشن آبانگان +08/15 گاهنبار اَیاثرَم، جشن میانهٔ پاییز +09/01 آذرجشن +09/09 آذرروز، جشن آذرگان +09/30 جشن شب یلدا (چله)/ گاهنبار میدیارِم، جشن میانهٔ سال گاهنباری (از مبدأ آغاز تابستان) و پایان پاییز +10/01 روز میلاد خورشید، جشن خرم‌روز/ نخستین جشن دیگان/ دیدار طلوع خورشید در تقویم آفتابی چارتاقی نیاسر +10/05 بازارجشن +10/08 دی‌به‌آذرروز، دومین جشن دیگان +10/14 سیرسور، جشن گیاه‌خواری +10/15 جشن پیکرتراشی/ دی‌به‌مهرروز، سومین جشن دیگان +10/16 جشن درامزینان، جشن درفش‌ها +10/23 دی‌به‌دین‌روز، چهارمین جشن دیگان +11/01 زادروز فردوسی +11/02 بهمن‌روز، جشن بهمنگان +11/04 شهریورروز، آغاز پادشاهی داراب (کوروش) +11/05 جشن نوسَره +11/10 آبان‌روز، جشن سَدَه، آتش‌افروزی بر بام‌ها/ نمایش‌بازی همگانی +11/15 جشن میانهٔ زمستان +11/22 بادروز، جشن بادروزی +12/01 جشن اسفندی/ جشن آبسالان، بهارجشن/ نمایش‌بازی همگانی +12/05 اسفندروز، جشن اسفندگان، گرامیداشت زمین و بانوان/ جشن برزگران +12/10 جشن وخشنکام +12/19 جشن نوروز رودها +12/20 جشن گلدان +12/25 هزارهٔ شاهنامه، هزارمین سالگرد پایان سرایش شاهنامهٔ فرودسی +12/26 فروردگان +12/29 گاهنبار هَمَسپَتمَدَم، جشن پایان زمستان (در آخرین روز سال)/ زادروز زرتشت/ جشن اوشیدر (نجات‌بخش ایرانی) در دریاچهٔ هامون و کوه خواجه/ آتش‌افروزی بر بام‌ها در استقبال از نوروز + + + + +1388/01/05 جشن نخستین چهارشنبهٔ سال +1388/12/25 چهارشبنه‌سوری، جشن شب چهارشنبهٔ آخر سال + + + + + + + + + diff --git a/plugins/iran-ancient.json b/plugins/iran-ancient.json new file mode 100644 index 000000000..90b2f5817 --- /dev/null +++ b/plugins/iran-ancient.json @@ -0,0 +1,16 @@ +{ + "type": "yearlyText", + "dataFile": "iran-ancient-data.txt", + "calType": "jalali", + "title": "جشن‌های باستانی ایران", + "about": "جشن‌های باستانی ایران\nآخرین تغییرات: ۲۰۰۹/۰۷/۱۹", + "authors": [ + "رضا مرادی قیاس آبادی (www.ghiasabadi.com)", + "سعید رسولی " + ], + "hasConfig": false, + "hasImage": false, + "lastDayMerge": true, + "default_enable": false, + "default_show_date": false +} diff --git a/plugins/iran-ancient.spg b/plugins/iran-ancient.spg new file mode 100644 index 000000000..1cf0a8e2a --- /dev/null +++ b/plugins/iran-ancient.spg @@ -0,0 +1,18 @@ +#!/usr/bin/env python +## StarCal Builtin Plugin +db_name = 'iran-ancient-data.txt' +mode = 'jalali' +desc = 'جشن‌های باستانی ایران' +about = \ +'''جشن‌های باستانی ایران +آخرین تغییرات: ۲۰۰۹/۰۷/۱۹''' +authors = [ +'رضا مرادی قیاس آبادی (www.ghiasabadi.com)', +'سعید رسولی ' +] +has_config = False +has_image = False + +default_enable = False +default_show_date = False + diff --git a/plugins/iran-gregorian-2-data.txt b/plugins/iran-gregorian-2-data.txt new file mode 100644 index 000000000..bfe4704a0 --- /dev/null +++ b/plugins/iran-gregorian-2-data.txt @@ -0,0 +1,41 @@ +#!/usr/bin/python +## شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران‬، و شورای فرهنگ عمومی +## سعید رسولی +## مولا پهنادایان +03/16 تولد ریچارد استالمن (مؤسس بنیاد نرم‌افزار آزاد و پروژهٔ گنو) +03/22 روز جهانی آب +03/23 روز جهانی هواشناسی +05/05 روز جهانی ماما +05/08 روز جهانی صلیب سرخ و هلال احمر +#05/16 روز جهانی ارتباطات +#05/17 روز جهانی ارتباطات +05/18 روز جهانی موزه و میراث فرهنگی +05/31 روز جهانی بدون دخانیات +06/10 روز جهانی صنایع دستی +06/17 روز جهانی بیابان‌زدایی +06/26 روز جهانی مبارزه با مواد مخدر +06/29 انتشار مجوز همگانی عمومی گنو نسخهٔ ۳ (GNU GPLv3) توسط بنیاد نرم‌افزار آزاد (۲۰۰۷ میلادی) +08/01 روز جهانی شیر مادر +08/06 انفجار بمب اتمی آمریکا در هیروشیما با بیش از ۱۶۰ هزار کشته و مجروح (۱۹۴۵ میلادی) +09/27 روز جهانی جهانگردی +09/30 روز جهانی ناشنوایان – روز جهانی دریانوردی +10/01 روز جهانی سالمندان +10/08 روز جهانی کودک +10/09 روز جهانی پست +10/14 روز جهانی استاندارد +10/16 روز جهانی غذا +12/01 روز جهانی مبارزه با ایدز +## ^^^ Search about HIV (EIDZ) international day +12/07 روز جهانی هواپیمایی + +## روز آزادی نرم‌افزار: سومین شنبهٔ ماه سپتامبر +2006/09/16 روز جهانی آزادی نرم‌افزار (http://softwarefreedomday.org/SoftwareFreedom) +2007/09/15 روز جهانی آزادی نرم‌افزار (http://softwarefreedomday.org/SoftwareFreedom) +2008/09/20 روز جهانی آزادی نرم‌افزار (http://softwarefreedomday.org/SoftwareFreedom) +2009/09/19 روز جهانی آزادی نرم‌افزار (http://softwarefreedomday.org/SoftwareFreedom) +2010/09/18 روز جهانی آزادی نرم‌افزار (http://softwarefreedomday.org/SoftwareFreedom) +2011/09/17 روز جهانی آزادی نرم‌افزار (http://softwarefreedomday.org/SoftwareFreedom) +2012/09/15 روز جهانی آزادی نرم‌افزار (http://softwarefreedomday.org/SoftwareFreedom) +2013/09/21 روز جهانی آزادی نرم‌افزار (http://softwarefreedomday.org/SoftwareFreedom) +2014/09/20 روز جهانی آزادی نرم‌افزار (http://softwarefreedomday.org/SoftwareFreedom) +2015/09/19 روز جهانی آزادی نرم‌افزار (http://softwarefreedomday.org/SoftwareFreedom) diff --git a/plugins/iran-gregorian-2.json b/plugins/iran-gregorian-2.json new file mode 100644 index 000000000..87bc8b31f --- /dev/null +++ b/plugins/iran-gregorian-2.json @@ -0,0 +1,17 @@ +{ + "type": "yearlyText", + "dataFile": "iran-gregorian-2-data.txt", + "calType": "gregorian", + "title": "مناسبت‌های میلادی(سایر)", + "about": "مناسبت‌های میلادی(سایر)\nمناسبت‌های میلادی که توسط مؤلف برنامه اضافه شده‌اند و در تقویم رسمی ایران نیست. و یا به تازگی از تقویم رسمی ایران حذف شده‌اند.\nآخرین تغییرات: ۲۰۱۳/۰۲/۱۵", + "authors": [ + "شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران و شورای فرهنگ عمومی", + "سعید رسولی ", + "مولا پهنادایان " + ], + "hasConfig": false, + "hasImage": false, + "lastDayMerge": true, + "default_enable": false, + "default_show_date": false +} diff --git a/plugins/iran-gregorian-2.spg b/plugins/iran-gregorian-2.spg new file mode 100644 index 000000000..99497ef1c --- /dev/null +++ b/plugins/iran-gregorian-2.spg @@ -0,0 +1,21 @@ +#!/usr/bin/env python +## StarCal Builtin Plugin +db_name = 'iran-gregorian-2-data.txt' +mode = 'gregorian' +desc = \ +'مناسبت‌های میلادی(سایر)' +about = \ +'''مناسبت‌های میلادی(سایر) +مناسبت‌های میلادی که توسط مؤلف برنامه اضافه شده‌اند و در تقویم رسمی ایران نیست. و یا به تازگی از تقویم رسمی ایران حذف شده‌اند. +آخرین تغییرات: ۲۰۱۳/۰۲/۱۵''' +authors = [ +'شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران و شورای فرهنگ عمومی', +'سعید رسولی ', +'مولا پهنادایان ' +] +has_config = False +has_image = False + +default_enable = False +default_show_date = False + diff --git a/plugins/iran-gregorian-data.txt b/plugins/iran-gregorian-data.txt new file mode 100644 index 000000000..32c7d7021 --- /dev/null +++ b/plugins/iran-gregorian-data.txt @@ -0,0 +1,12 @@ +#!/usr/bin/python +## مطابق با استخراج و تنظیم شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران‬ (مناسبت‌ها از شورای فرهنگ عمومی) +## گردآوری، فرمت‌بندی و به روزآوری: سعید رسولی ، مولا پهنادایان +01/01 آغاز سال میلادی +05/01 روز جهانی کار و کارگر +06/05 روز جهانی محیط زیست +08/21 روز جهانی مسجد +10/14 روز جهانی استاندارد +10/15 روز جهانی نابینایان (عصای سفید) +11/10 روز جهانی علم در خدمت صلح و توسعه +12/03 روز جهانی معلولان +12/25 ‫ولادت حضرت عیسی مسیح (ع) diff --git a/plugins/iran-gregorian.json b/plugins/iran-gregorian.json new file mode 100644 index 000000000..4fd5e8b73 --- /dev/null +++ b/plugins/iran-gregorian.json @@ -0,0 +1,17 @@ +{ + "type": "yearlyText", + "dataFile": "iran-gregorian-data.txt", + "calType": "gregorian", + "title": "مناسبت‌های میلادی (تقویم رسمی ایران)", + "about": "مناسبت‌های میلادی (تقویم رسمی ایران)\nآخرین تغییرات: ۲۰۱۳/۰۲/۱۵", + "authors": [ + "شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران و شورای فرهنگ عمومی", + "سعید رسولی ", + "مولا پهنادایان " + ], + "hasConfig": false, + "hasImage": false, + "lastDayMerge": true, + "default_enable": true, + "default_show_date": false +} diff --git a/plugins/iran-gregorian.spg b/plugins/iran-gregorian.spg new file mode 100644 index 000000000..f1dbbb673 --- /dev/null +++ b/plugins/iran-gregorian.spg @@ -0,0 +1,20 @@ +#!/usr/bin/env python +## StarCal Builtin Plugin +db_name = 'iran-gregorian-data.txt' +mode = 'gregorian' +desc = \ +'مناسبت‌های میلادی (تقویم رسمی ایران)' +about = \ +'''مناسبت‌های میلادی (تقویم رسمی ایران) +آخرین تغییرات: ۲۰۱۳/۰۲/۱۵''' +authors = [ +'شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران و شورای فرهنگ عمومی', +'سعید رسولی ', +'مولا پهنادایان ' +] +has_config = False +has_image = False + +default_enable = True +default_show_date = False + diff --git a/plugins/iran-hijri-2-data.txt b/plugins/iran-hijri-2-data.txt new file mode 100644 index 000000000..61c5a16e0 --- /dev/null +++ b/plugins/iran-hijri-2-data.txt @@ -0,0 +1,32 @@ +#!/usr/bin/python +## سعید رسولی +02/03 ولادت امام محمد باقر(ع)(۵۷ ﻫ. ق) به روایتی +03/12 آغاز واجب شدن نماز، ورود پیامبر به مدینه +03/15 بنای مسجد قبا(اولین مسجد در اسلام) +03/16 ورود اهل بیت اما حسین(ع) به شام +03/17 بنای مسجدالنبی در مدینه +03/23 ورود حضرت معصومه(س) به قم +03/26 صلح امام حسن(ع) +04/03 سفر اما حسن(ع) به جرجان +04/04 ولادت شاه عبدالعظیم حسنی(ع) +04/06 هلاکت هشام بن عبدالملک صادر کنندهٔ دستور شهادت امام باقر(ع) +04/08 شهادت حضرت فاطمهٔ زهرا(س)(۱۱ ﻫ. ق) به روایتی +05/10 وقوع جنگ جمل بین سپاهیان امام علی(ع) و ناکثین +05/27 وفات حضرت عبدالمطلب(ع) +06/04 هلاکت هارون‌الرشید قاتل اما کاظم(ع) +06/12 حرکت پیامبر به سمت خیبر +06/13 وفات ام‌البنین(س) +06/19 ازدواج حضرت عبدالله(ع) و آمنه(س) +06/21 ولادت حضرت ام‌کلثوم(س) +07/09 ولادت حضرت علی اصغر(ع) +07/16 خروج فاطمه بنت اسد از کعبه +07/23 مجروح شدن اما حسن مجتبی(ع) در ستبتط مدائن، مسموم شدن امام موسی کاظم(ع) به دستور مأمون +07/24 فتح قلعهٔ خیبر توسط حضرت علی(ع)، بازگشت حعفر بن ابی‌طالب از حبشه +07/26 وفات حضرت ابوطالب(ع) +07/28 حرکت امام حسین(ع) از مدینه به مکه +07/30 هجرت مسلمانان به حبشه، غزوهٔ نخله +08/15 روز سربازان گمنام امام زمان(عج) +10/21 فتح اندلس به دست مسلمانان(۹۲ ه‍‍.ق) +11/05 ‫روز بزرگداشت حضرت صالح بن موسی كاظم (ع) + + diff --git a/plugins/iran-hijri-2.json b/plugins/iran-hijri-2.json new file mode 100644 index 000000000..31d2f1baf --- /dev/null +++ b/plugins/iran-hijri-2.json @@ -0,0 +1,15 @@ +{ + "type": "yearlyText", + "dataFile": "iran-hijri-2-data.txt", + "calType": "hijri", + "title": "مناسبت‌های اسلامی (سایر)", + "about": "مناسبت‌های اسلامی (سایر)\nوقایع اسلامی که در تقویم رسمی ایران درج نشده‌اند\nآخرین تغییرات: ۲۰۱۲/۰۶/۰۶", + "authors": [ + "سعید رسولی " + ], + "hasConfig": false, + "hasImage": false, + "lastDayMerge": true, + "default_enable": false, + "default_show_date": false +} diff --git a/plugins/iran-hijri-2.spg b/plugins/iran-hijri-2.spg new file mode 100644 index 000000000..c962a0cbc --- /dev/null +++ b/plugins/iran-hijri-2.spg @@ -0,0 +1,19 @@ +#!/usr/bin/env python +## StarCal Builtin Plugin +db_name = 'iran-hijri-2-data.txt' +mode = 'hijri' +desc = \ +'مناسبت‌های اسلامی (سایر)' +about = \ +'''مناسبت‌های اسلامی (سایر) +وقایع اسلامی که در تقویم رسمی ایران درج نشده‌اند +آخرین تغییرات: ۲۰۱۲/۰۶/۰۶''' +authors = [ +'سعید رسولی ' +] +has_config = False +has_image = False + +default_enable = False +default_show_date = False + diff --git a/plugins/iran-hijri-data.txt b/plugins/iran-hijri-data.txt new file mode 100644 index 000000000..3ad9c423f --- /dev/null +++ b/plugins/iran-hijri-data.txt @@ -0,0 +1,78 @@ +#!/usr/bin/python +## مطابق با استخراج و تنظیم شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران‬(مناسبت‌ها از شورای فرهنگ عمومی) +## گردآوری، فرمت‌بندی و به روزآوری: سعید رسولی ، مولا پهنادایان +01/01 آغاز سال هجری قمری +01/09 تاسوعای حسینی +01/10 عاشورای حسینی +01/11 روز تجلیل از اسرا و مفقودان +01/12 شهادت امام زین‌العابدین(ع)(۹۵ ه‍‍.ق) +##01/18 تغییر قبلهٔ مسلمین از از بیت‌المقدس به مکهٔ معظمه(۲ ه‍‍.ق) ###### تغییر به ۱۵ رجب(07/15) +01/25 شهادت امام زین‌العابدین(ع)(۹۵ ه‍‍.ق) به روایتی +02/07 ولادت امام موسی کاظم(ع)(۱۲۸ ه‍‍.ق) – روز بزرگداشت سلمان فارسی +02/20 اربعین حسینی +02/27 روز وقف +02/28 رحلت حضرت رسول اکرم(ص)(۱۱ ه‍‍.ق) – شهادت امام حسن مجتبی(ع)(۵۰ ه‍‍.ق) +02/30 شهادت امام رضا(ع)(۲۰۳ ه‍‍.ق) +03/01 هجرت رسول اکرم(ص) از مکه به مدینه +03/08 شهادت امام حسن عسگری(ع)(۲۶۰ ه‍‍.ق) +03/12 ولادت حضرت رسول اکرم(ص) به روایت اهل سنت(۵۳ سال قبل از هجرت). آغاز هفتهٔ وحدت +03/17 ولادت حضرت رسول اکرم(ص)(۵۳ سال قبل از هجرت) و روز اخلاق و مهرورزی – ولادت امام جعفر صادق(ع) مؤسس مذهب جعفری(۸۳ ه‍‍.ق) +04/08 ولادت امام حسن عسگری(ع)(۲۳۲ ه‍‍.ق) +04/10 وفات حضرت معصومه(س)(۲۰۱ ه‍‍.ق) +05/05 ولادت حضرت زینب(س)(۵ ه‍‍.ق) و روز پرستار +05/13 شهادت حضرت فاطمهٔ زهرا(س)(۱۱ ه‍‍.ق) به روایتی +06/03 شهادت حضرت فاطمهٔ زهرا(س)(۱۱ ه‍‍.ق) +06/13 ‫سالروز وفات حضرت ام‌البنین (س) ـ روز تکریم مادران و همسران شهدا‬ +06/20 ولادت حضرت فاطمهٔ زهرا(س)(سال هشتم قبل از هجرت) و روز زن – تولد امام خمینی(ره) رهبر کبیر انقلاب اسلامی(۱۳۲۰ ه‍‍.ق) +07/01 ولادت امام محمد باقر(ع)(۵۷ ه‍‍.ق) +07/03 شهادت امام علی النقی الهادی(ع)(۲۵۴ ه‍‍.ق) +07/10 ولادت امام محمد تقی(ع)(۱۹۵ ه‍‍.ق) +07/13 ولادت حضرت امام علی(ع)(۲۳ سال قبل از هجرت) – آغاز ایام‌البیض(اعتکاف) +07/15 وفات حضرت زینب(س)(۶۲ ه‍‍.ق) – تغییر قبلهٔ مسلمین از از بیت‌المقدس به مکهٔ معظمه(۲ ه‍‍.ق) +07/25 شهادت امام موسی کاظم(ع)(۱۸۳ ه‍‍.ق) +07/27 مبعث حضرت رسول اکرم(ص)(۱۳ سال قبل از هجرت) +08/03 ولادت امام حسین(ع)(۴ ه‍‍.ق) و روز پاسدار +08/04 ولادت حضرت ابوالفضل العباس(ع)(۲۶ ه‍‍.ق) و روز جانباز +08/05 ولادت امام زین‌العابدین(ع)(۳۸ ه‍‍.ق) +08/11 ولادت حضرت علی اکبر(ع)(۳۳ ه‍‍.ق) و روز جوان +08/15 ولادت حضرت قائم(عج)(۲۵۵ ه‍‍.ق) و روز جهانی مستضعفان +09/10 وفات حضرت خدیجه(س)(۳ سال قبل از هجرت) +09/15 ولادت امام حسن مجتبی(ع)(۳ ه‍‍.ق) و روز اکرام +09/18 شب قدر +09/19 ضربت خوردن حضرت علی(ع)(۵۴۰ ه‍‍.ق) +09/20 شب قدر +09/21 شهادت حضرت علی(ع)(۵۴۰ ه‍‍.ق) +09/22 شب قدر +10/01 عید سعید فطر +10/17 ‫روز فرهنگ پهلوانی و ورزش زورخانه‌ای +10/25 شهادت امام جعفر صادق(ع)(۱۴۸ ه‍‍.ق) +11/01 ولادت حضرت معصومه(س)(۱۷۳ ه‍‍.ق) و روز دختران +11/05 روز تجلیل از امام‌زادگان و بقاع متبرکه +11/06 روز بزرگداشت حضرت احمدبن‌موسی شاهچراغ(ع) +11/11 ولادت امام رضا(ع)(۱۴۸ ه‍‍.ق) +11/30 شهادت امام محمد تقی(ع)(۲۲۰ ه‍‍.ق) +12/01 سالروز ازدواج حضرت علی(ع) و حضرت فاطمه(س)(۲ ه‍‍.ق) – روز ازدواج +12/06 ‫شهادت مظلومانه زائران خانهٔ خدا به دستور آمریکا به دست مأموران آل سعود(۱۳۶۶ ه‍.ش برابر با ۶ ذی‌الحجه ۱۴۰۷ ه‍‍.ق) +12/07 شهادت امام محمد باقر(ع)(۱۱۴ ه‍‍.ق) +12/09 روز عرفه – روز نیایش +12/10 عید سعید قربان +12/15 ولادت امام علی النقی الهادی(ع)(۲۱۲ ه‍‍.ق) +12/18 عید سعید غدیر خم(۱۰ ه‍‍.ق) +12/24 روز مباهلهٔ پیامبر اسلام(ص)(۱۰ ه‍‍.ق) +12/25 روز خانواده و تکریم بازنشستگان +1430/12/01 روز تکریم بازنشستگان +1427/09/26 روز جهانی قدس(آخرین جمعهٔ ماه رمضان) +1428/09/30 روز جهانی قدس(آخرین جمعهٔ ماه رمضان) +1429/09/25 روز جهانی قدس(آخرین جمعهٔ ماه رمضان) +1430/09/28 روز جهانی قدس(آخرین جمعهٔ ماه رمضان) +1431/09/23 روز جهانی قدس(آخرین جمعهٔ ماه رمضان) +1432/09/26 روز جهانی قدس(آخرین جمعهٔ ماه رمضان) +1433/09/28 روز جهانی قدس(آخرین جمعهٔ ماه رمضان) +1434/09/24 روز جهانی قدس(آخرین جمعهٔ ماه رمضان) +1435/09/27 روز جهانی قدس(آخرین جمعهٔ ماه رمضان) +1436/09/30 روز جهانی قدس(آخرین جمعهٔ ماه رمضان) + + + + + diff --git a/plugins/iran-hijri.json b/plugins/iran-hijri.json new file mode 100644 index 000000000..7307831b9 --- /dev/null +++ b/plugins/iran-hijri.json @@ -0,0 +1,17 @@ +{ + "type": "yearlyText", + "dataFile": "iran-hijri-data.txt", + "calType": "hijri", + "title": "مناسبت‌های اسلامی (تقویم رسمی ایران)", + "about": "مناسبت‌های اسلامی (تقویم رسمی ایران)\nآخرین تغییرات: ۲۰۱۳/۰۲/۱۵", + "authors": [ + "شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران و شورای فرهنگ عمومی", + "سعید رسولی ", + "مولا پهنادایان " + ], + "hasConfig": false, + "lastDayMerge": true, + "hasImage": false, + "default_enable": true, + "default_show_date": false +} diff --git a/plugins/iran-hijri.spg b/plugins/iran-hijri.spg new file mode 100644 index 000000000..321e25c86 --- /dev/null +++ b/plugins/iran-hijri.spg @@ -0,0 +1,22 @@ +#!/usr/bin/env python +## StarCal Builtin Plugin +db_name = 'iran-hijri-data.txt' +mode = 'hijri' +desc = \ +'مناسبت‌های اسلامی (تقویم رسمی ایران)' + + +about = \ +'''مناسبت‌های اسلامی (تقویم رسمی ایران) +آخرین تغییرات: ۲۰۱۳/۰۲/۱۵''' +authors = [ +'شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران و شورای فرهنگ عمومی', +'سعید رسولی ', +'مولا پهنادایان ' +] +has_config = False +has_image = False + +default_enable = True +default_show_date = False + diff --git a/plugins/iran-jalali-2-data.txt b/plugins/iran-jalali-2-data.txt new file mode 100644 index 000000000..50e5b2a72 --- /dev/null +++ b/plugins/iran-jalali-2-data.txt @@ -0,0 +1,63 @@ +#!/usr/bin/python +## شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران‬، و شورای فرهنگ عمومی +## سعید رسولی +## مولا پهنادایان + +01/10 همه‌پرسی تغییر نظام شاهنشاهی به جمهوری اسلامی ایران(۱۳۵۸ ه‍.ش) +01/15 روز ذخایر ژنتیکی و زیستی +01/19 شهادت آیت‌الله سید محمد باقر صدر و خواهر ایشان بنت‌الهدی توسط رژیم بعث عراق(۱۳۵۹ ه‍.ش) +01/20 قطع مناسبات سیاسی ایران و آمریکا(۱۳۵۹ ه‍.ش) +02/02 روز زمین پاک +02/10 آغاز عملیات بیت المقدس(۱۳۶۱ ه‍.ش) +02/19 روز اسناد ملی و میراث مکتوب +02/27 روز ارتباطات و روابط عمومی +03/05 روز نسیم مهر(روز حمایت از خانوادهٔ زندانیان) +03/07 افتتاح اولین دورهٔ مجلس شورای اسلامی(۱۳۵۹ ه‍.ش) +03/15 زندانی شدن حضرت امام خمینی(ره) به دست مأموران ستم‌شاهی پهلوی(۱۳۴۲ ه‍.ش) +03/20 شهادت آیت‌الله سعیدی به دست مأموران ستم‌شاهی پهلوی(۱۳۴۹ ه‍.ش) +03/25 روز گل و گیاه +03/30 ‫شهادت زائران حرم رضوی(ع) به دست ایادی آمریکا(عاشورای ۱۳۷۳ ه‍.ش)‬ +04/10 روز آزادسازی شهر مهران +04/12 روز بزرگداشت علامه امینی(۱۳۴۹ ه‍.ش) +04/16 روز مالیات +04/21 کشف توطعهٔ کودتای آمریکایی در پایگاه هوایی شهید نوژه(۱۳۵۹ ه‍.ش) – حمله به مسجد گوهرشاد و کشتار مردم توسط رضاخان(۱۳۱۴ ه‍.ش) +04/23 گشایش نخستین مجلس خبرگان رهبری(۱۳۶۲ ه‍.ش) +04/27 اعلام پذیرش قطعنامهٔ ۵۹۸ شورای امنیت از سوی ایران(۱۳۶۷ ه‍.ش) +05/06 روز کارآفرینی و آموزش‌های فنی و حرفه‌ای +05/08 روز بزرگداشت شیخ شهاب‌الدین سهروردی(شیخ اشراق) +05/11 شهادت آیت‌الله شیخ فضل‌الله نوری(۱۲۸۸ ه‍.ش) +05/28 گشایش مجلس خبرگان برای بررسی نهایی قانون اساسی جمهوری اسلامی ایران(۱۳۵۸ ه‍.ش) +05/31 روز صنعت دفاعی +06/03 اشغال ایران توسط متفقین و فرار رضاخان(۱۳۲۰ ه‍.ش) +06/10 روز بانکداری اسلامی(سالروز تصویب قانون عملیات بانکی بدون ربا، ۱۳۶۲ ه‍.ش) +06/11 روز صنعت چاپ +06/12 روز بهورز +06/21 روز سینما +06/30 روز گفت‌وگوی تمدن‌ها +07/20 روز اسکان معلولان و سالمندان +07/24 روز پیوند اولیا و مربیان – ‫سالروز واقعه به آتش كشيدن مسجد جامع شهر كرمان توسط دژخيمان رژيم پهلوی(۱۳۵۷ ه‍.ش) +07/29 روز صادرات +08/01 شهادت مظلومانهٔ آیت‌الله حاج سید مصطفی خمینی(۱۳۵۶ ه‍.ش) +08/01 روز آمار و برنامه‌ریزی +08/13 تبعید امام خمینی(ره) از ایران به ترکیه(۱۳۴۳ ه‍.ش) +08/18 روز کیفیت +08/26 سالروز آزادسازی سوسنگرد +09/11 شهادت میرزا کوچک خان جنگلی(۱۳۰۰ ه‍.ش) +09/13 روز بیمه +09/18 معرفی عراق به عنوان مسئول و آغازگر جنگ از سوی سازمان ملل(۱۳۷۰ ه‍.ش) +09/25 روز تجلیل از شهید تندگویان +09/26 روز حمل و نقل و رانندگان +10/03 روز ثبت احوال +10/05 روز ملی ایمنی در برابر زلزله +10/07 شهادت آیت‌الله حسین غفاری به دست مأموران ستم‌شاهی پهلوی(۱۳۵۳ ه‍.ش) +10/13 ابلاغ پیام تاریخی امام خمینی به گورباچف رهبر شوروی سابق(۱۳۶۷ ه‍.ش) +10/17 اجرای طرح استعماری حذف حجاب توسط رضاخان(۱۳۱۴ ه‍.ش) +11/05 انتخابات اولین دورهٔ ریاست جمهوری(۱۳۵۸ ه‍.ش) +11/21 شکسته شدن حکومت نظامی به فرمان امام خمینی(۱۳۵۷ ه‍.ش) +11/25 صدور حکم تاریخی امام خمینی مبنی بر ارتداد سلمان رشدی نویسندهٔ خائن کتاب آیات شیطانی(۱۳۶۷ ه‍.ش) +12/03 کودتای انگلیسی رضاخان(۱۲۹۹ ه‍.ش) +12/08 روز امور تربیتی و تربیت اسلامی +12/09 روز ملی حمایت از حقوق مصرف‌کنندگان +12/24 برگزاری انتخابات اولین دورهٔ مجلس شورای اسلامی(۱۳۵۸ ه‍.ش) +12/25 بمباران شیمیایی حلبچه توسط ارتش بعث عراق(۱۳۶۶ ه‍.ش) + diff --git a/plugins/iran-jalali-2.json b/plugins/iran-jalali-2.json new file mode 100644 index 000000000..52797772b --- /dev/null +++ b/plugins/iran-jalali-2.json @@ -0,0 +1,17 @@ +{ + "type": "yearlyText", + "dataFile": "iran-jalali-2-data.txt", + "calType": "jalali", + "title": "مناسبت‌های هجری شمسی (سایر)", + "about": "مناسبت‌های هجری شمسی (سایر)\nمناسبت‌های هجری شمسی که توسط مؤلف برنامه اضافه شده‌اند و در تقویم رسمی ایران نیست. و یا به تازگی از تقویم رسمی ایران حذف شده‌اند.\nآخرین تغییرات: ۲۰۱۲/۰۶/۰۶", + "authors": [ + "شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران و شورای فرهنگ عمومی", + "سعید رسولی ", + "مولا پهنادایان " + ], + "hasConfig": false, + "hasImage": false, + "lastDayMerge": true, + "default_enable": false, + "default_show_date": false +} diff --git a/plugins/iran-jalali-2.spg b/plugins/iran-jalali-2.spg new file mode 100644 index 000000000..da1e1026b --- /dev/null +++ b/plugins/iran-jalali-2.spg @@ -0,0 +1,21 @@ +#!/usr/bin/env python +## StarCal Builtin Plugin +db_name = 'iran-jalali-2-data.txt' +mode = 'jalali' +desc = \ +'مناسبت‌های هجری شمسی (سایر)' +about = \ +'''مناسبت‌های هجری شمسی (سایر) +مناسبت‌های هجری شمسی که توسط مؤلف برنامه اضافه شده‌اند و در تقویم رسمی ایران نیست. و یا به تازگی از تقویم رسمی ایران حذف شده‌اند. +آخرین تغییرات: ۲۰۱۲/۰۶/۰۶''' +authors = [ +'شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران و شورای فرهنگ عمومی', +'سعید رسولی ', +'مولا پهنادایان ' +] +has_config = False +has_image = False + +default_enable = False +default_show_date = False + diff --git a/plugins/iran-jalali-data.txt b/plugins/iran-jalali-data.txt new file mode 100644 index 000000000..1c39f1fdd --- /dev/null +++ b/plugins/iran-jalali-data.txt @@ -0,0 +1,137 @@ +#!/usr/bin/python +## مطابق با استخراج و تنظیم شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران‬(مناسبت‌ها از شورای فرهنگ عمومی) +## گردآوری، فرمت‌بندی و به روزآوری: سعید رسولی ، مولا پهنادایان + +01/01 آغاز عید نوروز +01/02 عید نوروز – هجوم مأموران ستم‌شاهی پهلوی به مدرسهٔ فیضیهٔ قم(۱۳۴۲ ه‍.ش) – آغاز عملیات فتح‌المبین(۱۳۶۱ ه‍.ش) +01/03 عید نوروز +01/04 عید نوروز +01/12 روز جمهوری اسلامی ایران +01/13 روز طبیعت +01/18 روز سلامتی(روز جهانی بهداشت) +01/20 روز ملی فناوری هسته‌ای – روز هنر انقلاب اسلامی (سالروز شهادت سید مرتضی آوینی – ۱۳۷۲ ه‍.ش) +01/21 شهادت امیر سپهبد علی صیاد شیرازی(۱۳۷۸ ه‍.ش) – سالروز افتتاح حساب شمارهٔ ۱۰۰ به فرمان حضرت امام(ره) و تأسیس بنیاد مسکن انقلاب اسلامی(۱۳۵۸ ه‍.ش) +01/25 روز بزرگداشت عطار نیشابوری +01/29 روز ارتش جمهوری اسلامی و نیروی زمینی +02/01 روز بزرگداشت سعدی +02/02 تأسیس سپاه پاسداران انقلاب اسلامی(۱۳۵۸ ه‍.ش) – سالروز اعلام انقلاب فرهنگی(۱۳۵۹ ه‍.ش) +02/03 روز بزرگداشت شیخ بهایی +02/05 شکست حملهٔ نظامی آمریکا به ایران در طبس(۱۳۵۹ ه‍.ش) +02/09 روز شوراها +02/10 روز ملی خلیج فارس +02/12 شهادت استاد مرتضی مطهری(۱۳۵۸ ه‍.ش) – روز معلم +02/15 روز بزرگداشت شیخ صدوق +02/18 روز بیماری‌های خاص و صعب‌العلاج +02/19 روز بزرگداشت شیخ کلینی +02/24 لغو امتیاز تنباکو به فتوای آیت‌الله میرزا حسن شیرازی(۱۲۷۰ ه‍.ش) +02/25 روز بزرگداشت فردوسی +02/28 روز بزرگداشت حکیم عمر خیام +03/01 روز بهره‌وری و بهینه‌سازی مصرف – روز بزرگداشت ملاصدرا(صدرالمتألهین) +03/03 فتح خرمشهر در عملیات بیت المقدس(۱۳۶۱ ه‍.ش) و روز مقاومت، ایثار و پیروزی +03/04 روز مقاومت و پایداری – روز دزفول +03/14 رحلت امام خمینی رهبر کبیر انقلاب اسلامی(۱۳۶۸ ه‍.ش) – انتخاب آیت‌الله خامنه‌ای به رهبری(۱۳۶۸ ه‍.ش) +03/15 قیام خونین ۱۵ خرداد(۱۳۴۲ ه‍.ش) +03/20 روز صنایع دستی +03/26 شهادت سربازان دلیر اسلام: بخارایی، امانی، صفار هندی و نیک‌نژاد(۱۳۴۴ ه‍.ش) +03/27 روز جهاد کشاورزی(تشکیل جهاد سازندگی به فرمان امام خمینی، ۱۳۵۸ ه‍.ش) +03/29 درگذشت دکتر علی شریعتی(۱۳۵۶ ه‍.ش) +03/31 شهادت دکتر مصطفی چمران(۱۳۶۰ ه‍.ش) – روز بسیج اساتید +04/01 ‫روز تبلیغ و اطلاع‌رسانی دینی(سالروز صدور فرمان حضرت امام خمینی(ره) مبنی بر‬ تأسیس سازمان تبلیغات اسلامی ۱۳۶۰ ه‍.ش) – روز اصناف‬ +04/07 شهادت مظلومانهٔ آیت‌الله دکتر بهشتی و ۷۲ تن از یاران امام خمینی(۱۳۶۰ ه‍.ش) – روز قوهٔ قضاییه +04/08 روز مبارزه با سلاح‌های شیمیایی و میکروبی +04/10 روز صنعت و معدن +04/11 شهادت آیت‌الله صدوقی چهارمین شهید محراب به دست منافقان(۱۳۶۱ ه‍.ش) +04/12 ‫حملهٔ ددمنشانهٔ ناوگان آمریکای جنایتکار به هواپیمای مسافربری جمهوری اسلامی ایران‫(۱۳۶۷ ه‍.ش) +04/14 روز قلم +04/18 ‫روز ادبیات کودکان و نوجوانان‬ +04/21 روز عفاف و حجاب +04/25 روز بهزیستی و تامین اجتماعی +04/26 سالروز تأسیس نهاد شورای نگهبان +05/05 سالروز عملیات افتخار آفرین مرصاد(۱۳۶۷ ه‍.ش) +05/09 روز اهدای خون +05/14 صدور فرمان مشروطیت(۱۲۸۵ ه‍.ش) – روز حقوق بشر اسلامی و کرامت انسانی +05/16 تشکیل جهاد دانشگاهی(۱۳۵۹ ه‍.ش) +05/17 روز خبرنگار +05/21 روز حمایت از صنایع کوچک +05/23 روز مقاومت اسلامی +05/26 آغاز بازگشت آزادگان به میهن اسلامی(۱۳۶۹ ه‍.ش) +05/28 کودتای آمریکا برای بازگرداندن شاه(۱۳۳۲ ه‍.ش) +05/30 روز بزرگداشت علامه مجلسی +06/01 روز بزرگداشت ابوعلی سینا – روز پزشک +06/02 آغاز هفتهٔ دولت – ‫شهادت سید علی اندرزگو(۲ شهریور ۱۳۵۷ ه‍.ش و ۱۹ رمضان ۱۳۹۸ ه‍‍.ق) +06/04 روز کارمند +06/05 روز بزرگداشت محمد بن زکریای رازی – روز داروسازی +06/08 روز مبارزه با تروریسم(انفجار دفتر نخست وزیری به دست منافقان و شهادت مظلومانهٔ شهیدان رجایی و باهنر – ۱۳۶۰ ه‍.ش) +06/12 روز مبارزه با استعمار انگلیس(سالروز شهادت رئیسعلی دلواری) +06/13 روز تعاون – روز بزرگداشت ابوریحان بیرونی +06/14 شهادت آیت‌الله قدوسی و سرتیپ وحید دستجردی(۱۳۶۰ ه‍.ش) +06/17 قیام ۱۷ شهریور و کشتار جمعی از مردم به دست مأموران ستم‌شاهی پهلوی(۱۳۵۷ ه‍.ش) +06/19 وفات آیت‌الله سید محمود طالقانی اولین امام جمعهٔ تهران(۱۳۵۸ ه‍.ش) +06/20 شهادت دومین شهید محراب آیت‌الله مدنی به دست منافقان(۱۳۶۰ ه‍.ش) +06/21 روز سینما +06/27 روز شعر و ادب فارسی – روز بزرگداشت استاد سید محمد حسین شهریار +06/31 آغاز جنگ تحمیلی(۱۳۵۹ ه‍.ش) – آغاز هفتهٔ دفاع مقدس +07/05 شکست حصر آبادان در عملیات ثامن‌الأئمه(ع)(۱۳۶۰ ه‍.ش) +07/07 روز آتش نشانی و ایمنی-شهادت سرداران اسلام: فلاحی، فکوری، نامحو، کلاهدوز و جهان‌آرا(۱۳۶۰ ه‍.ش) – روز بزرگداشت شمس – روز بزرگداشت فرماندهان شهید دفاع مقدس +07/08 روز بزرگداشت مولوی +07/09 ‫روز همبستگی و همدردی با کودکان و نوجوانان فلسطینی +07/13 هجرت امام خمینی از عراق به پاریس(۱۳۵۷ ه‍.ش) – روز نیروی انتظامی +07/14 روز دامپزشکی +07/15 روز روستا +07/20 روز بزرگداشت حافظ +07/23 شهادت پنجمین شهید محراب آیت‌الله اشرفی اصفهانی به دست منافقان(۱۳۶۱ ه‍.ش) +07/24 روز ملی پارالمپیک +07/26 روز تربیت بدنی و ورزش +07/29 روز صادرات +08/04 اعتراض و افشاگری امام خمینی علیه پذیرش کاپیتولاسیون(۱۳۴۳ ه‍.ش) +08/08 شهادت محمد حسین فهمیده(بسیجی ۱۳ ساله) – روز نوجوان و بسیج دانش‌آموزی +08/10 شهادت آیت‌الله قاضی طباطبایی اولین شهید محراب به دست منافقان(۱۳۵۸ ه‍.ش) +08/13 تسخیر لانهٔ جاسوسی آمریکا به دست دانشجویان پیرو خط امام(۱۳۵۸ ه‍.ش) – روز ملی مبارزه با استکبار جهانی – روز دانش‌آموز +08/14 روز فرهنگ عمومی +##08/22 شهادت سید علی اندرزگو به دست مأموران ستم‌شاهی پهلوی(۱۳۵۷ ه‍.ش) ### تغییر به ۲ شهریور(06/02) +08/24 روز کتاب، کتابخوانی و کتابدار – روز بزرگداشت آیت‌الله علامه سید محمد حسین طباطبایی(۱۳۶۰ ه‍.ش) +09/05 روز بسیج مستضعفان(تشکیل بسیج مستضعفان به فرمان امام خمینی(ره) – ۱۳۵۸ ه‍.ش) +09/07 روز نیروی دریایی +09/09 روز بزرگداشت شیخ مفید +09/10 شهادت آیت‌الله سید حسن مدرس(۱۳۱۶ ه‍.ش) و روز مجلس +09/12 روز قانون اساسی جمهوری اسلامی ایران(تصویب قانون اساسی جمهوری اسلامی ایران، ۱۳۵۸ ه‍.ش) +09/16 روز دانشجو +09/19 تشکیل شورای عالی انقلاب فرهنگی به فرمان امام خمینی(۱۳۶۳ ه‍.ش) +09/20 شهادت آیت‌الله دستغیب سومین شهید محراب به دست منافقان(۱۳۶۰ ه‍.ش) +09/25 روز پژوهش +09/27 شهادت آیت‌الله دکتر محمد مفتح(۱۳۵۸ ه‍.ش) – روز وحدت حوزه و دانشگاه +09/30 شب یلدا +10/05 روز ایمنی در برابر زلزله و کاهش اثرات بلایای طبیعی +10/09 روز بصیرت و میثاق امت با ولایت +10/07 سالروز تشکیل نهضت سوادآموزی به فرمان امام خمینی(ره)(۱۳۵۸ ه‍.ش) +10/19 قیام خونین مردم قم(۱۳۵۶ ه‍.ش) +10/20 شهادت میرزاتقی خان امیرکبیر(۱۲۳۰ ه‍.ش) +10/22 تشکیل شورای انقلاب به فرمان امام خمینی(۱۳۵۷ ه‍.ش) +10/26 فرار شاه معدوم(۱۳۵۷ ه‍.ش) +10/27 شهادت نواب صفوی، طهماسبی، برادران واحدی و ذوالقدر از فداییان اسلام(۱۳۳۴ ه‍.ش) +# روز غزه: ۲۹ دی، یا ۲۵ صفر؟؟ +10/29 روز غزه +11/06 سالروز حماسهٔ مردم آمل +11/12 بازگشت امام خمینی به ایران(۱۳۵۷ ه‍.ش) و آغاز دههٔ مبارک فجر انقلاب اسلامی +11/14 روز فناوری فضایی +11/19 روز نیروی هوایی +11/22 پیروزی انقلاب اسلامی ایران و سقوط نظام شاهنشاهی(۱۳۵۷ ه‍.ش) +11/29 قیام مردم تبریز به مناسبت چهلمین روز شهادت شهدای قم(۱۳۵۶ ه‍.ش) +12/05 روزبزرگداشت خواجه نصیرالدین طوسی – روز مهندسی +12/14 روز احسان و نیکوکاری +12/15 روز درختکاری +12/18 روز بزرگداشت سید جمال‌الدین اسدآبادی +12/20 روز راهیان نور +12/22 روز بزرگداشت شهدا(سال‌روز صدور فرمان امام خمینی مبنی بر تأسیس بنیاد شهید انقلاب اسلامی، ۱۳۵۸ ه‍.ش) +12/25 روز بزرگداشت پروین اعتصامی +12/29 روز ملی شدن صنعت نفت ایران(۱۳۲۹ ه‍.ش) +1387/01/01 ‫لحظهٔ تحویل سال ۱۳۸۷ هجری شمسی به ساعت رسمی جمهوری اسلامی ایران‬: ‫ساعت ۹ و ۱۸ دقیقه و ۱۹ ثانیه روز پنجشنبه ۱ فروردین ۱۳۸۷، مطابق ۱۲ ربیع‌الاول ۱۴۲۹ و ۲۰ مارس ۲۰۰۸ +1387/12/30 ‫لحظهٔ تحویل سال ۱۳۸۸ هجری شمسی به ساعت رسمی جمهوری اسلامی ایران‬: ‫ساعت ۱۵ و ۱۳ دقیقه و ۳۹ ثانیه روز جمعه ۳۰ اسفند ۱۳۸۷‬، ‫مطابق ۲۲ ربیع‌الاول ۱۴۳۰ و ۲۰ مارس ۲۰۰۹‬ +1388/12/29 ‫لحظهٔ تحویل سال ۱۳۸۹ هجری شمسی به ساعت رسمی جمهوری اسلامی ایران‬: ‫ساعت ۲۱ و ۲ دقیقه و ۱۳ ثانیه روز شنبه ۲۹ اسفند ۱۳۸۸، ‫مطابق ۴ ربیع‌الثانی ۱۳۴۱ و ۲۰ مارس ۲۰۱۰ +1390/01/01 ‫لحظهٔ تحویل سال ۱۳۹۰ هجری شمسی به ساعت رسمی جمهوری اسلامی ایران‬: ساعت ۲ و ۵۰ دقیقه و ۲۵ ثانیه روز دوشنبه ۱ فروردین ۱۳۹۰، ‫مطابق ۱۶ ربیعالثانی ۱۴۳۲ و ۲۱ مارس ۲۰۱۱ +1391/01/01 ‫لحظهٔ تحویل سال ۱۳۹۱ هجری شمسی به ساعت رسمی جمهوری اسلامی ایران‬: ساعت ۸ و ۴۴ دقیقه و ۲۷ ثانیه روز سه‌شنبه ۱ فروردین ۱۳۹۱، ‫مطابق ۲۷ ربیع‌الثانی ‍۱۴۳۳ و ۲۰ مارس ۲۰۱۲ +1391/12/30 ‫لحظه تحویل سال ۱۳۹۲ هجری شمسی به ساعت رسمی جمهوری اسلامی ایران‬: ساعت ۱۴ و ۳۱ دقیقه و ۵۶ ثانیه روز چهارشنبه ۳۰ اسفند ۱۳۹۱، ‫مطابق ۸ جمادی‌الاولی ۱۴۳۴ و ۲۰ مارس ۲۰۱۳ +1392/12/29 ‫لحظه تحویل سال ۱۳۹۳ هجری شمسی به ساعت رسمی جمهوری اسلامی ایران‬: ساعت ۲۰ و ۲۷ دقیقه و ۷ ثانیه روز پنج‌شنبه ۲۹ اسفند ۱۳۹۲، ‫مطابق ۱۸ جمادی‌الاولی ۱۴۳۵ و ۲۰ مارس ۲۰۱۴ + + + diff --git a/plugins/iran-jalali.json b/plugins/iran-jalali.json new file mode 100644 index 000000000..dd2d85377 --- /dev/null +++ b/plugins/iran-jalali.json @@ -0,0 +1,17 @@ +{ + "type": "yearlyText", + "dataFile": "iran-jalali-data.txt", + "calType": "jalali", + "title": "مناسبت‌های هجری شمسی (تقویم رسمی ایران)", + "about": "مناسبت‌های هجری شمسی (تقویم رسمی ایران)\nآخرین تغییرات: ۲۰۱۳/۰۲/۱۵", + "authors": [ + "شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران و شورای فرهنگ عمومی", + "سعید رسولی ", + "مولا پهنادایان " + ], + "hasConfig": false, + "hasImage": false, + "lastDayMerge": true, + "default_enable": true, + "default_show_date": false +} diff --git a/plugins/iran-jalali.spg b/plugins/iran-jalali.spg new file mode 100644 index 000000000..da2ed3288 --- /dev/null +++ b/plugins/iran-jalali.spg @@ -0,0 +1,20 @@ +#!/usr/bin/env python +## StarCal Builtin Plugin +db_name = 'iran-jalali-data.txt' +mode = 'jalali' +desc = \ +'مناسبت‌های هجری شمسی (تقویم رسمی ایران)' +about = \ +'''مناسبت‌های هجری شمسی (تقویم رسمی ایران) +آخرین تغییرات: ۲۰۱۳/۰۲/۱۵''' +authors = [ +'شورای مرکز تقویم مؤسسهٔ ژئوفیزیک دانشگاه تهران و شورای فرهنگ عمومی', +'سعید رسولی ', +'مولا پهنادایان ' +] +has_config = False +has_image = False + +default_enable = True +default_show_date = False + diff --git a/plugins/pray_times.json b/plugins/pray_times.json new file mode 100644 index 000000000..9819fa85d --- /dev/null +++ b/plugins/pray_times.json @@ -0,0 +1,16 @@ +{ + "type": "external", + "mainFile": "pray_times_files/pray_times.py", + "calType": "gregorian", + "title": "Islamic Pray Times", + "about": "Islamic Pray Times", + "authors": [ + "Hamid Zarrabi-Zadeh ", + "Saeed Rasooli " + ], + "hasConfig": true, + "hasImage": false, + "lastDayMerge": true, + "default_enable": false, + "default_show_date": false +} diff --git a/plugins/pray_times_files/__init__.py b/plugins/pray_times_files/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/pray_times_files/locations.txt b/plugins/pray_times_files/locations.txt new file mode 100644 index 000000000..f371859f6 --- /dev/null +++ b/plugins/pray_times_files/locations.txt @@ -0,0 +1,4189 @@ +Africa + Algeria Africa/Algiers + Adrar 27.900000 -0.283333 + Algiers 36.763056 3.050556 + Annaba 36.900000 7.766667 + Batna 35.555278 6.178611 + Bechar 31.616667 -2.216667 + Bejaia 36.750000 5.083333 + Berriane 32.833333 3.766667 + Biskra 34.850000 5.733333 + Bou Saada 35.214167 4.182500 + Chlef 36.164722 1.334722 + Constantine 36.365000 6.614722 + Dar el Beida 36.713333 3.212500 + Djanet 24.552500 9.482222 + El Golea 30.566667 2.883333 + Ghardaia 32.483333 3.666667 + Hassi Messaoud 31.701944 6.054444 + I-n-Amenas 28.050000 9.550000 + I-n-Salah 27.216667 2.466667 + Illizi 26.483333 8.466667 + Jijel 36.800000 5.766667 + Laghouat 33.800000 2.883333 + Mascara 35.394444 0.139722 + Oran 35.691111 -0.641667 + Ouargla 31.957500 5.327778 + Setif 36.191389 5.409444 + Sidi Amrane 33.500000 6.016667 + Sidi Bel Abbes 35.193889 -0.641389 + Tamanrasset 22.785000 5.522778 + Tebessa 35.404167 8.124167 + Tiaret 35.375833 1.313056 + Timimoun 29.250000 0.250000 + Tindouf 27.651944 -8.214444 + Tlemcen 34.878333 -1.315000 + Touggourt 33.100000 6.066667 + Angola Africa/Luanda + Benin Africa/Porto-Novo + Cotonou 6.350000 2.433333 + Porto-Novo 6.483333 2.616667 + Botswana Africa/Gaborone + Francistown -21.166667 27.516667 + Gaborone -24.646389 25.911944 + Ghanzi -21.566667 21.783333 + Kasane -17.816667 25.150000 + Letlhakane -21.416667 25.583333 + Lokerane -24.883333 24.666667 + Maun -19.983333 23.416667 + Mochudi -24.416667 26.150000 + Selebi-Phikwe -21.966111 27.917222 + Tshabong -26.050000 22.450000 + Burkina Faso Africa/Ouagadougou + Ouagadougou 12.370278 -1.524722 + Burundi Africa/Bujumbura + Bujumbura -3.376111 29.360000 + Cameroon Africa/Douala + Douala 4.050278 9.700000 + Garoua 9.300000 13.400000 + Ngaoundere 7.316667 13.583333 + Yaounde 3.866667 11.516667 + Cape Verde Atlantic/Cape_Verde + Preguiça 16.750000 -22.950000 + Central African Republic Africa/Bangui + Bangassou 4.733333 22.816667 + Bangui 4.366667 18.583333 + Berberati 4.266667 15.783333 + Chad Africa/Ndjamena + Moundou 8.566667 16.083333 + N'Djamena 12.108502 15.048173 + Sarh 9.150000 18.383333 + Comoros Indian/Comoro + Mbaléni -11.550000 43.274167 + Moroni -11.704167 43.240278 + Congo, Democratic Republic of the Africa/Kinshasa + Congo, Republic of the Africa/Brazzaville + Brazzaville -4.259167 15.284722 + Pointe-Noire -4.794722 11.846111 + Cote d'Ivoire Africa/Abidjan + Abidjan 5.341111 -4.028056 + Djibouti / Ambouli 11.550000 43.166667 + Egypt Africa/Cairo + Al 'Arish 31.126111 33.801944 + Al Ghardaqah 27.238889 33.836111 + Al Qabuti 31.237778 32.281389 + Nouzha Airport 31.200000 29.950000 + Aswan 24.087500 32.898889 + Asyut 27.182778 31.182778 + Cairo Airport 30.133333 31.400000 + Luxor 25.683333 32.650000 + Marsa Matruh 31.350000 27.233333 + Sharm ash Shaykh 27.863611 34.288056 + Taba 29.491944 34.891389 + Equatorial Guinea Africa/Malabo + Malabo 3.750000 8.783333 + Eritrea Africa/Asmara + Ethiopia Africa/Addis_Ababa + French Southern & Antarctic Lands Indian/Kerguelen + Gabon Africa/Libreville + Franceville -1.633333 13.583333 + Libreville 0.383333 9.450000 + Port-Gentil -0.716667 8.783333 + Gambia Africa/Banjul + Banjul 13.453056 -16.577500 + Ghana Africa/Accra + Accra 5.550000 -0.216667 + Guatemala America/Guatemala + Guatemala 14.63 -90.5 + Guinea Africa/Conakry + Conakry 9.509167 -13.712222 + Guinea-Bissau Africa/Bissau + + Kenya Africa/Nairobi + Eldoret 0.516667 35.283333 + Kisumu -0.100000 34.750000 + Mombasa -4.050000 39.666667 + Nairobi -1.283333 36.816667 + Lesotho Africa/Maseru + Libya Africa/Tripoli + Baninah 32.083333 20.266667 + Sabha 27.033333 14.433333 + Tripoli 32.892500 13.180000 + Madagascar Indian/Antananarivo + Ankarena -17.083333 49.816667 + Antananarivo -18.916667 47.516667 + Antsiranana -12.266667 49.283333 + Fasenina-Ampasy -13.300000 48.316667 + Mahajanga -15.716667 46.316667 + Toamasina -18.166667 49.383333 + Tolanaro -25.033333 47.000000 + Malawi Africa/Blantyre + Mali Africa/Bamako + Mauritania Africa/Nouakchott + Nouadhibou 20.902500 -17.042222 + Nouakchott 18.119444 -16.040556 + Mauritius Indian/Mauritius + Plaisance -20.418333 57.671389 + Port Louis -20.161944 57.498889 + Port Mathurin -19.683333 63.416667 + Mayotte Indian/Mayotte + Dzaoudzi -12.779444 45.253611 + Mamoudzou -12.779444 45.227222 + Morocco Africa/Casablanca + Agadir 30.400000 -9.600000 + Al Hoceima 35.240000 -3.930000 + Fes 34.050000 -4.980000 + Marrakech 31.630000 -8.000000 + Meknes 33.900000 -5.550000 + Nador 35.170000 -2.930000 + Nouaseur 33.380000 -7.610000 + Ouarzazat 30.920000 -6.910000 + Oujda 34.680000 -1.910000 + Rabat 34.020000 -6.830000 + Tangier 35.780000 -5.810000 + Tetouan 35.570000 -5.370000 + Mozambique Africa/Maputo + Beira -19.843611 34.838889 + Chimoio -19.116389 33.483333 + Lichinga -13.312778 35.240556 + Maputo -25.965278 32.589167 + Nampula -15.119722 39.264722 + Pemba -12.960833 40.507778 + Quelimane -17.878611 36.888333 + Namibia Africa/Windhoek + Niger Africa/Niamey + Agadez 16.973333 7.991111 + Niamey 13.516667 2.116667 + Zinder 13.804869 8.988371 + Nigeria Africa/Lagos + Ikeja 6.596667 3.343056 + Ilorin 8.500000 4.550000 + Kaduna 10.523056 7.440278 + Kano 11.996389 8.516667 + Port Harcourt 4.789167 6.998611 + Rwanda Africa/Kigali + Reunion Indian/Reunion + Saint-Denis -20.866667 55.466667 + Saint-Pierre -21.333333 55.483333 + San Marino Europe/San_Marino + Sao Tome & Principe Africa/Sao_Tome + Senegal Africa/Dakar + Boukot Ouolof 12.416389 -16.750556 + Dakar 14.736667 -17.633889 + Saint-Louis 16.032222 -16.616667 + Tambacounda 13.753889 -13.758611 + Ziguinchor 12.583333 -16.271944 + Seychelles Indian/Mahe + Cascade -4.666667 55.500000 + Seychelles International Airport -4.666667 55.516667 + Sierra Leone Africa/Freetown + Freetown 8.490000 -13.234167 + Lungi 8.640833 -13.221944 + Somalia Africa/Mogadishu + South Africa Africa/Johannesburg + Bloemfontein -29.133333 26.200000 + Cape Town -33.916667 18.416667 + Durban -29.850000 31.016667 + Johannesburg -26.200000 28.083333 + Klerksdorp -26.866667 26.666667 + Port Elizabeth -33.966667 25.583333 + Potchefstroom -26.716667 27.100000 + Pretoria -25.706944 28.229444 + Springs -26.250000 28.400000 + Upington -28.450000 21.250000 + Vereeniging -26.666667 27.933333 + Sudan Africa/Khartoum + Khartoum 15.588056 32.534167 + Swaziland Africa/Mbabane + Lobamba -26.466667 31.200000 + Manzini -26.483333 31.366667 + Mbabane -26.316667 31.133333 + Tanzania Africa/Dar_es_Salaam + Arusha -3.366667 36.683333 + Bukoba -1.331667 31.812222 + Dar es Salaam -6.800000 39.283333 + Dodoma -6.183333 35.750000 + Iringa -7.766667 35.700000 + Kigoma -4.876944 29.626667 + Mbeya -8.900000 33.450000 + Morogoro -6.816667 37.666667 + Moshi -3.350000 37.333333 + Mtwara -10.266667 40.183333 + Musoma -1.500000 33.800000 + Mwanza -2.516667 32.900000 + Songea -10.683333 35.650000 + Tabora -5.016667 32.800000 + Zanzibar -6.166667 39.183333 + Togo Africa/Lome + Lome 6.131944 1.222778 + Niamtougou 9.768056 1.105278 + Tunisia Africa/Tunis + Bizerte 37.274444 9.873889 + El Borma 31.666667 9.133333 + Gabes 33.883333 10.116667 + Gafsa 34.425000 8.784167 + Houmt Souk 33.874722 10.859167 + Jendouba 36.501111 8.779444 + Kairouan 35.674444 10.101667 + Monastir 35.783333 10.833333 + Qulaybiyah 36.850000 11.100000 + Remada 32.316667 10.400000 + Sfax 34.740556 10.760278 + Tabarka 36.954444 8.758056 + Tozeur 33.920556 8.133333 + Tunis 36.802778 10.179722 + Uganda Africa/Kampala + Arua 3.019167 30.930833 + Entebbe 0.064444 32.446944 + Kabale -1.326111 30.003889 + Kampala 0.315556 32.565556 + Western Sahara Africa/El_Aaiun + Zambia Africa/Lusaka + Chinganze -13.266667 31.916667 + Livingstone -17.850000 25.866667 + Lusaka -15.416667 28.283333 + Ndola -12.966667 28.633333 + Zimbabwe Africa/Harare + Palmer Station (Chile Time) Antarctica/McMurdo +Asia + Afghanistan Asia/Kabul + Herat 34.346944 62.198333 + Kabul 34.516667 69.183333 + Armenia Asia/Yerevan + Yerevan 40.181111 44.513611 + Azerbaijan Asia/Baku + Baku 40.395278 49.882222 + Ganca 40.682778 46.360556 + Bangladesh Asia/Dhaka + Chittagong 22.333056 91.836389 + Dhaka 23.723056 90.408611 + Solpur 23.850000 90.400000 + Bhutan Asia/Thimphu + Brunei Asia/Brunei + Bandar Seri Begawan 4.883333 114.933333 + Cambodia Asia/Phnom_Penh + Phnom Penh 11.550000 104.916667 + Siemreab 13.366667 103.850000 + China Asia/Shanghai + Anhui NoneTZ + Hefei 31.863889 117.280833 + Beijing 39.933333 116.283333 + Chongqing 29.516667 106.483333 + Fujian 26.061389 119.306111 + Xiamen 24.479794 118.081869 + Gansu 36.056389 103.792222 + Guangdong 23.116667 113.250000 + Shantou 23.368140 116.714790 + Shenzhen 22.533333 114.133333 + Guangxi 25.281944 110.286389 + Nanning 22.816667 108.316667 + Guizhou 18.243056 109.505000 + Heilongjiang 45.750000 126.650000 + Henan 34.757778 113.648611 + Hubei 30.583333 114.266667 + Hunan 28.200000 112.966667 + Inner Mongolia 40.810556 111.652222 + Jiangsu 32.061667 118.777778 + Jilin 43.880000 125.322778 + Liaoning 38.912222 121.602222 + Shenyang 41.792222 123.432778 + Shaanxi 34.258333 108.928611 + Shandong 36.098611 120.371944 + Hongqiao Airport 31.166667 121.433333 + Shanxi 37.869444 112.560278 + Sichuan 30.666667 104.066667 + Zhangguizhu Airport 39.183333 117.350000 + Xinjiang 39.454722 75.979722 + Urumqi 43.800000 87.583333 + Yunnan 25.038889 102.718333 + Zhejiang 30.255278 120.168889 + Hong Kong Asia/Hong_Kong + Kowloon 22.316667 114.183333 + India Asia/Kolkata + Ahmadabad 23.033333 72.616667 + Amritsar 31.633056 74.865556 + Benares 25.333333 83.000000 + Bombay 18.975000 72.825833 + Calcutta 22.569722 88.369722 + Hyderabad 17.375278 78.474444 + Jaipur 26.916667 75.816667 + Lucknow 26.850000 80.916667 + Madras 13.083333 80.283333 + Nagpur 21.150000 79.100000 + New Delhi 28.600000 77.200000 + Patna 25.600000 85.116667 + Thiruvananthapuram 8.506944 76.956944 + Tiruchchirappalli 10.805000 78.685556 + Japan Asia/Tokyo + Akita 39.716667 140.066667 + Ami 36.033333 140.200000 + Aomori 40.821111 140.751111 + Asahikawa 43.767778 142.370278 + Ashiya 33.883333 130.666667 + Chitose 42.819444 141.652222 + Chofu 35.655556 139.552222 + Fuji 35.166667 138.683333 + Fukue 32.688056 128.841944 + Fukuoka 33.583333 130.400000 + Futemma 26.287222 127.773056 + Gifu 35.416667 136.750000 + Hakodate 41.775833 140.736667 + Hamamatsu 34.700000 137.733333 + Hamanaka 38.816667 139.783333 + Hanamaki 39.383333 141.116667 + Hiroshima 34.400000 132.450000 + Hofu 34.050000 131.566667 + Ishigaki 24.333333 124.150000 + Iwakuni 34.150000 132.183333 + Izumo 35.366667 132.766667 + Janado 26.333333 126.800000 + Kadena 26.366667 127.750000 + Kagoshima 31.600000 130.550000 + Kanayama 33.683333 135.350000 + Kanoya 31.383333 130.850000 + Kashoji 34.400000 135.283333 + Kitakyushu 33.833333 130.833333 + Kochi 33.550000 133.550000 + Komatsu 36.400000 136.450000 + Komatsushima 34.000000 134.583333 + Kumamoto 32.800000 130.716667 + Kushiro 42.975000 144.374722 + Matsubara 24.783333 125.266667 + Matsumoto 36.233333 137.966667 + Matsushima 38.366667 141.071667 + Matsuyama 33.836389 132.753056 + Memambetsu 43.907222 144.173889 + Mihonoseki 35.566667 133.316667 + Minami 24.783333 141.316667 + Misawa 40.683611 141.359722 + Mito 36.366667 140.466667 + Miyazaki 31.900000 131.433333 + Mombetsu 44.352500 143.352500 + Nagasaki 32.755000 129.868333 + Nagoya 35.166667 136.916667 + Naha 26.207222 127.673333 + Naka-shibetsu 43.550000 144.983333 + Niigata 37.916667 139.050000 + Obihiro 42.917222 143.204444 + Odaira 30.550000 130.966667 + Odaka 37.200000 140.416667 + Odate 40.268611 140.568333 + Ofunakoshi 34.273333 129.354722 + Ogimachiya 35.833333 139.400000 + Oita 33.237222 131.604444 + Okata 34.783333 139.383333 + Okayama 34.650000 133.916667 + Okazato 33.100000 139.783333 + Osaka 34.666667 135.500000 + Ozuki 34.066667 131.033333 + Saga 33.250000 130.300000 + Sanrizuka 35.750000 140.383333 + Sawada 24.833333 125.166667 + Sendai 38.254722 140.884722 + Shiroi 35.800000 140.066667 + Takamatsu 34.333333 134.050000 + Takatsu 34.683333 131.783333 + Tateyama 34.983333 139.866667 + Tokyo 35.685000 139.751389 + Tottori 35.500000 134.233333 + Toyama 36.683333 137.216667 + Toyooka 35.533333 134.833333 + Tsuiki 33.666667 131.050000 + Ushuku 28.450000 129.716667 + Wakkanai 45.409444 141.673889 + Yamagata 38.252778 140.337500 + Yamaguchi 34.166667 131.483333 + Yao 34.616667 135.600000 + Yokota 35.750000 139.383333 + Yoshinaga 34.800000 138.300000 + Kazakhstan Asia/Almaty + Almaty 43.250000 76.950000 + Aqtau 43.650000 51.200000 + Aqtöbe 50.298056 57.181389 + Astana 51.181111 71.427778 + Oral 51.233333 51.366667 + Qaraghandy 49.798889 73.099444 + Qostanay 53.166667 63.583333 + Qyzylorda 44.852778 65.509167 + Shymkent 42.300000 69.600000 + Kyrgyzstan Asia/Bishkek + Bishkek 42.873056 74.600278 + Laos Asia/Vientiane + Vientiane 17.966667 102.600000 + Taipa 22.166667 113.566667 + Taipa 22.155833 113.556944 + Malaysia Asia/Kuala_Lumpur + Bintulu 3.166667 113.033333 + Bayan Lepas Airport 5.300000 100.266667 + Johor Bahru 1.466667 103.750000 + Klang 3.033333 101.450000 + Kota Baharu 6.133333 102.250000 + Kota Kinabalu 5.983333 116.066667 + Kuah 6.316667 99.850000 + Kuala Lumpur 3.166667 101.700000 + Kuantan 3.800000 103.333333 + Kuching 1.550000 110.333333 + Kudat 6.879722 116.849167 + Melaka 2.196944 102.248056 + Miri 4.383333 113.983333 + Sandakan 5.833333 118.116667 + Sepang 2.700000 101.750000 + Sibu 2.300000 111.816667 + Sitiawan 4.216667 100.700000 + Tawau 4.250000 117.900000 + Victoria 5.300000 115.250000 + Maldives Indian/Maldives + Male 4.166667 73.500000 + Monaco Europe/Monaco + Mongolia Asia/Ulaanbaatar + Ulaanbaatar 47.916667 106.916667 + Myanmar Asia/Rangoon + Rangoon 16.805278 96.156111 + Nepal Asia/Kathmandu + Kathmandu 27.716667 85.316667 + North Korea Asia/Pyongyang + Pakistan Asia/Karachi + Islamabad 33.700000 73.166667 + Karachi 24.866667 67.050000 + Lahore 31.549722 74.343611 + Nawabshah 26.250000 68.416667 + Philippines Asia/Manila + Angeles 15.135833 120.591111 + Davao 7.073056 125.612778 + Laoag 18.198889 120.593611 + Manila 14.604167 120.982222 + Masbate 12.366667 123.616667 + Pildira 14.516667 121.000000 + Subic 14.879722 120.233333 + Zamboanga City 6.910255 122.071715 + Paya Lebar Airport 1.366667 103.916667 + South Korea Asia/Seoul + Ch'ongju 36.637222 127.489722 + Cheju 33.509722 126.521944 + Inch'on 37.453611 126.731667 + Kunsan 35.978611 126.711389 + Osan 37.152222 127.070556 + P'yongt'aek 36.994722 127.088889 + Pusan 35.102778 129.040278 + Seoul 37.566389 126.999722 + Taegu 35.870278 128.591111 + Sri Lanka Asia/Colombo + Colombo 6.931944 79.847778 + Katunayaka 7.164722 79.873056 + Sri Jayewardenepura Kotte 6.902778 79.908333 + Taiwan Asia/Taipei + Kao-hsiung-shih 22.616256 120.313329 + Pu-ting 25.083498 121.215105 + T'ai-pei Shih 25.047763 121.531846 + Tajikistan Asia/Dushanbe + Dushanbe 38.560000 68.773889 + Thailand Asia/Bangkok + Bangkok 13.750000 100.516667 + Chiang Mai 18.790278 98.981667 + Chon Buri 13.366667 100.983333 + Hat Yai 7.016667 100.466667 + Hua Hin 12.566667 99.966667 + Khon Kaen 16.433333 102.833333 + Lampang 18.298333 99.507222 + Mae Hong Son 19.266667 97.933333 + Nan 18.783333 100.783333 + Phrae 18.150000 100.133333 + Phuket 7.883333 98.400000 + Ranong 9.966667 98.633333 + Rayong 12.666667 101.283333 + Surat Thani 9.133333 99.316667 + Trang 7.550000 99.600000 + Ubon Ratchathani 15.233056 104.863056 + Udon Thani 17.407500 102.793056 + Turkmenistan Asia/Ashgabat + Ashgabat 37.950000 58.383333 + Uzbekistan Asia/Tashkent + Nukus 42.453056 59.610278 + Samarqand 39.654167 66.959722 + Tashkent 41.316667 69.250000 + Termiz 37.224167 67.278333 + Urganch 41.550000 60.633333 + Vietnam Asia/Ho_Chi_Minh + Da Nang 16.067778 108.220833 + Hanoi 21.033333 105.850000 + Ho Chi Minh City 10.750000 106.666667 + Anguilla America/Anguilla + The Valley 18.216667 -63.050000 + Antigua & Barbuda America/Antigua + Fitches Creek 17.116667 -61.783333 + V. C. Bird International Airport, Antigua 17.116667 -61.783333 + Barbados America/Barbados + Bridgetown 13.100000 -59.616667 + Paragon 13.066667 -59.483333 + Bermuda Atlantic/Bermuda + Bermuda 32.366667 -64.683333 + Bermuda 32.366667 -64.683333 + Djibouti Africa/Djibouti + Dominica America/Dominica + Marigot 15.533333 -61.300000 + Canefield Airport 15.533333 -61.400000 + Saint Joseph 15.400000 -61.433333 + Greenland America/Godthab + Dundas 76.550000 -68.800000 + Godthåb 64.183333 -51.750000 + Ittorisseq 70.450000 -22.450000 + Jakobshavn 69.216667 -51.100000 + Kulusuk 65.566667 -37.183333 + Narsarsuaq 61.166667 -45.416667 + Søndre Strømfjord 67.000000 -50.700000 + Puerto Rico America/Puerto_Rico + Carolina 18.380782 -65.957387 + Ponce 18.011077 -66.614062 + Rafael Hernandez 18.471333 -67.079069 + San Juan 18.466334 -66.105722 + Saint Barthélemy America/St_Barthelemy + Saint Helena Atlantic/St_Helena + Wide Awake Field Ascension Island -7.966667 -14.400000 + Saint Kitts & Nevis America/St_Kitts + Basseterre 17.300000 -62.716667 + Golden Rock 17.316667 -62.716667 + Charlestown / Newcastle 17.200000 -62.583333 + United States Virgin Islands America/St_Thomas + Charlotte Amalie 18.341900 -64.930701 + Christiansted 17.746640 -64.703198 +Australasia & Oceania + American Samoa Pacific/Pago_Pago + Pago Pago -14.278056 -170.702500 + Australia Australia/Melbourne + Canberra -35.283333 149.216667 + New South Wales -32.250000 148.616667 + Forest Hill -35.150000 147.466667 + Richmond Air Force Base -33.600000 150.783333 + Sydney Airport -33.950000 151.183333 + Tamworth -31.100000 150.933333 + Northern Territory -23.700000 133.883333 + Darwin -12.466667 130.833333 + Katherine -14.466667 132.266667 + Queensland -27.500000 153.016667 + Cairns -16.916667 145.766667 + Coolangatta -28.166667 153.533333 + Mount Isa -20.733333 139.500000 + Rockhampton -23.383333 150.500000 + Townsville -19.250000 146.800000 + South Australia -34.933333 138.600000 + Woomera -31.150000 136.800000 + Tasmania -42.916667 147.333333 + Launceston -41.450000 147.166667 + Lara -38.016667 144.400000 + Melbourne Airport -37.666667 144.833333 + Western Australia -17.966667 122.233333 + Bullsbrook -31.666667 116.000000 + Kalgoorlie -30.750000 121.466667 + Kununurra -15.766667 128.733333 + Learmonth -22.250000 114.083333 + Perth -31.933333 115.833333 + Shellborough -20.016667 119.366667 + British Indian Ocean Territory Indian/Chagos + Christmas Island Indian/Christmas + Drumsite -10.433333 105.683333 + Flying Fish Cove -10.416667 105.716667 + Cocos (Keeling) Islands Indian/Cocos + Bantam Village -12.116667 96.883333 + Cook Islands Pacific/Rarotonga + Avarua -21.207778 -159.775000 + Fiji Pacific/Fiji + French Polynesia Pacific/Tahiti + Papeete -17.533333 -149.566667 + Guam Pacific/Guam + Asatdas 13.516944 144.863611 + Hagåtña 13.474167 144.747778 + Indonesia Asia/Jakarta + Jakarta -6.174444 106.829444 + Makassar -5.140000 119.422100 + Medan 3.583333 98.666667 + Palembang -2.916667 104.750000 + Pekanbaru 0.533333 101.450000 + Kiribati Pacific/Tarawa + Christmas, Cassidy Airport 1.983333 -157.483333 + Marshall Islands Pacific/Majuro + Majuro 7.100000 171.383333 + Micronesia Pacific/Truk;Pacific/Ponape;Pacific/Kosrae + Nauru Pacific/Nauru + New Caledonia Pacific/Noumea + Karenga -22.016667 166.233333 + Nouméa -22.266667 166.450000 + New Zealand Pacific/Auckland + Auckland -36.866667 174.766667 + Christchurch -43.533333 172.633333 + Wellington -41.300000 174.783333 + Niue Pacific/Niue + Alofi -19.016667 -169.916667 + Norfolk Island Pacific/Norfolk + Norfolk Island Airport -29.033333 167.933333 + Northern Mariana Islands Pacific/Saipan + Chalan Kanoa 15.145556 145.699167 + Palau Pacific/Palau + Koror 7.333333 134.483333 + Melekeok 7.493333 134.634167 + Papua New Guinea Pacific/Port_Moresby + Port Moresby -9.464722 147.192500 + Pitcairn Pacific/Pitcairn + Samoa Pacific/Apia + Apia -13.833333 -171.733333 + Singapore Asia/Singapore + Solomon Islands Pacific/Guadalcanal + Honiara -9.433333 159.950000 + St Kitts & Nevis America/St_Kitts + St Lucia America/St_Lucia + St Vincent America/St_Vincent + East Timor Asia/Dili Timor-Leste + Tokelau Pacific/Fakaofo + Tonga Pacific/Tongatapu + Fua'amotu -21.250000 -175.133333 + Nuku'alofa -21.133333 -175.200000 + Tuvalu Pacific/Funafuti + Funafuti -8.516667 179.216667 + United States Minor Outlying Islands NoneTZ + Johnston Atoll (Hawaii Time) NoneTZ + Midway Atoll (Samoa Time) NoneTZ + Wake Island NoneTZ + Wake Island, Wake Island Army Airfield Airport 19.283333 166.650000 + Vanuatu Pacific/Efate + Wallis & Futuna Pacific/Wallis + Mata'utu -13.283333 -176.133333 +Central & South America + Argentina America/Argentina/Buenos_Aires + Buenos Aires -34.587500 -58.672500 + Comodoro Rivadavia -45.866667 -67.500000 + Corrientes -27.466667 -58.833333 + Córdoba Airport -31.316667 -64.216667 + El Palomar -34.541667 -58.615278 + Ezeiza -34.838056 -58.517222 + Formosa -26.183333 -58.183333 + Mar del Plata -38.000000 -57.550000 + Mendoza -32.883333 -68.816667 + Neuquén -38.950000 -68.066667 + Posadas -27.383333 -55.883333 + Puerto Iguazú -25.566667 -54.566667 + Reconquista -29.150000 -59.650000 + Resistencia -27.450000 -58.983333 + Rosario -32.951111 -60.666389 + Río Gallegos -51.633333 -69.216667 + Río Grande -53.783333 -67.700000 + Salta -24.783333 -65.416667 + San Carlos de Bariloche -41.150000 -71.300000 + San Fernando -34.453056 -58.589722 + San Salvador de Jujuy -24.183333 -65.300000 + Ushuaia -54.800000 -68.300000 + Aruba America/Aruba + Camacuri 12.500000 -70.016667 + Oranjestad 12.516667 -70.033333 + Bahamas America/Nassau + Freeport, Grand Bahama 26.550000 -78.700000 + Georgetown, Exuma 23.475000 -75.766667 + Nassau 25.083333 -77.350000 + Belize America/Belize + Belize City 17.483333 -88.183333 + Bolivia America/La_Paz + Camiri -20.050000 -63.516667 + Cobija -11.033333 -68.733333 + Cochabamba -17.383333 -66.150000 + Concepción -16.150000 -62.016667 + Alto Airport -16.516667 -68.183333 + Magdalena -13.233333 -64.200000 + Oruro -17.983333 -67.150000 + Potosí -19.583611 -65.753056 + Puerto Suárez -18.950000 -57.800000 + Reyes -14.316667 -67.383333 + Riberalta -10.983333 -66.100000 + Roboré -18.333333 -59.750000 + Rurrenabaque -14.466667 -67.566667 + San Borja -14.816667 -66.850000 + San Ignacio de Velasco -16.366667 -60.950000 + San Joaquín -13.066667 -64.816667 + San José de Chiquitos -17.850000 -60.750000 + Santa Ana de Yacuma -13.750000 -65.433333 + Santa Cruz -17.800000 -63.166667 + Sucre -19.043056 -65.259167 + Tarija -21.516667 -64.750000 + Trinidad -14.816667 -64.916667 + Villamontes -21.250000 -63.500000 + Viro Viro -17.650000 -63.133333 + Yacuiba -22.033333 -63.683333 + Brazil America/Sao_Paulo + Cruzeiro do Sul -7.633333 -72.600000 + Rio Branco -9.966667 -67.800000 + Tarauacá -8.166667 -70.766667 + Alagoas -9.666667 -35.716667 + Amapá 3.833333 -51.833333 + Amazonas -3.113333 -60.025278 + Manicoré -5.812222 -61.297500 + São Félix -3.750000 -69.483333 + São Gabriel -0.133333 -67.083333 + Tefé -3.366667 -64.700000 + Bom Jesus da Lapa -13.250000 -43.416667 + Ilhéus -14.816667 -39.033333 + Paulo Afonso -9.350000 -38.233333 + Pôrto Seguro -16.433333 -39.083333 + Salvador -12.983333 -38.516667 + Vitória da Conquista -14.850000 -40.850000 + Ceará -3.716667 -38.500000 + Brasília -15.783333 -47.916667 + Espírito Santo -20.316667 -40.350000 + Goiás -16.333333 -48.966667 + Goiânia -16.666667 -49.266667 + Maranhão -5.533333 -47.483333 + São Luís -2.516667 -44.266667 + Alta Floresta -9.900000 -55.900000 + Barra do Garças -15.883333 -52.250000 + Cuiabá -15.583333 -56.083333 + Campo Grande -20.450000 -54.616667 + Corumbá -19.016667 -57.650000 + Ponta Porã -22.533333 -55.716667 + Minas Gerais -21.233333 -43.766667 + Belo Horizonte -19.916667 -43.933333 + Juiz de Fora -21.751667 -43.352778 + Montes Claros -16.716667 -43.866667 + Poços de Caldas -21.800000 -46.566667 + Uberaba -19.750000 -47.916667 + Uberlândia -18.916667 -48.300000 + Paraná -25.416667 -49.250000 + Foz do Iguaçu -25.550000 -54.583333 + Londrina -23.300000 -51.150000 + Maringá -23.416667 -51.916667 + Paraíba -7.216667 -35.883333 + João Pessoa -7.116667 -34.866667 + Pará -3.200000 -52.200000 + Belém -1.450000 -48.483333 + Cachimbo -8.950000 -54.900000 + Conceição do Araguaia -8.241111 -49.296111 + Itaituba -4.216667 -56.016667 + Jacareacanga -6.266667 -57.650000 + Marabá -5.350000 -49.116667 + Piri Grande 0.066667 -50.000000 + Santarém -2.433333 -54.700000 + Tucuruí -3.700000 -49.700000 + Petrolina -9.400000 -40.500000 + Recife -8.050000 -34.900000 + Vila dos Remédios -3.850000 -32.400000 + Piauí -2.909167 -41.774722 + Teresina -5.083333 -42.816667 + Rio Grande do Norte -5.191389 -37.344722 + Natal Airport -5.916667 -35.250000 + Rio Grande do Sul -31.766667 -52.333333 + Porto Alegre -30.033333 -51.200000 + Santa Maria Airport -29.716667 -53.700000 + Uruguaiana -29.750000 -57.083333 + Campos -21.750000 -41.300000 + Rio de Janeiro Airport -22.900000 -43.166667 + São Pedro da Aldeia -22.850000 -42.100000 + Rondônia -8.766667 -63.900000 + Vilhena -12.716667 -60.116667 + Boa Vista 2.816667 -60.666667 + Santa Catarina -27.583333 -48.566667 + Sergipe -10.916667 -37.066667 + Bauru -22.316667 -49.066667 + Campinas -22.900000 -47.083333 + Guaratinguetá -22.816667 -45.216667 + Guarulhos -23.466667 -46.533333 + Palmeiras -21.133333 -47.750000 + Piraçununga -21.983333 -47.416667 + Presidente Prudente -22.116667 -51.366667 + Santos -23.950000 -46.333333 + São José dos Campos -23.183333 -45.883333 + Marte Airport -23.516667 -46.633333 + Tocantis 18.416667 -64.616667 + The Mill 18.450000 -64.533333 + Cayman Islands America/Cayman + Owen Roberts Airport, Grand Cayman 19.283333 -81.350000 + Knob Hill 19.683333 -79.883333 + Red Bay Estate 19.283333 -81.350000 + Chile America/Santiago + Antofagasta -23.650000 -70.400000 + Arica -18.483333 -70.333333 + Balmaceda -45.916667 -71.683333 + Concepción -36.766667 -73.050000 + Hanga Roa -27.150000 -109.433333 + Iquique -20.216667 -70.166667 + La Serena -29.907778 -71.254167 + Puerto Montt -41.471667 -72.936944 + Punta Arenas -53.150000 -70.916667 + Santa Teresa de Lo Ovalle -33.383333 -70.783333 + Pudahuel -33.383333 -70.783333 + Temuco -38.733333 -72.600000 + Colombia America/Bogota + Barranquilla 10.963889 -74.796389 + Bogotá 4.600000 -74.083333 + Bucaramanga 7.129722 -73.125833 + Cali 3.437222 -76.522500 + Cartagena 10.399722 -75.514444 + Cúcuta 7.883333 -72.505278 + Leticia -4.215278 -69.940556 + Pereira 4.813333 -75.696111 + Rionegro 6.155278 -75.388889 + San Andrés 12.584722 -81.700556 + Costa Rica America/Costa_Rica + Alajuela 10.016667 -84.216667 + Liberia 10.616667 -85.433333 + Mata de Palo 9.950000 -84.150000 + Puerto Limón 10.000000 -83.033333 + San José 9.933333 -84.083333 + Cuba America/Havana + Camagüey 21.380833 -77.916944 + Cienfuegos 22.146111 -80.435556 + Guantánamo 20.144444 -75.209167 + Havana 23.131944 -82.364167 + Holguín 20.887222 -76.263056 + Manzanillo 20.333333 -77.116667 + Matanzas 23.041111 -81.577500 + Santiago de Cuba 20.024722 -75.821944 + Dominican Republic America/Santo_Domingo + Barahona 18.200000 -71.100000 + La Romana 18.416667 -68.966667 + Mancha Nueva 18.433333 -69.683333 + Pantanal 18.533333 -68.366667 + Puerto Plata 19.800000 -70.683333 + Santiago 19.450000 -70.700000 + Santo Domingo 18.466667 -69.900000 + Ecuador America/Guayaquil + Guayaquil -2.166667 -79.900000 + Latacunga -0.933333 -78.616667 + Manta -0.950000 -80.733333 + Quito -0.216667 -78.500000 + El Salvador America/El_Salvador + Comalapa 13.501111 -89.075833 + Ilopango 13.701667 -89.109444 + San Salvador 13.708611 -89.203056 + Falkland Islands (Malvinas) Atlantic/Stanley + Mount Pleasant Airport -51.816667 -58.450000 + French Guiana America/Cayenne + Cayenne 4.933333 -52.333333 + Grenada America/Grenada + Bamboo 12.000000 -61.783333 + Saint George's 12.050000 -61.750000 + Guadeloupe America/Guadeloupe + Basse-Terre 16.000000 -61.716667 + Les Abymes 16.266667 -61.500000 + Guatemala Aurora Airport 14.583333 -90.516667 + Huehuetenango 15.319722 -91.470833 + Puerto Barrios 15.716667 -88.600000 + Puerto San José 13.925556 -90.824444 + Retalhuleu 14.533333 -91.683333 + Tikal 17.225000 -89.613333 + Guyana America/Guyana + Cheddi Jagan International Airport 6.483333 -58.250000 + Haiti America/Port-au-Prince + Honduras America/Tegucigalpa + Amapala 13.292222 -87.653889 + Catacamas 14.800000 -85.900000 + Ciudad Choluteca 13.300278 -87.190833 + Comayagua 14.450000 -87.633333 + Guanaja 16.400000 -85.900000 + La Ceiba 15.783333 -86.800000 + La Esperanza 14.300000 -88.183333 + La Mesa 15.416667 -87.883333 + Puerto Lempira 15.266667 -83.766667 + Roatán 16.300000 -86.550000 + Santa Rosa de Copán 14.766667 -88.783333 + Tegucigalpa 14.100000 -87.216667 + Tela 15.783333 -87.450000 + Yoro 15.133333 -87.133333 + Jamaica America/Jamaica + Norman Manley Airport 17.933333 -76.783333 + Montego Bay 18.466667 -77.916667 + Martinique America/Martinique + Fort-de-France 14.600000 -61.083333 + Le Lamentin 14.600000 -61.000000 + Montserrat America/Montserrat + Netherlands Antilles America/Curacao + Benners 17.483333 -62.983333 + Cupe Coy 18.050000 -63.133333 + Dorp Nikiboko 12.150000 -68.266667 + Gato 12.166667 -68.966667 + Nicaragua America/Managua + Bluefields 12.000000 -83.750000 + Chinandega 12.616667 -87.150000 + Jinotega 13.100000 -86.000000 + Juigalpa 12.083333 -85.400000 + Managua 12.150833 -86.268333 + Puerto Cabezas 14.033333 -83.383333 + Rivas 11.433333 -85.833333 + Panama America/Panama + David 8.433333 -82.433333 + Fuerte Kobbe 8.916667 -79.583333 + Panamá 8.966667 -79.533333 + Tocumen 9.083333 -79.383333 + Paraguay America/Asuncion + Asunción -25.266667 -57.666667 + Colonia Félix de Azara -25.483333 -54.766667 + Andahuaylas -13.655556 -73.387222 + Arequipa -16.398889 -71.535000 + Ayacucho -13.158333 -74.223889 + Chiclayo -6.773611 -79.841667 + Cusco -13.518333 -71.978056 + Iquitos -3.748056 -73.247222 + Juliaca -15.500000 -70.133333 + Lima-Callao, Jorge Chavez International Airport -12.000000 -77.116667 + Pisco -13.700000 -76.216667 + Pucallpa -8.382500 -74.538056 + Puerto Maldonado -12.600000 -69.183333 + Tacna -17.994722 -70.241111 + Talara -4.577222 -81.271944 + Tarapoto -6.501389 -76.365556 + Trujillo -8.111944 -79.025556 + Tumbes -3.566667 -80.441389 + Saint Lucia America/St_Lucia + Castries 14.000000 -61.000000 + Pointe Sable 13.750000 -60.950000 + Vigie 14.033333 -61.016667 + Saint Martin America/Marigot + Saint Vincent & the Grenadines America/St_Vincent + Arnos Vale 13.133333 -61.200000 + Kingstown 13.133333 -61.216667 + South Georgia & the South Sandwich Islands Atlantic/South_Georgia + Georgia Asia/Tbilisi + Suriname America/Paramaribo + Paramaribo 5.833333 -55.166667 + Zanderij 5.450000 -55.200000 + Trinidad & Tobago America/Port_of_Spain + Bon Accord 11.150000 -60.833333 + Piarco 10.583333 -61.333333 + Port-of-Spain 10.650000 -61.516667 + Turks & Caicos Islands America/Grand_Turk + Uruguay America/Montevideo + Carrasco -34.885278 -56.060556 + Colonia -34.466667 -57.850000 + Durazno -33.413056 -56.500556 + Maldonado -34.900000 -54.950000 + Melilla -34.783333 -56.250000 + Venezuela America/Caracas + Acarigua 9.559722 -69.201944 + Barcelona 10.116667 -64.683333 + Barinas 8.629167 -70.207222 + Barquisimeto 10.073889 -69.322778 + Calabozo 8.934444 -67.426667 + Caracas 10.500000 -66.916667 + Ciudad Bolívar 8.122222 -63.549722 + Coro 11.409167 -69.667222 + El Variante 7.579444 -72.073889 + El Vigía 8.621944 -71.650556 + Guanare 9.050000 -69.750000 + Guaricure 11.766667 -70.131111 + Guasdalito 7.247222 -70.729444 + Güiria 10.571111 -62.294444 + La Chica 10.933333 -64.016667 + Maracaibo 10.631667 -71.640556 + Maracay 10.246944 -67.595833 + Maturín 9.750000 -63.176667 + Mene Grande 9.850278 -70.925556 + Morocure 8.260278 -62.707500 + Mérida 8.600000 -71.183333 + Paramillo 7.805556 -72.216944 + Puerto Ayacucho 5.663889 -67.623611 + Puerto Borburata 10.483889 -67.983889 + San Antonio del Táchira 7.817778 -72.442500 + San Felipe 10.340556 -68.737222 + San Fernando 7.900000 -67.416667 + San Juan de los Morros 9.911111 -67.358333 + San Tomé 8.940833 -64.130833 + Santa Bárbara 8.983333 -71.900000 + Valencia 10.166667 -67.933333 + Valera 9.317778 -70.603611 +Europe + Albania Europe/Tirane + Tirana 41.327500 19.818889 + Andorra Europe/Andorra + Austria Europe/Vienna + Aigen im Ennstal 47.516667 14.133333 + Graz 47.066667 15.450000 + Hohenems 47.366667 9.683056 + Innsbruck 47.266667 11.400000 + Klagenfurt 46.624722 14.305278 + Linz 48.300000 14.300000 + Salzburg 47.800000 13.033333 + Teesdorf 47.950000 16.283333 + Tulln 48.333333 16.050000 + Vienna 48.200000 16.366667 + Wiener Neustadt 47.800000 16.250000 + Zell am See 47.316667 12.783333 + Zeltweg 47.183333 14.750000 + Belarus Europe/Minsk + Brest 52.108333 23.896667 + Homyel' 52.441667 30.983333 + Hrodna 53.681389 23.814722 + Minsk 53.900000 27.566667 + Vitsyebsk 55.192500 30.194444 + Belgium Europe/Brussels + Antwerp / Deurne 51.200000 4.466667 + Brussels, Flemish & Walloon Brabant 50.783333 4.766667 + Brussels 50.833333 4.333333 + Schaffen 51.000000 5.083333 + East-Flanders 50.583333 3.800000 + Gosselies 50.466667 4.416667 + Limburg 51.166667 5.433333 + Liège 50.650000 5.433333 + Elsenborn 50.450000 6.216667 + Namur 50.250000 4.616667 + West-Flanders 51.100000 2.650000 + Oostende 51.216667 2.916667 + Bosnia & Herzegovina Europe/Sarajevo + Banja Luka 44.775833 17.185556 + Mostar 43.343333 17.808056 + Sarajevo 43.850000 18.383333 + Bulgaria Europe/Sofia + Burgas 42.506058 27.467810 + Gorna Oryakhovitsa 43.127778 25.701667 + Plovdiv 42.150000 24.750000 + Sofia 42.683333 23.316667 + Varna 43.216667 27.916667 + Croatia Europe/Zagreb + Bol 43.261944 16.655000 + Dubrovnik 42.650556 18.091389 + Jelovice 45.498056 14.014444 + Liška 44.576667 14.376111 + Osijek 45.551111 18.693889 + Rijeka 45.343056 14.409167 + Split 43.513889 16.455833 + Zadar 44.119722 15.242222 + Zagreb 45.800000 16.000000 + Cyprus Asia/Nicosia + Akrotiri 34.604167 32.958333 + Larnaca 34.916667 33.629167 + Nicosia 35.166667 33.366667 + Paphos 34.766667 32.416667 + Tymbou 35.133333 33.512500 + Czech Republic Europe/Prague + Brno 49.200000 16.633333 + Holešov 49.333333 17.583333 + Karlovy Vary 50.216667 12.900000 + Liberec 50.763889 15.065278 + Ostrava 49.833333 18.283333 + Prague 50.083333 14.466667 + Denmark Europe/Copenhagen + Billund 55.733333 9.116667 + Copenhagen 55.666667 12.583333 + Esbjerg 55.466667 8.450000 + Karup 56.300000 9.166667 + Kastrup 55.633333 12.650000 + Mejlby 56.000000 8.333333 + Odense 55.400000 10.383333 + Roskilde 55.650000 12.083333 + Rønne 55.100000 14.700000 + Skrydstrup 55.233333 9.250000 + Sottrupskov 54.966667 9.750000 + Tirstrup 56.300000 10.700000 + Vamdrup 55.416667 9.283333 + Ålborg 57.050000 9.933333 + Estonia Europe/Tallinn + Kuressaare 58.248056 22.503889 + Kärdla 58.997778 22.749167 + Pärnu 58.374722 24.513611 + Tallinn 59.433889 24.728056 + Tartu 58.366111 26.736111 + Faroe Islands Atlantic/Faroe + Sørvágur 62.066667 -7.300000 + Tórshavn 62.016667 -6.766667 + Finland Europe/Helsinki + Enontekiö 68.383333 23.633333 + Halli 61.866667 24.833333 + Helsinki 60.175556 24.934167 + Ivalo 68.650000 27.600000 + Joensuu 62.600000 29.766667 + Jyväskylä 62.233333 25.733333 + Kajaani 64.233333 27.683333 + Kauhava 63.100000 23.083333 + Kemi 65.733333 24.566667 + Kittilä 67.666667 24.900000 + Kruunupyy 63.716667 23.033333 + Kuopio 62.900000 27.683333 + Kuusamo 65.966667 29.183333 + Lappeenranta 61.066667 28.183333 + Mikkeli 61.683333 27.250000 + Oulu 65.016667 25.466667 + Pori 61.483333 21.783333 + Rovaniemi 66.500000 25.716667 + Savonlinna 61.866667 28.883333 + Seinäjoki 62.800000 22.833333 + Tampere 61.500000 23.750000 + Turku 60.450000 22.283333 + Utti 60.883333 26.933333 + Vaasa 63.100000 21.600000 + Varkaus 62.316667 27.916667 + France Europe/Paris + Abbeville 50.100000 1.833333 + Acon 48.766667 1.100000 + Agen 44.200000 0.633333 + Ajaccio 41.916667 8.733333 + Alençon 48.433333 0.083333 + Ambérieu-en-Bugey 45.950000 5.350000 + Auch 43.650000 0.583333 + Aurillac 44.916667 2.450000 + Avord 47.033333 2.650000 + Bastia 42.702778 9.450000 + Beauvais 49.433333 2.083333 + Bergerac 44.850000 0.483333 + Biarritz 43.483333 -1.566667 + Bordeaux-Merignac Airport 44.833333 -0.700000 + Bourges 47.083333 2.400000 + Brest 48.450000 -4.416667 + Brive 45.150000 1.533333 + Béziers 43.350000 3.250000 + Caen 49.183333 -0.350000 + Calvi 42.566667 8.750000 + Cambrai 50.166667 3.233333 + Cannes 43.550000 7.016667 + Carcassonne 43.216667 2.350000 + Cazaux 44.533333 -1.150000 + Chambéry 45.600000 5.900000 + Chartres 48.450000 1.500000 + Cherbourg 49.650000 -1.650000 + Châlons-en-Champagne 48.950000 4.366667 + Châteaudun 48.083333 1.333333 + Châteauroux 46.816667 1.700000 + Clermont-Ferrand 45.783333 3.083333 + Cognac 45.700000 -0.333333 + Colmar 48.083333 7.366667 + Creil 49.266667 2.483333 + Dax 43.716667 -1.050000 + Dijon 47.316667 5.016667 + Dinard 48.633333 -2.066667 + Dole 47.100000 5.500000 + Dollemard 49.516667 0.066667 + Grenoble 45.166667 5.716667 + Hoëricourt 48.633333 4.900000 + Hyères 43.116667 6.116667 + Hésingue 47.583333 7.516667 + Istres 43.516667 4.983333 + La Roche-sur-Yon 46.666667 -1.433333 + La Rochelle 46.166667 -1.150000 + Lannion 48.733333 -3.466667 + Le Mans 48.000000 0.200000 + Le Puy 45.033333 3.883333 + Lille 50.633333 3.066667 + Limoges 45.850000 1.250000 + Luxeuil-les-Bains 47.816667 6.383333 + Lyon 45.750000 4.850000 + Marseille 43.300000 5.400000 + Melun 48.533333 2.666667 + Metz 49.133333 6.166667 + Mont-de-Marsan 43.883333 -0.500000 + Montgauch 43.000000 1.083333 + Montpellier 43.600000 3.883333 + Montélimar 44.566667 4.750000 + Mâcon 46.300000 4.833333 + Méné Guen 47.783333 -3.433333 + Nancy 48.683333 6.200000 + Nantes 47.216667 -1.550000 + Nevers 46.983333 3.166667 + Nice 43.700000 7.250000 + Nîmes 43.833333 4.350000 + Orange 44.133333 4.833333 + Orléans 47.916667 1.900000 + Paris-Le Bourget Airport 48.966667 2.450000 + Pau 43.300000 -0.366667 + Perpignan 42.683333 2.883333 + Poggiale 41.516667 9.083333 + Poitiers 46.583333 0.333333 + Quimper 48.000000 -4.100000 + Reims 49.250000 4.033333 + Rennes 48.083333 -1.683333 + Rodez 44.333333 2.566667 + Romorantin 47.366667 1.750000 + Rouen 49.433333 1.083333 + Saint-Brieuc 48.516667 -2.783333 + Saint-Quentin 49.850000 3.283333 + Saint-Yan 46.416667 4.033333 + Salon 43.633333 5.100000 + Strasbourg 48.583333 7.750000 + Tarbes 43.233333 0.083333 + Toulouse 43.600000 1.433333 + Tours 47.383333 0.683333 + Trignac 47.316667 -2.183333 + Troyes 48.300000 4.083333 + Veauche 45.550000 4.283333 + Vichy 46.166667 3.400000 + Vélizy 48.800000 2.183333 + Évreux 49.016667 1.150000 + Germany Europe/Berlin + Baden-Württemberg NoneTZ + Donaueschingen 47.950000 8.500000 + Friedrichshafen 47.650000 9.483333 + Karlsruhe 49.004722 8.385833 + Lahr 48.350000 7.866667 + Laupheim 48.233333 9.883333 + Meßstetten 48.183333 8.966667 + Neuostheim 49.477500 8.514444 + Niederstetten 49.400000 9.919444 + Stuttgart-Echterdingen 48.683333 9.216667 + Bavaria 48.366667 10.883333 + Dorfgmünd 49.700000 11.950000 + Hof 50.316667 11.916667 + Illesheim 49.466667 10.383333 + Katterbach 49.316667 10.633333 + Lager Lechfeld 48.166667 10.850000 + Landsberg 48.050000 10.866667 + Munich 48.150000 11.583333 + Neuburg an der Donau 48.733333 11.183333 + Nuremberg 49.447778 11.068333 + Oberpfaffenhofen 48.066667 11.266667 + Roth 49.245000 11.091111 + Würzburg 49.787778 9.936111 + Berlin-Tegel 52.566667 13.316667 + Brandenburg 53.083333 8.800000 + Hamburg-Fuhlsbuettel 53.633333 10.000000 + Hesse 50.116667 8.683333 + Fritzlar 51.133333 9.283333 + Kassel 51.316667 9.500000 + Wiesbaden 50.083333 8.250000 + Lower Saxony 52.283333 9.083333 + Braunschweig 52.266667 10.533333 + Celle 52.616667 10.083333 + Faßberg 52.900000 10.166667 + Hannover 52.366667 9.716667 + Nordholz 53.783333 8.600000 + Webershausen 53.550000 7.666667 + Wunstorf 52.433333 9.416667 + Mecklenburg-Western Pomerania 53.933333 12.350000 + Parchim 53.433333 11.850000 + Seebad Heringsdorf 53.966667 14.166667 + Trollenhagen 53.600000 13.300000 + North Rhine-Westphalia 51.616667 6.133333 + Bonn 50.733333 7.100000 + Bredeck 51.916667 8.300000 + Dortmund 51.516667 7.450000 + Geilenkirchen 50.966667 6.116667 + Kalkar 51.733333 6.300000 + Kalkum 51.300000 6.766667 + Klemenshof 50.833333 6.666667 + Mönchengladbach 51.200000 6.433333 + Münster 51.966667 7.633333 + Paderborn 51.716667 8.766667 + Rheine 52.283333 7.450000 + Rhineland-Palatinate 50.166667 7.083333 + Hahn 49.966667 7.266667 + Liebenscheid 50.700000 8.100000 + Ramstein 49.450000 7.533333 + Spangdahlem 49.983333 6.683333 + Zweibrücken 49.250000 7.366667 + Saarland 49.200000 7.100000 + Saxony 51.050000 13.750000 + Leipzig 51.300000 12.333333 + Saxony-Anhalt 51.766667 13.133333 + Schleswig-Holstein 54.300000 9.500000 + Kiel 54.333333 10.133333 + Schleswig 54.516667 9.550000 + Ulstrupfeld 54.833333 9.533333 + Vorrade 53.816667 10.683333 + Westerland 54.900000 8.300000 + Thuringia 50.983333 12.450000 + Bindersleben 50.966667 10.950000 + Gibraltar 36.150000 -5.350000 + Greece Europe/Athens + Alexandroúpolis 40.847500 25.874444 + Andravída 37.900000 21.266667 + Argostólion 38.173056 20.481944 + Athens Eleftherios Venizelos International Airport 37.933333 23.933333 + Chrysoúpolis 40.985556 24.693889 + Chíos 38.367778 26.135833 + Elefsís 38.033333 23.533333 + Irákleion 35.325000 25.130556 + Kalamáta 37.038889 22.114167 + Karpásion 39.933333 25.233333 + Katomérion 38.650000 20.783333 + Kos 36.893333 27.288889 + Kozáni 40.301111 21.786389 + Kárpathos 35.500000 27.233333 + Kérkyra 39.620000 19.919722 + Kýthira 36.150000 22.992500 + Lárisa 39.637222 22.420278 + Monólithos 36.400000 25.483333 + Mytilíni 39.110000 26.554722 + Mýkonos 37.450000 25.333333 + Náxos 37.105556 25.376389 + Paradeísion 36.400000 28.083333 + Páros 37.083333 25.150000 + Skíathos 39.166667 23.483333 + Soúda 35.484444 24.074444 + Sámos 37.757222 26.976944 + Tanágra 38.316667 23.533333 + Thessaloníki 40.640278 22.943889 + Zákynthos 37.791389 20.895278 + Áno Síros 37.450000 24.933333 + Áraxos 38.166667 21.416667 + Áyios Athanásios 39.233333 22.766667 + Guernsey Europe/Guernsey + Hautnez 49.433333 -2.600000 + Saint Peter Port 49.456111 -2.540833 + Hungary Europe/Budapest + Budapest 47.500000 19.083333 + Debrecen 47.533333 21.633333 + Kecskemét 46.900000 19.783333 + Pápa 47.333333 17.466667 + Pécs 46.083333 18.233333 + Szeged 46.250000 20.166667 + Szolnok 47.183333 20.200000 + Iceland Atlantic/Reykjavik + Akureyri 65.666667 -18.100000 + Eiðar 65.366667 -14.350000 + Reykjavík 64.150000 -21.950000 + Ytri-Njarðvík 63.983333 -22.550000 + Ireland Europe/Dublin + Cork 51.898611 -8.495833 + Dublin Airport 53.433333 -6.250000 + Dunleary 53.292500 -6.128611 + Glentavraun 53.883333 -8.800000 + Shannon 52.703889 -8.864167 + Isle of Man Europe/Isle_of_Man + Isle of Man, Ronaldsway Airport 54.083333 -4.633333 + Ronaldsway 54.083333 -4.616667 + Italy Europe/Rome + Albenga 44.050000 8.216667 + Alghero 40.558889 8.318056 + Aviano 46.070556 12.594722 + Bari 41.133333 16.850000 + Bergamo 45.683333 9.716667 + Bologna 44.483333 11.333333 + Bolzano 46.516667 11.366667 + Brescia 45.550000 10.250000 + Breuil-Cervinia 45.933333 7.633333 + Brindisi 40.633333 17.933333 + Cagliari 39.216667 9.116667 + Capri 40.550000 14.233333 + Case Arfel 44.250000 7.783333 + Catania 37.500000 15.100000 + Cervia 44.261667 12.358889 + Crotone 39.083333 17.133333 + Cuneo 44.383333 7.533333 + Decimomannu 39.312500 8.969444 + Dobbiaco 46.742500 12.231389 + Ferrara 44.833333 11.583333 + Peretola Airport 43.800000 11.200000 + Forlì 44.223611 12.052778 + Frosinone 41.633333 13.316667 + Genoa 44.416667 8.950000 + Ginosa Marina 40.433333 16.883333 + Gioia del Colle 40.800000 16.916667 + Grazzanise 41.083333 14.100000 + Grosseto 42.766667 11.133333 + Grottaglie 40.533333 17.433333 + Isola del Cantone 44.650000 8.950000 + Laigueglia 43.966667 8.150000 + Lampedusa 35.500000 12.600000 + Latina 41.466667 12.866667 + Lecce 40.383333 18.183333 + Messina 38.183333 15.566667 + Milan 45.466667 9.200000 + Molino di Ancona 43.616667 13.366667 + Capodichino Airport 40.850000 14.300000 + Olbia 40.916667 9.516667 + Palazzo 42.716667 10.383333 + Palermo 38.116667 13.366667 + Paneveggio 46.300000 11.733333 + Pantelleria 36.833333 11.950000 + Parma 44.800000 10.333333 + Perugia 43.133333 12.366667 + Pescara 42.466667 14.216667 + Piacenza 45.016667 9.666667 + Pisa 43.716667 10.383333 + Pontecagnano 40.633333 14.883333 + Pratica di Mare 41.666667 12.483333 + Reggio di Calabria 38.100000 15.650000 + Resia 46.833333 10.516667 + Rieti 42.400000 12.850000 + Rimini 44.063333 12.580833 + Urbe Airport 41.950000 12.500000 + Ronchi dei Legionari 45.823611 13.509722 + Salignano 39.833333 18.350000 + San Stèfano 39.107500 9.506111 + Sant'Eufemia Lamezia 38.916667 16.250000 + Sporminore 46.166667 11.033333 + Tamaricciola 41.416667 15.733333 + Tarvisio 46.510833 13.594167 + Trapani 38.016667 12.483333 + Trevico 41.050000 15.216667 + Treviso 45.666667 12.245000 + Trieste 45.648611 13.780000 + Turin 45.050000 7.666667 + Venice 45.438611 12.326667 + Verona 45.450000 11.000000 + Viterbo 42.416667 12.100000 + Àrbatax 39.934444 9.705556 + Jersey Europe/Jersey + La Hougue 49.216667 -2.216667 + Saint Helier 49.183333 -2.100000 + Latvia Europe/Riga + Liepāja 56.516667 21.016667 + Rīga 56.950000 24.100000 + Liberia Africa/Monrovia + Liechtenstein Europe/Vaduz + Vaduz 47.133333 9.516667 + Lithuania Europe/Vilnius + Kaunas 54.900000 23.900000 + Palanga 55.917500 21.068611 + Vilnius 54.683333 25.316667 + Šiauliai 55.933333 23.316667 + Luxembourg Airport 49.616667 6.216667 + Luxembourg Europe/Luxembourg + Macedonia Europe/Skopje + Ohrid 41.117222 20.801944 + Skopje 42.000000 21.433333 + Luqa 35.858889 14.488611 + Valletta 35.899722 14.514722 + Moldova Europe/Chisinau + Chişinău 47.005556 28.857500 + Nice 43.650000 7.200000 + Montenegro Europe/Podgorica + Podgorica 42.441111 19.263611 + Tivat 42.436389 18.696111 + Netherlands Europe/Amsterdam + Amsterdam 52.350000 4.916667 + De Kooy 52.916667 4.800000 + Deelen 52.066667 5.900000 + Eindhoven 51.450000 5.466667 + Gilze 51.550000 4.950000 + Groningen 53.216667 6.550000 + Leeuwarden 53.200000 5.783333 + Maastricht 50.850000 5.683333 + Oost-Vlieland 53.283333 5.066667 + Rotterdam 51.916667 4.500000 + The Hague 52.083333 4.300000 + Valkenburg 52.183333 4.433333 + Volkel 51.650000 5.650000 + Woensdrecht 51.433333 4.300000 + Norway Europe/Oslo + Alta 69.966667 23.241667 + Berlevåg 70.850000 29.100000 + Bodø 67.283333 14.383333 + Bolle 68.158611 13.596389 + Boltåsen 68.533333 16.700000 + Brønnøysund 65.466667 12.216667 + Båtsfjord 70.632222 29.699722 + Dalem 69.066667 18.516667 + Djupdalen 63.700000 9.583333 + Eldskog 70.050000 25.000000 + Fagernes 60.983333 9.250000 + Fiskenes 69.263611 16.176667 + Flesland 60.293889 5.215278 + Florø 61.600000 5.000000 + Førde 61.450000 5.866667 + Gardermoen 60.216667 11.100000 + Hammerfest 70.661667 23.688333 + Hasvik 70.483333 22.150000 + Haugesund 59.411944 5.277500 + Holm 61.150000 7.166667 + Honningsvåg 70.983333 25.983333 + Kirkenes 69.725000 30.051667 + Kjevik 58.200000 8.100000 + Kristiansund 63.116667 7.750000 + Langenes 69.666667 18.916667 + Mehamn 71.033333 27.850000 + Molde 62.733333 7.183333 + Mosjøen 65.833333 13.200000 + Namsos 64.483333 11.500000 + Narvik 68.435556 17.437222 + Notodden 59.566667 9.283333 + Oseberg 59.316667 10.450000 + Oslo 59.916667 10.750000 + Rygge 59.383333 10.716667 + Røros 62.583333 11.400000 + Rørvik 64.850000 11.233333 + Røssvoll 66.366667 14.333333 + Røst 67.516667 12.116667 + Sandane 61.766667 6.216667 + Skagen 68.583333 15.050000 + Skien 59.200000 9.600000 + Sola 58.883333 5.600000 + Stokka 65.766667 12.566667 + Svartnes 70.350000 31.033333 + Svolvær 68.233333 14.566667 + Sørkjosen 69.800000 20.933333 + Torp 59.179722 10.242500 + Trondheim 63.416667 10.416667 + Vadsø 70.073056 29.769722 + Ålesund 62.466667 6.150000 + Ørsta 62.200000 6.150000 + Peru America/Lima + Poland Europe/Warsaw + Gdańsk 54.350000 18.666667 + Katowice 50.266667 19.016667 + Kraków 50.083333 19.916667 + Poznań 52.416667 16.966667 + Rzeszów 50.050000 22.000000 + Szczecin 53.416667 14.583333 + Warszawa-Okecie 52.166667 20.966667 + Wrocław 51.100000 17.033333 + Portugal Europe/Lisbon + Beja 38.016667 -7.866667 + Castelo Branco 38.516667 -28.733333 + Faro 37.016667 -7.933333 + Flor da Rosa 36.966667 -25.150000 + Lajes 38.766667 -27.100000 + Lisbon 38.716667 -9.133333 + Monte Real 39.850000 -8.866667 + Montijo 38.700000 -8.966667 + Ovar 40.866667 -8.633333 + Ponta Delgada 37.733333 -25.666667 + Porto 41.150000 -8.616667 + Porto Santo 33.050000 -16.333333 + Santa Cruz das Flores 39.450000 -31.116667 + Sintra 38.800000 -9.383333 + Água de Pena 32.700000 -16.766667 + Romania Europe/Bucharest + Arad 46.183333 21.316667 + Bacău 46.566667 26.900000 + Baia Mare 47.666667 23.583333 + Bucharest 44.433333 26.100000 + Cluj-Napoca 46.766667 23.600000 + Craiova 44.316667 23.800000 + Iaşi 47.166667 27.600000 + Mihail Kogălniceanu 44.366667 28.450000 + Oradea 47.066667 21.933333 + Satu Mare 47.800000 22.883333 + Sibiu 45.800000 24.150000 + Suceava 47.633333 26.250000 + Timişoara 45.749444 21.227222 + Tulcea 45.166667 28.800000 + Târgu-Mureş 46.550000 24.566667 + Russia Asia/Krasnoyarsk + Adler 43.431111 39.923611 + Anadyr' 64.750000 177.483333 + Anapa 44.894444 37.321667 + Arkhangel'sk 64.547222 40.548611 + Astrakhan' 46.349444 48.049167 + Barnaul 53.360556 83.763611 + Bratsk 56.350000 101.916667 + Bryansk 53.287500 34.380556 + Chelyabinsk 55.154444 61.429722 + Chita 52.031712 113.500867 + Chul'man 56.846944 124.906111 + Engel's 51.483889 46.105278 + Irkutsk 52.297778 104.296389 + Kaliningrad 54.710000 20.500000 + Kazan' 55.750000 49.133333 + Kemerovo 55.333333 86.083333 + Khabarovsk 48.480833 135.092778 + Khanty-Mansiysk 61.004167 69.001944 + Krasnodar 45.032778 38.976944 + Magadan 59.566667 150.800000 + Mineral'nyye Vody 44.210278 43.135278 + Mirnyy 62.535278 113.961111 + Vnukovo Airport 55.650000 37.266667 + Murmansk 68.971667 33.081944 + Nal'chik 43.498056 43.618889 + Nizhnevartovsk 60.933333 76.566667 + Novokuznetsk 53.750000 87.100000 + Novosibirsk 55.041111 82.934444 + Omsk 55.000000 73.400000 + Orenburg 51.784722 55.098611 + Penza 53.195833 45.000000 + Perm' 58.000000 56.250000 + Petropavlovsk 53.016667 158.650000 + Rostov 47.236389 39.713889 + Saint Petersburg 59.894444 30.264167 + Samara 53.200000 50.150000 + Saratov 51.540556 46.008611 + Stavropol' 45.042778 41.973333 + Strigino 56.202778 43.798611 + Surgut 61.250000 73.416667 + Syktyvkar 61.666667 50.812222 + Tiksi 71.687222 128.869444 + Udachnyy 66.416667 112.400000 + Ufa 54.775000 56.037500 + Ul'yanovsk 54.316944 48.366111 + Ulan-Ude 51.826053 107.609794 + Velikiye Luki 56.340000 30.534722 + Vladivostok 43.128056 131.901111 + Volgograd 48.804722 44.585833 + Voronezh 51.669905 39.192267 + Yakutsk 62.033889 129.733056 + Yekaterinburg 56.857500 60.612500 + Yuzhno-Sakhalinsk 46.958116 142.733665 + Rimini 44.033333 12.616667 + Serbia Europe/Belgrade + Belgrade 44.818611 20.468056 + Niš 43.324722 21.903333 + Vršac 45.116667 21.303611 + Zemun 44.843056 20.401111 + Slovakia Europe/Bratislava + Bratislava 48.150000 17.116667 + Dolný Hričov 49.233333 18.633333 + Kamenica nad Cirochou 48.933333 22.000000 + Košice 48.716667 21.250000 + Lučenec 48.333333 19.666667 + Nitra 48.316667 18.083333 + Piešťany 48.600000 17.833333 + Poprad 49.050000 20.300000 + Prievidza 48.766667 18.633333 + Sliač 48.616667 19.183333 + Slovenia Europe/Ljubljana + Ljubljana 46.055278 14.514444 + Maribor 46.554722 15.646667 + Portorož 45.516111 13.588333 + Spain Europe/Madrid + A Coruña 43.366667 -8.383333 + Agoncillo 42.450000 -2.283333 + Alcantarilla 37.966667 -1.216667 + Alicante 38.350000 -0.483333 + Almería 36.833333 -2.450000 + Armilla 37.150000 -3.616667 + Atogo 28.050000 -16.583333 + Avilés 43.556944 -5.924722 + Barajas 40.483333 -3.583333 + Barcelona Airport 41.283333 2.066667 + Bilbao 43.250000 -2.966667 + Colmenar Viejo 40.666667 -3.766667 + Corcovados 28.616667 -17.750000 + Cuatro Vientos 40.383333 -3.783333 + Córdoba Airport 37.850000 -4.850000 + El Matorral 28.433333 -13.866667 + Fuenterrabía 43.366667 -1.783333 + Gando 27.950000 -15.366667 + Gerona 41.983333 2.816667 + Getafe 40.300000 -3.716667 + Granada 37.183333 -3.600000 + Güime 28.966667 -13.600000 + Ibiza 38.900000 1.433333 + Jerez 36.683333 -6.133333 + León Airport, Virgen del Camino 42.583333 -5.650000 + Los Baldíos 28.466667 -16.316667 + Los Llanos 38.916667 -1.850000 + Madrid 40.400000 -3.683333 + Mahón 39.883333 4.250000 + Melilla 35.316667 -2.950000 + Morón 37.133333 -5.450000 + Málaga 36.716667 -4.416667 + Noáin 42.750000 -1.633333 + Palma 39.566667 2.650000 + Reus 41.150000 1.116667 + Rota 36.616667 -6.350000 + Sabadell 41.550000 2.100000 + Salamanca 40.966667 -5.650000 + San Javier 37.800000 -0.850000 + San Pablo 37.416667 -5.883333 + Santander 43.464722 -3.804444 + Santiago Airport, Labacolla 42.900000 -8.433333 + Talavera la Real 38.883333 -6.766667 + Tamaduste 27.816667 -17.883333 + Torrejón del Rey 40.650000 -3.333333 + Valencia Airport 39.500000 -0.466667 + Vigo 42.233333 -8.716667 + Villanubla 41.700000 -4.833333 + Vitoria-Gasteiz 42.850000 -2.666667 + Zaragoza 41.633333 -0.883333 + Svalbard & Jan Mayen Arctic/Longyearbyen + Longyearbyen 78.216667 15.633333 + Sweden Europe/Stockholm + Borlänge 60.483333 15.416667 + Gällivare 67.133333 20.700000 + Göteborg 57.716667 11.966667 + Halmstad 56.671389 12.855556 + Jönköping 57.783333 14.183333 + Kalmar 56.666667 16.366667 + Karlstad 59.366667 13.500000 + Kiruna 67.850000 20.216667 + Kramfors 62.933333 17.792500 + Kristianstad 56.033333 14.133333 + Linköping 58.416667 15.616667 + Ljungbyhed 56.066667 13.216667 + Luleå 65.583333 22.150000 + Lycksele 64.600000 18.666667 + Malmö 55.600000 13.000000 + Norrköping 58.600000 16.183333 + Nyköping 58.750000 17.000000 + Ronneby 56.200000 15.300000 + Skellefteå 64.766667 20.950000 + Skövde 58.400000 13.850000 + Stockholm 59.333333 18.050000 + Sundsvall 62.383333 17.300000 + Söderhamn 61.300000 17.050000 + Umeå 63.833333 20.250000 + Visby 57.633333 18.300000 + Västerås 59.616667 16.550000 + Växjö 56.883333 14.816667 + Ängelholm 56.250000 12.866667 + Örebro 59.283333 15.216667 + Örnsköldsvik 63.300000 18.716667 + Switzerland Europe/Zurich + Bern 46.916667 7.466667 + Geneva 46.200000 6.166667 + Grenchen 47.183333 7.383333 + Lugano 46.000000 8.966667 + Neuchâtel 47.000000 6.966667 + Sankt Gallen 47.466667 9.400000 + Sion 46.233333 7.350000 + Zürich 47.366667 8.550000 + Turkey Europe/Istanbul + Adana 37.001667 35.328889 + Ankara 39.927222 32.864444 + Antalya 36.912500 30.689722 + Balikesir 39.649167 27.886111 + Bandirma 40.352222 27.976667 + Bodrum 37.038333 27.429167 + Burdur 37.720278 30.290833 + Bursa 40.191667 29.061111 + Corlu 41.159167 27.800000 + Dalaman 36.765000 28.804167 + Diyarbakir 37.915833 40.218889 + Erzurum 39.908611 41.276944 + Eskisehir 39.776667 30.520556 + Gaziantep 37.059444 37.382500 + Istanbul 41.018611 28.964722 + Izmir 38.410375 27.142010 + Kars 40.608056 43.097500 + Kayseri 38.732222 35.485278 + Kislakoy 40.099082 32.602115 + Konya 37.865556 32.482500 + Malatya 38.353333 38.311944 + Merzifon 40.873333 35.463056 + Nevsehir 38.625000 34.712222 + Samsun 41.286667 36.330000 + Tepetarla 40.750000 30.066667 + Trabzon 41.005000 39.726944 + Van 38.494167 43.380000 + Ukraine Europe/Kiev + Boryspil' 50.350000 30.950000 + Chagor 48.250000 25.983333 + Dnipropetrovs'k 48.450000 34.983333 + Donets'k 48.000000 37.800000 + Hostomel' 50.583333 30.266667 + Ivano-Frankivs'k 48.917222 24.707222 + Kharkiv 50.000000 36.250000 + Kiev 50.433333 30.516667 + Kryvyy Rih 47.916667 33.350000 + L'viv 49.833333 24.000000 + Mokroye 47.816667 35.250000 + Mykolayiv 46.966667 32.000000 + Odesa 46.466667 30.733333 + Rivne 50.616667 26.250000 + Simferopol' 44.950000 34.100000 + Telichka 50.400000 30.566667 + Uzhhorod 48.616667 22.300000 + United Kingdom Europe/London + East & South East England NoneTZ + Benson 51.616667 -1.083333 + Biggin Hill 51.283333 0.033333 + Brize Norton 51.766667 -1.566667 + Cambridge 52.200000 0.183333 + Farnborough 51.266667 -0.733333 + Lakenheath 52.433333 0.566667 + Heathrow Airport 51.483333 -0.450000 + Luton 51.883333 -0.416667 + Lydd 50.950000 0.916667 + Manston 51.350000 1.366667 + Marham 52.650000 0.550000 + Mildenhall 52.350000 0.516667 + Northolt 51.533333 -0.366667 + Norwich 52.633333 1.300000 + Odiham 51.250000 -0.933333 + Shoreham-by-Sea 50.833333 -0.233333 + Southampton 50.900000 -1.400000 + Southend 51.566667 0.700000 + Stansted Mountfitchet 51.900000 0.200000 + Wainfleet 53.100000 0.250000 + Wattisham 52.116667 0.950000 + Wittering 52.600000 -0.450000 + Fairford 51.700000 -1.783333 + Midlands 52.466667 -1.916667 + Castle Donnington 52.833333 -1.333333 + Cottesmore 52.700000 -0.650000 + Coventry 52.416667 -1.550000 + Cranfield 52.066667 -0.600000 + North East England 53.116667 -0.166667 + Cranwell 53.033333 -0.466667 + Dishforth 54.150000 -1.416667 + Newcastle 55.033333 -1.700000 + Tees-Side 54.516667 -1.416667 + Topcliffe 54.183333 -1.383333 + Waddington 53.166667 -0.533333 + North West England 53.816667 -3.050000 + Carlisle 54.883333 -2.933333 + Church Fenton 53.816667 -1.216667 + Kirmington 53.583333 -0.333333 + Leeds 53.800000 -1.583333 + Leeming Bar 54.300000 -1.550000 + Linton upon Ouse 54.033333 -1.250000 + Liverpool Airport 53.333333 -2.850000 + Manchester Airport 53.350000 -2.283333 + Shawbury 52.783333 -2.650000 + Northern Ireland 54.583333 -5.933333 + Eglinton 55.016667 -7.183333 + Scotland 57.133333 -2.100000 + Campbeltown 55.433333 -5.633333 + Dundee 56.500000 -2.966667 + Edinburgh 55.950000 -3.200000 + Glasgow Airport 55.866667 -4.433333 + Gramisdale 57.483333 -7.333333 + Inverness 57.466667 -4.233333 + Kilmoluag 56.500000 -6.916667 + Kinloss 57.616667 -3.566667 + Kintra 55.650000 -6.250000 + Kirkwall 58.966667 -2.950000 + Leuchars 56.383333 -2.883333 + Lossiemouth 57.700000 -3.283333 + Mossbank 60.450000 -1.200000 + Prestwick 55.483333 -4.616667 + Stornoway 58.216667 -6.366667 + Sumburgh 59.866667 -1.283333 + Wick 58.433333 -3.083333 + South & South West England 51.150000 -1.716667 + Bournemouth 50.716667 -1.883333 + Bristol 51.450000 -2.583333 + Butes 49.717500 -2.202500 + Exeter 50.700000 -3.533333 + Filton 51.508611 -2.574722 + Helston 50.099722 -5.272222 + Hugh Town 49.916667 -6.316667 + Lyneham 51.516667 -1.966667 + Middle Wallop 51.133333 -1.583333 + Plymouth / Roborough 50.416667 -4.116667 + Staverton 51.916667 -2.166667 + Yeovilton 51.004167 -2.650278 + Wales 51.500000 -3.200000 + Hawarden 53.183333 -3.033333 + Pembrey 51.689167 -4.277222 + Saint Athan 51.402778 -3.413889 + Valley 53.283333 -4.566667 + Vatican City Europe/Vatican + Aaland Islands Europe/Mariehamn + Mariehamn 60.100000 19.950000 +Middle East + Bahrain Asia/Bahrain + Al Hadd 26.245556 50.654167 + Manama 26.236111 50.583056 + Iran Asia/Tehran + Abadan 30.335 48.285 Ābādān + Abadeh 31.185 52.675 Ābādeh + Abhar 36.155 49.225 + Abyek 36.055 50.535 Ābyek + Ahar 38.475 47.065 + Ahvaz 31.285 48.725 Ahvāz,Nasir + Aliabad 36.915 54.875 'Alīābād + Aligudarz 33.375 49.725 Alīgūdarz,Aligudar + Amol 36.435 52.405 Āmol,Amul + Andimeshk 33.455 48.355 Andīmeshk,Andimish + Andisheh 35.705 51.005 Andīsheh + Arak 34.085 49.705 Arāk,Sultanaba + Aran va Bid Gol 34.065 51.485 Ārān va Bīd Gol,  + Ardabil 38.255 48.305 Ardebi,Ardabīl + Ardakan 32.325 54.015 Ardakān + Asadabad 34.795 48.115 Asadābād + Astara 38.435 48.855 Āstārā,  + Azadshahr 37.095 55.165 Azad Shah,Āzādshahr + Babol 36.535 52.705 Babu,Bābol + Babol Sar 36.715 52.645 Babul Sa,Bābol Sar + Baft 29.285 56.605 Bāft,  + Baharestan 32.475 51.805 Bahārestān + Bam 29.085 58.355 + Bandar-e Abbas 27.255 56.255 Bandar-e Abba,Bandar-e 'Abbās + Bandar-e Anzali 37.475 49.455 Bandar-e Pahlavī,Bandar-e Anzalī + Bandar-e Emam Khomeyni 30.435 49.085 Bandar-e Emam Khomeynī,Bandar-e Emam Khomeyn + Bandar-e-Gonaveh 29.575 50.525 Bandar-e-Gonāveh,Bandar-e Genave + Bandar-e Mahshahr 30.655 49.225 Bandar-e Māhshahr,Bandar-e Mahshah + Bandar-e Torkeman 36.885 54.075 + Baneh 35.985 45.925 Banê,Bāneh + Behbahan 30.585 50.275 Behbehān,Behbahān + Behshahr 36.725 53.555 + Bijar 35.875 47.605 Bījār,Bidja + Birjand 32.885 59.225 Bīrjand,Bîrcen + Bojnurd 37.475 57.325 Bojnur,Bojnūrd + Bonab 37.335 46.055 Bonāb,Benā + Borazjan 29.275 51.205 Borāzjān,Borazja + Borujen 31.975 51.295 Boruje,Borūjen + Borujerd 33.925 48.805 Burujir,Borūjerd + Bukan 36.535 46.205 Bokan,Būkān + Bumahen 35.735 51.865 Būmahen,  + Bushehr 28.925 50.835 Būshehr,Bandar-e Būsheh + Chah Bahar 25.305 60.635 Bandar Beheshti,Chāh Bahār + Chalus 36.665 51.415 Chālūs + Chenaran 36.655 59.105 Chenārān,  + Damavand 35.725 52.075 Damavan,Damāvand + Damghan 36.175 54.355 Dāmghān,  + Darab 28.755 54.545 Dārāb + Darcheh Piaz 32.625 51.555 Darcheh Pīāz + Deh Dasht 30.795 50.555 + Dezful 32.385 48.475 Desfu,Dezfūl + Dorud 33.495 49.055 Do Ru,Dorūd + Dow Gonbadan 30.365 50.785 Dow Gonbadān,Dow Gonbada + Eqlid 31.015 52.715 Eqlīd,Iqli + Esfahan 32.685 51.685 Isfahan,Eşfahān + Esfarayen 37.105 57.505 Esferaia,Esfarāyen + Eslamabad-e Gharb 34.325 47.125 Eslamaba,Eslāmābād-e Gharb + Eslamshahr 35.545 51.205 Eslamshah,Eslāmshahr + Fasa 28.975 53.685 Fasā + Felavarjan 32.575 51.495 Felāvarjān,Falavarja + Firuzabad 28.875 52.605 Fīrūzābād,Firuzaba + Fulad Shahr 32.435 51.385 Fūlād Shahr,Fooladshah + Garmsar 35.225 52.335 Qishlaq,Garmsār + Golpayegan 33.455 50.285 Golpayega,Golpāyegān + Gonbad-e Kavus 37.255 55.175 Gonbad-e Qabu,Gonbad-e Kāvūs + Gorgan 36.835 54.485 Astaraba,Gorgān + Hamadan 34.775 48.585 Hameda,Hamadān + Harsin 34.265 47.605 Hersîn,Harsīn + Hashtgerd 35.975 50.665 + Hashtpar 37.805 48.925 + Ilam 33.635 46.435 Īlām,Îla + Iranshahr 27.205 60.705 Īrānshahr,  + Izeh 31.805 49.905 Īzeh + Jahrom 28.555 53.575 + Javanrud 34.805 46.525 Javānrūd + Jiroft 28.675 57.735 Jīroft,Jirof + Kahnuj 27.875 57.705 Kehnu,Kahnūj + Kamal Shahr 35.865 50.875 Kamāl Shahr,  + Kamyaran 34.805 46.945 Kāmyārān,  + Kangavar 34.505 47.955 Kangāvar,Kongavar + Karaj 35.805 50.975 + Kashan 33.985 51.585 Kāshān,Kachan + Kashmar 35.185 58.455 Kāshmar,Turshi + Kazerun 29.605 51.675 Kāzerūn + Kerman 30.305 57.085 Kirman,Kermān + Kermanshah 34.385 47.065 Kermānshāh,Bakhtara + Khalkhal 37.635 48.525 Khalkhāl,Xelxa + Khash 28.225 61.235 Vāsht,Khāsh + Khomeynishahr 32.705 51.475 Khomeynishah,Khomeynīshahr + Khorramabad 33.485 48.355 Khurramabad,Khorramābād + Khorramdareh 36.205 49.185 + Khorramshahr 30.435 48.185 + Khowmeyn 33.635 50.055 + Khvorasgan 32.655 51.755 Khvorāsgān,Khvoresga + Khvoy 38.535 44.975 + Kuhdasht 33.535 47.605 Kūhdasht + Lahijan 37.205 50.005 Lāhījān + Langerud 37.185 50.155 Langaru,Langerūd + Lar 27.685 54.285 Lār + Mahabad 36.775 45.725 Mahābād,Mehaba + Mahdasht 35.725 50.825 Mardaba,Māhdāsht + Maku 39.305 44.505 Mako,Mākū + Malard 35.675 50.995 Malārd,  + Malayer 34.325 48.855 Dowlataba,Malāyer + Maragheh 37.425 46.225 Maraghe,Marāgheh + Marand 38.435 45.775 + Marivan 35.455 46.205 Merîva,Marīvān + Marv Dasht 29.805 52.835 + Mashhad 36.275 59.575 + Masjed-e Soleyman 31.985 49.305 Masjed-e Soleymān,Masjed-e Soleyma + Meshgin Shahr 38.385 47.685 Meshgīn Shahr + Meshkin Dasht 35.755 50.945 Meshkīn Dasht + Meybod 32.235 54.015 + Mianduab 36.975 46.105 Miandowa,Mīāndūāb + Mianeh 37.335 47.705 Mīāneh,Miane + Minab 27.155 57.075 Mīnāb,  + Mobarakeh 32.495 51.665 Mobārakeh + Nahavand 34.205 48.375 Nehaven,Nahāvand + Najafabad 32.675 51.355 Nejafaba,Najafābād + Naqadeh 36.955 45.375 + Naz̨arabad 35.955 50.605 Nazarabad-e Bozorg,Naz̨arābād + Neka 36.665 53.295 Nekā + Neyriz 29.205 54.335 Neiri,Neyrīz + Neyshabur 36.225 58.825 Nayshabu,Neyshābūr + Nurabad 34.085 47.975 Nūrābād + Nurabad 30.115 51.535 Mamasan,Nūrābād + Nushahr 36.655 51.555 Nūshahr,Now Shah + Omidiyeh 30.755 49.715 Omidiye,Omīdīyeh + Orumiyeh 37.535 45.005 Orumiye,Orūmīyeh + Pakdasht 35.475 51.705 Pākdasht + Parsabad 39.655 47.935 Pārsābād,  + Piranshahr 36.705 45.135 Piramşar,Pīrānshahr + Pishva 35.305 51.735 Pīshvā + Qaemshahr 36.475 52.875 Qā'emshahr,Qaemshah + Qarchak 35.425 51.585 + Qazvin 36.275 50.005 Qazvi,Qazvīn + Qods 35.735 51.185 + Qom 34.655 50.955 + Qorveh 35.175 47.805 + Quchan 37.125 58.505 Qūchān,Qucha + Rafsanjan 30.425 56.025 Rafsanjān,Bahramabad + Ramhormoz 31.275 49.625 Rāmhormoz,Ram Hormuz + Resht 37.305 49.635 + Robaţ Karim 35.485 51.085 Robāţ Karīm,Robat Kari + Sabzevar 36.225 57.635 Sabzevār,Sabzawa + Salmas 38.185 44.755 Salmās,Selma + Sanandaj 35.305 47.025 + Saqqez 36.235 46.285 + Sarab 37.955 47.575 Sarāb,  + Saravan 27.405 62.585 Sarāvān + Sar Dasht 36.155 45.535 + Sari 36.555 53.105 Sārī + Saveh 35.025 50.335 Sāveh,Sav + Semnan 35.555 53.385 Semnān,Semma + Shadegan 30.655 48.675 Shādegān,  + Shahin Shahr 32.815 51.545 Shāhīn Shahr,Shahin Shah + Shahrak-e Golestan 29.265 51.235 Shahrak-e Golestān + Shahr-e Babak 30.135 55.155 Shahrbabak,Shahr-e Bābak + Shahr-e Kord 32.325 50.855 + Shahreza 32.025 51.875 Shahrezā,Qomshe + Shahriar 35.665 51.065 Shahrīār + Shahrud 36.425 54.975 Shahru,Shāhrūd + Shiravan 37.455 57.925 Şêrva,Shīravān + Shiraz 29.635 52.575 Shīrāz + Shush 32.195 48.245 Shūsh + Shushtar 32.055 48.835 Shushter,Shūshtar + Sirjan 29.475 55.735 Sīrjān,Saidaba + Sonqor 34.785 47.605 + Sowmaeh Sara 37.285 49.325 Sowmaeh Sar,Şowma'eh Sarā + Susangerd 31.555 48.175 Dasht-e Azadega,Sūsangerd + Tabriz 38.085 46.305 Tabrīz,Tebri + Takab 36.415 47.105 Tîkab,Takāb + Takestan 36.075 49.705 Tākestān,Takista + Taybad 34.745 60.775 Ţāyyebā,Tāybād + Tehran 35.675 51.435 Tehrān,Tehera + Tonekabon 36.825 50.885 Tonekābon,Tonkabo + Torbat-e H̨eydariyeh 35.285 59.225 Torbat-e Heydariye,Torbat-e H̨eydarīyeh + Torbat-e Jam 35.225 60.625 Torbat Ja,Torbat-e Jām + Tuyserkan 34.555 48.445 Tuysarka,Tūyserkān + Varamin 35.325 51.655 Varāmīn + Yasuj 30.825 51.685 Boir Ahma,Yāsūj + Yazd 31.925 54.375 + Zabol 31.025 61.485 Zabu,Zābol + Zahedan 29.505 60.835 Zāhedān,Zahida + Zanjan 36.675 48.505 Zanjān,Zinja + Zarand 30.805 56.585 + Zarrin Shahr 32.455 51.595 Zarrīn Shahr,Lanja + Iraq Asia/Baghdad + Israel Asia/Jerusalem + Elat (Eilat) 29.561111 34.951667 + Tel Aviv 32.066667 34.766667 + Mahanayim 32.983333 35.566667 + Mahane Israel 32.000000 34.916667 + Ramot Remez 32.781667 35.016667 + Shizzafon 30.041667 35.027778 + Al 'Aqabah 29.526667 35.007778 + Al Jizah 31.700000 35.950000 + Jordan + Amman 31.950000 35.933333 + Kuwait Asia/Kuwait + Kuwait International Airport 29.216667 47.983333 + Lebanon Asia/Beirut + Beirut 33.871944 35.509722 + Oman Asia/Muscat + Mu'askar al Murtafi'ah 23.563611 58.250000 + Muscat 23.613333 58.593333 + Salalah 17.017500 54.082778 + Palestine Asia/Gaza + Qatar Asia/Qatar + Doha 25.286667 51.533333 + Saudi Arabia Asia/Riyadh + 'Ar'ar 30.985000 41.020556 + Abha 18.216389 42.505278 + Ad Dalfa'ah 26.326667 43.676389 + Ad Dammam 26.425833 50.114167 + Al 'Aqiq 20.266667 41.666667 + Al Qaysumah 28.309722 46.127500 + Al Qurayyat 31.333333 37.341944 + Al Wajh 26.233333 36.466667 + Al Wuday'ah 17.033333 47.116667 + Ar Ruqayyiqah 25.350000 49.566667 + At Ta'if 21.270278 40.415833 + Dhahran 26.304167 50.132500 + Ha'il 27.516389 41.696944 + Jiddah 21.516944 39.219167 + Jizan 16.889167 42.551111 + Khamis Mushayt 18.306389 42.729167 + Masjid Ibn Rashid 27.916667 45.400000 + Mecca 21.426667 39.826111 + Medina 24.468611 39.614167 + Najran 17.505556 44.184167 + Qal'at Bishah 20.000000 42.600000 + Qara 29.876944 40.226944 + Rafha 29.638611 43.501389 + Riyadh 24.640833 46.772778 + Tabuk 28.383333 36.583333 + Tamrah 20.398333 45.415000 + Turayf 31.677500 38.653056 + Yanbu' al Bahr 24.085278 38.048611 + Syria Asia/Damascus + Al Qamishli 37.049722 41.226389 + Aleppo 36.202778 37.158611 + Damascus 33.500000 36.300000 + Dayr az Zawr 35.333333 40.150000 + Latakia 35.516667 35.783333 + United Arab Emirates Asia/Dubai + Abu Dhabi 24.466667 54.366667 + Al 'Ayn 24.216667 55.766667 + Al Fujayrah 25.123056 56.337500 + Dubai 25.252222 55.280000 + Ra's al Khaymah 25.791111 55.942778 + Sharjah 25.362222 55.391111 + Yemen Asia/Aden + 'Adan 12.779444 45.036667 + 'Ataq 14.550000 46.800000 + Al Hudaydah 14.797778 42.952222 + Ma'rib 15.416667 45.350000 + Mori 12.633333 53.900000 + Sa'dah 16.935833 43.764444 + Sanaa 15.354722 44.206667 + Say'un 15.943333 48.793333 + Ta'izz 13.566667 44.033333 +North America + Canada America/Edmonton + Banff 51.166667 -115.566667 + Bergen 51.700000 -114.633333 + Bow Island 49.866667 -111.366667 + Brooks 50.566667 -111.900000 + Calgary 51.083333 -114.083333 + Cardston 49.200000 -113.316667 + Claresholm 50.033333 -113.583333 + Cold Lake 54.465000 -110.183056 + Coleman 49.633333 -114.500000 + Coronation 52.083333 -111.450000 + Drumheller 51.466667 -112.700000 + Edmonton 53.550000 -113.500000 + Edson 53.583333 -116.416667 + Embarras Portage 58.450000 -111.500000 + Esther 51.683333 -110.250000 + Fort Chipewyan 58.716667 -111.150000 + Fort McMurray 56.733333 -111.383333 + Grande Prairie 55.166667 -118.800000 + High Level 58.516667 -117.133333 + Jasper Warden Automated Reporting Station 52.933333 -118.316667 + Lac La Biche 54.771944 -111.964722 + Lethbridge 49.700000 -112.833333 + Medicine Hat 50.033333 -110.683333 + Milk River 49.133333 -112.083333 + Onefour 49.066667 -110.450000 + Peace River 56.233333 -117.283333 + Pincher Creek 49.483333 -113.950000 + Red Deer 52.266667 -113.800000 + Rocky Mountain House 52.366667 -114.916667 + Seebe 51.100000 -115.066667 + Slave Lake 55.283333 -114.783333 + Spirit River 55.783333 -118.833333 + Three Hills 51.700000 -113.266667 + Vegreville 53.500000 -112.050000 + Whitecourt 54.133333 -115.683333 + British Columbia 49.050000 -122.300000 + Agassiz 49.233333 -121.766667 + Allison Harbour 51.033333 -127.516667 + Alta Lake 50.116667 -122.983333 + Baldonnel 56.216667 -120.683333 + Bella Coola 52.366667 -126.750000 + Blue River 52.100000 -119.300000 + Boat Basin 49.483333 -126.400000 + Burns Lake 54.216667 -125.766667 + Campbell River 50.016667 -125.250000 + Castlegar 49.316667 -117.666667 + Clinton 51.150000 -121.500000 + Comox 49.683333 -124.933333 + Cranbrook 49.516667 -115.766667 + Creston Automatic Weather Reporting System 49.083333 -116.500000 + Dease Lake 58.450000 -130.033333 + Esquimalt 48.433333 -123.416667 + Fort Grahame 56.600000 -124.633333 + Fort Nelson 58.816667 -122.533333 + Gabriola 49.183333 -123.850000 + Golden 51.300000 -116.966667 + Hollyburn 49.333333 -123.166667 + Hope 49.383333 -121.433333 + Kamloops 50.666667 -120.333333 + Kelowna 49.900000 -119.483333 + Lasqueti 49.483333 -124.350000 + Little Prairie 55.700000 -121.616667 + Lytton 50.233333 -121.566667 + McLeod Lake 55.000000 -123.033333 + Nakusp 50.233333 -117.800000 + Nanaimo 49.150000 -123.916667 + Nanoose Bay 49.250000 -124.183333 + Sparwood, Sparwood-Elk Valley Airport 49.750000 -114.883333 + Nelson 49.483333 -117.283333 + North Kamloops 50.683333 -120.366667 + Ocean Falls 52.350000 -127.700000 + Osoyoos 49.033333 -119.466667 + Penticton 49.500000 -119.583333 + Pitt Meadows 49.233333 -122.683333 + Port Hardy 50.716667 -127.500000 + Port Simpson 54.550000 -130.416667 + Powell River 49.883333 -124.550000 + Prince George 53.916667 -122.766667 + Prince Rupert 54.316667 -130.333333 + Princeton Airport 49.466667 -120.516667 + Queen Charlotte 53.250000 -132.083333 + Quesnel 52.983333 -122.483333 + Revelstoke 51.000000 -118.183333 + Rocky Point 48.316667 -123.550000 + Salmon Arm 50.700000 -119.283333 + Sandspit 53.250000 -131.833333 + Smithers 54.766667 -127.166667 + Squamish 49.700000 -123.150000 + Stephen 51.450000 -116.300000 + Steveston 49.133333 -123.183333 + Stewart 55.933333 -130.000000 + Summerland 49.600000 -119.666667 + Terrace 54.500000 -128.583333 + Tofino 49.133333 -125.900000 + Tow Hill 54.066667 -131.783333 + Trout Lake 53.816667 -89.883333 + Vancouver International Airport 49.183333 -123.166667 + Vernon 50.233333 -119.300000 + Victoria Harbour 48.416667 -123.333333 + White Rock 49.033333 -122.816667 + Williams Lake 52.116667 -122.150000 + Winter Harbour 50.516667 -128.033333 + Manitoba 52.366667 -97.033333 + Brandon 49.833333 -99.950000 + Carman 49.500000 -98.000000 + Churchill 58.766667 -94.166667 + Dauphin 51.150000 -100.050000 + Flin Flon 54.766667 -101.883333 + Gillam 56.350000 -94.700000 + Gimli 50.633333 -97.000000 + Grand Rapids 53.183333 -99.266667 + Island Lake 53.966667 -94.766667 + Lynn Lake 56.850000 -101.050000 + Deerwood 49.400000 -98.316667 + Morden 49.183333 -98.100000 + Norway House 53.966667 -97.833333 + Pilot Mound 49.200000 -98.900000 + Swan River 52.100000 -101.266667 + The Pas 53.816667 -101.233333 + Thompson 55.750000 -97.866667 + Victoria Beach 50.700000 -96.550000 + Winnipeg 49.883333 -97.166667 + New Brunswick 47.600000 -65.650000 + Dipper Harbour 45.083333 -66.416667 + Fredericton 45.950000 -66.633333 + Gagetown 45.766667 -66.150000 + Moncton 46.083333 -64.766667 + Saint John 45.266667 -66.066667 + Saint Leonard 47.166667 -67.916667 + Saint Stephen 45.200000 -67.266667 + Newfoundland & Labrador 47.300000 -54.000000 + Cape Race 46.666667 -53.083333 + Cartwright 53.700000 -57.016667 + Deer Lake 49.183333 -57.433333 + Englee 50.733333 -56.100000 + Ferolle Point 51.016667 -57.100000 + Gander 48.950000 -54.550000 + Goose Bay 53.333333 -60.416667 + Harbour Breton 47.483333 -55.833333 + Saglek Bay 58.333333 -62.583333 + Hopedale 55.450000 -60.216667 + Makkovik 55.083333 -59.183333 + Mary's Harbour 52.300000 -55.833333 + Mount Pearl Park 47.516667 -52.783333 + Nain 56.550000 -61.683333 + Neddy Harbour 49.533333 -57.866667 + Nutak 57.466667 -61.833333 + Saint Anthony 51.383333 -55.600000 + St. John's 47.616667 -52.733333 + Stephenville 48.533333 -58.550000 + Terra Nova 48.500000 -54.216667 + Twillingate 49.650000 -54.750000 + Wabush 52.916667 -66.866667 + Northwest Territories 68.216667 -135.000000 + Cape Parry 70.166667 -124.666667 + Délįne 65.183333 -123.416667 + Fort Good Hope 66.266667 -128.633333 + Fort Providence 61.366667 -117.650000 + Fort Simpson 61.850000 -121.333333 + Fort Smith 60.016667 -111.950000 + Hay River 60.850000 -115.700000 + Holman 70.733333 -117.750000 + Inuvik 68.350000 -133.700000 + Nahanni Butte 61.050000 -123.366667 + Norman Wells 65.283333 -126.850000 + Paulatuk 69.383333 -123.983333 + Sachs Harbour 71.983333 -125.200000 + Tuktoyaktuk 69.450000 -133.066667 + Tununuk 69.000000 -134.666667 + Wha Ti 63.133333 -117.266667 + Yellowknife 62.450000 -114.350000 + Nova Scotia 47.000000 -60.466667 + Beaver Harbour 44.900000 -62.416667 + Caledonia 44.366667 -65.033333 + Canso 45.350000 -61.000000 + Caribou Island 45.750000 -62.733333 + Chéticamp 46.616667 -61.016667 + Dingwall 46.900000 -60.466667 + Grand-Etang 46.550000 -61.033333 + Greenwood 44.983333 -64.916667 + Halifax 44.650000 -63.600000 + Kentville 45.066667 -64.500000 + Western Head 43.983333 -64.666667 + Sheet Harbour 44.933333 -62.533333 + Sydney 46.166667 -60.050000 + Westport 44.266667 -66.350000 + Yarmouth 43.833333 -66.116667 + Nunavut 61.116667 -94.050000 + Baker Lake 64.316667 -96.016667 + Cambridge Bay 69.116667 -105.033333 + Cape Dorset 64.233333 -76.550000 + Cape Dyer 66.666667 -61.366667 + Chesterfield Inlet 63.333333 -90.700000 + Clyde River 70.450000 -68.566667 + Coral Harbour 64.133333 -83.166667 + Ennadai 61.133333 -100.883333 + Eureka 79.983333 -85.933333 + Gjoa Haven 68.633333 -95.916667 + Hall Beach 68.766667 -81.200000 + Igloolik 69.400000 -81.800000 + Iqaluit 63.733333 -68.500000 + Kugaaruk 68.533333 -89.816667 + Kugluktuk 67.833333 -115.083333 + Pangnirtung 66.133333 -65.750000 + Pond Inlet 72.700000 -78.000000 + Qikiqtarjuaq 67.550000 -64.033333 + Rankin Inlet 62.816667 -92.083333 + Repulse Bay 66.516667 -86.233333 + Resolute 74.683333 -94.900000 + Taloyoak 69.533333 -93.533333 + Bancroft 45.050000 -77.850000 + Beardmore 49.600000 -87.950000 + Borden 44.283333 -79.916667 + Burlington Piers 43.300000 -79.800000 + Central Patricia 51.483333 -90.150000 + Chapleau 47.833333 -83.400000 + Cobourg 43.966667 -78.166667 + Collingwood 44.500000 -80.216667 + Coppell 49.533333 -83.833333 + Delhi 42.850000 -80.500000 + Dryden Airport 49.833333 -92.750000 + Earlton 47.716667 -79.816667 + Elliot Lake 46.383333 -82.650000 + Erieau 42.250000 -81.916667 + Front of Escott 44.433333 -75.950000 + Geraldton 49.716667 -86.966667 + Goderich 43.733333 -81.700000 + Gore Bay 45.916667 -82.466667 + Hallowell 44.000000 -77.233333 + Hamilton Airport 43.166667 -79.933333 + Heron Bay 48.650000 -86.283333 + Kapuskasing 49.416667 -82.433333 + Kenora 49.766667 -94.466667 + Kingston 44.216667 -76.600000 + London 43.033333 -81.150000 + Moosonee 51.266667 -80.650000 + Mount Forest 43.966667 -80.733333 + Muskoka Falls 45.000000 -79.300000 + Nanticoke 42.800000 -80.066667 + North Bay 46.300000 -79.450000 + Ottawa 45.416667 -75.700000 + Petawawa 45.900000 -77.283333 + Peterborough 44.300000 -78.333333 + Port Weller 43.216667 -79.216667 + Red Lake 51.033333 -93.833333 + Saint Catharines 43.166667 -79.233333 + Sarnia 42.966667 -82.400000 + Sault Sainte Marie 46.500000 -84.333333 + Sioux Lookout 50.100000 -91.916667 + Sudbury 46.500000 -80.966667 + Thunder Bay 48.400000 -89.233333 + Timmins 48.466667 -81.333333 + Tobermory 45.250000 -81.666667 + Toronto 43.666667 -79.416667 + Trenton 44.116667 -77.533333 + Upsala 49.050000 -90.466667 + Waterloo Well 43.466667 -80.383333 + Wawa 48.000000 -84.783333 + Whitefish Falls 46.100000 -81.716667 + Wiarton 44.733333 -81.133333 + Windsor 42.333333 -83.033333 + Prince Edward Island 46.233333 -63.133333 + East Point 46.450000 -61.966667 + Summerside 46.400000 -63.783333 + Tignish 46.950000 -64.033333 + Amqui 48.466667 -67.433333 + Bagotville 48.350000 -70.883333 + Baie-Comeau 49.216667 -68.150000 + Baie-Sainte-Catherine 48.100000 -69.733333 + Baie-de-la-Trinité 49.416667 -67.316667 + Beauceville 46.200000 -70.766667 + Beauport 46.850000 -71.183333 + Frelighsburg 45.050000 -72.833333 + Bellin 60.016667 -70.033333 + Blanc-Sablon 51.433333 -57.116667 + Canton-Bégin 48.683333 -71.366667 + Cap-Chat 49.083333 -66.683333 + Cap-aux-Meules 47.383333 -61.866667 + Cape Cove 48.433333 -64.333333 + Chibougamau 49.866667 -74.350000 + Fort-Rupert 51.483333 -78.766667 + Gaspé 48.833333 -64.483333 + Gatineau 45.483333 -75.650000 + Harrington Harbour 50.516667 -59.483333 + Havre-Saint-Pierre 50.250000 -63.583333 + Inoucdjouac 58.450000 -78.150000 + Ivugivik 62.416667 -77.900000 + Jacques-Cartier 45.533333 -73.483333 + Jonquière 48.416667 -71.250000 + Koartac 61.033333 -69.616667 + Kuujjuaq 58.100000 -68.400000 + L'Anse-Saint-Jean 48.233333 -70.183333 + L'Ascension 48.683333 -71.666667 + L'Assomption 45.816667 -73.433333 + La Baie 48.333333 -70.866667 + La Tuque 47.450000 -72.783333 + Leaf River 58.766667 -69.116667 + Lennoxville 45.366667 -71.866667 + Longue-Pointe-de-Mingan 50.266667 -64.150000 + Maniwaki 46.366667 -75.966667 + Maricourt 61.600000 -71.950000 + Matagami 49.750000 -77.633333 + Mont-Apica 47.983333 -71.433333 + Mont-Joli 48.583333 -68.183333 + Montmagny 46.966667 -70.550000 + Montreal 45.500000 -73.583333 + Natashquan 50.183333 -61.816667 + New Carlisle 48.000000 -65.333333 + Nicolet 46.216667 -72.600000 + Normandin 48.833333 -72.516667 + Notre-Dame-de-la-Salette 45.766667 -75.583333 + Nouveau-Comptoir 53.000000 -78.816667 + Parent 47.916667 -74.616667 + Petite-Rivière 47.316667 -70.566667 + Port-Menier 49.816667 -64.350000 + Portneuf 46.683333 -71.883333 + Poste-de-la-Baleine 55.266667 -77.783333 + Puvirnituq 60.033333 -77.283333 + Quebec 46.800000 -71.383333 + Radisson 53.783333 -77.616667 + Rivière-du-Loup 47.833333 -69.533333 + Rivière-la-Madeleine 49.233333 -65.316667 + Roberval 48.516667 -72.216667 + Rouyn 48.250000 -79.033333 + Saint-Anicet 45.133333 -74.350000 + Saint-Chrysostome 45.100000 -73.750000 + Saint-Fabien 48.283333 -68.866667 + Saint-François 47.000000 -70.816667 + Saint-Henri-de-Taillon 48.666667 -71.816667 + Saint-Jean 45.300000 -73.250000 + Saint-Joachim 47.050000 -70.850000 + Saint-Jovite 46.116667 -74.583333 + Sainte-Anne-de-la-Pocatière 47.366667 -70.033333 + Schefferville 54.800000 -66.816667 + Senneville 45.433333 -73.966667 + Sept-Îles 50.200000 -66.383333 + Shawinigan 46.550000 -72.733333 + Sherbrooke 45.400000 -71.900000 + Stoneham 46.983333 -71.366667 + Trois-Rivières 46.350000 -72.550000 + Val-d'Or 48.116667 -77.766667 + Valcartier Station 46.883333 -71.516667 + Varennes 45.683333 -73.433333 + Saskatchewan 49.616667 -105.983333 + Broadview 50.383333 -102.566667 + Buffalo Narrows 55.854167 -108.484167 + East Poplar 49.066667 -105.383333 + Eastend 49.516667 -108.816667 + Estevan 49.150000 -103.000000 + Kindersley 51.466667 -109.133333 + La Ronge 55.100000 -105.300000 + Leader 50.883333 -109.550000 + Lloydminster 53.283333 -110.000000 + Lucky Lake 50.983333 -107.133333 + Maple Creek 49.916667 -109.466667 + Meadow Lake 54.129722 -108.434722 + Melfort 52.866667 -104.600000 + Nipawin 53.366667 -104.016667 + North Battleford 52.766667 -108.283333 + Prince Albert 53.200000 -105.750000 + Regina 50.450000 -104.616667 + Rockglen 49.183333 -105.950000 + Rosetown 51.550000 -107.983333 + Saskatoon 52.133333 -106.666667 + Southend Automatic Weather Reporting System 56.333333 -103.283333 + Spiritwood 53.366667 -107.516667 + Stony Rapids 59.266667 -105.833333 + Swift Current 50.283333 -107.766667 + Uranium City 59.566667 -108.616667 + Val Marie 49.233333 -107.733333 + Watrous 51.666667 -105.466667 + Weyburn 49.666667 -103.850000 + Wynyard 51.766667 -104.183333 + Yorkton 51.216667 -102.466667 + Yukon Territory 61.350000 -139.000000 + Carmacks 62.100000 -136.316667 + Dawson 64.066667 -139.416667 + Haines Junction 60.750000 -137.500000 + Mayo 63.600000 -135.916667 + Old Crow 67.566667 -139.800000 + Shingle Point 69.000000 -137.366667 + Snag 62.383333 -140.366667 + Teslin 60.166667 -132.700000 + Watson Lake 60.116667 -128.800000 + Whitehorse 60.716667 -135.050000 + Mexico America/Mexico_City + Aguascalientes 21.883333 -102.300000 + Baja California 32.651944 -115.468333 + Tijuana 32.533333 -117.016667 + Baja California Sur 24.166667 -110.300000 + Loreto 26.016667 -111.350000 + San José del Cabo 23.050000 -109.683333 + Campeche 19.850000 -90.550000 + Carmen 18.633333 -91.833333 + Chiapas 14.900000 -92.283333 + Tuxtla 16.750000 -93.116667 + Chihuahua International Airport 28.700000 -105.966667 + Coahuila 26.900000 -101.416667 + Piedras Negras 28.700000 -100.516667 + Saltillo 25.416667 -101.000000 + Torreón 25.550000 -103.433333 + Colima 19.266667 -103.583333 + Manzanillo International Airport 19.150000 -104.566667 + Mexico City 19.434167 -99.138611 + Durango Airport 24.133333 -104.533333 + Guanajuato 21.116667 -101.666667 + Guerrero 16.850000 -99.916667 + Ixtapa 17.666667 -101.650000 + Hidalgo 20.666667 -103.333333 + Puerto Vallarta 20.616667 -105.250000 + Michoacán 19.700000 -101.116667 + Uruapan 19.416667 -102.066667 + Morelos 18.916667 -99.250000 + México 31.733333 -106.483333 + Toluca 19.288333 -99.667222 + Nayarit 25.666667 -100.316667 + El Zapote 15.771111 -96.263056 + Ixtepec 16.578056 -95.101389 + Laguna Tepic 21.516667 -104.883333 + Xoxocotlan Airport 16.966667 -96.733333 + Puerto Escondido 15.850000 -97.066667 + Puebla 19.050000 -98.166667 + Querétaro 20.600000 -100.383333 + Quintana Roo 21.166667 -86.833333 + Chetumal 18.500000 -88.300000 + Cozumel 20.508333 -86.945833 + San Luis Potosí 22.150000 -100.983333 + Sinaloa 24.799444 -107.389722 + Los Mochis 25.766667 -108.966667 + Mazatlán 23.216667 -106.416667 + Ciudad Obregón 27.483333 -109.933333 + Guaymas 27.933333 -110.900000 + Hermosillo 29.066667 -110.966667 + Tabasco 17.983333 -92.916667 + Tamaulipas 23.733333 -99.133333 + Matamoros 25.883333 -97.500000 + Nuevo Laredo 27.500000 -99.516667 + Reynosa 26.083333 -98.283333 + Tampico 22.300000 -97.850000 + Tlaxcala 17.983333 -94.516667 + Poza Rica de Hidalgo 20.550000 -97.450000 + Veracruz / Las Bajadas, General Heriberto Jara Airport 19.150000 -96.183333 + Yucatán 20.666667 -88.566667 + Manuel Crecencio Airport 20.933333 -89.650000 + Zacatecas Airport 22.900000 -102.683333 + Saint Pierre & Miquelon America/Miquelon + Saint-Pierre 46.766667 -56.166667 + United States America/Chicago + Alabaster 33.244281 -86.816377 + Albertville 34.267594 -86.208867 + Alexander City 32.944012 -85.953853 + Andalusia 31.308504 -86.483291 + Anniston 33.659826 -85.831632 + Auburn-Opelika Airport 32.616667 -85.433333 + Birmingham International Airport 33.565556 -86.745000 + Cullman 34.174821 -86.843612 + Daleville 31.310172 -85.712991 + Pryor Field 34.658056 -86.943333 + Dothan 31.223231 -85.390489 + Evergreen 31.433499 -86.956918 + Fort Payne 34.444255 -85.719689 + Gadsden 34.014264 -86.006639 + Haleyville 34.226488 -87.621413 + Madison County Executive Airport 34.861389 -86.557222 + Mobile 30.694357 -88.043054 + Maxwell Air Force Base / Montgomery 32.383333 -86.366667 + Muscle Shoals 34.744811 -87.667529 + Ozark 31.459058 -85.640493 + Troy Municipal Airport 31.860556 -86.012222 + Tuscaloosa 33.209841 -87.569174 + Alaska 51.880000 -176.658056 + Deadhorse, Alpine Airstrip 70.333333 -150.933333 + Ambler 67.086111 -157.851389 + Anaktuvuk Pass 68.143333 -151.735833 + Anchorage 61.218056 -149.900278 + Angoon 57.503333 -134.583889 + Aniak 61.578333 -159.522222 + Annette 55.061667 -131.541667 + Anvik 62.656111 -160.206667 + Arctic Village 68.126944 -145.537778 + Barrow 71.290556 -156.788611 + Bethel 60.792222 -161.755833 + Bettles 66.918889 -151.516111 + Birchwood 61.405278 -149.468889 + Buckland 65.979722 -161.123056 + Chandalar 67.505278 -148.493611 + Chignik 56.295278 -158.402222 + Chisana 62.066111 -142.040833 + Chistochina 62.565000 -144.664722 + Chulitna 62.886944 -149.583056 + Cold Bay 55.185833 -162.721111 + Cordova 60.542778 -145.757500 + Deadhorse 70.205556 -148.511667 + Delta Junction 64.037778 -145.732222 + Dillingham 59.039722 -158.457500 + Eagle Airport 64.776389 -141.150833 + Egegik 58.215556 -157.375833 + Elfin Cove 58.194444 -136.343333 + Emmonak 62.777778 -164.523056 + Eureka Roadhouse 61.938611 -147.168056 + Fairbanks 64.837778 -147.716389 + Fort Yukon 66.564722 -145.273889 + Galena 64.733333 -156.927500 + Gambell 63.779722 -171.741111 + Gulkana 62.271389 -145.382222 + Gustavus 58.413333 -135.736944 + Haines 59.235833 -135.445000 + Healy 63.856944 -148.966111 + Homer 59.642500 -151.548333 + Hoonah 58.110000 -135.443611 + Hooper Bay 61.531111 -166.096667 + Huslia 65.698611 -156.399722 + Hydaburg 55.208056 -132.826667 + Juneau International Airport 58.354722 -134.576111 + Kake 56.975833 -133.947222 + Kaktovik 70.131944 -143.623889 + Kaltag 64.327222 -158.721944 + Kenai 60.554444 -151.258333 + Ketchikan 55.342222 -131.646111 + King Salmon 58.688333 -156.661389 + Kipnuk 59.938889 -164.041389 + Kivalina 67.726944 -164.533333 + Klawock 55.552222 -133.095833 + Kodiak 57.790000 -152.407222 + Kotzebue 66.898333 -162.596667 + Koyuk 64.931944 -161.156944 + Kustatan 60.715833 -151.747500 + Lake Minchumina 63.882778 -152.312222 + Lime Village 61.356389 -155.435556 + Manley Hot Springs 65.001111 -150.633889 + McCarthy 61.433333 -142.921667 + McGrath 62.956389 -155.595833 + McKinley Park 63.732778 -148.914167 + Mekoryuk 60.388056 -166.185000 + Metlakatla 55.129167 -131.572222 + Nabesna 62.371944 -143.008611 + Nenana 64.563889 -149.093056 + Newhalen 59.720000 -154.897222 + Noatak 67.571111 -162.965278 + Nome 64.501111 -165.406389 + Northway 62.961667 -141.937222 + Nuiqsut 70.217500 -150.976389 + Palmer 61.599722 -149.112778 + Paxson 63.089722 -145.613056 + Petersburg 56.816667 -132.966667 + Platinum 59.013056 -161.816389 + Point Hope 68.347778 -166.808056 + Point Lay 69.757500 -163.051111 + Port Alexander 56.249722 -134.644444 + Port Alsworth 60.202500 -154.312778 + Port Heiden 56.949167 -158.626944 + St. George, St. George Airport 56.578611 -169.661389 + Saint Marys 62.053056 -163.165833 + Saint Paul 57.122222 -170.275000 + Sand Point 55.339722 -160.497222 + Savoonga 63.694167 -170.478889 + Scammon Bay 61.842778 -165.581667 + Selawik 66.603889 -160.006944 + Seldovia 59.438056 -151.711389 + Seward 60.104167 -149.442222 + Shishmaref 66.256667 -166.071944 + Sitka 57.053056 -135.330000 + Skagway 59.458333 -135.313889 + Sleetmute 61.702500 -157.169722 + Soldotna 60.487778 -151.058333 + Sutton 61.716667 -148.883333 + Takotna 62.988611 -156.064167 + Talkeetna 62.323889 -150.109444 + Tanana 65.171944 -152.078889 + Tin City 65.558611 -167.948056 + Togiak 59.061944 -160.376389 + Unalakleet 63.873056 -160.788056 + Unalaska 53.873611 -166.536667 + Valdez 61.130833 -146.348333 + Wainwright 70.636944 -160.038333 + Wasilla 61.581389 -149.439444 + Whittier 60.773056 -148.683889 + Willow 61.747222 -150.037500 + Wrangell 56.470833 -132.376667 + Yakutat 59.546944 -139.727222 + Arizona 35.147777 -114.568298 + Casa Grande 32.879502 -111.757352 + Chandler Municipal Airport 33.269167 -111.811111 + Childs 32.452835 -112.843488 + Douglas Bisbee, Bisbee Douglas International Airport 31.469167 -109.603611 + Flagstaff 35.198067 -111.651273 + Gilbert 33.352826 -111.789027 + Glendale Municipal Airport 33.527222 -112.295278 + Goodyear 33.435320 -112.358214 + Grand Canyon 36.054427 -112.139336 + Kingman 35.189443 -114.053006 + Mesa 33.422269 -111.822640 + Nogales 31.340377 -110.934253 + Page 36.914722 -111.455833 + Glendale, Glendale Municipal Airport 33.527222 -112.295278 + Phoenix 33.448377 -112.074037 + Prescott 34.540024 -112.468502 + Safford 32.833955 -109.707580 + Saint Johns 34.505870 -109.360933 + Scottsdale 33.509210 -111.899033 + Show Low 34.254208 -110.029833 + Sierra Vista 31.554539 -110.303691 + Tempe 33.414768 -111.909310 + Tucson 32.221743 -110.926479 + Window Rock 35.680573 -109.052593 + Winslow 35.024187 -110.697357 + Yuma 32.725325 -114.624397 + Arkansas 34.120929 -93.053784 + Batesville 35.769799 -91.640972 + Bentonville 36.372854 -94.208817 + Blytheville 35.927295 -89.918975 + Camden 33.584558 -92.834329 + De Queen 34.037892 -94.341317 + El Dorado 33.207630 -92.666267 + Drake Field 36.010278 -94.167778 + Flippin 36.278958 -92.597108 + Fort Smith Regional Airport 35.333611 -94.365000 + Harrison 36.229794 -93.107676 + Memorial Field Airport 34.478056 -93.096111 + Jonesboro 35.842297 -90.704279 + Little Rock 34.746481 -92.289595 + Mena 34.586217 -94.239655 + Monticello Municipal Airport / Ellis Field 33.638333 -91.751111 + Mount Ida 34.556764 -93.634081 + Baxter County Regional Airport 36.370556 -92.471944 + Newport Municipal Airport 35.637500 -91.176111 + Pine Bluff 34.228431 -92.003196 + Rogers 36.332020 -94.118537 + Russellville 35.278417 -93.133786 + Searcy 35.250641 -91.736249 + Siloam Springs 36.188136 -94.540496 + Springdale 36.186744 -94.128814 + Stuttgart Municipal Airport 34.600000 -91.566667 + Texarkana 33.441792 -94.037688 + Walnut Ridge 36.068404 -90.955953 + West Memphis 35.146480 -90.184539 + California 41.487115 -120.542456 + Anaheim 33.835293 -117.914504 + Arcata 40.866517 -124.082840 + Auburn Municipal Airport 38.950000 -121.066667 + Avalon 33.342807 -118.327851 + Bakersfield 35.373292 -119.018713 + Berkeley 37.871593 -122.272747 + Bishop 37.363540 -118.395110 + Blythe 33.610302 -114.596346 + Burbank 34.180839 -118.308966 + Camarillo 34.216394 -119.037602 + Campo 32.606449 -116.468905 + McClellan-Palomar Airport 33.130000 -117.275833 + Chico 39.728494 -121.837478 + China Lake 35.650789 -117.661730 + Chino 34.012235 -117.688944 + Chula Vista 32.640054 -117.084196 + Buchanan Field 37.991667 -122.051944 + Chino, Chino Airport 33.975556 -117.623611 + Costa Mesa 33.641132 -117.918669 + Crescent City 41.755948 -124.201747 + Daggett 34.863319 -116.888092 + Daly City 37.705766 -122.461917 + Edwards 34.926088 -117.935068 + El Centro 32.792000 -115.563051 + El Monte 34.068621 -118.027567 + Emigrant Gap 39.296845 -120.672712 + Escondido 33.119207 -117.086421 + Fairfield / Travis Air Force Base 38.266667 -121.950000 + Fontana 34.092233 -117.435048 + Palo Alto Airport 37.466667 -122.116667 + Fresno 36.747727 -119.772366 + Fullerton 33.870292 -117.925338 + Garden Grove 33.773905 -117.941448 + Burbank, Burbank-Glendale-Pasadena Airport 34.199722 -118.364722 + Hanford Municipal Airport 36.318611 -119.628889 + Hawthorne 33.916403 -118.352575 + Hayward Air Terminal 37.660833 -122.118333 + Huntington Beach 33.660297 -117.999227 + Imperial County Airport 32.834167 -115.578611 + Imperial Beach 32.583944 -117.113085 + Inglewood 33.961680 -118.353131 + Inyokern 35.646899 -117.812567 + Irvine 33.669465 -117.823111 + La Verne 34.100843 -117.767836 + General William J. Fox Airfield Airport 34.740833 -118.218889 + Lemoore 36.300784 -119.782911 + Livermore 37.681874 -121.768009 + Lompoc 34.639150 -120.457941 + Long Beach 33.766962 -118.189235 + Los Alamitos 33.803072 -118.072564 + Los Angeles 34.052234 -118.243685 + Madera 36.961336 -120.060718 + Yuba County Airport 39.097778 -121.569722 + Merced 37.302163 -120.482968 + Modesto 37.639097 -120.996878 + Montague 41.728198 -122.527801 + Monterey 36.600238 -121.894676 + Mount Shasta 41.309875 -122.310567 + Mount Wilson 34.226393 -118.066180 + Mountain View 37.386052 -122.083851 + Napa 38.297137 -122.285529 + Needles 34.848060 -114.614131 + Newhall 34.384720 -118.530919 + Norwalk 33.902237 -118.081733 + Oakland 37.804372 -122.270803 + Oceanside 33.195870 -117.379483 + Ontario International Airport 34.053333 -117.575833 + Santa Ana, John Wayne Airport-Orange County Airport 33.680000 -117.866389 + Oroville 39.513775 -121.556359 + Oxnard 34.197505 -119.177052 + Palm Springs 33.830296 -116.545292 + Palmdale 34.579434 -118.116461 + Palo Alto 37.441883 -122.143019 + Mount Wilson 34.233333 -118.066667 + Paso Robles 35.626637 -120.691004 + Pomona 34.055289 -117.752279 + Porterville 36.065230 -119.016768 + Ramona 33.041711 -116.868082 + Rancho Cucamonga 34.106399 -117.593108 + Red Bluff 40.178489 -122.235830 + Redding 40.586540 -122.391675 + Riverside 33.953349 -117.396156 + Sacramento 38.581572 -121.494400 + Salinas 36.677737 -121.655501 + San Bernardino 34.108345 -117.289765 + San Carlos 37.507159 -122.260522 + San Diego 32.715329 -117.157255 + San Francisco 37.774929 -122.419415 + San Jose 37.339386 -121.894955 + San Luis Obispo 35.282752 -120.659616 + Sandberg 34.741093 -118.709534 + Santa Ana 33.745573 -117.867834 + Santa Barbara 34.420831 -119.698190 + San Jose, San Jose International Airport 37.359167 -121.924167 + Santa Maria Public Airport 34.899444 -120.448611 + Santa Monica 34.019454 -118.491191 + Santa Rosa 38.440467 -122.714431 + Simi Valley 34.269447 -118.781482 + South Lake Tahoe 38.933241 -119.984348 + Stockton Metropolitan Airport 37.889722 -121.223611 + Sunnyvale 37.368830 -122.036350 + Thousand Oaks 34.170561 -118.837594 + Torrance 33.835849 -118.340629 + Truckee 39.327962 -120.183253 + Twentynine Palms 34.135558 -116.054169 + Ukiah 39.150171 -123.207783 + Vacaville 38.356577 -121.987744 + Vallejo 38.104086 -122.256637 + Van Nuys 34.186672 -118.448971 + Ventura 34.278335 -119.293168 + Victorville 34.536107 -117.291156 + Visalia 36.330228 -119.292058 + Watsonville 36.910231 -121.756895 + West Covina 34.068621 -117.938953 + Colorado 40.160537 -103.214384 + Alamosa 37.469449 -105.870022 + Arvada 39.802764 -105.087484 + Aspen 39.191098 -106.817539 + Buckley Air Force Base Airport 39.710000 -104.758056 + Broomfield 39.920541 -105.086650 + Carson County Airport 39.242222 -102.282778 + Sunlight 39.425556 -107.379167 + Colorado Springs 38.833882 -104.821363 + Cortez 37.348883 -108.585926 + Craig 40.515249 -107.546454 + Denver 39.739154 -104.984703 + Durango-La Plata County Airport 37.143056 -107.759722 + Eagle County Regional 39.650000 -106.916667 + Elbert 39.219434 -104.537192 + Fort Carson 38.737494 -104.788861 + Fort Collins 40.585260 -105.084423 + Grand Junction 39.063871 -108.550649 + Gunnison 38.545825 -106.925321 + Hayden 40.491918 -107.257558 + La Junta 37.985009 -103.543832 + La Veta 37.505012 -105.007775 + Lakewood 39.704709 -105.081373 + Lamar 38.087231 -102.620750 + Leadville 39.250823 -106.292524 + Limon 39.263876 -103.692174 + Meeker 40.037473 -107.913130 + Montrose 38.478320 -107.876174 + Pagosa Springs 37.269450 -107.009762 + Pueblo 38.254447 -104.609141 + Rifle 39.534702 -107.783120 + Saguache 38.087500 -106.141967 + Salida 38.534719 -105.998902 + Springfield, Comanche National Grassland 37.283333 -102.616667 + Steamboat Springs 40.484977 -106.831716 + Tarryall 39.121935 -105.475555 + Telluride 37.937494 -107.812285 + Perry Stokes Airport 37.266667 -104.433333 + Westminster 39.836653 -105.037205 + Connecticut 41.167041 -73.204835 + Danbury 41.394817 -73.454011 + Groton 41.350098 -72.078409 + Hartford 41.763711 -72.685093 + Meriden 41.538153 -72.807044 + New Haven 41.308153 -72.928158 + Waterbury-Oxford Airport 41.483333 -73.133333 + Stamford 41.053430 -73.538734 + Waterbury 41.558152 -73.051497 + Windsor Locks 41.929264 -72.627312 + Delaware 39.158168 -75.524368 + Sussex County Airport 38.690000 -75.362500 + New Castle County Airport 39.672778 -75.600833 + District of Columbia 38.895112 -77.036366 + Florida 29.725767 -84.983244 + Bartow 27.896415 -81.843137 + Boca Raton 26.358688 -80.083098 + Brooksville 28.555272 -82.387871 + Cape Canaveral 28.405837 -80.604773 + Cape Coral 26.562854 -81.949533 + Clearwater 27.965853 -82.800103 + Cocoa 28.386116 -80.741998 + Crestview 30.762133 -86.570508 + Cross City 29.634398 -83.125131 + Daytona Beach 29.210815 -81.022833 + Destin 30.393534 -86.495783 + Fort Lauderdale 26.122308 -80.143379 + Fort Myers 26.640628 -81.872308 + Fort Pierce 27.446706 -80.325606 + Fort Walton Beach 30.405755 -86.618842 + Gainesville Regional Airport 29.691944 -82.275556 + Hialeah 25.857596 -80.278106 + Hollywood 26.011201 -80.149490 + Homestead 25.468722 -80.477557 + Jacksonville, Naval Air Station 30.234167 -81.674722 + Key West 24.555702 -81.782591 + Lakeland 28.039465 -81.949804 + Leesburg Regional Airport 28.822500 -81.808889 + Marathon 24.713752 -81.090351 + Marianna 30.774360 -85.226873 + Mayport 30.393296 -81.430642 + Melbourne International Airport 28.102778 -80.645833 + Miami International Airport 25.790556 -80.316389 + Milton 30.632415 -87.039688 + Naples Municipal Airport 26.150000 -81.766667 + New Smyrna Beach 29.025819 -80.926998 + Ocala 29.187199 -82.140092 + Okeechobee 27.243935 -80.829783 + Orlando 28.538335 -81.379237 + Ormond Beach 29.285813 -81.055889 + Panama City 30.158813 -85.660206 + Pembroke Pines 26.003146 -80.223937 + Pensacola 30.421309 -87.216915 + Perry 30.117435 -83.581815 + Pompano Beach 26.237860 -80.124767 + Punta Gorda 26.929784 -82.045366 + Sarasota 27.336435 -82.530653 + Tyndall Air Force Base 30.066667 -85.583333 + Stuart 27.197548 -80.252826 + Tallahassee 30.438256 -84.280733 + Tampa 27.947522 -82.458428 + The Villages 28.933596 -81.948417 + Titusville 28.612219 -80.807554 + Valparaiso / Eglin Air Force Base 30.483333 -86.516667 + Vero Beach 27.638643 -80.397274 + Vilano Beach 29.918858 -81.292850 + West Palm Beach 26.715342 -80.053375 + Winter Haven 28.022243 -81.732857 + Southwest Georgia Regional Airport 31.535556 -84.194444 + Bacon County Airport 31.536111 -82.506667 + Athens Airport 33.950833 -83.328056 + Atlanta 33.748995 -84.387982 + Daniel Field 33.466944 -82.038611 + Bainbridge 30.903800 -84.575470 + Barretts 31.001590 -83.199318 + Malcolm McKinnon Airport 31.151667 -81.391389 + Canton 34.236762 -84.490762 + Cartersville 34.165097 -84.799938 + Columbus Metropolitan Airport 32.516111 -84.942222 + Dalton 34.769802 -84.970223 + Douglas Municipal Airport 31.476667 -82.860278 + W H 'Bud' Barron Airport 32.564444 -82.985000 + Fort Benning 32.352369 -84.968819 + Gilmer Memorial Airport 34.271944 -83.830278 + Greene County Regional Airport 33.597500 -83.138889 + Hinesville 31.846877 -81.595945 + Callaway Airport 33.008889 -85.072500 + Gwinnett County-Briscoe Field Airport 33.978056 -83.962500 + Macon 32.840695 -83.632402 + Marietta 33.952602 -84.549933 + Milledgeville 33.080143 -83.232099 + Moultrie 31.179908 -83.789063 + Newnan 33.380672 -84.799657 + R. B. Russell Airport 34.347778 -85.161111 + Savannah 32.083541 -81.099834 + Statesboro 32.448788 -81.783167 + Sylvania 32.750444 -81.636776 + Thomaston 32.888188 -84.326585 + Thomson 33.470693 -82.504573 + Valdosta 30.832702 -83.278485 + Vidalia 32.217686 -82.413461 + Warner Robins 32.620978 -83.599905 + Waycross 31.213551 -82.354018 + Winder 33.992610 -83.720171 + Hawaii 19.729722 -155.090000 + Honolulu 21.306944 -157.858333 + Kahului 20.894722 -156.470000 + Kailua 19.640556 -155.995556 + Kaumalapau 20.790000 -156.990000 + Kaunakakai 21.093333 -157.023889 + Kekaha 21.970833 -159.715000 + Lahaina 20.878333 -156.682500 + Lihue 21.981111 -159.371111 + Wahiawā 21.502778 -158.023611 + Waiki‘i 19.860556 -155.652222 + Idaho 43.613500 -116.203451 + Burley 42.535743 -113.792795 + Caldwell Industrial Airport 43.633333 -116.633333 + Challis 44.504644 -114.231731 + Coeur d'Alene 47.677683 -116.780466 + Hailey 43.519629 -114.315325 + Idaho Falls 43.466581 -112.034137 + Jerome 42.724073 -114.518653 + Lewiston 46.416551 -117.017657 + Lowell 46.144167 -115.596389 + Malta 42.316667 -113.333333 + McCall 44.911006 -116.098736 + Pullman / Moscow, Pullman / Moscow Regional Airport 46.743889 -117.109722 + Mountain Home Air Force Base 43.050000 -115.866667 + Mullan 47.470208 -115.801825 + Pocatello 42.871303 -112.445534 + Rexburg 43.826023 -111.789688 + Salmon 45.175755 -113.895901 + Sandpoint 48.276590 -116.553248 + Soda Springs 42.654365 -111.604669 + Stanley Ranger Station 44.208611 -114.934444 + Twin Falls 42.562967 -114.460871 + Illinois 38.890604 -90.184276 + Aurora Municipal Airport 41.770000 -88.481389 + Belleville 38.520050 -89.983993 + Bloomington / Normal, Central Illinois Regional Airport at Bloomington-Normal 40.476944 -88.915833 + Cahokia 38.570885 -90.190111 + Cairo Regional Airport 37.064444 -89.219444 + Carbondale 37.727273 -89.216750 + Carmi 38.090880 -88.158649 + Centralia 38.525049 -89.133404 + Champaign 40.116420 -88.243383 + Chicago 41.850033 -87.650052 + Vermilion County Airport 40.199444 -87.595556 + DeKalb 41.929474 -88.750365 + Decatur Airport 39.834444 -88.865556 + Effingham 39.120042 -88.543383 + Fairfield Municipal Airport 38.378611 -88.412778 + Flora 38.668936 -88.485604 + Albertus Airport 42.246389 -89.582222 + Galesburg 40.947816 -90.371240 + Grafton 38.970048 -90.431505 + Harrisburg-Raleigh Airport 37.811389 -88.549167 + Jacksonville Municipal Airport 39.774167 -90.238611 + Joliet 41.525031 -88.081725 + Kankakee 41.120033 -87.861153 + Lacon 41.024758 -89.411201 + Lawrenceville-Vincennes International Airport 38.760556 -87.598889 + Logan County Airport 40.158611 -89.335000 + Litchfield Municipal Airport 39.162222 -89.674444 + Macomb 40.459208 -90.671797 + Williamson County Regional Airport 37.750278 -89.001111 + Mattoon 39.483090 -88.372826 + Metropolis 37.151165 -88.731998 + Moline 41.506700 -90.515134 + Morris Municipal-James R Washburn Field Airport 41.425278 -88.418611 + Mount Carmel 38.410880 -87.761417 + Mount Vernon 38.317271 -88.903120 + Naperville 41.785863 -88.147289 + Olney 38.730881 -88.085315 + Edgar County Airport 39.700278 -87.669722 + Greater Peoria Regional Airport 40.667500 -89.683889 + Illinois Valley Regional-Walter A Duncan Field Airport 41.351944 -89.153056 + Pittsfield Penstone Municipal Airport 39.638889 -90.778333 + Pontiac Municipal Airport 40.923611 -88.625278 + Quincy 39.935602 -91.409873 + Rantoul 40.308367 -88.155879 + Robinson 39.005320 -87.739194 + Rochelle 41.923918 -89.068707 + Rockford 42.271131 -89.093995 + Salem-Leckrone Airport 38.642778 -88.964167 + Savanna 42.094467 -90.156794 + Sparta Community-Hunter Field Airport 38.148889 -89.698611 + Abraham Lincoln Capital Airport 39.845278 -89.683889 + Sterling 41.788642 -89.696219 + Taylorville 39.548935 -89.294533 + Waukegan 42.363633 -87.844794 + West Chicago 41.884751 -88.203961 + Anderson Municipal 40.116667 -85.616667 + Monroe County Airport 39.143056 -86.616667 + Columbus / Bakalar 39.266667 -85.900000 + Elkhart Municipal 41.716667 -86.000000 + Evansville 37.974764 -87.555848 + Fort Wayne 41.130604 -85.128860 + Gary 41.593370 -87.346427 + Goshen 41.582272 -85.834438 + Indianapolis 39.768377 -86.158042 + Kokomo 40.486427 -86.133603 + Purdue University Airport 40.412500 -86.947500 + Muncie 40.193377 -85.386360 + Grissom Air Force Base / Peru 40.650000 -86.150000 + Shelbyville 39.521437 -85.776924 + South Bend 41.683381 -86.250007 + Terre Haute 39.466703 -87.413909 + Porter County Municipal Airport 41.453056 -86.998056 + Warsaw Municipal Airport 41.274444 -85.840000 + Iowa 42.034708 -93.619940 + Ankeny 41.729710 -93.605773 + Atlantic 41.400000 -95.050000 + Audubon 41.718042 -94.932487 + Boone Municipal 42.050000 -93.850000 + Burlington Regional Airport 40.772778 -91.125278 + Carroll 42.065818 -94.866930 + Cedar Rapids 42.008333 -91.644068 + Chariton 41.013889 -93.306598 + Charles City 43.066361 -92.672411 + Cherokee 42.749428 -95.551674 + Clarinda 40.741935 -95.038313 + Clarion 42.731639 -93.732992 + Clinton Municipal Airport 41.833333 -90.333333 + Council Bluffs 41.261943 -95.861123 + Creston 41.016667 -94.366667 + Davenport 41.523644 -90.577637 + Decorah 43.303306 -91.785709 + Denison 42.017766 -95.355276 + Des Moines 41.600545 -93.609106 + Dubuque 42.500558 -90.664572 + Estherville 43.401626 -94.832764 + Fairfield 41.050000 -91.983333 + Fort Dodge 42.497469 -94.168016 + Fort Madison 40.629763 -91.315151 + Harlan 41.653044 -95.325554 + Iowa City 41.661128 -91.530168 + Keokuk 40.397266 -91.384874 + Knoxville 41.300000 -93.116667 + Lamoni 40.622777 -93.934116 + Le Mars 42.794157 -96.165578 + Marshalltown 42.049432 -92.907977 + Mason City 43.153573 -93.201037 + Monticello Municipal 42.233333 -91.166667 + Mount Pleasant Municipal Airport 40.946667 -91.511111 + Muscatine 41.424473 -91.043205 + Oelwein 42.673317 -91.913504 + Orange City 43.007209 -96.058352 + Oskaloosa 41.296395 -92.644359 + Ottumwa 41.020015 -92.411296 + Pella 41.408053 -92.916309 + Red Oak 41.009715 -95.225547 + Sheldon 43.181089 -95.856128 + Shenandoah 40.765553 -95.372210 + Sioux City 42.499994 -96.400307 + Spencer 43.141358 -95.144439 + Storm Lake 42.641092 -95.209718 + Vinton 42.168606 -92.023514 + Washington 41.283333 -91.666667 + Waterloo Municipal Airport 42.554444 -92.401111 + Webster City 42.469418 -93.816054 + Kansas 37.679214 -95.457203 + Coffeyville 37.037301 -95.616366 + Concordia 39.570835 -97.662540 + Dodge City 37.752798 -100.017079 + Elkhart-Morton County Airport 37.000000 -101.883333 + Elwood 39.755550 -94.872466 + Emporia Municipal Airport 38.328889 -96.193889 + Garden City Regional Airport 37.927500 -100.724444 + Goodland 39.350833 -101.710172 + Great Bend 38.364457 -98.764807 + Hays 38.879178 -99.326770 + Hill City 39.364728 -99.842065 + Hutchinson Municipal Airport 38.068056 -97.860556 + Junction City 39.028609 -96.831398 + Kansas City Downtown Airport 39.120833 -94.596944 + Lawrence Municipal Airport 39.008333 -95.211667 + Liberal 37.043081 -100.920999 + Manhattan 39.183608 -96.571669 + Medicine Lodge 37.281134 -98.580361 + Newton 38.046678 -97.345037 + Olathe 38.881396 -94.819129 + Overland Park 38.982228 -94.670792 + Parsons 37.340338 -95.261084 + Pratt 37.643907 -98.737591 + Russell 38.895289 -98.859806 + Salina 38.840280 -97.611424 + Topeka 39.048334 -95.678037 + Wichita 37.692236 -97.337545 + Winfield 37.239749 -96.995592 + Kentucky 36.990320 -86.443602 + Capital City Airport 38.184722 -84.903333 + Glasgow Municipal Airport 37.031667 -85.953611 + Henderson City 37.816667 -87.683333 + Carroll Airport 37.591389 -83.314444 + Blue Grass Airport 38.040833 -84.605833 + London-Corbin Airport-Magee Field 37.089444 -84.068611 + Louisville 38.254238 -85.759407 + Middlesboro 36.608415 -83.716582 + Muldraugh 37.937016 -85.991631 + Owensboro 37.774215 -87.113330 + Paducah 37.083389 -88.600048 + Somerset 37.092022 -84.604108 + Louisiana 31.311294 -92.445137 + Amelia 29.666320 -91.102045 + Baton Rouge 30.450746 -91.154551 + Boothville 29.343552 -89.419777 + DeRidder 30.846305 -93.289053 + Fort Polk 31.046578 -93.205440 + Galliano 29.442165 -90.299246 + Salt Point 29.562222 -91.525556 + Goosport 30.256872 -93.180431 + Grand Isle 29.236617 -89.987294 + Hammond 30.504358 -90.461200 + Hicks 31.184905 -93.015991 + Houma 29.595770 -90.719535 + Lafayette Regional Airport 30.202222 -91.993056 + Lake Charles 30.226595 -93.217376 + Leeville 29.248005 -90.207577 + Monroe Regional Airport 32.511667 -92.031389 + Natchitoches 31.760720 -93.086275 + New Iberia 30.003536 -91.818729 + New Orleans 29.954648 -90.075072 + Oakdale 30.816028 -92.660421 + Patterson 29.693264 -91.302050 + Peason 31.413789 -93.294613 + Ruston 32.523205 -92.637927 + Shreveport 32.525152 -93.750179 + Slidell 30.275195 -89.781175 + Maine 44.097851 -70.231166 + Augusta State Airport 44.320556 -69.797222 + Bangor 44.801182 -68.777814 + Bar Harbor 44.387579 -68.203902 + Brunswick, Naval Air Station 43.900278 -69.935000 + Caribou 46.860598 -68.011971 + Frenchville 47.280873 -68.379765 + Fryeburg 44.016459 -70.980624 + Greenville 45.466667 -69.583333 + Houlton 46.126164 -67.840296 + Millinocket 45.657272 -68.709758 + Portland International Jetport 43.642222 -70.304444 + Presque Isle 46.681153 -68.015862 + Rockland 44.103691 -69.108929 + Sanford Regional Airport 43.400000 -70.716667 + Waterville 44.552011 -69.631712 + Wiscasset 44.002856 -69.665602 + Maryland 38.978445 -76.492183 + Baltimore 39.290385 -76.612189 + Camp Springs 38.804003 -76.906640 + Cumberland 39.652865 -78.762518 + Easton 38.774283 -76.076330 + Frederick Municipal Airport 39.417500 -77.374444 + Hagerstown 39.641763 -77.719993 + Ocean City 38.336503 -75.084906 + Patuxent 38.539288 -76.748577 + Saint Marys City 38.187070 -76.434396 + Salisbury-Ocean City Wicomico County Regional Airport 38.340556 -75.510278 + Massachusetts 42.490650 -71.276169 + Beverly 42.558428 -70.880049 + Boston 42.358431 -71.059773 + Boston, Logan International Airport 42.360556 -71.010556 + Chatham 41.682056 -69.959738 + Chicopee Falls 42.152038 -72.575922 + East Milton 42.258432 -71.042550 + Fitchburg 42.583423 -71.802296 + Hyannis 41.652889 -70.282799 + Lawrence Municipal Airport 42.713056 -71.125833 + Lawrence, Lawrence Municipal Airport 42.713056 -71.125833 + Nantucket 41.283456 -70.099461 + New Bedford 41.636215 -70.934205 + North Adams 42.700915 -73.108715 + Norwood 42.194543 -71.199498 + Orange Municipal Airport 42.571667 -72.277500 + Pittsfield Municipal Airport 42.427222 -73.289167 + Plymouth Municipal Airport 41.908611 -70.728056 + Provincetown 42.058436 -70.178637 + Sandwich 41.758995 -70.493916 + Chicopee Falls / Westover Air Force Base 42.200000 -72.533333 + Vineyard Haven 41.454279 -70.603639 + Westfield 42.125093 -72.749538 + Worcester 42.262593 -71.802293 + Michigan 41.897547 -84.037166 + Gratiot Community Airport 43.321944 -84.687778 + Alpena 45.061679 -83.432753 + Ann Arbor 42.270872 -83.726329 + Bad Axe 43.801959 -83.000777 + Battle Creek 42.321152 -85.179714 + Bellaire 44.980282 -85.211173 + Benton Harbor 42.116706 -86.454189 + Big Rapids 43.698078 -85.483656 + Cadillac 44.251953 -85.401162 + Caro 43.491132 -83.396897 + Charlevoix 45.318063 -85.258400 + Fitch H Beach Airport 42.574444 -84.811389 + Cheboygan 45.646956 -84.474480 + Coldwater 41.940326 -85.000522 + Copper Harbor 47.468794 -87.888442 + Detroit 42.331427 -83.045754 + Escanaba 45.745247 -87.064580 + Flint 43.012527 -83.687456 + Frankfort Dow Memorial Field Airport 44.625000 -86.200556 + Gaylord 45.027513 -84.674752 + Gerald R. Ford International Airport 42.880833 -85.522778 + Grayling 44.661404 -84.714751 + Hancock 47.126871 -88.580956 + Harbor Springs 45.431676 -84.991999 + Hillsdale 41.920047 -84.630510 + Holland 42.787523 -86.108930 + Houghton Lake 44.314739 -84.764750 + Howell 42.607255 -83.929395 + Iron Mountain 45.820233 -88.065960 + Ironwood 46.454670 -90.171008 + Jackson County-Reynolds Field Airport 42.259722 -84.459444 + Kalamazoo 42.291707 -85.587229 + Kinross 46.275019 -84.514768 + Lambertville 41.765882 -83.627992 + Lansing 42.732535 -84.555535 + Livonia 42.368370 -83.352710 + Ludington 43.955283 -86.452583 + Mackinac Island 45.849180 -84.618934 + Manistee 44.244447 -86.324253 + Manistique 45.957751 -86.246252 + Marquette 46.543544 -87.395417 + Brooks Field Airport 42.251111 -84.955556 + St. Clair County International Airport 42.916667 -82.533333 + Mason 42.579203 -84.443585 + Menominee 45.107763 -87.614274 + Custer Airport 41.940000 -83.434722 + Mount Pleasant Municipal Airport 43.616667 -84.733333 + Munising 46.411057 -86.647936 + Muskegon 43.234181 -86.248392 + Newberry 46.354998 -85.509559 + Oscoda 44.420293 -83.330801 + Owosso 42.997805 -84.176636 + Pellston 45.552789 -84.783936 + Oakland County International Airport 42.663056 -83.410000 + Port Hope 43.940845 -82.712712 + Rogers City 45.421402 -83.818330 + Saginaw 43.419470 -83.950807 + Beaver Island, Beaver Island Airport 45.692222 -85.566389 + Sault Ste. Marie 46.495300 -84.345317 + South Haven 42.403087 -86.273641 + Sterling Heights 42.580312 -83.030203 + Sturgis 41.799217 -85.419148 + Traverse City 44.763057 -85.620632 + Oakland / Troy Airport 42.542778 -83.177778 + Warren 42.477536 -83.027700 + Minnesota 46.533013 -93.710249 + Albert Lea 43.648013 -93.368266 + Chandler Field 45.868611 -95.394167 + Angle Inlet 49.345274 -95.062738 + Appleton Municipal Airport 45.227500 -96.004167 + Austin Municipal 43.666667 -92.933333 + Baudette 48.712474 -94.599930 + Bemidji 47.473563 -94.880277 + Benson Municipal 45.316667 -95.650000 + Bigfork 47.744387 -93.654085 + Brainerd 46.358022 -94.200829 + Buffalo Municipal Airport 45.158889 -93.843056 + Cambridge Municipal 45.566667 -93.266667 + Cloquet 46.721610 -92.459357 + Cook 47.852418 -92.689618 + Crane Lake 48.266572 -92.488491 + Crookston 47.774138 -96.608121 + Detroit Lakes 46.817181 -95.845325 + Dodge Center 44.028020 -92.854638 + Duluth 46.783273 -92.106579 + Ely Municipal Airport 47.816667 -91.833333 + Eveleth 47.462428 -92.539906 + Fairmont 43.652178 -94.461083 + Faribault 44.294964 -93.268827 + Fergus Falls 46.283015 -96.077558 + Fosston 47.576348 -95.751415 + Glencoe 44.769129 -94.151642 + Glenwood 45.650239 -95.389758 + Grand Marais 47.750447 -90.334273 + Grand Rapids / Itasca County Airport-Gordon Newstrom Field 47.216667 -93.516667 + Granite Falls 44.809958 -95.545575 + Hallock 48.774426 -96.946447 + Hibbing 47.427155 -92.937689 + Hutchinson Municipal Airport-Butler Field 44.866667 -94.383333 + International Falls 48.601049 -93.410982 + Inver Grove Heights 44.848022 -93.042715 + Jackson Municipal Airport 43.650000 -94.983333 + Litchfield Municipal Airport 45.097222 -94.507222 + Little Falls 45.976354 -94.362502 + Longville 46.986344 -94.211364 + Luverne 43.654136 -96.212807 + Madison-Lac Qui Parle County Airport 44.986111 -96.177778 + Mankato 44.163578 -93.999400 + Maple Lake 45.229132 -94.001923 + Southwest Minnesota Regional Airport - Marshall / Ryan Field 44.450000 -95.816667 + McGregor 46.606615 -93.313842 + Minneapolis 44.979965 -93.263836 + Montevideo-Chippewa County Airport 44.966667 -95.716667 + Moorhead 46.873852 -96.767581 + Moose Lake 46.454113 -92.761866 + Mora 45.876903 -93.293835 + Morris Municipal Airport 45.566667 -95.966667 + New Ulm 44.312463 -94.460529 + Olivia 44.776350 -94.989721 + Orr 48.053527 -92.831002 + Ortonville 45.304687 -96.444779 + Owatonna 44.083852 -93.226044 + Park Rapids 46.922181 -95.058632 + Paynesville 45.380520 -94.711948 + Pine River 46.718016 -94.404162 + Pipestone 44.000526 -96.317534 + Preston 43.670242 -92.083216 + Princeton 45.550000 -93.600000 + Red Wing 44.562468 -92.533801 + Redwood Falls 44.539404 -95.116942 + Rochester International Airport 43.904167 -92.491667 + Roseau Municipal Airport / Rudy Billberg Field 48.850000 -95.700000 + Rush City 45.685514 -92.965490 + Saint Cloud 45.560799 -94.162490 + St. James, St. James Municipal Airport 43.986389 -94.558056 + Silver Bay 47.294365 -91.257386 + Slayton 43.987742 -95.755846 + Stanton 44.471911 -93.022989 + Staples 46.355519 -94.792241 + Thief River Falls 48.119135 -96.181147 + Tracy 44.233291 -95.619177 + Two Harbors 47.022711 -91.670732 + Wadena 46.442461 -95.136139 + Warroad 48.905266 -95.314404 + Waseca 44.077741 -93.507443 + Waskish 48.161352 -94.512455 + Wheaton 45.804405 -96.499233 + Windom 43.866346 -95.116937 + Winona 44.049963 -91.639315 + Worthington 43.619964 -95.596398 + Mississippi 30.396032 -88.885308 + Columbus / West Point / Starkville, Golden Triangle Regional Airport 33.450000 -88.583333 + Mid Delta Regional Airport 33.482778 -90.985556 + Greenwood-LeFlore Airport 33.492500 -90.083611 + Gulfport 30.367420 -89.092816 + Hattiesburg 31.327119 -89.290339 + Hawkins Field Airport 32.334722 -90.222500 + McComb 31.243787 -90.453154 + Meridian 32.364310 -88.703656 + Natchez 31.560444 -91.403171 + Olive Branch 34.961760 -89.829532 + University-Oxford Airport 34.384444 -89.535556 + Pascagoula 30.365755 -88.556127 + Tunica 34.684545 -90.382877 + Tupelo 34.257607 -88.703386 + Vicksburg 32.352646 -90.877882 + Missouri 37.305884 -89.518148 + Chesterfield 38.663108 -90.577067 + Chillicothe 39.795295 -93.552436 + Columbia Regional Airport 38.816944 -92.218333 + Farmington Airport 37.766667 -90.433333 + Independence 39.091116 -94.415507 + Jefferson City 38.576702 -92.173516 + Joplin 37.084227 -94.513281 + Kaiser 38.133645 -92.589906 + Kansas City Downtown Airport 39.120833 -94.596944 + Kirksville 40.194754 -92.583250 + Knob Noster 38.766680 -93.558546 + Poplar Bluff 36.756999 -90.392888 + Robertson 38.764218 -90.382060 + Sedalia 38.704461 -93.228261 + Springfield Regional Airport 37.239722 -93.389722 + Unity Village 38.951396 -94.401618 + Rolla / Vichy, Rolla National Airport 38.131944 -91.765278 + Waynesville 37.828652 -92.200723 + West Plains 36.728115 -91.852371 + Montana 46.366950 -104.284663 + Billings 45.783286 -108.500690 + Black Eagle 47.524680 -111.278307 + Bozeman 45.679653 -111.038558 + Browning 48.556917 -113.013418 + Butte 46.003815 -112.534745 + Cut Bank 48.633040 -112.326162 + Dillon 45.216311 -112.637520 + Drummond 46.667431 -113.147286 + Glasgow International Airport 48.213889 -106.621389 + Glendive 47.105290 -104.712460 + Great Falls 47.500235 -111.300808 + Havre 48.549999 -109.684089 + Helena 46.592712 -112.036109 + Jordan Airport 47.325833 -106.947500 + Kalispell 48.195793 -114.312908 + Lewistown 47.062473 -109.428238 + Livingston 45.662435 -110.561040 + Miles City 46.408336 -105.840558 + Missoula 46.872146 -113.993998 + Sidney-Richland 47.700000 -104.200000 + Wolf Point 48.090574 -105.640557 + Nebraska 42.549999 -99.862624 + Albion 41.690844 -98.003672 + Alliance 42.101634 -102.872145 + Aurora Municipal Airport 40.893889 -97.994444 + Beatrice 40.268056 -96.746970 + Broken Bow 41.401951 -99.639279 + Chadron 42.829419 -102.999907 + Columbus Municipal Airport 41.450000 -97.333333 + Falls City 40.060835 -95.601929 + Fremont Municipal Airport 41.450000 -96.516667 + Grand Island 40.925012 -98.342007 + Hastings 40.586125 -98.388393 + Hebron Municipal Airport 40.152222 -97.586944 + Holdrege 40.440289 -99.369822 + Imperial Municipal Airport 40.516667 -101.616667 + Kearney 40.699457 -99.081477 + Kimball 41.235814 -103.662997 + Jim Kelly Field Airport 40.791111 -99.777222 + Lincoln Municipal Airport 40.831111 -96.764444 + McCook 40.201948 -100.625708 + Nebraska City 40.676668 -95.859169 + Stefan Memorial Airport 41.980556 -97.436944 + North Platte 41.123887 -100.765423 + O'Neill 42.457781 -98.647587 + Ogallala 41.128048 -101.719618 + Omaha 41.258610 -95.937792 + Ord 41.603343 -98.926199 + Plattsmouth 41.011388 -95.882231 + Scottsbluff 41.866634 -103.667166 + Sidney Municipal Airport 41.099444 -102.985556 + Tekamah 41.778322 -96.221128 + Thedford 41.978332 -100.576252 + Valentine 42.872783 -100.550967 + Wayne 42.230559 -97.017824 + York Municipal Airport 40.896667 -97.622778 + Nevada 40.832421 -115.763123 + Ely Airport 39.295000 -114.845278 + Eureka 39.601389 -116.005556 + Fallon 39.473529 -118.777374 + Las Vegas, Henderson Executive Airport 35.972778 -115.134444 + North Las Vegas Airport 36.211667 -115.195833 + Lovelock 40.179354 -118.473481 + Mercury 36.660511 -115.994475 + North Las Vegas 36.198859 -115.117501 + Reno 39.529633 -119.813803 + Tonopah 38.067155 -117.230082 + Winnemucca 40.972958 -117.735685 + New Hampshire 44.468670 -71.185077 + Concord Municipal Airport 43.195278 -71.501111 + Gorham 44.387839 -71.173131 + Jaffrey 42.813973 -72.023136 + Keene 42.933692 -72.278141 + Laconia 43.527855 -71.470351 + Lebanon Municipal Airport 43.627222 -72.305833 + Manchester Airport 42.929167 -71.435833 + Nashua 42.765366 -71.467566 + Plymouth Municipal Airport 43.779167 -71.753611 + Pease Air Force Base / Portsmouth 43.083333 -70.816667 + Skyhaven Airport 43.278056 -70.922222 + Whitefield 44.373116 -71.610084 + New Jersey 40.985931 -74.742109 + Atlantic City 39.364283 -74.422927 + Belmar 40.178447 -74.021804 + Essex County Airport 40.876389 -74.283056 + Elizabeth 40.663992 -74.210701 + Jersey City 40.728158 -74.077642 + Juliustown 40.013446 -74.668769 + Millville 39.402060 -75.039344 + Morristown 40.796767 -74.481544 + Mount Holly 39.992890 -74.787662 + Newark International Airport 40.682500 -74.169444 + Paterson 40.916765 -74.171811 + Somerville 40.574270 -74.609880 + Sussex 41.209818 -74.607661 + Teterboro 40.859822 -74.059308 + Mercer County Airport 40.276389 -74.816389 + New Mexico 32.899532 -105.960265 + Albuquerque 35.084491 -106.651137 + Artesia 32.842334 -104.403296 + Cavern City Air Terminal Airport 32.337500 -104.263333 + Chama 36.903068 -106.579479 + Clayton 36.451693 -103.184104 + Clines Corners 35.009498 -105.669180 + Clovis 34.404799 -103.205227 + Corona / Lincoln 34.100000 -105.683333 + Deming 32.268698 -107.758640 + Four Corners Regional Airport 36.743611 -108.229167 + Gallup 35.528078 -108.742584 + Grants 35.147260 -107.851447 + Hobbs 32.702612 -103.136040 + Las Cruces 32.312316 -106.778337 + Las Vegas Municipal Airport 35.654167 -105.142500 + Los Alamos 35.888080 -106.306972 + Moriarty 34.990050 -106.049189 + Raton 36.903358 -104.439153 + Roswell 33.394266 -104.523024 + Ruidoso 33.331749 -105.673041 + Santa Fe 35.686975 -105.937799 + Silver City 32.770075 -108.280326 + Taos 36.407249 -105.573066 + Torreon 35.797244 -107.213933 + Truth or Consequences 33.128405 -107.252807 + Tucumcari 35.171723 -103.724966 + Albany International Airport 42.748056 -73.801667 + Binghamton 42.098687 -75.917974 + Black River 44.012564 -75.794367 + Greater Buffalo International Airport 42.940833 -78.735833 + Dansville 42.560900 -77.696106 + Dunkirk 42.479502 -79.333932 + East Hampton 40.963434 -72.184801 + Elmira / Corning Regional Airport 42.156389 -76.902778 + Farmingdale 40.732600 -73.445401 + Fulton 43.322846 -76.417158 + Glens Falls 43.309516 -73.644006 + Islip 40.729821 -73.210393 + Ithaca 42.440628 -76.496607 + Chautauqua County / Jamestown Airport 42.150000 -79.266667 + Massena 44.928105 -74.891865 + Montauk 41.035935 -71.954515 + Orange County Airport 41.509167 -74.265000 + Sullivan County International Airport 41.700000 -74.800000 + New York City, Central Park 40.783333 -73.966667 + Newburgh 41.503427 -74.010418 + Niagara Falls 43.094500 -79.056711 + Penn Yan 42.660903 -77.053858 + Plattsburgh 44.699487 -73.452912 + Poughkeepsie 41.700371 -73.920970 + Greater Rochester International Airport 43.116667 -77.676667 + Griffiss Air Force Base / Rome 43.233333 -75.400000 + Saranac Lake 44.329496 -74.131266 + Shirley 40.801488 -72.867603 + Syracuse 43.048122 -76.147424 + Watertown International Airport 43.991944 -76.021667 + Wellsville 42.122012 -77.948058 + Westhampton Beach 40.803155 -72.614539 + White Plains 41.033986 -73.762910 + Yonkers 40.931210 -73.898747 + North Carolina 35.370995 -77.953319 + Ahoskie 36.286822 -76.984681 + Albemarle 35.350143 -80.200058 + Andrews 35.201755 -83.824067 + Asheboro 35.707915 -79.813645 + Asheville 35.600945 -82.554015 + Michael J Smith Field Airport 34.733611 -76.660556 + Bogue 34.699329 -77.036891 + Watauga County Hospital Heliport 36.200000 -81.650000 + Burlington Alamance Regional Airport 36.047778 -79.473889 + Chapel Hill 35.913200 -79.055845 + Charlotte / Douglas International Airport 35.213333 -80.948611 + Sampson County Airport 34.975556 -78.364722 + Concord Regional Airport 35.385278 -80.709722 + Currituck 36.449877 -76.015482 + Durham 35.994033 -78.898619 + Edenton 36.057938 -76.607721 + Elizabeth City 36.294601 -76.251046 + Elizabethtown 34.629337 -78.605290 + Erwin 35.326829 -78.676128 + Fayetteville Regional Airport 34.989444 -78.880000 + Macon County Airport 35.216667 -83.416667 + Gastonia 35.262082 -81.187301 + Goldsboro 35.384884 -77.992765 + Piedmont Triad International Airport 36.097500 -79.943611 + Hatteras 35.219342 -75.690161 + Havelock 34.879049 -76.901330 + Hickory 35.733188 -81.341197 + Hoffman 35.032378 -79.547544 + Jacksonville, New River, Marine Corps Air Station 34.705833 -77.440833 + Jefferson 36.420403 -81.473438 + Kenansville 34.962388 -77.962207 + Kill Devil Hills 36.030723 -75.676010 + Kinston 35.262664 -77.581635 + Davidson County Airport 35.781111 -80.303889 + Louisburg 36.099039 -78.301106 + Lumberton 34.618220 -79.008642 + Manteo 35.908226 -75.675730 + Maxton 34.735161 -79.348932 + Monroe Airport 35.016944 -80.620556 + Mount Airy 36.499301 -80.607286 + New Bern 35.108493 -77.044114 + North Wilkesboro 36.158465 -81.147584 + Oak Island 33.916562 -78.161106 + Henderson-Oxford Airport 36.361667 -78.529167 + Pinehurst 35.195434 -79.469477 + Raleigh 35.772096 -78.638615 + Roanoke Rapids 36.461540 -77.654146 + Rocky Mount 35.938210 -77.790534 + Roe 34.992387 -76.308803 + Roxboro 36.393752 -78.982788 + Rowan County Airport 35.650000 -80.516667 + Sanford-Lee County Regional Airport 35.582500 -79.101389 + Shelby 35.292351 -81.535646 + Smithfield 35.508494 -78.339445 + Statesville 35.782636 -80.887296 + Wadesboro 34.968210 -80.076727 + Warren Field Airport 35.570556 -77.049722 + Wilmington International Airport 34.270556 -77.902500 + Winston-Salem 36.099860 -80.244216 + Winterville 35.529051 -77.401076 + North Dakota 46.808327 -100.783739 + Bowman 46.183062 -103.394906 + Devils Lake 48.112779 -98.865120 + Dickinson 46.879176 -102.789624 + Fargo 46.877186 -96.789803 + Garrison 47.652223 -101.415717 + Grand Forks 47.925257 -97.032855 + Hettinger 46.001395 -102.636824 + Jamestown Municipal Airport 46.929722 -98.678333 + Minot 48.232509 -101.296273 + Wahpeton 46.265237 -96.605907 + Williston 48.146968 -103.617975 + Ohio 41.081445 -81.519005 + Ashtabula 41.865053 -80.789809 + Cincinnati 39.162004 -84.456886 + Cleveland 41.499495 -81.695409 + Port Columbus International Airport 39.995000 -82.876389 + Dayton 39.758948 -84.191607 + Defiance 41.284493 -84.355780 + Elyria 41.368380 -82.107649 + Findlay 41.044220 -83.649932 + Butler County Regional Airport 39.361389 -84.520833 + Fairfield County Airport 39.757222 -82.663333 + Lima Allen County Airport 40.708056 -84.021389 + Mansfield 40.758390 -82.515447 + Marion Municipal Airport 40.616667 -83.068333 + New Philadelphia 40.489787 -81.445671 + Newark Heath Airport 40.022778 -82.462500 + Springfield-Beckley Municipal Airport 39.840278 -83.840000 + Toledo 41.663938 -83.555212 + Airborne Airpark Airport 39.428333 -83.779167 + Wooster 40.805056 -81.935143 + Youngstown 41.099780 -80.649519 + Zanesville 39.940345 -82.013192 + Oklahoma 34.774531 -96.678345 + Altus 34.638126 -99.333975 + Alva 36.805031 -98.666474 + Ardmore 34.174261 -97.143625 + Atoka 34.385926 -96.128325 + Bartlesville 36.747311 -95.980818 + Chandler Municipal Airport 35.723889 -96.820278 + Chickasha 35.052565 -97.936433 + Claremore 36.312596 -95.616090 + Clinton Regional Airport 35.538056 -98.921389 + Cushing 35.985064 -96.766970 + Duncan 34.502303 -97.957813 + Durant 33.993986 -96.370824 + El Reno 35.532274 -97.955049 + Enid 36.395589 -97.878391 + Frederick Municipal Airport 34.344444 -98.983056 + Gage 36.315594 -99.757618 + Grove 36.593686 -94.769119 + Guthrie 35.878937 -97.425319 + Guymon 36.682804 -101.481549 + Hobart Municipal Airport 34.989444 -99.052500 + Idabel 33.895665 -94.826328 + Lawton 34.608685 -98.390331 + McAlester 34.933430 -95.769713 + Muskogee 35.747877 -95.369691 + Norman 35.222567 -97.439478 + Oklahoma City 35.467560 -97.516428 + Okmulgee 35.623437 -95.960550 + Pauls Valley 34.740081 -97.222245 + Ponca City 36.706981 -97.085595 + Poteau 35.053709 -94.623558 + Sallisaw 35.460371 -94.787446 + Seminole Municipal Airport 35.274444 -96.675000 + Shawnee 35.327293 -96.925300 + Stillwater 36.115607 -97.058368 + Tahlequah 35.915370 -94.969956 + Tulsa 36.153982 -95.992775 + Weatherford 35.526163 -98.707574 + Oregon 46.187884 -123.831253 + Aurora State Airport 45.248889 -122.765556 + Baker City 44.774875 -117.834385 + Brookings Airport 42.074444 -124.290000 + Burns 43.586261 -119.054103 + Corvallis 44.564566 -123.262044 + Eugene 44.052069 -123.086754 + Hermiston 45.840410 -119.289461 + Klamath Falls 42.224867 -121.781670 + La Grande 45.324577 -118.087719 + Lakeview 42.188772 -120.345792 + McMinnville 45.210116 -123.198716 + Meacham 45.506519 -118.421349 + Rogue Valley International Airport 42.381111 -122.872222 + Newport Municipal Airport 44.580278 -124.058056 + North Bend 43.406501 -124.224280 + Ontario Municipal Airport 44.019444 -117.009722 + Pendleton 45.672075 -118.788597 + Placer 42.632063 -123.315339 + Portland International Airport 45.590833 -122.600278 + Redmond 44.272620 -121.173921 + Rome 42.590556 -117.864444 + Roseburg 43.216505 -123.341738 + McNary Field 44.907778 -122.995000 + Scappoose 45.754281 -122.877604 + The Dalles 45.594564 -121.178682 + Pennsylvania 40.608430 -75.490183 + Altoona 40.518681 -78.394736 + Beaver Falls 40.752010 -80.319230 + Bradford 41.955896 -78.643916 + Butler 40.861176 -79.895333 + Clearfield 41.027280 -78.439188 + Doylestown 40.310106 -75.129894 + Du Bois 41.119228 -78.760030 + Erie 42.129224 -80.085059 + Venango Regional Airport 41.383333 -79.866667 + Capital City Airport 40.217222 -76.851389 + Indiana / Stewart Field 40.633333 -79.100000 + Indiantown 40.409812 -76.580244 + Johnstown 40.326741 -78.921970 + Lancaster Airport 40.120278 -76.294444 + Latrobe 40.321181 -79.379481 + Meadville 41.641444 -80.151448 + Mount Pocono 41.122034 -75.364628 + New Castle 41.003672 -80.347009 + Philadelphia 39.952335 -75.163789 + Pittsburgh 40.440625 -79.995886 + Pottstown 40.245374 -75.649630 + Quakertown 40.441768 -75.341567 + Reading 40.335648 -75.926875 + Selinsgrove 40.798974 -76.862194 + State College 40.793395 -77.860001 + Washington County Airport 40.133333 -80.283333 + Wilkes-Barre 41.245915 -75.881308 + Williamsport 41.241190 -77.001079 + Willow Grove 40.143999 -75.115729 + York Airport 39.919444 -76.876944 + Rhode Island 41.490102 -71.312828 + Pawtucket 41.878711 -71.382556 + Providence 41.823989 -71.412834 + Westerly 41.377600 -71.827291 + South Carolina 34.503439 -82.650133 + Beaufort, Marine Corps Air Station 32.493611 -80.703056 + Charleston Air Force Base 32.898889 -80.040556 + Clemson 34.683438 -82.837365 + Columbia Owens Downtown Airport 33.970833 -80.994444 + Dalzell 34.016821 -80.430082 + Darlington 34.299876 -79.876174 + Florence Regional Airport 34.187778 -79.730833 + Greenville Downtown Airport 34.846111 -82.346111 + Greenwood County Airport 34.247222 -82.154722 + Greer 34.938728 -82.227057 + Hilton Head Island 32.216316 -80.752608 + Myrtle Beach 33.689060 -78.886694 + North Myrtle Beach 33.816006 -78.680016 + Orangeburg 33.491820 -80.855648 + Rock Hill 34.924867 -81.025078 + South Dakota 45.464698 -98.486483 + Box Elder 44.112488 -103.068232 + Brookings Municipal Airport 44.300000 -96.816667 + Buffalo 45.604444 -103.546389 + Chamberlain 43.810828 -99.330656 + Custer 43.766651 -103.598806 + Faith 45.023039 -102.035992 + Huron 44.363317 -98.214257 + Mitchell 43.709428 -98.029799 + Mobridge 45.537216 -100.427913 + Philip 44.039433 -101.665144 + Pierre 44.368316 -100.350966 + Pine Ridge 43.025541 -102.556274 + Rapid City 44.080543 -103.231015 + Sioux Falls 43.549975 -96.700327 + Sisseton 45.664682 -97.049805 + Watertown Municipal Airport 44.904722 -97.149444 + Yankton 42.871109 -97.397281 + Tennessee 35.045630 -85.309680 + Outlaw Field Airport 36.621944 -87.415000 + Crossville 35.948957 -85.026901 + Dyersburg 36.034516 -89.385628 + McKellar-Sipes Regional Airport 35.593056 -88.916667 + Kingsport 36.548434 -82.561819 + McGhee Tyson Airport 35.818056 -83.985833 + Memphis 35.149534 -90.048980 + Millington 35.341474 -89.897308 + Nashville 36.165890 -86.784443 + Oak Ridge 36.010356 -84.269645 + Smyrna 35.982841 -86.518604 + Texas 32.448736 -99.733144 + Alice 27.752249 -98.069725 + Alpine-Casparis Municipal Airport 30.384167 -103.683333 + Amarillo 35.221997 -101.831297 + Angleton 29.169410 -95.431885 + Arlington Municipal Airport 32.663889 -97.095833 + Austin City, Austin Camp Mabry 30.316667 -97.766667 + Bay City 28.982757 -95.969402 + Beaumont 30.086046 -94.101846 + Big Spring 32.250398 -101.478736 + Borger 35.667820 -101.397388 + Brady 31.135168 -99.335055 + Brenham 30.166883 -96.397744 + Brownsville 25.901747 -97.497484 + Brownwood 31.709320 -98.991161 + Burnet 30.758238 -98.228358 + Caldwell Municipal Airport 30.515278 -96.703889 + Canadian 35.912820 -100.382077 + Carrollton 32.953735 -96.890282 + Childress 34.426453 -100.204002 + Clarksville / Red River County-J D Trissell Field Airport 33.593056 -95.063333 + College Station 30.627977 -96.334407 + Conroe 30.311877 -95.456051 + Corpus Christi 27.800583 -97.396381 + Corsicana 32.095430 -96.468873 + Cotulla 28.436934 -99.235032 + Crockett 31.318236 -95.456614 + Dalhart 36.059477 -102.513250 + Dallas 32.783056 -96.806667 + Decatur Municipal Airport 33.254444 -97.580556 + Del Rio 29.362730 -100.896761 + Denton 33.214841 -97.133068 + Terrel County Airport 30.048056 -102.213056 + Dumas 35.865595 -101.973235 + Edinburg 26.301737 -98.163343 + El Paso 31.758720 -106.486931 + Falfurrias 27.226987 -98.144171 + Fort Stockton 30.894043 -102.879322 + Fort Worth 32.725409 -97.320850 + Fredericksburg 30.275201 -98.871984 + Gainesville Municipal Airport 33.651389 -97.196944 + Galveston 29.301348 -94.797696 + Garland 32.912624 -96.638883 + Gatesville 31.435164 -97.743911 + Georgetown Municipal Airport 30.683333 -97.683333 + Giddings 30.182716 -96.936371 + Gilmer 32.728747 -94.942438 + Graham 33.107060 -98.589502 + Granbury 32.442083 -97.794197 + Grand Prairie 32.745964 -96.997785 + Greenville / Majors 33.066667 -96.066667 + Harlingen 26.190631 -97.696103 + Hearne 30.878524 -96.593026 + Hebbronville 27.306706 -98.678352 + Hillsboro 32.010989 -97.130006 + Hondo 29.347456 -99.141425 + Houston 29.763284 -95.363271 + Huntsville Municipal Airport 30.743889 -95.586111 + Irving 32.814018 -96.948894 + Cherokee County Airport 31.869167 -95.217222 + Jasper County-Bell Field Airport 30.885556 -94.034722 + Junction 30.489355 -99.772011 + Kerrville 30.047433 -99.140319 + Killeen 31.117119 -97.727796 + Kingsville 27.515869 -97.856109 + Fayette Regional Air Center Airport 29.908056 -96.950000 + Lancaster Airport 32.579167 -96.718889 + Laredo 27.506407 -99.507542 + Llano 30.759345 -98.675038 + Longview 32.500704 -94.740489 + Lubbock 33.577863 -101.855166 + Lufkin 31.338241 -94.729097 + Marfa 30.307938 -104.019072 + McAllen 26.203407 -98.230012 + McKinney 33.197616 -96.615269 + Mesquite 32.766796 -96.599159 + Midland 31.997346 -102.077915 + Midlothian 32.482361 -96.994449 + Mineral Wells 32.808461 -98.112822 + Mount Pleasant Regional Airport 33.095278 -94.961389 + Nacogdoches 31.603513 -94.655487 + New Braunfels 29.703002 -98.124453 + Odessa 31.845682 -102.367643 + Orange County Airport 30.069167 -93.803611 + Palacios 28.708046 -96.217467 + Palestine 31.762115 -95.630789 + Pampa 35.536156 -100.959871 + Paris / Cox Field 33.633333 -95.450000 + Houston, Houston Hobby Airport 29.637500 -95.282500 + Pecos 31.422912 -103.493229 + Perryton 36.400031 -100.802650 + Pine Springs 31.892616 -104.815503 + Plainview 34.184794 -101.706842 + Plano 33.019843 -96.698886 + Port Aransas 27.833916 -97.061099 + Port Isabel 26.073412 -97.208584 + Port Lavaca 28.614997 -96.626089 + Rockport 28.020573 -97.054434 + Rocksprings 30.015765 -100.205358 + San Angelo 31.463772 -100.437038 + San Antonio 29.424122 -98.493628 + San Marcos 29.883275 -97.941394 + Gaines County Airport 32.675278 -102.652500 + Sherman 33.635662 -96.608880 + Snyder 32.717886 -100.917618 + Sonora Municipal Airport 30.585556 -100.648333 + Spofford 29.175239 -100.413688 + Clark Field Municipal Airport 32.216667 -98.183333 + Sulphur Springs 33.138448 -95.601067 + Sweetwater 32.470952 -100.405938 + Temple 31.098234 -97.342782 + Terrell 32.735963 -96.275257 + Tyler 32.351260 -95.301062 + Universal City 29.548007 -98.291123 + Uvalde 29.209684 -99.786168 + Wilbarger County Airport 34.225556 -99.283611 + Victoria Regional Airport 28.862500 -96.929722 + Waco 31.549333 -97.146670 + Weslaco 26.159519 -97.990837 + Wharton 29.311637 -96.102737 + Wichita Falls 33.913708 -98.493387 + Wink 31.751240 -103.159888 + Utah 37.628316 -112.167695 + Cedar City 37.677477 -113.061893 + Delta 39.352178 -112.577170 + Hanksville 38.373038 -110.714039 + Lakeside 41.222430 -112.865534 + Logan 41.735486 -111.834388 + Milford 38.396911 -113.010789 + Moab 38.573315 -109.549840 + Ogden 41.223000 -111.973830 + Price 39.599410 -110.810715 + Provo 40.233844 -111.658534 + Salt Lake City 40.760779 -111.891047 + St George, St George Municipal Airport 37.083333 -113.600000 + Vernal 40.455516 -109.528748 + Wendover 40.737152 -114.037510 + West Valley City 40.691613 -112.001050 + Vermont 44.197006 -72.502049 + Bennington 42.878135 -73.196774 + Burlington International Airport 44.468056 -73.150278 + Morrisville 44.561719 -72.598449 + Rutland 43.610624 -72.972606 + Saint Johnsbury 44.419225 -72.015095 + Hartness State Springfield Airport 43.342500 -72.521667 + Virginia 36.709834 -81.977348 + Washington DC, Reagan National Airport 38.848333 -77.034167 + Hanover County Municipal Airport 37.708056 -77.434444 + Blacksburg 37.229573 -80.413939 + Charlottesville 38.029306 -78.476678 + Chesapeake 36.819037 -76.274940 + Chincoteague 37.933179 -75.378809 + Culpeper 38.473182 -77.996664 + Danville Regional Airport 36.572778 -79.336111 + Dublin / New River Valley 37.133333 -80.683333 + Emporia-Greensville Regional Airport 36.686944 -77.482778 + Farmville 37.302096 -78.391940 + Franklin / J B Rose 36.700000 -76.900000 + Hampton 37.029869 -76.345222 + Hillsville 36.762628 -80.734795 + Hot Springs / Ingalls 37.950000 -79.833333 + Leesburg / Godfrey 39.083333 -77.566667 + Louisa 38.025139 -78.004165 + Lynchburg 37.413754 -79.142246 + Manassas 38.750949 -77.475267 + Marion / Wytheville 36.900000 -81.350000 + Martinsville 36.691526 -79.872539 + Melfa 37.649299 -75.741318 + Newport News 36.978759 -76.428003 + Norfolk, Naval Air Station 36.933611 -76.295833 + Orange County Airport 38.247222 -78.045556 + Dinwiddie County Airport 37.183333 -77.516667 + Pohick 38.710115 -77.196648 + Norfolk, Naval Air Station 36.933611 -76.295833 + Quantico 38.522343 -77.293593 + Richmond International Airport 37.511111 -77.323333 + Roanoke 37.270970 -79.941427 + South Hill 36.726532 -78.128886 + Stafford 38.422069 -77.408316 + Staunton 38.149576 -79.071696 + Suffolk 36.728205 -76.583562 + Virginia Beach 36.852926 -75.977985 + Wakefield 36.968206 -76.989683 + West Point 37.531534 -76.796350 + Williamsburg 37.270702 -76.707457 + Winchester 39.185660 -78.163334 + Wise 36.975935 -82.575711 + Arlington Municipal 48.166667 -122.166667 + Bellevue 47.610377 -122.200679 + Bellingham 48.759553 -122.488225 + Bremerton 47.567318 -122.632639 + Burlington / Mount Vernon, Skagit Regional Airport 48.470833 -122.420833 + Deer Park 47.954338 -117.476891 + Eastsound 48.696771 -122.905462 + Ellensburg 46.996514 -120.547847 + Ephrata 47.317639 -119.553649 + Everett 47.978985 -122.202079 + Fairchild 47.634054 -117.668552 + Friday Harbor 48.534266 -123.017124 + Hanford 46.566667 -119.600000 + Hoquiam 46.980925 -123.889335 + Kelso 46.146779 -122.908445 + Moses Lake 47.130142 -119.278077 + Oak Harbor 48.293156 -122.643225 + Olympia 47.037874 -122.900695 + Omak 48.410985 -119.527551 + Pasco 46.239579 -119.100566 + Port Angeles 48.118146 -123.430741 + Quillayute 47.943130 -124.542435 + Renton 47.482878 -122.217066 + Seattle 47.606209 -122.332071 + Shelton 47.215094 -123.100707 + Spokane 47.658780 -117.426047 + Stampede 47.262890 -121.366482 + Tacoma 47.252877 -122.444291 + Tillicum 47.123431 -122.557070 + Pearson Field Airport 45.620278 -122.656389 + Walla Walla 46.064581 -118.343021 + Wenatchee 47.423460 -120.310349 + Yakima 46.602071 -120.505899 + West Virginia 37.778170 -81.188156 + Bluefield 37.269840 -81.222319 + Buckhannon 38.993987 -80.232028 + Yeager Airport 38.379444 -81.591389 + Clarksburg 39.280645 -80.344534 + Elkins 38.925940 -79.846735 + Huntington 38.419250 -82.445154 + Lewisburg 37.801788 -80.445630 + Martinsburg 39.456210 -77.963887 + Morgantown 39.629526 -79.955897 + Parkersburg 39.266742 -81.561514 + Grant County Airport 38.983333 -79.133333 + Point Pleasant 38.844525 -82.137089 + Braxton County Airport 38.686944 -80.651667 + Wheeling 40.063962 -80.720915 + Wisconsin 45.140245 -89.152335 + Appleton / Outagamie 44.250000 -88.516667 + Kennedy Memorial Airport 46.549722 -90.918333 + Baraboo 43.471094 -89.744291 + Boscobel 43.134429 -90.705405 + Burlington Municipal Airport 42.690556 -88.304722 + Camp Douglas 43.922467 -90.271519 + Clintonville 44.620535 -88.762323 + Eagle River 45.917176 -89.244299 + Eau Claire 44.811349 -91.498494 + Fond du Lac 43.773045 -88.447051 + Green Bay 44.519159 -88.019826 + Hayward Municipal Airport 46.020556 -91.450278 + Janesville 42.682789 -89.018722 + Dodge County Airport 43.426667 -88.703333 + Kenosha 42.584742 -87.821185 + La Crosse 43.801356 -91.239581 + Ladysmith 45.463023 -91.104036 + Land O' Lakes 46.161339 -89.218749 + Lone Rock 43.183324 -90.197902 + Dane County Regional-Truax Field 43.140556 -89.345278 + Manitowoc 44.088606 -87.657584 + Marshfield 44.668852 -90.171799 + Taylor County Airport 45.101111 -90.303333 + Menomonie 44.875518 -91.919342 + Merrill 45.180522 -89.683459 + Milwaukee 43.038902 -87.906474 + Monroe Municipal Airport 42.615000 -89.590833 + Mosinee 44.793023 -89.703178 + New Richmond 45.123021 -92.536586 + Osceola 45.320520 -92.704930 + Oshkosh 44.024706 -88.542614 + Phillips 45.696626 -90.400430 + Prairie du Chien 43.051651 -91.141240 + Racine 42.726131 -87.782852 + Rhinelander 45.636623 -89.412075 + Rice Lake 45.506068 -91.738225 + Sheboygan 43.750828 -87.714530 + Siren 45.785782 -92.381028 + Sparta / Fort McCoy Airport 43.958333 -90.737778 + Stevens Point 44.523579 -89.574563 + Sturgeon Bay 44.834164 -87.377042 + Superior 46.720774 -92.104080 + Tomahawk 45.471079 -89.729859 + Watertown 43.166667 -88.716667 + Waukesha 43.011678 -88.231481 + Waupaca 44.358035 -89.085946 + Wausau 44.959135 -89.630122 + Wautoma 44.074700 -89.287897 + West Bend 43.425278 -88.183428 + Wisconsin Rapids 44.383576 -89.817347 + Woodruff 45.896341 -89.699037 + Big Piney 42.538275 -110.114325 + Bordeaux 41.933333 -104.950000 + Buffalo Johnson County Airport 44.381389 -106.718889 + Casper 42.866632 -106.313081 + Cheyenne 41.139981 -104.820246 + Cody 44.526342 -109.056531 + Converse County Airport 42.794167 -105.381944 + Evanston 41.268279 -110.963237 + Gillette 44.291092 -105.502221 + Greybull 44.489124 -108.056213 + Jackson Hole Airport 43.600000 -110.733333 + Lander 42.833014 -108.730672 + Laramie 41.311367 -105.591101 + Pinedale 42.866610 -109.860986 + Rawlins 41.791070 -107.238663 + Riverton 43.024959 -108.380104 + Rock Springs 41.587464 -109.202904 + Sheridan 44.797194 -106.956179 + Torrington 42.062465 -104.184394 + West Thumb 44.415495 -110.575485 + Worland 44.016901 -107.955372 + Vedauwoo 41.150000 -105.400000 diff --git a/plugins/pray_times_files/pray_times.py b/plugins/pray_times_files/pray_times.py new file mode 100644 index 000000000..fd1158b49 --- /dev/null +++ b/plugins/pray_times_files/pray_times.py @@ -0,0 +1,352 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# Also avalable in /usr/share/common-licenses/LGPL on Debian systems +# or /usr/share/licenses/common/LGPL/license.txt on ArchLinux + + +import sys, os, gettext + +import time +from time import localtime +from time import time as now + +from os.path import join, isfile, dirname + + +#_mypath = __file__ +#if _mypath.endswith('.pyc'): +# _mypath = _mypath[:-1] +#dataDir = dirname(_mypath) +dataDir = dirname(__file__) +rootDir = '/usr/share/starcal3' + +sys.path.insert(0, dataDir)## FIXME +sys.path.insert(0, rootDir)## FIXME + +from natz.local import get_localzone + +from scal3 import plugin_api as api + +from scal3.path import * +from pray_times_backend import PrayTimes + +## DO NOT IMPORT core IN PLUGINS +from scal3.json_utils import * +from scal3.time_utils import floatHourToTime +from scal3.locale_man import tr as _ +from scal3.cal_types.gregorian import to_jd as gregorian_to_jd +from scal3.time_utils import getUtcOffsetByJd, getUtcOffsetCurrent, getEpochFromJd +from scal3.os_utils import kill, goodkill +from scal3.utils import myRaise +#from scal3 import event_lib## needs core!! FIXME + +from threading import Timer + +#if 'gtk' in sys.modules: +from pray_times_gtk import * +#else: +# from pray_times_qt import * + +#################################################### + +localTz = get_localzone() + + +####################### Methods and Classes ################## + +def readLocationData(): + lines = open(dataDir+'/locations.txt').read().split('\n') + cityData = [] + country = '' + for l in lines: + p = l.split('\t') + if len(p)<2: + #print(p) + continue + if p[0]=='': + if p[1]=='': + city, lat, lng = p[2:5] + #if country=='Iran': + # print(city) + if len(p)>4: + cityData.append(( + country + '/' + city, + _(country) + '/' + _(city), + float(lat), + float(lng) + )) + else: + print(country, p) + else: + country = p[1] + return cityData + +def guessLocation(cityData): + tzname = str(localTz) + ## FIXME + #for countryCity, countryCityLocale, lat, lng in cityData: + return 'Tehran', 35.705, 51.4216 + + +''' +event_classes = api.get('event_lib', 'classes') +EventRule = api.get('event_lib', 'EventRule') + +@event_classes.rule.register +class PrayTimeEventRule(EventRule): + plug = None ## FIXME + name = 'prayTime' + desc = _('Pray Time') + provide = ('time',) + need = () + conflict = ('dayTimeRange', 'cycleLen',) + def __init__(self, parent): + EventRule.__init__(self, parent) + def calcOccurrence(self, startEpoch, endEpoch, event): + self.plug.get_times_jd(jd) + getInfo = lambda self: self.desc +''' + +class TextPlugin(BaseJsonPlugin, TextPluginUI): + name = 'pray_times' + ## all options (except for "enable" and "show_date") will be saved in file confPath + confPath = join(confDir, 'pray_times.json') + confParams = ( + 'lat', + 'lng', + 'method', + 'locName', + 'shownTimeNames', + 'imsak', + 'sep', + 'azanEnable', + 'azanFile', + 'preAzanEnable', + 'preAzanFile', + 'preAzanMinutes', + ) + azanTimeNamesAll = ( + 'fajr', + 'dhuhr', + 'asr', + 'maghrib', + 'isha', + ) + def __init__(self, _file): + #print('----------- praytime TextPlugin.__init__') + #print('From plugin: core.VERSION=%s'%api.get('core', 'VERSION')) + #print('From plugin: core.aaa=%s'%api.get('core', 'aaa')) + BaseJsonPlugin.__init__( + self, + _file, + ) + self.lastDayMerge = False + self.cityData = readLocationData() + ############## + confNeedsSave = False + ###### + self.locName, self.lat, self.lng = '', 0, 0 + method = '' + ####### + self.imsak = 10 ## minutes before Fajr (Morning Azan) + #self.asrMode=ASR_STANDARD + #self.highLats='NightMiddle' + #self.timeFormat='24h' + self.shownTimeNames = ( + 'fajr', + 'sunrise', + 'dhuhr', + 'maghrib', + 'midnight', + ) + ## FIXME rename shownTimeNames to activeTimeNames + ## or add another list azanSoundTimeNames + self.sep = ' ' + ## + self.azanEnable = False + self.azanFile = None + ## + self.preAzanEnable = False + self.preAzanFile = None + self.preAzanMinutes = 2.0 + #### + loadModuleJsonConf(self) + #### + if not isfile(self.confPath): + confNeedsSave = True + #### + if not self.locName: + confNeedsSave = True + self.locName, self.lat, self.lng = guessLocation(self.cityData) + self.method = 'Tehran' + ## guess method from location FIXME + ####### + self.backend = PrayTimes( + self.lat, + self.lng, + methodName=self.method, + imsak='%d min'%self.imsak, + ) + #### + ####### + #PrayTimeEventRule.plug = self + ####### + if confNeedsSave: + self.saveConfig() + ####### + self.makeWidget()## FIXME + #self.onCurrentDateChange(localtime()[:3]) + ### + #self.doPlayPreAzan() + #time.sleep(2) + #self.doPlayAzan() ## for testing ## FIXME + def saveConfig(self): + self.lat = self.backend.lat + self.lng = self.backend.lng + self.method = self.backend.method.name + saveModuleJsonConf(self) + #def date_change_after(self, widget, year, month, day): + # self.dialog.menuCell.add(self.menuitem) + # self.menu_unmap_id = self.dialog.menuCell.connect('unmap', self.menu_unmap) + #def menu_unmap(self, menu): + # menu.remove(self.menuitem) + # menu.disconnect(self.menu_unmap_id) + def get_times_jd(self, jd): + times = self.backend.getTimesByJd( + jd, + getUtcOffsetByJd(jd)/3600.0, + ) + return [(name, times[name]) for name in self.shownTimeNames] + def getFormattedTime(self, tm):## tm is float hour + try: + h, m, s = floatHourToTime(float(tm)) + except ValueError: + return tm + else: + return '%d:%.2d'%(h, m) + def get_text_jd(self, jd): + return self.sep.join([ + '%s: %s'%(_(name.capitalize()), self.getFormattedTime(tm)) + for name, tm in self.get_times_jd(jd) + ]) + def get_text(self, year, month, day):## just for compatibity (usage by external programs) + return self.get_text_jd(gregorian_to_jd(year, month, day)) + def update_cell(self, c): + text = self.get_text_jd(c.jd) + if text!='': + if c.pluginsText!='': + c.pluginsText += '\n' + c.pluginsText += text + def killPrevSound(self): + try: + p = self.proc + except AttributeError: + pass + else: + print('killing %s'%p.pid) + goodkill(p.pid, interval=0.01) + #kill(p.pid, 15) + #p.terminate() + def doPlayAzan(self):## , tm + if not self.azanEnable: + return + #dt = tm - now() + #print('---------------------------- doPlayAzan, dt=%.1f'%dt) + #if dt > 1: + # Timer( + # int(dt), + # self.doPlayAzan, + # #tm, + # ).start() + # return + self.killPrevSound() + self.proc = popenFile(self.azanFile) + def doPlayPreAzan(self):## , tm + if not self.preAzanEnable: + return + #dt = tm - now() + #print('---------------------------- doPlayPreAzan, dt=%.1f'%dt) + #if dt > 1: + # Timer( + # int(dt), + # self.doPlayPreAzan, + # #tm, + # ).start() + # return + self.killPrevSound() + self.proc = popenFile(self.preAzanFile) + def onCurrentDateChange(self, gdate): + print('praytimes: onCurrentDateChange', gdate) + if not self.enable: + return + jd = gregorian_to_jd(*tuple(gdate)) + #print(getUtcOffsetByJd(jd)/3600.0, getUtcOffsetCurrent()/3600.0) + #utcOffset = getUtcOffsetCurrent() + utcOffset = getUtcOffsetByJd(jd) + tmUtc = now() + epochLocal = tmUtc + utcOffset + secondsFromMidnight = epochLocal % (24*3600) + midnightUtc = tmUtc - secondsFromMidnight + #print('------- hours from midnight', secondsFromMidnight/3600.0) + for timeName, azanHour in self.backend.getTimesByJd( + jd, + utcOffset/3600.0, + ).items(): + if timeName not in self.azanTimeNamesAll: + continue + if timeName not in self.shownTimeNames: + continue + azanSec = azanHour * 3600.0 + ##### + toAzanSecs = int(azanSec - secondsFromMidnight) + if toAzanSecs >= 0: + preAzanSec = azanSec - self.preAzanMinutes * 60 + toPreAzanSec = max( + 0, + int(preAzanSec - secondsFromMidnight) + ) + print('toPreAzanSec=%.1f'%toPreAzanSec) + Timer( + toPreAzanSec, + self.doPlayPreAzan, + #midnightUtc + preAzanSec, + ).start() + ### + print('toAzanSecs=%.1f'%toAzanSecs) + Timer( + toAzanSecs, + self.doPlayAzan, + #midnightUtc + azanSec, + ).start() + + + +if __name__=='__main__': + #from scal3 import core + #from scal3.locale_man import rtl + #if rtl: + # gtk.widget_set_default_direction(gtk.TextDirection.RTL) + dialog = LocationDialog(readLocationData()) + dialog.connect('delete-event', gtk.main_quit) + #dialog.connect('response', gtk.main_quit) + dialog.resize(600, 600) + print(dialog.run()) + #gtk.main() + + + + diff --git a/plugins/pray_times_files/pray_times_backend.py b/plugins/pray_times_files/pray_times_backend.py new file mode 100644 index 000000000..5ba4b5bc3 --- /dev/null +++ b/plugins/pray_times_files/pray_times_backend.py @@ -0,0 +1,315 @@ +# -*- coding: utf-8 -*- +#--------------------- Copyright Block ---------------------- +# Prayer Times Calculator +# Copyright (C) 2007-2010 Hamid Zarrabi-Zadeh +# Copyright (C) Saeed Rasooli +# +# Source: http://praytimes.org +# License: GNU General Public License, version 3 +# +# Permission is granted to use this code, with or without +# modification, in any website or application provided that +# the following conditions are met: +# +# 1. Credit is given to the original work with a +# link back to PrayTimes.org. +# +# 2. Redistributions of the source code and its +# translations into other programming languages +# must retain the above copyright notice. +# +# This program is distributed in the hope that it will +# be useful, but WITHOUT ANY WARRANTY. +# +# PLEASE DO NOT REMOVE THIS COPYRIGHT BLOCK. + +# User's Manual: +# http://praytimes.org/manual +# +# Calculation Formulas: +# http://praytimes.org/calculation + +import time +import math +from math import floor + +tr = str ## FIXME + +timeNames = ('imsak', 'fajr', 'sunrise', 'dhuhr', 'asr', 'sunset', 'maghrib', 'isha', 'midnight', 'timezone') + +ASR_STANDARD, ASR_HANAFI = (1, 2) +## asr juristics: +## standard => Shafi`i, Maliki, Ja`fari, Hanbali +## hanafi => Hanafi +## used in which method? FIXME + +MIDNIGHT_STANDARD, MIDNIGHT_JAFARI = list(range(2)) +## midnight methods +## standard => Mid Sunset to Sunrise +## jafari => Mid Maghrib to Fajr + +## Adjust Methods for Higher Latitudes +highLatMethods = ( + 'NightMiddle', # middle of night + 'AngleBased', # angle/60th of night + 'OneSeventh', # 1/7th of night + 'None' # No adjustment +) + +class Method: + def __init__(self, name, desc, fajr=15, isha=15, maghrib='0 min', midnight=MIDNIGHT_STANDARD): + self.name = name + self.desc = desc + self.fajr = fajr + self.isha = isha + self.maghrib = maghrib + self.midnight = midnight + +methodsList = [ + Method('MWL', 'Muslim World League', fajr=18, isha=17), + Method('ISNA', 'Islamic Society of North America', fajr=15, isha=15), + Method('Egypt', 'Egyptian General Authority of Survey', fajr=19.5, isha=17.5), + Method('Makkah', 'Umm Al-Qura University, Makkah', fajr=18.5, isha='90 min'),## fajr was 19 degrees before 1430 hijri + Method('Karachi', 'University of Islamic Sciences, Karachi', fajr=18, isha=18), + Method('Jafari', 'Shia Ithna-Ashari, Leva Research Institute, Qum', fajr=16, maghrib=4, isha=14, midnight=MIDNIGHT_JAFARI), + Method('Tehran', 'Institute of Geophysics, University of Tehran', fajr=17.7, maghrib=4.5, midnight=MIDNIGHT_JAFARI), +] + +methodsDict = dict([(m.name, m) for m in methodsList]) + +########################### Functions #################################### + +isMin = lambda tm: isinstance(tm, str) and tm.endswith('min') +minEval = lambda tm: float(tm.split(' ')[0]) if isinstance(tm, str) else tm +dirSign = lambda direction: -1 if direction=='ccw' else 1 + +dtr = lambda d: (d * math.pi) / 180.0 +rtd = lambda r: (r * 180.0) / math.pi + +sin = lambda d: math.sin(dtr(d)) +cos = lambda d: math.cos(dtr(d)) +tan = lambda d: math.tan(dtr(d)) + +arcsin = lambda d: rtd(math.asin(d)) +arccos = lambda d: rtd(math.acos(d)) +arctan = lambda d: rtd(math.atan(d)) + +arccot = lambda x: rtd(math.atan(1.0/x)) +arctan2 = lambda y, x: rtd(math.atan2(y, x)) + +def fix(a, b): + a = a - b*floor(a/b) + return a+b if a<0 else a +fixAngle = lambda a: fix(a, 360) +fixHour = lambda a: fix(a, 24) + +timeDiff = lambda time1, time2: fixHour(time2 - time1) +timesMiddle = lambda time1, time2: time1 + fixHour(time2 - time1)/2.0 + +################################ Classes ################################ + +class PrayTimes: + numIterations = 1 + def __init__(self, lat, lng, elv=0, methodName='Tehran', imsak='10 min', asrMode=ASR_STANDARD, + highLats='NightMiddle', timeFormat='24h'): + ''' + timeFormat possible values: '24h', '12h', '12hNS' (12-hour format with no suffix), 'Float' + ''' + self.lat = lat + self.lng = lng + self.elv = elv + self.method = methodsDict[methodName] + self.imsak = imsak + self.asrMode = asrMode + self.highLats = highLats + self.timeFormat = timeFormat + + # return prayer times for a given julian day + def getTimesByJd(self, jd, utcOffset): + #if time.daylight and time.gmtime(core.getEpochFromJd(jd)): + #print(time.gmtime((jd-2440588)*(24*3600)).tm_isdst) + self.utcOffset = utcOffset + self.jDate = jd - 0.5 - self.lng/(15*24) + return self.computeTimes() + + # convert float time to the given format (see timeFormats) + def getFormattedTime(self, tm, format=None): + assert isinstance(tm, float) + if not format: + format = self.timeFormat + if format == 'float': + return tm + else: + tm = fixHour(tm+0.5/60) ## add 0.5 minutes to round + hours = floor(tm) + minutes = floor((tm-hours)*60) + if format == '24h': + return '%d:%.2d'%(hours, minutes) + elif format == '12h': + return '%d:%.2d %s'%( + (hours-1)%12 + 1, + minutes, + tr('AM') if hours<12 else tr('PM'), + ) + elif format == '12hNS': + return '%d:%.2d'%( + (hours-1)%12 + 1, + minutes, + ) + else: + raise ValueError('bad time format %s'%format) + + # compute mid-day time + def midDay(self, tm): + return fixHour(12-self.sunEquation(self.jDate+tm)) + + # compute the time at which sun reaches a specific angle below horizon + def sunAngleTime(self, angle, tm, direction='cw'): + decl = self.sunDeclination(self.jDate+tm) + noon = self.midDay(tm) + ratio = (-sin(angle) - sin(decl)*sin(self.lat)) / (cos(decl)*cos(self.lat)) + ratio = min(max(ratio, -1.0), 1.0) + #try: + t = arccos(ratio) / 15.0 + #except: + # print('sunAngleTime: angle=%s, tm=%s, direction=%s ==> ratio=%s'%(angle, tm, direction, ratio)) + # return 0 + return noon + dirSign(direction)*t + + # compute asr time + def asrTime(self, factor, tm): + return self.sunAngleTime( + -arccot(factor + tan(abs(self.lat-self.sunDeclination(self.jDate+tm)))), + tm, + ) + + + ''' + # compute declination angle of sun and equation of time + # Ref: http://aa.usno.navy.mil/faq/docs/SunApprox.php + def sunPosition(self, jd): + D = jd - 2451545.0 + g = fixAngle(357.529 + 0.98560028*D) + q = fixAngle(280.459 + 0.98564736*D) + L = fixAngle(q + 1.915*sin(g) + 0.020*sin(2*g)) + + R = 1.00014 - 0.01671*cos(g) - 0.00014*cos(2*g) + e = 23.439 - 0.00000036*D + + RA = arctan2(cos(e)*sin(L), cos(L)) / 15.0 + eqt = q/15 - fixHour(RA) + decl = arcsin(sin(e)*sin(L)) + + return {'declination': decl, 'equation': eqt} + ''' + + def sunDeclination(self, jd): + D = jd - 2451545.0 + g = fixAngle(357.529 + 0.98560028*D) + q = fixAngle(280.459 + 0.98564736*D) + L = fixAngle(q + 1.915*sin(g) + 0.020*sin(2*g)) + e = 23.439 - 0.00000036*D + return arcsin(sin(e)*sin(L)) + + def sunEquation(self, jd): + D = jd - 2451545.0 + g = fixAngle(357.529 + 0.98560028*D) + q = fixAngle(280.459 + 0.98564736*D) + L = fixAngle(q + 1.915*sin(g) + 0.020*sin(2*g)) + e = 23.439 - 0.00000036*D + RA = arctan2(cos(e)*sin(L), cos(L)) / 15.0 + return q/15.0 - fixHour(RA) + + #---------------------- Compute Prayer Times ----------------------- + + # compute prayer times + def computeTimes(self, format=None): + # default times + times = { + 'imsak': 5, + 'fajr': 5, + 'sunrise': 6, + 'dhuhr': 12, + 'asr': 13, + 'sunset': 18, + 'maghrib': 18, + 'isha': 18, + } + + # main iterations + for i in range(self.numIterations): + ## computePrayerTimes + ## dayPortion + for key in times: + times[key] /= 24.0 + times['imsak'] = self.sunAngleTime(minEval(self.imsak), times['imsak'], 'ccw') + times['fajr'] = self.sunAngleTime(minEval(self.method.fajr), times['fajr'], 'ccw') + times['sunrise'] = self.sunAngleTime(self.riseSetAngle(), times['sunrise'], 'ccw') + times['dhuhr'] = self.midDay(times['dhuhr']) + times['asr'] = self.asrTime(self.asrMode, times['asr']) + times['sunset'] = self.sunAngleTime(self.riseSetAngle(), times['sunset']) + times['maghrib'] = self.sunAngleTime(minEval(self.method.maghrib), times['maghrib']) + times['isha'] = self.sunAngleTime(minEval(self.method.isha), times['isha']) + + ## adjustTimes + for key in times: + times[key] += self.utcOffset - self.lng/15.0 + if self.highLats != 'None': + ## adjustHighLats + nightTime = timeDiff(times['sunset'], times['sunrise']) + times['imsak'] = self.adjustHLTime(times['imsak'], times['sunrise'], minEval(self.imsak), nightTime, 'ccw') + times['fajr'] = self.adjustHLTime(times['fajr'], times['sunrise'], minEval(self.method.fajr), nightTime, 'ccw') + times['isha'] = self.adjustHLTime(times['isha'], times['sunset'], minEval(self.method.isha), nightTime) + times['maghrib'] = self.adjustHLTime(times['maghrib'], times['sunset'], minEval(self.method.maghrib), nightTime) + + if isMin(self.imsak): + times['imsak'] = times['fajr'] - minEval(self.imsak)/60.0 + if isMin(self.method.maghrib): + times['maghrib'] = times['sunset'] + minEval(self.method.maghrib)/60.0 + if isMin(self.method.isha): + times['isha'] = times['maghrib'] + minEval(self.method.isha)/60.0 + + # add midnight time + times['midnight'] = timesMiddle( + times['sunset'], + times['fajr' if self.method.midnight == MIDNIGHT_JAFARI else 'sunrise'], + ) + + for key in times: + times[key] = times[key] % 24.0 + + #times = self.tuneTimes(times) ## FIXME + #for key in times: + # times[key] = self.getFormattedTime(times[key], format) + + times['timezone'] = 'GMT%+.1f'%self.utcOffset ## utcOffset is not timeZone FIXME + + return times + + # return sun angle for sunset/sunrise + def riseSetAngle(self): + #earthRad = 6371009 ## in meters + #angle = arccos(earthRad/(earthRad+self.elv)) + angle = 0.0347 * math.sqrt(self.elv) ## an approximation + return 0.833 + angle + + #def tuneTimes: ## FIXME + + # adjust a time for higher latitudes + def adjustHLTime(self, tm, base, angle, night, direction='cw'): + ## nightPortion: the night portion used for adjusting times in higher latitudes + if self.highLats == 'AngleBased': + portion = angle/60.0 + elif self.highLats == 'OneSeventh': + portion = 1.0/7 + else: + portion = 0.5 ## MidNight + portion *= night + + diff = timeDiff(tm, base) if direction=='ccw' else timeDiff(base, tm) + + if diff > portion: + tm = base + dirSign(direction)*portion + return tm + + diff --git a/plugins/pray_times_files/pray_times_gtk.py b/plugins/pray_times_files/pray_times_gtk.py new file mode 100644 index 000000000..2c99147dd --- /dev/null +++ b/plugins/pray_times_files/pray_times_gtk.py @@ -0,0 +1,491 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# Also avalable in /usr/share/common-licenses/LGPL on Debian systems +# or /usr/share/licenses/common/LGPL/license.txt on ArchLinux + +import os +from os.path import dirname +import math + +from scal3 import locale_man +from scal3.locale_man import tr as _ +from pray_times_backend import timeNames, methodsList +from pray_times_utils import * + +from scal3.ui_gtk import * +from scal3.ui_gtk.app_info import popenFile +from scal3.ui_gtk.about import AboutDialog +## do I have to duplicate AboutDialog class code? + +buffer_get_text = lambda b: b.get_text(b.get_start_iter(), b.get_end_iter(), True) +buffer_select_all = lambda b: b.select_range(b.get_start_iter(), b.get_end_iter()) + + +dataDir = dirname(__file__) +earthR = 6370 + + + +class LocationDialog(gtk.Dialog): + EXIT_OK = 0 + EXIT_CANCEL = 1 + def __init__(self, cityData, maxResults=200, width=600, height=600, **kwargs): + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Location')) + self.maxResults = maxResults + self.resize(width, height) + ## width is used for CellRendererText as well + ############### + cancelB = self.add_button(gtk.STOCK_CANCEL, self.EXIT_CANCEL) + okB = self.add_button(gtk.STOCK_OK, self.EXIT_OK) + #if autoLocale: + cancelB.set_label(_('_Cancel')) + cancelB.set_image(gtk.Image.new_from_stock(gtk.STOCK_CANCEL, gtk.IconSize.BUTTON)) + okB.set_label(_('_OK')) + okB.set_image(gtk.Image.new_from_stock(gtk.STOCK_OK, gtk.IconSize.BUTTON)) + self.okB = okB + ############### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Search Cities:'))) + entry = gtk.Entry() + pack(hbox, entry, 1, 1) + entry.connect('changed', self.entry_changed) + pack(self.vbox, hbox) + ###################### + treev = gtk.TreeView() + treev.set_headers_clickable(False) + treev.set_headers_visible(False) + trees = gtk.ListStore(int, str) + treev.set_model(trees) + swin = gtk.ScrolledWindow() + swin.add(treev) + swin.set_policy(gtk.PolicyType.AUTOMATIC, gtk.PolicyType.AUTOMATIC) + pack(self.vbox, swin, 1, 1) + self.treev = treev + self.trees = trees + treev.connect('cursor-changed', self.treev_cursor_changed) + ######### + #cell = gtk.CellRendererText() + #col = gtk.TreeViewColumn('Index', cell, text=0) + #col.set_resizable(True)## No need! + #treev.append_column(col) + ######## + cell = gtk.CellRendererText() + cell.set_fixed_size(width-30, -1) + col = gtk.TreeViewColumn('City', cell, text=1) + #col.set_resizable(True)## No need! + treev.append_column(col) + ######### + treev.set_search_column(1) + ########### + frame = gtk.Frame() + checkb = gtk.CheckButton(_('Edit Manually')) + checkb.connect('clicked', self.edit_checkb_clicked) + frame.set_label_widget(checkb) + self.checkbEdit = checkb + vbox = gtk.VBox() + group = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Name:')) + pack(hbox, label) + group.add_widget(label) + label.set_alignment(0, 0.5) + entry = gtk.Entry() + pack(hbox, entry, 1, 1) + pack(vbox, hbox) + self.entry_edit_name = entry + #### + hbox = gtk.HBox() + label = gtk.Label(_('Latitude:')) + pack(hbox, label) + group.add_widget(label) + label.set_alignment(0, 0.5) + spin = gtk.SpinButton() + spin.set_increments(1, 10) + spin.set_range(-180, 180) + spin.set_digits(3) + spin.set_direction(gtk.TextDirection.LTR) + pack(hbox, spin) + pack(vbox, hbox) + self.spin_lat = spin + #### + hbox = gtk.HBox() + label = gtk.Label(_('Longitude:')) + pack(hbox, label) + group.add_widget(label) + label.set_alignment(0, 0.5) + spin = gtk.SpinButton() + spin.set_increments(1, 10) + spin.set_range(-180, 180) + spin.set_digits(3) + spin.set_direction(gtk.TextDirection.LTR) + pack(hbox, spin) + pack(vbox, hbox) + self.spin_lng = spin + #### + hbox = gtk.HBox() + self.lowerLabel = gtk.Label('') + pack(hbox, self.lowerLabel, 1, 1) + self.lowerLabel.set_alignment(0, 0.5) + button = gtk.Button(_('Calculate Nearest City')) + button.connect('clicked', self.calc_clicked) + pack(hbox, button) + pack(vbox, hbox) + #### + vbox.set_sensitive(False) + frame.add(vbox) + self.vbox_edit = vbox + pack(self.vbox, frame) + ### + self.vbox.show_all() + ######### + self.cityData = cityData + self.update_list() + def calc_clicked(self, button): + lat = self.spin_lat.get_value() + lng = self.spin_lng.get_value() + md = earthR*2*math.pi + city = '' + for (name, lname, lat2, lng2) in self.cityData: + d = earthDistance(lat, lng, lat2, lng2) + assert d>=0 + if d=mr: + break + self.treev.scroll_to_cell((0, 0)) + self.okB.set_sensitive(self.checkbEdit.get_active()) + entry_changed = lambda self, entry: self.update_list(entry.get_text()) + def run(self): + ex = gtk.Dialog.run(self) + self.hide() + if ex==self.EXIT_OK: + if self.checkbEdit.get_active() or self.treev.get_cursor()[0]!=None:#????????????????? + return (self.entry_edit_name.get_text(), self.spin_lat.get_value(), self.spin_lng.get_value()) + return None + + +class LocationButton(gtk.Button): + def __init__(self, cityData, locName, lat, lng, window=None): + gtk.Button.__init__(self) + self.setLocation(locName, lat, lng) + self.dialog = LocationDialog(cityData, parent=window) + #### + self.connect('clicked', self.onClicked) + def setLocation(self, locName, lat, lng): + self.locName = locName + self.lat = lat + self.lng = lng + self.set_label(self.locName) + def onClicked(self, widget): + res = self.dialog.run() + if res: + locName, lat, lng = res + self.setLocation(locName, lat, lng) + + + + +class TextPluginUI: + def makeWidget(self): + self.confDialog = gtk.Dialog() + self.confDialog.set_title(_('Pray Times') + ' - ' + _('Configuration')) + group = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ### + hbox = gtk.HBox() + label = gtk.Label(_('Location')) + group.add_widget(label) + label.set_alignment(0, 0.5) + pack(hbox, label) + self.locButton = LocationButton( + self.cityData, + self.locName, + self.backend.lat, + self.backend.lng, + window=self.confDialog, + ) + pack(hbox, self.locButton) + pack(self.confDialog.vbox, hbox) + ### + hbox = gtk.HBox() + label = gtk.Label(_('Calculation Method')) + group.add_widget(label) + label.set_alignment(0, 0.5) + pack(hbox, label) + self.methodCombo = gtk.ComboBoxText() + for methodObj in methodsList: + self.methodCombo.append_text(_(methodObj.desc)) + pack(hbox, self.methodCombo) + pack(self.confDialog.vbox, hbox) + ####### + treev = gtk.TreeView() + treev.set_headers_clickable(False) + treev.set_headers_visible(False) + trees = gtk.ListStore(bool, str, str)## enable, desc, name + treev.set_model(trees) + ### + cell = gtk.CellRendererToggle() + #cell.set_property('activatable', True) + cell.connect('toggled', self.shownTreeviewCellToggled) + col = gtk.TreeViewColumn(_('Enable'), cell) + col.add_attribute(cell, 'active', 0) + #cell.set_active(False) + col.set_resizable(True) + treev.append_column(col) + ### + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('Name'), cell, text=1)## desc, not name + treev.append_column(col) + ### + self.shownTimesTreestore = trees + for name in timeNames: + trees.append([True, _(name.capitalize()), name]) + frame = gtk.Frame() + frame.set_label(_('Shown Times')) + frame.add(treev) + pack(self.confDialog.vbox, frame) + ###### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Imsak'))) + spin = gtk.SpinButton() + spin.set_increments(1, 5) + spin.set_range(0, 99) + spin.set_digits(0) + spin.set_direction(gtk.TextDirection.LTR) + self.imsakSpin = spin + pack(hbox, spin) + pack(hbox, gtk.Label(' '+_('minutes before fajr'))) + pack(self.confDialog.vbox, hbox) + ###### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Seperator'))) + textview = gtk.TextView() + textview.set_wrap_mode(gtk.WrapMode.CHAR) + if locale_man.rtl: + textview.set_direction(gtk.TextDirection.RTL) + self.sepView = textview + self.sepBuff = textview.get_buffer() + frame = gtk.Frame() + frame.set_border_width(4) + frame.add(textview) + pack(hbox, frame, 1, 1) + pack(self.confDialog.vbox, hbox) + ###### + hbox = gtk.HBox() + frame = gtk.Frame() + #frame.set_border_width(5) + frame.set_label(_('Azan')) + hbox.set_border_width(5) + vboxFrame = gtk.VBox() + vboxFrame.set_border_width(10) + ##### + sgroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + #sgroupFcb = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + #### + hbox1 = gtk.HBox() + self.preAzanEnableCheck = gtk.CheckButton(_('Play Pre-Azan Sound')) + sgroup.add_widget(self.preAzanEnableCheck) + hbox2 = gtk.HBox() + self.preAzanEnableCheck.box = hbox2 + self.preAzanEnableCheck.connect('clicked', lambda w: w.box.set_sensitive(w.get_active())) + pack(hbox1, self.preAzanEnableCheck) + pack(hbox2, gtk.Label(' ')) + self.preAzanFileButton = gtk.FileChooserButton(_('Pre-Azan Sound')) + #sgroupFcb.add_widget(self.preAzanFileButton) + pack(hbox2, self.preAzanFileButton, 1, 1) + pack(hbox2, gtk.Label(' ')) + ## + spin = gtk.SpinButton() + spin.set_increments(1, 5) + spin.set_range(0, 60) + spin.set_digits(2) + spin.set_direction(gtk.TextDirection.LTR) + self.preAzanMinutesSpin = spin + pack(hbox2, spin) + ## + pack(hbox2, gtk.Label(' ')) + pack(hbox2, gtk.Label(_('minutes before azan'))) + pack(hbox1, hbox2, 1, 1) + pack(vboxFrame, hbox1) + ##### + hbox1 = gtk.HBox() + self.azanEnableCheck = gtk.CheckButton(_('Play Azan Sound')) + sgroup.add_widget(self.azanEnableCheck) + hbox2 = gtk.HBox() + self.azanEnableCheck.box = hbox2 + self.azanEnableCheck.connect('clicked', lambda w: w.box.set_sensitive(w.get_active())) + pack(hbox1, self.azanEnableCheck) + pack(hbox2, gtk.Label(' ')) + self.azanFileButton = gtk.FileChooserButton(_('Azan Sound')) + #sgroupFcb.add_widget(self.azanFileButton) + pack(hbox2, self.azanFileButton, 1, 1) + #pack(hbox2, gtk.Label(''), 1, 1) + ## + pack(hbox1, hbox2, 1, 1) + pack(vboxFrame, hbox1) + ##### + frame.add(vboxFrame) + pack(hbox, frame, 1, 1) + pack(self.confDialog.vbox, hbox) + ###### + self.updateConfWidget() + ### + cancelB = self.confDialog.add_button(gtk.STOCK_CANCEL, 1) + okB = self.confDialog.add_button(gtk.STOCK_OK, 3) + #if autoLocale: + cancelB.set_label(_('_Cancel')) + cancelB.set_image(gtk.Image.new_from_stock(gtk.STOCK_CANCEL, gtk.IconSize.BUTTON)) + okB.set_label(_('_OK')) + okB.set_image(gtk.Image.new_from_stock(gtk.STOCK_OK, gtk.IconSize.BUTTON)) + cancelB.connect('clicked', self.confDialogCancel) + okB.connect('clicked', self.confDialogOk) + ### + self.confDialog.vbox.show_all() + ############## + ''' + submenu = gtk.Menu() + submenu.add(gtk.MenuItem('Item 1')) + submenu.add(gtk.MenuItem('Item 2')) + #self.submenu = submenu + self.menuitem = gtk.MenuItem('Owghat') + self.menuitem.set_submenu(submenu) + self.menuitem.show_all() + ''' + self.dialog = None + def updateConfWidget(self): + self.locButton.setLocation(self.locName, self.backend.lat, self.backend.lng) + self.methodCombo.set_active(methodsList.index(self.backend.method)) + ### + for row in self.shownTimesTreestore: + row[0] = (row[2] in self.shownTimeNames) + ### + self.imsakSpin.set_value(self.imsak) + self.sepBuff.set_text(self.sep) + buffer_select_all(self.sepBuff) + ### + self.preAzanEnableCheck.set_active(self.preAzanEnable) + self.preAzanEnableCheck.box.set_sensitive(self.preAzanEnable) + if self.preAzanFile: + self.preAzanFileButton.set_filename(self.preAzanFile) + self.preAzanMinutesSpin.set_value(self.preAzanMinutes) + ## + self.azanEnableCheck.set_active(self.azanEnable) + self.azanEnableCheck.box.set_sensitive(self.azanEnable) + if self.azanFile: + self.azanFileButton.set_filename(self.azanFile) + def updateConfVars(self): + self.locName = self.locButton.locName + self.backend.lat = self.locButton.lat + self.backend.lng = self.locButton.lng + self.backend.method = methodsList[self.methodCombo.get_active()] + self.shownTimeNames = [row[2] for row in self.shownTimesTreestore if row[0]] + self.imsak = int(self.imsakSpin.get_value()) + self.sep = buffer_get_text(self.sepBuff) + self.backend.imsak = '%d min'%self.imsak + ### + self.preAzanEnable = self.preAzanEnableCheck.get_active() + self.preAzanFile = self.preAzanFileButton.get_filename() + self.preAzanMinutes = self.preAzanMinutesSpin.get_value() + ## + self.azanEnable = self.azanEnableCheck.get_active() + self.azanFile = self.azanFileButton.get_filename() + def confDialogCancel(self, widget): + self.confDialog.hide() + self.updateConfWidget() + def confDialogOk(self, widget): + self.confDialog.hide() + self.updateConfVars() + self.saveConfig() + def shownTreeviewCellToggled(self, cell, path): + i = int(path) + active = not cell.get_active() + self.shownTimesTreestore[i][0] = active + cell.set_active(active) + def set_dialog(self, dialog): + self.dialog = dialog + def open_configure(self): + self.confDialog.run() + def open_about(self): + about = AboutDialog( + name=self.title, + title=_('About')+' '+self.title, + authors=self.authors, + comments=self.about, + ) + about.connect('delete-event', lambda w, e: about.destroy()) + #about.connect('response', lambda w: about.hide()) + #about.set_skip_taskbar_hint(True) + about.run() + about.destroy() + + + + + + + + + diff --git a/plugins/pray_times_files/pray_times_utils.py b/plugins/pray_times_files/pray_times_utils.py new file mode 100644 index 000000000..8bec9209a --- /dev/null +++ b/plugins/pray_times_files/pray_times_utils.py @@ -0,0 +1,35 @@ +import math +from math import pi + +earthR = 6370 + +sind = lambda x: math.sin(pi/180.0*x) +cosd = lambda x: math.cos(pi/180.0*x) +#tand = lambda x: math.tan(pi/180.0*x) +#asind = lambda x: math.asin(x)*180.0/pi +#acosd = lambda x: math.acos(x)*180.0/pi +#atand = lambda x: math.atan(x)*180.0/pi +#loc2hor = lambda z, delta, lat: acosd((cosd(z)-sind(delta)*sind(lat))/cosd(delta)/cosd(lat))/15.0 + +def earthDistance(lat1, lng1, lat2, lng2): + #if lat1==lat2 and lng1==lng2: + # return 0 + dx = lng2 - lng1 + if dx<0: + dx += 360 + if dx>180: + dx = 360-dx + #### + dy = lat2 - lat1 + if dy<0: + dy += 360 + if dy>180: + dy = 360-dy + #### + deg = math.acos(cosd(dx)*cosd(dy)) + if deg > pi: + deg = 2*pi - deg + return deg*earthR + #return ang*180/pi + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/scal3/__init__.py b/scal3/__init__.py new file mode 100644 index 000000000..669431679 --- /dev/null +++ b/scal3/__init__.py @@ -0,0 +1 @@ +__all__ = ['path', 'cal_types', 'core', 'ui', 'ui_gtk', 'ui_qt', 'plugins', 'locale', 'plugin_man', 'event_lib'] diff --git a/scal3/account/__init__.py b/scal3/account/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scal3/account/google.py b/scal3/account/google.py new file mode 100644 index 000000000..b90f0d72c --- /dev/null +++ b/scal3/account/google.py @@ -0,0 +1,554 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +developerKey = 'AI39si4QJ0bmdZJd7nVz0j3zuo1JYS3WUJX8y0f2mvGteDtiKY8TUSzTsY4oAcGlYAM0LmOxHmWWyFLU'## FIXME + +import sys +from os.path import splitext +from time import time as now +import http.server + +from pprint import pprint, pformat + +try: + from urllib.parse import parse_qsl +except ImportError: + from cgi import parse_qsl + +import httplib2 +from httplib2 import * + +from scal3.path import * + +sys.path.append(join(rootDir, 'google-api-python-client'))## FIXME +sys.path.append(join(rootDir, 'oauth2client'))## FIXME + +from scal3.utils import toBytes, toStr + +from scal3.ics import * +from scal3.cal_types import to_jd, jd_to, DATE_GREG +from scal3.locale_man import tr as _ +from scal3 import core + +from scal3 import event_lib +from scal3.event_lib import Account + + +auth_local_webserver = True +auth_host_name = 'localhost' +auth_host_port = [8080, 8090] + + +STATUS_UNCHANCHED, STATUS_ADDED, STATUS_DELETED, STATUS_MODIFIED = range(4) + +calcEtag = lambda gevent: core.compressLongInt(abs(hash(repr(gevent)))) + +decodeIcsStartEnd = lambda value: { + ('dateTime' if 'T' in value else 'date'): value, + 'timeZone': 'GMT', +} + +def encodeIcsStartEnd(value): + timeZone = value.get('timeZone', 'GMT')## FIXME + if 'date' in value: + icsValue = value['date'].replace('-', '') + elif 'dateTime' in value: + icsValue = value['dateTime'].replace('-', '').replace(':', '') + else: + raise ValueError('bad gcal start/end value %r'%value) + return icsValue + + +def exportEvent(event): + if not event.changeMode(DATE_GREG): + return + icsData = event.getIcsData(True) + if not icsData: + return + gevent = { + 'kind': 'calendar#event', + 'summary': toStr(event.summary), + 'description': toStr(event.description), + 'attendees': [], + 'status': 'confirmed', + 'visibility': 'default', + 'guestsCanModify': False, + 'reminders': { + 'overrides': { + 'minutes': event.getNotifyBeforeMin(), + 'method': 'popup',## FIXME + }, + }, + 'extendedProperties':{ + 'shared': { + 'starcal_id': event.id, + 'starcal_type': event.name, + }, + } + } + for key, value in icsData: + key = key.upper() + if key=='DTSTART': + gevent['start'] = decodeIcsStartEnd(value) + elif key=='DTEND': + gevent['end'] = decodeIcsStartEnd(value) + elif key in ('RRULE', 'RDATE', 'EXRULE', 'EXDATE'): + if not 'recurrence' in gevent: + gevent['recurrence'] = [] + gevent['recurrence'].append(key + ':' + value) + elif key=='TRANSP': + gevent['transparency'] = value.lower() + #elif key=='CATEGORIES': + return gevent + +#def exportToEvent(event, group, gevent):## FIXME + + +def importEvent(gevent, group): + #open('/tmp/gevent.js', 'a').write('%s\n\n'%pformat(gevent)) + icsData = [ + ('DTSTART', encodeIcsStartEnd(gevent['start'])), + ('DTEND', encodeIcsStartEnd(gevent['end'])), + ] + ## + recurring = False + if 'recurrence' in gevent: + recurring = True + for recStr in gevent['recurrence']: + key, value = recStr.upper().split(':')## multi line? FIXME + icsData.append((key, value)) + try: + eventType = gevent['extendedProperties']['shared']['starcal_type'] + except KeyError: + if recurring: + eventType = 'custom' + else: + eventType = 'task' + ## + event = group.createEvent(eventType) + event.mode = DATE_GREG ## FIXME + if not event.setIcsData(dict(icsData)): + return + event.summary = toBytes(gevent['summary']) + event.description = toBytes(gevent.get('description', '')) + if 'reminders' in gevent: + try: + minutes = gevent['reminders']['overrides']['minutes'] + except KeyError: + myRaise()## FIXME + else: + self.notifyBefore = (minutes, 60) + return event + + + +class ClientRedirectServer(http.server.HTTPServer): + """A server to handle OAuth 2.0 redirects back to localhost. + + Waits for a single request and parses the query parameters + into query_params and then stops serving. + """ + query_params = {} + + +class ClientRedirectHandler(http.server.BaseHTTPRequestHandler): + """A handler for OAuth 2.0 redirects back to localhost. + + Waits for a single request and parses the query parameters + into the servers query_params and then stops serving. + """ + + def do_GET(s): + """Handle a GET request. + + Parses the query parameters and prints a message + if the flow has completed. Note that we can't detect + if an error occurred. + """ + s.send_response(200) + s.send_header("Content-type", "text/html") + s.end_headers() + query = s.path.split('?', 1)[-1] + query = dict(parse_qsl(query)) + s.server.query_params = query + s.wfile.write(b"Authentication Status") + s.wfile.write(b"

The authentication flow has completed.

") + s.wfile.write(b"") + + def log_message(self, format, *args): + """Do not log messages to stdout while running as command line program.""" + pass + + +def dumpRequest(request): + open('/tmp/starcal-request', 'a').write('uri=%r\nmethod=%r\nheaders=%r\nbody=%r\n\n\n'%( + request.uri, + request.method, + request.headers, + request.body, + )) + + +@event_lib.classes.account.register +class GoogleAccount(Account): + name = 'google' + desc = _('Google') + paramsOrder = Account.paramsOrder + ('email',) + params = Account.params + ('email',) + def __init__(self, aid=None, email=''): + from oauth2client.client import OAuth2WebServerFlow + Account.__init__(self, aid) + self.authFile = splitext(self.file)[0] + '.oauth2' + self.email = email + self.flow = OAuth2WebServerFlow( + client_id='536861675971.apps.googleusercontent.com', + client_secret='BviBsCKTbXrzY0hZbioS6FAt', + scope=[ + 'https://www.googleapis.com/auth/calendar', + 'https://www.googleapis.com/auth/tasks', + ], + user_agent='%s/%s'%(core.APP_NAME, core.VERSION), + ) + def getData(self): + data = Account.getData(self) + data.update({ + 'email': self.email, + }) + return data + def setData(self, data): + Account.setData(self, data) + for attr in ('email',): + try: + setattr(self, attr, data[attr]) + except KeyError: + pass + askVerificationCode = lambda self: input('Enter verification code: ').strip() + def showError(self, error): + sys.stderr.write(error+'\n') + def showHttpException(self, e): + self.showError( + _('HTTP Error') + '\n' + + _('Error Code') + ': ' + _(e.resp.status) + '\n' + + _('Error Message') + ': ' + _(e._get_reason().strip()) + ) + def authenticate(self): + global auth_local_webserver + import socket + from oauth2client.file import Storage + storage = Storage(self.authFile) + credentials = storage.get() + if credentials and not credentials.invalid: + return credentials + + if auth_local_webserver: + success = False + port_number = 0 + for port in auth_host_port: + port_number = port + try: + httpd = ClientRedirectServer( + (auth_host_name, port), + ClientRedirectHandler, + ) + except socket.error as e: + print('-------- counld no use port %s for local web server: %s'%(port, e)) + pass + else: + success = True + break + auth_local_webserver = success + + if auth_local_webserver: + oauth_callback = 'http://%s:%s/' % (auth_host_name, port_number) + else: + oauth_callback = 'oob' + core.openUrl(self.flow.step1_get_authorize_url(oauth_callback)) + + code = None + if auth_local_webserver: + httpd.handle_request() + if 'error' in httpd.query_params: + self.showError(_('Authentication request was rejected.')) + return + if 'code' in httpd.query_params: + code = httpd.query_params['code'] + else: + self.showError(_('Failed to find "code" in the query parameters of the redirect.')) + return + else: + code = self.askVerificationCode() + try: + credential = self.flow.step2_exchange(code) + except Exception as e: + self.showError(_('Authentication has failed')+':\n%s'%e) + return + storage.put(credential) + credential.set_store(storage) + return credentials + def getHttp(self): + credentials = self.authenticate() + if not credentials: + return False + http = credentials.authorize(httplib2.Http()) + http.request = lambda uri, *args, **kwargs:\ + httplib2.Http.request(http, toStr(uri), *args, **kwargs) + #http.request('google.com') + return http + def getCalendarService(self): + from apiclient.discovery import build, HttpError + try: + return build( + serviceName='calendar', + version='v3', + http=self.getHttp(), + developerKey=developerKey, + ) + ## returns a Resource instance + except HttpError as e: + self.showHttpException(e) + def getTasksService(self): + from apiclient.discovery import build, HttpError + try: + return build( + serviceName='tasks', + version='v1', + http=self.getHttp(), + developerKey=developerKey, + ) + except HttpError as e: + self.showHttpException(e) + def addNewGroup(self, title): + service = self.getCalendarService() + if not service: + return + service.calendars().insert( + body={ + 'kind': 'calendar#calendar', + 'summary': title, + } + ).execute()['id'] + def deleteGroup(self, remoteGroupId): + service = self.getCalendarService() + if not service: + return + service.calendars().delete(calendarId=remoteGroupId).execute() + def fetchGroups(self): + service = self.getCalendarService() + if not service: + return + groups = [] + for group in service.calendarList().list().execute()['items']: + #print('group =', group) + groups.append({ + 'id': group['id'], + 'title': group['summary'], + }) + self.remoteGroups = groups + return True + def fetchAllEventsInGroup(self, remoteGroupId): + service = self.getCalendarService() + if not service: + return + eventsRes = service.events().list( + calendarId=remoteGroupId, + orderBy='updated', + ).execute() + return eventsRes.get('items', []) + def sync(self, group, remoteGroupId, resPerPage=1000): + from apiclient.discovery import HttpError + ## if remoteGroupId=='tasks':## FIXME + ## service = self.getTasksService() + service = self.getCalendarService() + if not service: + return + lastSync = group.getLastSync() + funcStartTime = now() + ########################### Pull + #print('------------------- pulling...') + kwargs = dict( + calendarId=remoteGroupId, + orderBy='updated', + showDeleted=True,## with event.status == 'cancelled', + maxResults=resPerPage, + #timeZone="GMT", + #pageToken=0, + ) + if lastSync: + kwargs['updatedMin'] = getIcsTimeByEpoch(lastSync, True) ## FIXME + ## int(lastSync) + #print(kwargs) + request = service.events().list(**kwargs) + ## request is a HttpRequest instance + #dumpRequest(request) + try: + geventsRes = request.execute() + except HttpError as e: + self.showHttpException(e) + return False + #pprint(geventsRes) + try: + gevents = geventsRes['items'] + except KeyError: + gevents = [] + #pprint(gevents) + diff = {} + def addToDiff(key, here, status, *args): + value = (status, here) + args + try: + diff[key].append(value) + except KeyError: + diff[key] = [value] + for gevent in gevents: + remoteIds = (self.id, remoteGroupId, gevent['id']) + ### + try: + #eventId = group.eventIdByRemoteIds[remoteIds] + eventId = gevent['extendedProperties']['shared']['starcal_id'] + except KeyError: + eventId = None + ### + bothId = (eventId, gevent['id']) + if gevent['status'] == 'cancelled': + if eventId is not None: + addToDiff(bothId, False, STATUS_DELETED) + #group.remove(group[eventId]) + #group.save() ## FIXME + if gevent['status'] != 'confirmed':## FIXME + print(gevent['status'], gevent['summary']) + continue + event = importEvent(gevent, group) + if not event: + #print('---------- event can not be pulled: %s'%pformat(gevent)) + continue + event.remoteIds = remoteIds + if eventId is None: + addToDiff(bothId, False, STATUS_ADDED, event) + #event.afterModify() + #group.append(event) + #event.save() + #group.save() + #print('---------- event %s added in starcal'%event.summary) + else: + addToDiff(bothId, False, STATUS_MODIFIED, event) + #local_event = group[eventId] + #local_event.copyFrom(event) + #local_event.save() + #group.afterSync()## FIXME + #group.save()## FIXME + ########################### Push + #print('------------------- pushing...') + ## if remoteGroupId=='tasks':## FIXME + for eventId, eventRemoteAttrs in group.deletedRemoteEvents.items(): + deletedEpoch, tmp_accountId, tmp_remoteGroupId, remoteEventId = eventRemoteAttrs + if deletedEpoch > funcStartTime: + continue + if (tmp_accountId, tmp_remoteGroupId) != (self.id, remoteGroupId): + del group.deletedRemoteEvents[eventId] + continue + bothId = (eventId, remoteEventId) + addToDiff(bothId, True, STATUS_DELETED) + for event in group: + if event.modified > funcStartTime: + continue + #print('---------- event %s'%event.summary) + remoteEventId = None + if event.remoteIds: + if event.remoteIds[:2] == (self.id, remoteGroupId): + remoteEventId = event.remoteIds[2] + #print('---------- remoteEventId = %s'%remoteEventId) + if remoteEventId and lastSync and event.modified < lastSync: + #print('---------- skipping event %s (modified = %s < %s = lastPush)'%(event.summary, event.modified, lastPush)) + continue + bothId = (event.id, remoteEventId) + addToDiff(bothId, True, STATUS_MODIFIED, event) + ''' + gevent = exportEvent(event) + if gevent is None: + print('---------- event %s can not be pushed'%event.summary) + continue + gevent['etag'] = calcEtag(gevent) + #print('etag = %r'%gevent['etag']) + gevent.update({ + 'calendarId': remoteGroupId, + 'sequence': group.index(event.id), + 'organizer': { + 'displayName': core.userDisplayName,## FIXME + 'email': self.email, + }, + }) + if remoteEventId: + #gevent['id'] = remoteEventId + #if not 'recurrence' in gevent: + # gevent['recurrence'] = None ## or [] FIXME + request = service.events().update(## patch or update? FIXME + eventId=remoteEventId, + body=gevent, + calendarId=remoteGroupId + ) + try: + request.execute() + except HttpError as e: + self.showHttpException(e) + return False ## FIXME + else: + print('---------- event %s updated on server'%event.summary) + else:## FIXME + request = service.events().insert( + body=gevent, + calendarId=remoteGroupId, + sendNotifications=False, + ) + #dumpRequest(request) + try: + response = request.execute() + except HttpError as e: + self.showHttpException(e) + return False ## FIXME + #print('response = %s'%pformat(response)) + remoteEventId = response['id'] + print('----------- event %s added on server'%event.summary) + event.remoteIds = [self.id, remoteGroupId, remoteEventId] + event.save() + #group.eventIdByRemoteIds[tuple(event.remoteIds)] = event.id## TypeError: unhashable type: 'list' + ''' + group.afterSync()## FIXME + group.save()## FIXME + return True + + +def printAllEvent(account, remoteGroupId): + for gevent in account.fetchAllEventsInGroup(remoteGroupId): + print(gevent['summary'], gevent['updated']) + + +if __name__=='__main__': + from scal3 import ui + account = GoogleAccount.load(1) + print(account.fetchGroups()) + #remoteGroupId = 'gi646vjovfrh2u2u2l9hnatvq0@group.calendar.google.com' + #groupId = 102 + #ui.eventGroups = event_lib.EventGroupsHolder.load() + #group = ui.eventGroups[groupId] + #print('group.remoteIds', group.remoteIds) + #group.remoteIds = (account.id, remoteGroupId) + #account.sync(group, remoteGroupId)## 400 Bad Request + #group.save() + + + + + + diff --git a/scal3/bin_heap.py b/scal3/bin_heap.py new file mode 100644 index 000000000..efe38c118 --- /dev/null +++ b/scal3/bin_heap.py @@ -0,0 +1,166 @@ +from math import log +from scal3.utils import s_join + +from heapq import heappush, heappop + +class MaxHeap(list): + copy = lambda self: MaxHeap(self[:]) + def exch(self, i, j): + self[i], self[j] = self[j], self[i] + less = lambda self, i, j: self[i][0] > self[j][0] + def swim(self, k): + while k > 0: + j = (k-1) // 2 + if self.less(k, j): + break + self.exch(k, j) + k = j + def sink(self, k): + N = len(self) + while True: + j = 2*k + 1 + if j > N-1: + break + if j < N-1 and self.less(j, j+1): + j += 1 + if self.less(j, k): + break + self.exch(k, j) + k = j + push = lambda self, key, value: heappush(self, (-key, value)) + def pop(self, index=None): + if index is None: + mkey, value = heappop(self) + else: + N = len(self) + if index < 0 or index > N-1: + raise ValueError('invalid index to pop()') + if index == N-1: + return list.pop(self, index) + self.exch(index, N-1) + mkey, value = list.pop(self, N-1) + self.sink(index) + self.swim(index) + return -mkey, value + moreThan = lambda self, key: self.moreThanStep(key, 0) + def moreThanStep(self, key, index): + if index < 0: + raise StopIteration + try: + item = self[index] + except IndexError: + raise StopIteration + if -item[0] <= key: + raise StopIteration + yield -item[0], item[1] + for k, v in self.moreThanStep(key, 2*index+1): + yield k, v + for k, v in self.moreThanStep(key, 2*index+2): + yield k, v + __str__ = lambda self: ' '.join(['%s'%(-k) for k, v in self]) + def delete(self, key, value): + try: + index = self.index((-key, value))## not optimal FIXME + except ValueError: + pass + else: + self.pop(index) + verify = lambda self: self.verifyIndex(0) + def verifyIndex(self, i): + assert i >= 0 + try: + k = self[i] + except IndexError: + return True + try: + if self[2*i + 1] < k: + print('[%s] > [%s]'%(2*i + 1, i)) + return False + except IndexError: + return True + try: + if self[2*i + 2] < k: + print('[%s] > [%s]'%(2*i + 2, i)) + return False + except IndexError: + return True + return self.verifyIndex(2*i + 1) and self.verifyIndex(2*i + 2) + def getAll(self): + for key, value in self: + yield -key, value + def getMax(self): + if not self: + raise ValueError('heap empty') + k, v = self[0] + return -k, v + def getMin(self): + ## at least 2 times faster than max(self) + if not self: + raise ValueError('heap empty') + k, v = max( + self[-2**int( + log(len(self), 2) + ):] + ) + return -k, v + ''' + def deleteLessThanStep(self, key, index): + try: + key1, value1 = self[index] + except IndexError: + return + key1 = -key1 + #if key + def deleteLessThan(self, key): + pass + ''' + + +def getMinTest(N): + from random import randint + from time import time as now + h = MaxHeap() + for i in range(N): + x = randint(1, 10*N) + h.push(x, 0) + t0 = now() + k1 = -max(h)[0] + t1 = now() + k2 = h.getMin()[0] + t2 = now() + assert k1 == k2 + #print('time getMin(h)/min(h) = %.5f'%((t2-t1)/(t1-t0))) + #print('min key = %s'%k1) + +def testDeleteStep(N, maxKey): + from random import randint + from heapq import heapify + ### + h = MaxHeap() + for i in range(N): + h.push(randint(0, maxKey), 0) + h0 = h.copy() + rmIndex = randint(0, N-1) + rmKey = -h[rmIndex][0] + rmKey2 = h.pop(rmIndex) + if not h.verify(): + print('not verified, N=%s, I=%s'%(N, rmIndex)) + print(h0) + print(h) + print('------------------------') + return False + return True + + +def testDelete(): + for N in range(10, 30): + for p in range(10000): + if not testDeleteStep(N, 10000): + break + +#if __name__=='__main__': +# testDelete() + + + + diff --git a/scal3/cal_types/__init__.py b/scal3/cal_types/__init__.py new file mode 100644 index 000000000..2ae821445 --- /dev/null +++ b/scal3/cal_types/__init__.py @@ -0,0 +1,138 @@ +import sys +from os.path import join +from time import localtime + +from scal3.cal_types import gregorian +from scal3.path import * +from scal3.utils import printError + +DATE_GREG = 0 ## Gregorian (common calendar) +modules = [gregorian] + +def myRaise(): + i = sys.exc_info() + sys.stdout.write('File "%s", line %s: %s: %s\n'%(__file__, i[2].tb_lineno, i[0].__name__, i[1])) + + +for name in open(join(modDir, 'modules.list')).read().split('\n'): + name = name.strip() + if name=='': + continue + if name.startswith('#'): + continue + #try: + mod = __import__('scal3.cal_types.%s'%name, fromlist=[name]) + #mod = __import__(name) ## Need to "sys.path.insert(0, modDir)" before + #except: + # myRaise() + # sys.stdout.write('Could not load calendar modules "%s"\n%s\n'%(name,sys.exc_info()[1])) + # continue + for attr in ( + 'name', + 'desc', + 'origLang', + 'getMonthName', + 'getMonthNameAb', + 'minMonthLen', + 'maxMonthLen', + 'getMonthLen', + 'to_jd', + 'jd_to', + 'options', + 'save', + ): + if not hasattr(mod, attr): + printError( + 'Invalid calendar module: module "%s" has no attribute "%s"\n'\ + %(name, attr) + ) + modules.append(mod) + + + +class CalTypesHolder: + byName = dict([(mod.name, mod) for mod in modules]) + names = [mod.name for mod in modules] + ## calOrigLang = [m.origLang for m in modules] + __len__ = lambda self: len(self.names) + def __init__(self): + self.activeNames = ['gregorian'] + self.inactiveNames = [] + self.update() + ##attributemethod ## FIXME + ##primary = lambda self: self.active[0] + primaryModule = lambda self: modules[self.primary] + def update(self): + self.active = [] + self.inactive = [] ## range(len(modules)) + remainingNames = self.names[:] + for name in self.activeNames: + try: + i = self.names.index(name) + except ValueError: + pass + else: + self.active.append(i) + remainingNames.remove(name) + #### + inactiveToRemove = [] + for name in self.inactiveNames: + try: + i = self.names.index(name) + except ValueError: + pass + else: + if i in self.active: + inactiveToRemove.append(name) + else: + self.inactive.append(i) + remainingNames.remove(name) + for name in inactiveToRemove: + self.inactiveNames.remove(name) + #### + for name in remainingNames: + try: + i = self.names.index(name) + except ValueError: + pass + else: + self.inactive.append(i) + self.inactiveNames.append(name) + #### + self.primary = self.active[0] + def __iter__(self): + for i in self.active + self.inactive: + yield modules[i] + def iterIndexModule(self): + for i in self.active + self.inactive: + yield i, modules[i] + allIndexes = lambda self: self.active + self.inactive + def __getitem__(self, key): + if isinstance(key, str): + return self.byName[key] + if isinstance(key, int): + return modules[key] + else: + raise TypeError('invalid key %r given to %s.__getitem__'%( + key, + self.__class__.__name__, + )) + + +calTypes = CalTypesHolder() + +jd_to = lambda jd, target: modules[target].jd_to(jd) +to_jd = lambda y, m, d, source: modules[source].to_jd(y, m, d) +convert = lambda y, m, d, source, target:\ + (y, m, d) if source==target else modules[target].jd_to(modules[source].to_jd(y, m, d)) + +def getSysDate(mode): + if mode==DATE_GREG: + return localtime()[:3] + else: + gy, gm, gd = localtime()[:3] + return convert(gy, gm, gd, DATE_GREG, mode) + + + + diff --git a/scal3/cal_types/ethiopian.py b/scal3/cal_types/ethiopian.py new file mode 100644 index 000000000..f610bc9f4 --- /dev/null +++ b/scal3/cal_types/ethiopian.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# Used code from http://code.google.com/p/ethiocalendar/ +# Copyright (C) 2008-2009 Yuji DOI +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# Also avalable in /usr/share/common-licenses/LGPL on Debian systems +# or /usr/share/licenses/common/LGPL/license.txt on ArchLinux + + +name = 'ethiopian' +desc = 'Ethiopian' +origLang = 'en'## FIXME + +monthName = ('Meskerem', 'Tekimt', 'Hidar', 'Tahsas', 'Ter', 'Yekoutit', + 'Meyabit', 'Meyaziya', 'Genbot', 'Sene', 'Hamle', 'Nahse') + +monthNameAb = monthName ## FIXME + +monthLen = [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 35] + +getMonthName = lambda m, y=None: monthName.__getitem__(m-1) +getMonthNameAb = lambda m, y=None: monthNameAb.__getitem__(m-1) + +getMonthsInYear = lambda y: 12 + +epoch = 1724235 +minMonthLen = 30 +maxMonthLen = 36 +avgYearLen = 365.25 + +options = () + +def save(): + pass + + +isLeap = lambda y: (y + 1) % 4 == 0 + + +to_jd = lambda year, month, day: epoch + 365*(year-1) + year/4 + (month-1)*30 + day - 15 + + +def jd_to(jd) : + quad, dquad = divmod(jd - epoch, 1461) + yindex = min(3, dquad//365) + year = quad*4 + yindex + 1 + ### + yearday = jd - to_jd(year, 1, 1) + month, day = divmod(yearday, 30) + day += 1 + month += 1 + if month == 13: + month -= 1 + day += 30 + if month == 12: + mLen = 35 + isLeap(year) + if day > mLen: + year += 1 + month = 1 + day -= mLen + ### + return year, month, day + + +def getMonthLen(year, month): + if month==12: + return 35 + isLeap(year) + else: + return monthLen[month-1] + + +if __name__=='__main__': + import sys + from . import gregorian + ### + for gy in range(2012, 1990, -1): + jd = gregorian.to_jd(gy, 1, 1) + ey, em, ed = jd_to(jd) + #if ed==22: + # print(gy) + print('%.4d/%.2d/%.2d\t%.4d/%.2d/%.2d'%(gy, 1, 1, ey, em, ed)) + + + + diff --git a/scal3/cal_types/gregorian.py b/scal3/cal_types/gregorian.py new file mode 100644 index 000000000..d860d5e10 --- /dev/null +++ b/scal3/cal_types/gregorian.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# Copyright (C) 2007 Mehdi Bayazee +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + + +## Gregorian calendar: +## http://en.wikipedia.org/wiki/Gregorian_calendar + +name = 'gregorian' +desc = 'Gregorian' +origLang = 'en' + +monthName = ('January','February','March','April','May','June', + 'July','August','September','October','November','December') + +monthNameAb = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') + +getMonthName = lambda m, y=None: monthName.__getitem__(m-1) +getMonthNameAb = lambda m, y=None: monthNameAb.__getitem__(m-1) + +getMonthsInYear = lambda y: 12 + +epoch = 1721426 +minMonthLen = 29 +maxMonthLen = 31 +avgYearLen = 365.2425 ## FIXME + +options = () + +def save(): + pass + +def isLeap(y): + if y<1: + y += 1 + return y%4==0 and not ( y%100==0 and y%400!=0 ) + +def to_jd(year, month, day): + # Python 2.x and 3.x: + if month <= 2: + tm = 0 + elif isLeap(year): + tm = -1 + else: + tm = -2 + # Python >= 2.5: + #tm = 0 if month <= 2 else (-1 if isLeap(year) else -2) + return epoch - 1 + 365*(year-1) + (year-1)//4 - (year-1)//100 + \ + (year-1)//400 + (367*month-362)//12 + tm + day + + +def jd_to(jd) : + ##wjd = floor(jd - 0.5) + 0.5 + qc, dqc = divmod(jd - epoch, 146097) ## qc ~~ quadricent + cent, dcent = divmod(dqc, 36524) + quad, dquad = divmod(dcent, 1461) + yindex = dquad//365 ## divmod(dquad, 365)[0] + year = qc*400 + cent*100 + quad*4 + yindex + (cent!=4 and yindex!=4) + yearday = jd - to_jd(year, 1, 1) + # Python 2.x and 3.x: + if jd < to_jd(year, 3, 1): + leapadj = 0 + elif isLeap(year): + leapadj = 1 + else: + leapadj = 2 + # Python >= 2.5: + #leapadj = 0 if jd < to_jd(year, 3, 1) else (1 if isLeap(year) else 2) + month = ((yearday+leapadj) * 12 + 373) // 367 + day = jd - to_jd(year, month, 1) + 1 + return int(year), int(month), int(day) + +def getMonthLen(y, m): + if m==12: + return to_jd(y+1, 1, 1) - to_jd(y, 12, 1) + else: + return to_jd(y, m+1, 1) - to_jd(y, m, 1) + +J0001 = to_jd(1, 1, 1) +J1970 = to_jd(1970, 1, 1) + diff --git a/scal3/cal_types/gregorian_proleptic.py b/scal3/cal_types/gregorian_proleptic.py new file mode 100644 index 000000000..ef0dc0bb5 --- /dev/null +++ b/scal3/cal_types/gregorian_proleptic.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# Using kdelibs-4.4.0/kdecore/date/kcalendarsystemgregorianproleptic.cpp +# Copyright (C) 2009 John Layt +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +name = 'gregorian_proleptic' +desc = 'Gregorian Proleptic' +origLang = 'en' + +monthName = ('January','February','March','April','May','June', + 'July','August','September','October','November','December') + +monthNameAb = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') + +getMonthName = lambda m, y=None: monthName.__getitem__(m-1) +getMonthNameAb = lambda m, y=None: monthNameAb.__getitem__(m-1) + +getMonthsInYear = lambda y: 12 + +from math import floor +ifloor = lambda x: int(floor(x)) + +epoch = 1721426 +minMonthLen = 29 +maxMonthLen = 31 +avgYearLen = 365.2425 ## FIXME + +options = () + +def save(): + pass + +def isLeap(y): + if y<1: + y += 1 + return y%4==0 and not ( y%100==0 and y%400!=0 ) + +def getMonthLen(y, m): + if m==2: + if isLeap(y): + return 29 + else: + return 28 + if m in (4, 6, 9, 11): + return 30 + return 31 + + +def to_jd(year, month, day): + ## Formula from The Calendar FAQ by Claus Tondering + ## http:##www.tondering.dk/claus/cal/node3.html#SECTION003161000000000000000 + ## NOTE: Coded from scratch from mathematical formulas, not copied from + ## the Boost licensed source code + ## + ## If year is -ve then is BC. In Gregorian there is no year 0, but the maths + ## is easier if we pretend there is, so internally year of -1 = 1BC = 0 internally + if year<1: + y = year + 1 + else: + y = year; + a = (14-month)//12 + y = y + 4800 - a + m = month + 12*a - 3; + return day + (153*m+2)//5 + 365*y + y//4 - y//100 + y//400 - 32045 + +def jd_to(jd): + ## Formula from The Calendar FAQ by Claus Tondering + ## http:##www.tondering.dk/claus/cal/node3.html#SECTION003161000000000000000 + ## NOTE: Coded from scratch from mathematical formulas, not copied from + ## the Boost licensed source code + a = jd + 32044 + b = (4*a + 3) // 146097 + c = a - 146097*b // 4 + d = (4*c + 3) // 1461 + e = c - 1461*d // 4 + m = (5*e + 2) // 153 + day = e - (153*m+2)//5 + 1 + month = m + 3 - 12*(m//10) + year = 100*b + d - 4800 + (m//10) + ## If year is -ve then is BC. In Gregorian there is no year 0, but the maths + ## is easier if we pretend there is, so internally year of 0 = 1BC = -1 outside + if year < 1: + year -= 1 + return (year, month, day) + + + +"""bool KCalendarSystemGregorianProleptic::isValid( int year, int month, int day ) const +{ + if ( year < -4713 || year > 9999 || year == 0 ) { + return false; + } + + if ( month < 1 || month > 12 ) { + return false; + } + + if ( month == 2 ) { + if ( isLeapYear( year ) ) { + return ( day >= 1 && day <= 29 ); + } else { + return ( day >= 1 && day <= 28 ); + } + } + + if ( month == 4 || month == 6 || month == 9 || month == 11 ) { + return ( day >= 1 && day <= 30 ); + } + + return ( day >= 1 && day <= 31 ); +}""" + + + + + + + + + + + + diff --git a/scal3/cal_types/hijri-monthes.json b/scal3/cal_types/hijri-monthes.json new file mode 100644 index 000000000..48dac8dcd --- /dev/null +++ b/scal3/cal_types/hijri-monthes.json @@ -0,0 +1,19 @@ +{ + "version": [1437, 5], + "startDate": [1426, 2, 1], + "startJd": 2453442, + "monthLen": [ + [1426, 0, 29, 30, 29, 30, 30, 30, 30, 29, 30, 29, 29], + [1427, 30, 29, 29, 30, 29, 30, 30, 30, 30, 29, 29, 30], + [1428, 29, 30, 29, 29, 29, 30, 30, 29, 30, 30, 30, 29], + [1429, 30, 29, 30, 29, 29, 29, 30, 30, 29, 30, 30, 29], + [1430, 30, 30, 29, 29, 30, 29, 30, 29, 29, 30, 30, 29], + [1431, 30, 30, 29, 30, 29, 30, 29, 30, 29, 29, 30, 29], + [1432, 30, 30, 29, 30, 30, 30, 29, 29, 30, 29, 30, 29], + [1433, 29, 30, 29, 30, 30, 30, 29, 30, 29, 30, 29, 30], + [1434, 29, 29, 30, 29, 30, 30, 29, 30, 30, 29, 30, 29], + [1435, 29, 30, 29, 30, 29, 30, 29, 30, 30, 30, 29, 30], + [1436, 29, 30, 29, 29, 30, 29, 30, 29, 30, 29, 30, 30], + [1437, 29, 30, 30, 29, 30] + ] +} diff --git a/scal3/cal_types/hijri.py b/scal3/cal_types/hijri.py new file mode 100644 index 000000000..99552d656 --- /dev/null +++ b/scal3/cal_types/hijri.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# Copyright (C) 2007 Mehdi Bayazee +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +## Islamic (Hijri) calendar: http://en.wikipedia.org/wiki/Islamic_calendar + +name = 'hijri' +desc = 'Hijri(Islamic)' +origLang = 'ar' + +monthName = ('Muharram','Safar','Rabia\' 1','Rabia\' 2','Jumada 1','Jumada 2', + 'Rajab','Sha\'aban','Ramadan','Shawwal','Dhu\'l Qidah','Dhu\'l Hijjah') + +monthNameAb = ('Moh', 'Saf', 'Rb1', 'Rb2', 'Jm1', 'Jm2', + 'Raj', 'Shb', 'Ram', 'Shw', 'DhQ', 'DhH') + +getMonthName = lambda m, y=None: monthName.__getitem__(m-1) +getMonthNameAb = lambda m, y=None: monthNameAb.__getitem__(m-1) + +getMonthsInYear = lambda y: 12 + + +epoch = 1948439.5 +minMonthLen = 29 +maxMonthLen = 30 +avgYearLen = 354.3666 ## FIXME + + +hijriAlg = 0 +hijriUseDB = True + + + +#('hijriAlg', list, 'Hijri Calculation Algorithm', +# ('Internal', 'ITL (idate command)', 'ITL (idate command) Umm Alqura')), +options = ( +('hijriUseDB',bool,'Use Hijri month length data (Iranian official calendar)'), +('button', 'Tune Hijri Monthes', 'hijri', 'tuneHijriMonthes'), +) + + + +import os +from os.path import join, isfile +from scal3.path import sysConfDir, confDir, modDir +from scal3.json_utils import * +from scal3.lib import OrderedDict +from scal3.utils import iceil, ifloor +from scal3.utils import myRaise + + +oldDbPath = '%s/hijri.db'%confDir +if isfile(oldDbPath): + os.remove(oldDbPath) + + +## Here load user options (hijriUseDB) from file +sysConfPath = '%s/%s.json'%(sysConfDir, name) +loadJsonConf(__name__, sysConfPath) + + +confPath = '%s/%s.json'%(confDir, name) +loadJsonConf(__name__, confPath) + + +def save():## Here save user options (hijriUseDB) to file + saveJsonConf(__name__, confPath, ( + 'hijriAlg', + 'hijriUseDB', + )) + + +class MonthDbHolder: + def __init__(self): + self.startDate = (1426, 2, 1) ## hijriDbInitH + self.startJd = 2453441 ## hijriDbInitJD + self.endJd = self.startJd ## hijriDbEndJD + self.monthLenByYm = {} ## hijriMonthLen + self.userDbPath = join(confDir, 'hijri-monthes.json') + self.sysDbPath = '%s/hijri-monthes.json'%modDir + def setMonthLenByYear(self, monthLenByYear): + self.endJd = self.startJd + self.monthLenByYm = {} + for y in monthLenByYear: + lst = monthLenByYear[y] + for m in range(len(lst)): + ml = lst[m] + if ml:## positive integer + self.monthLenByYm[y*12+m] = ml + self.endJd += ml + def setData(self, data): + self.startDate = tuple(data['startDate']) + self.startJd = data['startJd'] + ### + monthLenByYear = {} + for row in data['monthLen']: + monthLenByYear[row[0]] = row[1:] + self.setMonthLenByYear(monthLenByYear) + def load(self): + data = jsonToData(open(self.sysDbPath).read()) + self.origVersion = data['version'] + ## + if isfile(self.userDbPath): + userData = jsonToData(open(self.userDbPath).read()) + if userData['origVersion'] >= self.origVersion: + data = userData + else: + print('---- ignoring user\'s old db', self.userDbPath) + self.setData(data) + def getMonthLenByYear(self): + monthLenByYear = {} + for ym, mLen in self.monthLenByYm.items(): + year, month0 = divmod(ym, 12) + if not year in monthLenByYear: + monthLenByYear[year] = [0,] * month0 + monthLenByYear[year].append(mLen) + return monthLenByYear + def save(self): + mLenData = [] + for year, mLenList in self.getMonthLenByYear().items(): + mLenData.append([year]+mLenList) + text = dataToPrettyJson(OrderedDict([ + ('origVersion', self.origVersion), + ('startDate', self.startDate), + ('startJd', self.startJd), + ('monthLen', mLenData), + ])) + open(self.userDbPath, 'w').write(text) + def getMonthLenList(self):## returns a list of (index, ym, mLen) + ls = [] + for index, ym in enumerate(sorted(self.monthLenByYm.keys())): + ls.append((index, ym, self.monthLenByYm[ym])) + return ls + def getDateFromJd(self, jd): + if not self.endJd >= jd >= self.startJd: + return + #yi, mi, di = self.startDate + #ymi = yi*12 + mi + y, m, d = self.startDate + ym = y*12 + m-1 + while jd > self.startJd: + monthLen = self.monthLenByYm[ym] + if jd-monthLen > self.startJd: + ym += 1 + jd -= monthLen + elif d+jd-self.startJd > monthLen: + ym += 1 + d = d + jd - self.startJd - monthLen + jd = self.startJd + else: + d = d + jd - self.startJd + jd = self.startJd + y, m = divmod(ym, 12) + m += 1 + return (y, m, d) + def getJdFromDate(self, year, month, day): + ym = year*12 + month-1 + y0, m0, d0 = monthDb.startDate + ym0 = y0*12 + m0-1 + if not ym-1 in monthDb.monthLenByYm: + return + jd = monthDb.startJd + for ymi in range(ym0, ym): + jd += monthDb.monthLenByYm[ymi] + return jd + day - 1 + +monthDb = MonthDbHolder() +monthDb.load() +## monthDb.save() + +################################################################################ + +is_leap = lambda year: (((year * 11) + 14) % 30) < 11 + +to_jd_c = lambda year, month, day:\ + day + iceil(29.5 * (month - 1)) + \ + (year - 1) * 354 + \ + (11*year + 3) // 30 + \ + int(epoch) + 1 + +def to_jd(year, month, day): + if hijriUseDB:## and hijriAlg==0 + jd = monthDb.getJdFromDate(year, month, day) + if jd is not None: + return jd + return to_jd_c(year, month, day) + +def jd_to(jd): + ## hijriAlg==0 + if hijriUseDB: + #jd = ifloor(jd) + date = monthDb.getDateFromJd(jd) + if date: + return date + ##jd = ifloor(jd) + 0.5 + year = ifloor(((30 * (jd - epoch)) + 10646) // 10631) + month = min( + 12, + iceil( + (jd - (29 + to_jd(year, 1, 1))) / 29.5 + ) + 1 + ) + day = jd - to_jd(year, month, 1) + 1 + return year, month, day + + +def getMonthLen(y, m): + """ + if hijriUseDB:## and hijriAlg==0 + try: + return monthDb.monthLenByYm[y*12+m] + except KeyError: + pass + """ + if m==12: + return to_jd(y+1, 1, 1) - to_jd(y, 12, 1) + else: + return to_jd(y, m+1, 1) - to_jd(y, m, 1) + + +if __name__=='__main__': + for ym in monthDb.monthLenByYm: + y, m = divmod(ym, 12) + m += 1 + print(to_jd(y, m, 1) - to_jd_c(y, m, 1)) + + diff --git a/scal3/cal_types/indian_national.py b/scal3/cal_types/indian_national.py new file mode 100644 index 000000000..18d6f53d1 --- /dev/null +++ b/scal3/cal_types/indian_national.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# Using kdelibs-4.4.0/kdecore/date/kcalendarsystemindiannational.cpp +# Copyright (C) 2009 John Layt +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +name = 'indian_national' +desc = 'Indian National' +origLang = 'hi' ## or 'en' ## FIXME + +monthName = ('Chaitra','Vaishākh','Jyaishtha','Āshādha','Shrāvana','Bhādrapad', + 'Āshwin','Kārtik','Agrahayana','Paush','Māgh','Phālgun') + +monthNameAb = ('Cha','Vai','Jya','Āsh','Shr','Bhā', + 'Āsw','Kār','Agr','Pau','Māg','Phā') + +getMonthName = lambda m, y=None: monthName.__getitem__(m-1) +getMonthNameAb = lambda m, y=None: monthNameAb.__getitem__(m-1) + +getMonthsInYear = lambda y: 12 + +## Monday Somavãra +## Tuesday Mañgalvã +## Wednesday Budhavãra +## Thursday Guruvãra +## Friday Sukravãra +## Saturday Sanivãra +## Sunday Raviãra + +## Mon Som +## Tue Mañ +## Wed Bud +## Thu Gur +## Fri Suk +## Sat San +## Sun Rav + +epoch = 1749994 +minMonthLen = 30 +maxMonthLen = 31 +avgYearLen = 365.2425 ## FIXME + + +options = () + +def save(): + pass + + +try: + from scal3.cal_types import gregorian +except ImportError: + from . import gregorian + +isLeap = lambda y: gregorian.isLeap(y+78) + +"""bool KCalendarSystemIndianNational::isValid( int year, int month, int day ) const +{ + if ( year < 0 || year > 9999 ) { + return false + } + + if ( month < 1 || month > 12 ) { + return false + } + + if ( month == 1 ) { + if ( isLeapYear( year ) ) { + return ( day >= 1 && day <= 31 ) + } else { + return ( day >= 1 && day <= 30 ) + } + } + + if ( month >= 2 || month <= 6 ) { + return ( day >= 1 && day <= 31 ) + } + + return ( day >= 1 && day <= 30 ) +}""" + +def getMonthLen(y, m): + if m==1: + if isLeap(y): + return 31 + else: + return 30 + if 2 <= m <= 6: + return 31 + return 30 + + +def jd_to(jd): + ## The calendar is closely synchronized to the Gregorian Calendar, always starting on the same day + ## We can use this and the regular sequence of days in months to do a simple conversion by finding + ## what day in the Gregorian year the Julian Day number is, converting this to the day in the + ## Indian year and subtracting off the required number of months and days to get the final date + + gregorianYear, gregorianMonth, gregorianDay = gregorian.jd_to(jd) + jdGregorianFirstDayOfYear = gregorian.to_jd(gregorianYear, 1, 1) + gregorianDayOfYear = jd - jdGregorianFirstDayOfYear + 1 + + ## There is a fixed 78 year difference between year numbers, but the years do not exactly match up, + ## there is a fixed 80 day difference between the first day of the year, if the Gregorian day of + ## the year is 80 or less then the equivalent Indian day actually falls in the preceding year + if gregorianDayOfYear > 80: + year = gregorianYear - 78 + else: + year = gregorianYear - 79 + + ## If it is a leap year then the first month has 31 days, otherwise 30. + if isLeap(year): + daysInMonth1 = 31 + else: + daysInMonth1 = 30 + + ## The Indian year always starts 80 days after the Gregorian year, calculate the Indian day of + ## the year, taking into account if it falls into the previous Gregorian year + if gregorianDayOfYear>80: + indianDayOfYear = gregorianDayOfYear - 80 + else: + indianDayOfYear = gregorianDayOfYear + daysInMonth1 + 5*31 + 6*30 - 80 + + ## Then simply remove the whole months from the day of the year and you are left with the day of month + if indianDayOfYear <= daysInMonth1: + month = 1 + day = indianDayOfYear + elif indianDayOfYear <= daysInMonth1 + 5*31: + month = (indianDayOfYear-daysInMonth1-1) // 31 + 2 + day = indianDayOfYear - daysInMonth1 - (month-2)*31 + else: + month = (indianDayOfYear - daysInMonth1 - 5*31 - 1) // 30 + 7 + day = indianDayOfYear - daysInMonth1 - 5*31 - (month-7)*30 + return (year, month, day) + + +def to_jd(year, month, day): + ## The calendar is closely synchronized to the Gregorian Calendar, always starting on the same day + ## We can use this and the regular sequence of days in months to do a simple conversion by finding + ## the Julian Day number of the first day of the year and adding on the required number of months + ## and days to get the final Julian Day number + + ## Calculate the jd of 1 Chaitra for this year and how many days are in Chaitra this year + ## If a Leap Year, then 1 Chaitra == 21 March of the Gregorian year and Chaitra has 31 days + ## If not a Leap Year, then 1 Chaitra == 22 March of the Gregorian year and Chaitra has 30 days + ## Need to use dateToJulianDay() to calculate instead of setDate() to avoid the year 9999 validation + if isLeap(year): + jdFirstDayOfYear = gregorian.to_jd(year+78, 3, 21) + daysInMonth1 = 31 + else: + jdFirstDayOfYear = gregorian.to_jd(year+78, 3, 22) + daysInMonth1 = 30 + + ## Add onto the jd of the first day of the year the number of days required + ## Calculate the number of days in the months before the required month + ## Then add on the required days + ## The first month has 30 or 31 days depending on if it is a Leap Year (determined above) + ## The second to sixth months have 31 days each + ## The seventh to twelth months have 30 days each + ## Note: could be expressed more efficiently, but I think this is clearer + if month==1: + jd = jdFirstDayOfYear + day - 1 + elif month<=6: + jd = jdFirstDayOfYear + daysInMonth1 + (month-2)*31 + day - 1 + else: ## month > 6 + jd = jdFirstDayOfYear + daysInMonth1 + 5*31 + (month-7)*30 + day - 1 + return jd + + + diff --git a/scal3/cal_types/jalali.py b/scal3/cal_types/jalali.py new file mode 100644 index 000000000..a68559c72 --- /dev/null +++ b/scal3/cal_types/jalali.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# Copyright (C) 2007 Mehdi Bayazee +# Copyright (C) 2001 Roozbeh Pournader +# Copyright (C) 2001 Mohammad Toossi +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +## Iranian (Jalali) calendar: +## http://en.wikipedia.org/wiki/Iranian_calendar + +name = 'jalali' +desc = 'Jalali' +origLang = 'fa' + +monthNameMode = 0 +jalaliAlg = 0 +options = ( + ( + 'monthNameMode', + list, + 'Jalali Month Names', + ('Iranian', 'Kurdish', 'Dari', 'Pashto'), + ), + ( + 'jalaliAlg', + list, + 'Jalali Calculation Algorithm', + ('33 year algorithm', '2820 year algorithm'), + ), +) + + +monthNameVars = ( + ( + ('Farvardin','Ordibehesht','Khordad','Teer','Mordad','Shahrivar', + 'Mehr','Aban','Azar','Dey','Bahman','Esfand'), + ('Far', 'Ord', 'Khr', 'Tir', 'Mor', 'Shr', + 'Meh', 'Abn', 'Azr', 'Dey', 'Bah', 'Esf'), + ), + ( + ('Xakelêwe','Gullan','Cozerdan','Pûşper','Gelawêj','Xermanan', + 'Rezber','Gelarêzan','Sermawez','Befranbar','Rêbendan','Reşeme'), + ), + ( + ('Hamal','Sawr','Jawzā','Saratān','Asad','Sonbola', + 'Mizān','Aqrab','Qaws','Jadi','Dalvæ','Hūt'), + ), + ( + ('Wray','Ǧwayay','Ǧbargolay','Čungāx̌','Zmaray','Waǵay', + 'Təla','Laṛam','Līndəi','Marǧūmay','Salwāǧa','Kab'), + ), +) + +# ('','','','','','', +# '','','','','','') + + +getMonthName = lambda m, y=None: monthNameVars[monthNameMode][0][m-1] + +def getMonthNameAb(m, y=None): + v = monthNameVars[monthNameMode] + try: + l = v[1] + except IndexError: + l = v[0] + return l[m-1] + + + +getMonthsInYear = lambda y: 12 + + +epoch = 1948321 +minMonthLen = 29 +maxMonthLen = 31 +avgYearLen = 365.2425 ## FIXME + +GREGORIAN_EPOCH = 1721426 +monthLen = (31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 30) + +monthLenSum = [0] +for i in range(12): + monthLenSum.append(monthLenSum[-1] + monthLen[i]) + +#print(monthLenSum) +## monthLenSum[i] == sum(monthLen[:i]) + +import os +from bisect import bisect_left + +from scal3.path import sysConfDir, confDir +from scal3.utils import iceil +from scal3.utils import myRaise +from scal3.json_utils import * + +## Here load user options(jalaliAlg) from file +sysConfPath = '%s/%s.json'%(sysConfDir, name) +loadJsonConf(__name__, sysConfPath) + + +confPath = '%s/%s.json'%(confDir, name) +loadJsonConf(__name__, confPath) + + + +def save():## Here save user options to file + saveJsonConf(__name__, confPath, ( + 'monthNameMode', + 'jalaliAlg', + )) + + +def isLeap(year): + "isLeap: Is a given year a leap year in the Jalali calendar ?" + if jalaliAlg==1:## 2820-years + return (( (year - 473 - (year>0)) % 2820) * 682) % 2816 < 682 + elif jalaliAlg==0:## 33-years + jy = year - 979 + gdays = ( 365*jy + (jy//33)*8 + (jy%33+3)//4 + 79 ) % 146097 + ## 36525 = 365*100 + 100//4 + if gdays >= 36525: + gdays = (gdays-1) % 36524 + 1 + if gdays < 366: + return False + if gdays % 1461 >= 366: + return False + return True + + else: + raise RuntimeError('bad option jalaliAlg=%s'%jalaliAlg) + +def getMonthDayFromYdays(yday): + month = bisect_left(monthLenSum, yday) + day = yday - monthLenSum[month - 1] + return month, day + +def to_jd(year, month, day): + "TO_JD: Determine Julian day from Jalali date" + if jalaliAlg==1:## 2820-years + epbase = year - 474 if year>=0 else 473 + epyear = 474 + epbase % 2820 + return day + \ + (month-1) * 30 + min(6, month-1) + \ + (epyear * 682 - 110) // 2816 + \ + (epyear - 1) * 365 + \ + epbase // 2820 * 1029983 + \ + epoch - 1 + elif jalaliAlg==0:## 33-years + y2 = year - 979 + jdays = 365*y2 + y2//33 * 8 + (y2%33+3)//4 + monthLenSum[month-1] + (day-1) + return jdays + 584101 + GREGORIAN_EPOCH + else: + raise RuntimeError('bad option jalaliAlg=%s'%jalaliAlg) + +def jd_to(jd): + "JD_TO_JALALI: Calculate Jalali date from Julian day" + if jalaliAlg==1:## 2820-years + cycle, cyear = divmod(jd - to_jd(475, 1, 1), 1029983) + if cyear == 1029982 : + ycycle = 2820 + else: + aux1, aux2 = divmod(cyear, 366) + ycycle = (2134*aux1 + 2816*aux2 + 2815) // 1028522 + aux1 + 1 + year = 2820*cycle + ycycle + 474 + if year <= 0 : + year -= 1 + yday = jd - to_jd(year, 1, 1) + 1 + month, day = getMonthDayFromYdays(yday) + elif jalaliAlg==0:## 33-years + jdays = int(jd - GREGORIAN_EPOCH - 584101) + ## -(1600*365 + 1600//4 - 1600//100 + 1600//400) + 365 -79 +1== -584101 + #print('jdays =',jdays) + j_np = jdays // 12053 + jdays %= 12053 + year = 979 + 33*j_np + 4*(jdays//1461) + jdays %= 1461 + if jdays >= 366: + year += (jdays-1) // 365 + jdays = (jdays-1) % 365 + yday = jdays+1 + month, day = getMonthDayFromYdays(yday) + else: + raise RuntimeError('bad option jalaliAlg=%s'%jalaliAlg) + return year, month, day + + +## Normal: esfand = 29 days +## Leap: esfand = 30 days + +def getMonthLen(year, month): + if month==12: + return 29 + isLeap(year) + else: + return monthLen[month-1] + diff --git a/scal3/cal_types/julian.py b/scal3/cal_types/julian.py new file mode 100644 index 000000000..2e74d8aa6 --- /dev/null +++ b/scal3/cal_types/julian.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# Using libkal code +# The 'libkal' library for date conversion: +# Copyright (C) 1996-1998 Petr Tomasek +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +name = 'julian' +desc = 'Julian' +origLang = 'en' + +from math import floor +ifloor = lambda x: int(floor(x)) + +monthName = ('January','February','March','April','May','June', + 'July','August','September','October','November','December') + +monthNameAb = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') + +getMonthName = lambda m, y=None: monthName.__getitem__(m-1) +getMonthNameAb = lambda m, y=None: monthNameAb.__getitem__(m-1) + +getMonthsInYear = lambda y: 12 + +epoch = 1721058 +minMonthLen = 28 +maxMonthLen = 32 +avgYearLen = 365.2425 ## FIXME + +options = () + +def save(): + pass + +def kal_s(m, p): + if m==13: + return 365 + else: + t = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334) + ## (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30) + #m = (m-1)%12 + 1 ## for stability reasons + b = t[m-1] + if m<3: + b -= p + return b + +def to_jd(y, m, d): + p1, p2 = divmod(y, 4) + return ifloor(d + kal_s(m, int(p2==0)) + 1461*p1 + 365*p2 + epoch) + +def jd_to(jd): + ##wjd = ifloor(jd - 0.5) + 1 + p1, q1 = divmod(jd-epoch, 1461) + #if q1==0:## ?????????????????? + # return (4*p1, 1, 1) + #else: + if True: + p2, q2 = divmod(q1-1, 365) + y = 4*p1 + p2; + q = int(y%4==0) + m = 1; + while m<12 and q2+1 > kal_s(m+1, q): + m += 1 + d = q2 + 1 - kal_s(m, q) + return (y, m, d) + +def getMonthLen(y, m): + if m==12: + return to_jd(y+1, 1, 1) - to_jd(y, 12, 1) + else: + return to_jd(y, m+1, 1) - to_jd(y, m, 1) + + + diff --git a/scal3/cal_types/modules.list b/scal3/cal_types/modules.list new file mode 100644 index 000000000..c2c707fef --- /dev/null +++ b/scal3/cal_types/modules.list @@ -0,0 +1,6 @@ +jalali +hijri +julian +gregorian_proleptic +indian_national +ethiopian diff --git a/scal3/color_utils.py b/scal3/color_utils.py new file mode 100644 index 000000000..3bc04bd7b --- /dev/null +++ b/scal3/color_utils.py @@ -0,0 +1,74 @@ + +invertColor = lambda r, g, b: (255-r, 255-g, 255-b) + +def rgbToHsl(r, g, b): + r /= 255.0 + g /= 255.0 + b /= 255.0 + ### + mx = max(r, g, b) + mn = min(r, g, b) + dm = float(mx - mn) + ### + if dm == 0: + h = None + elif mx == r: + h = 60.0*(g-b)/dm + if h < 0: + h += 360 + elif mx == g: + h = 60.0*(b-r)/dm + 120 + else:## mx == b: + h = 60.0*(r-g)/dm + 260 + ### + l = (mx+mn)/2.0 + ### + if l == 0 or dm == 0: + s = 0 + elif 0 < l < 0.5: + s = dm/(mx+mn) + else:## l > 0.5 + s = dm/(2.0-mx-mn) + return (h, s, l) + + +def hslToRgb(h, s, l): + ## 0.0 <= h <= 360.0 + ## 0.0 <= s <= 1.0 + ## 0.0 <= l <= 1.0 + if l < 0.5: + q = l * (1.0+s) + else: + q = l + s - l*s + p = 2*l - q + hk = h/360.0 + tr = (hk+1.0/3) % 1 + tg = hk % 1 + tb = (hk-1.0/3) % 1 + rgb = [] + for tc in (tr, tg, tb): + if tc < 1.0/6: + c = p + (q-p)*6*tc + elif 1.0/6 <= tc < 1.0/2: + c = q + elif 1.0/2 <= tc < 2.0/3: + c = p + (q-p)*6*(2.0/3-tc) + else: + c = p + rgb.append(int(c*255)) + rgb = tuple(rgb) + #rgb = rgb + (255,) + return rgb + +#def getRandomHueColor(s, l): +# import random +# h = random.uniform(0, 360) +# return hslToRgb(h, s, l) + +htmlColorToRgb = lambda hc: (int(hc[1:3], 16), int(hc[3:5], 16), int(hc[5:7], 16)) + +def rgbToHtmlColor(r, g, b): + return '#' + ''.join(['%.2x'%x for x in (r, g, b)]) + + + diff --git a/scal3/core.py b/scal3/core.py new file mode 100644 index 000000000..551cc406a --- /dev/null +++ b/scal3/core.py @@ -0,0 +1,611 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux +from time import localtime +from time import time as now + +import sys, os, subprocess +from subprocess import Popen +from io import StringIO +import os.path +from os.path import join, isfile, isdir +#from pprint import pprint + + +from scal3.path import * +from scal3.time_utils import * +from scal3.date_utils import * +from scal3.os_utils import * +from scal3.json_utils import * +from scal3.utils import * + +from scal3.cal_types import calTypes, DATE_GREG, getSysDate +from scal3 import locale_man +from scal3.locale_man import tr as _ +from scal3.locale_man import localTz +from scal3.plugin_man import * + + + +try: + __file__ +except NameError: + import inspect, scal3 + __file__ = join(os.path.dirname(inspect.getfile(scal3)), 'core.py') + + +VERSION = '3.0.0' +BRANCH = join(rootDir, 'branch') +APP_DESC = 'StarCalendar' +COMMAND = APP_NAME +homePage = 'http://ilius.github.io/starcal/' +osName = getOsName() +userDisplayName = getUserDisplayName() +#print('--------- Hello %s'%userDisplayName) + + +#print('__file__ = %r'%__file__) +#print('__name__ = %r'%__name__) +#print('__package__ = %r'%__package__) +#print('__builtins__',) +#pprint(__builtins__) +#print +#print('core.dir:') +#pprint(dir()) + +#print('sys.modules =',) +#pprint(sys.modules) + +#__plugin_api_get__ = [ +# 'VERSION', 'APP_NAME', 'APP_DESC', 'COMMAND', 'homePage', 'osName', 'userDisplayName' +# 'jd_to_primary', 'primary_to_jd', +#] +#__plugin_api_set__ = [] + +#def pluginCanGet(funcClass): +# global __plugin_api_get__ +# __plugin_api_get__.append(funcClass.__name__) +# return funcClass + +#def pluginCanSet(funcClass): +# global __plugin_api_set__ +# __plugin_api_set__.append(funcClass.__name__) + +#################### Defining user core configuration #################### + +sysConfPath = join(sysConfDir, 'core.json') + +confPath = join(confDir, 'core.json') + +confParams = ( + 'version', + 'allPlugList', + 'plugIndex', + 'activeCalTypes', + 'inactiveCalTypes', + 'holidayWeekDays', + 'firstWeekDayAuto', + 'firstWeekDay', + 'weekNumberModeAuto', + 'weekNumberMode', + 'debugMode', +) + +confDecoders = { + 'allPlugList': lambda pdataList: [ + loadPlugin(**pdata) for pdata in pdataList + ], +} + +confEncoders = { + 'allPlugList': lambda plugList: [ + plug.getArgs() for plug in plugList if plug is not None + ], +} + + +def loadConf(): + global version, prefVersion, activeCalTypes, inactiveCalTypes + ########### + loadModuleJsonConf(__name__) + ########### + try: + version + except NameError: + prefVersion = '' + else: + prefVersion = version + del version + ########### + try: + calTypes.activeNames = activeCalTypes ## loaded from json config file + calTypes.inactiveNames = inactiveCalTypes ## loaded from json config file + except NameError: + pass + activeCalTypes = inactiveCalTypes = None + calTypes.update() + + +def saveConf(): + global activeCalTypes, inactiveCalTypes + activeCalTypes, inactiveCalTypes = calTypes.activeNames, calTypes.inactiveNames + saveModuleJsonConf(__name__) + activeCalTypes = inactiveCalTypes = None + +################################################################################ + +if os.path.exists(confDir): + if not isdir(confDir): + os.rename(confDir, confDir+'-old') + os.mkdir(confDir) +else: + os.mkdir(confDir) + +makeDir(join(confDir, 'log')) +################################################################################ + +try: + import logging + import logging.config + + logConfText = open(join(rootDir, 'conf', 'logging-user.conf')).read() + for varName in ('confDir',): + logConfText = logConfText.replace(varName, eval(varName)) + + logging.config.fileConfig(StringIO(logConfText)) + log = logging.getLogger(APP_NAME) +except: + from scal3.utils import FallbackLogger + log = FallbackLogger() + +def myRaise(File=None): + i = sys.exc_info() + typ, value, tback = sys.exc_info() + text = 'line %s: %s: %s\n'%(tback.tb_lineno, typ.__name__, value) + if File: + text = 'File "%s", '%File + text + log.error(text) + + +################################################################################ +####################### class and function defenitions ######################### +################################################################################ + + +popen_output = lambda cmd: Popen(cmd, stdout=subprocess.PIPE).communicate()[0] + +primary_to_jd = lambda y, m, d: calTypes.primaryModule().to_jd(y, m, d) +jd_to_primary = lambda jd: calTypes.primaryModule().jd_to(jd) + +def getCurrentJd():## time.time() and mktime(localtime()) both return GMT, not local + y, m, d = localtime()[:3] + return calTypes[DATE_GREG].to_jd(y, m, d) + +def getWeekDateHmsFromEpoch(epoch): + jd, hour, minute, sec = getJhmsFromEpoch(epoch) + absWeekNumber, weekDay = getWeekDateFromJd(jd) + return (absWeekNumber, weekDay, hour, minute, sec) + +def getMonthWeekNth(jd, mode): + year, month, day = calTypes[mode].jd_to(jd) + absWeekNumber, weekDay = getWeekDateFromJd(jd) + ## + dayDiv, dayMode = divmod(day-1, 7) + return month, dayDiv, weekDay + + +getWeekDay = lambda y, m, d: jwday(primary_to_jd(y, m, d)-firstWeekDay) + +getWeekDayN = lambda i: weekDayName[(i+firstWeekDay)%7] +## 0 <= i < 7 (0 = first day) +def getWeekDayAuto(i, abr=False): + if abr: + return weekDayNameAb[(i+firstWeekDay)%7] + else: + return weekDayName[(i+firstWeekDay)%7] + +weekDayNameAuto = lambda abr: weekDayNameAb if abr else weekDayName + +def getLocaleFirstWeekDay(): + #log.debug('first_weekday', popen_output(['locale', 'first_weekday'])) + return int(popen_output(['locale', 'first_weekday']))-1 + #return int(popen_output('LANG=%s locale first_weekday'%locale_man.lang))-1 + ##retrun int(trans('calendar:week_start:0').split(':')[-1]) + ## "trans" must read from gtk-2.0.mo !! + + +## week number in year +def getWeekNumberByJdAndDate(jd, year, month, day): + if primary_to_jd(year+1, 1, 1) - jd < 7:## FIXME + if getWeekNumber(*jd_to_primary(jd+14)) == 3: + return 1 + ### + absWeekNum, weekDay = getWeekDateFromJd(jd) + ystartAbsWeekNum, ystartWeekDay = getWeekDateFromJd(primary_to_jd(year, 1, 1)) + weekNum = absWeekNum - ystartAbsWeekNum + 1 + ### + if weekNumberMode < 7: + if ystartWeekDay > (weekNumberMode-firstWeekDay)%7: + weekNum -= 1 + if weekNum==0: + weekNum = getWeekNumber(*jd_to_primary(jd-7)) + 1 + ### + return weekNum + +def getWeekNumber(year, month, day): + jd = primary_to_jd(year, month, day) + return getWeekNumberByJdAndDate(jd, year, month, day) + +def getWeekNumberByJd(jd): + year, month, day = jd_to_primary(jd) + return getWeekNumberByJdAndDate(jd, year, month, day) + +#getYearWeeksCount = lambda year: getWeekNumberByJd(primary_to_jd(year+1, 1, 1)-7) +## FIXME + +def getJdFromWeek(year, weekNumber):## FIXME + ## weekDay == 0 + wd0 = getWeekDay(year, 1, 1) - 1 + wn0 = getWeekNumber(year, 1, 1, False) + jd0 = primary_to_jd(year, 1, 1) + return jd0 - wd0 + (weekNumber-wn0)*7 + + +getWeekDateFromJd = lambda jd: divmod(jd - firstWeekDay + 1, 7) +## return (absWeekNumber, weekDay) + +getAbsWeekNumberFromJd = lambda jd: getWeekDateFromJd(jd)[0] + +getStartJdOfAbsWeekNumber = lambda absWeekNumber: absWeekNumber*7 + firstWeekDay - 1 + + +#def getLocaleWeekNumberMode():##???????????? +# return (int(popen_output(['locale', 'week-1stweek']))-1)%8 + ## will be 7 for farsi (OK) + ## will be 6 for english (usa) (NOT OK, must be 4) + #return int(popen_output('LANG=%s locale first_weekday'%locale_man.lang))-1 + ## locale week-1stweek: + ## en_US.UTF-8 7 + ## en_GB.UTF-8 4 + ## fa_IR.UTF-8 0 + + +###################################################### + +def validatePlugList(): + global allPlugList, plugIndex + n = len(allPlugList) + i = 0 + while i1: + if sys.argv[1] in ('--help', '-h'): + print('No help implemented yet!') + sys.exit(0) + elif sys.argv[1]=='--version': + print(VERSION) + sys.exit(0) + + +#holidayWeekDay=6 ## 6 means last day of week ( 0 means first day of week) +#thDay = (tr('First day'), tr('2nd day'), tr('3rd day'), tr('4th day'),\ +# tr('5th day'), tr('6th day'), tr('Last day')) +#holidayWeekEnable = True + +libDir = join(rootDir, 'lib') +if isdir(libDir): + sys.path.insert(libDir) + pyVersion = '%d.%d'%tuple(sys.version_info[:2]) + pyLibDir = join(libDir, pyVersion) + if isdir(pyLibDir): + sys.path.insert(0, pyLibDir) + del pyVersion, pyLibDir + + +################################################################################ +###################### Default Configuration ################################### +allPlugList = [] +plugIndex = [] + +holidayWeekDays = [0] ## 0 means Sunday (5 means Friday) +## [5] or [4,5] in Iran +## [0] in most of contries +firstWeekDayAuto = True +firstWeekDay = 0 ## 0 means Sunday (6 means Saturday) +weekNumberModeAuto = False #???????????? +weekNumberMode = 7 + +# 0: First week contains first Sunday of year +# 4: First week contains first Thursday of year (ISO 8601, Gnome Clock) +# 6: First week contains first Saturday of year +# 7: First week contains first day of year +# 8: as Locale +## 1971(53), 1972(52), 1977, 1982, 1983, 1988, 1993, 1994 +## 1999(53),2000(52),2005(53),2010(53),2011(52),2016(53),2021,2022,2027,2028 +################################################################################ +################################################################################ +debugMode = False +useCompactJson = False ## FIXME +useAsciiJson = False +eventTextSep = ': ' ## use to seperate summary from description for display +eventTrashLastTop = True + + +########################################################################## + +loadConf() + +############################################################################ + + + +licenseText = _('licenseText') +if licenseText in ('licenseText', ''): + licenseText = open('%s/license'%rootDir).read() + +aboutText = _('aboutText') +if aboutText in ('aboutText', ''): + aboutText = open('%s/about'%rootDir).read() + + +weekDayName = ( + _('Sunday'), + _('Monday'), + _('Tuesday'), + _('Wednesday'), + _('Thursday'), + _('Friday'), + _('Saturday'), +) +weekDayNameAb = ( + _('Sun'), + _('Mon'), + _('Tue'), + _('Wed'), + _('Thu'), + _('Fri'), + _('Sat'), +) + + + +#if firstWeekDayAuto and os.sep=='/':## only if unix +# firstWeekDay = getLocaleFirstWeekDay() + +#if weekNumberModeAuto and os.sep=='/':## FIXME +# weekNumberMode = getLocaleWeekNumberMode() + diff --git a/scal3/date_utils.py b/scal3/date_utils.py new file mode 100644 index 000000000..276432d0e --- /dev/null +++ b/scal3/date_utils.py @@ -0,0 +1,81 @@ +from scal3.cal_types import calTypes, to_jd +from scal3.time_utils import getEpochFromJd + +getMonthLen = lambda year, month, mode: calTypes[mode].getMonthLen(year, month) + +def monthPlus(y, m, p): + y, m = divmod(y * 12 + m-1 + p, 12) + return y, m+1 + +dateEncode = lambda date: '%.4d/%.2d/%.2d'%tuple(date) +dateEncodeDash = lambda date: '%.4d-%.2d-%.2d'%tuple(date) + +def checkDate(date): + if not 1 <= date[1] <= 12: + raise ValueError('bad date %s (invalid month)'%date) + if not 1 <= date[2] <= 31: + raise ValueError('bad date %s (invalid day)'%date) + +def dateDecode(st): + neg = False + if st.startswith('-'): + neg = True + st = st[1:] + if '-' in st: + parts = st.split('-') + elif '/' in st: + parts = st.split('/') + else: + raise ValueError('bad date %s (invalid seperator)'%st) + if len(parts)!=3: + raise ValueError('bad date %s (invalid numbers count %s)'%(st, len(parts))) + try: + date = [int(p) for p in parts] + except ValueError: + raise ValueError('bad date %s (omitting non-numeric)'%st) + if neg: + date[0] *= -1 + checkDate(date) + return date + + +def validDate(mode, y, m, d):## move to cal-modules + if y<0: + return False + if m<1 or m>12: + return False + if d > getMonthLen(y, m, mode): + return False + return True + +datesDiff = lambda y1, m1, d1, y2, m2, d2: to_jd(calType.primary, y2, m2, d2) - to_jd(calType.primary, y1, m1, d1) + +dayOfYear = lambda y, m, d: datesDiff(y, 1, 1, y, m, d) + 1 + +## jwday: Calculate day of week from Julian day +## 0 = Sunday +## 1 = Monday +jwday = lambda jd: (jd + 1) % 7 + +def getJdRangeForMonth(year, month, mode): + day = getMonthLen(year, month, mode) + return ( + to_jd(year, month, 1, mode), + to_jd(year, month, day, mode) + 1, + ) + +def getFloatYearFromEpoch(epoch, mode): + module = calTypes[mode] + return float(epoch - module.epoch)/module.avgYearLen + 1 + +def getEpochFromFloatYear(year, mode): + module = calTypes[mode] + return module.epoch + (year-1)*module.avgYearLen + +getFloatYearFromJd = lambda jd, mode: getFloatYearFromEpoch(getEpochFromJd(jd), mode) + +getJdFromFloatYear = lambda year, mode: getJdFromEpoch(getEpochFromFloatYear(year, mode)) + +getEpochFromDate = lambda y, m, d, mode: getEpochFromJd(to_jd(y, m, d, mode)) + + diff --git a/scal3/event_diff.py b/scal3/event_diff.py new file mode 100644 index 000000000..a6052147a --- /dev/null +++ b/scal3/event_diff.py @@ -0,0 +1,86 @@ + +class EventDiff: + def __init__(self): + self.clear() + def clear(self): + self.byEventId = {} + ''' + self.byEventId = { + eid -> (order, action, gid, path) + } + actions: + '+' add + '-' remove + 'e' edit (modify) in-place + 'v' move to a new path + ''' + self.lastOrder = 0 + def add(self, action, event): + eid = event.id + gid = event.parent.id + path = event.getPath() + try: + prefOrder, prefAction, prefGid, prefPath = self.byEventId[eid] + except KeyError: + self.byEventId[eid] = (self.lastOrder, action, gid, path) + self.lastOrder += 1 + else: + if prefAction == '-' or action == '+': + raise RuntimeError('EventDiff.add: eid=%s, prefAction=%s, action=%s'%(eid, prefAction, action)) + both = prefAction + action + if both in ('+e', 'ee', 've'):## skip the new action + pass + elif both == '+-':## remove the last '+' action + del self.byEventId[eid] + elif both in ('e-', 'ev'):## replace the last edit action + self.byEventId[eid] = self.lastOrder, action, gid, path + self.lastOrder += 1 + elif both == 'v-': + self.byEventId[eid] = prefOrder, prefAction, gid, path + def __iter__(self): + for order, action, eid, gid, path in sorted([ + (order, action, eid, gid, path) + for eid, (order, action, gid, path) in self.byEventId.items() + ]): + if action == 'v': + yield '-', eid, gid, path + yield '+', eid, gid, path + else: + yield action, eid, gid, path + del self.byEventId[eid] + +def testEventDiff(): + d = EventDiff() + for action, eid in [ + ('+', 1), + ('+', 2), + ('+', 3), + ('-', 4), + ('e', 5), + ('-', 2), + ('e', 3), + ('e', 6), + ('e', 5), + ]: + d.add(action, eid, None) + for action, eid, path in d: + print(action, eid) + +if __name__=='__main__': + testEventDiff() + + + + + + + + + + + + + + + + diff --git a/scal3/event_lib.py b/scal3/event_lib.py new file mode 100644 index 000000000..76731d118 --- /dev/null +++ b/scal3/event_lib.py @@ -0,0 +1,4364 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + + +import json, os +from os.path import join, split, isdir, isfile, dirname, splitext +from os import listdir +import math +from time import time as now + +import natz + +from scal3.lib import OrderedDict + +from .path import * + +from scal3.lockfile import checkAndSaveJsonLockFile +from scal3.utils import printError, ifloor, iceil, findNearestIndex, myRaise, myRaiseTback +from scal3.utils import toStr +from scal3.os_utils import makeDir +from scal3.interval_utils import * +from scal3.time_utils import * +from scal3.date_utils import * +from scal3.json_utils import jsonToData + + +from scal3.s_object import * + +from scal3.cal_types import calTypes, jd_to, to_jd, convert, DATE_GREG, getSysDate +from scal3 import ics +from scal3.locale_man import tr as _ +from scal3.locale_man import getMonthName, textNumEncode +from scal3 import core +from scal3.core import log, getAbsWeekNumberFromJd, jwday, jd_to_primary + + + +dayLen = 24*3600 + +icsMinStartYear = 1970 +icsMaxEndYear = 2050 + +eventsDir = join(confDir, 'event', 'events') +groupsDir = join(confDir, 'event', 'groups') +accountsDir = join(confDir, 'event', 'accounts') + +########################## + +lockPath = join(confDir, 'event', 'lock.json') +readOnly = checkAndSaveJsonLockFile(lockPath) +if readOnly: + print('Event lock file %s exists, EVENT DATA IS READ-ONLY'%lockPath) + +########################## + +makeDir(eventsDir) +makeDir(groupsDir) +makeDir(accountsDir) + +################################################### + +class JsonEventObj(JsonSObj): + def save(self): + if readOnly: + print('events are read-only, ignored file %s'%self.file) + return + JsonSObj.save(self) + + +class BsonHistEventObj(BsonHistObj): + def save(self, *args): + if readOnly: + print('events are read-only, ignored file %s'%self.file) + return + return BsonHistObj.save(self, *args) + + + + +class InfoWrapper(JsonEventObj): + file = join(confDir, 'event', 'info.json') + skipLoadNoFile = True + params = ( + 'version', + 'last_run', + ) + paramsOrder = ( + 'version', + 'last_run', + ) + def __init__(self): + self.version = '' + self.last_run = 0 + def update(self): + self.version = core.VERSION + self.last_run = int(now()) + def updateAndSave(self): + self.update() + self.save() + +info = InfoWrapper.load() + +################################################### + +class LastIdsWrapper(JsonEventObj): + skipLoadNoFile = True + file = join(confDir, 'event', 'last_ids.json') + params = ( + 'event', + 'group', + 'account', + ) + paramsOrder = ( + 'event', + 'group', + 'account', + ) + def __init__(self): + self.event = 0 + self.group = 0 + self.account = 0 + + +lastIds = LastIdsWrapper.load() + +########################################################################### + +class ClassGroup(list): + def __init__(self, tname): + list.__init__(self) + self.tname = tname + self.names = [] + self.byName = {} + self.byDesc = {} + self.main = None + def register(self, cls): + assert cls.name != '' + cls.tname = self.tname + self.append(cls) + self.names.append(cls.name) + self.byName[cls.name] = cls + self.byDesc[cls.desc] = cls + return cls + def setMain(self, cls): + self.main = cls + return cls + +class classes: + rule = ClassGroup('rule') + notifier = ClassGroup('notifier') + event = ClassGroup('event') + group = ClassGroup('group') + account = ClassGroup('account') + +defaultEventTypeIndex = 0 ## FIXME +defaultGroupTypeIndex = 0 ## FIXME + +__plugin_api_get__ = [ + 'classes', 'defaultEventTypeIndex', 'defaultGroupTypeIndex', + 'EventRule', 'EventNotifier', 'Event', 'EventGroup', 'Account', +] + + +########################################################################### + +def getEventUID(event): + import socket + event_st = core.compressLongInt(hash(str(event.getData()))) + time_st = core.getCompactTime() + host = socket.gethostname() + return event_st + '_' + time_st + '@' + host + +class BadEventFile(Exception):## FIXME + pass + + +class Occurrence(SObj): + def __init__(self): + self.event = None + def intersection(self): + raise NotImplementedError + getDaysJdList = lambda self: [] ## make generator FIXME + getTimeRangeList = lambda self: [] ## make generator FIXME + def getFloatJdRangeList(self): + ls = [] + for ep0, ep1 in self.getTimeRangeList(): + ls.append((getFloatJdFromEpoch(ep0), getFloatJdFromEpoch(ep1))) + return ls + def getStartJd(self): + raise NotImplementedError + def getEndJd(self): + raise NotImplementedError + #__iter__ = lambda self: iter(self.getTimeRangeList()) + +class JdSetOccurrence(Occurrence): + name = 'jdSet' + def __init__(self, jdSet=None): + Occurrence.__init__(self) + if not jdSet: + jdSet = [] + self.jdSet = set(jdSet) + __repr__ = lambda self: 'JdSetOccurrence(%r)'%list(self.jdSet) + __bool__ = lambda self: bool(self.jdSet) + __len__ = lambda self: len(self.jdSet) + getStartJd = lambda self: min(self.jdSet) + getEndJd = lambda self: max(self.jdSet)+1 + def intersection(self, occur): + if isinstance(occur, JdSetOccurrence): + return JdSetOccurrence(self.jdSet.intersection(occur.jdSet)) + elif isinstance(occur, TimeRangeListOccurrence): + return TimeRangeListOccurrence( + intersectionOfTwoIntervalList( + self.getTimeRangeList(), + occur.getTimeRangeList(), + ) + ) + elif isinstance(occur, TimeListOccurrence): + return occur.intersection(self) + else: + raise TypeError + getDaysJdList = lambda self: sorted(self.jdSet) + getTimeRangeList = lambda self: [ + ( + getEpochFromJd(jd), + getEpochFromJd(jd+1), + ) for jd in self.jdSet + ] + def calcJdRanges(self): + jdList = sorted(self.jdSet) ## jdList is sorted + if not jdList: + return [] + startJd = jdList[0] + endJd = startJd + 1 + jdRanges = [] + for jd in jdList[1:]: + if jd == endJd: + endJd += 1 + else: + jdRanges.append((startJd, endJd)) + startJd = jd + endJd = startJd + 1 + jdRanges.append((startJd, endJd)) + return jdRanges + + +class TimeRangeListOccurrence(Occurrence): + name = 'timeRange' + def __init__(self, rangeList=None): + Occurrence.__init__(self) + if not rangeList: + rangeList = [] + self.rangeList = rangeList + __repr__ = lambda self: 'TimeRangeListOccurrence(%r)'%self.rangeList + __bool__ = lambda self: bool(self.rangeList) + __len__ = lambda self: len(self.rangeList) + #__getitem__ = lambda i: self.rangeList.__getitem__(i)## FIXME + getStartJd = lambda self: getJdFromEpoch(min([r[0] for r in self.rangeList])) + getEndJd = lambda self: getJdFromEpoch(max([r[1] for r in self.rangeList]+[r[1] for r in self.rangeList])) + def intersection(self, occur): + if isinstance(occur, (JdSetOccurrence, TimeRangeListOccurrence)): + return TimeRangeListOccurrence( + intersectionOfTwoIntervalList( + self.getTimeRangeList(), + occur.getTimeRangeList(), + ) + ) + elif isinstance(occur, TimeListOccurrence): + return occur.intersection(self) + else: + raise TypeError('bad type %s (%r)'%(occur.__class__.__name__, occur)) + def getDaysJdList(self): + jds = set() + for startEpoch, endEpoch in self.rangeList: + for jd in getJdListFromEpochRange(startEpoch, endEpoch): + jds.add(jd) + return sorted(jds) + getTimeRangeList = lambda self: self.rangeList + @staticmethod + def newFromStartEnd(startEpoch, endEpoch): + if startEpoch > endEpoch: + return TimeRangeListOccurrence([]) + else: + return TimeRangeListOccurrence([(startEpoch, endEpoch)]) + + + +class TimeListOccurrence(Occurrence): + name = 'repeativeTime' + def __init__(self, *args): + Occurrence.__init__(self) + if len(args)==0: + self.startEpoch = 0 + self.endEpoch = 0 + self.stepSeconds = -1 + self.epochList = set() + if len(args)==1: + self.epochList = set(args[0]) + elif len(args)==3: + self.setRange(*args) + else: + raise ValueError + __repr__ = lambda self: 'TimeListOccurrence(%r)'%self.epochList + #__bool__ = lambda self: self.startEpoch == self.endEpoch + __bool__ = lambda self: bool(self.epochList) + getStartJd = lambda self: getJdFromEpoch(min(self.epochList)) + getEndJd = lambda self: getJdFromEpoch(max(self.epochList)+1) + def setRange(self, startEpoch, endEpoch, stepSeconds): + try: + from numpy.core.multiarray import arange + except ImportError: + from scal3.utils import arange + ###### + self.startEpoch = startEpoch + self.endEpoch = endEpoch + self.stepSeconds = stepSeconds + self.epochList = set(arange(startEpoch, endEpoch, stepSeconds)) + def intersection(self, occur): + if isinstance(occur, (JdSetOccurrence, TimeRangeListOccurrence)): + epochBetween = [] + for epoch in self.epochList: + for startEpoch, endEpoch in occur.getTimeRangeList(): + if startEpoch <= epoch < endEpoch: + epochBetween.append(epoch) + break + return TimeListOccurrence(epochBetween) + elif isinstance(occur, TimeListOccurrence): + return TimeListOccurrence( + self.epochList.intersection(occur.epochList) + ) + else: + raise TypeError + def getDaysJdList(self):## improve performance ## FIXME + jds = set() + for epoch in self.epochList: + jds.add(getJdFromEpoch(epoch)) + return sorted(jds) + getTimeRangeList = lambda self:\ + [ + ( + epoch, + epoch, + ) + for epoch in self.epochList + ]## or end=None ## FIXME + + + +## Should not be registered, or instantiate directly +@classes.rule.setMain +class EventRule(SObj): + name = '' + desc = '' + provide = () + need = () + conflict = () + sgroup = -1 + expand = False + params = () + __bool__ = lambda self: True + def __init__(self, parent):## parent can be an event or group + self.parent = parent + getMode = lambda self: self.parent.mode + changeMode = lambda self, mode: True + def calcOccurrence(self, startJd, endJd, event): + raise NotImplementedError + getInfo = lambda self: self.desc + ': %s'%self + getEpochFromJd = lambda self, jd: getEpochFromJd(jd, tz=self.parent.getTimeZoneObj()) + +class AllDayEventRule(EventRule): + jdMatches = lambda self, jd: True + def calcOccurrence(self, startJd, endJd, event):## improve performance ## FIXME + jds = set() + for jd in range(startJd, endJd): + if self.jdMatches(jd): + jds.add(jd)## benchmark FIXME + return JdSetOccurrence(jds) + +## Should not be registered, or instantiate directly +class MultiValueAllDayEventRule(AllDayEventRule): + conflict = ( + 'date', + ) + #params = ('values',) + expand = True ## FIXME + def __init__(self, parent): + EventRule.__init__(self, parent) + self.values = [] + getData = lambda self: self.values + def setData(self, data): + if not isinstance(data, (tuple, list)): + data = [data] + self.values = data + formatValue = lambda self, v: _(v) + __str__ = lambda self: textNumEncode(numRangesEncode(self.values)) + def hasValue(self, value): + for item in self.values: + if isinstance(item, (tuple, list)): + if item[0] <= value <= item[1]: + return True + elif item == value: + return True + return False + def getValuesPlain(self): + ls = [] + for item in self.values: + if isinstance(item, (tuple, list)): + ls += list(range(item[0], item[1]+1)) + else: + ls.append(item) + return ls + def setValuesPlain(self, values): + self.values = simplifyNumList(values) + changeMode = lambda self, mode: False + +@classes.rule.register +class YearEventRule(MultiValueAllDayEventRule): + name = 'year' + desc = _('Year') + def __init__(self, parent): + MultiValueAllDayEventRule.__init__(self, parent) + self.values = [getSysDate(self.getMode())[0]] + jdMatches = lambda self, jd: self.hasValue(jd_to(jd, self.getMode())[0]) + def newModeValues(self, newMode): + curMode = self.getMode() + yearConv = lambda year: convert(year, 7, 1, curMode, newMode)[0] + values2 = [] + for item in self.values: + if isinstance(item, (tuple, list)): + values2.append(( + yearConv(item[0]), + yearConv(item[1]), + )) + else: + values2.append(yearConv(item)) + return values + def changeMode(self, mode):## FIXME + self.values = self.newModeValues(mode) + return True + +@classes.rule.register +class MonthEventRule(MultiValueAllDayEventRule): + name = 'month' + desc = _('Month') + def __init__(self, parent): + MultiValueAllDayEventRule.__init__(self, parent) + self.values = [1] + jdMatches = lambda self, jd: self.hasValue(jd_to(jd, self.getMode())[1]) + ## overwrite __str__? FIXME + + +@classes.rule.register +class DayOfMonthEventRule(MultiValueAllDayEventRule): + name = 'day' + desc = _('Day of Month') + def __init__(self, parent): + MultiValueAllDayEventRule.__init__(self, parent) + self.values = [1] + jdMatches = lambda self, jd: self.hasValue(jd_to(jd, self.getMode())[2]) + + +@classes.rule.register +class WeekNumberModeEventRule(EventRule): + name = 'weekNumMode' + desc = _('Week Number') + need = ( + 'start',## FIXME + ) + conflict = ( + 'date', + ) + params = ( + 'weekNumMode', + ) + EVERY_WEEK, ODD_WEEKS, EVEN_WEEKS = list(range(3)) ## remove EVERY_WEEK? FIXME + weekNumModeNames = ('any', 'odd', 'even') + def __init__(self, parent): + EventRule.__init__(self, parent) + self.weekNumMode = self.EVERY_WEEK + getData = lambda self: self.weekNumModeNames[self.weekNumMode] + def setData(self, modeName): + if not modeName in self.weekNumModeNames: + raise BadEventFile('bad rule weekNumMode=%r, the value for weekNumMode must be one of %r'\ + %(modeName, self.weekNumModeNames)) + self.weekNumMode = self.weekNumModeNames.index(modeName) + def calcOccurrence(self, startJd, endJd, event):## improve performance ## FIXME + startAbsWeekNum = getAbsWeekNumberFromJd(event.getStartJd()) - 1 ## 1st week ## FIXME + if self.weekNumMode==self.EVERY_WEEK: + return JdSetOccurrence(list(range(startJd, endJd))) + elif self.weekNumMode==self.ODD_WEEKS: + jds = set() + for jd in range(startJd, endJd): + if (getAbsWeekNumberFromJd(jd)-startAbsWeekNum)%2 == 1: + jds.add(jd) + return JdSetOccurrence(jds) + elif self.weekNumMode==self.EVEN_WEEKS: + jds = set() + for jd in range(startJd, endJd): + if (getAbsWeekNumberFromJd(jd)-startAbsWeekNum)%2 == 0: + jds.add(jd) + return JdSetOccurrence(jds) + def getInfo(self): + if self.weekNumMode == self.EVERY_WEEK: + return '' + elif self.weekNumMode == self.ODD_WEEKS: + return _('Odd Weeks') + elif self.weekNumMode == self.EVEN_WEEKS: + return _('Even Weeks') + +@classes.rule.register +class WeekDayEventRule(AllDayEventRule): + name = 'weekDay' + desc = _('Day of Week') + conflict = ( + 'date', + ) + params = ( + 'weekDayList', + ) + def __init__(self, parent): + EventRule.__init__(self, parent) + self.weekDayList = list(range(7)) ## or [] ## FIXME + getData = lambda self: self.weekDayList + def setData(self, data): + if isinstance(data, int): + self.weekDayList = [data] + elif isinstance(data, (tuple, list)): + self.weekDayList = data + else: + raise BadEventFile('bad rule weekDayList=%s, value for weekDayList must be a list of integers (0 for sunday)'%data) + jdMatches = lambda self, jd: jwday(jd) in self.weekDayList + def getInfo(self): + if self.weekDayList == list(range(7)): + return '' + sep = _(',') + ' ' + sep2 = ' ' + _('or') + ' ' + return _('Day of Week') + ': ' + \ + sep.join([core.weekDayName[wd] for wd in self.weekDayList[:-1]]) + \ + sep2 + core.weekDayName[self.weekDayList[-1]] + + +@classes.rule.register +class WeekMonthEventRule(EventRule): + name = 'weekMonth' + desc = _('Week-Month') + conflict = ( + 'date', + 'month', + 'day', + 'weekNumMode' + 'weekday', + 'start', + 'end', + 'cycleDays', + 'duration', + 'cycleLen', + ) + params = ( + 'month',## 0..12 ## 0 means every month + 'wmIndex',## 0..4 + 'weekDay',## 0..7 + ) + ''' + paramsValidators = { + 'month': lambda m: 0 <= m <= 12, + 'wmIndex': lambda m: 0 <= m <= 4, + 'weekDay': lambda m: 0 <= m <= 7, + } + ''' + wmIndexNames = ( + _('First'),## 0 + _('Second'),## 1 + _('Third'),## 2 + _('Fourth'),## 3 + _('Last'),## 4 + ) + def __init__(self, parent): + EventRule.__init__(self, parent) + self.month = 1 + self.wmIndex = 4 + self.weekDay = core.firstWeekDay + #def setJd(self, jd):## usefull? FIXME + # self.month, self.wmIndex, self.weekDay = core.getMonthWeekNth(jd, self.getMode()) + #def getJd(self): + def calcOccurrence(self, startJd, endJd, event): + mode = self.getMode() + startYear, startMonth, startDay = jd_to(startJd, mode) + endYear, endMonth, endDay = jd_to(endJd, mode) + jds = set() + monthList = list(range(1, 13)) if self.month==0 else [self.month] + for year in range(startYear, endYear): + for month in monthList: + jd = to_jd(year, month, 7*self.wmIndex+1, mode) + jd += (self.weekDay-jwday(jd))%7 + if self.wmIndex == 4:## Last (Fouth or Fifth) + if jd_to(jd, mode)[1] != month: + jd -= 7 + if startJd <= jd < endJd: + jds.add(jd) + return JdSetOccurrence(jds) + + + +@classes.rule.register +class DateEventRule(EventRule): + name = 'date' + desc = _('Date') + need = () + conflict = ( + 'year', 'month', 'day', 'weekNumMode', 'weekDay' + 'start', 'end', 'cycleDays', 'duration', 'cycleLen' + )## all rules except for dayTime and dayTimeRange (and possibly hourList, minuteList, secondList) + ## also conflict with 'holiday' ## FIXME + __str__ = lambda self: dateEncode(self.date) + def __init__(self, parent): + EventRule.__init__(self, parent) + self.date = getSysDate(self.getMode()) + getData = lambda self: str(self) + def setData(self, data): + self.date = dateDecode(data) + def getJd(self): + year, month, day = self.date + return to_jd(year, month, day, self.getMode()) + getEpoch = lambda self: self.getEpochFromJd(self.getJd()) + def setJd(self, jd): + self.date = jd_to(jd, self.getMode()) + def calcOccurrence(self, startJd, endJd, event): + myJd = self.getJd() + if startJd <= myJd < endJd: + return JdSetOccurrence([myJd]) + else: + return JdSetOccurrence() + def changeMode(self, mode): + self.date = jd_to(self.getJd(), mode) + return True + +class DateAndTimeEventRule(DateEventRule): + sgroup = 1 + params = ( + 'date', + 'time', + ) + def __init__(self, parent): + DateEventRule.__init__(self, parent) + self.time = localtime()[3:6] + getEpoch = lambda self: self.parent.getEpochFromJhms( + self.getJd(), + self.time[0], + self.time[1], + self.time[2], + ) + def setEpoch(self, epoch): + jd, h, m, s = self.parent.getJhmsFromEpoch(epoch) + self.setJd(jd) + self.time = (h, m, s) + def setJdExact(self, jd): + self.setJd(jd) + self.time = (0, 0, 0) + def setDate(self, date): + self.date = date + self.time = (0, 0, 0) + getDate = lambda self, mode: convert(self.date[0], self.date[1], self.date[2], self.getMode(), mode) + getData = lambda self: { + 'date': dateEncode(self.date), + 'time': timeEncode(self.time), + } + def setData(self, arg): + if isinstance(arg, dict): + self.date = dateDecode(arg['date']) + if 'time' in arg: + self.time = timeDecode(arg['time']) + elif isinstance(arg, str): + self.date = dateDecode(arg) + else: + raise BadEventFile('bad rule %s=%r'%(self.name, arg)) + getInfo = lambda self: self.desc + ': ' + dateEncode(self.date) + _(',') + ' ' + _('Time') + ': ' + timeEncode(self.time) + + + +@classes.rule.register +class DayTimeEventRule(EventRule):## Moment Event + name = 'dayTime' + desc = _('Time in Day') + provide = ( + 'time', + ) + conflict = ( + 'dayTimeRange', + 'cycleLen', + ) + params = ( + 'dayTime', + ) + def __init__(self, parent): + EventRule.__init__(self, parent) + self.dayTime = localtime()[3:6] + getData = lambda self: timeEncode(self.dayTime) + def setData(self, data): + self.dayTime = timeDecode(data) + def calcOccurrence(self, startJd, endJd, event): + mySec = getSecondsFromHms(*self.dayTime) + return TimeListOccurrence(## FIXME + self.getEpochFromJd(startJd) + mySec, + self.getEpochFromJd(endJd) + mySec + 1, + dayLen, + ) + getInfo = lambda self: _('Time in Day') + ': ' + timeEncode(self.dayTime) + +@classes.rule.register +class DayTimeRangeEventRule(EventRule): + name = 'dayTimeRange' + desc = _('Day Time Range') + conflict = ( + 'dayTime', + 'cycleLen', + ) + params = ( + 'dayTimeStart', + 'dayTimeEnd', + ) + def __init__(self, parent): + EventRule.__init__(self, parent) + self.dayTimeStart = (0, 0, 0) + self.dayTimeEnd = (24, 0, 0) + def setRange(self, start, end): + self.dayTimeStart = tuple(start) + self.dayTimeEnd = tuple(end) + getHourRange = lambda self: ( + timeToFloatHour(*self.dayTimeStart), + timeToFloatHour(*self.dayTimeEnd), + ) + getSecondsRange = lambda self: ( + getSecondsFromHms(*self.dayTimeStart), + getSecondsFromHms(*self.dayTimeEnd), + ) + getData = lambda self: (timeEncode(self.dayTimeStart), timeEncode(self.dayTimeEnd)) + setData = lambda self, data: self.setRange(timeDecode(data[0]), timeDecode(data[1])) + def calcOccurrence(self, startJd, endJd, event): + daySecStart = getSecondsFromHms(*self.dayTimeStart) + daySecEnd = getSecondsFromHms(*self.dayTimeEnd) + if daySecEnd <= daySecStart: + daySecEnd = daySecStart + tmList = [] + for jd in range(startJd, endJd): + epoch = self.getEpochFromJd(jd) + tmList.append((epoch+daySecStart, epoch+daySecEnd)) + return TimeRangeListOccurrence(tmList) + + +@classes.rule.register +class StartEventRule(DateAndTimeEventRule): + name = 'start' + desc = _('Start') + conflict = ( + 'date', + ) + def calcOccurrence(self, startJd, endJd, event): + return TimeRangeListOccurrence.newFromStartEnd( + max(self.getEpochFromJd(startJd), self.getEpoch()), + self.getEpochFromJd(endJd), + ) + + +@classes.rule.register +class EndEventRule(DateAndTimeEventRule): + name = 'end' + desc = _('End') + conflict = ( + 'date', + 'duration', + ) + def calcOccurrence(self, startJd, endJd, event): + return TimeRangeListOccurrence.newFromStartEnd( + self.getEpochFromJd(startJd), + min(self.getEpochFromJd(endJd), self.getEpoch()), + ) + + +@classes.rule.register +class DurationEventRule(EventRule): + name = 'duration' + desc = _('Duration') + need = ( + 'start', + ) + conflict = ( + 'date', + 'end', + ) + params = ( + 'value', + 'unit', + ) + sgroup = 1 + units = (1, 60, 3600, dayLen, 7*dayLen) + def __init__(self, parent): + EventRule.__init__(self, parent) + self.value = 0 + self.unit = 1 ## seconds + getSeconds = lambda self: self.value * self.unit + def setSeconds(self, s): + for unit in reversed(self.units): + if s%unit == 0: + self.value, self.unit = int(s//unit), unit + return + self.unit, self.value = int(s), 1 + def setData(self, data): + try: + self.value, self.unit = durationDecode(data) + except Exception as e: + log.error('Error while loading event rule "%s": %s'%(self.name, e)) + getData = lambda self: durationEncode(self.value, self.unit) + def calcOccurrence(self, startJd, endJd, event): + myStartEpoch = self.parent['start'].getEpoch() + startEpoch = max( + myStartEpoch, + self.getEpochFromJd(startJd), + ) + endEpoch = min( + myStartEpoch + self.getSeconds(), + self.getEpochFromJd(endJd), + ) + return TimeRangeListOccurrence.newFromStartEnd( + startEpoch, + endEpoch, + ) + + + +def cycleDaysCalcOccurrence(days, startJd, endJd, event): + eStartJd = event.getStartJd() + if startJd <= eStartJd: + startJd = eStartJd + else: + startJd = eStartJd + ((startJd - eStartJd - 1) // days + 1) * days + return JdSetOccurrence(list(range( + startJd, + endJd, + days, + ))) + +@classes.rule.register +class CycleDaysEventRule(EventRule): + name = 'cycleDays' + desc = _('Cycle (Days)') + need = ( + 'start', + ) + conflict = ( + 'date', + 'cycleLen', + ) + params = ( + 'days', + ) + def __init__(self, parent): + EventRule.__init__(self, parent) + self.days = 7 + getData = lambda self: self.days + def setData(self, days): + self.days = days + calcOccurrence = lambda self, startJd, endJd, event: cycleDaysCalcOccurrence(self.days, startJd, endJd, event) + getInfo = lambda self: _('Repeat: Every %s Days')%_(self.days) + +@classes.rule.register +class CycleWeeksEventRule(EventRule): + name = 'cycleWeeks' + desc = _('Cycle (Weeks)') + need = ( + 'start', + ) + conflict = ( + 'date', + 'cycleDays', + 'cycleLen', + ) + params = ( + 'weeks', + ) + def __init__(self, parent): + EventRule.__init__(self, parent) + self.weeks = 1 + getData = lambda self: self.weeks + def setData(self, weeks): + self.weeks = weeks + calcOccurrence = lambda self, startJd, endJd, event: cycleDaysCalcOccurrence(self.weeks*7, startJd, endJd, event) + getInfo = lambda self: _('Repeat: Every %s Weeks')%_(self.weeks) + + +@classes.rule.register +class CycleLenEventRule(EventRule): + name = 'cycleLen' ## or 'cycle' FIXME + desc = _('Cycle (Days & Time)') + provide = ( + 'time', + ) + need = ( + 'start', + ) + conflict = ( + 'date', + 'dayTime', + 'dayTimeRange', + 'cycleDays', + ) + params = ( + 'days', + 'extraTime', + ) + def __init__(self, parent): + EventRule.__init__(self, parent) + self.days = 7 + self.extraTime = (0, 0, 0) + getData = lambda self: { + 'days': self.days, + 'extraTime': timeEncode(self.extraTime), + } + def setData(self, arg): + self.days = arg['days'] + self.extraTime = timeDecode(arg['extraTime']) + def calcOccurrence(self, startJd, endJd, event): + startEpoch = self.getEpochFromJd(startJd) + eventStartEpoch = event.getStartEpoch() + ## + cycleSec = self.days*dayLen + getSecondsFromHms(*self.extraTime) + ## + if startEpoch <= eventStartEpoch: + startEpoch = eventStartEpoch + else: + startEpoch = eventStartEpoch + ((startEpoch - eventStartEpoch - 1) // cycleSec + 1) * cycleSec + ## + return TimeListOccurrence( + startEpoch, + self.getEpochFromJd(endJd), + cycleSec, + ) + getInfo = lambda self: _('Repeat: Every %s Days and %s')%(_(self.days), timeEncode(self.extraTime)) + +@classes.rule.register +class ExYearEventRule(YearEventRule): + name = 'ex_year' + desc = '[%s] %s'%(_('Exception'), _('Year')) + jdMatches = lambda self, jd: not YearEventRule.jdMatches(self, jd) + +@classes.rule.register +class ExMonthEventRule(MonthEventRule): + name = 'ex_month' + desc = '[%s] %s'%(_('Exception'), _('Month')) + jdMatches = lambda self, jd: not MonthEventRule.jdMatches(self, jd) + +@classes.rule.register +class ExDayOfMonthEventRule(DayOfMonthEventRule): + name = 'ex_day' + desc = '[%s] %s'%(_('Exception'), _('Day of Month')) + jdMatches = lambda self, jd: not DayOfMonthEventRule.jdMatches(self, jd) + +@classes.rule.register +class ExDatesEventRule(EventRule): + name = 'ex_dates' + desc = '[%s] %s'%(_('Exception'), _('Date')) + #conflict = ('date',)## FIXME + params = ( + 'dates', + ) + def __init__(self, parent): + EventRule.__init__(self, parent) + self.setDates([]) + def setDates(self, dates): + self.dates = dates + self.jdList = [to_jd(y, m, d, self.getMode()) for y, m, d in dates] + def calcOccurrence(self, startJd, endJd, event):## improve performance ## FIXME + return JdSetOccurrence( + set(range(startJd, endJd)).difference(self.jdList) + ) + def getData(self): + datesConf = [] + for date in self.dates: + datesConf.append(dateEncode(date)) + return datesConf + def setData(self, datesConf): + dates = [] + if isinstance(datesConf, str): + for date in datesConf.split(','): + dates.append(dateDecode(date.strip())) + else: + for date in datesConf: + if isinstance(date, str): + date = dateDecode(date) + elif isinstance(date, (tuple, list)): + checkDate(date) + dates.append(date) + self.setDates(dates) + def changeMode(self, mode): + dates = [] + for jd in self.jdList: + dates.append(jd_to(jd, mode)) + self.dates = dates + +#@classes.rule.register +#class HolidayEventRule(EventRule):## FIXME +# name = 'holiday' +# desc = _('Holiday') +# conflict = ('date',) + + +#@classes.rule.register +#class ShowInMCalEventRule(EventRule):## FIXME +# name = 'show_cal' +# desc = _('Show in Calendar') + +#@classes.rule.register +#class SunTimeRule(EventRule):## FIXME +## ... minutes before Sun Rise eval('sunRise-x') +## ... minutes after Sun Rise eval('sunRise+x') +## ... minutes before Sun Set eval('sunSet-x') +## ... minutes after Sun Set eval('sunSet+x') + +########################################################################### +########################################################################### + +## Should not be registered, or instantiate directly +@classes.notifier.setMain +class EventNotifier(SObj): + name = '' + desc = '' + params = () + def __init__(self, event): + self.event = event + getMode = lambda self: self.event.mode + def notify(self, finishFunc): + pass + +@classes.notifier.register +class AlarmNotifier(EventNotifier): + name = 'alarm' + desc = _('Alarm') + params = ( + 'alarmSound', + 'playerCmd', + ) + def __init__(self, event): + EventNotifier.__init__(self, event) + self.alarmSound = '' ## FIXME + self.playerCmd = 'mplayer' + +@classes.notifier.register +class FloatingMsgNotifier(EventNotifier): + name = 'floatingMsg' + desc = _('Floating Message') + params = ( + 'fillWidth', + 'speed', + 'bgColor', + 'textColor', + ) + def __init__(self, event): + EventNotifier.__init__(self, event) + ### + self.fillWidth = False + self.speed = 100 + self.bgColor = (255, 255, 0) + self.textColor = (0, 0, 0) + +@classes.notifier.register +class WindowMsgNotifier(EventNotifier): + name = 'windowMsg' + desc = _('Message Window')## FIXME + params = ( + 'extraMessage', + ) + def __init__(self, event): + EventNotifier.__init__(self, event) + self.extraMessage = '' + ## window icon ## FIXME + +#@classes.notifier.register## FIXME +class CommandNotifier(EventNotifier): + name = 'command' + desc = _('Run a Command') + params = ( + 'command', + 'pyEval', + ) + def __init__(self, event): + EventNotifier.__init__(self, event) + self.command = '' + self.pyEval = False + +########################################################################### +########################################################################### + +class RuleContainer: + requiredRules = () + supportedRules = None + params = ( + 'timeZoneEnable', + 'timeZone', + ) + paramsOrder = ( + 'timeZoneEnable', + 'timeZone', + ) + def __init__(self): + self.timeZoneEnable = False + self.timeZone = '' + ### + self.clearRules() + self.rulesHash = None + def clearRules(self): + self.rulesOd = OrderedDict() + getRule = lambda self, key: self.rulesOd.__getitem__(key) + setRule = lambda self, key, value: self.rulesOd.__setitem__(key, value) + getRulesData = lambda self: [(rule.name, rule.getData()) for rule in self.rulesOd.values()] + getRulesHash = lambda self: hash(str( + ( + self.getTimeZoneStr(), + sorted(self.getRulesData()), + ) + )) + getRuleNames = lambda self: self.rulesOd.keys() + addRule = lambda self, rule: self.rulesOd.__setitem__(rule.name, rule) + def addNewRule(self, ruleType): + rule = classes.rule.byName[ruleType](self) + self.addRule(rule) + return rule + def getAddRule(self, ruleType): + try: + return self.getRule(ruleType) + except KeyError: + return self.addNewRule(ruleType) + removeRule = lambda self, rule: self.rulesOd.__delitem__(rule.name) + __delitem__ = lambda self, key: self.rulesOd.__delitem__(key) + __getitem__ = lambda self, key: self.getRule(key) + __setitem__ = lambda self, key, value: self.setRule(key, value) + __iter__ = lambda self: iter(self.rulesOd.values()) + def setRulesData(self, rulesData): + self.clearRules() + for ruleName, ruleData in rulesData: + rule = classes.rule.byName[ruleName](self) + rule.setData(ruleData) + self.addRule(rule) + def addRequirements(self): + for name in self.requiredRules: + if not name in self.rulesOd: + self.addNewRule(name) + def checkAndAddRule(self, rule): + ok, msg = self.checkRulesDependencies(newRule=rule) + if ok: + self.addRule(rule) + return (ok, msg) + def removeSomeRuleTypes(self, *rmTypes): + for ruleType in rmTypes: + try: + del self.rulesOd[ruleType] + except KeyError: + pass + def checkAndRemoveRule(self, rule): + ok, msg = self.checkRulesDependencies(disabledRule=rule) + if ok: + self.removeRule(rule) + return (ok, msg) + def checkRulesDependencies(self, newRule=None, disabledRule=None, autoCheck=True): + rulesOd = self.rulesOd.copy() + if newRule: + rulesOd[newRule.name] = newRule + if disabledRule: + #try: + del rulesOd[disabledRule.name] + #except: + # pass + provideList = [] + for ruleName, rule in rulesOd.items(): + provideList.append(ruleName) + provideList += rule.provide + for rule in rulesOd.values(): + for conflictName in rule.conflict: + if conflictName in provideList: + return (False, '%s "%s" %s "%s"'%( + _('Conflict between'), + _(rule.desc), + _('and'), + _(rulesOd[conflictName].desc), + )) + for needName in rule.need: + if not needName in provideList: + ## find which rule(s) provide(s) needName ## FIXME + return (False, '"%s" %s "%s"'%( + _(rule.desc), + _('needs'), + _(needName), #_(rulesOd[needName].desc) + )) + return (True, '') + def copyRulesFrom(self, other): + for ruleType, rule in other.rulesOd.items(): + if ruleType in self.supportedRules: + self.getAddRule(ruleType).copyFrom(rule) + def copySomeRuleTypesFrom(self, other, *ruleTypes): + for ruleType in ruleTypes: + if not ruleType in self.supportedRules: + print('copySomeRuleTypesFrom: unsupported rule %s for container %r'%(ruleType, self)) + continue + try: + rule = other.rulesOd[ruleType] + except KeyError: + pass + else: + self.getAddRule(ruleType).copyFrom(rule) + def getTimeZoneObj(self): + if self.timeZoneEnable and self.timeZone: + try: + return natz.timezone(self.timeZone) + except: + myRaise() + return core.localTz + getTimeZoneStr = lambda self: str(self.getTimeZoneObj()) + getEpochFromJd = lambda self, jd: getEpochFromJd(jd, tz=self.getTimeZoneObj()) + getJdFromEpoch = lambda self, jd: getJdFromEpoch(jd, tz=self.getTimeZoneObj()) + getJhmsFromEpoch = lambda self, epoch: getJhmsFromEpoch(epoch, tz=self.getTimeZoneObj()) + getEpochFromJhms = lambda self, jd, h, m , s: getEpochFromJhms(jd, h, m , s, tz=self.getTimeZoneObj()) + + +def fixIconInData(data): + icon = data['icon'] + iconDir, iconName = split(icon) + if iconDir == join(pixDir, 'event'): + icon = iconName + data['icon'] = icon + +def fixIconInObj(self): + icon = self.icon + if icon and not '/' in icon: + icon = join(pixDir, 'event', icon) + self.icon = icon + +########################################################################### +########################################################################### + +## Should not be registered, or instantiate directly +@classes.event.setMain +class Event(BsonHistEventObj, RuleContainer): + name = 'custom'## or 'event' or '' FIXME + desc = _('Custom Event') + iconName = '' + #requiredNotifiers = ()## needed? FIXME + readOnly = False + isAllDay = False + isSingleOccur = False + basicParams = ( + #'modified', + 'remoteIds', + 'notifiers',## FIXME + ) + params = RuleContainer.params + ( + 'icon', + 'summary', + 'description', + ) + paramsOrder = RuleContainer.paramsOrder + ( + 'type', + 'calType', + 'summary', + 'description', + 'rules', + 'notifiers', + 'notifyBefore', + 'remoteIds', + 'modified', + ) + @classmethod + def getFile(cls, _id): + return join(eventsDir, '%s.json'%_id) + @classmethod + def getSubclass(cls, _type): + return classes.event.byName[_type] + @classmethod + def getDefaultIcon(cls): + return join(pixDir, 'event', cls.iconName+'.png') if cls.iconName else '' + __bool__ = lambda self: bool(self.rulesOd) ## FIXME + __repr__ = lambda self: '%s(id=%s)'%(self.__class__.__name__, self.id) + __str__ = lambda self: '%s(id=%s, summary=%s)'%( + self.__class__.__name__, + self.id, + self.summary, + ) + def __init__(self, _id=None, parent=None): + if _id is None: + self.id = None + else: + self.setId(_id) + self.parent = parent + try: + self.mode = parent.mode + except: + self.mode = calTypes.primary + self.icon = self.__class__.getDefaultIcon() + self.summary = self.desc ## + ' (' + _(self.id) + ')' ## FIXME + self.description = '' + self.files = [] + ###### + RuleContainer.__init__(self) + self.timeZoneEnable = not self.isAllDay + self.notifiers = [] + self.notifyBefore = (0, 1) ## (value, unit) like DurationEventRule + ## self.snoozeTime = (5, 60) ## (value, unit) like DurationEventRule ## FIXME + self.addRequirements() + self.setDefaults() + if parent is not None: + self.setDefaultsFromGroup(parent) + ###### + self.modified = now()## FIXME + self.remoteIds = None## (accountId, groupId, eventId) + ## remote groupId and eventId both can be integer or string or unicode (depending on remote account type) + def getShownDescription(self): + if not self.description: + return '' + try: + showFull = self.parent.showFullEventDesc + except: + showFull = False + if showFull: + return self.description + else: + return self.description.split('\n')[0] + def afterModify(self): + if self.id is None: + self.setId() + self.modified = now()## FIXME + #self.parent.eventsModified = self.modified + ### + if self.parent and self.id in self.parent.idList: + rulesHash = self.getRulesHash() + ## what is self.notifyBefore is changed? BUG FIXME + if rulesHash != self.rulesHash: + self.parent.updateOccurrenceEvent(self) + self.rulesHash = rulesHash + else:## None or enbale=False + self.rulesHash = '' + getNotifyBeforeSec = lambda self: self.notifyBefore[0] * self.notifyBefore[1] + getNotifyBeforeMin = lambda self: int(self.getNotifyBeforeSec()/60) + def setDefaults(self): + ''' + sets default values that depends on event type + not common parameters, like those are set in __init__ + DON'T call this method from parent event class + ''' + pass + def setDefaultsFromGroup(self, group): + ''' + Call this method from parent event class + ''' + if group.icon:## and not self.icon FIXME + self.icon = group.icon + def getInfo(self): + lines = [] + rulesDict = self.rulesOd.copy() + for rule in rulesDict.values(): + lines.append(rule.getInfo()) + return '\n'.join(lines) + #def addRequirements(self): + # RuleContainer.addRequirements(self) + # notifierNames = (notifier.name for notifier in self.notifiers) + # for name in self.requiredNotifiers: + # if not name in notifierNames: + # self.notifiers.append(classes.notifier.byName[name](self)) + def loadFiles(self): + self.files = [] + #if isdir(self.filesDir): + # for fname in listdir(self.filesDir): + # if isfile(join(self.filesDir, fname)) and not fname.endswith('~'):## FIXME + # self.files.append(fname) + #getUrlForFile = lambda self, fname: 'file:' + os.sep*2 + self.filesDir + os.sep + fname + def getFilesUrls(self): + data = [] + baseUrl = self.getUrlForFile('') + for fname in self.files: + data.append(( + baseUrl + fname, + _('File') + ': ' + fname, + )) + return data + getSummary = lambda self: self.summary + getDescription = lambda self: self.description + def getTextParts(self): + summary = self.getSummary() + description = self.getDescription() + try: + sep = self.parent.eventTextSep + except: + sep = core.eventTextSep + if description: + return (summary, sep, description) + else: + return (summary,) + getText = lambda self, showDesc=True: ''.join(self.getTextParts()) if showDesc else self.summary + def setId(self, _id=None): + if _id is None or _id<0: + _id = lastIds.event + 1 ## FIXME + lastIds.event = _id + elif _id > lastIds.event: + lastIds.event = _id + self.id = _id + self.file = self.getFile(self.id) + #self.filesDir = join(self.dir, 'files') + self.loadFiles() + def invalidate(self): + ## make sure it can't be written to file again, it's about to be deleted + self.id = None + self.file = '' + def save(self): + if self.id is None: + self.setId() + #makeDir(self.dir) + BsonHistEventObj.save(self) + def copyFrom(self, other, exact=False):## FIXME + BsonHistEventObj.copyFrom(self, other) + self.mode = other.mode + self.notifyBefore = other.notifyBefore[:] + #self.files = other.files[:] + self.notifiers = other.notifiers[:]## FIXME + self.copyRulesFrom(other) + self.addRequirements() + #### + ## copy dates between different rule types in different event types + jd = other.getJd() + if jd is not None: + if exact: + self.setJdExact(jd) + else: + self.setJd(jd) + def getData(self): + data = BsonHistEventObj.getData(self) + data.update({ + 'type': self.name, + 'calType': calTypes.names[self.mode], + 'rules': self.getRulesData(), + 'notifiers': self.getNotifiersData(), + 'notifyBefore': durationEncode(*self.notifyBefore), + }) + fixIconInData(data) + return data + def setData(self, data): + BsonHistEventObj.setData(self, data) + if self.remoteIds: + self.remoteIds = tuple(self.remoteIds) + if 'id' in data: + self.setId(data['id']) + if 'calType' in data: + calType = data['calType'] + try: + self.mode = calTypes.names.index(calType) + except ValueError: + raise ValueError('Invalid calType: %r'%calType) + self.clearRules() + if 'rules' in data: + self.setRulesData(data['rules']) + self.notifiers = [] + if 'notifiers' in data: + for notifierName, notifierData in data['notifiers']: + notifier = classes.notifier.byName[notifierName](self) + notifier.setData(notifierData) + self.notifiers.append(notifier) + if 'notifyBefore' in data: + self.notifyBefore = durationDecode(data['notifyBefore']) + fixIconInObj(self) + #def load(self):## skipRules arg for use in ui_gtk/event/notify.py ## FIXME + getNotifiersData = lambda self: [(notifier.name, notifier.getData()) for notifier in self.notifiers] + getNotifiersDict = lambda self: dict(self.getNotifiersData()) + def calcOccurrence(self, startJd, endJd):## float jd ## cache Occurrences ## FIXME + rules = list(self.rulesOd.values()) + if not rules: + return JdSetOccurrence() + occur = rules[0].calcOccurrence(startJd, endJd, self) + for rule in rules[1:]: + try: + startJd = occur.getStartJd() + except: + pass + try: + endJd = occur.getEndJd() + except: + pass + occur = occur.intersection(rule.calcOccurrence(startJd, endJd, self)) + occur.event = self + return occur ## FIXME + calcOccurrenceAll = lambda self: self.calcOccurrence(self.parent.startJd, self.parent.endJd) + #def calcFirstOccurrenceAfterJd(self, startJd):## too much tricky! FIXME + def notify(self, finishFunc): + self.n = len(self.notifiers) + def notifierFinishFunc(): + self.n -= 1 + if self.n<=0: + try: + finishFunc() + except: + pass + for notifier in self.notifiers: + notifier.notify(notifierFinishFunc) + def getIcsData(self, prettyDateTime=False):## FIXME + return None + def setIcsData(self, data): + ''' + if 'T' in data['DTSTART']: + return False + if 'T' in data['DTEND']: + return False + startJd = ics.getJdByIcsDate(data['DTSTART']) + endJd = ics.getJdByIcsDate(data['DTEND']) + if 'RRULE' in data: + rrule = dict(ics.splitIcsValue(data['RRULE'])) + if rrule['FREQ'] == 'YEARLY': + y0, m0, d0 = jd_to(startJd, self.mode) + y1, m1, d1 = jd_to(endJd, self.mode) + if y0 != y1:## FIXME + return False + yr = self.getAddRule('year') + interval = int(rrule.get('INTERVAL', 1)) + + elif rrule['FREQ'] == 'MONTHLY': + pass + elif rrule['FREQ'] == 'WEEKLY': + pass + ''' + return False + def changeMode(self, mode): + backupRulesOd = self.rulesOd.copy()## is it deep copy? FIXME + if mode != self.mode: + for rule in self.rulesOd.values(): + if not rule.changeMode(mode): + self.rulesOd = backupRulesOd + return False + self.mode = mode + return True + def getStartJd(self):## FIXME + try: + return self['start'].getJd() + except KeyError: + pass + try: + return self['date'].getJd() + except KeyError: + pass + return self.parent.startJd + def getEndJd(self):## FIXME + try: + return self['end'].getJd() + except KeyError: + pass + try: + return self['date'].getJd() + except KeyError: + pass + return self.parent.endJd + def getStartEpoch(self):## FIXME + try: + return self['start'].getEpoch() + except KeyError: + pass + try: + return self['date'].getEpoch() + except KeyError: + pass + return getEpochFromJd(self.parent.startJd) + def getEndEpoch(self):## FIXME + try: + return self['end'].getEpoch() + except KeyError: + pass + try: + return self['date'].getEpoch() + except KeyError: + pass + return self.getEpochFromJd(self.parent.endJd) + getJd = lambda self: self.getStartJd() + setJd = lambda self, jd: None + setJdExact = lambda self, jd: self.setJd(jd) + +class SingleStartEndEvent(Event): + isSingleOccur = True + setStartEpoch = lambda self, epoch: self.getAddRule('start').setEpoch(epoch) + setEndEpoch = lambda self, epoch: self.getAddRule('end').setEpoch(epoch) + setJd = lambda self, jd: self.getAddRule('start').setJd(jd) + def setJdExact(self, jd): + self.getAddRule('start').setJdExact(jd) + self.getAddRule('end').setJdExact(jd+1) + getIcsData = lambda self, prettyDateTime=False: [ + ('DTSTART', ics.getIcsTimeByEpoch(self.getStartEpoch(), prettyDateTime)), + ('DTEND', ics.getIcsTimeByEpoch(self.getEndEpoch(), prettyDateTime)), + ('TRANSP', 'OPAQUE'), + ('CATEGORIES', self.name),## FIXME + ] + def calcOccurrence(self, startJd, endJd): + return TimeRangeListOccurrence.newFromStartEnd( + max(self.getEpochFromJd(startJd), self.getStartEpoch()), + min(self.getEpochFromJd(endJd), self.getEndEpoch()), + ) + + + +@classes.event.register +class TaskEvent(SingleStartEndEvent): + ## overwrites getEndEpoch from Event + ## overwrites setEndEpoch from SingleStartEndEvent + ## overwrites setJdExact from SingleStartEndEvent + ## Methods neccessery for modifying event by hand in timeline: + ## getStartEpoch, getEndEpoch, modifyStart, modifyEnd, modifyPos + name = 'task' + desc = _('Task') + iconName = 'task' + requiredRules = ( + 'start', + ) + supportedRules = ( + 'start', + 'end', + 'duration', + ) + isAllDay = False + def setDefaults(self): + self.setStart( + getSysDate(self.mode), + tuple(localtime()[3:6]), + ) + self.setEnd('duration', 1, 3600) + def setDefaultsFromGroup(self, group): + Event.setDefaultsFromGroup(self, group) + if group.name == 'taskList': + value, unit = group.defaultDuration + self.setEnd('duration', value, unit) + def setStart(self, date, dayTime): + start = self['start'] + start.date = date + start.time = dayTime + def setEnd(self, endType, *values): + self.removeSomeRuleTypes('end', 'duration') + if endType=='date': + rule = EndEventRule(self) + rule.date, rule.time = values + elif endType=='epoch': + rule = EndEventRule(self) + rule.setEpoch(values[0]) + elif endType=='duration': + rule = DurationEventRule(self) + rule.value, rule.unit = values + else: + raise ValueError('invalid endType=%r'%endType) + self.addRule(rule) + def getStart(self): + start = self['start'] + return (start.date, start.time) + def getEnd(self): + try: + end = self['end'] + except KeyError: + pass + else: + return ('date', (end.date, end.time)) + try: + duration = self['duration'] + except KeyError: + pass + else: + return ('duration', (duration.value, duration.unit)) + raise ValueError('no end date neither duration specified for task') + def getEndEpoch(self): + try: + end = self['end'] + except KeyError: + pass + else: + return end.getEpoch() + try: + duration = self['duration'] + except KeyError: + pass + else: + return self['start'].getEpoch() + duration.getSeconds() + raise ValueError('no end date neither duration specified for task') + def setEndEpoch(self, epoch): + try: + end = self['end'] + except KeyError: + pass + else: + end.setEpoch(epoch) + return + try: + duration = self['duration'] + except KeyError: + pass + else: + duration.setSeconds(epoch-self['start'].getEpoch()) + return + raise ValueError('no end date neither duration specified for task') + def modifyPos(self, newStartEpoch): + start = self['start'] + try: + end = self['end'] + except KeyError: + pass + else: + end.setEpoch(end.getEpoch() + newStartEpoch - start.getEpoch()) + start.setEpoch(newStartEpoch) + def modifyStart(self, newStartEpoch): + start = self['start'] + try: + duration = self['duration'] + except KeyError: + pass + else: + duration.value -= float(newStartEpoch - start.getEpoch()) / duration.unit + start.setEpoch(newStartEpoch) + def modifyEnd(self, newEndEpoch): + try: + end = self['end'] + except KeyError: + duration = self['duration'] + duration.value = float(newEndEpoch - self.getStartEpoch()) / duration.unit + else: + end.setEpoch(newEndEpoch) + def setJdExact(self, jd): + self.getAddRule('start').setJdExact(jd) + self.setEnd('duration', 24, 3600) + def copyFrom(self, other, *a, **kw): + Event.copyFrom(self, other, *a, **kw) + myStart = self['start'] + ## + if other.name == self.name: + endType, values = other.getEnd() + self.setEnd(endType, *values) + else: + if other.name == 'dailyNote': + myStart.time = (0, 0, 0) + self.setEnd('duration', 24, 3600) + elif other.name == 'allDayTask': + self.removeSomeRuleTypes('end', 'duration') + self.copySomeRuleTypesFrom(other, 'start', 'end', 'duration') + else: + try: + myStart.time = other['dayTime'].dayTime + except KeyError: + pass + def setIcsData(self, data): + self.setStartEpoch(ics.getEpochByIcsTime(data['DTSTART'])) + self.setEndEpoch(ics.getEpochByIcsTime(data['DTEND']))## FIXME + return True + + +@classes.event.register +class AllDayTaskEvent(SingleStartEndEvent):## overwrites getEndEpoch from SingleStartEndEvent + name = 'allDayTask' + desc = _('All-Day Task') + iconName = 'task' + requiredRules = ( + 'start', + ) + supportedRules = ( + 'start', + 'end', + 'duration', + ) + isAllDay = True + def setJd(self, jd): + self.getAddRule('start').setJdExact(jd) + def setStartDate(self, date): + self.getAddRule('start').setDate(date) + def setJdExact(self, jd): + self.setJd(jd) + self.setEnd('duration', 1) + def setDefaults(self): + jd = core.getCurrentJd() + self.setJd(jd) + self.setEnd('duration', 1) + #def setDefaultsFromGroup(self, group):## FIXME + # Event.setDefaultsFromGroup(self, group) + # if group.name == 'taskList': + # value, unit = group.defaultAllDayDuration + # if value > 0: + # self.setEnd('duration', value) + def setEnd(self, endType, value): + self.removeSomeRuleTypes('end', 'duration') + if endType=='date': + rule = EndEventRule(self) + rule.setDate(value) + elif endType=='jd': + rule = EndEventRule(self) + rule.setJd(value) + elif endType=='epoch': + rule = EndEventRule(self) + rule.setJd(self.getJdFromEpoch(values[0])) + elif endType=='duration': + rule = DurationEventRule(self) + rule.value = value + rule.unit = dayLen + else: + raise ValueError('invalid endType=%r'%endType) + self.addRule(rule) + def getEnd(self): + try: + end = self['end'] + except KeyError: + pass + else: + return ('date', end.date) + try: + duration = self['duration'] + except KeyError: + pass + else: + return ('duration', duration.value) + raise ValueError('no end date neither duration specified for task') + def getEndJd(self): + try: + end = self['end'] + except KeyError: + pass + else: + return end.getJd() + try: + duration = self['duration'] + except KeyError: + pass + else: + return self['start'].getJd() + duration.getSeconds()//dayLen + raise ValueError('no end date neither duration specified for task') + getEndEpoch = lambda self: self.getEpochFromJd(self.getEndJd()) + #def setEndJd(self, jd): + # self.getAddRule('end').setJdExact(jd) + def setEndJd(self, jd): + try: + end = self['end'] + except KeyError: + pass + else: + end.setJd(jd) + return + try: + duration = self['duration'] + except KeyError: + pass + else: + duration.setSeconds(dayLen*(jd-self['start'].getJd())) + return + raise ValueError('no end date neither duration specified for task') + getIcsData = lambda self, prettyDateTime=False: [ + ('DTSTART', ics.getIcsDateByJd(self.getJd(), prettyDateTime)), + ('DTEND', ics.getIcsDateByJd(self.getEndJd(), prettyDateTime)), + ('TRANSP', 'OPAQUE'), + ('CATEGORIES', self.name),## FIXME + ] + def setIcsData(self, data): + self.setJd(ics.getJdByIcsDate(data['DTSTART'])) + self.setEndJd(ics.getJdByIcsDate(data['DTEND']))## FIXME + return True + def copyFrom(self, other): + SingleStartEndEvent.copyFrom(self, other) + if other.name == self.name: + self.setEnd(*other.getEnd()) + + +@classes.event.register +class DailyNoteEvent(Event): + name = 'dailyNote' + desc = _('Daily Note') + isSingleOccur = True + iconName = 'note' + requiredRules = ( + 'date', + ) + supportedRules = ( + 'date', + ) + isAllDay = True + getDate = lambda self: self['date'].date + def setDate(self, year, month, day): + self['date'].date = (year, month, day) + getJd = lambda self: self['date'].getJd() + setJd = lambda self, jd: self['date'].setJd(jd) + def setDefaults(self): + self.setDate(*getSysDate(self.mode)) + def calcOccurrence(self, startJd, endJd):## float jd + jd = self.getJd() + return JdSetOccurrence([jd] if startJd <= jd < endJd else []) + def getIcsData(self, prettyDateTime=False): + jd = self.getJd() + return [ + ('DTSTART', ics.getIcsDateByJd(jd, prettyDateTime)), + ('DTEND', ics.getIcsDateByJd(jd+1, prettyDateTime)), + ('TRANSP', 'TRANSPARENT'), + ('CATEGORIES', self.name),## FIXME + ] + def setIcsData(self, data): + self.setJd(ics.getJdByIcsDate(data['DTSTART'])) + return True + +@classes.event.register +class YearlyEvent(Event): + name = 'yearly' + desc = _('Yearly Event') + iconName = 'birthday' + requiredRules = ( + 'month', + 'day', + ) + supportedRules = requiredRules + ('start',) + paramsOrder = Event.paramsOrder + ('startYear', 'month', 'day') + isAllDay = True + getMonth = lambda self: self['month'].values[0] + setMonth = lambda self, month: self.getAddRule('month').setData(month) + getDay = lambda self: self['day'].values[0] + setDay = lambda self, day: self.getAddRule('day').setData(day) + def setDefaults(self): + y, m, d = getSysDate(self.mode) + self.setMonth(m) + self.setDay(d) + def getJd(self):## used only for copyFrom + try: + startRule = self['start'] + except: + y = getSysDate(self.mode)[0] + else: + y = startRule.getDate(self.mode)[0] + m = self.getMonth() + d = self.getDay() + return to_jd(y, m, d, self.mode) + def setJd(self, jd):## used only for copyFrom + y, m, d = jd_to(jd, self.mode) + self.setMonth(m) + self.setDay(d) + self.getAddRule('start').date = (y, 1, 1) + def calcOccurrence(self, startJd, endJd):## float jd + mode = self.mode + month = self.getMonth() + day = self.getDay() + try: + startRule = self['start'] + except: + pass + else: + startJd = max(startJd, startRule.getJd()) + startYear = jd_to(ifloor(startJd), mode)[0] + endYear = jd_to(iceil(endJd), mode)[0] + jds = set() + for year in (startYear, endYear+1): + jd = to_jd(year, month, day, mode) + if startJd <= jd < endJd: + jds.add(jd) + for year in range(startYear+1, endYear): + jds.add(to_jd(year, month, day, mode)) + return JdSetOccurrence(jds) + def getData(self): + data = Event.getData(self) + try: + data['startYear'] = int(self['start'].date[0]) + except KeyError: + pass + data['month'] = self.getMonth() + data['day'] = self.getDay() + del data['rules'] + return data + def setData(self, data): + Event.setData(self, data) + try: + startYear = int(data['startYear']) + except KeyError: + pass + except Exception as e: + print(str(e)) + else: + self.getAddRule('start').date = (startYear, 1, 1) + try: + month = data['month'] + except KeyError: + pass + else: + self.setMonth(month) + try: + day = data['day'] + except KeyError: + pass + else: + self.setDay(day) + def getSuggestedStartYear(self): + try: + startJd = self.parent.startJd + except: + startJd = core.getCurrentJd() + return jd_to(startJd, self.mode)[0] + def getSummary(self): + summary = Event.getSummary(self) + try: + showDate = self.parent.showDate + except AttributeError: + showDate = True + if showDate: + newParts = [ + _(self.getDay()), + getMonthName(self.mode, self.getMonth()), + ] + try: + startRule = self['start'] + except KeyError: + pass + else: + newParts.append(_(startRule.date[0])) + summary = ' '.join(newParts) + ': ' + summary + return summary + def getIcsData(self, prettyDateTime=False): + if self.mode != DATE_GREG: + return None + month = self.getMonth() + day = self.getDay() + startYear = icsMinStartYear + try: + startRule = self['start'] + except: + try: + startYear = jd_to(self.parent.startJd, DATE_GREG)[0] + except AttributeError: + pass + else: + startYear = startRule.getDate(DATE_GREG)[0] + jd = to_jd( + startYear, + month, + day, + DATE_GREG, + ) + return [ + ('DTSTART', ics.getIcsDateByJd(jd, prettyDateTime)), + ('DTEND', ics.getIcsDateByJd(jd+1, prettyDateTime)), + ('RRULE', 'FREQ=YEARLY;BYMONTH=%d;BYMONTHDAY=%d'%(month, day)), + ('TRANSP', 'TRANSPARENT'), + ('CATEGORIES', self.name),## FIXME + ] + def setIcsData(self, data): + rrule = dict(ics.splitIcsValue(data['RRULE'])) + try: + month = int(rrule['BYMONTH'])## multiple values are not supported + except: + return False + try: + day = int(rrule['BYMONTHDAY'])## multiple values are not supported + except: + return False + self.setMonth(month) + self.setDay(day) + self.mode = DATE_GREG + return True + + +@classes.event.register +class MonthlyEvent(Event): + name = 'monthly' + desc = _('Monthly Event') + iconName = '' + requiredRules = ( + 'start', + 'end', + 'day', + 'dayTimeRange', + ) + supportedRules = requiredRules + isAllDay = False + def setJd(self, jd): + year, month, day = jd_to(jd, self.mode) + self['start'].setDate((year, month, 1)) + self['end'].setDate((year+1, month, 1)) + self.setDay(day) + def setDefaults(self): + self.setJd(core.getCurrentJd()) + def getDay(self): + try: + return self['day'].values[0] + except IndexError: + return 1 + def setDay(self, day): + self['day'].values = [day] + + +@classes.event.register +class WeeklyEvent(Event): + name = 'weekly' + desc = _('Weekly Event') + iconName = '' + requiredRules = ( + 'start', + 'end', + 'cycleWeeks', + 'dayTimeRange', + ) + supportedRules = requiredRules + isAllDay = False + def setDefaults(self): + currentJd = core.getCurrentJd() + self['start'].setJd(currentJd) + self['end'].setJd(currentJd+8) + +#@classes.event.register +#class UniversityCourseOwner(Event):## FIXME + +@classes.event.register +class UniversityClassEvent(Event): + name = 'universityClass' + desc = _('Class') + iconName = 'university' + requiredRules = ( + 'weekNumMode', + 'weekDay', + 'dayTimeRange', + ) + supportedRules = ( + 'weekNumMode', + 'weekDay', + 'dayTimeRange', + ) + params = Event.params + ( + 'courseId', + ) + isAllDay = False + def __init__(self, _id=None, parent=None): + ## assert group is not None ## FIXME + Event.__init__(self, _id, parent) + self.courseId = None ## FIXME + def setDefaultsFromGroup(self, group): + Event.setDefaultsFromGroup(self, group) + if group.name=='universityTerm': + try: + tm0, tm1 = group.classTimeBounds[:2] + except: + myRaise() + else: + self['dayTimeRange'].setRange( + tm0 + (0,), + tm1 + (0,), + ) + getCourseName = lambda self: self.parent.getCourseNameById(self.courseId) + getWeekDayName = lambda self: core.weekDayName[self['weekDay'].weekDayList[0]] + def updateSummary(self): + self.summary = _('%s Class')%self.getCourseName() + ' (' + self.getWeekDayName() + ')' + def setJd(self, jd): + self['weekDay'].weekDayList = [jwday(jd)] + ## set weekNumMode from absWeekNumber FIXME + def getIcsData(self, prettyDateTime=False): + startJd = self['start'].getJd() + endJd = self['end'].getJd() + occur = event.calcOccurrence(startJd, endJd) + tRangeList = occur.getTimeRangeList() + if not tRangeList: + return + return [ + ('DTSTART', ics.getIcsTimeByEpoch( + tRangeList[0][0], + prettyDateTime, + )), + ('DTEND', ics.getIcsTimeByEpoch( + tRangeList[0][1], + prettyDateTime, + )), + ('RRULE', 'FREQ=WEEKLY;UNTIL=%s;INTERVAL=%s;BYDAY=%s'%( + ics.getIcsDateByJd(endJd, prettyDateTime), + 1 if event['weekNumMode'].getData()=='any' else 2, + ics.encodeIcsWeekDayList(event['weekDay'].weekDayList), + )), + ('TRANSP', 'OPAQUE'), + ('CATEGORIES', self.name),## FIXME + ] + +@classes.event.register +class UniversityExamEvent(DailyNoteEvent): + name = 'universityExam' + desc = _('Exam') + iconName = 'university' + requiredRules = ( + 'date', + 'dayTimeRange', + ) + supportedRules = ( + 'date', + 'dayTimeRange', + ) + params = DailyNoteEvent.params + ( + 'courseId', + ) + isAllDay = False + def __init__(self, _id=None, parent=None): + ## assert group is not None ## FIXME + DailyNoteEvent.__init__(self, _id, parent) + self.courseId = None ## FIXME + def setDefaults(self): + self['dayTimeRange'].setRange((9, 0), (11, 0))## FIXME + def setDefaultsFromGroup(self, group): + DailyNoteEvent.setDefaultsFromGroup(self, group) + if group.name=='universityTerm': + self.setJd(group.endJd)## FIXME + getCourseName = lambda self: self.parent.getCourseNameById(self.courseId) + def updateSummary(self): + self.summary = _('%s Exam')%self.getCourseName() + def calcOccurrence(self, startJd, endJd): + jd = self.getJd() + if startJd <= jd < endJd: + epoch = self.getEpochFromJd(jd) + startSec, endSec = self['dayTimeRange'].getSecondsRange() + return TimeRangeListOccurrence( + [ + ( + epoch + startSec, + epoch + endSec, + ) + ] + ) + else: + return TimeRangeListOccurrence() + def getIcsData(self, prettyDateTime=False): + dayStart = self['date'].getEpoch() + startSec, endSec = self['dayTimeRange'].getSecondsRange() + return [ + ('DTSTART', ics.getIcsTimeByEpoch( + dayStart + startSec, + prettyDateTime, + )), + ('DTEND', ics.getIcsTimeByEpoch( + dayStart + endSec, + prettyDateTime + )), + ('TRANSP', 'OPAQUE'), + ] + + + + + +@classes.event.register +class LifeTimeEvent(SingleStartEndEvent): + name = 'lifeTime' + desc = _('Life Time Event') + requiredRules = ( + 'start', + 'end', + ) + supportedRules = ( + 'start', + 'end', + ) + isAllDay = True + #def setDefaults(self): + # self['start'].date = ... + def setJd(self, jd): + self.getAddRule('start').setJdExact(jd) + def addRule(self, rule): + if rule.name in ('start', 'end'): + rule.time = (0, 0, 0) + SingleStartEndEvent.addRule(self, rule) + def modifyPos(self, newStartEpoch): + start = self['start'] + end = self['end'] + newStartJd = round(getFloatJdFromEpoch(newStartEpoch)) + end.setJdExact(end.getJd() + newStartJd - start.getJd()) + start.setJdExact(newStartJd) + def modifyStart(self, newEpoch): + self['start'].setEpoch(roundEpochToDay(newEpoch)) + def modifyEnd(self, newEpoch): + self['end'].setEpoch(roundEpochToDay(newEpoch)) + + + + + + + +@classes.event.register +class LargeScaleEvent(Event):## or MegaEvent? FIXME + name = 'largeScale' + desc = _('Large Scale Event') + isSingleOccur = True + _myParams = ( + 'scale', + 'start', + 'end', + 'endRel', + ) + params = Event.params + _myParams + paramsOrder = Event.paramsOrder + _myParams + __bool__ = lambda self: True + isAllDay = True + def __init__(self, _id=None, parent=None): + self.scale = 1 ## 1, 1000, 1000**2, 1000**3 + self.start = 0 + self.end = 1 + self.endRel = True + Event.__init__(self, _id, parent) + def setData(self, data): + Event.setData(self, data) + if 'duration' in data: + data['end'] = data['duration'] + data['endRel'] = True + getRulesHash = lambda self: hash(str(( + self.getTimeZoneStr(), + 'largeScale', + self.scale, + self.start, + self.end, + self.endRel, + )))## FIXME hash(tpl) ot hash(str(tpl)) + getEnd = lambda self: self.start + self.end if self.endRel else self.end + def setDefaultsFromGroup(self, group): + Event.setDefaultsFromGroup(self, group) + if group.name == 'largeScale': + self.scale = group.scale + self.start = group.getStartValue() + getJd = lambda self: to_jd(self.start*self.scale, 1, 1, self.mode) + def setJd(self, jd): + self.start = jd_to(jd, self.mode)[0]//self.scale + def calcOccurrence(self, startJd, endJd): + myStartJd = iceil(to_jd(self.scale*self.start, 1, 1, self.mode)) + myEndJd = ifloor(to_jd(self.scale*self.getEnd(), 1, 1, self.mode)) + return TimeRangeListOccurrence.newFromStartEnd( + max( + self.getEpochFromJd(myStartJd), + self.getEpochFromJd(startJd), + ), + min( + self.getEpochFromJd(myEndJd), + self.getEpochFromJd(endJd), + ) + ) + #def getIcsData(self, prettyDateTime=False): + # pass + + + +@classes.event.register +class CustomEvent(Event): + name = 'custom' + desc = _('Custom Event') + isAllDay = False + + +########################################################################### +########################################################################### + + +class EventContainer(BsonHistEventObj): + name = '' + desc = '' + basicParams = ( + 'idList',## FIXME + ) + params = BsonHistEventObj.params + ( + 'icon', + 'title', + 'showFullEventDesc', + 'idList', + 'modified', + ) + def __getitem__(self, key): + if isinstance(key, int):## eventId + return self.getEvent(key) + else: + raise TypeError('invalid key type %r give to EventContainer.__getitem__'%key) + byIndex = lambda self, index: self.getEvent(self.idList[index]) + __str__ = lambda self: '%s(title=%s)'%(self.__class__.__name__, self.title) + def __init__(self, title='Untitled'): + self.parent = None + self.mode = calTypes.primary + self.idList = [] + self.title = title + self.icon = '' + self.showFullEventDesc = False + ###### + self.modified = now() + #self.eventsModified = self.modified + def afterModify(self): + self.modified = now() + def getEvent(self, eid): + if not eid in self.idList: + raise ValueError('%s does not contain %s'%(self, eid)) + eventFile = join(eventsDir, '%s.json'%eid) + if not isfile(eventFile): + self.idList.remove(eid) + self.save()## FIXME + raise FileNotFoundError( + 'error while loading event file %r: file not found (container: %r)'%(eventFile, self) + ) + data = jsonToData(open(eventFile).read()) + data['id'] = eid ## FIXME + updateBasicDataFromBson(data, eventFile, 'event') + event = classes.event.byName[data['type']](eid) + event.setData(data) + return event + def __iter__(self): + for eid in self.idList: + try: + event = self.getEvent(eid) + except Exception as e: + myRaise(e) + else: + yield event + __len__ = lambda self: len(self.idList) + def preAdd(self, event): + if event.id in self.idList: + raise ValueError('%s already contains %s'%(self, event)) + if event.parent not in (None, self): + raise ValueError('%s already has a parent=%s, trying to add to %s'%(event, event.parent, self)) + def postAdd(self, event): + event.parent = self ## needed? FIXME + def insert(self, index, event): + self.preAdd(event) + self.idList.insert(index, event.id) + self.postAdd(event) + def append(self, event): + self.preAdd(event) + self.idList.append(event.id) + self.postAdd(event) + index = lambda self, eid: self.idList.index(eid) + moveUp = lambda self, index: self.idList.insert(index-1, self.idList.pop(index)) + moveDown = lambda self, index: self.idList.insert(index+1, self.idList.pop(index)) + def remove(self, event):## call when moving to trash + ''' + excludes event from this container (group or trash), not delete event data completely + and returns the index of (previously contained) event + ''' + index = self.idList.index(event.id) + self.idList.remove(event.id) + event.parent = None + return index + def copyFrom(self, other): + BsonHistEventObj.copyFrom(self, other) + self.mode = other.mode + def getData(self): + data = BsonHistEventObj.getData(self) + data['calType'] = calTypes.names[self.mode] + fixIconInData(data) + return data + def setData(self, data): + BsonHistEventObj.setData(self, data) + if 'calType' in data: + calType = data['calType'] + try: + self.mode = calTypes.names.index(calType) + except ValueError: + raise ValueError('Invalid calType: %r'%calType) + ### + fixIconInObj(self) + + +@classes.group.register +@classes.group.setMain +class EventGroup(EventContainer): + name = 'group' + desc = _('Event Group') + acceptsEventTypes = ( + 'yearly', + 'dailyNote', + 'task', + 'allDayTask', + 'weekly', + 'monthly', + 'lifeTime', + 'largeScale', + 'custom', + ) + canConvertTo = () + actions = []## [('Export to ICS', 'exportToIcs')] + eventActions = [] ## FIXME + sortBys = ( + ## name, description, is_type_dependent + ('mode', _('Calendar Type'), False), + ('summary', _('Summary'), False), + ('description', _('Description'), False), + ('icon', _('Icon'), False), + ) + sortByDefault = 'summary' + basicParams = EventContainer.basicParams + ( + #'enable',## FIXME + #'remoteIds', user edits the value ## FIXME + 'remoteSyncData', + #'eventIdByRemoteIds', + 'deletedRemoteEvents', + ) + params = EventContainer.params + ( + #'enable', + 'showInDCal', + 'showInWCal', + 'showInMCal', + 'showInStatusIcon', + 'showInTimeLine', + 'color', + 'eventCacheSize', + 'eventTextSep', + 'startJd', + 'endJd', + 'remoteIds', + 'remoteSyncData', + #'eventIdByRemoteIds', + 'deletedRemoteEvents', + ## 'defaultEventType' + ) + paramsOrder = ( + 'enable', + 'type', + 'title', + 'calType', + 'showInDCal', + 'showInWCal', + 'showInMCal', + 'showInStatusIcon', + 'showInTimeLine', + 'showFullEventDesc', + 'color', + 'icon', + 'eventCacheSize', + 'eventTextSep', + 'startJd', + 'endJd', + 'remoteIds', + 'remoteSyncData', + #'eventIdByRemoteIds', + 'deletedRemoteEvents', + 'idList', + ) + @classmethod + def getFile(cls, _id): + return join(groupsDir, '%d.json'%_id) + @classmethod + def getSubclass(cls, _type): + return classes.group.byName[_type] + showInCal = lambda self: self.showInDCal or self.showInWCal or self.showInMCal + def getSortBys(self): + l = list(self.sortBys) + if self.enable: + l.append(('time_last', _('Last Occurrence Time'), False)) + l.append(('time_first', _('First Occurrence Time'), False)) + return 'time_last', l + else: + return self.sortByDefault, l + def getSortByValue(self, event, attr): + if attr in ('time_last', 'time_first'): + if event.isSingleOccur: + epoch = event.getStartEpoch() + if epoch is not None: + return epoch + if self.enable: + method = self.occur.getLastOfEvent if 'time_last' else self.occur.getFirstOfEvent + last = method(event.id) + if last: + return last[0] + else: + print('no time_last returned for event %s'%event.id) + return None + return getattr(event, attr, None) + def sort(self, attr='summary', reverse=False): + isTypeDep = True + for name, desc, dep in self.getSortBys()[1]: + if name == attr: + isTypeDep = dep + break + if isTypeDep: + event_key = lambda event: (event.name, self.getSortByValue(event, attr)) + else: + event_key = lambda event: self.getSortByValue(event, attr) + self.idList = sorted( + self.idList, + key=lambda eid: event_key(self.getEvent(eid)), + reverse=reverse, + ) + def __getitem__(self, key): + #if isinstance(key, basestring):## ruleName + # return self.getRule(key) + if isinstance(key, int):## eventId + return self.getEvent(key) + else: + raise TypeError('invalid key %r given to EventGroup.__getitem__'%key) + def __setitem__(self, key, value): + #if isinstance(key, basestring):## ruleName + # return self.setRule(key, value) + if isinstance(key, int):## eventId + raise TypeError('can not assign event to group')## FIXME + else: + raise TypeError('invalid key %r give to EventGroup.__setitem__'%key) + def __delitem__(self, key): + if isinstance(key, int):## eventId + self.remove(self.getEvent(key)) + else: + raise TypeError('invalid key %r give to EventGroup.__delitem__'%key) + checkEventToAdd = lambda self, event: event.name in self.acceptsEventTypes + __repr__ = lambda self: '%s(_id=%s)'%(self.__class__.__name__, self.id) + __str__ = lambda self: '%s(_id=%s, title=%s)'%( + self.__class__.__name__, + self.id, + self.title, + ) + def __init__(self, _id=None): + EventContainer.__init__(self, title=self.desc) + if _id is None: + self.id = None + else: + self.setId(_id) + self.enable = True + self.showInDCal = True + self.showInWCal = True + self.showInMCal = True + self.showInStatusIcon = False + self.showInTimeLine = True + self.color = (0, 0, 0) ## FIXME + #self.defaultNotifyBefore = (10, 60) ## FIXME + if len(self.acceptsEventTypes)==1: + self.defaultEventType = self.acceptsEventTypes[0] + icon = classes.event.byName[self.acceptsEventTypes[0]].getDefaultIcon() + if icon: + self.icon = icon + else: + self.defaultEventType = 'custom' + self.eventCacheSize = 0 + self.eventTextSep = core.eventTextSep + ### + self.eventCache = {} ## from eid to event object + ### + year, month, day = getSysDate(self.mode) + self.startJd = to_jd(year-10, 1, 1, self.mode) + self.endJd = to_jd(year+5, 1, 1, self.mode) + ## + self.initOccurrence() + ### + self.setDefaults() + ########### + self.clearRemoteAttrs() + def setRandomColor(self): + import random + from scal3.color_utils import hslToRgb + self.color = hslToRgb(random.uniform(0, 360), 1, 0.5)## FIXME + def clearRemoteAttrs(self): + self.remoteIds = None## (accountId, groupId) + ## remote groupId can be an integer or string or unicode (depending on remote account type) + self.remoteSyncData = {} + #self.eventIdByRemoteIds = {} + self.deletedRemoteEvents = {} + def save(self): + if self.id is None: + self.setId() + EventContainer.save(self) + def afterSync(self): + self.remoteSyncData[self.remoteIds] = now() + def getLastSync(self): + if self.remoteIds: + try: + return self.remoteSyncData[self.remoteIds] + except KeyError: + pass + def setDefaults(self): + ''' + sets default values that depends on group type + not common parameters, like those are set in __init__ + ''' + __bool__ = lambda self: self.enable ## FIXME + def setId(self, _id=None): + if _id is None or _id<0: + _id = lastIds.group + 1 ## FIXME + lastIds.group = _id + elif _id > lastIds.group: + lastIds.group = _id + self.id = _id + self.file = self.getFile(self.id) + def setTitle(self, title): + self.title = title + def setColor(self, color): + self.color = color + def getData(self): + data = EventContainer.getData(self) + data['type'] = self.name + for attr in ( + 'remoteSyncData', + #'eventIdByRemoteIds', + 'deletedRemoteEvents', + ): + if isinstance(data[attr], dict): + data[attr] = sorted(data[attr].items()) + return data + def setData(self, data): + if 'showInCal' in data:## for compatibility + data['showInDCal'] = data['showInWCal'] = data['showInMCal'] = data['showInCal'] + del data['showInCal'] + EventContainer.setData(self, data) + if isinstance(self.remoteIds, list): + self.remoteIds = tuple(self.remoteIds) + for attr in ( + 'remoteSyncData', + #'eventIdByRemoteIds', + 'deletedRemoteEvents', + ): + value = getattr(self, attr) + if isinstance(value, list): + valueDict = {} + for item in value: + if len(item) != 2: + continue + if not isinstance(item[0], (tuple, list)): + continue + valueDict[tuple(item[0])] = item[1] + setattr(self, attr, valueDict) + if 'id' in data: + self.setId(data['id']) + self.startJd = int(self.startJd) + self.endJd = int(self.endJd) + #### + #if 'defaultEventType' in data: + # self.defaultEventType = data['defaultEventType'] + # if not self.defaultEventType in classes.event.names: + # raise ValueError('Invalid defaultEventType: %r'%self.defaultEventType) + ################# Event objects should be accessed from outside only within one of these 3 methods + def getEvent(self, eid): + if not eid in self.idList: + raise ValueError('%s does not contain %s'%(self, eid)) + if eid in self.eventCache: + return self.eventCache[eid] + event = EventContainer.getEvent(self, eid) + event.parent = self + event.rulesHash = event.getRulesHash() + if self.enable and len(self.eventCache) < self.eventCacheSize: + self.eventCache[eid] = event + return event + def createEvent(self, eventType): + #if not eventType in self.acceptsEventTypes:## FIXME + # raise ValueError('Event type "%s" not supported in group "%s"'%(eventType, self.name)) + event = classes.event.byName[eventType](parent=self)## FIXME + return event + def copyEventWithType(self, event, eventType):## FIXME + newEvent = self.createEvent(eventType) + ### + newEvent.changeMode(event.mode) + newEvent.copyFrom(event) + ### + newEvent.setId(event.id) + event.invalidate() + ### + return newEvent + ############################################### + def remove(self, event):## call when moving to trash + index = EventContainer.remove(self, event) + try: + del self.eventCache[event.id] + except: + pass + if event.remoteIds: + self.deletedRemoteEvents[event.id] = (now(),) + event.remoteIds + #try: + # del self.eventIdByRemoteIds[event.remoteIds] + #except: + # pass + self.occurCount -= self.occur.delete(event.id) + return index + def removeAll(self):## clearEvents or excludeAll or removeAll FIXME + for event in self.eventCache.values(): + event.parent = None ## needed? FIXME + ### + self.idList = [] + self.eventCache = {} + self.occur.clear() + self.occurCount = 0 + def postAdd(self, event): + EventContainer.postAdd(self, event) + if len(self.eventCache) < self.eventCacheSize: + self.eventCache[event.id] = event + #if event.remoteIds: + # self.eventIdByRemoteIds[event.remoteIds] = event.id + ## need to update self.occur? + ## its done in event.afterModify() right? not when moving event from another group + if self.enable: + self.updateOccurrenceEvent(event) + def updateCache(self, event): + if event.id in self.eventCache: + self.eventCache[event.id] = event + event.afterModify() + def copy(self): + newGroup = SObj.copy(self) + newGroup.removeAll() + return newGroup + def copyAs(self, newGroupType): + newGroup = classes.group.byName[newGroupType]() + newGroup.copyFrom(self) + newGroup.removeAll() + return newGroup + def deepCopy(self): + newGroup = self.copy() + for event in self: + newEvent = event.copy() + newEvent.save() + newGroup.append(newEvent) + return newGroup + def deepConvertTo(self, newGroupType): + newGroup = self.copyAs(newGroupType) + newEventType = newGroup.acceptsEventTypes[0] + newGroup.enable = False ## to prevent per-event node update + for event in self: + newEvent = newGroup.createEvent(newEventType) + newEvent.changeMode(event.mode)## FIXME needed? + newEvent.copyFrom(event, True) + newEvent.setId(event.id) + newEvent.save() + newGroup.append(newEvent) + newGroup.enable = self.enable + self.removeAll()## events with the same id's, can not be contained by two groups + return newGroup + def calcOccurrenceAll(self): + startJd = self.startJd + endJd = self.endJd + for event in self: + occur = event.calcOccurrence(startJd, endJd) + if occur: + yield event, occur + def afterModify(self):## FIXME + EventContainer.afterModify(self) + self.initOccurrence() + #### + if self.enable: + self.updateOccurrence() + else: + self.eventCache = {} + def updateOccurrenceEvent(self, event): + if core.debugMode: + print('updateOccurrenceEvent', self.id, self.title, event.id) + eid = event.id + self.occurCount -= self.occur.delete(eid) + for t0, t1 in event.calcOccurrenceAll().getTimeRangeList(): + self.addOccur(t0, t1, eid) + def initOccurrence(self): + from scal3.event_search_tree import EventSearchTree + #from scal3.time_line_tree import TimeLineTree + #self.occur = TimeLineTree(offset=self.getEpochFromJd(self.endJd)) + self.occur = EventSearchTree() + #self.occurLoaded = False + self.occurCount = 0 + def clear(self): + self.occur.clear() + self.occurCount = 0 + def addOccur(self, t0, t1, eid): + self.occur.add(t0, t1, eid) + self.occurCount += 1 + def updateOccurrenceLog(self, stm0): + if core.debugMode: + print('updateOccurrence, id=%s, title=%s, count=%s, time=%s'%( + self.id, + self.title, + self.occurCount, + now()-stm0, + )) + def updateOccurrence(self): + stm0 = now() + self.clear() + for event, occur in self.calcOccurrenceAll(): + for t0, t1 in occur.getTimeRangeList(): + self.addOccur(t0, t1, event.id) + #self.occurLoaded = True + if core.debugMode: + print('time = %d ms'%((now()-stm0)*1000)) + #print('updateOccurrence, id=%s, title=%s, count=%s, time=%s'%( + # self.id, + # self.title, + # self.occurCount, + # now()-stm0, + #)) + #print('%s %d %.1f'%(self.id, 1000*(now()-stm0), self.occur.calcAvgDepth())) + def exportToIcsFp(self, fp): + currentTimeStamp = ics.getIcsTimeByEpoch(now()) + for event in self: + print('exportToIcsFp', event.id) + icsData = event.getIcsData() + ### + commonText = 'BEGIN:VEVENT\n' + commonText += 'CREATED:%s\n'%currentTimeStamp + commonText += 'DTSTAMP:%s\n'%currentTimeStamp ## FIXME + commonText += 'LAST-MODIFIED:%s\n'%currentTimeStamp + commonText += 'SUMMARY:%s\n'%event.getText() + commonText += 'DESCRIPTION:\n' + #commonText += 'CATEGORIES:%s\n'%self.title## FIXME + commonText += 'CATEGORIES:%s\n'%event.name## FIXME + commonText += 'LOCATION:\n' + commonText += 'SEQUENCE:0\n' + commonText += 'STATUS:CONFIRMED\n' + commonText += 'UID:%s\n'%getEventUID(event) + ### + if icsData is None: + occur = event.calcOccurrenceAll() + if occur: + if isinstance(occur, JdSetOccurrence): + for sectionStartJd, sectionEndJd in occur.calcJdRanges(): + #for sectionStartJd in occur.getDaysJdList(): + #sectionEndJd = sectionStartJd + 1 + vevent = commonText + vevent += 'DTSTART;VALUE=DATE:%.4d%.2d%.2d\n'%jd_to(sectionStartJd, DATE_GREG) + vevent += 'DTEND;VALUE=DATE:%.4d%.2d%.2d\n'%jd_to(sectionEndJd, DATE_GREG) + vevent += 'TRANSP:TRANSPARENT\n' ## http://www.kanzaki.com/docs/ical/transp.html + vevent += 'END:VEVENT\n' + fp.write(vevent) + elif isinstance(occur, (TimeRangeListOccurrence, TimeListOccurrence)): + for startEpoch, endEpoch in occur.getTimeRangeList(): + vevent = commonText + vevent += 'DTSTART:%s\n'%ics.getIcsTimeByEpoch(startEpoch) + if endEpoch is not None and endEpoch-startEpoch > 1: + vevent += 'DTEND:%s\n'%ics.getIcsTimeByEpoch(int(endEpoch))## why its float? FIXME + vevent += 'TRANSP:OPAQUE\n' ## FIXME ## http://www.kanzaki.com/docs/ical/transp.html + vevent += 'END:VEVENT\n' + fp.write(vevent) + else: + raise RuntimeError + else: + vevent = commonText + for key, value in icsData: + vevent += '%s:%s\n'%(key, value) + vevent += 'END:VEVENT\n' + fp.write(vevent) + importExportExclude = ( + 'remoteIds', + 'remoteSyncData', + #'eventIdByRemoteIds', + 'deletedRemoteEvents', + ) + def exportData(self): + data = self.getData() + for attr in self.importExportExclude: + del data[attr] + data = makeOrderedData(data, self.paramsOrder) + data['events'] = [] + for eventId in self.idList: + eventData = EventContainer.getEvent(self, eventId).getDataOrdered() + data['events'].append(eventData) + try: + del eventData['remoteIds'] ## FIXME + except KeyError: + pass + if not eventData['notifiers']: + del eventData['notifiers'] + del eventData['notifyBefore'] + del data['idList'] + return data + def importData(self, data): + self.setData(data) + self.clearRemoteAttrs() + for eventData in data['events']: + event = self.createEvent(eventData['type']) + event.setData(eventData) + event.save() + self.append(event) + self.save() + simpleFilters = { + 'text': lambda event, text: not text or text in event.getText(), + 'text_lower': lambda event, text: not text or text in event.getText().lower(), + 'modified_from': lambda event, epoch: event.modified >= epoch, + 'type': lambda event, _type: event.name == _type, + } + def search(self, conds): + if 'time_from' in conds or 'time_to' in conds: + try: + time_from = conds['time_from'] + except KeyError: + time_from = self.getEpochFromJd(self.startJd) + else: + del conds['time_from'] + try: + time_to = conds['time_to'] + except KeyError: + time_to = self.getEpochFromJd(self.endJd) + else: + del conds['time_to'] + idList = set() + for epoch0, epoch1, eid, odt in self.occur.search(time_from, time_to): + idList.add(eid) + idList = sorted(idList) + else: + idList = self.idList + ##### + data = [] + for eid in idList: + try: + event = self[eid] + except: + continue + for key, value in conds.items(): + func = self.simpleFilters[key] + if not func(event, value): + break + else: + data.append({ + 'id': eid, + 'icon': event.icon, + 'summary': event.summary, + 'description': event.getShownDescription(), + }) + ##### + return data + + + +@classes.group.register +class TaskList(EventGroup): + name = 'taskList' + desc = _('Task List') + acceptsEventTypes = ( + 'task', + 'allDayTask', + ) + #actions = EventGroup.actions + [] + sortBys = EventGroup.sortBys + ( + ('start', _('Start'), True), + ('end', _('End'), True), + ) + sortByDefault = 'start' + def getSortByValue(self, event, attr): + if event.name in self.acceptsEventTypes: + if attr=='start': + return event.getStartEpoch() + elif attr=='end': + return event.getEndEpoch() + return EventGroup.getSortByValue(self, event, attr) + def __init__(self, _id=None): + EventGroup.__init__(self, _id) + self.defaultDuration = (0, 1) ## (value, unit) + ## if defaultDuration[0] is set to zero, the checkbox for task's end, will be unchecked for new tasks + def copyFrom(self, other): + EventGroup.copyFrom(self, other) + if other.name == self.name: + self.defaultDuration = other.defaultDuration[:] + def getData(self): + data = EventGroup.getData(self) + data['defaultDuration'] = durationEncode(*self.defaultDuration) + return data + def setData(self, data): + EventGroup.setData(self, data) + if 'defaultDuration' in data: + self.defaultDuration = durationDecode(data['defaultDuration']) + +@classes.group.register +class NoteBook(EventGroup): + name = 'noteBook' + desc = _('Note Book') + acceptsEventTypes = ( + 'dailyNote', + ) + canConvertTo = ( + 'yearly', + 'taskList', + ) + #actions = EventGroup.actions + [] + sortBys = EventGroup.sortBys + ( + ('date', _('Date'), True), + ) + sortByDefault = 'date' + def getSortByValue(self, event, attr): + if event.name in self.acceptsEventTypes: + if attr=='date': + return event.getJd() + return EventGroup.getSortByValue(self, event, attr) + + +@classes.group.register +class YearlyGroup(EventGroup): + name = 'yearly' + desc = _('Yearly Events Group') + acceptsEventTypes = ( + 'yearly', + ) + canConvertTo = ( + 'noteBook', + ) + params = EventGroup.params + ( + 'showDate', + ) + def __init__(self, _id=None): + EventGroup.__init__(self, _id) + self.showDate = True + +@classes.group.register +class UniversityTerm(EventGroup): + name = 'universityTerm' + desc = _('University Term') + acceptsEventTypes = ( + 'universityClass', + 'universityExam', + ) + actions = EventGroup.actions + [ + ('View Weekly Schedule', 'viewWeeklySchedule'), + ] + sortBys = EventGroup.sortBys + ( + ('course', _('Course'), True), + ('time', _('Time'), True), + ) + sortByDefault = 'time' + params = EventGroup.params + ( + 'courses', + ) + paramsOrder = EventGroup.paramsOrder + ( + 'classTimeBounds', + 'classesEndDate', + 'courses', + ) + noCourseError = _('Edit University Term and define some Courses before you add a Class/Exam') + def getSortByValue(self, event, attr): + if event.name in self.acceptsEventTypes: + if attr=='course': + return event.courseId + elif attr=='time': + if event.name == 'universityClass': + wd = event['weekDay'].weekDayList[0] + return (wd - core.firstWeekDay)%7, event['dayTimeRange'].getHourRange() + elif event.name == 'universityExam': + return event['date'].getJd(), event['dayTimeRange'].getHourRange() + return EventGroup.getSortByValue(self, event, attr) + def __init__(self, _id=None): + EventGroup.__init__(self, _id) + self.classesEndDate = getSysDate(self.mode)## FIXME + self.setCourses([]) ## list of (courseId, courseName, courseUnits) + self.classTimeBounds = [ + (8, 0), + (10, 0), + (12, 0), + (14, 0), + (16, 0), + (18, 0), + ] ## FIXME + def getClassBoundsFormatted(self): + count = len(self.classTimeBounds) + if count < 2: + return + titles = [] + tmfactors = [] + firstTm = timeToFloatHour(*self.classTimeBounds[0]) + lastTm = timeToFloatHour(*self.classTimeBounds[-1]) + deltaTm = lastTm - firstTm + for i in range(count-1): + tm0, tm1 = self.classTimeBounds[i:i+2] + titles.append( + textNumEncode(simpleTimeEncode(tm0)) + ' ' + _('to') + ' ' + textNumEncode(simpleTimeEncode(tm1)) + ) + for tm1 in self.classTimeBounds: + tmfactors.append((timeToFloatHour(*tm1)-firstTm)/deltaTm) + return (titles, tmfactors) + def getWeeklyScheduleData(self, currentWeekOnly=False): + boundsCount = len(self.classTimeBounds) + boundsHour = [h + m/60.0 for h,m in self.classTimeBounds] + data = [ + [ + [] for i in range(boundsCount-1) + ] for weekDay in range(7) + ] + ## data[weekDay][intervalIndex] = {'name': 'Course Name', 'weekNumMode': 'odd'} + ### + if currentWeekOnly: + currentJd = core.getCurrentJd() + if ( + getAbsWeekNumberFromJd(currentJd) - \ + getAbsWeekNumberFromJd(self.startJd) + ) % 2 == 1: + currentWeekNumMode = 'odd' + else: + currentWeekNumMode = 'even' + #print('currentWeekNumMode = %r'%currentWeekNumMode) + else: + currentWeekNumMode = '' + ### + for event in self: + if event.name != 'universityClass': + continue + weekNumMode = event['weekNumMode'].getData() + if currentWeekNumMode: + if weekNumMode not in ('any', currentWeekNumMode): + continue + weekNumMode = '' + else: + if weekNumMode=='any': + weekNumMode = '' + ### + weekDay = event['weekDay'].weekDayList[0] + h0, h1 = event['dayTimeRange'].getHourRange() + startIndex = findNearestIndex(boundsHour, h0) + endIndex = findNearestIndex(boundsHour, h1) + ### + classData = { + 'name': self.getCourseNameById(event.courseId), + 'weekNumMode': weekNumMode, + } + for i in range(startIndex, endIndex): + data[weekDay][i].append(classData) + + return data + def setCourses(self, courses): + self.courses = courses + #self.lastCourseId = max([1]+[course[0] for course in self.courses]) + #print('setCourses: lastCourseId=%s'%self.lastCourseId) + #getCourseNamesDictById = lambda self: dict([c[:2] for c in self.courses]) + def getCourseNameById(self, courseId): + for course in self.courses: + if course[0] == courseId: + return course[1] + return _('Deleted Course') + def setDefaults(self): + calType = calTypes.names[self.mode] + ## FIXME + ## odd term or even term? + jd = core.getCurrentJd() + year, month, day = jd_to(jd, self.mode) + md = (month, day) + if calType=='jalali': + ## 0/07/01 to 0/11/01 + ## 0/11/15 to 1/03/20 + if (1, 1) <= md < (4, 1): + self.startJd = to_jd(year-1, 11, 15, self.mode) + self.classesEndDate = (year, 3, 20) + self.endJd = to_jd(year, 4, 10, self.mode) + elif (4, 1) <= md < (10, 1): + self.startJd = to_jd(year, 7, 1, self.mode) + self.classesEndDate = (year, 11, 1) + self.endJd = to_jd(year, 11, 1, self.mode) + else:## md >= (10, 1) + self.startJd = to_jd(year, 11, 15, self.mode) + self.classesEndDate = (year+1, 3, 1) + self.endJd = to_jd(year+1, 3, 20, self.mode) + #elif calType=='gregorian': + # pass + #def getNewCourseID(self): + # self.lastCourseId += 1 + # print('getNewCourseID: lastCourseId=%s'%self.lastCourseId) + # return self.lastCourseId + def copyFrom(self, other): + EventGroup.copyFrom(self, other) + if other.name == self.name: + self.classesEndDate = other.classesEndDate[:] + self.classTimeBounds = other.classTimeBounds[:] + def getData(self): + data = EventGroup.getData(self) + data.update({ + 'classTimeBounds': [hmEncode(hm) for hm in self.classTimeBounds], + 'classesEndDate': dateEncode(self.classesEndDate), + }) + return data + def setData(self, data): + EventGroup.setData(self, data) + #self.setCourses(data['courses']) + if 'classesEndDate' in data: + self.classesEndDate = dateDecode(data['classesEndDate']) + if 'classTimeBounds' in data: + self.classTimeBounds = sorted([hmDecode(hm) for hm in data['classTimeBounds']]) + def afterModify(self): + EventGroup.afterModify(self) + for event in self: + try: + event.updateSummary() + except AttributeError: + pass + else: + event.save() + +@classes.group.register +class LifeTimeGroup(EventGroup): + name = 'lifeTime' + desc = _('Life Time Events Group') + acceptsEventTypes = ( + 'lifeTime', + ) + sortBys = EventGroup.sortBys + ( + ('start', _('Start'), True), + ) + params = EventGroup.params + ( + 'showSeperatedYmdInputs', + ) + def getSortByValue(self, event, attr): + if event.name in self.acceptsEventTypes: + if attr=='start': + return event.getStartJd() + elif attr=='end': + return event.getEndJd() + return EventGroup.getSortByValue(self, event, attr) + def __init__(self, _id=None): + self.showSeperatedYmdInputs = False + EventGroup.__init__(self, _id) + def setDefaults(self): + ## only in time line ## or in init? FIXME + self.showInDCal = False + self.showInWCal = False + self.showInMCal = False + self.showInStatusIcon = False + + +@classes.group.register +class LargeScaleGroup(EventGroup): + name = 'largeScale' + desc = _('Large Scale Events Group') + acceptsEventTypes = ( + 'largeScale', + ) + sortBys = EventGroup.sortBys + ( + ('start', _('Start'), True), + ('end', _('End'), True), + ) + sortByDefault = 'start' + def getSortByValue(self, event, attr): + if event.name in self.acceptsEventTypes: + if attr=='start': + return event.start * event.scale + elif attr=='end': + return event.getEnd() * event.scale + return EventGroup.getSortByValue(self, event, attr) + def __init__(self, _id=None): + self.scale = 1 ## 1, 1000, 1000**2, 1000**3 + EventGroup.__init__(self, _id) + def setDefaults(self): + self.startJd = 0 + self.endJd = self.startJd + self.scale * 9999 + ## only in time line ## or in init? FIXME + self.showInDCal = False + self.showInWCal = False + self.showInMCal = False + self.showInStatusIcon = False + def copyFrom(self, other): + EventGroup.copyFrom(self, other) + if other.name == self.name: + self.scale = other.scale + def getData(self): + data = EventGroup.getData(self) + data['scale'] = self.scale + return data + def setData(self, data): + EventGroup.setData(self, data) + try: + self.scale = data['scale'] + except KeyError: + pass + getStartValue = lambda self: jd_to(self.startJd, self.mode)[0]//self.scale + getEndValue = lambda self: jd_to(self.endJd, self.mode)[0]//self.scale + def setStartValue(self, start): + self.startJd = int(to_jd(start*self.scale, 1, 1, self.mode)) + def setEndValue(self, end): + self.endJd = int(to_jd(end*self.scale, 1, 1, self.mode)) + + +########################################################################### +########################################################################### + +class VcsEpochBaseEvent(Event): + readOnly = True + params = Event.params + ( + 'epoch', + ) + @classmethod + def load(cls):## FIXME + pass + __bool__ = lambda self: True + def save(self): + pass + def afterModify(self): + pass + getInfo = lambda self: self.getText()## FIXME + def calcOccurrence(self, startJd, endJd): + epoch = self.epoch + if epoch is not None: + if self.getEpochFromJd(startJd) <= epoch < self.getEpochFromJd(endJd): + if not self.parent.showSeconds: + print('-------- showSeconds = False') + epoch -= (epoch % 60) + return TimeListOccurrence(epoch) + return TimeListOccurrence() + +#@classes.event.register ## FIXME +class VcsCommitEvent(VcsEpochBaseEvent): + name = 'vcs' + desc = _('VCS Commit') + params = VcsEpochBaseEvent.params + ( + 'author', + 'shortHash', + ) + def __init__(self, parent, _id): + Event.__init__(self, parent=parent) + self.id = _id ## commit full hash + ### + self.epoch = None + self.author = '' + self.shortHash = '' + __repr__ = lambda self: '%r.getEvent(%r)'%(self.parent, self.id) + + + +class VcsTagEvent(VcsEpochBaseEvent): + name = 'vcsTag' + desc = _('VCS Tag') + params = VcsEpochBaseEvent.params + ( + ) + def __init__(self, parent, _id): + Event.__init__(self, parent=parent) + self.id = _id ## tag name + self.epoch = None + self.author = '' + + + +class VcsBaseEventGroup(EventGroup): + acceptsEventTypes = () + myParams = ( + 'vcsType', + 'vcsDir', + ) + def __init__(self, _id=None): + self.vcsType = 'git' + self.vcsDir = '' + #self.branch = 'master' + EventGroup.__init__(self, _id) + __str__ = lambda self: '%s(_id=%s, title=%s, vcsType=%s, vcsDir=%s)'%( + self.__class__.__name__, + self.id, + self.title, + self.vcsType, + self.vcsDir, + ) + def setDefaults(self): + self.eventTextSep = '\n' + self.showInTimeLine = False + getRulesHash = lambda self: hash(str(( + self.name, + self.vcsType, + self.vcsDir, + )))## FIXME + def __getitem__(self, key): + if key in classes.rule.names: + return EventGroup.__getitem__(self, key) + else:## len(commit_id)==40 for git + return self.getEvent(key) + def getVcsModule(self): + name = toStr(self.vcsType) + #if not isinstance(name, str): + # raise TypeError('getVcsModule(%r): bad type %s'%(name, type(name))) + try: + mod = __import__('scal3.vcs_modules', fromlist=[name]) + except ImportError: + myRaise() + return + return getattr(mod, name) + def updateVcsModuleObj(self): + mod = self.getVcsModule() + mod.clearObj(self) + if self.enable and self.vcsDir: + try: + mod.prepareObj(self) + except: + myRaise() + def afterModify(self): + self.updateVcsModuleObj() + EventGroup.afterModify(self) + def setData(self, data): + EventGroup.setData(self, data) + self.updateVcsModuleObj() + + + +class VcsEpochBaseEventGroup(VcsBaseEventGroup): + myParams = VcsBaseEventGroup.myParams + ( + 'showSeconds', + ) + canConvertTo = VcsBaseEventGroup.canConvertTo + ( + 'taskList', + ) + def __init__(self, _id=None): + self.showSeconds = True + self.vcsIds = [] + VcsBaseEventGroup.__init__(self, _id) + def clear(self): + EventGroup.clear(self) + self.vcsIds = [] + def addOccur(self, t0, t1, eid): + EventGroup.addOccur(self, t0, t1, eid) + self.vcsIds.append(eid) + getRulesHash = lambda self: hash(str(( + self.name, + self.vcsType, + self.vcsDir, + self.showSeconds, + ))) + def deepConvertTo(self, newGroupType): + newGroup = self.copyAs(newGroupType) + if newGroupType == 'taskList': + newEventType = 'task' + newGroup.enable = False ## to prevent per-event node update + for vcsId in self.vcsIds: + event = self.getEvent(vcsId) + newEvent = newGroup.createEvent(newEventType) + newEvent.changeMode(event.mode)## FIXME needed? + newEvent.copyFrom(event, True) + newEvent.setStartEpoch(event.epoch) + newEvent.setEnd('duration', 0, 1) + newEvent.save() + newGroup.append(newEvent) + newGroup.enable = self.enable + return newGroup + + + +@classes.group.register +class VcsCommitEventGroup(VcsEpochBaseEventGroup): + name = 'vcs' + desc = _('VCS Repository (Commits)') + myParams = VcsEpochBaseEventGroup.myParams + ( + 'showAuthor', + 'showShortHash', + 'showStat', + ) + params = EventGroup.params + myParams + paramsOrder = EventGroup.paramsOrder + myParams + def __init__(self, _id=None): + VcsEpochBaseEventGroup.__init__(self, _id) + self.showAuthor = True + self.showShortHash = True + self.showStat = True + def updateOccurrence(self): + stm0 = now() + self.clear() + if not self.vcsDir: + return + mod = self.getVcsModule() + try: + commitsData = mod.getCommitList(self, startJd=self.startJd, endJd=self.endJd) + except: + printError('Error while fetching commit list of %s repository in %s'%( + self.vcsType, + self.vcsDir, + )) + myRaise() + return + for epoch, commit_id in commitsData: + if not self.showSeconds: + epoch -= (epoch % 60) + self.addOccur(epoch, epoch, commit_id) + ### + self.updateOccurrenceLog(stm0) + def updateEventDesc(self, event): + mod = self.getVcsModule() + lines = [] + if event.description: + lines.append(event.description) + if self.showStat: + statLine = mod.getCommitShortStatLine(self, event.id) + if statLine: + lines.append(statLine)## translation FIXME + if self.showAuthor and event.author: + lines.append(_('Author')+': '+event.author) + if self.showShortHash and event.shortHash: + lines.append(_('Hash')+': '+event.shortHash) + event.description = '\n'.join(lines) + def getEvent(self, commit_id):## cache commit data FIXME + mod = self.getVcsModule() + data = mod.getCommitInfo(self, commit_id) + if not data: + raise ValueError('No commit with id=%r'%commit_id) + data['summary'] = self.title +': ' + data['summary'] + data['icon'] = self.icon + event = VcsCommitEvent(self, commit_id) + event.setData(data) + self.updateEventDesc(event) + return event + + + + + +@classes.group.register +class VcsTagEventGroup(VcsEpochBaseEventGroup): + name = 'vcsTag' + desc = _('VCS Repository (Tags)') + myParams = VcsEpochBaseEventGroup.myParams + ( + 'showStat', + ) + params = EventGroup.params + myParams + paramsOrder = EventGroup.paramsOrder + myParams + def __init__(self, _id=None): + VcsEpochBaseEventGroup.__init__(self, _id) + self.showStat = True + def updateOccurrence(self): + stm0 = now() + self.clear() + if not self.vcsDir: + return + mod = self.getVcsModule() + try: + tagsData = mod.getTagList(self, self.startJd, self.endJd)## TOO SLOW + except: + printError('Error while fetching tag list of %s repository in %s'%( + self.vcsType, + self.vcsDir, + )) + myRaise() + return + #self.updateOccurrenceLog(stm0) + for epoch, tag in tagsData: + if not self.showSeconds: + epoch -= (epoch % 60) + self.addOccur(epoch, epoch, tag) + ### + self.updateOccurrenceLog(stm0) + def updateEventDesc(self, event): + mod = self.getVcsModule() + tag = event.id + lines = [] + if self.showStat: + tagIndex = self.vcsIds.index(tag) + if tagIndex > 0: + prevTag = self.vcsIds[tagIndex-1] + else: + prevTag = None + statLine = mod.getTagShortStatLine(self, prevTag, tag) + if statLine: + lines.append(statLine)## translation FIXME + event.description = '\n'.join(lines) + def getEvent(self, tag):## cache commit data FIXME + if not tag in self.vcsIds: + raise ValueError('No tag %r'%tag) + data = {} + data['summary'] = self.title + ' ' + tag ## FIXME + data['icon'] = self.icon + event = VcsTagEvent(self, tag) + event.setData(data) + self.updateEventDesc(event) + return event + + + +class VcsDailyStatEvent(Event): + name = 'vcsDailyStat' + desc = _('VCS Daily Stat') + readOnly = True + isAllDay = True + params = Event.params + ( + 'jd', + ) + __bool__ = lambda self: True + def __init__(self, parent, jd): + Event.__init__(self, parent=parent) + self.id = jd ## ID is Julian Day + def save(self): + pass + @classmethod + def load(cls):## FIXME + pass + def afterModify(self): + pass + getInfo = lambda self: self.getText()## FIXME + def calcOccurrence(self, startJd, endJd): + jd = self.jd + if jd is not None: + if startJd <= jd < endJd: + JdSetOccurrence(jd) + return JdSetOccurrence() + + +@classes.group.register +class VcsDailyStatEventGroup(VcsBaseEventGroup): + name = 'vcsDailyStat' + desc = _('VCS Repository (Daily Stat)') + myParams = VcsBaseEventGroup.myParams + ( + ) + params = EventGroup.params + myParams + paramsOrder = EventGroup.paramsOrder + myParams + def __init__(self, _id=None): + VcsBaseEventGroup.__init__(self, _id) + self.statByJd = {} + def clear(self): + VcsBaseEventGroup.clear(self) + self.statByJd = {} ## a dict of (commintsCount, lastCommitId)s + def updateOccurrence(self): + stm0 = now() + self.clear() + if not self.vcsDir: + return + mod = self.getVcsModule() + #### + try: + utc = natz.timezone('UTC') + self.vcsMinJd = getJdFromEpoch(mod.getFirstCommitEpoch(self), tz=utc) + self.vcsMaxJd = getJdFromEpoch(mod.getLastCommitEpoch(self), tz=utc) + 1 + except: + myRaise() + return + ### + startJd = max(self.startJd, self.vcsMinJd) + endJd = min(self.endJd, self.vcsMaxJd) + ### + lastCommitId = mod.getLastCommitIdUntilJd(self, startJd) + for jd in range(startJd, endJd): + commits = mod.getCommitList(self, startJd=jd, endJd=jd+1) + if not commits: + continue + lastCommitIdPrev, lastCommitId = lastCommitId, commits[0][1] + if not lastCommitIdPrev: + continue + stat = mod.getShortStat(self, lastCommitIdPrev, lastCommitId) + self.statByJd[jd] = (len(commits), stat) + self.addOccur( + getEpochFromJd(jd), + getEpochFromJd(jd+1), + jd, + ) + ### + self.updateOccurrenceLog(stm0) + def getEvent(self, jd):## cache commit data FIXME + from scal3.vcs_modules import encodeShortStat + try: + commitsCount, stat = self.statByJd[jd] + except KeyError: + raise ValueError('No commit for jd %s'%jd) + mod = self.getVcsModule() + event = VcsDailyStatEvent(self, jd) + ### + event.icon = self.icon + ## + statLine = encodeShortStat(*stat) + event.summary = self.title + ': ' + _('%d commits')%commitsCount ## FIXME + event.summary += ', ' + statLine + #event.description = statLine + ## FIXME + ### + return event + + +########################################################################### +########################################################################### + +class JsonObjectsHolder(JsonEventObj): + ## keeps all objects in memory + ## Only use to keep groups and accounts, but not events + skipLoadNoFile = True + def __init__(self, _id=None): + self.clear() + def clear(self): + self.byId = {} + self.idList = [] + def __iter__(self): + for _id in self.idList: + yield self.byId[_id] + __len__ = lambda self: len(self.idList) + __bool__ = lambda self: bool(self.idList) + index = lambda self, _id: self.idList.index(_id) ## or get object instead of obj_id? FIXME + __getitem__ = lambda self, _id: self.byId.__getitem__(_id) + byIndex = lambda self, index: self.byId[self.idList[index]] + #byIndex = lambda + __setitem__ = lambda self, _id, group: self.byId.__setitem__(_id, group) + def insert(self, index, obj): + assert not obj.id in self.idList + self.byId[obj.id] = obj + self.idList.insert(index, obj.id) + def append(self, obj): + assert not obj.id in self.idList + self.byId[obj.id] = obj + self.idList.append(obj.id) + def delete(self, obj): + assert obj.id in self.idList + try: + os.remove(obj.file) + except: + myRaise() + try: + del self.byId[obj.id] + except: + myRaise() + try: + self.idList.remove(obj.id) + except: + myRaise() + pop = lambda self, index: self.byId.pop(self.idList.pop(index)) + moveUp = lambda self, index: self.idList.insert(index-1, self.idList.pop(index)) + moveDown = lambda self, index: self.idList.insert(index+1, self.idList.pop(index)) + def setData(self, data): + self.clear() + for sid in data: + assert isinstance(sid, int) and sid != 0 + _id = sid + _id = abs(sid) + cls = getattr(classes, self.childName).main + obj = cls.load(_id) + obj.parent = self + obj.enable = (sid > 0) + self.idList.append(_id) + self.byId[obj.id] = obj + getData = lambda self: [_id if self.byId[_id] else -_id for _id in self.idList] + + + +class EventGroupsHolder(JsonObjectsHolder): + file = join(confDir, 'event', 'group_list.json') + childName = 'group' + def __init__(self, _id=None): + JsonObjectsHolder.__init__(self) + self.id = None + self.parent = None + def delete(self, obj): + assert not obj.idList ## FIXME + obj.parent = None + JsonObjectsHolder.delete(self, obj) + def setData(self, data): + self.clear() + if data: + JsonObjectsHolder.setData(self, data) + for obj in self: + if obj.enable: + obj.updateOccurrence() + else: + for name in ( + 'noteBook', + 'taskList', + 'group', + ): + cls = classes.group.byName[name] + obj = cls()## FIXME + obj.setRandomColor() + obj.setTitle(cls.desc) + obj.save() + self.append(obj) + def getEnableIds(self): + ids = [] + for group in self: + if group.enable: + ids.append(group.id) + return ids + def moveToTrash(self, group, trash, addToFirst=True): + if core.eventTrashLastTop: + trash.idList = group.idList + trash.idList + else: + trash.idList += group.idList + group.idList = [] + self.delete(group) + self.save() + trash.save() + def convertGroupTo(self, group, newGroupType): + groupIndex = self.index(group.id) + newGroup = group.deepConvertTo(newGroupType) + newGroup.setId(group.id) + newGroup.afterModify() + newGroup.save() + self.byId[newGroup.id] = newGroup + return newGroup + ## and then never use old `group` object + def exportData(self, gidList): + data = OrderedDict([ + ('info', OrderedDict([ + ('appName', core.APP_NAME), + ('version', core.VERSION), + ])), + ('groups', []), + ]) + for gid in gidList: + data['groups'].append(self.byId[gid].exportData()) + return data + def importData(self, data): + newGroups = [] + for gdata in data['groups']: + group = classes.group.byName[gdata['type']]() + group.importData(gdata) + self.append(group) + newGroups.append(group) + self.save()## FIXME + return newGroups + importJsonFile = lambda self, fpath: self.importData(jsonToData(open(fpath, 'rb').read())) + def exportToIcs(self, fpath, gidList): + fp = open(fpath, 'w') + fp.write(ics.icsHeader) + for gid in gidList: + self[gid].exportToIcsFp(fp) + fp.write('END:VCALENDAR\n') + fp.close() + def checkForOrphans(self): + newGroup = EventGroup() + newGroup.setTitle(_('Orphan Events')) + newGroup.setColor((255, 255, 0)) + newGroup.enable = False + for gid_fname in listdir(groupsDir): + try: + gid = int(splitext(gid_fname)[0]) + except ValueError: + continue + if not gid in self.idList: + try: + os.remove(join(groupsDir, gid_fname)) + except: + myRaise() + ###### + myEventIds = [] + for group in self: + myEventIds += group.idList + myEventIds = set(myEventIds) + ## + for fname in listdir(eventsDir): + fname_nox, ext = splitext(fname) + if ext != '.json': + continue + try: + eid = int(fname_nox) + except ValueError: + continue + if eid in myEventIds: + continue + newGroup.idList.append(eid) + if newGroup.idList: + newGroup.save() + self.append(newGroup) + self.save() + return newGroup + else: + return + + +class EventAccountsHolder(JsonObjectsHolder): + file = join(confDir, 'event', 'account_list.json') + def loadClass(self, name): + try: + return classes.account.byName[name] + except KeyError:## FIXME + try: + __import__('scal3.account.%s'%name) + except ImportError: + myRaiseTback() + else: + try: + return classes.account.byName[name] + except KeyError:## FIXME + pass + log.error('error while loading account: no account type "%s"'%name) + def loadData(self, _id): + objFile = join(accountsDir, '%s.json'%_id) + if not isfile(objFile): + log.error('error while loading account file %r: file not found'%objFile)## FIXME + ## FileNotFoundError + data = jsonToData(open(objFile).read()) + updateBasicDataFromBson(data, objFile, 'account') + #if data['id'] != _id: + # log.error('attribute "id" in json file does not match the file name: %s'%objFile) + #del data['id'] + return data + ''' + def load(self): + #print('------------ EventAccountsHolder.load') + self.clear() + if isfile(self.file): + for _id in jsonToData(open(self.file).read()): + data = self.loadData(_id) + if not data: + continue + name = data['type'] + if data['enable']: + cls = self.loadClass(name) + if cls is None: + continue + try: + obj = cls(_id) + except: + myRaise() + continue + #data['id'] = _id ## FIXME + obj.setData(data) + else: + obj = DummyAccount( + name, + _id, + data['title'], + ) + self.append(obj) + ''' + def getLoadedObj(self, obj): + _id = obj.id + data = self.loadData(_id) + name = data['type'] + cls = self.loadClass(name) + if cls is None: + return + obj = cls(_id) + data = self.loadData(_id) + obj.setData(data) + return obj + def replaceDummyObj(self, obj): + _id = obj.id + index = self.idList.index(_id) + obj = self.getLoadedObj(obj) + self.byId[_id] = obj + return obj + + + +class EventTrash(EventContainer): + name = 'trash' + desc = _('Trash') + file = join(confDir, 'event', 'trash.json')## FIXME + id = -1 ## FIXME + def __init__(self): + EventContainer.__init__(self, title=_('Trash')) + self.icon = join(pixDir, 'trash.png') + self.enable = False + def delete(self, eid): + from shutil import rmtree + ## different from EventContainer.remove + ## remove() only removes event from this group, but event file and data still available + ## and probably will be added to another event container + ## but after delete(), there is no event file, and not event data + if not isinstance(eid, int): + raise TypeError("delete takes event ID that is integer") + assert eid in self.idList + try: + rmtree(join(eventsDir, str(eid))) + except: + myRaise() + else: + self.idList.remove(eid) + def empty(self): + from shutil import rmtree + idList2 = self.idList[:] + for eid in self.idList: + try: + rmtree(join(eventsDir, str(eid))) + except: + myRaise() + idList2.remove(eid) + self.idList = idList2 + self.save() + + +class DummyAccount: + loaded = False + enable = False + params = () + paramsOrder = () + accountsDesc = { + 'google': _('Google'), + } + def __init__(self, _type, _id, title): + self.name = _type + self.desc = self.accountsDesc[_type] + self.id = _id + self.title = title + def save(): + pass + def load(): + pass + def getLoadedObj(self): + pass + +## Should not be registered, or instantiate directly +@classes.account.setMain +class Account(BsonHistObj): + loaded = True + name = '' + desc = '' + basicParams = (## FIXME + 'enable', + ) + params = ( + 'enable', + 'title', + 'remoteGroups', + ) + paramsOrder = ( + 'enable', + 'type', + 'title', + 'remoteGroups', + ) + @classmethod + def getFile(cls, _id): + return join(accountsDir, '%d.json'%_id) + @classmethod + def getSubclass(cls, _type): + return classes.account.byName[_type] + def __init__(self, _id=None): + if _id is None: + self.id = None + else: + self.setId(_id) + self.enable = True + self.title = 'Account' + self.remoteGroups = []## a list of dictionarise {'id':..., 'title':...} + self.status = None## {'action': 'pull', 'done': 10, 'total': 20} + ## action values: 'fetchGroups', 'pull', 'push' + def save(self): + if self.id is None: + self.setId() + BsonHistObj.save(self) + def setId(self, _id=None): + if _id is None or _id<0: + _id = lastIds.account + 1 ## FIXME + lastIds.account = _id + elif _id > lastIds.account: + lastIds.account = _id + self.id = _id + self.file = self.getFile(self.id) + def stop(self): + self.status = None + def fetchGroups(self): + raise NotImplementedError + def fetchAllEventsInGroup(self, remoteGroupId): + raise NotImplementedError + def sync(self, group, remoteGroupId): + raise NotImplementedError + def getData(self): + data = BsonHistObj.getData(self) + data['type'] = self.name + return data + + +######################################################################## + +def getDayOccurrenceData(curJd, groups): + data = [] + for groupIndex, group in enumerate(groups): + if not group.enable: + continue + if not group.showInCal(): + continue + #print('\nupdateData: checking event', event.summary) + gid = group.id + color = group.color + for epoch0, epoch1, eid, odt in group.occur.search(getEpochFromJd(curJd), getEpochFromJd(curJd+1)): + event = group[eid] + ### + text = event.getTextParts() + ### + timeStr = '' + if epoch1-epoch0 < dayLen: + jd0, h0, m0, s0 = getJhmsFromEpoch(epoch0) + if jd0 < curJd: + h0, m0, s0 = 0, 0, 0 + if epoch1 - epoch0 < 1: + timeStr = timeEncode((h0, m0, s0), True) + else: + jd1, h1, m1, s1 = getJhmsFromEpoch(epoch1) + if jd1 > curJd: + h1, m1, s1 = 24, 0, 0 + timeStr = hmsRangeToStr(h0, m0, s0, h1, m1, s1) + ### + try: + eventIndex = group.index(eid) + except ValueError: + eventIndex = event.modified ## FIXME + data.append(( + (epoch0, epoch1, groupIndex, eventIndex),## FIXME for sorting + { + 'time': timeStr, + 'time_epoch': (epoch0, epoch1), + 'is_allday': epoch0 % dayLen == 0 and epoch1 % dayLen == 0, + 'text': text, + 'icon': event.icon, + 'color': color, + 'ids': (gid, eid), + 'show': (group.showInDCal, group.showInWCal, group.showInMCal), + 'showInStatusIcon': group.showInStatusIcon, + } + )) + data.sort() + return [item[1] for item in data] + + +def getWeekOccurrenceData(curAbsWeekNumber, groups): + startJd = core.getStartJdOfAbsWeekNumber(absWeekNumber) + endJd = startJd + 7 + data = [] + for group in groups: + if not group.enable: + continue + for event in group: + if not event: + continue + occur = event.calcOccurrence(startJd, endJd) + if not occur: + continue + text = event.getText() + icon = event.icon + ids = (group.id, event.id) + if isinstance(occur, JdSetOccurrence): + for jd in occur.getDaysJdList(): + wnum, weekDay = core.getWeekDateFromJd(jd) + if wnum==curAbsWeekNumber: + data.append({ + 'weekDay':weekDay, + 'time':'', + 'text':text, + 'icon':icon, + 'ids': ids, + }) + elif isinstance(occur, TimeRangeListOccurrence): + for startEpoch, endEpoch in occur.getTimeRangeList(): + jd1, h1, min1, s1 = getJhmsFromEpoch(startEpoch) + jd2, h2, min2, s2 = getJhmsFromEpoch(endEpoch) + wnum, weekDay = core.getWeekDateFromJd(jd1) + if wnum==curAbsWeekNumber: + if jd1==jd2: + data.append({ + 'weekDay':weekDay, + 'time':hmsRangeToStr(h1, min1, s1, h2, min2, s2), + 'text':text, + 'icon':icon, + 'ids': ids, + }) + else:## FIXME + data.append({ + 'weekDay':weekDay, + 'time':hmsRangeToStr(h1, min1, s1, 24, 0, 0), + 'text':text, + 'icon':icon, + 'ids': ids, + }) + for jd in range(jd1+1, jd2): + wnum, weekDay = core.getWeekDateFromJd(jd) + if wnum==curAbsWeekNumber: + data.append({ + 'weekDay':weekDay, + 'time':'', + 'text':text, + 'icon':icon, + 'ids': ids, + }) + else: + break + wnum, weekDay = core.getWeekDateFromJd(jd2) + if wnum==curAbsWeekNumber: + data.append({ + 'weekDay':weekDay, + 'time':hmsRangeToStr(0, 0, 0, h2, min2, s2), + 'text':text, + 'icon':icon, + 'ids': ids, + }) + elif isinstance(occur, TimeListOccurrence): + for epoch in occur.epochList: + jd, hour, minute, sec = getJhmsFromEpoch(epoch) + wnum, weekDay = core.getWeekDateFromJd(jd) + if wnum==curAbsWeekNumber: + data.append({ + 'weekDay':weekDay, + 'time':timeEncode((hour, minute, sec), True), + 'text':text, + 'icon':icon, + 'ids': ids, + }) + else: + raise TypeError + return data + + +def getMonthOccurrenceData(curYear, curMonth, groups): + startJd, endJd = core.getJdRangeForMonth(curYear, curMonth, calTypes.primary) + data = [] + for group in groups: + if not group.enable: + continue + for event in group: + if not event: + continue + occur = event.calcOccurrence(startJd, endJd) + if not occur: + continue + text = event.getText() + icon = event.icon + ids = (group.id, event.id) + if isinstance(occur, JdSetOccurrence): + for jd in occur.getDaysJdList(): + y, m, d = jd_to_primary(jd) + if y==curYear and m==curMonth: + data.append({ + 'day':d, + 'time':'', + 'text':text, + 'icon':icon, + 'ids': ids, + }) + elif isinstance(occur, TimeRangeListOccurrence): + for startEpoch, endEpoch in occur.getTimeRangeList(): + jd1, h1, min1, s1 = getJhmsFromEpoch(startEpoch) + jd2, h2, min2, s2 = getJhmsFromEpoch(endEpoch) + y, m, d = jd_to_primary(jd1) + if y==curYear and m==curMonth: + if jd1==jd2: + data.append({ + 'day':d, + 'time':hmsRangeToStr(h1, min1, s1, h2, min2, s2), + 'text':text, + 'icon':icon, + 'ids': ids, + }) + else:## FIXME + data.append({ + 'day':d, + 'time':hmsRangeToStr(h1, min1, s1, 24, 0, 0), + 'text':text, + 'icon':icon, + 'ids': ids, + }) + for jd in range(jd1+1, jd2): + y, m, d = jd_to_primary(jd) + if y==curYear and m==curMonth: + data.append({ + 'day':d, + 'time':'', + 'text':text, + 'icon':icon, + 'ids': ids, + }) + else: + break + y, m, d = jd_to_primary(jd2) + if y==curYear and m==curMonth: + data.append({ + 'day':d, + 'time':hmsRangeToStr(0, 0, 0, h2, min2, s2), + 'text':text, + 'icon':icon, + 'ids': ids, + }) + elif isinstance(occur, TimeListOccurrence): + for epoch in occur.epochList: + jd, hour, minute, sec = getJhmsFromEpoch(epoch) + y, m, d = jd_to_primary(jd1) + if y==curYear and m==curMonth: + data.append({ + 'day':d, + 'time':timeEncode((hour, minute, sec), True), + 'text':text, + 'icon':icon, + 'ids': ids, + }) + else: + raise TypeError + return data + + + +################################################################################# + +def loadEventTrash(groups=[]): + trash = EventTrash.load() + ### + groupedIds = trash.idList[:] + for group in groups: + groupedIds += group.idList + nonGroupedIds = [] + for eid in listdir(eventsDir): + try: + eid = int(eid) + except: + continue + if not eid in groupedIds: + nonGroupedIds.append(eid) + if nonGroupedIds: + trash.idList += nonGroupedIds + trash.save() + ### + return trash + + + + diff --git a/scal3/event_search_tree.py b/scal3/event_search_tree.py new file mode 100644 index 000000000..4a83a4796 --- /dev/null +++ b/scal3/event_search_tree.py @@ -0,0 +1,325 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# Also avalable in /usr/share/common-licenses/LGPL on Debian systems +# or /usr/share/licenses/common/LGPL/license.txt on ArchLinux + +import sys +from math import log + +from scal3.utils import myRaise +from scal3.time_utils import * +from scal3.bin_heap import MaxHeap + + +epsTm = 0.01 ## seconds ## configure somewhere? FIXME + +getCount = lambda x: x.count if x else 0 +isRed = lambda x: x.red if x else False + + +class Node: + def __init__(self, mt, red=True): + self.mt = mt + self.red = red + self.min_t = mt + self.max_t = mt + self.events = MaxHeap() + self.left = None + self.right = None + self.count = 0 + def add(self, t0, t1, dt, eid): + self.events.push(dt, eid) + if t0 < self.min_t: + self.min_t = t0 + if t1 > self.max_t: + self.max_t = t1 + def updateMinMax(self): + self.updateMinMaxChild(self.left) + self.updateMinMaxChild(self.right) + def updateMinMaxChild(self, child): + if child: + if child.min_t < self.min_t: + self.min_t = child.min_t + if child.max_t > self.max_t: + self.max_t = child.max_t + #def updateCount(self): + # self.count = len(self.events) + getCount(self.left) + getCount(self.right) + + +def rotateLeft(h): + #if not isRed(h.right): + # raise RuntimeError('rotateLeft: h.right is not red') + x = h.right + h.right = x.left + x.left = h + x.red = h.red + h.red = True + return x + +def rotateRight(h): + #if not isRed(h.left): + # raise RuntimeError('rotateRight: h.left is not red') + x = h.left + h.left = x.right + x.right = h + x.red = h.red + h.red = True + return x + +def flipColors(h): + #if isRed(h): + # raise RuntimeError('flipColors: h is red') + #if not isRed(h.left): + # raise RuntimeError('flipColors: h.left is not red') + #if not isRed(h.right): + # raise RuntimeError('flipColors: h.right is not red') + h.red = True + h.left.red = False + h.right.red = False + +class EventSearchTree: + def __init__(self): + self.clear() + def clear(self): + self.root = None + self.byId = {} + def doCountBalancing(self, node): + if node.left and not node.left.right and \ + node.left.count - getCount(node.right) > len(node.events): + #print('moving up from left') + ## `mup` is the node that is moving up and taking place of `node` + mup, node.left = node.left, None + #node.red, mup.red = mup.red, node.red + mup.right, node = node, mup + if node.right and not node.right.left and \ + node.right.count - getCount(node.left) > len(node.events): + #print('moving up from right') + ## `mup` is the node that is moving up and taking place of `node` + mup, node.right = node.right, None + #node.red, mup.red = mup.red, node.red + mup.left, node = node, mup + return node + def addStep(self, node, t0, t1, mt, dt, eid): + if t0 > t1: + return node + if not node: + node = Node(mt) + node.add(t0, t1, dt, eid) + return node + if mt < node.mt: + node.left = self.addStep(node.left , t0, t1, mt, dt, eid) + elif mt > node.mt: + node.right = self.addStep(node.right, t0, t1, mt, dt, eid) + else:## mt == node.mt + node.add(t0, t1, dt, eid) + ## node = self.doCountBalancing(node) + if isRed(node.right) and not isRed(node.left): + node = rotateLeft(node) + if isRed(node.left) and isRed(node.left.left): + node = rotateRight(node) + if isRed(node.left) and isRed(node.right): + flipColors(node) + ## node.updateCount() + node.updateMinMax() + return node + def add(self, t0, t1, eid, debug=False): + if debug: + from time import strftime, localtime + f = '%F, %T' + print('EventSearchTree.add: %s\t%s\t%s'%( + eid, + strftime(f, localtime(t0)), + strftime(f, localtime(t1)), + )) + ### + if t0 == t1: + t1 += epsTm ## needed? FIXME + mt = (t0 + t1)/2.0 + dt = (t1 - t0)/2.0 + ### + try: + self.root = self.addStep(self.root, t0, t1, mt, dt, eid) + except: + myRaise() + try: + hp = self.byId[eid] + except KeyError: + hp = self.byId[eid] = MaxHeap() + hp.push(mt, dt)## FIXME + def searchStep(self, node, t0, t1): + if not node: + raise StopIteration + t0 = max(t0, node.min_t) + t1 = min(t1, node.max_t) + if t0 >= t1: + raise StopIteration + ### + for item in self.searchStep(node.left, t0, t1): + yield item + ### + min_dt = abs((t0 + t1)/2.0 - node.mt) - (t1 - t0)/2.0 + if min_dt <= 0: + for dt, eid in node.events.getAll(): + yield node.mt, dt, eid + else: + for dt, eid in node.events.moreThan(min_dt): + yield node.mt, dt, eid + ### + for item in self.searchStep(node.right, t0, t1): + yield item + def search(self, t0, t1): + for mt, dt, eid in self.searchStep(self.root, t0, t1): + yield ( + max(t0, mt-dt), + min(t1, mt+dt), + eid, + 2*dt, + ) + def getLastBefore(self, t1): + res = self.getLastBeforeStep(self.root, t1) + if res: + mt, dt, eid = res + return mt-dt, mt+dt, eid + def getLastBeforeStep(self, node, t1): + if not node: + return + t1 = min(t1, node.max_t) + if t1 <= node.min_t: + return + ### + right_res = self.getLastBeforeStep(node.right, t1) + if right_res: + return right_res + ### + if node.mt < t1: + dt, eid = node.events.getMax() + return node.mt, dt, eid + ### + return self.getLastBeforeStep(node.left, t1) + def getMinNode(self, node): + if not node: + return + while node.left: + node = node.left + return node + def deleteMinNode(self, node): + if not node.left: + return node.right + node.left = self.deleteMinNode(node.left) + return node + def deleteStep(self, node, mt, dt, eid): + if not node: + return + if mt < node.mt: + node.left = self.deleteStep(node.left, mt, dt, eid) + elif mt > node.mt: + node.right = self.deleteStep(node.right, mt, dt, eid) + else:## mt == node.mt + node.events.delete(dt, eid) + if not node.events:## Cleaning tree, not essential + if not node.right: + return node.left + if not node.left: + return node.right + node2 = node + node = self.getMinNode(node2.right) + node.right = self.deleteMinNode(node2.right) + node.left = node2.left + ## node.updateCount() + return node + def delete(self, eid): + try: + hp = self.byId[eid] + except KeyError: + return 0 + else: + n = 0 + for mt, dt in hp.getAll(): + try: + self.root = self.deleteStep(self.root, mt, dt, eid) + except: + myRaise() + else: + n += 1 + del self.byId[eid] + return n + def getLastOfEvent(self, eid): + try: + hp = self.byId[eid] + except KeyError: + return + try: + mt, dt = hp.getMax() + except ValueError: + return + return mt-dt, mt+dt + def getFirstOfEvent(self, eid): + try: + hp = self.byId[eid] + except KeyError: + return + try: + mt, dt = hp.getMin()## slower than getMax, but twice faster than max() and + except ValueError: + return + return mt-dt, mt+dt + #def deleteMoreThanStep(self, node, t0): + # if not node: + # return + # if node.max_t <= t0: + # return node + # max_dt = node.mt - t0 + # if max_dt > 0: + # node.events.deleteLessThan(max_dt) ## FIXME + # self.deleteMoreThanStep(self, node.left, t0) + # self.deleteMoreThanStep(self, node.right, t0) + #def deleteMoreThan(self, t0): + # self.root = self.deleteMoreThanStep(self.root, t0) + getDepthNode = lambda self, node:\ + 1 + max( + self.getDepthNode(node.left), + self.getDepthNode(node.right), + ) if node else 0 + getDepth = lambda self: self.getDepthNode(self.root) + def calcAvgDepthStep(self, node, depth): + if not node: + return 0, 0 + left_s, left_n = self.calcAvgDepthStep(node.left, depth+1) + right_s, right_n = self.calcAvgDepthStep(node.right, depth+1) + return ( + len(node.events) * depth + left_s + right_s, + len(node.events) + left_n + right_n, + ) + def calcAvgDepth(self): + s, n = self.calcAvgDepthStep(self.root, 0) + if n > 0: + return float(s) / n + + + +if __name__=='__main__': + from random import shuffle + n = 100 + ls = list(range(n)) + shuffle(ls) + tree = EventSearchTree() + for x in ls: + tree.add(x, x+4, x) + print(tree.getLastBefore(15.5)) + + + diff --git a/scal3/export.py b/scal3/export.py new file mode 100644 index 000000000..8da458497 --- /dev/null +++ b/scal3/export.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3.utils import toBytes +from scal3.cal_types import calTypes +from scal3 import locale_man +from scal3.locale_man import tr as _ + +from scal3 import core + +from scal3 import ui +from scal3.monthcal import getMonthDesc + +rgbToHtml = lambda r, g, b, a=None: '#%.2x%.2x%.2x'%(r, g, b) ## What to do with alpha ?? + +def colorComposite(front, back): + if len(back)==3: + r0, g0, b0 = back + a0 = 1.0 + elif len(back)==4: + r0, g0, b0 = back[:3] + a0 = back[3]/255.0 + else: + raise ValueError + if len(front)==3: + r1, g1, b1 = front + a1 = 1.0 + elif len(front)==4: + r1, g1, b1 = front[:3] + a1 = front[3]/255.0 + else: + raise ValueError + return (a1*r1 + (1-a1)*a0*r0, + a1*g1 + (1-a1)*a0*g0, + a1*b1 + (1-a1)*a0*b0) + + +def colorComposite3(front, middle, back):## FIXME + c1 = colorComposite(colorComposite(front, middle), back) + c2 = colorComposite(front, colorComposite(middle, back)) + assert c1 == c2 + return c1 + + +def exportToHtml(fpath, monthsStatus, title=''): + ##################### Options: + format = ((2, 'SUB'), (0, None), (1, 'SUB')) ## (dateMode, htmlTag) + sizeMap = lambda size: size*0.25 - 0.5 ## ??????????????? + sep = ' ' + pluginsTextSep = ' ـ ' + pluginsTextPerLine = True ## description of each day in one line + ##################### + bgColor = rgbToHtml(*ui.bgColor) + inactiveColor = rgbToHtml(*colorComposite(ui.inactiveColor, ui.bgColor)) + borderColor= rgbToHtml(*colorComposite(ui.borderColor, ui.bgColor)) + borderTextColor = rgbToHtml(*ui.borderTextColor) + textColor = rgbToHtml(*ui.textColor) + holidayColor = rgbToHtml(*ui.holidayColor) + colors = [rgbToHtml(*x['color']) for x in ui.mcalTypeParams] + if locale_man.rtl: + DIR = 'RTL' + else: + DIR = 'LRT' + text = ''' + + + +%s + +\n'''%(title, locale_man.langSh, DIR, bgColor) + for status in monthsStatus: + text += '

\n' + for i, line in enumerate(getMonthDesc(status).split('\n')): + try: + color = colors[i] + except IndexError: + color = textColor + text += ' %s\n
\n'%(color, line) + text += '

\n' + text += ''' + \n'''\ + %(bgColor, int(ui.mcalGrid)) + text += ''' \n'''%borderColor## what text???????? + for j in range(7): + text += ''' \n'''%(borderColor, borderTextColor, core.getWeekDayN(j)) + pluginsText = '

\n'%colors[0] + text += '

\n' + for i in range(6): + text += ''' + \n'''%(borderColor, borderTextColor, _(status.weekNum[i])) + for j in range(7): + cell = status[i][j] + text += ' \n' + if cell.month == status.month: + if cell.holiday: + color = holidayColor + else: + color = colors[0] + t = cell.pluginsText.replace('\n', pluginsTextSep) + if t: + pluginsText += '%s: %s'%( + color, + _(cell.dates[calTypes.primary][2]), + t, + ) + if pluginsTextPerLine: + pluginsText += '
\n' + else: + pluginsText += ' \n' + text += '
\n' + pluginsText += '

\n' + text += '
+

+
+

+ %s +

+
+

+ %s +

+
\n

\n' + for (ind, tag) in format: + try: + mode = calTypes.active[ind] + except IndexError: + continue + try: + params = ui.mcalTypeParams[ind] + except IndexError: + continue + day = _(cell.dates[mode][2], mode)## , 2 + font = params['font'] + face = font[0] + if font[1]: + face += ' Bold' + if font[2]: + face += ' Underline' + size = str(sizeMap(font[3])) + if cell.month != status.month: + if ind==0: + text += ' ' + if tag: + text += '<%s>'%tag + text += '%s'%( + inactiveColor, + face, + size, + day, + ) + if tag: + text += ''%tag + text += '\n' + break + else: + continue + text += ' ' + if tag: + text += '<%s>'%tag + if ind==0 and cell.holiday: + color = holidayColor + else: + color = colors[ind] + text += '%s'%(color, face, size, day) + if tag: + text += ''%tag + text += '\n' + #text += sep##??????????? + text += '

\n
\n' + text = toBytes(text) ## needed for windows + text += pluginsText + text += '\n

\n'%colors[0] + text += '''

+ %s %s %s %s +

+ +'''%( + colors[0], + toBytes(_('Generated by')), + core.homePage, + core.APP_DESC, + toBytes(_('version')), + core.VERSION, + ) + open(fpath, 'w').write(text) + + + diff --git a/scal3/format_time.py b/scal3/format_time.py new file mode 100644 index 000000000..5fed206cb --- /dev/null +++ b/scal3/format_time.py @@ -0,0 +1,381 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# Also avalable in /usr/share/common-licenses/LGPL on Debian systems +# or /usr/share/licenses/common/LGPL/license.txt on ArchLinux + + +import time +from time import time as now + +from scal3.time_utils import getUtcOffsetByDateSec +from scal3.cal_types import calTypes, gregorian, to_jd +from scal3 import core +from scal3.locale_man import tr as _ + + + +## Return Julian day of given ISO year, week, and day +def iso_to_jd(year, week, day): + #assert week>0 and day>0 and day<=7 + jd0 = gregorian.to_jd(year-1, 12, 28) + return day + 7*week + jd0 - jd0%7 - 1 + +def isow_year(jd):## iso week year + year = gregorian.jd_to(jd - 3)[0] + if jd>=iso_to_jd(year+1, 1, 1): + year += 1 + return year + +def isow(jd):## iso week number + year = gregorian.jd_to(jd - 3)[0] + if jd>=iso_to_jd(year+1, 1, 1): + year += 1 + return (jd - iso_to_jd(year, 1, 1)) // 7 + 1 + + + +def compileTmFormat(format, hasTime=True): + ## format: 'Today: %Y/%m/%d' + ## pyFmt: 'Today: %s/%s/%s' + ## funcs: (get_y, get_m, get_d) + pyFmt = '' + funcs = [] + n = len(format) + i = 0 + while i +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux +from time import strftime, gmtime, strptime, mktime + +import sys + +from os.path import join, split, splitext + +from scal3.path import * +from scal3.time_utils import getJhmsFromEpoch +from scal3.cal_types import jd_to, to_jd, DATE_GREG + + +icsTmFormat = '%Y%m%dT%H%M%S' +icsTmFormatPretty = '%Y-%m-%dT%H:%M:%SZ' +## timezone? (Z%Z or Z%z) + +icsHeader = '''BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +''' + +icsWeekDays = ('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA') + +encodeIcsWeekDayList = lambda weekDayList: ','.join([icsWeekDays[wd] for wd in weekDayList]) + + +getIcsTimeByEpoch = lambda epoch, pretty=False: strftime( + icsTmFormatPretty if pretty else icsTmFormat, + gmtime(epoch) +) + #format = icsTmFormatPretty if pretty else icsTmFormat + #jd, hour, minute, second = getJhmsFromEpoch(epoch) + #year, month, day = jd_to(jd, DATE_GREG) + #return strftime(format, (year, month, day, hour, minute, second, 0, 0, 0)) + +getIcsDate = lambda y, m, d, pretty=False: ('%.4d-%.2d-%.2d' if pretty else '%.4d%.2d%.2d') % (y, m, d) + + +def getIcsDateByJd(jd, pretty=False): + y, m, d = jd_to(jd, DATE_GREG) + return getIcsDate(y, m, d, pretty) + +def getJdByIcsDate(dateStr): + tm = strptime(dateStr, '%Y%m%d') + return to_jd(tm.tm_year, tm.tm_mon, tm.tm_mday, DATE_GREG) + +def getEpochByIcsTime(tmStr): + ## python-dateutil + from dateutil.parser import parse + return int( + mktime( + parse(tmStr).timetuple() + ) + ) + + +''' +def getEpochByIcsTime(tmStr): + utcOffset = 0 + if 'T' in tmStr: + if '+' in tmStr or '-' in tmStr: + format = '%Y%m%dT%H%M%S%z' ## not working FIXME + else: + format = '%Y%m%dT%H%M%S' + else: + format = '%Y%m%d' + try: + tm = strptime(tmStr, format) + except ValueError as e: + raise ValueError('getEpochByIcsTime: Bad ics time format "%s"'%tmStr) + return int(mktime(tm)) +''' + +def splitIcsValue(value): + data = [] + for p in value.split(';'): + pp = p.split('=') + if len(pp)==1: + data.append([pp[0], '']) + elif len(pp)==2: + data.append(pp) + else: + raise ValueError('unkown ics value %r'%value) + return data + +def convertHolidayPlugToIcs(plug, startJd, endJd, namePostfix=''): + icsText = icsHeader + currentTimeStamp = strftime(icsTmFormat) + for jd in range(startJd, endJd): + isHoliday = False + for mode in plug.holidays.keys(): + myear, mmonth, mday = jd_to(jd, mode) + if (mmonth, mday) in plug.holidays[mode]: + isHoliday = True + break + if isHoliday: + gyear, gmonth, gday = jd_to(jd, DATE_GREG) + gyear_next, gmonth_next, gday_next = jd_to(jd+1, DATE_GREG) + ####### + icsText += 'BEGIN:VEVENT\n' + icsText += 'CREATED:%s\n'%currentTimeStamp + icsText += 'LAST-MODIFIED:%s\n'%currentTimeStamp + icsText += 'DTSTART;VALUE=DATE:%.4d%.2d%.2d\n'%(gyear, gmonth, gday) + icsText += 'DTEND;VALUE=DATE:%.4d%.2d%.2d\n'%(gyear_next, gmonth_next, gday_next) + icsText += 'CATEGORIES:Holidays\n' + icsText += 'TRANSP:TRANSPARENT\n' + ## TRANSPARENT because being in holiday time, does not make you busy! + ## see http://www.kanzaki.com/docs/ical/transp.html + icsText += 'SUMMARY:%s\n'%_('Holiday') + icsText += 'END:VEVENT\n' + icsText += 'END:VCALENDAR\n' + fname = split(plug.fpath)[-1] + fname = splitext(fname)[0] + '%s.ics'%namePostfix + open(fname, 'w').write(icsText) + +def convertBuiltinTextPlugToIcs(plug, startJd, endJd, namePostfix=''): + plug.load() ## FIXME + mode = plug.mode + icsText = icsHeader + currentTimeStamp = strftime(icsTmFormat) + for jd in range(startJd, endJd): + myear, mmonth, mday = jd_to(jd, mode) + dayText = plug.get_text(myear, mmonth, mday) + if dayText: + gyear, gmonth, gday = jd_to(jd, DATE_GREG) + gyear_next, gmonth_next, gday_next = jd_to(jd+1, DATE_GREG) + ####### + icsText += 'BEGIN:VEVENT\n' + icsText += 'CREATED:%s\n'%currentTimeStamp + icsText += 'LAST-MODIFIED:%s\n'%currentTimeStamp + icsText += 'DTSTART;VALUE=DATE:%.4d%.2d%.2d\n'%(gyear, gmonth, gday) + icsText += 'DTEND;VALUE=DATE:%.4d%.2d%.2d\n'%(gyear_next, gmonth_next, gday_next) + icsText += 'SUMMARY:%s\n'%dayText + icsText += 'END:VEVENT\n' + icsText += 'END:VCALENDAR\n' + fname = split(plug.fpath)[-1] + fname = splitext(fname)[0] + '%s.ics'%namePostfix + open(fname, 'w').write(icsText) + + diff --git a/scal3/import_config_2to3.py b/scal3/import_config_2to3.py new file mode 100755 index 000000000..f3e137d99 --- /dev/null +++ b/scal3/import_config_2to3.py @@ -0,0 +1,455 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import sys +import os +from os.path import join, split, splitext, dirname, isfile, isdir +from time import time as now +import json + +from collections import OrderedDict + +import shutil +import re + +from scal3.path import confDir as newConfDir +from scal3.os_utils import makeDir +from scal3.json_utils import dataToPrettyJson, dataToCompactJson +from scal3.s_object import saveBsonObject + +oldConfDir = newConfDir.replace('starcal3', 'starcal2') + +oldEventDir = join(oldConfDir, 'event') +newEventDir = join(newConfDir, 'event') + +oldEventEventsDir = join(oldEventDir, 'events') +newEventEventsDir = join(newEventDir, 'events') + +oldGroupsDir = join(oldEventDir, 'groups') +newGroupsDir = join(newEventDir, 'groups') + +oldAccountsDir = join(oldEventDir, 'accounts') +newAccountsDir = join(newEventDir, 'accounts') + + + + + +def loadConf(confPath): + if not isfile(confPath): + return + try: + text = open(confPath).read() + except Exception as e: + print('failed to read file %r: %s'%(confPath, e)) + return + ##### + data = OrderedDict() + exec(text, {}, data) + return data + +def loadCoreConf(): + confPath = join(oldConfDir, 'core.conf') + ##### + def loadPlugin(fname, **data): + data['_file'] = fname + return data + try: + text = open(confPath).read() + except Exception as e: + print('failed to read file %r: %s'%(confPath, e)) + return + ###### + text = text.replace('calTypes.activeNames', 'activeCalTypes') + text = text.replace('calTypes.inactiveNames', 'inactiveCalTypes') + ###### + data = OrderedDict() + exec(text, { + 'loadPlugin': loadPlugin + }, data) + return data + +def loadUiCustomizeConf(): + confPath = join(oldConfDir, 'ui-customize.conf') + ##### + if not isfile(confPath): + return + ##### + try: + text = open(confPath).read() + except Exception as e: + print('failed to read file %r: %s'%(confPath, e)) + return + ##### + text = re.sub('^ui\.', '', text, flags=re.M) + text = re.sub('^ud\.', 'ud__', text, flags=re.M) + ###### + data = OrderedDict() + exec(text, {}, data) + data['wcal_toolbar_mainMenu_icon'] = 'starcal-24.png' + return data + + +def writeJsonConf(name, data): + if data is None: + return + fname = name + '.json' + jsonPath = join(newConfDir, fname) + text = dataToPrettyJson(data, sort_keys=True) + try: + open(jsonPath, 'w').write(text) + except Exception as e: + print('failed to write file %r: %s'%(jsonPath, e)) + + +def importEventsIter(): + makeDir(newEventEventsDir) + oldFiles = os.listdir(oldEventEventsDir) + yield len(oldFiles) + index = 0 + for dname in oldFiles: + yield index ; index += 1 + #### + try: + _id = int(dname) + except ValueError: + continue + dpath = join(oldEventEventsDir, dname) + newDpath = join(newEventEventsDir, dname) + if not isdir(dpath): + print('"%s" must be a directory'%dpath) + continue + jsonPath = join(dpath, 'event.json') + if not isfile(jsonPath): + print('"%s": not such file'%jsonPath) + continue + try: + data = json.loads(open(jsonPath).read()) + except Exception as e: + print('error while loading json file "%s"'%jsonPath) + continue + try: + tm = data.pop('modified') + except KeyError: + tm = now() + ### + basicData = {} + #basicData['modified'] = tm + ### + ## remove extra params from data and add to basicData + for param in ( + 'remoteIds', + 'notifiers',## FIXME + ): + try: + basicData[param] = data.pop(param) + except KeyError: + pass + ### + _hash = saveBsonObject(data) + basicData['history'] = [(tm, _hash)] + open(newDpath + '.json', 'w').write(dataToPrettyJson(basicData, sort_keys=True)) + + +def importGroupsIter(): + groupsEnableDict = {} ## {groupId -> enable} + ### + makeDir(newGroupsDir) + ### + oldFiles = os.listdir(oldGroupsDir) + yield len(oldFiles) + 1 + index = 0 + ### + for fname in oldFiles: + yield index ; index += 1 + jsonPath = join(oldGroupsDir, fname) + newJsonPath = join(newGroupsDir, fname) + if not isfile(jsonPath): + print('"%s": not such file'%jsonPath) + continue + jsonPathNoX, ext = splitext(fname) + if ext != '.json': + continue + try: + _id = int(jsonPathNoX) + except ValueError: + continue + try: + data = json.loads(open(jsonPath).read()) + except Exception as e: + print('error while loading json file "%s"'%jsonPath) + continue + #### + groupsEnableDict[_id] = data.pop('enable', True) + #### + if 'history' in data: + print('skipping "%s": history already exists'%jsonPath) + continue + try: + tm = data.pop('modified') + except KeyError: + tm = now() + ### + basicData = {} + #basicData['modified'] = tm + ### + ## remove extra params from data and add to basicData + for param in ( + 'remoteIds', + ): + basicData[param] = data.pop(param, None) + for param in ( + 'enable', + 'idList', + 'remoteSyncData', + 'deletedRemoteEvents', + ): + try: + basicData[param] = data.pop(param) + except KeyError: + pass + ### + _hash = saveBsonObject(data) + basicData['history'] = [(tm, _hash)] + open(newJsonPath, 'w').write(dataToPrettyJson(basicData, sort_keys=True)) + #### + yield index ; index += 1 + oldGroupListFile = join(oldEventDir, 'group_list.json') + newGroupListFile = join(newEventDir, 'group_list.json') + try: + groupIds = json.loads(open(oldGroupListFile).read()) + except Exception as e: + print('error while loading %s: %s'%(oldGroupListFile, e)) + else: + if isinstance(groupIds, list): + signedGroupIds = [ + (1 if groupsEnableDict.get(gid, True) else -1) * gid \ + for gid in groupIds + ] + try: + open(newGroupListFile, 'w').write(dataToPrettyJson(signedGroupIds)) + except Exception as e: + print('error while writing %s: %s'%(newGroupListFile, e)) + else: + print('file "%s" contains invalid data, must contain a list'%oldGroupListFile) + + + +def importAccountsIter(): + makeDir(newAccountsDir) + ### + oldFiles = os.listdir(oldAccountsDir) + yield len(oldFiles) + index = 0 + ### + for fname in oldFiles: + yield index ; index += 1 + jsonPath = join(oldAccountsDir, fname) + newJsonPath = join(newAccountsDir, fname) + if not isfile(jsonPath): + print('"%s": not such file'%jsonPath) + continue + jsonPathNoX, ext = splitext(fname) + if ext != '.json': + continue + try: + _id = int(jsonPathNoX) + except ValueError: + continue + try: + data = json.loads(open(jsonPath).read()) + except Exception as e: + print('error while loading json file "%s"'%jsonPath) + continue + if 'history' in data: + print('skipping "%s": history already exists'%jsonPath) + continue + try: + tm = data.pop('modified') + except KeyError: + tm = now() + ### + basicData = {} + #basicData['modified'] = tm + ### + ## remove extra params from data and add to basicData + for param in ( + 'enable', + ): + try: + basicData[param] = data.pop(param) + except KeyError: + pass + ### + _hash = saveBsonObject(data) + basicData['history'] = [(tm, _hash)] + open(newJsonPath, 'w').write(dataToPrettyJson(basicData, sort_keys=True)) + + + +def importTrashIter(): + yield 1 + yield 0 + jsonPath = join(oldEventDir, 'trash.json') + newJsonPath = join(newEventDir, 'trash.json') + try: + data = json.loads(open(jsonPath).read()) + except Exception as e: + print(e) + return + if 'history' in data: + print('skipping "%s": history already exists'%jsonPath) + return + try: + tm = data.pop('modified') + except KeyError: + tm = now() + ### + basicData = {} + #basicData['modified'] = tm + ### + ## remove extra params from data and add to basicData + for param in ( + 'idList', + ): + try: + basicData[param] = data.pop(param) + except KeyError: + pass + ### + _hash = saveBsonObject(data) + basicData['history'] = [(tm, _hash)] + open(newJsonPath, 'w').write(dataToPrettyJson(basicData, sort_keys=True)) + + + +def importBasicConfigIter(): + yield 8 ## number of steps + index = 0 + #### + coreData = loadCoreConf() + coreData['version'] = '3.0.0' ## FIXME + writeJsonConf('core', coreData) + yield index ; index += 1 + #### + writeJsonConf('ui-customize', loadUiCustomizeConf()) + yield index ; index += 1 + ## remove adjustTimeCmd from ui-gtk.conf + for name in ( + 'hijri', + 'jalali', + 'locale', + 'ui', + 'ui-gtk', + 'ui-live', + ): + yield index ; index += 1 + confPath = join(oldConfDir, name + '.conf') + writeJsonConf(name, loadConf(confPath)) + + +def importEventBasicJsonIter(): + yield 4 ## number of steps + index = 0 + #### + for name in ( + 'account_list', + 'info', + 'last_ids', + ): + yield index ; index += 1 + fname = name + '.json' + try: + shutil.copy( + join(oldEventDir, fname), + join(newEventDir, fname), + ) + except Exception as e: + print(e) + + +def importPluginsIter(): + oldPlugConfDir = join(oldConfDir, 'plugins.conf') + if isdir(oldPlugConfDir): + files = os.listdir(oldPlugConfDir) + else: + files = [] + ######## + yield len(files) + index = 0 + #### + for plugName in files: + writeJsonConf( + plugName,## move it out of plugins.conf FIXME + loadConf( + join(oldPlugConfDir, plugName) + ), + ) + yield index ; index += 1 + +def importConfigIter(): + makeDir(newConfDir) + makeDir(newEventDir) + ######### + funcs = [ + importBasicConfigIter, + importEventBasicJsonIter, + importPluginsIter, + importGroupsIter, + importGroupsIter, + importAccountsIter, + importTrashIter, + importEventsIter, + ] + ### + iters = [func() for func in funcs] + ### + counts = [itr.send(None) for itr in iters] + totalCount = sum(counts) + ### + totalRatio = 0.0 + delta = 1.0 / totalCount + for iterIndex, itr in enumerate(iters): + iterCount = counts[iterIndex] + for stepIndex in itr: + yield totalRatio + stepIndex*delta + totalRatio += iterCount * delta + yield totalRatio + ### + yield 1.0 + + + + +def getOldVersion():## return version of starcal 2.* + data = loadCoreConf() + try: + return data['version'] + except: + return '' + + +################################## + +if __name__=='__main__': + list(importConfigIter()) + + + diff --git a/scal3/interval_utils.py b/scal3/interval_utils.py new file mode 100644 index 000000000..1904abaa7 --- /dev/null +++ b/scal3/interval_utils.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3.utils import s_join +from scal3.bin_heap import MaxHeap + + +ab_overlaps = lambda a0, b0, a1, b1: b0-a0+b1-a1 - abs(a0+b1-a1-b1) > 0.01 +md_overlaps = lambda m0, d0, m1, d1: d0+d1 - abs(m0-m1) > 0.01 + + +def simplifyNumList(nums, minCount=3):## nums must be sorted, minCount >= 2 + ranges = [] + tmp = [] + for n in nums: + if tmp and n - tmp[-1] != 1: + if len(tmp)>minCount: + ranges.append((tmp[0], tmp[-1])) + else: + ranges += tmp + tmp = [] + tmp.append(n) + if tmp: + if len(tmp)>minCount: + ranges.append((tmp[0], tmp[-1])) + else: + ranges += tmp + return ranges + +def cleanTimeRangeList(lst): + num = len(lst) + points = [] + for start, end in lst: + points += [ + (start, False), + (end, True), + ] + lst = [] + points.sort() + started_pq = MaxHeap() + for cursor, isEnd in points: + if isEnd: + if not started_pq: + raise RuntimeError('cursor=%s, lastStart=None'%cursor) + start, tmp = started_pq.pop() + #print('pop %s'%start) + if not started_pq: + lst.append((start, cursor)) + else: + #print('push %s'%cursor) + started_pq.push(cursor, None) + return lst + +def intersectionOfTwoIntervalList(*lists): + listsN = len(lists) + assert listsN == 2 + points = [] + for lst_index, lst in enumerate(lists): + lst = cleanTimeRangeList(lst) + for start, end in lst: + if end == start: + points += [ + (start, 0, lst_index), + (end, 1, lst_index), + ] + else: + points += [ + (start, 0, lst_index), + (end, -1, lst_index), + ] + points.sort() + openStartList = [None for i in range(listsN)] + result = [] + for cursor, ptype, lst_index in points: + if ptype == 0: ## start + ## start == cursor + if openStartList[lst_index] is None: + openStartList[lst_index] = cursor + else: + raise RuntimeError('cursor=%s, openStartList[%s]=%s'%( + cursor, + lst_index, + openStartList[lst_index], + )) + else:## end (closed or open) + ## end == cursor + if None not in openStartList: + start = max(openStartList) + if start > cursor: + raise RuntimeError('start - cursor = %s'%(start-cursor)) + result.append((start, cursor)) + #if start == cursor:## FIXME + # print('start = cursor = %s, ptype=%s'%(start%(24*3600)/3600.0, ptype)) + openStartList[lst_index] = None + return result + + +######################################################################## + +def testCleanTimeRangeList(): + pprint.pprint(cleanTimeRangeList([ + (6, 7), + (0, 4), + (1, 5), + (2, 3), + (8, 9), + (7, 8), + (8.5, 10), + (11, 11), + (5.5, 5.5), + ])) + +def testIntersection(): + pprint.pprint(intersectionOfTwoIntervalList( + [(0,1.5), (3,5), (7,9)], + [(1,3.5), (4,7.5), (8,10)] + )) + +def testJdRanges(): + pprint.pprint(JdListOccurrence([1, 3, 4, 5, 7, 9, 10, 11, 12, 13, 14, 18]).calcJdRanges()) + +def testSimplifyNumList(): + pprint.pprint(simplifyNumList([1, 2, 3, 4, 5, 7, 9, 10, 14, 16, 17, 18, 19, 21, 22, 23, 24])) + +def testOverlapsSpeed(): + from random import normalvariate + from time import time + N = 2000000 + a0, b0 = -1, 1 + b_mean = 0 + b_sigma = 2 + ### + getRandomPair = lambda: sorted([normalvariate(b_mean, b_sigma) for i in (0, 1)]) + ### + data = [] + for i in range(N): + a, b = getRandomPair() + data.append((a, b)) + t0 = time() + for a, b in data: + ab_overlaps(a0, b0, a, b) + print('%.2f'%(time()-t0)) + +if __name__=='__main__': + import pprint + testCleanTimeRangeList() + + diff --git a/scal3/json_utils.py b/scal3/json_utils.py new file mode 100644 index 000000000..278613875 --- /dev/null +++ b/scal3/json_utils.py @@ -0,0 +1,122 @@ +import sys +try: + import json +except ImportError: + import simplejson as json + +from scal3.lib import OrderedDict + +dataToPrettyJson = lambda data, ensure_ascii=False, sort_keys=False: json.dumps( + data, + sort_keys=sort_keys, + indent=2, + ensure_ascii=ensure_ascii, +) + +dataToCompactJson = lambda data, ensure_ascii=False, sort_keys=False: json.dumps( + data, + sort_keys=sort_keys, + separators=(',', ':'), + ensure_ascii=ensure_ascii, +) + +jsonToData = json.loads + +jsonToOrderedData = lambda text: json.JSONDecoder( + object_pairs_hook=OrderedDict, +).decode(text) + +############################### + +def loadJsonConf(module, confPath, decoders={}): + from os.path import isfile + ### + if not isfile(confPath): + return + ### + try: + text = open(confPath).read() + except Exception as e: + print('failed to read file "%s": %s'%(confPath, e)) + return + ### + try: + data = json.loads(text) + except Exception as e: + print('invalid json file "%s": %s'%(confPath, e)) + return + ### + if isinstance(module, str): + module = sys.modules[module] + for param, value in data.items(): + try: + decoder = decoders[param] + except KeyError: + pass + else: + value = decoder(value) + setattr(module, param, value) + + +def saveJsonConf(module, confPath, params, encoders={}): + if isinstance(module, str): + module = sys.modules[module] + ### + data = OrderedDict() + for param in params: + value = getattr(module, param) + try: + encoder = encoders[param] + except KeyError: + pass + else: + value = encoder(value) + data[param] = value + ### + text = dataToPrettyJson(data) + try: + open(confPath, 'w').write(text) + except Exception as e: + print('failed to save file "%s": %s'%(confPath, e)) + return + + +def loadModuleJsonConf(module): + if isinstance(module, str): + module = sys.modules[module] + ### + decoders = getattr(module, 'confDecoders', {}) + ### + try: + sysConfPath = module.sysConfPath + except AttributeError: + pass + else: + loadJsonConf( + module, + sysConfPath, + decoders, + ) + #### + loadJsonConf( + module, + module.confPath, + decoders, + ) + ## should use module.confParams to restrict json keys? FIXME + + + +def saveModuleJsonConf(module): + if isinstance(module, str): + module = sys.modules[module] + ### + saveJsonConf( + module, + module.confPath, + module.confParams, + getattr(module, 'confEncoders', {}), + ) + + + diff --git a/scal3/lib/__init__.py b/scal3/lib/__init__.py new file mode 100644 index 000000000..bfdfb4e76 --- /dev/null +++ b/scal3/lib/__init__.py @@ -0,0 +1,5 @@ +from collections import OrderedDict + + + + diff --git a/scal3/locale_man.py b/scal3/locale_man.py new file mode 100644 index 000000000..9f9639e1c --- /dev/null +++ b/scal3/locale_man.py @@ -0,0 +1,435 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import os, string +from os.path import join, isfile, isdir, isabs +from os.path import splitext +import locale, gettext + +from natz.local import get_localzone + +from .path import * +from scal3.utils import StrOrderedDict, myRaise +from scal3.utils import toBytes, toStr +from scal3.json_utils import * +from scal3.s_object import JsonSObj +from scal3.cal_types import calTypes + +#import codecs +#open = lambda filename, mode='r': codecs.open(filename, mode=mode, encoding='utf-8') + +########################################################## + +localTz = get_localzone() +localTzStr = str(localTz) + +########################################################## + +confPath = join(confDir, 'locale.json') + +confParams = ( + 'lang', + 'enableNumLocale', +) + +def loadConf(): + loadModuleJsonConf(__name__) + +def saveConf(): + saveModuleJsonConf(__name__) + + +########################################################## + +langDir = join(rootDir, 'conf', 'lang') +localeDir = '/usr/share/locale' + +## point FIXME +digits = { + 'en':('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'), + 'ar':('٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'), + 'fa':('۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'), + 'ur':('۔', '١', '٢', '٣', '۴', '۵', '٦', '٧', '٨', '٩'), + 'hi':('०', '१', '२', '३', '४', '५', '६', '७', '८', '९'), + 'th':('๐', '๑', '๒', '๓', '๔', '๕', '๖', '๗', '๘', '๙'), +} + +def getLangDigits(langSh0): + try: + return digits[langSh0] + except KeyError: + return digits['en'] + +## ar: Aarbic ar_* Arabic-indic Arabic Contries +## fa: Persian fa_IR Eastern (Extended) Arabic-indic Iran & Afghanintan +## ur: Urdu ur_PK (Eastern) Arabic-indic Pakistan (& Afghanintan??) +## hi: Hindi hi_IN Devenagari India +## th: Thai th_TH ---------- Thailand + +## Urdu digits is a combination of Arabic and Persian digits, except for Zero that is +## named ARABIC-INDIC DIGIT ZERO in unicode database + +LRM = '\u200e' ## left to right mark +RLM = '\u200f' ## right to left mark +ZWNJ = '\u200c' ## zero width non-joiner +ZWJ = '\u200d' ## zero width joiner + +sysLangDefault = os.environ.get('LANG', '') + +langDefault = '' +lang = '' +langActive = '' +## langActive==lang except when lang=='' (in that case, langActive will be taken from system) +## langActive will not get changed while the program is running +## (because it needs to restart program to apply new language) +langSh = '' ## short language name, for example 'en', 'fa', 'fr', ... +rtl = False ## right to left + +enableNumLocale = True + +########################################################## + +loadConf() + +########################################################## + +## translator +tr = lambda s, *a, **ka: numEncode(s, *a, **ka) if isinstance(s, int) else str(s) + +class LangData(JsonSObj): + params = ( + 'code', + 'name', + 'nativeName', + 'fileName',## shortName, ... FIXME + 'flag',## flagFile + 'rtl', + 'timeZoneList', + ) + def __init__(self, _file): + self.file = _file ## json file path + #### + self.code = '' + self.name = '' + self.nativeName = '' + self.fileName = '' + self.flag = '' + self.rtl = False + self.transPath = '' + ## + self.timeZoneList = [] + def setData(self, data): + JsonSObj.setData(self, data) + ##### + for param in ( + 'code', + 'name', + 'nativeName', + ): + if not getattr(self, param): + raise ValueError('missing or empty parameter "%s" in language file "%s"'%(param, self.file)) + ##### + if not isabs(self.flag): + self.flag = join(pixDir, 'flags', self.flag) + ##### + transPath = '' + if self.fileName: + path = join(rootDir, 'locale.d', self.fileName+'.mo') + #print('path=%s'%path) + if isfile(path): + transPath = path + else: + #print('-------- File %r does not exists'%path) + for prefix in ('/usr', '/usr/local'): + path = join(prefix, 'share', 'locale', self.fileName, 'LC_MESSAGES', '%s.mo'%APP_NAME) + if isfile(path): + transPath = path + break + #print(code, transPath) + self.transPath = transPath + + + +langDict = StrOrderedDict() + +try: + langDefault = open(join(langDir, 'default')).read().strip() +except Exception as e: + print('failed to read default lang file: %s'%e) + + +for fname in os.listdir(langDir): + fname_nox, ext = splitext(fname) + ext = ext.lower() + if ext != '.json': + continue + fpath = join(langDir, fname) + try: + data = jsonToData(open(fpath).read()) + except Exception as e: + print('failed to load json file %s'%fpath) + raise e + langObj = LangData(fpath) + langObj.setData(data) + langDict[langObj.code] = langObj + ### + if localTzStr in langObj.timeZoneList: + langDefault = langObj.code + + +langDict.sort('name') ## OR 'code' or 'nativeName' ???????????? + + +def prepareLanguage(): + global lang, langActive, langSh, rtl + if lang=='': + #langActive = locale.setlocale(locale.LC_ALL, '') + langActive = sysLangDefault + if not langActive in langDict.keyList: + langActive = langDefault + #os.environ['LANG'] = langActive + elif lang in langDict.keyList: + #try: + # lang = locale.setlocale(locale.LC_ALL, locale.normalize(lang)) + #except locale.Error: + #lang = lang.lower() + #lines = popen_output('locale -a').split('\n') ## FIXME + #for line in lines: + # if line.lower().starts(lang) + #locale.setlocale(locale.LC_ALL, lang) ## lang = locale.setlocale(... + langActive = lang + os.environ['LANG'] = lang + else:## not lang in langDict.keyList + #locale.setlocale(locale.LC_ALL, langDefault) ## lang = locale.setlocale(... + lang = langDefault + langActive = langDefault + os.environ['LANG'] = langDefault + langSh = langActive.split('_')[0] + #sysRtl = (popen_output(['locale', 'cal_direction'])=='3\n')## FIXME + rtl = langDict[langActive].rtl + return langSh + + +def loadTranslator(ui_is_qt=False): + global tr + ## FIXME: How to say to gettext that itself detects coding(charset) from locale name and return a unicode object instead of str? + #if isdir(localeDir): + # transObj = gettext.translation(APP_NAME, localeDir, languages=[langActive, langDefault], fallback=True) + #else:## for example on windows (what about mac?) + try: + fd = open(langDict[langActive].transPath, 'rb') + except: + transObj = None + else: + transObj = gettext.GNUTranslations(fd) + if transObj: + def tr(s, *a, **ka): + if isinstance(s, (int, float)): + s = numEncode(s, *a, **ka) + else: + s = toStr(transObj.gettext(s)) + if ui_is_qt: + s = s.replace('_', '&') + if a: + s = s % a + if ka: + s = s % ka + return s + ''' + if ui_is_qt:## qt takes "&" instead of "_" as trigger + tr = lambda s, *a, **ka: numEncode(s, *a, **ka) \ + if isinstance(s, int) \ + else transObj.gettext(toBytes(s)).replace('_', '&').decode('utf-8') + else: + tr = lambda s, *a, **ka: numEncode(s, *a, **ka) \ + if isinstance(s, int) \ + else transObj.gettext(toBytes(s)).decode('utf-8') + ''' + else: + def tr(s, *a, **ka): + return str(s) + return tr + +rtlSgn = lambda: 1 if rtl else -1 + +getMonthName = lambda mode, month, year=None: tr(calTypes[mode].getMonthName(month, year)) + +getNumSep = lambda: tr('.') if enableNumLocale else '.' + +def getDigits(): + if enableNumLocale: + try: + return digits[langSh] + except KeyError: + pass + return digits['en'] + +def getAvailableDigitKeys(): + keys = set(digits['en']) + if langSh != 'en': + try: + locDigits = digits[langSh] + except KeyError: + pass + else: + keys.update(locDigits) + return keys + +def numEncode(num, mode=None, fillZero=0, negEnd=False): + if not enableNumLocale: + mode = 'en' + if mode==None: + mode = langSh + elif isinstance(mode, int): + if langSh != 'en': + try: + mode = calTypes[mode].origLang + except AttributeError: + mode = langSh + if mode=='en' or not mode in digits: + if fillZero: + return '%.*d'%(fillZero, num) + else: + return '%d'%num + neg = (num<0) + dig = getLangDigits(mode) + res = '' + for c in str(abs(num)): + if c=='.': + if enableNumLocale: + c = tr('.') + res += c + else: + res += dig[int(c)] + if fillZero>0: + res = res.rjust(fillZero, dig[0]) + if neg: + if negEnd: + res = res + '-' + else: + res = '-' + res + return res + +def textNumEncode(st, mode=None, changeSpecialChars=True, changeDot=False): + if not enableNumLocale: + mode = 'en' + if mode==None: + mode = langSh + elif isinstance(mode, int): + if langSh != 'en': + try: + mode = calTypes[mode].origLang + except AttributeError: + mode = langSh + dig = getLangDigits(mode) + res = '' + for c in toStr(st): + try: + i = int(c) + except: + if enableNumLocale: + if c in (',', '_', '%'):## FIXME + if changeSpecialChars: + c = tr(c) + elif c=='.':## FIXME + if changeDot: + c = tr(c) + res += c + else: + res += dig[i] + return res ## .encode('utf8') + +floatEncode = lambda st, mode=None:\ + textNumEncode(st, mode, changeSpecialChars=False, changeDot=True) + +def numDecode(numSt): + numSt = numSt.strip() + try: + return int(numSt) + except ValueError: + pass + numSt = toStr(numSt) + tryLangs = list(digits.keys()) + if langSh in digits: + tryLangs.remove(langSh) + tryLangs.insert(0, langSh) + for tryLang in tryLangs: + tryLangDigits = digits[tryLang] + numEn = '' + for dig in numSt: + if dig=='-': + numEn += dig + else: + try: + numEn += str(tryLangDigits.index(dig)) + except ValueError as e: + print('error in decoding num char %s'%dig) + #raise e + break + else: + return int(numEn) + raise ValueError('invalid locale number %s'%numSt) + +def textNumDecode(text):## converts '۱۲:۰۰, ۱۳' to '12:00, 13' + text = toStr(text) + textEn = '' + langDigits = getLangDigits(langSh) + for ch in text: + try: + textEn += str(langDigits.index(ch)) + except ValueError: + for sch in (',', '_', '.'): + if ch == tr(sch): + ch = sch + break + textEn += ch + return textEn + +dateLocale = lambda year, month, day:\ + numEncode(year, fillZero=4) + '/' + \ + numEncode(month, fillZero=2) + '/' + \ + numEncode(day, fillZero=2) + +def cutText(text, n): + text = toStr(text) + newText = text[:n] + if len(text) > n: + if text[n] not in list(string.printable)+[ZWNJ]: + try: + newText += ZWJ + except UnicodeDecodeError: + pass + return newText + +addLRM = lambda text: LRM + toStr(text) + +def popenDefaultLang(*args, **kwargs): + global sysLangDefault, lang + from subprocess import Popen + os.environ['LANG'] = sysLangDefault + p = Popen(*args, **kwargs) + os.environ['LANG'] = lang + return p + +############################################## + +prepareLanguage() +loadTranslator() + + diff --git a/scal3/lockfile.py b/scal3/lockfile.py new file mode 100644 index 000000000..63418a01d --- /dev/null +++ b/scal3/lockfile.py @@ -0,0 +1,73 @@ +import os +from os.path import isfile, exists +from time import time as now +from collections import OrderedDict +import atexit + + +import psutil + +from scal3.utils import myRaise +from scal3.json_utils import jsonToData, dataToPrettyJson + + + +def checkAndSaveJsonLockFile(fpath): + locked = False + if isfile(fpath): + try: + text = open(fpath).read() + except: + myRaise() + locked = True + else: + try: + data = jsonToData(text) + except: + print('lock file %s is not valid'%fpath) + else: + try: + pid = data['pid'] + cmd = data['cmd'] + except: + print('lock file %s is not valid'%fpath) + else: + try: + proc = psutil.Process(pid) + except psutil.NoSuchProcess: + print('lock file %s: pid %s does not exist'%(fpath, pid)) + else: + if proc.cmdline() == cmd: + locked = True + else: + print('lock file %s: cmd does match: %s != %s'%(fpath, proc.cmdline(), cmd)) + elif exists(fpath): + ## what to do? FIXME + pass + ###### + if not locked: + my_pid = os.getpid() + my_proc = psutil.Process(my_pid) + my_text = dataToPrettyJson(OrderedDict([ + ('pid', my_pid), + ('cmd', my_proc.cmdline()), + ('time', now()), + ])) + try: + open(fpath, 'w').write(my_text) + except Exception as e: + print('failed to write lock file %s: %s'%(fpath, e)) + else: + atexit.register(os.remove, fpath) + ###### + return locked + + + + + + + + + + diff --git a/scal3/monthcal.py b/scal3/monthcal.py new file mode 100644 index 000000000..8bf460575 --- /dev/null +++ b/scal3/monthcal.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3.cal_types import calTypes +from scal3 import core +from scal3.core import myRaise, getMonthName, getMonthLen, getWeekDay, getWeekNumber +from scal3.locale_man import rtl, rtlSgn +from scal3.locale_man import tr as _ +from scal3 import ui + +pluginName = 'MonthCal' + +class MonthStatus(list): ## FIXME + ## self[sy<6][sx<7] of cells + ## list (of 6 lists, each list containing 7 cells) + def __init__(self, cellCache, year, month): + self.year = year + self.month = month + self.monthLen = getMonthLen(year, month, calTypes.primary) + self.offset = getWeekDay(year, month, 1)## month start offset + self.weekNum = [getWeekNumber(year, month, 1+7*i) for i in range(6)] + ######### + startJd, endJd = core.getJdRangeForMonth(year, month, calTypes.primary) + tableStartJd = startJd - self.offset + ##### + list.__init__(self, [ + [ + cellCache.getCell( + tableStartJd + yPos*7 + xPos + ) for xPos in range(7) + ] for yPos in range(6) + ]) + #def getDayCell(self, day):## needed? FIXME + # yPos, xPos = divmod(day + self.offset - 1, 7) + # return self[yPos][xPos] + def allCells(self): + l = [] + for row in self: + l += row + return l + +def setParamsFunc(cell): + offset = getWeekDay(cell.year, cell.month, 1)## month start offset + yPos, xPos = divmod(offset + cell.day - 1, 7) + cell.monthPos = (xPos, yPos) + ### + """ + if yPos==0: + cell.monthPosPrev = (xPos, 5) + else: + cell.monthPosPrev = None + ### + if yPos==5: + cell.monthPosNext = (xPos, 0) + else: + cell.monthPosNext = None + """ + +getMonthStatus = lambda year, month: ui.cellCache.getCellGroup(pluginName, year, month) +getCurrentMonthStatus = lambda: ui.cellCache.getCellGroup(pluginName, ui.cell.year, ui.cell.month) + +######################## + +def getMonthDesc(status=None): + if not status: + status = getCurrentMonthStatus() + first = None + last = None + for i in range(6): + for j in range(7): + c = status[i][j] + if first: + if c.month == status.month: + last = c + else: + break + else: + if c.month == status.month: + first = c + else: + continue + text = '' + for mode in calTypes.active: + if text != '': + text += '\n' + if mode==calTypes.primary: + y, m = first.dates[mode][:2] ## = (status.year, status.month) + text += '%s %s'%(getMonthName(mode, m), _(y)) + else: + y1, m1 = first.dates[mode][:2] + y2, m2 = last.dates[mode][:2] + dy = y2 - y1 + if dy==0: + dm = m2 - m1 + elif dy==1: + dm = m2 + 12 - m1 + else: + raise RuntimeError('y1=%d, m1=%d, y2=%d, m2=%d'%(y1, m1, y2, m2)) + if dm==0: + text += '%s %s'%(getMonthName(mode, m1), _(y1)) + elif dm==1: + if dy==0: + text += '%s %s %s %s'%( + getMonthName(mode, m1), + _('and'), + getMonthName(mode, m2), + _(y1), + ) + else: + text += '%s %s %s %s %s'%( + getMonthName(mode, m1), + _(y1), + _('and'), + getMonthName(mode, m2), + _(y2), + ) + elif dm==2: + if dy==0: + text += '%s%s %s %s %s %s'%( + getMonthName(mode, m1), + _(','), + getMonthName(mode, m1+1), + _('and'), + getMonthName(mode, m2), + _(y1), + ) + else: + if m1==11: + text += '%s %s %s %s %s %s %s'%( + getMonthName(mode, m1), + _('and'), + getMonthName(mode, m1+1), + _(y1), + _('and'), + getMonthName(mode, 1), + _(y2), + ) + elif m1==12: + text += '%s %s %s %s %s %s %s'%( + getMonthName(mode, m1), + _(y1), + _('and'), + getMonthName(mode, 1), + _('and'), + getMonthName(mode, 2), + _(y2), + ) + return text + + +######################## +ui.cellCache.registerPlugin(pluginName, setParamsFunc, MonthStatus) + + + + diff --git a/scal3/mywidgets/__init__.py b/scal3/mywidgets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scal3/mywidgets/multi_spin.py b/scal3/mywidgets/multi_spin.py new file mode 100644 index 000000000..ef1642965 --- /dev/null +++ b/scal3/mywidgets/multi_spin.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# Also avalable in /usr/share/common-licenses/LGPL on Debian systems +# or /usr/share/licenses/common/LGPL/license.txt on ArchLinux + +from scal3.utils import toBytes, toStr +from scal3.utils import myRaise + +from scal3 import locale_man +from scal3.locale_man import numEncode, floatEncode, numDecode, textNumEncode, textNumDecode + +from scal3.cal_types import to_jd, jd_to, convert + + + + +class Field: + myKeys = set() + def setDefault(self): + pass + def setValue(self, v): + pass + getValue = lambda self: None + def plus(self, p):## p is usually 1, -1, 10, -10 + pass + def setText(self): + pass + def getText(self): + pass + def getMaxWidth(self): + raise NotImplementedError + getFieldAt = lambda self, text, pos: self + +class NumField(Field): + def setRange(self, _min, _max): + self._min = _min + self._max = _max + self.setValue(self.value) + def setDefault(self): + self.value = self._min + def setValue(self, v): + if v < self._min: + v = self._min + elif v > self._max: + v = self._max + self.value = v + getValue = lambda self: self.value + + + +class IntField(NumField): + def __init__(self, _min, _max, fill=0): + self._min = _min + self._max = _max + self.fill = fill + self.myKeys = locale_man.getAvailableDigitKeys() + self.setDefault() + def setText(self, text): + try: + num = int(float(textNumDecode(text))) + except: + myRaise() + self.setDefault() + else: + self.setValue(num) + setValue = lambda self, v: NumField.setValue(self, int(v)) + getText = lambda self: numEncode(self.value, fillZero=self.fill) + getMaxWidth = lambda self: max( + len(str(self._min)), + len(str(self._max)), + ) + plus = lambda self, p: self.setValue(self.value + p) + + +class FloatField(NumField): + def __init__(self, _min, _max, digits): + self._min = _min + self._max = _max + self.digits = digits + self.digDec = 10**digits + self.myKeys = locale_man.getAvailableDigitKeys() + self.setDefault() + def setText(self, text): + try: + num = float(textNumDecode(text)) + except: + myRaise() + self.setDefault() + else: + self.setValue(num) + getText = lambda self: floatEncode('%.*f'%(self.digits, self.value)) + getMaxWidth = lambda self: max( + len('%.*f'%(self.digits, self._min)), + len('%.*f'%(self.digits, self._max)), + ) + plus = lambda self, p: self.setValue(self.value + float(p)/self.digDec) + + +class YearField(IntField): + def __init__(self): + IntField.__init__(self, -9999, 9999) + +class MonthField(IntField): + def __init__(self): + IntField.__init__(self, 1, 12, 2) + +class DayField(IntField): + def __init__(self, pad=2): + IntField.__init__(self, 1, 31, pad) + def setMax(self, _max): + self._max = _max + #if self.value > _max: + # self.value = _max + +class HourField(IntField): + def __init__(self): + IntField.__init__(self, 0, 24, 2) + +class Z60Field(IntField): + def __init__(self): + IntField.__init__(self, 0, 59, 2) + + +class SingleCharField(Field): + def __init__(self, *values): + self.values = values + self.myKeys = set(values) + self.setDefault() + def setDefault(self): + self.value = 0 + def setValue(self, v): + if not v in self.values: + raise ValueError('SingleCharField.setValue: %r is not a valid value'%v) + self.value = v + getValue = lambda self: self.value + setText = lambda text: self.setValue(text) + getText = lambda self: self.value + getMaxWidth = lambda self: 1 + +class StrConField(Field): + def __init__(self, text): + self._text = text + getValue = lambda self: self._text + getText = lambda self: self_text + getMaxWidth = lambda self: len(self._text) + +class ContainerField(Field): + __len__ = lambda self: len(self.children) + def __init__(self, sep, *children): + self.sep = sep + self.children = children + for child in children: + self.myKeys.update(child.myKeys) + def setDefault(self): + for child in self.children: + child.setDefault() + def setValue(self, value): + if not isinstance(value, (tuple, list)): + value = (value,) + n = min(len(value), len(self)) + for i in range(n): + self.children[i].setValue(value[i]) + getValue = lambda self: [child.getValue() for child in self.children] + def setText(self, text): + parts = text.split(self.sep) + n = len(self) + pn = min(n, len(parts)) + ## pn <= n + for i in range(pn): + self.children[i].setText(parts[i]) + for i in range(pn, n): + self.children[i].setDefault() + getText = lambda self: self.sep.join([child.getText() for child in self.children]) + getMaxWidth = lambda self: sum( + [child.getMaxWidth() for child in self.children] + ) + len(self.sep)*(len(self)-1) + def getFieldAt(self, text, pos): + if not self.children: + return self + fieldIndex = 0 + i = 0 + n = len(text) + fn = len(self) + while True: + i2 = text.find(self.sep, i+1) + if i2 == -1 or i2 >= pos: + break + i = i2 + fieldIndex += 1 + return self.children[fieldIndex].getFieldAt(text[i:], pos-i) + #def getRegion(self, text, pos, fieldIndexPlus): + + + + + + + diff --git a/scal3/os_utils.py b/scal3/os_utils.py new file mode 100644 index 000000000..ab457d7bd --- /dev/null +++ b/scal3/os_utils.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import sys +import os +from os.path import isdir, isfile +#import platform + + +def getOsName():## 'linux', 'win', 'mac', 'unix' + #psys = platform.system().lower()## 'linux', 'windows', 'darwin', ... + plat = sys.platform ## 'linux2', 'win32', 'darwin' + if plat.startswith('linux'): + return 'linux' + elif plat.startswith('win'): + return 'win' + elif plat=='darwin': + ## os.environ['OSTYPE'] == 'darwin10.0' + ## os.environ['MACHTYPE'] == 'x86_64-apple-darwin10.0' + ## platform.dist() == ('', '', '') + ## platform.release() == '10.3.0' + return 'mac' + elif os.sep=='\\': + return 'win' + elif os.sep=='/': + return 'unix' + else: + raise OSError('Unkown operating system!') + + +def makeDir(direc): + if not isdir(direc): + os.makedirs(direc) + +def getUsersData(): + data = [] + for line in open('/etc/passwd').readlines(): + parts = line.strip().split(':') + if len(parts) < 7: + continue + data.append({ + 'login': parts[0], + 'uid': parts[2], + 'gid': parts[3], + 'real_name': parts[4], + 'home_dir': parts[5], + 'shell': parts[6], + }) + return data + +def getUserDisplayName(): + if os.sep=='/': + username = os.getenv('USER') + if isfile('/etc/passwd'): + for user in getUsersData(): + if user['login'] == username: + if user['real_name']: + return user['real_name'] + else: + return username + return username + else:## FIXME + username = os.getenv('USERNAME') + return username + + +def kill(pid, signal=0): + ''' + sends a signal to a process + returns True if the pid is dead + with no signal argument, sends no signal + ''' + #if 'ps --no-headers' returns no lines, the pid is dead + try: + return os.kill(pid, signal) + except OSError as e: + #process is dead + if e.errno == 3: + return True + #no permissions + elif e.errno == 1: + return False + else: + raise e + +def dead(pid): + if kill(pid): + return True + + #maybe the pid is a zombie that needs us to wait for it + from os import waitpid, WNOHANG + try: + dead = waitpid(pid, WNOHANG)[0] + except OSError as e: + #pid is not a child + if e.errno == 10: + return False + else: + raise e + return dead + +#def kill(pid, sig=0): pass #DEBUG: test hang condition + + +def goodkill(pid, interval=1, hung=20): + 'let process die gracefully, gradually send harsher signals if necessary' + from signal import SIGTERM, SIGINT, SIGHUP, SIGKILL + from time import sleep + + for signal in (SIGTERM, SIGINT, SIGHUP): + if kill(pid, signal): + return + if dead(pid): + return + sleep(interval) + + i = 0 + while True: + #infinite-loop protection + if i < hung: + i += 1 + else: + raise OSError('Process %s is hung. Giving up kill.'%pid) + if kill(pid, SIGKILL): + return + if dead(pid): + return + sleep(interval) + + + + + diff --git a/scal3/path.py b/scal3/path.py new file mode 100644 index 000000000..8cf21e63d --- /dev/null +++ b/scal3/path.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import os +from os.path import dirname, join, abspath + +from scal3.os_utils import getOsName + +APP_NAME = 'starcal3' + +osName = getOsName() + +srcDir = dirname(__file__) +cwd = os.getcwd() +if srcDir in ('.', ''): + srcDir = cwd +elif os.sep=='/': + if srcDir.startswith('./'): + srcDir = cwd + srcDir[1:] + elif srcDir[0] != '/': + srcDir = join(cwd, srcDir) +elif os.sep=='\\': + if srcDir.startswith('.\\'): + srcDir = cwd + srcDir[1:] +#print('srcDir=%r'%srcDir) + +rootDir = abspath(dirname(srcDir)) +pixDir = join(rootDir, 'pixmaps') +plugDir = join(rootDir, 'plugins') + +if osName in ('linux', 'unix'): + homeDir = os.getenv('HOME') + confDir = homeDir + '/.' + APP_NAME + sysConfDir = '/etc/' + APP_NAME + tmpDir = '/tmp' + #user = os.getenv('USER') +elif osName=='mac': + homeDir = os.getenv('HOME') + confDir = homeDir + '/Library/Preferences/' + APP_NAME ## OR '/Library/' + APP_NAME + sysConfDir = join(rootDir, 'config')## FIXME + tmpDir = '/tmp' + #user = os.getenv('USER') +elif osName=='win': + #homeDrive = os.environ['HOMEDRIVE'] + homeDir = os.getenv('HOMEPATH') + confDir = os.getenv('APPDATA') + '\\' + APP_NAME + sysConfDir = join(rootDir, 'config') + tmpDir = os.getenv('TEMP') + #user = os.getenv('USERNAME') +else: + raise OSError('Unkown operating system!') + +deskDir = join(homeDir, 'Desktop')## in all operating systems? FIXME + +userPlugConf = join(confDir, 'plugin.conf') +modDir = '%s/cal_types'%srcDir +plugDirUser = join(confDir, 'plugins') +objectDir = join(confDir, 'objects') + +purpleDir = join(homeDir, '.purple')## FIXME + diff --git a/scal3/plugin_api.py b/scal3/plugin_api.py new file mode 100644 index 000000000..0425571c7 --- /dev/null +++ b/scal3/plugin_api.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import sys + +#modulesDict = {} +## we dont have the module object inside the module itself!! +## how can we register module (with its getList and setList) ? + +class PluginError(Exception): + pass + +def get(moduleName, attr, default=None, absolute=False): + if not absolute: + moduleName = 'scal3.' + moduleName + #module = __import__(moduleName, fromlist=['__plugin_api_get__', attr]) + #print(sorted(sys.modules.keys())) + module = sys.modules[moduleName] + allowed = getattr(module, '__plugin_api_get__', []) + if not attr in allowed: + raise PluginError('plugin is not allowed to get attribute %s from module %s'%(attr, moduleName)) + return getattr(module, attr, default) + +def set(moduleName, attr, value, absolute=False): + if not absolute: + moduleName = 'scal3.' + moduleName + #module = __import__(moduleName, fromlist=['__plugin_api_set__', attr]) + module = sys.modules[moduleName] + allowed = getattr(module, '__plugin_api_set__', []) + if not attr in allowed: + raise PluginError('plugin is not allowed to set attribute %s to module %s'%(attr, moduleName)) + setattr(module, attr, value) + +#def add(moduleName, attr, value):## FIXME +# module = __import__(moduleName) +# if not module.get('__plugin_api_add__', False) + + diff --git a/scal3/plugin_man.py b/scal3/plugin_man.py new file mode 100644 index 000000000..1a9950f78 --- /dev/null +++ b/scal3/plugin_man.py @@ -0,0 +1,703 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import sys +from time import strftime +from time import localtime +from os.path import isfile, dirname, join, split, splitext, isabs + + +from scal3.path import * +from scal3.utils import myRaiseTback +from scal3.json_utils import * +from scal3.cal_types import calTypes, jd_to, to_jd, convert, DATE_GREG +from scal3.locale_man import tr as _ +from scal3.locale_man import getMonthName +from scal3.ics import icsTmFormat, icsHeader +from scal3.s_object import * + +try: + import logging + log = logging.getLogger(APP_NAME) +except: + from scal3.utils import FallbackLogger + log = FallbackLogger() + +## FIXME +pluginsTitleByName = { + 'pray_times': _('Islamic Pray Times'), +} + +pluginClassByName = {} + +def registerPlugin(cls): + assert cls.name + pluginClassByName[cls.name] = cls + + +getPlugPath = lambda _file: _file if isabs(_file) else join(plugDir, _file) + + +def myRaise(File=__file__): + i = sys.exc_info() + log.error('File "%s", line %s: %s: %s\n'%(File, i[2].tb_lineno, i[0].__name__, i[1])) + + +class BasePlugin(SObj): + name = None + external = False + loaded = True + getArgs = lambda self: { + '_file': self.file, + 'enable': self.enable, + 'show_date': self.show_date, + } + params = ( + #'mode', + 'title',## previously 'desc' + 'enable', + 'show_date', + 'default_enable', + 'default_show_date', + 'about', + 'authors', + 'hasConfig', + 'hasImage', + 'lastDayMerge', + ) + essentialParams = (## FIXME + 'title', + ) + __bool__ = lambda self: self.enable ## FIXME + def __init__( + self, + _file, + ): + self.file = _file + ###### + self.mode = DATE_GREG + self.title = '' + ### + self.enable = False + self.show_date = False + ## + self.default_enable = False + self.default_show_date = False + ### + self.about = '' + self.authors = [] + self.hasConfig = False + self.hasImage = False + self.lastDayMerge = True + def getData(self): + data = JsonSObj.getData(self) + data['calType'] = calTypes.names[self.mode] + return data + def setData(self, data): + if not 'enable' in data: + data['enable'] = data.get('default_enable', self.default_enable) + ### + if not 'show_date' in data: + data['show_date'] = data.get('default_show_date', self.default_show_date) + ### + try: + data['title'] = _(data['title']) + except KeyError: + pass + ### + try: + data['about'] = _(data['about']) + except KeyError: + pass + ### + try: + authors = data['authors'] + except KeyError: + pass + else: + data['authors'] = [_(author) for author in authors] + ##### + if 'calType' in data: + calType = data['calType'] + try: + self.mode = calTypes.names.index(calType) + except ValueError: + #raise ValueError('Invalid calType: %r'%calType) + log.error('Plugin "%s" needs calendar module "%s" that is not loaded!\n'%(_file, mode)) + self.mode = None + del data['calType'] + + ##### + JsonSObj.setData(self, data) + def clear(self): + pass + def load(self): + pass + get_text = lambda self, year, month, day: '' + def update_cell(self, c): + y, m, d = c.dates[self.mode] + text = '' + t = self.get_text(y, m, d) + if t: + text += t + if self.lastDayMerge and d>=calTypes[self.mode].minMonthLen: + ## and d<=calTypes[self.mode].maxMonthLen: + ny, nm, nd = jd_to(c.jd + 1, self.mode) + if nm > m or ny > y: + nt = self.get_text(y, m, d+1) + if nt: + text += nt + if text: + if c.pluginsText: + c.pluginsText += '\n' + c.pluginsText += text + def onCurrentDateChange(self, gdate): + pass + def exportToIcs(self, fileName, startJd, endJd): + currentTimeStamp = strftime(icsTmFormat) + self.load() ## FIXME + mode = self.mode + icsText = icsHeader + for jd in range(startJd, endJd): + myear, mmonth, mday = jd_to(jd, mode) + dayText = self.get_text(myear, mmonth, mday) + if dayText: + gyear, gmonth, gday = jd_to(jd, DATE_GREG) + gyear_next, gmonth_next, gday_next = jd_to(jd+1, DATE_GREG) + ####### + icsText += 'BEGIN:VEVENT\n' + icsText += 'CREATED:%s\n'%currentTimeStamp + icsText += 'LAST-MODIFIED:%s\n'%currentTimeStamp + icsText += 'DTSTART;VALUE=DATE:%.4d%.2d%.2d\n'%(gyear, gmonth, gday) + icsText += 'DTEND;VALUE=DATE:%.4d%.2d%.2d\n'%(gyear_next, gmonth_next, gday_next) + icsText += 'SUMMARY:%s\n'%dayText + icsText += 'END:VEVENT\n' + icsText += 'END:VCALENDAR\n' + open(fileName, 'w').write(icsText) + + +class BaseJsonPlugin(BasePlugin, JsonSObj): + def save(self):## json file self.file is read-only + pass + + +class DummyExternalPlugin(BasePlugin): + name = 'external' ## FIXME + external = True + loaded = False + enable = False + show_date = False + about = '' + authors = [] + hasConfig = False + hasImage = False + __repr__ = lambda self: 'loadPlugin(%r, enable=False, show_date=False)'%self.file + def __init__(self, _file, title): + self.file = _file + self.title = title + + +def loadExternalPlugin(_file, **data): + _file = getPlugPath(_file) + fname = split(_file)[-1] + if not isfile(_file): + log.error('plugin file "%s" not found! maybe removed?'%_file) + #try: + # plugIndex.remove( + return None #????????????????????????? + ##plug = BaseJsonPlugin(_file, mode=0, title='Failed to load plugin', enable=enable, show_date=show_date) + ##plug.external = True + ##return plug + ### + direc = dirname(_file) + name = splitext(fname)[0] + ### + if not data.get('enable'): + return DummyExternalPlugin( + _file, + pluginsTitleByName.get(name, name), + ) + ### + try: + mainFile = data['mainFile'] + except KeyError: + log.error('invalid external plugin "%s"'%_file) + return + ### + mainFile = getPlugPath(mainFile) + ### + pyEnv = { + '__file__': mainFile, + 'BasePlugin': BasePlugin, + 'BaseJsonPlugin': BaseJsonPlugin, + } + try: + exec(open(mainFile).read(), pyEnv) + except: + log.error('error while loading external plugin "%s"'%_file) + myRaiseTback() + return + ### + try: + cls = pyEnv['TextPlugin'] + except KeyError: + log.error('invalid external plugin "%s", no TextPlugin class'%_file) + return + ### + try: + plugin = cls(_file) + except: + log.error('error while loading external plugin "%s"'%_file) + myRaiseTback() + return + + #sys.path.insert(0, direc) + #try: + # mod = __import__(name) + #except: + # myRaiseTback() + # return None + #finally: + # sys.path.pop(0) + ## mod.module_init(rootDir, ) ## FIXME + #try: + # plugin = mod.TextPlugin(_file) + #except: + # myRaiseTback() + # #print(dir(mod)) + # return + plugin.external = True + plugin.setData(data) + plugin.onCurrentDateChange(localtime()[:3]) + return plugin + + + +@registerPlugin +class HolidayPlugin(BaseJsonPlugin): + name = 'holiday' + def __init__(self, _file): + BaseJsonPlugin.__init__( + self, + _file, + ) + self.lastDayMerge = True ## FIXME + self.holidays = {} + + def setData(self, data): + if 'holidays' in data: + for modeName in data['holidays']: + try: + mode = calTypes.names.index(modeName) + except ValueError: + continue + self.holidays[mode] = data['holidays'][modeName] + del data['holidays'] + else: + log.error('no "holidays" key in holiday plugin "%s"'%self.file) + ### + BaseJsonPlugin.setData(self, data) + def update_cell(self, c): + if not c.holiday: + for mode in self.holidays: + y, m, d = c.dates[mode] + for hm, hd in self.holidays[mode]: + if m==hm: + if d==hd: + c.holiday = True + break + elif self.lastDayMerge and d==hd-1 and hd>=calTypes[mode].minMonthLen: + ny, nm, nd = jd_to(c.jd+1, mode) + if (ny, nm) > (y, m): + c.holiday = True + break + def exportToIcs(self, fileName, startJd, endJd): + currentTimeStamp = strftime(icsTmFormat) + icsText = icsHeader + for jd in range(startJd, endJd): + isHoliday = False + for mode in self.holidays.keys(): + myear, mmonth, mday = jd_to(jd, mode) + if (mmonth, mday) in self.holidays[mode]: + isHoliday = True + break + if isHoliday: + gyear, gmonth, gday = jd_to(jd, DATE_GREG) + gyear_next, gmonth_next, gday_next = jd_to(jd+1, DATE_GREG) + ####### + icsText += 'BEGIN:VEVENT\n' + icsText += 'CREATED:%s\n'%currentTimeStamp + icsText += 'LAST-MODIFIED:%s\n'%currentTimeStamp + icsText += 'DTSTART;VALUE=DATE:%.4d%.2d%.2d\n'%(gyear, gmonth, gday) + icsText += 'DTEND;VALUE=DATE:%.4d%.2d%.2d\n'%(gyear_next, gmonth_next, gday_next) + icsText += 'CATEGORIES:Holidays\n' + icsText += 'TRANSP:TRANSPARENT\n' + ## TRANSPARENT because being in holiday time, does not make you busy! + ## see http://www.kanzaki.com/docs/ical/transp.html + icsText += 'SUMMARY:%s\n'%_('Holiday') + icsText += 'END:VEVENT\n' + icsText += 'END:VCALENDAR\n' + open(fileName, 'w').write(icsText) + #def getJdList(self, startJd, endJd): + + + + + +@registerPlugin +class YearlyTextPlugin(BaseJsonPlugin): + name = 'yearlyText' + params = BaseJsonPlugin.params + ( + 'dataFile', + ) + def __init__(self, _file): + BaseJsonPlugin.__init__( + self, + _file, + ) + self.dataFile = '' + def setData(self, data): + if 'dataFile' in data: + self.dataFile = getPlugPath(data['dataFile']) + del data['dataFile'] + else: + log.error('no "dataFile" key in yearly text plugin "%s"'%self.file) + #### + BaseJsonPlugin.setData(self, data) + def clear(self): + self.yearlyData = [] + def load(self): + #print('YearlyTextPlugin(%s).load()'%self._file) + yearlyData = [] + for j in range(12): + monthDb = [] + for k in range(calTypes[self.mode].maxMonthLen): + monthDb.append('') + yearlyData.append(monthDb) + ## last item is a dict of dates (y, m, d) and the description of day: + yearlyData.append({}) + ext = splitext(self.dataFile)[1].lower() + if ext == '.txt': + sep = '\t' + lines = open(self.dataFile).read().split('\n') + for line in lines[1:]: + line = line.strip() + if not line: + continue + if line[0]=='#': + continue + parts = line.split('\t') + if len(parts)<2: + log.error('bad plugin data line: %s'%line) + continue + date = parts[0].split('/') + text = '\t'.join(parts[1:]) + if len(date)==3: + y = int(date[0]) + m = int(date[1]) + d = int(date[2]) + yearlyData[12][(y, m, d)] = text + elif len(date)==2: + m = int(date[0]) + d = int(date[1]) + yearlyData[m-1][d-1] = text + else: + raise IOError('Bad line in database %s:\n%s'%(self.dataFile, line)) + else: + raise ValueError('invalid plugin dataFile extention "%s"'%ext) + self.yearlyData = yearlyData + def get_text(self, year, month, day): + yearlyData = self.yearlyData + if not yearlyData: + return '' + mode = self.mode + text = '' + #if mode!=calTypes.primary: + # year, month, day = convert(year, month, day, calTypes.primary, mode) + try: + text = yearlyData[month-1][day-1] + except:## KeyError or IndexError + pass + else: + if self.show_date and text: + text = '%s %s: %s'%( + _(day), + getMonthName(mode, month), + text, + ) + try: + text2 = yearlyData[12][(year, month, day)] + except:## KeyError or IndexError + pass + else: + if text: + text += '\n' + if self.show_date: + text2 = '%s %s %s: %s'%( + _(day), + getMonthName(mode, month, year), + _(year), + text2, + ) + + text += text2 + return text + + +@registerPlugin +class IcsTextPlugin(BasePlugin): + name = 'ics' + def __init__(self, _file, enable=True, show_date=False, all_years=False): + title = splitext(_file)[0] + self.ymd = None + self.md = None + self.all_years = all_years + BasePlugin.__init__( + self, + _file, + mode=DATE_GREG, + title=title, + enable=enable, + show_date=show_date, + ) + def clear(self): + self.ymd = None + self.md = None + def load(self): + lines = open(self.fpath).read().replace('\r', '').split('\n') + n = len(lines) + i = 0 + while True: + try: + if lines[i]=='BEGIN:VEVENT': + break + except IndexError: + log.error('bad ics file "%s"'%self.fpath) + return + i += 1 + SUMMARY = '' + DESCRIPTION = '' + DTSTART = None + DTEND = None + if self.all_years: + md = {} + while True: + i += 1 + try: + line = lines[i] + except IndexError: + break + if line=='END:VEVENT': + if SUMMARY and DTSTART and DTEND: + text = SUMMARY + if DESCRIPTION: + text += '\n%s'%DESCRIPTION + for (y, m, d) in ymdRange(DTSTART, DTEND): + md[(m, d)] = text + else: + log.error('unsupported ics event, SUMMARY=%s, DTSTART=%s, DTEND=%s'%( + SUMMARY, + DTSTART, + DTEND, + )) + SUMMARY = '' + DESCRIPTION = '' + DTSTART = None + DTEND = None + elif line.startswith('SUMMARY:'): + SUMMARY = line[8:].replace('\\,', ',').replace('\\n', '\n') + elif line.startswith('DESCRIPTION:'): + DESCRIPTION = line[12:].replace('\\,', ',').replace('\\n', '\n') + elif line.startswith('DTSTART;'): + #if not line.startswith('DTSTART;VALUE=DATE;'): + # log.error('unsupported ics line: %s'%line) + # continue + date = line.split(':')[-1] + #if len(date)!=8: + # log.error('unsupported ics line: %s'%line) + # continue + try: + DTSTART = (int(date[:4]), int(date[4:6]), int(date[6:8])) + except: + log.error('unsupported ics line: %s'%line) + myRaise() + continue + elif line.startswith('DTEND;'): + #if not line.startswith('DTEND;VALUE=DATE;'): + # log.error('unsupported ics line: %s'%line) + # continue + date = line.split(':')[-1] + #if len(date)!=8: + # log.error('unsupported ics line: %s'%line) + # continue + try: + DTEND = (int(date[:4]), int(date[4:6]), int(date[6:8])) + except: + log.error('unsupported ics line: %s'%line) + myRaise() + continue + self.ymd = None + self.md = md + else:## not self.all_years + ymd = {} + while True: + i += 1 + try: + line = lines[i] + except IndexError: + break + if line=='END:VEVENT': + if SUMMARY and DTSTART and DTEND: + text = SUMMARY + if DESCRIPTION: + text += '\n%s'%DESCRIPTION + for (y, m, d) in ymdRange(DTSTART, DTEND): + ymd[(y, m, d)] = text + SUMMARY = '' + DESCRIPTION = '' + DTSTART = None + DTEND = None + elif line.startswith('SUMMARY:'): + SUMMARY = line[8:].replace('\\,', ',').replace('\\n', '\n') + elif line.startswith('DESCRIPTION:'): + DESCRIPTION = line[12:].replace('\\,', ',').replace('\\n', '\n') + elif line.startswith('DTSTART;'): + #if not line.startswith('DTSTART;VALUE=DATE;'): + # log.error('unsupported ics line: %s'%line) + # continue + date = line.split(':')[-1] + #if len(date)!=8: + # log.error('unsupported ics line: %s'%line) + # continue + try: + DTSTART = (int(date[:4]), int(date[4:6]), int(date[6:8])) + except: + log.error('unsupported ics line: %s'%line) + myRaise() + continue + elif line.startswith('DTEND;'): + #if not line.startswith('DTEND;VALUE=DATE;'): + # log.error('unsupported ics line: %s'%line) + # continue + date = line.split(':')[-1] + #if len(date)!=8: + # log.error('unsupported ics line: %s'%line) + # continue + try: + DTEND = (int(date[:4]), int(date[4:6]), int(date[6:8])) + except: + log.error('unsupported ics line: %s'%line) + myRaise() + continue + self.ymd = ymd + self.md = None + def get_text(self, y, m, d): + if self.ymd: + if (y, m, d) in self.ymd: + if self.show_date: + return '%s %s %s: %s'%(_(d), getMonthName(self.mode, m), + _(y), self.ymd[(y, m, d)]) + else: + return self.ymd[(y, m, d)] + if self.md: + if (m, d) in self.md: + if self.show_date: + return '%s %s %s: %s'%( + _(d), + getMonthName(self.mode, m), + _(y), + self.ymd[(y, m, d)], + ) + else: + return self.md[(m, d)] + return '' + def open_configure(self): + pass + def open_about(self): + pass + +## class EveryDayTextPlugin(BaseJsonPlugin): +## class RandomTextPlugin(BaseJsonPlugin): + + + +def loadPlugin(_file=None, **kwargs): + if not _file: + log.error('plugin file is empty!') + return + _file = getPlugPath(_file) + if not isfile(_file): + log.error('error while loading plugin "%s": no such file!\n'%_file) + return + ext = splitext(_file)[1].lower() + #### + ## should ics plugins require a json file too? + ## FIXME + if ext == '.ics': + return IcsTextPlugin(_file, **kwargs) + #### + if ext != '.json': + log.error('unsupported plugin extention %s, new style plugins have a json file'%ext) + return + try: + text = open(_file).read() + except Exception as e: + log.error('error while reading plugin file "%s": %s'%(_file, e)) + return + try: + data = jsonToData(text) + except Exception as e: + log.error('invalid json file "%s"'%_file) + return + #### + data.update(kwargs) ## FIXME + #### + try: + name = data['type'] + except KeyError: + log.error('invalid plugin "%s", no "type" key'%_file) + return + #### + if name == 'external': + return loadExternalPlugin(_file, **data) + #### + try: + cls = pluginClassByName[name] + except: + log.error('invald plugin type "%s" in file "%s"'%(name, _file)) + return + #### + for param in cls.essentialParams: + if not data.get(param): + log.error('invalid plugin "%s": parameter "%s" is missing'%(_file, param)) + return + #### + plug = cls(_file) + plug.setData(data) + #### + return plug + + + + + + + + + diff --git a/scal3/s_object.py b/scal3/s_object.py new file mode 100644 index 000000000..2baee0ed0 --- /dev/null +++ b/scal3/s_object.py @@ -0,0 +1,278 @@ +import os +from os.path import isfile, join +from time import time as now +from collections import OrderedDict + +from hashlib import sha1 +from bson import BSON + +from scal3.path import objectDir +from scal3.os_utils import makeDir +from scal3.json_utils import * +from scal3.utils import myRaise + +dataToJson = dataToPrettyJson +#from scal3.core import dataToJson## FIXME + + +class SObj: + @classmethod + def getSubclass(cls, _type): + return cls + ### + params = ()## used in getData and setData and copyFrom + canSetDataMultipleTimes = True + __nonzero__ = lambda self: self.__bool__() + ### + def __bool__(self): + raise NotImplementedError + def copyFrom(self, other): + from copy import deepcopy + for attr in self.params: + try: + value = getattr(other, attr) + except AttributeError: + continue + setattr( + self, + attr, + deepcopy(value), + ) + def copy(self): + newObj = self.__class__() + newObj.copyFrom(self) + return newObj + getData = lambda self:\ + dict([(param, getattr(self, param)) for param in self.params]) + def setData(self, data): + if not self.__class__.canSetDataMultipleTimes: + if getattr(self, 'dataIsSet', False): + raise RuntimeError( + 'can not run setData multiple times for %s instance'%\ + self.__class__.__name__ + ) + self.dataIsSet = True + ########### + #if isinstance(data, dict):## FIXME + for key, value in data.items(): + if key in self.params: + setattr(self, key, value) + def getIdPath(self): + try: + parent = self.parent + except AttributeError: + raise NotImplementedError('%s.getIdPath: no parent attribute'%self.__class__.__name__) + try: + _id = self.id + except AttributeError: + raise NotImplementedError('%s.getIdPath: no id attribute'%self.__class__.__name__) + ###### + path = [] + if _id is not None: + path.append(_id) + if parent is None: + return path + else: + return parent.getIdPath() + path + def getPath(self): + parent = self.parent + if parent is None: + return [] + index = parent.index(self.id) + return parent.getPath() + [index] + + +def makeOrderedData(data, params): + if isinstance(data, dict): + if params: + data = list(data.items()) + def paramIndex(key): + try: + return params.index(key) + except ValueError: + return len(params) + data.sort(key=lambda x: paramIndex(x[0])) + data = OrderedDict(data) + return data + +def getSortedDict(data): + return OrderedDict(sorted(data.items())) + +class JsonSObj(SObj): + canSetDataMultipleTimes = False + skipLoadExceptions = False + skipLoadNoFile = False + file = '' + ### + @classmethod + def getFile(cls, _id=None): + return cls.file + ### + @classmethod + def load(cls, *args): + _file = cls.getFile(*args) + data = {} + if isfile(_file): + try: + jsonStr = open(_file).read() + data = jsonToData(jsonStr) + except Exception as e: + if not cls.skipLoadExceptions: + raise e + else: + if not cls.skipLoadNoFile: + raise FileNotFoundError('%s : file not found'%_file) + try: + _type = data['type'] + except (KeyError, TypeError): + subCls = cls + else: + subCls = cls.getSubclass(_type) + obj = subCls(*args) + obj.setData(data) + return obj + ##### + paramsOrder = () + getDataOrdered = lambda self: makeOrderedData(self.getData(), self.paramsOrder) + getJson = lambda self: dataToJson(self.getDataOrdered()) + setJson = lambda self, jsonStr: self.setData(jsonToData(jsonStr)) + def save(self): + if self.file: + jstr = self.getJson() + open(self.file, 'w').write(jstr) + else: + print('save method called for object %r while file is not set'%self) + def setData(self, data): + SObj.setData(self, data) + self.setModifiedFromFile() + def setModifiedFromFile(self): + if hasattr(self, 'modified'): + try: + self.modified = int(os.stat(self.file).st_mtime) + except OSError: + pass + #else: + # print('no modified param for object %r'%self) + + +def saveBsonObject(data): + data = getSortedDict(data) + bsonBytes = bytes(BSON.encode(data)) + _hash = sha1(bsonBytes).hexdigest() + dpath = join(objectDir, _hash[:2]) + fpath = join(dpath, _hash[2:]) + if not isfile(fpath): + makeDir(dpath) + open(fpath, 'wb').write(bsonBytes) + return _hash + +def loadBsonObject(_hash): + fpath = join(objectDir, _hash[:2], _hash[2:]) + bsonBytes = open(fpath, 'rb').read() + if _hash != sha1(bsonBytes).hexdigest(): + raise IOError('sha1 diggest does not match for object file "%s"'%fpath) + return BSON.decode(bsonBytes) + + +class BsonHistObj(SObj): + canSetDataMultipleTimes = False + skipLoadExceptions = False + skipLoadNoFile = False + file = '' + ## basicParams or noHistParams ? FIXME + basicParams = ( + ) + @classmethod + def load(cls, *args): + _file = cls.getFile(*args) + data = {} + if not isfile(_file): + if not cls.skipLoadNoFile: + raise FileNotFoundError('%s : file not found'%_file) + try: + jsonStr = open(_file).read() + data = jsonToData(jsonStr) + except Exception as e: + if not cls.skipLoadExceptions: + raise e + try: + _type = data['type'] + except (KeyError, TypeError): + subCls = cls + else: + subCls = cls.getSubclass(_type) + obj = subCls(*args) + obj.setData(data) + return obj + ####### + getDataOrdered = lambda self: makeOrderedData(self.getData(), self.paramsOrder) + def loadBasicData(self): + if not isfile(self.file): + return {} + return jsonToData(open(self.file).read()) + def loadHistory(self): + lastBasicData = self.loadBasicData() + try: + return lastBasicData['history'] + except KeyError: + if lastBasicData: + print('no "history" in json file "%s"'%self.file) + return [] + def saveBasicData(self, basicData): + jsonStr = dataToJson(basicData) + open(self.file, 'w').write(jsonStr) + def save(self, *histArgs): + ''' + returns last history record: (lastEpoch, lastHash, **args) + ''' + if not self.file: + raise RuntimeError('save method called for object %r while file is not set'%self) + data = self.getData() + basicData = {} + for param in self.basicParams: + try: + basicData[param] = data.pop(param) + except KeyError: + pass + try: + data.pop('modified') + except KeyError: + pass + _hash = saveBsonObject(data) + ### + history = self.loadHistory() + ### + try: + lastHash = history[0][1] + except IndexError: + lastHash = None + if _hash != lastHash:## or lastHistArgs != histArgs:## FIXME + tm = now() + history.insert(0, [tm, _hash] + list(histArgs)) + self.modified = tm + basicData['history'] = history + self.saveBasicData(basicData) + return history[0] + def setData(self, data): + history = data.pop('history')## we don't keep the history in memory + lastHistRecord = history[0] + lastEpoch = lastHistRecord[0] + lastHash = lastHistRecord[1] + #### + data.update(loadBsonObject(lastHash)) + SObj.setData(self, data) + self.modified = int(lastEpoch) + return lastHistRecord + +def updateBasicDataFromBson(data, filePath, fileType): + ''' + fileType: 'event' | 'group' | 'account'..., display only, does not matter much + ''' + try: + lastHash = data['history'][0][1] + except (KeyError, IndexError): + raise ValueError('invalid %s file "%s", no "history"'%(fileType, filePath)) + data.update(loadBsonObject(lastHash)) + + + diff --git a/scal3/season.py b/scal3/season.py new file mode 100644 index 000000000..10d24be99 --- /dev/null +++ b/scal3/season.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +avgYearLen = 365.24219 +springRefJd = 2456372.4597222223 + + +def getSeasonValueFromJd(jd): + return ((jd-springRefJd) % avgYearLen) / avgYearLen * 4.0 + +def getSpringJdAfter(fromJd): + d, m = divmod(fromJd - 1 - springRefJd, avgYearLen) + return int(fromJd + (d + 1) * avgYearLen) + +def getSeasonNamePercentFromJd(jd): + d, m = divmod(getSeasonValueFromJd(jd), 1) + name = [ + 'Spring', + 'Summer', + 'Autumn', + 'Winter', + ][int(d)] + return name, m + + +def test(): + from scal3.cal_types.jalali import to_jd as jalali_to_jd + for year in range(1390, 1400): + #for month in (1, 4, 7, 10): + for month in (1,): + s = getSeasonFromJd(jalali_to_jd(year, month, 1)) + print('%.4d/%.2d/01\t%.5f'%(year, month, s)) + #print + + +if __name__=='__main__': + test() diff --git a/scal3/show_object.py b/scal3/show_object.py new file mode 100644 index 000000000..3e26a3a2f --- /dev/null +++ b/scal3/show_object.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +import json +from pprint import pprint +from scal3.s_object import loadBsonObject + +dataToPrettyJson = lambda data: json.dumps( + data, + sort_keys=True, + indent=4, + ensure_ascii=False, +) + +if __name__=='__main__': + for arg in sys.argv[1:]: + data = loadBsonObject(arg) + #pprint(data, indent=4, width=80) + print(dataToPrettyJson(data)) + print('-------------------') + + diff --git a/scal3/startup.py b/scal3/startup.py new file mode 100644 index 000000000..e602b4913 --- /dev/null +++ b/scal3/startup.py @@ -0,0 +1,63 @@ +from os.path import isfile, isdir + +from scal3.path import * +from scal3.os_utils import makeDir + +from scal3 import core +from scal3.core import osName + +comDeskDir = '%s/.config/autostart'%homeDir +comDesk = '%s/%s.desktop'%(comDeskDir, APP_NAME) +#kdeDesk='%s/.kde/Autostart/%s.desktop'%(homeDir, APP_NAME) + + +def addStartup(): + if osName=='win': + from scal3.windows import winMakeShortcut + makeDir(winStartupDir) + #fname = APP_NAME + ('-qt' if uiName=='qt' else '') + '.pyw' + fname = core.COMMAND + '.pyw' + fpath = join(rootDir, fname) + #open(winStartupFile, 'w').write('execfile(%r, {"__file__":%r})'%(fpath, fpath)) + try: + winMakeShortcut(fpath, winStartupFile) + except: + return False + else: + return True + elif isdir('%s/.config'%homeDir):## osName in ('linux', 'mac') ## maybe Gnome/KDE on Solaris, *BSD, ... + text = '''[Desktop Entry] +Type=Application +Name=%s %s +Icon=%s +Exec=%s'''%(core.APP_DESC, core.VERSION, APP_NAME, core.COMMAND)## double quotes needed when the exec path has space + makeDir(comDeskDir) + try: + fp = open(comDesk, 'w') + except: + core.myRaise(__file__) + return False + else: + fp.write(text) + return True + elif osName=='mac':## FIXME + pass + return False + + +def removeStartup(): + if osName=='win':## FIXME + if isfile(winStartupFile): + os.remove(winStartupFile) + elif isfile(comDesk): + os.remove(comDesk) + +def checkStartup(): + if osName=='win': + return isfile(winStartupFile) + elif isfile(comDesk): + return True + return False + + + diff --git a/scal3/time_line_tree.py b/scal3/time_line_tree.py new file mode 100644 index 000000000..84b3a1d9b --- /dev/null +++ b/scal3/time_line_tree.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# Also avalable in /usr/share/common-licenses/LGPL on Debian systems +# or /usr/share/licenses/common/LGPL/license.txt on ArchLinux + +import sys +from math import log + +from scal3.interval_utils import ab_overlaps +from scal3.time_utils import * + +#maxLevel = 1 +#minLevel = 1 + +class Node: + def __init__(self, base, level, offset, rightOri): + #global maxLevel, minLevel + self.base = base ## 8 or 16 is better + self.level = level ## base ** level is the mathematical scope of the node (with its children) + #if level > maxLevel: + # maxLevel = level + # print('maxLevel =', level) + #if level < minLevel: + # minLevel = level + # print('minLevel =', level) + self.offset = offset ## in days + self.rightOri = rightOri ## FIXME + ### + width = base ** level + if rightOri: + self.s0, self.s1 = offset, offset + width + else: + self.s0, self.s1 = offset - width, offset + ### + self.clear() + def clear(self): + self.children = {} ## possible keys are 0 to base-1 for right node, or -(base-1) to 0 for left node + self.events = [] ## list of tuples (rel_start, rel_end, event_id) + sOverlaps = lambda self, t0, t1: ab_overlaps(t0, t1, self.s0, self.s1) + def search(self, t0, t1):## t0 < t1 + ''' + returns a generator to iterate over (ev_t0, ev_t1, eid, ev_dt) s + ''' + ## t0 and t1 are absolute. not relative to the self.offset + if not self.sOverlaps(t0, t1): + raise StopIteration + for ev_rt0, ev_rt1, eid in self.events: + ev_t0 = ev_rt0 + self.offset + ev_t1 = ev_rt1 + self.offset + if ab_overlaps(t0, t1, ev_t0, ev_t1): + yield ( + max(t0, ev_t0), + min(t1, ev_t1), + eid, + ev_rt1 - ev_rt0, + ) + for child in self.children.values(): + for item in child.search(t0, t1): + yield item + def getChild(self, tm): + if not self.s0 <= tm <= self.s1: + raise RuntimeError('Node.getChild: Out of scope (level=%s, offset=%s, rightOri=%s'% + (self.level, self.offset, self.rightOri)) + dt = self.base ** (self.level - 1) + index = int((tm-self.offset) // dt) + try: + return self.children[index] + except KeyError: + child = self.children[index] = self.__class__( + self.base, + self.level-1, + self.offset + index * dt, + self.rightOri, + ) + return child + def newParent(self): + parent = self.__class__( + self.base, + self.level+1, + self.offset, + self.rightOri, + ) + parent.children[0] = self + return parent + def getDepth(self): + if self.children: + return 1 + max([c.getDepth() for c in self.children.values()]) + else: + return 0 + + +class TimeLineTree: + def __init__(self, offset=0, base=4): + ## base 4 and 8 are the best (about speed of both add and search) + self.base = base + self.offset = offset + self.clear() + def clear(self): + self.right = Node(self.base, 1, self.offset, True) + self.left = Node(self.base, 1, self.offset, False) + self.byEvent = {} + def search(self, t0, t1): + if self.offset < t1: + for item in self.right.search(t0, t1): + yield item + if t0 < self.offset: + for item in self.left.search(t0, t1): + yield item + def add(self, t0, t1, eid, debug=False): + if debug: + from time import strftime, localtime + f = '%F, %T' + print('%s.add: %s\t%s'%( + self.__class__.__name__, + strftime(f, localtime(t0)), + strftime(f, localtime(t1)), + )) + if self.offset <= t0: + isRight = True + node = self.right + elif t0 < self.offset < t1: + self.add(t0, self.offset, eid) + self.add(self.offset, t1, eid) + return + elif t1 <= self.offset: + isRight = False + node = self.left + else: + raise RuntimeError + ######## + while True: + if node.s0 <= t0 < node.s1 and node.s0 < t1 <= node.s1: + break + node = node.newParent() + ## now `node` is the new side (left/right) node + if isRight: + self.right = node + else: + self.left = node + while True: + child = node.getChild(t0) + if child.s0 <= t1 <= child.s1: + node = child + else: + break + ## now `node` is the node that event should be placed in + ev_tuple = (t0-node.offset, t1-node.offset, eid) + node.events.append(ev_tuple) + try: + self.byEvent[eid].append((node, ev_tuple)) + except KeyError: + self.byEvent[eid] = [(node, ev_tuple)] + def delete(self, eid): + try: + refList = self.byEvent.pop(eid) + except KeyError: + return 0 + n = len(refList) + for node, ev_tuple in refList: + try: + node.events.remove(ev_tuple) + except ValueError: + continue + #if not node.events: + # node.parent.removeChild(node) + return n + def getLastOfEvent(self, eid): + try: + node, ev_tuple = self.byEvent[eid][-1] + ## self.byEvent is sorted by time? FIXME + except KeyError as IndexError: + return None + return ev_tuple[0], ev_tuple[1] + def getFirstOfEvent(self, eid): + try: + node, ev_tuple = self.byEvent[eid][0] + except KeyError as IndexError: + return None + return ev_tuple[0], ev_tuple[1] + getDepth = lambda self: 1 + max( + self.left.getDepth(), + self.right.getDepth(), + ) + +#if __name__=='__main__': +# from scal3 import ui +# from time import time as now +# ui.eventGroups = event_lib.EventGroupsHolder.load() +# for group in ui.eventGroups: +# t0 = now() +# group.updateOccurrenceNode() +# print(now()-t0, group.title) + + + diff --git a/scal3/time_utils.py b/scal3/time_utils.py new file mode 100644 index 000000000..d8b5fe2b5 --- /dev/null +++ b/scal3/time_utils.py @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import time +from time import localtime, mktime +from time import time as now +from datetime import datetime + +import natz +import natz.local + +from scal3.cal_types.gregorian import J0001, J1970 +from scal3.cal_types.gregorian import jd_to as jd_to_g +from scal3.utils import ifloor, iceil + +## jd is the integer value of Chronological Julian Day, which is specific to time zone +## but epoch time is based on UTC, and not location-dependent + +## Time Zone is different from UTC Offset +## Time Zone is a result of UTC Offset + DST + +## Using datetime module is not preferred because of year limitation (1 to 9999) in TimeLine +## Just use for testing and comparing the result +## Or put in try...except block + +## now() ~~ epoch +## function time.time() having the same name as its module is problematic +## don't use time.time() directly again (other than once) + +#def getUtcOffsetByEpoch(epoch): +# try: +# return (datetime.fromtimestamp(epoch) - datetime.utcfromtimestamp(epoch)).total_seconds() +# except ValueError:## year is out of range +# return 0 + +def getUtcOffsetByEpoch(epoch, tz=None): + if not tz: + tz = natz.local.get_localzone() + delta = 0 + while True: + try: + return tz.utcoffset(datetime.fromtimestamp(epoch + delta)).total_seconds() + except natz.AmbiguousTimeError:## FIXME + #d = datetime.fromtimestamp(epoch+3600) + #print('AmbiguousTimeError', d.year, d.month, d.day, d.hour, d.minute, d.second) + delta += 3600 + print('delta = %s'%delta) + except ( + ValueError, + OverflowError, + ): + return tz._utcoffset.total_seconds() + + +def getUtcOffsetByDateSec(year, month, day, tz=None): + if not tz: + tz = natz.local.get_localzone() + try: + return tz.utcoffset(datetime(year, month, day)).total_seconds() + except (ValueError, OverflowError): + return tz._utcoffset.total_seconds() + except natz.NonExistentTimeError: + return tz.utcoffset(datetime(year, month, day, 1, 0, 0)).total_seconds() + + +def getUtcOffsetByDateHM(year, month, day, tz=None): + s = getUtcOffsetByDateSec(year, month, day, tz) + return divmod(s/60, 60) + +def getUtcOffsetByDateHMS(year, month, day, tz=None): + s = getUtcOffsetByDateSec(year, month, day, tz) + m, s = divmod(s, 60) + h, m = divmod(m, 60) + return (h, m, s) + +#getUtcOffsetByJd = lambda jd, tz=None: getUtcOffsetByEpoch(getEpochFromJd(jd), tz) + +def getUtcOffsetByJd(jd, tz=None): + y, m, d = jd_to_g(jd) + return getUtcOffsetByDateSec(y, m, d, tz) + + +getUtcOffsetCurrent = lambda tz=None: getUtcOffsetByEpoch(now(), tz) +#getUtcOffsetCurrent = lambda: -time.altzone if time.daylight and localtime().tm_isdst else -time.timezone + +getGtkTimeFromEpoch = lambda epoch: int((epoch-1.32171528839e+9)*1000 // 1) + + +getFloatJdFromEpoch = lambda epoch, tz=None: \ + (epoch + getUtcOffsetByEpoch(epoch, tz)) / (24.0*3600) + J1970 +#getFloatJdFromEpoch = lambda epoch: datetime.fromtimestamp(epoch).toordinal() - 1 + J0001 + + +getJdFromEpoch = lambda epoch, tz=None: ifloor(getFloatJdFromEpoch(epoch, tz)) + + +def getEpochFromJd(jd, tz=None): + localEpoch = (jd-J1970) * 24*3600 + year, month, day = jd_to_g(jd-1) ## FIXME + return localEpoch - getUtcOffsetByDateSec(year, month, day, tz) + +#getEpochFromJd = lambda jd: int(mktime(datetime.fromordinal(int(jd)-J0001+1).timetuple())) + +roundEpochToDay = lambda epoch: getEpochFromJd(round(getFloatJdFromEpoch(epoch))) + +def getJdListFromEpochRange(startEpoch, endEpoch): + startJd = getJdFromEpoch(startEpoch) + endJd = getJdFromEpoch(endEpoch-0.01) + 1 + return list(range(startJd, endJd)) + +def getHmsFromSeconds(second): + minute, second = divmod(int(second), 60) + hour, minute = divmod(minute, 60) + return hour, minute, second + +def getJhmsFromEpoch(epoch, currentOffset=False, tz=None): + ## return a tuple (julain_day, hour, minute, second) from epoch + offset = getUtcOffsetCurrent(tz) if currentOffset else getUtcOffsetByEpoch(epoch, tz) ## FIXME + days, second = divmod(ifloor(epoch + offset), 24*3600) + return (days+J1970,) + getHmsFromSeconds(second) + +getSecondsFromHms = lambda hour, minute, second=0: hour*3600 + minute*60 + second + +getEpochFromJhms = lambda jd, hour, minute, second, tz=None: \ + getEpochFromJd(jd, tz) + hour*3600 + minute*60 + second + +def getJdAndSecondsFromEpoch(epoch):## return a tuple (julain_day, extra_seconds) from epoch + days, second = divmod(epoch, 24*3600) + return (days + J1970, second) + + + +durationUnitsRel = ( + (1, 'second'), + (60, 'minute'), + (60, 'hour'), + (24, 'day'), + (7, 'week'), +) + +durationUnitsAbs = [] +num = 1 +for item in durationUnitsRel: + num *= item[0] + durationUnitsAbs.append((num, item[1])) + +durationUnitValueToName = dict(durationUnitsAbs) +durationUnitValues = [item[0] for item in durationUnitsAbs] +durationUnitNames = [item[1] for item in durationUnitsAbs] + + +def timeEncode(tm, checkSec=False): + if len(tm)==2: + tm = tm + (0,) + if checkSec: + if len(tm)==3 and tm[2]>0: + return '%.2d:%.2d:%.2d'%tuple(tm) + else: + return '%.2d:%.2d'%tuple(tm[:2]) + else: + return '%.2d:%.2d:%.2d'%tuple(tm) + +def simpleTimeEncode(tm): + if len(tm)==1: + return '%d'%tm + elif len(tm)==2: + if tm[1]==0: + return '%d'%tm[0] + else: + return '%d:%.2d'%tm + elif len(tm)==3: + if tm[1]==0: + if tm[2]==0: + return '%d'%tm[0] + else: + return '%d:%.2d:%.2d'%tm + else: + return '%d:%.2d:%.2d'%tm + +def timeDecode(st): + parts = st.split(':') + try: + tm = tuple([int(p) for p in parts]) + except ValueError: + raise ValueError('bad time %s'%st) + if len(tm)==1: + tm += (0, 0) + elif len(tm)==2: + tm += (0,) + elif len(tm)!=3: + raise ValueError('bad time %s'%st) + return tm + +hmEncode = lambda hm: '%.2d:%.2d'%tuple(hm) + +def hmDecode(st): + parts = st.split(':') + if len(parts)==1: + return (int(parts[0]), 0) + elif len(parts)==2: + return (int(parts[0]), int(parts[1])) + else: + raise ValueError('bad hour:minute time %s'%st) + + +hmsRangeToStr = lambda h1, m1, s1, h2, m2, s2: timeEncode((h1, m1, s1), True) + ' - ' + timeEncode((h2, m2, s2), True) + +def epochGregDateTimeEncode(epoch, tz=None): + jd, hour, minute, second = getJhmsFromEpoch(epoch, tz) + year, month, day = jd_to_g(jd) + return '%.4d/%.2d/%.2d %.2d:%.2d:%.2d'%(year, month, day, hour, minute, second) + +encodeJd = lambda jd: epochGregDateTimeEncode(getEpochFromJd(jd)) + +def durationEncode(value, unit): + iValue = int(value) + if iValue==value: + value = iValue + return '%s %s'%(value, durationUnitValueToName[unit]) + +def durationDecode(durStr): + durStr = durStr.strip() + if ' ' in durStr: + value, unit = durStr.split(' ') + value = float(value) + unit = unit.lower() + if not unit: + return (value, 1) + for unitValue, unitName in durationUnitsAbs: + if unit in (unitName, unitName+'s'):## ,unitName[0] + return (value, unitValue) + raise ValueError('invalid duration %r'%durStr) + + +timeToFloatHour = lambda h, m, s=0: h + m/60.0 + s/3600.0 + +def floatHourToTime(fh): + h, r = divmod(fh, 1) + m, r = divmod(r*60, 1) + return ( + int(h), + int(m), + int(r*60), + ) + +if __name__=='__main__': + #print(floatHourToTime(3.6)) + for tm in ( + (8, 0, 0), + (8, 0), + (8,), + (8, 30), + (8, 30, 55), + (8, 0, 10), + ): + print('%r, %r'%(tm, simpleTimeEncode(tm))) + + + + + + diff --git a/scal3/timeline.py b/scal3/timeline.py new file mode 100644 index 000000000..f593f6d18 --- /dev/null +++ b/scal3/timeline.py @@ -0,0 +1,372 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from math import log10 + +from scal3.time_utils import getEpochFromJd, getJdFromEpoch, getFloatJdFromEpoch, getJhmsFromEpoch, getUtcOffsetCurrent +from scal3.date_utils import jwday, getEpochFromDate +from scal3.cal_types import calTypes, jd_to, to_jd +from scal3.timeline_box import * +from scal3.locale_man import tr as _ +from scal3.locale_man import rtl, numEncode, textNumEncode, addLRM + +from scal3 import core +from scal3.core import myRaise, getMonthName, jd_to_primary + +from scal3.color_utils import hslToRgb +from scal3.utils import ifloor, iceil, toBytes +from scal3 import ui +from scal3.ui import getHolidaysJdList + +#################################################### + +bgColor = ui.bgColor ## FIXME +fgColor = ui.textColor ## FIXME +baseFontSize = 8 + +majorStepMin = 50 ## with label +minorStepMin = 5 ## with or without label +maxLabelWidth = 60 ## or the same majorStepMin +baseTickHeight = 1 +baseTickWidth = 0.5 +maxTickWidth = 20 +maxTickHeightRatio = 0.3 +labelYRatio = 1.1 + +currentTimeMarkerHeightRatio = 0.3 +currentTimeMarkerWidth = 2 +currentTimeMarkerColor = (255, 100, 100) + +#sunLightH = 10## FIXME + + +showWeekStart = True +showWeekStartMinDays = 1 +showWeekStartMaxDays = 60 +weekStartTickColor = (0, 200, 0) + +changeHolidayBg = False +changeHolidayBgMinDays = 1 +changeHolidayBgMaxDays = 60 +holidayBgBolor = (60, 35, 35) + + +scrollZoomStep = 1.2 ## > 1 +keyboardZoomStep = 1.2 ## > 1 + +############################################# + +enableAnimation = False +movingStaticStep = 20 +movingUpdateTime = 10 ## milisecons + +movingV0 = 0 + +## Force is the same as Acceleration, assuming Mass == 1 + +## different for keyboard (arrows) and mouse (scroll) FIXME +movingHandForce = 1100 ## px / (sec**2) +movingHandSmallForce = 900 ## px / (sec**2) + +movingFrictionForce = 600 ## px / (sec**2) +## movingHandForce > movingFrictionForce + +movingMaxSpeed = 1200 ## px / sec +## movingMaxSpeed = movingAccel * 4 +## reach to maximum speed in 4 seconds + + + +movingKeyTimeoutFirst = 0.5 +movingKeyTimeout = 0.1 ## seconds ## continiouse keyPress delay is about 0.05 sec + +############################################# +truncateTickLabel = False + +## 0: no rotation +## 1: 90 deg CCW (if needed) +## -1: 90 deg CW (if needed) + +#################################################### + +fontFamily = ui.getFont()[0] + +dayLen = 24 * 3600 +minYearLenSec = 365 * dayLen +avgMonthLen = 30 * dayLen + +unitSteps = ( + (3600, 12), + (3600, 6), + (3600, 3), + (3600, 1), + (60, 30), + (60, 15), + (60, 5), + (60, 1), + (1, 30), + (1, 15), + (1, 5), + (1, 1), +) + + +class Tick: + def __init__(self, epoch, pos, unitSize, label, color=None): + self.epoch = epoch + self.pos = pos ## pixel position + self.height = unitSize ** 0.5 * baseTickHeight + self.width = min(unitSize ** 0.2 * baseTickWidth, maxTickWidth) + self.fontSize = unitSize ** 0.1 * baseFontSize + self.maxLabelWidth = min(unitSize*0.5, maxLabelWidth) ## FIXME + self.label = label + if color is None: + color = fgColor + self.color = color + + + + +#class Range: +# def __init__(self, start, end): +# self.start = start +# self.end = end +# dt = lambda self: self.end - self.start +# __cmp__ = lambda self, other: cmp(self.dt(), other.dt()) + + + +def getNum10FactPow(n): + if n == 0: + return 0, 1 + n = str(int(n)) + nozero = n.rstrip('0') + return int(nozero), len(n) - len(nozero) + +getNum10Pow = lambda n: getNum10FactPow(n)[1] + +def getYearRangeTickValues(u0, y1, minStepYear): + data = {} + step = 10 ** max(0, ifloor(log10(y1 - u0)) - 1) + u0 = step * (u0//step) + for y in range(u0, y1, step): + n = 10 ** getNum10Pow(y) + if n >= minStepYear: + data[y] = n + if u0 <= 0 <= y1: + data[0] = max(data.values()) + return sorted(data.items()) + +def formatYear(y, prettyPower=False): + if abs(y) < 10 ** 4:## FIXME + y_st = _(y) + else: + #y_st = textNumEncode('%.0E'%y, changeDot=True)## FIXME + fac, pw = getNum10FactPow(y) + if not prettyPower or abs(fac) >= 100:## FIXME + y_e = '%E'%y + for i in range(10): + y_e = y_e.replace('0E', 'E') + y_e = y_e.replace('.E', 'E') + y_st = textNumEncode(y_e, changeDot=True) + else: + sign = ('-' if fac < 0 else '') + fac = abs(fac) + if fac == 1: + fac_s = '' + else: + fac_s = '%s×'%_(fac) + pw_s = _(10) + 'ˆ' + _(pw) + ## pw_s = _(10) + '' + _(pw) + ''## Pango Markup Language + y_st = sign + fac_s + pw_s + return addLRM(y_st) + +#def setRandomColorsToEvents(): +# import random +# events = ui.events[:] +# random.shuffle(events) +# dh = 360.0/len(events) +# hue = 0 +# for event in events: +# event.color = hslToRgb(hue, boxColorSaturation, boxColorLightness) +# hue += dh + +def calcTimeLineData(timeStart, timeWidth, pixelPerSec, borderTm): + timeEnd = timeStart + timeWidth + jd0 = getJdFromEpoch(timeStart) + jd1 = getJdFromEpoch(timeEnd) + widthDays = float(timeWidth) / dayLen + dayPixel = dayLen * pixelPerSec ## px + #print('dayPixel = %s px'%dayPixel) + getEPos = lambda epoch: (epoch-timeStart)*pixelPerSec + getJPos = lambda jd: (getEpochFromJd(jd)-timeStart)*pixelPerSec + ######################## Holidays + holidays = [] + if changeHolidayBg and changeHolidayBgMinDays < widthDays < changeHolidayBgMaxDays: + for jd in getHolidaysJdList(jd0, jd1+1): + holidays.append(getJPos(jd)) + ######################## Ticks + ticks = [] + tickEpochList = [] + minStep = minorStepMin / pixelPerSec ## second + ################# + year0, month0, day0 = jd_to_primary(jd0) + year1, month1, day1 = jd_to_primary(jd1) + ############ Year + minStepYear = minStep // minYearLenSec ## years ## int or iceil? + yearPixel = minYearLenSec * pixelPerSec ## pixels + for (year, size) in getYearRangeTickValues(year0, year1+1, minStepYear): + tmEpoch = getEpochFromDate(year, 1, 1, calTypes.primary) + if tmEpoch in tickEpochList: + continue + unitSize = size * yearPixel + label = formatYear(year) if unitSize >= majorStepMin else '' + ticks.append(Tick( + tmEpoch, + getEPos(tmEpoch), + unitSize, + label, + )) + tickEpochList.append(tmEpoch) + ############ Month + monthPixel = avgMonthLen * pixelPerSec ## px + minMonthUnit = float(minStep) / avgMonthLen ## month + if minMonthUnit <= 3: + for ym in range(year0*12+month0-1, year1*12+month1-1+1):## +1 FIXME + if ym%3==0: + monthUnit = 3 + else: + monthUnit = 1 + if monthUnit < minMonthUnit: + continue + y, m = divmod(ym, 12) ; m+=1 + tmEpoch = getEpochFromDate(y, m, 1, calTypes.primary) + if tmEpoch in tickEpochList: + continue + unitSize = monthPixel * monthUnit + ticks.append(Tick( + tmEpoch, + getEPos(tmEpoch), + unitSize, + getMonthName(calTypes.primary, m) if unitSize >= majorStepMin else '', + )) + tickEpochList.append(tmEpoch) + ################ + if showWeekStart and showWeekStartMinDays < widthDays < showWeekStartMaxDays: + wd0 = jwday(jd0) + jdw0 = jd0 + (core.firstWeekDay - wd0) % 7 + unitSize = dayPixel * 7 + if unitSize < majorStepMin: + label = '' + else: + label = core.weekDayNameAb[core.firstWeekDay] + for jd in range(jdw0, jd1+1, 7): + tmEpoch = getEpochFromJd(jd) + ticks.append(Tick( + tmEpoch, + getEPos(tmEpoch), + unitSize, + label, + color=weekStartTickColor, + )) + #tickEpochList.append(tmEpoch) + ############ Day of Month + hasMonthName = timeWidth < 5 * dayLen + minDayUnit = float(minStep) / dayLen ## days + if minDayUnit <= 15: + for jd in range(jd0, jd1+1): + tmEpoch = getEpochFromJd(jd) + if tmEpoch in tickEpochList: + continue + year, month, day = jd_to_primary(jd) + if day==16: + dayUnit = 15 + elif day in (6, 11, 21, 26): + dayUnit = 5 + else: + dayUnit = 1 + if dayUnit < minDayUnit: + continue + unitSize = dayPixel*dayUnit + if unitSize < majorStepMin: + label = '' + elif hasMonthName: + label = _(day) + ' ' + getMonthName(calTypes.primary, month) + else: + label = _(day) + ticks.append(Tick( + tmEpoch, + getEPos(tmEpoch), + unitSize, + label, + )) + tickEpochList.append(tmEpoch) + ############ Hour, Minute, Second + for stepUnit, stepValue in unitSteps: + stepSec = stepUnit*stepValue + if stepSec < minStep: + break + unitSize = stepSec*pixelPerSec + utcOffset = int(getUtcOffsetCurrent()) + firstEpoch = iceil((timeStart+utcOffset) / stepSec) * stepSec - utcOffset + for tmEpoch in range(firstEpoch, iceil(timeEnd), stepSec): + if tmEpoch in tickEpochList: + continue + if unitSize < majorStepMin: + label = '' + else: + jd, h, m, s = getJhmsFromEpoch(tmEpoch) + if s==0: + label = '%s:%s'%( + _(h), + _(m, fillZero=2), + ) + else:# elif timeWidth < 60 or stepSec < 30: + label = addLRM('%s"'%_(s, fillZero=2)) + #else: + # label = '%s:%s:%s'%( + # _(h), + # _(m, fillZero=2), + # _(s, fillZero=2), + # ) + ticks.append(Tick( + tmEpoch, + getEPos(tmEpoch), + unitSize, + label, + )) + tickEpochList.append(tmEpoch) + ######################## Event Boxes + data = { + 'holidays': holidays, + 'ticks': ticks, + 'boxes': [], + } + ### + data['boxes'] = calcEventBoxes( + timeStart, + timeEnd, + pixelPerSec, + borderTm, + ) + ### + return data + + + diff --git a/scal3/timeline_box.py b/scal3/timeline_box.py new file mode 100644 index 000000000..5b08f6117 --- /dev/null +++ b/scal3/timeline_box.py @@ -0,0 +1,233 @@ +from time import time as now + +from scal3.locale_man import tr as _ +from scal3.core import debugMode +from scal3 import ui + + +movableEventTypes = ( + 'task', + 'lifeTime', +) + +######################################### + +boxLineWidth = 2 +boxInnerAlpha = 0.1 + +boxMoveBorder = 10 +boxMoveLineW = 0.5 + +editingBoxHelperLineWidth = 0.3 ## px + + +#boxColorSaturation = 1.0 +#boxColorLightness = 0.3 ## for random colors + + +boxReverseGravity = False + +boxSkipPixelLimit = 0.1 ## pixels + +rotateBoxLabel = -1 + +######################################### + +class Box: + def __init__( + self, + t0, + t1, + odt, + u0, + du, + text='', + color=None, + ids=None, + lineW=2, + ): + self.t0 = t0 + self.t1 = t1 + self.odt = odt ## original delta t + #self.mt = (t0+t1)/2.0 ## - timeMiddle ## FIXME + #self.dt = (t1-t0)/2.0 + #if t1-t0 != odt: + # print('Box, dt=%s, odt=%s'%(t1-t0, odt)) + self.u0 = u0 + self.du = du + #### + self.x = None + self.w = None + self.y = None + self.h = None + #### + self.text = text + if color is None: + color = ui.textColor ## FIXME + self.color = color + self.ids = ids ## (groupId, eventId) + self.lineW = lineW + #### + self.hasBorder = False + self.tConflictBefore = [] + mt_key = lambda self: self.mt + dt_key = lambda self: -self.dt + ######### + def setPixelValues(self, timeStart, pixelPerSec, beforeBoxH, maxBoxH): + self.x = (self.t0 - timeStart) * pixelPerSec + self.w = (self.t1 - self.t0) * pixelPerSec + self.y = beforeBoxH + maxBoxH * self.u0 + self.h = maxBoxH * self.du + contains = lambda self, px, py: 0 <= px-self.x < self.w and 0 <= py-self.y < self.h + +def makeIntervalGraph(boxes): + try: + from scal3.graph_utils import Graph + except ImportError: + return + g = Graph() + n = len(boxes) + g.add_vertices(n - g.vcount()) + g.vs['name'] = list(range(n)) + #### + points = [] ## (time, isStart, boxIndex) + for boxI, box in enumerate(boxes): + points += [ + (box.t0, True, boxI), + (box.t1, False, boxI), + ] + points.sort() + openBoxes = set() + for t, isStart, boxI in points: + if isStart: + g.add_edges([ + (boxI, oboxI) for oboxI in openBoxes + ]) + openBoxes.add(boxI) + else: + openBoxes.remove(boxI) + return g + + + + +def renderBoxesByGraph(boxes, graph, minColor, minU): + colorCount = max(graph.vs['color']) - minColor + 1 + if colorCount < 1: + return + du = (1.0-minU) / colorCount + min_vertices = graph.vs.select(color_eq=minColor) ## a VertexSeq + for v in min_vertices: + box = boxes[v['name']] + box_du = du * v['color_h'] + box.u0 = minU if boxReverseGravity else 1 - minU - box_du + box.du = box_du + graph.delete_vertices(min_vertices) + for sgraph in graph.decompose(): + renderBoxesByGraph( + boxes, + sgraph, + minColor + 1, + minU + du, + ) + + +def calcEventBoxes( + timeStart, + timeEnd, + pixelPerSec, + borderTm, +): + try: + from scal3.graph_utils import Graph, colorGraph + except ImportError: + errorBoxH = 0.8 ## FIXME + return [ + Box( + timeStart, + timeEnd, + timeEnd - timeStart, + 1-errorBoxH, ## u0 + errorBoxH, ## du + text = 'Install "python3-igraph" to see events', + color = (128, 0, 0),## FIXME + lineW = 2*boxLineWidth, + ) + ] + boxesDict = {} + #timeMiddle = (timeStart + timeEnd) / 2.0 + for groupIndex in range(len(ui.eventGroups)): + group = ui.eventGroups.byIndex(groupIndex) + if not group.enable: + continue + if not group.showInTimeLine: + continue + for t0, t1, eid, odt in group.occur.search(timeStart-borderTm, timeEnd+borderTm): + pixBoxW = (t1-t0) * pixelPerSec + if pixBoxW < boxSkipPixelLimit: + continue + #if not isinstance(eid, int): + # print('----- bad eid from search: %r'%eid) + # continue + event = group[eid] + eventIndex = group.index(eid) + if t0 <= timeStart and timeEnd <= t1:## Fills Range ## FIXME + continue + lineW = boxLineWidth + if lineW >= 0.5*pixBoxW: + lineW = 0 + box = Box( + t0, + t1, + odt, + 0, + 1, + text = event.getSummary(), + color = group.color,## or event.color FIXME + ids = (group.id, event.id) if pixBoxW > 0.5 else None, + lineW = lineW, + ) + box.hasBorder = (borderTm > 0 and event.name in movableEventTypes) + boxValue = (group.id, t0, t1) + try: + boxesDict[boxValue].append(box) + except KeyError: + boxesDict[boxValue] = [box] + ### + if debugMode: + t0 = now() + boxes = [] + for bvalue, blist in boxesDict.items(): + if len(blist) < 4: + boxes += blist + else: + box = blist[0] + box.text = _('%s events')%_(len(blist)) + box.ids = None + #print('len(blist) = %s'%len(blist)) + #print('%s secs'%(box.t1 - box.t0)) + boxes.append(box) + del boxesDict + ##### + if not boxes: + return [] + ##### + if debugMode: + t1 = now() + ### + graph = makeIntervalGraph(boxes) + if debugMode: + print('makeIntervalGraph: %e'%(now()-t1)) + ### + ##### + colorGraph(graph) + renderBoxesByGraph(boxes, graph, 0, 0) + if debugMode: + print('box placing time: %e'%(now()-t0)) + print('') + return boxes + + + + + diff --git a/scal3/ui.py b/scal3/ui.py new file mode 100644 index 000000000..233837044 --- /dev/null +++ b/scal3/ui.py @@ -0,0 +1,976 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from time import time as now + +import sys, os, os.path +from os import listdir +from os.path import dirname, join, isfile, splitext, isabs + +from scal3.utils import NullObj, cleanCacheDict, myRaise, myRaiseTback +from scal3.utils import toBytes +from scal3.json_utils import * +from scal3.path import * + +from scal3.cal_types import calTypes, jd_to + +from scal3 import locale_man +from scal3.locale_man import tr as _ +from scal3.locale_man import numDecode + +from scal3 import core + +from scal3 import event_lib +from scal3.event_diff import EventDiff + +uiName = '' +null = NullObj() + + +####################################################### + +sysConfPath = join(sysConfDir, 'ui.json') ## also includes LIVE config + +confPath = join(confDir, 'ui.json') + +confPathCustomize = join(confDir, 'ui-customize.json') + +confPathLive = join(confDir, 'ui-live.json') + +confParams = ( + 'showMain', + 'winTaskbar', + 'useAppIndicator', + 'showDigClockTr', + 'fontCustomEnable', + 'fontCustom', + 'bgUseDesk', + 'bgColor', + 'borderColor', + 'cursorOutColor', + 'cursorBgColor', + 'todayCellColor', + 'textColor', + 'holidayColor', + 'inactiveColor', + 'borderTextColor', + 'cursorDiaFactor', + 'cursorRoundingFactor', + 'statusIconImage', + 'statusIconImageHoli', + 'statusIconFontFamilyEnable', + 'statusIconFontFamily', + 'statusIconFixedSizeEnable', + 'statusIconFixedSizeWH', + 'maxDayCacheSize', + 'pluginsTextStatusIcon', + #'localTzHist',## FIXME + 'showYmArrows', + 'prefPagesOrder', +) + +confParamsLive = ( + 'winX', + 'winY', + 'winWidth', + 'winKeepAbove', + 'winSticky', + 'pluginsTextIsExpanded', + 'eventViewMaxHeight', + 'bgColor', + 'eventManPos',## FIXME + 'eventManShowDescription',## FIXME + 'localTzHist', + 'wcal_toolbar_weekNum_negative', +) + +confParamsCustomize = ( + 'mainWinItems', + 'winControllerButtons', + 'mcalHeight', + 'mcalLeftMargin', + 'mcalTopMargin', + 'mcalTypeParams', + 'mcalGrid', + 'mcalGridColor', + 'wcalHeight', + 'wcalTextSizeScale', + 'wcalItems', + 'wcalGrid', + 'wcalGridColor', + 'wcal_toolbar_mainMenu_icon', + 'wcal_weekDays_width', + 'wcalFont_weekDays', + 'wcalFont_pluginsText', + 'wcal_eventsIcon_width', + 'wcal_eventsText_showDesc', + 'wcal_eventsText_colorize', + 'wcalFont_eventsText', + 'wcal_daysOfMonth_dir', + 'wcalTypeParams', + 'wcal_daysOfMonth_width', + 'wcal_eventsCount_expand', + 'wcal_eventsCount_width', + 'wcalFont_eventsBox', + 'dcalHeight', + 'dcalTypeParams', + 'pluginsTextInsideExpander', + 'ud__wcalToolbarData', + 'ud__mainToolbarData', +) + +def loadConf(): + loadModuleJsonConf(__name__) + loadJsonConf(__name__, confPathCustomize) + loadJsonConf(__name__, confPathLive) + +def saveConf(): + saveModuleJsonConf(__name__) + +def saveConfCustomize(): + saveJsonConf(__name__, confPathCustomize, confParamsCustomize) + +def saveLiveConf():## rename to saveConfLive + if core.debugMode: + print('saveLiveConf', winX, winY, winWidth) + saveJsonConf(__name__, confPathLive, confParamsLive) + +def saveLiveConfLoop():## rename to saveConfLiveLoop + tm = now() + if tm-lastLiveConfChangeTime > saveLiveConfDelay: + saveLiveConf() + return False ## Finish loop + return True ## Continue loop + + +####################################################### + +def parseDroppedDate(text): + part = text.split('/') + if len(part)==3: + try: + part[0] = numDecode(part[0]) + part[1] = numDecode(part[1]) + part[2] = numDecode(part[2]) + except: + myRaise(__file__) + return None + maxPart = max(part) + if maxPart > 999: + minMax = ( + (1000, 2100), + (1, 12), + (1, 31), + ) + formats = ( + [0, 1, 2], + [1, 2, 0], + [2, 1, 0], + ) + for format in formats: + for i in range(3): + valid = True + f = format[i] + if not (minMax[f][0] <= part[i] <= minMax[f][1]): + valid = False + #print('format %s was not valid, part[%s]=%s'%(format, i, part[i])) + break + if valid: + year = part[format.index(0)] ## "format" must be list because of method "index" + month = part[format.index(1)] + day = part[format.index(2)] + break + else: + valid = 0 <= part[0] <= 99 and 1 <= part[1] <= 12 and 1 <= part[2] <= 31 + ### + year = 2000 + part[0] ## FIXME + month = part[1] + day = part[2] + if not valid: + return None + else: + return None + ##??????????? when drag from a persian GtkCalendar with format %y/%m/%d + #if year < 100: + # year += 2000 + return (year, month, day) + +def dictsTupleConfStr(data): + n = len(data) + st = '(' + for i in range(n): + d = data[i].copy() + st += '\n{' + for k in d.keys(): + v = d[k] + if isinstance(k, str): + ks = '\'%s\''%k + else: + ks = str(k) + if isinstance(v, str): + vs = '\'%s\''%v + else: + vs = str(v) + st += '%s:%s, '%(ks,vs) + if i==n-1: + st = st[:-2] + '})' + else: + st = st[:-2] + '},' + return st + + +def checkNeedRestart(): + for key in needRestartPref.keys(): + if needRestartPref[key] != eval(key): + print('"%s", "%s", "%s"'%(key, needRestartPref[key], eval(key))) + return True + return False + +getPywPath = lambda: join(rootDir, core.APP_NAME + ('-qt' if uiName=='qt' else '') + '.pyw') + + +def dayOpenEvolution(arg=None): + from subprocess import Popen + ##y, m, d = jd_to(cell.jd-1, core.DATE_GREG) ## in gnome-cal opens prev day! why?? + y, m, d = cell.dates[core.DATE_GREG] + Popen('LANG=en_US.UTF-8 evolution calendar:///?startdate=%.4d%.2d%.2d'%(y, m, d), shell=True)## FIXME + ## 'calendar:///?startdate=%.4d%.2d%.2dT120000Z'%(y, m, d) + ## What "Time" pass to evolution? like gnome-clock: T193000Z (19:30:00) / Or ignore "Time" + ## evolution calendar:///?startdate=$(date +"%Y%m%dT%H%M%SZ") + +def dayOpenSunbird(arg=None): + from subprocess import Popen + ## does not work on latest version of Sunbird ## FIXME + ## and Sunbird seems to be a dead project + ## Opens previous day in older version + y, m, d = cell.dates[core.DATE_GREG] + Popen('LANG=en_US.UTF-8 sunbird -showdate %.4d/%.2d/%.2d'%(y, m, d), shell=True) + +## How do this with KOrginizer? FIXME + +####################################################################### + + +class Cell:## status and information of a cell + #ocTimeMax = 0 + #ocTimeCount = 0 + #ocTimeSum = 0 + def __init__(self, jd): + self.eventsData = [] + #self.eventsDataIsSet = False ## not used + self.pluginsText = '' + ### + self.jd = jd + date = core.jd_to_primary(jd) + self.year, self.month, self.day = date + self.weekDay = core.jwday(jd) + self.weekNum = core.getWeekNumber(self.year, self.month, self.day) + #self.weekNumNeg = self.weekNum + 1 - core.getYearWeeksCount(self.year) + self.weekNumNeg = self.weekNum - int(calTypes.primaryModule().avgYearLen / 7) + self.holiday = (self.weekDay in core.holidayWeekDays) + ################### + self.dates = [ + date if mode==calTypes.primary else jd_to(jd, mode) + for mode in range(len(calTypes)) + ] + ''' + self.dates = dict([ + ( + mode, date if mode==calTypes.primary else jd_to(jd, mode) + ) + for mode in calTypes.active + ]) + ''' + ################### + for k in core.plugIndex: + plug = core.allPlugList[k] + if plug: + try: + plug.update_cell(self) + except: + myRaiseTback() + ################### + #t0 = now() + self.eventsData = event_lib.getDayOccurrenceData(jd, eventGroups)## here? FIXME + #dt = now() - t0 + #Cell.ocTimeSum += dt + #Cell.ocTimeCount += 1 + #Cell.ocTimeMax = max(Cell.ocTimeMax, dt) + def format(self, binFmt, mode=None, tm=null):## FIXME + if mode is None: + mode = calTypes.primary + pyFmt, funcs = binFmt + return pyFmt%tuple(f(self, mode, tm) for f in funcs) + inSameMonth = lambda self, other:\ + self.dates[calTypes.primary][:2] == other.dates[calTypes.primary][:2] + def getEventIcons(self, showIndex): + iconList = [] + for item in self.eventsData: + if not item['show'][showIndex]: + continue + icon = item['icon'] + if icon and not icon in iconList: + iconList.append(icon) + return iconList + getDayEventIcons = lambda self: self.getEventIcons(0) + getWeekEventIcons = lambda self: self.getEventIcons(1) + getMonthEventIcons = lambda self: self.getEventIcons(2) + + + +class CellCache: + def __init__(self): + self.jdCells = {} ## a mapping from julan_day to Cell instance + self.plugins = {} + self.weekEvents = {} + def clear(self): + global cell, todayCell + self.jdCells = {} + self.weekEvents = {} + cell = self.getCell(cell.jd) + todayCell = self.getCell(todayCell.jd) + def registerPlugin(self, name, setParamsCallable, getCellGroupCallable): + """ + setParamsCallable(cell): cell.attr1 = value1 .... + getCellGroupCallable(cellCache, *args): return cell_group + call cellCache.getCell(jd) inside getCellGroupFunc + """ + self.plugins[name] = ( + setParamsCallable, + getCellGroupCallable, + ) + for localCell in self.jdCells.values(): + setParamsCallable(localCell) + def getCell(self, jd): + try: + return self.jdCells[jd] + except KeyError: + return self.buildCell(jd) + def getTmpCell(self, jd):## don't keep, no eventsData, no plugin params + try: + return self.jdCells[jd] + except KeyError: + return Cell(jd) + getCellByDate = lambda self, y, m, d: self.getCell(core.primary_to_jd(y, m, d)) + getTodayCell = lambda self: self.getCell(core.getCurrentJd()) + def buildCell(self, jd): + localCell = Cell(jd) + for pluginData in self.plugins.values(): + pluginData[0](localCell) + self.jdCells[jd] = localCell + cleanCacheDict(self.jdCells, maxDayCacheSize, jd) + return localCell + getCellGroup = lambda self, pluginName, *args:\ + self.plugins[pluginName][1](self, *args) + def getWeekData(self, absWeekNumber): + cells = self.getCellGroup('WeekCal', absWeekNumber) + try: + wEventData = self.weekEvents[absWeekNumber] + except KeyError: + wEventData = event_lib.getWeekOccurrenceData(absWeekNumber, eventGroups) + cleanCacheDict(self.weekEvents, maxWeekCacheSize, absWeekNumber) + self.weekEvents[absWeekNumber] = wEventData + return cells, wEventData + #def getMonthData(self, year, month):## needed? FIXME + + +def changeDate(year, month, day, mode=None): + global cell + if mode is None: + mode = calTypes.primary + cell = cellCache.getCell(core.to_jd(year, month, day, mode)) + +def gotoJd(jd): + global cell + cell = cellCache.getCell(jd) + +def jdPlus(plus=1): + global cell + cell = cellCache.getCell(cell.jd + plus) + +def getMonthPlus(tmpCell, plus): + year, month = core.monthPlus(tmpCell.year, tmpCell.month, plus) + day = min(tmpCell.day, core.getMonthLen(year, month, calTypes.primary)) + return cellCache.getCellByDate(year, month, day) + +def monthPlus(plus=1): + global cell + cell = getMonthPlus(cell, plus) + +def yearPlus(plus=1): + global cell + year = cell.year + plus + month = cell.month + day = min(cell.day, core.getMonthLen(year, month, calTypes.primary)) + cell = cellCache.getCellByDate(year, month, day) + +def getFont(scale=1.0): + (name, bold, underline, size) = fontCustom if fontCustomEnable else fontDefaultInit + return [name, bold, underline, size*scale] + +def initFonts(fontDefaultNew): + global fontDefault, fontCustom, mcalTypeParams + fontDefault = fontDefaultNew + if not fontCustom: + fontCustom = fontDefault + ######## + ### + if mcalTypeParams[0]['font']==None: + mcalTypeParams[0]['font'] = getFont(1.0) + ### + for item in mcalTypeParams[1:]: + if item['font']==None: + item['font'] = getFont(0.6) + ###### + if dcalTypeParams[0]['font']==None: + dcalTypeParams[0]['font'] = getFont(10.0) + ### + for item in dcalTypeParams[1:]: + if item['font']==None: + item['font'] = getFont(3.0) + + +def getHolidaysJdList(startJd, endJd): + jdList = [] + for jd in range(startJd, endJd): + tmpCell = cellCache.getTmpCell(jd) + if tmpCell.holiday: + jdList.append(jd) + return jdList + + +###################################################################### + +def checkMainWinItems(): + global mainWinItems + #print(mainWinItems) + ## cleaning and updating mainWinItems + names = set([name for (name, i) in mainWinItems]) + defaultNames = set([name for (name, i) in mainWinItemsDefault]) + #print(mainWinItems) + #print(sorted(list(names))) + #print(sorted(list(defaultNames))) + ##### + ## removing items that are no longer supported + mainWinItems, mainWinItemsTmp = [], mainWinItems + for name, enable in mainWinItemsTmp: + if name in defaultNames: + mainWinItems.append((name, enable)) + ##### + ## adding items newly added in this version, this is for user's convenience + newNames = defaultNames.difference(names) + #print('mainWinItems: newNames =', newNames) + ## + name = 'winContronller' + if name in newNames: + mainWinItems.insert(0, (name, True)) + newNames.remove(name) + ## + for name in newNames: + mainWinItems.append((name, False))## FIXME + + +def deleteEventGroup(group): + eventGroups.moveToTrash(group, eventTrash) + +def moveEventToTrash(group, event): + eventIndex = group.remove(event) + group.save() + eventTrash.insert(0, event)## or append? FIXME + eventTrash.save() + return eventIndex + +def moveEventToTrashFromOutside(group, event): + global reloadGroups, reloadTrash + moveEventToTrash(group, event) + reloadGroups.append(group.id) + reloadTrash = True + +getEvent = lambda groupId, eventId: eventGroups[groupId][eventId] + +def duplicateGroupTitle(group): + title = toBytes(group.title) + titleList = [toBytes(g.title) for g in eventGroups] + parts = title.split('#') + try: + index = int(parts[-1]) + title = '#'.join(parts[:-1]) + except: + #myRaise() + index = 1 + index += 1 + while True: + newTitle = title + '#%d'%index + if newTitle not in titleList: + group.title = newTitle + return + index += 1 + + +def init(): + global todayCell, cell, eventAccounts, eventGroups + core.init() + #### Load accounts, groups and trash? FIXME + eventAccounts = event_lib.EventAccountsHolder.load() + eventGroups = event_lib.EventGroupsHolder.load() + #### + todayCell = cell = cellCache.getTodayCell() ## FIXME + + + +###################################################################### + +localTzHist = [ + str(core.localTz), +] + +shownCals = [] ## FIXME + +mcalTypeParams = [ + { + 'pos': (0, -2), + 'font': None, + 'color': (220, 220, 220), + }, + { + 'pos': (18, 5), + 'font': None, + 'color': (165, 255, 114), + }, + { + 'pos': (-18, 4), + 'font': None, + 'color': (0, 200, 205), + }, +] + +wcalTypeParams = [ + {'font': None}, + {'font': None}, + {'font': None}, +] + +dcalTypeParams = [## FIXME + { + 'pos': (0, -12), + 'font': None, + 'color': (220, 220, 220), + }, + { + 'pos': (125, 30), + 'font': None, + 'color': (165, 255, 114), + }, + { + 'pos': (-125, 24), + 'font': None, + 'color': (0, 200, 205), + }, +] + + +getActiveMonthCalParams = lambda: list(zip( + calTypes.active, + mcalTypeParams, +)) + + +getActiveDayCalParams = lambda: list(zip( + calTypes.active, + dcalTypeParams, +)) + + +################################ +tagsDir = join(pixDir, 'event') + +class TagIconItem: + def __init__(self, name, desc='', icon='', eventTypes=()): + self.name = name + if not desc: + desc = name.capitalize() + self.desc = _(desc) + if icon: + if not isabs(icon): + icon = join(tagsDir, icon) + else: + iconTmp = join(tagsDir, name) + '.png' + if isfile(iconTmp): + icon = iconTmp + self.icon = icon + self.eventTypes = eventTypes + self.usage = 0 + __repr__ = lambda self: 'TagIconItem(%r, desc=%r, icon=%r, eventTypes=%r)'%( + self.name, + self.desc, + self.icon, + self.eventTypes, + ) + + +eventTags = ( + TagIconItem('birthday', eventTypes=('yearly',)), + TagIconItem('marriage', eventTypes=('yearly',)), + TagIconItem('obituary', eventTypes=('yearly',)), + TagIconItem('note', eventTypes=('dailyNote',)), + TagIconItem('task', eventTypes=('task',)), + TagIconItem('alarm'), + TagIconItem('business'), + TagIconItem('personal'), + TagIconItem('favorite'), + TagIconItem('important'), + TagIconItem('appointment', eventTypes=('task',)), + TagIconItem('meeting', eventTypes=('task',)), + TagIconItem('phone_call', desc='Phone Call', eventTypes=('task',)), + TagIconItem('university', eventTypes=('task',)),## FIXME + TagIconItem('education'), + TagIconItem('holiday'), + TagIconItem('travel'), +) + +getEventTagsDict = lambda: dict([(tagObj.name, tagObj) for tagObj in eventTags]) +eventTagsDesc = dict([(t.name, t.desc) for t in eventTags]) + +################### +eventTrash = event_lib.EventTrash() +eventAccounts = [] +eventGroups = [] + +def iterAllEvents():## dosen't include orphan events + for group in eventGroups: + for event in group: + yield event + for event in eventTrash: + yield event + + + +changedGroups = []## list of groupId's +reloadGroups = [] ## a list of groupId's that their contents are changed +reloadTrash = False + +eventDiff = EventDiff() + + + +#def updateEventTagsUsage():## FIXME where to use? +# tagsDict = getEventTagsDict() +# for tagObj in eventTags: +# tagObj.usage = 0 +# for event in events:## FIXME +# for tag in event.tags: +# try: +# tagsDict[tag].usage += 1 +# except KeyError: +# pass + + +################### +## BUILD CACHE AFTER SETTING calTypes.primary +maxDayCacheSize = 100 ## maximum size of cellCache (days number) +maxWeekCacheSize = 12 + +cellCache = CellCache() +todayCell = cell = None +########################### +autoLocale = True +logo = join(pixDir, 'starcal.png') +########################### +#themeDir = join(rootDir, 'themes') +#theme = None +########################### Options ########################### +winWidth = 480 +mcalHeight = 250 +winTaskbar = False +useAppIndicator = True +showDigClockTb = True ## On Toolbar ## FIXME +showDigClockTr = True ## On Status Icon +#### +toolbarIconSizePixel = 24 ## used in pyqt ui +#### +bgColor = (26, 0, 1, 255)## or None +bgUseDesk = False +borderColor = (123, 40, 0, 255) +borderTextColor = (255, 255, 255, 255) ## text of weekDays and weekNumbers +#menuBgColor = borderColor ##??????????????? +textColor = (255, 255, 255, 255) +menuTextColor = None##borderTextColor##??????????????? +holidayColor = (255, 160, 0, 255) +inactiveColor = (255, 255, 255, 115) +todayCellColor = (0, 255, 0, 50) +########## +cursorOutColor = (213, 207, 0, 255) +cursorBgColor = (41, 41, 41, 255) +cursorDiaFactor = 0.15 +cursorRoundingFactor = 0.50 +mcalGrid = False +mcalGridColor = (255, 252, 0, 82) +########## +mcalLeftMargin = 30 +mcalTopMargin = 30 +#################### +wcalHeight = 200 +wcalTextSizeScale = 0.6 ## between 0 and 1 +#wcalTextColor = (255, 255, 255) ## FIXME +wcalPadding = 10 +wcalGrid = False +wcalGridColor = (255, 252, 0, 82) + +wcal_toolbar_mainMenu_icon = join(pixDir, 'starcal-24.png') +wcal_toolbar_mainMenu_icon_default = wcal_toolbar_mainMenu_icon +wcal_toolbar_weekNum_negative = False +wcal_weekDays_width = 80 +wcal_eventsCount_width = 80 +wcal_eventsCount_expand = False +wcal_eventsIcon_width = 50 +wcal_eventsText_showDesc = False +wcal_eventsText_colorize = True +wcal_daysOfMonth_width = 30 +wcal_daysOfMonth_dir = 'ltr' ## ltr/rtl/auto +wcalFont_eventsText = None +wcalFont_weekDays = None +wcalFont_pluginsText = None +wcalFont_eventsBox = None + + +##### just for compatibility +try: + wcal_weekDays_width = wcalWeekDaysWidth +except NameError: + pass +try: + wcal_eventsCount_width = wcalEventsCountColWidth +except NameError: + pass +try: + wcal_eventsCount_expand = wcalEventsCountExpand +except NameError: + pass +try: + wcal_eventsIcon_width = wcalEventsIconColWidth +except NameError: + pass +try: + wcal_eventsText_showDesc = wcalEventsTextShowDesc +except NameError: + pass +try: + wcal_eventsText_colorize = wcalEventsTextColorize +except NameError: + pass +try: + wcal_daysOfMonth_width = wcalDaysOfMonthColWidth +except NameError: + pass +try: + wcal_daysOfMonth_dir = wcalDaysOfMonthColDir +except NameError: + pass + +#################### +dcalHeight = 250 + + +#################### +boldYmLabel = True ## apply in Pref FIXME +showYmArrows = True ## apply in Pref FIXME +labelMenuDelay = 0.1 ## delay for shift up/down items of menu for right click on YearLabel +#################### +statusIconImage = join(rootDir, 'status-icons', 'dark-green.svg') +statusIconImageHoli = join(rootDir, 'status-icons', 'dark-red.svg') +statusIconImageDefault, statusIconImageHoliDefault = statusIconImage, statusIconImageHoli +statusIconFontFamilyEnable = False +statusIconFontFamily = None +statusIconFixedSizeEnable = False +statusIconFixedSizeWH = (24, 24) +#################### +menuActiveLabelColor = "#ff0000" +pluginsTextStatusIcon = False +pluginsTextInsideExpander = True +pluginsTextIsExpanded = True ## affect only if pluginsTextInsideExpander +eventViewMaxHeight = 200 +#################### +dragGetMode = core.DATE_GREG ## apply in Pref FIXME +#dragGetDateFormat = '%Y/%m/%d' +dragRecMode = core.DATE_GREG ## apply in Pref FIXME +#################### +monthRMenuNum = True +#monthRMenu +prefPagesOrder = tuple(range(5)) +winControllerButtons = ( + ('sep', True), + ('min', True), + ('max', False), + ('close', True), + ('sep', False), + ('sep', False), + ('sep', False), +) +winControllerSpacing = 0 +#################### +winKeepAbove = True +winSticky = True +winX = 0 +winY = 0 +### +fontDefault = ['Sans', False, False, 12] +fontDefaultInit = fontDefault +fontCustom = None +fontCustomEnable = False +##################### +showMain = True ## Show main window on start (or only goto statusIcon) +##################### +mainWinItems = ( + ('winContronller', True), + ('toolbar', True), + ('labelBox', True), + ('monthCal', False), + ('weekCal', True), + ('dayCal', False), + ('statusBar', True), + ('seasonPBar', True), + ('pluginsText', True), + ('eventDayView', True), +) + +mainWinItemsDefault = mainWinItems[:] + + +wcalItems = ( + ('toolbar', True), + ('weekDays', True), + ('pluginsText', True), + ('eventsIcon', True), + ('eventsText', True), + ('daysOfMonth', True), +) + +wcalItemsDefault = wcalItems[:] + +#################### + +ntpServers = ( + 'pool.ntp.org', + 'ir.pool.ntp.org', + 'asia.pool.ntp.org', + 'europe.pool.ntp.org', + 'north-america.pool.ntp.org', + 'oceania.pool.ntp.org', + 'south-america.pool.ntp.org', + 'ntp.ubuntu.com', +) + + +##################### +#dailyNoteChDateOnEdit = True ## change date of a dailyNoteEvent when editing it +eventManPos = (0, 0) +eventManShowDescription = True +##################### +focusTime = 0 +lastLiveConfChangeTime = 0 + + +saveLiveConfDelay = 0.5 ## seconds +timeout_initial = 200 +timeout_repeat = 50 + + +def updateFocusTime(*args): + global focusTime + focusTime = now() + + + +######################################################## + +loadConf() + +######################################################## + +if not isfile(statusIconImage): + statusIconImage = statusIconImageDefault +if not isfile(statusIconImageHoli): + statusIconImageHoli = statusIconImageHoliDefault + +try: + mcalGridColor = wcalGridColor = gridColor +except NameError: + pass + +try: + fontUseDefault +except NameError: + pass +else: + fontCustomEnable = not fontUseDefault ## for compatibilty + del fontUseDefault + +try: + localTzHist.remove(str(core.localTz)) +except ValueError: + pass +localTzHist.insert(0, str(core.localTz)) +saveLiveConf() + + +if shownCals:## just for compatibility + mcalTypeParams = [] + wcalTypeParams = [] + calTypes.activeNames = [] + for item in shownCals: + mcalTypeParams.append({ + 'pos': (item['x'], item['y']), + 'font': list(item['font']), + 'color': item['color'], + }) + wcalTypeParams.append({ + 'font': list(item['font']), + }) + calTypes.activeNames.append(calTypes.names[item['mode']]) + calTypes.update() + + + +needRestartPref = {} ### Right place ???????? +for key in ( + 'locale_man.lang', + 'locale_man.enableNumLocale', + 'winTaskbar', + 'showYmArrows', + 'useAppIndicator', +): + needRestartPref[key] = eval(key) + +if menuTextColor is None: + menuTextColor = borderTextColor + +################################## + +## move to gtk_ud ? FIXME +mainWin = None +prefDialog = None +eventManDialog = None +timeLineWin = None +weekCalWin = None + + + + + + diff --git a/scal3/ui_gtk/__init__.py b/scal3/ui_gtk/__init__.py new file mode 100644 index 000000000..1e66d38da --- /dev/null +++ b/scal3/ui_gtk/__init__.py @@ -0,0 +1,46 @@ +__all__ = [ + 'gtk', + 'gdk', + 'pack', + 'TWO_BUTTON_PRESS', + 'MenuItem', + 'ImageMenuItem', + 'getScrollValue', +] + +from gi.repository import Gtk as gtk +from gi.repository import Gdk as gdk +from gi.repository import GdkPixbuf + +def pack(box, child, expand=False, fill=False, padding=0): + if isinstance(box, gtk.Box): + box.pack_start(child, expand, fill, padding) + elif isinstance(box, gtk.CellLayout): + box.pack_start(child, expand) + else: + raise TypeError('pack: unkown type %s'%type(box)) + +TWO_BUTTON_PRESS = getattr(gdk.EventType, '2BUTTON_PRESS') + +class MenuItem(gtk.MenuItem): + def __init__(self, *args, **kwargs): + gtk.MenuItem.__init__(self, *args, **kwargs) + self.set_use_underline(True) + +class ImageMenuItem(gtk.ImageMenuItem): + def __init__(self, *args, **kwargs): + gtk.ImageMenuItem.__init__(self, *args, **kwargs) + self.set_use_underline(True) + +def getScrollValue(gevent): + value = gevent.direction.value_nick + if value == 'smooth':## happens *sometimes* in PyGI (Gtk3) + if gevent.delta_y < 0:## -1.0 (up) + value = 'up' + elif gevent.delta_y > 0:## 1.0 (down) + value = 'down' + return value + + + + diff --git a/scal3/ui_gtk/about.py b/scal3/ui_gtk/about.py new file mode 100644 index 000000000..e73b503c9 --- /dev/null +++ b/scal3/ui_gtk/about.py @@ -0,0 +1,29 @@ +#from scal3.locale_man import tr as _ +from scal3.ui_gtk import * + +class AboutDialog(gtk.AboutDialog): + def __init__( + self, + name='', + version='', + title='', + authors=[], + comments='', + license='', + website='', + **kwargs + ): + gtk.AboutDialog.__init__(self, **kwargs) + self.set_name(name) + self.set_program_name(name) + self.set_version(version) + self.set_title(title) ## must call after set_name and set_version ! + self.set_authors(authors) + self.set_comments(comments) + if license: + self.set_license(license) + self.set_wrap_license(True) + if website: + self.set_website(website) ## A plain label (not link) + + diff --git a/scal3/ui_gtk/adjust_dtime.py b/scal3/ui_gtk/adjust_dtime.py new file mode 100755 index 000000000..c38c51734 --- /dev/null +++ b/scal3/ui_gtk/adjust_dtime.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import os +os.environ['LANG']='en_US.UTF-8' #????????? + +from time import localtime +from time import time as now +import sys +from math import ceil + +from scal3 import ui + +from gobject import timeout_add + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.date import DateButton +from scal3.ui_gtk.mywidgets.multi_spin.time_b import TimeButton + + +_ = str ## FIXME +iceil = lambda f: int(ceil(f)) + + +def error_exit(text, parent=None): + d = gtk.MessageDialog(parent, gtk.DialogFlags.DESTROY_WITH_PARENT,\ + gtk.MessageType.ERROR, gtk.ButtonsType.OK, text.strip()) + d.set_title('Error') + d.run() + sys.exit(1) + +class AdjusterDialog(gtk.Dialog): + xpad = 15 + def __init__(self, **kwargs): + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Adjust System Date & Time'))##???????? + self.set_icon(self.render_icon(gtk.STOCK_PREFERENCES, gtk.IconSize.BUTTON)) + ######### + self.buttonCancel = self.add_button(gtk.STOCK_CANCEL, 0) + #self.buttonCancel.connect('clicked', lambda w: sys.exit(0)) + self.buttonSet = self.add_button(_('Set System Time'), 1) + #self.buttonSet.connect('clicked', self.setSysTimeClicked) + ######### + hbox = gtk.HBox() + self.label_cur = gtk.Label(_('Current:')) + pack(hbox, self.label_cur) + pack(self.vbox, hbox) + ######### + hbox = gtk.HBox() + self.radioMan = gtk.RadioButton(None, _('Set _Manully:'), True) + self.radioMan.connect('clicked', self.radioManClicked) + pack(hbox, self.radioMan) + pack(self.vbox, hbox) + ###### + vb = gtk.VBox() + sg = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ### + hbox = gtk.HBox() + ## + l = gtk.Label('') + l.set_property('width-request', self.xpad) + pack(hbox, l) + ## + self.ckeckbEditTime = gtk.CheckButton(_('Edit Time')) + self.editTime = False + self.ckeckbEditTime.connect('clicked', self.ckeckbEditTimeClicked) + pack(hbox, self.ckeckbEditTime) + sg.add_widget(self.ckeckbEditTime) + self.timeInput = TimeButton() ## ??????? options + pack(hbox, self.timeInput) + pack(vb, hbox) + ### + hbox = gtk.HBox() + ## + l = gtk.Label('') + l.set_property('width-request', self.xpad) + pack(hbox, l) + ## + self.ckeckbEditDate = gtk.CheckButton(_('Edit Date')) + self.editDate = False + self.ckeckbEditDate.connect('clicked', self.ckeckbEditDateClicked) + pack(hbox, self.ckeckbEditDate) + sg.add_widget(self.ckeckbEditDate) + self.dateInput = DateButton() ## ??????? options + pack(hbox, self.dateInput) + pack(vb, hbox) + ### + pack(self.vbox, vb, 0, 0, 10)#????? + self.vboxMan = vb + ###### + hbox = gtk.HBox() + self.radioNtp = gtk.RadioButton(self.radioMan, _('Set from _NTP:'), True) + self.radioNtp.connect('clicked', self.radioNtpClicked) + pack(hbox, self.radioNtp) + pack(self.vbox, hbox) + ### + hbox = gtk.HBox() + ## + l = gtk.Label('') + l.set_property('width-request', self.xpad) + pack(hbox, l) + ## + pack(hbox, gtk.Label(_('Server:')+' ')) + combo = gtk.combo_box_entry_new_text() + combo.get_child().connect('changed', self.updateSetButtonSensitive) + pack(hbox, combo, 1, 1) + self.ntpServerEntry = combo.get_child() + for s in ui.ntpServers: + combo.append_text(s) + combo.set_active(0) + self.hboxNtp = hbox + pack(self.vbox, hbox) + ###### + self.radioManClicked() + #self.radioNtpClicked() + self.ckeckbEditTimeClicked() + self.ckeckbEditDateClicked() + ###### + self.updateTimes() + self.vbox.show_all() + def radioManClicked(self, radio=None): + if self.radioMan.get_active(): + self.vboxMan.set_sensitive(True) + self.hboxNtp.set_sensitive(False) + else: + self.vboxMan.set_sensitive(False) + self.hboxNtp.set_sensitive(True) + self.updateSetButtonSensitive() + def radioNtpClicked(self, radio=None): + if self.radioNtp.get_active(): + self.vboxMan.set_sensitive(False) + self.hboxNtp.set_sensitive(True) + else: + self.vboxMan.set_sensitive(True) + self.hboxNtp.set_sensitive(False) + self.updateSetButtonSensitive() + def ckeckbEditTimeClicked(self, checkb=None): + self.editTime = self.ckeckbEditTime.get_active() + self.timeInput.set_sensitive(self.editTime) + self.updateSetButtonSensitive() + def ckeckbEditDateClicked(self, checkb=None): + self.editDate = self.ckeckbEditDate.get_active() + self.dateInput.set_sensitive(self.editDate) + self.updateSetButtonSensitive() + """def set_sys_time(self): + if os.path.isfile('/bin/date'): + pass##???????? + elif sys.platform == 'win32': + import win32api + win32api.SetSystemTime()##???????? + else: + pass""" + def updateTimes(self): + dt = now()%1 + timeout_add(iceil(1000*(1-dt)), self.updateTimes) + #print('updateTimes', dt) + lt = localtime() + self.label_cur.set_label(_('Current:')+' %.4d/%.2d/%.2d - %.2d:%.2d:%.2d'%lt[:6]) + if not self.editTime: + self.timeInput.set_value(lt[3:6]) + if not self.editDate: + self.dateInput.set_value(lt[:3]) + return False + def updateSetButtonSensitive(self, widget=None): + if self.radioMan.get_active(): + self.buttonSet.set_sensitive(self.editTime or self.editDate) + elif self.radioNtp.get_active(): + self.buttonSet.set_sensitive(self.ntpServerEntry.get_text()!='') + def setSysTimeClicked(self, widget=None): + if self.radioMan.get_active(): + if self.editTime: + h, m, s = self.timeInput.get_value() + if self.editDate: + Y, M, D = self.dateInput.get_value() + cmd = ['/bin/date', '-s', '%.4d/%.2d/%.2d %.2d:%.2d:%.2d'%(Y,M,D,h,m,s)] + else: + cmd = ['/bin/date', '-s', '%.2d:%.2d:%.2d'%(h, m, s)] + else: + if self.editDate: + Y, M, D = self.dateInput.get_value() + ##h, m, s = self.timeInput.get_value() + h, m, s = localtime()[3:6] + cmd = ['/bin/date', '-s', '%.4d/%.2d/%.2d %.2d:%.2d:%.2d'%(Y,M,D,h,m,s)] + else: + error_exit('No change!', self)#?????????? + elif self.radioNtp.get_active(): + cmd = ['ntpdate', self.ntpServerEntry.get_text()] + #if os.path.isfile('/usr/sbin/ntpdate'): + # cmd = ['/usr/sbin/ntpdate', self.ntpServerEntry.get_text()] + #else: + # error_exit('Could not find command /usr/sbin/ntpdate: no such file!', self)#?????????? + else: + error_exit('Not valid option!', self) + inp, out, err = os.popen3(cmd) + err_text = err.read() + if err_text=='': + sys.exit(0) + else: + error_exit(err_text, self)#?????????? + + +if __name__=='__main__': + if os.getuid()!=0: + error_exit('This program must be run as root') + #raise OSError('This program must be run as root') + ###os.setuid(0)#????????? + d = AdjusterDialog(parent=None) + #d.set_keap_above(True) + if d.run()==1: + d.setSysTimeClicked() + + + + + + + + + diff --git a/scal3/ui_gtk/app_info.py b/scal3/ui_gtk/app_info.py new file mode 100644 index 000000000..d7935417b --- /dev/null +++ b/scal3/ui_gtk/app_info.py @@ -0,0 +1,34 @@ +import sys + +from scal3.locale_man import popenDefaultLang + + +from scal3.ui_gtk import * + + +def getDefaultAppCommand(fpath): + from gi.repository import Gio as gio + mime_type = gio.content_type_guess(fpath)[0] + try: + app = gio.app_info_get_all_for_type(mime_type)[0] + except IndexError: + return + return app.get_executable() + + +def popenFile(fpath): + command = getDefaultAppCommand(fpath) + if not command: + return + return popenDefaultLang([ + command, + fpath, + ]) + + + + +if __name__=='__main__': + print(getDefaultAppCommand(sys.argv[1])) + + diff --git a/scal3/ui_gtk/arch-enable-locale.py b/scal3/ui_gtk/arch-enable-locale.py new file mode 100755 index 000000000..ed409e29b --- /dev/null +++ b/scal3/ui_gtk/arch-enable-locale.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +import sys, os, subprocess +from time import time as now + +from gi.repository import Gtk as gtk + +localeGen = '/etc/locale.gen' + +def error(text, parent=None): + d = gtk.MessageDialog(parent, gtk.DialogFlags.DESTROY_WITH_PARENT,\ + gtk.MessageType.ERROR, gtk.ButtonsType.OK, text.strip()) + d.set_title('Error') + d.run() + +def errorExit(text, parent=None): + error(text, parent) + sys.exit(1) + + +if __name__=='__main__': + if os.getuid()!=0: + errorExit('This program must be run as root') + if not os.path.isfile(localeGen): + errorExit('File "%s" does not exist!'%localeGen) + localeName = sys.argv[1].lower().replace('.', ' ') + lines = open(localeGen).read().split('\n') + for (i, line) in enumerate(lines): + if line.lower().startswith(localeName): + print('locale "%s" is already enabled'%localeName) + break + if line.lower().startswith('#'+localeName): + lines[i] = line[1:] + os.rename(localeGen, '%s.%s'%(localeGen, now())) + open(localeGen, 'w').write('\n'.join(lines)) + exit_code = subprocess.call('/usr/sbin/locale-gen') + print('enabling locale "%s" done'%localeName) + break + else: + errorExit('locale "%s" not found!'%localeName) + + diff --git a/scal3/ui_gtk/buffer.py b/scal3/ui_gtk/buffer.py new file mode 100644 index 000000000..c90e54e10 --- /dev/null +++ b/scal3/ui_gtk/buffer.py @@ -0,0 +1,16 @@ + +## Thanks to 'Pier Carteri' for program Py_Shell.py +class GtkBufferFile: + ## Implements a file-like object for redirect the stream to the buffer + def __init__(self, buff, tag): + self.buffer = buff + self.tag = tag + ## Write text into the buffer and apply self.tag + def write(self, text): + #text = text.replace('\x00', '') + self.buffer.insert_with_tags(self.buffer.get_end_iter(), text, self.tag) + writelines = lambda self, l: list(map(self.write, l)) + flush = lambda self: None + isatty = lambda self: False + + diff --git a/scal3/ui_gtk/cal_base.py b/scal3/ui_gtk/cal_base.py new file mode 100644 index 000000000..5d5e2c29a --- /dev/null +++ b/scal3/ui_gtk/cal_base.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from time import time + +from scal3 import core +from scal3 import ui + +from gi.repository import cairo +from gi.repository import GdkPixbuf + +from scal3.ui_gtk import * +from scal3.ui_gtk import listener +from scal3.ui_gtk.drawing import newDndDatePixbuf +from scal3.ui_gtk.color_utils import rgbToGdkColor +from scal3.ui_gtk.customize import CustomizableCalObj + + + +class CalBase(CustomizableCalObj): + signals = CustomizableCalObj.signals + [ + ('popup-cell-menu', [int, int, int]), + ('popup-main-menu', [int, int, int]), + ('2button-press', []), + ('pref-update-bg-color', []), + ('day-info', []), + ] + myKeys = ( + 'space', 'home', 't', + 'menu', + 'i', + ) + def initCal(self): + self.initVars() + listener.dateChange.add(self) + #### + self.defineDragAndDrop() + self.connect('2button-press', ui.dayOpenEvolution) + if ui.mainWin: + self.connect('popup-cell-menu', ui.mainWin.menuCellPopup) + self.connect('popup-main-menu', ui.mainWin.menuMainPopup) + self.connect('pref-update-bg-color', ui.mainWin.prefUpdateBgColor) + self.connect('day-info', ui.mainWin.dayInfoShow) + def gotoJd(self, jd): + ui.gotoJd(jd) + self.onDateChange() + goToday = lambda self, obj=None: self.gotoJd(core.getCurrentJd()) + def jdPlus(self, p): + ui.jdPlus(p) + self.onDateChange() + def changeDate(self, year, month, day, mode=None): + ui.changeDate(year, month, day, mode) + self.onDateChange() + def onCurrentDateChange(self, gdate): + self.queue_draw() + def getCellPagePlus(self, jd, plus):## use for sliding + raise NotImplementedError + def gridCheckClicked(self, checkb): + checkb.colorb.set_sensitive(checkb.get_active()) + checkb.item.updateVar() + self.queue_draw() + def gridColorChanged(self, colorb): + colorb.item.updateVar() + self.queue_draw() + def defineDragAndDrop(self): + self.drag_source_set( + gdk.ModifierType.MODIFIER_MASK, + [], + gdk.DragAction.MOVE,## FIXME + ) + self.drag_source_add_text_targets() + ### + self.connect('drag-data-get', self.dragDataGet) + self.connect('drag-begin', self.dragBegin) + self.connect('drag-data-received', self.dragDataRec) + ### + self.drag_dest_set( + gtk.DestDefaults.ALL, + [], + gdk.DragAction.COPY,## FIXME + ) + self.drag_dest_add_text_targets() + self.drag_dest_add_uri_targets() + ## ACTION_MOVE ????????????????????? + ## if source ACTION was ACTION_COPY, calendar recieves its own dragged day + ## just like gnome-calendar-applet (but it seems not a logical behaviar) + ''' + #self.drag_source_add_uri_targets()#??????? + ##self.connect('drag-end', self.dragCalEnd) + ##self.connect('drag-drop', self.dragCalDrop) + ##self.connect('drag-failed', self.dragCalFailed) + #self.connect('drag-leave', self.dragLeave) + ''' + def dragDataGet(self, obj, context, selection, target_id, etime): + ## context is instance of gi.repository.Gdk.DragContext + text = '%.2d/%.2d/%.2d'%ui.cell.dates[ui.dragGetMode] + selection.set_text(text, len(text)) + #pbuf = newDndDatePixbuf(ui.cell.dates[ui.dragGetMode]) + #selection.set_pixbuf(pbuf) + return True + def dragLeave(self, obj, context, etime): + context.drop_reply(False, etime) + return True + def dragDataRec(self, obj, context, x, y, selection, target_id, etime): + from scal3.ui_gtk.dnd import processDroppedDate + try: + dtype = selection.get_data_type() + except AttributeError:## Old PyGTK + dtype = selection.type + text = selection.get_text() + dateM = processDroppedDate(text, dtype) + if dateM: + self.changeDate(*dateM) + elif dtype=='application/x-color': + ## selection.get_text() == None + text = selection.data + ui.bgColor = ( + ord(text[1]), + ord(text[3]), + ord(text[5]), + ord(text[7]), + ) + self.emit('pref-update-bg-color') + self.queue_draw() + else: + print('Unknown dropped data type "%s", text="%s", data="%s"'%(dtype, text, selection.data)) + return True + return False + def dragBegin(self, obj, context): + ## context is instance of gi.repository.Gdk.DragContext + #win = context.get_source_window() + #print('dragBegin', id(win), win.get_geometry()) + pbuf = newDndDatePixbuf(ui.cell.dates[ui.dragGetMode]) + w = pbuf.get_width() + #print(dir(context)) + gtk.drag_set_icon_pixbuf( + context, + pbuf, + w/2,## y offset + -10,## x offset FIXME - not to be hidden behind mouse cursor + ) + return True + def getCellPos(self): + raise NotImplementedError + def keyPress(self, arg, gevent): + CustomizableCalObj.keyPress(self, arg, gevent) + kname = gdk.keyval_name(gevent.keyval).lower() + if kname in ('space', 'home', 't'): + self.goToday() + elif kname=='menu': + self.emit('popup-cell-menu', gevent.time, *self.getCellPos()) + elif kname=='i': + self.emit('day-info') + else: + return False + return True + + + diff --git a/scal3/ui_gtk/color_utils.py b/scal3/ui_gtk/color_utils.py new file mode 100644 index 000000000..72b614e65 --- /dev/null +++ b/scal3/ui_gtk/color_utils.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +from scal3.color_utils import rgbToHtmlColor +from gi.repository import Gdk as gdk + +## r, g, b in range(256) +rgbToGdkColor = lambda r, g, b, a=None: gdk.Color(int(r*257), int(g*257), int(b*257)) + +gdkColorToRgb = lambda gc: (gc.red//257, gc.green//257, gc.blue//257) + +#htmlColorToGdk = lambda hc: gdk.color_parse(hc) + +colorize = lambda text, color: '%s'%( + rgbToHtmlColor(*color), + text, +) + diff --git a/scal3/ui_gtk/customize.py b/scal3/ui_gtk/customize.py new file mode 100644 index 000000000..01a8b4863 --- /dev/null +++ b/scal3/ui_gtk/customize.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from os.path import join, isfile + +from scal3.path import confDir +from scal3.utils import myRaise +from scal3.json_utils import * +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from gi.overrides.GObject import Object + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import registerSignals +from scal3.ui_gtk import gtk_ud as ud + + + + +if 'mainMenu' not in dict(ud.wcalToolbarData['items']): + ud.wcalToolbarData['items'].insert(0, ('mainMenu', True)) + + +@registerSignals +class DummyCalObj(Object): + loaded = False + signals = [ + ('config-change', []), + ('date-change', []), + ] + def __init__(self, name, desc, pkg, customizable): + Object.__init__(self) + self.enable = False + self._name = name + self.desc = desc + self.moduleName = '.'.join([pkg, name]) + self.customizable = customizable + self.optionsWidget = None + self.items = [] + def getLoadedObj(self): + try: + module = __import__( + self.moduleName, + fromlist=['CalObj'], + ) + CalObj = module.CalObj + except: + myRaise() + return + obj = CalObj() + return obj + def updateVars(self): + pass + #def getData(self):## FIXME a real problem + # return None + def optionsWidgetCreate(self): + pass + + +class CustomizableCalObj(ud.BaseCalObj): + customizable = True + expand = False + params = () + myKeys = () + def initVars(self, optionsWidget=None): + ud.BaseCalObj.initVars(self) + self.itemWidgets = {} ## for lazy construction of widgets + self.optionsWidget = optionsWidget + if self.optionsWidget: + self.optionsWidget.show_all() + try: + self.connect('key-press-event', self.keyPress)## FIXME + except: + pass + getItemsData = lambda self: [(item._name, item.enable) for item in self.items] + def updateVars(self): + for item in self.items: + if item.customizable: + item.updateVars() + #def getData(self):## remove? FIXME + # data = {} + # for mod_attr in self.params: + # try: + # value = eval(mod_attr) + # except: + # myRaise() + # else: + # data[mod_attr] = value + # for item in self.items: + # if item.customizable: + # itemData = item.getData() + # if itemData: + # data.update(itemData) + # return data + def keyPress(self, arg, gevent): + kname = gdk.keyval_name(gevent.keyval).lower() + for item in self.items: + if item.enable and kname in item.myKeys: + if item.keyPress(arg, gevent): + break + def optionsWidgetCreate(self): + pass + +class CustomizableCalBox(CustomizableCalObj): + ## for GtkBox (HBox and VBox) + def appendItem(self, item): + CustomizableCalObj.appendItem(self, item) + if item.loaded: + pack(self, item, item.expand, item.expand) + item.showHide() + def moveItemUp(self, i): + if i > 0: + if self.items[i].loaded and self.items[i-1].loaded: + self.reorder_child(self.items[i], i-1) + CustomizableCalObj.moveItemUp(self, i) + def insertItemWidget(self, i): + item = self.items[i] + if not item.loaded: + return + pack(self, item, item.expand, item.expand) + self.reorder_child(item, i) + + + + + diff --git a/scal3/ui_gtk/customize_dialog.py b/scal3/ui_gtk/customize_dialog.py new file mode 100644 index 000000000..8a6f195b3 --- /dev/null +++ b/scal3/ui_gtk/customize_dialog.py @@ -0,0 +1,251 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3.path import * +from scal3.utils import myRaise +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import gtk_ud as ud +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import toolButtonFromStock, set_tooltip, dialog_add_button +from scal3.ui_gtk.tree_utils import tree_path_split + +class CustomizeDialog(gtk.Dialog): + def appendItemTree(self, item, parentIter): + itemIter = self.model.append(parentIter) + self.model.set(itemIter, 0, item.enable, 1, item.desc) + for child in item.items: + if child.customizable: + self.appendItemTree(child, itemIter) + def __init__(self, widget, **kwargs): + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Customize')) + #self.set_has_separator(False)## not in gtk3 + self.connect('delete-event', self.close) + dialog_add_button(self, gtk.STOCK_CLOSE, _('_Close'), 0, self.close) + ### + self._widget = widget + self.activeOptionsWidget = None + ### + self.model = gtk.TreeStore(bool, str) ## (GdkPixbuf.Pixbuf, str) + treev = self.treev = gtk.TreeView(self.model) + ## + treev.set_enable_tree_lines(True) + treev.set_headers_visible(False) + treev.connect('row-activated', self.rowActivated) + ## + col = gtk.TreeViewColumn('Widget') + ## + cell = gtk.CellRendererToggle() + cell.connect('toggled', self.enableCellToggled) + pack(col, cell) + col.add_attribute(cell, 'active', 0) + col.set_property('expand', False) + ## + treev.append_column(col) + col = gtk.TreeViewColumn('Widget') + col.set_property('expand', False) + ## + cell = gtk.CellRendererText() + pack(col, cell) + col.add_attribute(cell, 'text', 1) + col.set_property('expand', True) + ## + treev.append_column(col) + ### + for item in widget.items: + if item.customizable: + self.appendItemTree(item, None) + ### + hbox = gtk.HBox() + vbox_l = gtk.VBox() + pack(vbox_l, treev, 1, 1) + pack(hbox, vbox_l, 1, 1) + ### + toolbar = gtk.Toolbar() + toolbar.set_orientation(gtk.Orientation.VERTICAL) + size = gtk.IconSize.SMALL_TOOLBAR + toolbar.set_icon_size(size) + ## argument2 to image_new_from_stock does not affect + ### + tb = toolButtonFromStock(gtk.STOCK_GO_UP, size) + set_tooltip(tb, _('Move up')) + tb.connect('clicked', self.upClicked) + toolbar.insert(tb, -1) + ### + tb = toolButtonFromStock(gtk.STOCK_GO_DOWN, size) + set_tooltip(tb, _('Move down')) + tb.connect('clicked', self.downClicked) + toolbar.insert(tb, -1) + ### + pack(hbox, toolbar) + pack(self.vbox, hbox, 1, 1) + self.vbox_l = vbox_l + ### + self.vbox.connect('size-allocate', self.vboxSizeRequest) + self.vbox.show_all() + treev.get_selection().connect('changed', self.treevCursorChanged) + def vboxSizeRequest(self, widget, req): + self.resize(self.get_size()[0], 1) + def getItemByPath(self, path): + if isinstance(path, gtk.TreePath): + path = path.get_indices() + elif isinstance(path, str): + path = tree_path_split(path) + elif isinstance(path, int): + path = [path] + elif not isinstance(path, (tuple, list)): + raise TypeError('argument %s given to getItemByPath has bad type %s'%(path, type(path))) + item = self._widget.items[path[0]] + for i in path[1:]: + item = item.items[i] + return item + def treevCursorChanged(self, selection): + if self.activeOptionsWidget: + try: + self.vbox_l.remove(self.activeOptionsWidget) + except: + myRaise(__file__) + self.activeOptionsWidget = None + index_list = self.treev.get_cursor()[0] + if not index_list: + return + item = self.getItemByPath(index_list) + item.optionsWidgetCreate() + if item.optionsWidget: + item.optionsWidget.set_sensitive(item.enable) + self.activeOptionsWidget = item.optionsWidget + pack(self.vbox_l, item.optionsWidget) + item.optionsWidget.show() + def upClicked(self, button): + model = self.model + index_list = self.treev.get_cursor()[0] + if not index_list: + return + i = index_list[-1] + if len(index_list)==1: + if i<=0 or i>=len(model): + gdk.beep() + return + ### + self._widget.moveItemUp(i) + model.swap(model.get_iter(i-1), model.get_iter(i)) + self.treev.set_cursor(i-1) + else: + if i<=0: + gdk.beep() + return + ### + root = self.getItemByPath(index_list[:-1]) + if i>=len(root.items): + gdk.beep() + return + ### + root.moveItemUp(i) + index_list2 = list(index_list) + index_list2[-1] = i - 1 + model.swap(model.get_iter(index_list), model.get_iter(index_list2)) + self.treev.set_cursor(index_list2) + def downClicked(self, button): + model = self.model + index_list = self.treev.get_cursor()[0] + if not index_list: + return + i = index_list[-1] + if len(index_list)==1: + if i<0 or i>=len(model)-1: + gdk.beep() + return + ### + self._widget.moveItemUp(i+1) + model.swap(model.get_iter(i), model.get_iter(i+1)) + self.treev.set_cursor(i+1) + else: + if i<0: + gdk.beep() + return + ### + root = self.getItemByPath(index_list[:-1]) + if i>=len(root.items)-1: + gdk.beep() + return + ### + root.moveItemUp(i+1) + index_list2 = list(index_list) + index_list2[-1] = i + 1 + model.swap(model.get_iter(index_list), model.get_iter(index_list2)) + self.treev.set_cursor(index_list2) + def rowActivated(self, treev, path, col): + if treev.row_expanded(path): + treev.collapse_row(path) + else: + treev.expand_row(path, False) + def enableCellToggled(self, cell, path):## FIXME + active = not cell.get_active() + self.model.set_value(self.model.get_iter(path), 0, active) ## or set(...) + itemIter = self.model.get_iter(path) + ### + parentItem = self._widget + pp = tree_path_split(path) + item = parentItem.items[pp[0]] + for i in pp[1:]: + parentItem, item = item, item.items[i] + itemIndex = int(pp[-1]) + assert parentItem.items[itemIndex] == item + ### + if active: + if item.loaded: + item.enable = True + item.showHide() + else: + item = item.getLoadedObj() + parentItem.replaceItem(itemIndex, item) + parentItem.insertItemWidget(itemIndex) + for child in item.items: + if item.customizable: + self.appendItemTree(child, itemIter) + item.showHide() + item.onConfigChange() + item.onDateChange() + else: + item.enable = False + item.hide() + if item.customizable: + if item.optionsWidget: + item.optionsWidget.set_sensitive(item.enable) + if ui.mainWin: + ui.mainWin.setMinHeight() + def updateTreeEnableChecks(self): + for i, item in enumerate(self._widget.items): + self.model.set_value(self.model.get_iter((i,)), 0, item.enable) + def save(self): + self._widget.updateVars() + ui.ud__wcalToolbarData = ud.wcalToolbarData + ui.ud__mainToolbarData = ud.mainToolbarData + ui.saveConfCustomize() + #data = self._widget.getData()## remove? FIXME + def close(self, button=None, event=None): + self.save() + self.hide() + return True + + + diff --git a/scal3/ui_gtk/day_info.py b/scal3/ui_gtk/day_info.py new file mode 100644 index 000000000..56cec194c --- /dev/null +++ b/scal3/ui_gtk/day_info.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux +from time import localtime + +import sys + +from scal3.cal_types import calTypes +from scal3 import core +from scal3.locale_man import tr as _ +from scal3.locale_man import rtl, rtlSgn +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.utils import dialog_add_button +from scal3.ui_gtk import gtk_ud as ud +from scal3.ui_gtk.event.occurrence_view import DayOccurrenceView + + +@registerSignals +class AllDateLabelsVBox(gtk.VBox, ud.BaseCalObj): + _name = 'allDateLabels' + desc = _('Dates') + def __init__(self): + gtk.VBox.__init__(self, spacing=5) + self.initVars() + def onDateChange(self, *a, **ka): + ud.BaseCalObj.onDateChange(self, *a, **ka) + for child in self.get_children(): + child.destroy() + sgroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + for i, module in calTypes.iterIndexModule(): + hbox = gtk.HBox() + label = gtk.Label(_(module.desc)) + label.set_alignment(0, 0.5) + pack(hbox, label) + sgroup.add_widget(label) + pack(hbox, gtk.Label(' ')) + ### + pack(hbox, + gtk.Label( + ui.cell.format(ud.dateFormatBin, i) + ), + 0, + 0, + 0, + ) + ### + pack(self, hbox) + self.show_all() + + +@registerSignals +class PluginsTextView(gtk.TextView, ud.BaseCalObj): + _name = 'pluginsText' + desc = _('Plugins Text') + def __init__(self): + gtk.TextView.__init__(self) + self.initVars() + ### + self.set_wrap_mode(gtk.WrapMode.WORD) + self.set_editable(False) + self.set_cursor_visible(False) + self.set_justification(gtk.Justification.CENTER) + def onDateChange(self, *a, **ka): + ud.BaseCalObj.onDateChange(self, *a, **ka) + self.get_buffer().set_text(ui.cell.pluginsText) + + +@registerSignals +class DayInfoDialog(gtk.Dialog, ud.BaseCalObj): + _name = 'dayInfo' + desc = _('Day Info') + def __init__(self, **kwargs): + gtk.Dialog.__init__(self, **kwargs) + self.initVars() + ud.windowList.appendItem(self) + ### + self.set_title(_('Day Info')) + self.connect('delete-event', self.onClose) + self.vbox.set_spacing(15) + ### + dialog_add_button(self, gtk.STOCK_CLOSE, _('Close'), 0, self.onClose) + dialog_add_button(self, '', _('Previous'), 1, self.goBack) + dialog_add_button(self, '', _('Today'), 2, self.goToday) + dialog_add_button(self, '', _('Next'), 3, self.goNext) + ### + self.allDateLabels = AllDateLabelsVBox() + self.pluginsTextView = PluginsTextView() + self.eventsView = DayOccurrenceView() + ### + for item in (self.allDateLabels, self.pluginsTextView, self.eventsView): + self.appendItem(item) + ### + exp = gtk.Expander() + exp.set_label(item.desc) + exp.add(item) + exp.set_expanded(True) + pack(self.vbox, exp) + self.vbox.show_all() + ### + def onClose(self, obj=None, event=None): + self.hide() + return True + def goBack(self, obj=None): + ui.jdPlus(-1) + self.onDateChange() + def goToday(self, obj=None): + ui.gotoJd(core.getCurrentJd()) + self.onDateChange() + def goNext(self, obj=None): + ui.jdPlus(1) + self.onDateChange() + + + + + + + diff --git a/scal3/ui_gtk/decorators.py b/scal3/ui_gtk/decorators.py new file mode 100644 index 000000000..74e00822e --- /dev/null +++ b/scal3/ui_gtk/decorators.py @@ -0,0 +1,12 @@ +from gi.repository import GObject + +def registerType(cls): + GObject.type_register(cls) + return cls + +def registerSignals(cls): + GObject.type_register(cls) + for name, args in cls.signals: + GObject.signal_new(name, cls, GObject.SignalFlags.RUN_LAST, None, args) + return cls + diff --git a/scal3/ui_gtk/desktop.py b/scal3/ui_gtk/desktop.py new file mode 100644 index 000000000..3a34d7beb --- /dev/null +++ b/scal3/ui_gtk/desktop.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2003 Martin Grimme +# and Sebastien Bacher +# as part of package gdeskcal http://www.pycage.de/software_gdeskcal.html +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from gi.repository import Gtk as gtk +root = gdk.get_default_root_window() +wid = 0 + +# +# Restricts the given coordinates to visible values. +# +def _crop_coords(x, y, w, h): + sw = gdk.Screen.width() + sh = gdk.Screen.height() + x = min(x, sw-1) + y = min(y, sh-1) + return (x, y, min(sw-x, w), min(sh-y, h)) + + + +# +# Captures solid color wallpapers. Requires GNOME. +# +def get_wallcolor(width, height): + from gi.repository import GConf + client = GConf.Client.get_default() + client.add_dir("/desktop/gnome/background", GConf.ClientPreloadType.PRELOAD_RECURSIVE) + value = client.get("/desktop/gnome/background/primary_color") + color = value.get_string() + + pbuf = GdkPixbuf.Pixbuf(0, 1, 8, width, height) + c = gdk.color_parse(color) + fillr = (c.red / 256) << 24 + fillg = (c.green / 256) << 16 + fillb = (c.blue / 256) << 8 + fillcolor = fillr | fillg | fillb | 255 + pbuf.fill(fillcolor) + + return pbuf + + + +# +# Captures the wallpaper image by making a screen shot. +# +def get_wallpaper_fallback(x, y, width, height): + + x, y, width, height = _crop_coords(x, y, width, height) + + pbuf = GdkPixbuf.Pixbuf(0, 1, 8, width, height) + pbuf.get_from_drawable(root, root.get_colormap(), + x, y, 0, 0, width, height) + return pbuf + + + +# +# Captures the wallpaper image by accessing the background pixmap. +# +def get_wallpaper(x, y, width, height): + x, y, width, height = _crop_coords(x, y, width, height) + + # get wallpaper + pmap_id = get_wallpaper_id() + if hasattr(gtk.gdk, "gdk_pixmap_foreign_new"): + pmap = gdk.gdk_pixmap_foreign_new(pmap_id) + else: + pmap = gdk.pixmap_foreign_new(pmap_id) + pwidth, pheight = pmap.get_size() + + # create pixbuf + pbuf = GdkPixbuf.Pixbuf(0, 1, 8, width, height) + + # tile wallpaper over pixbuf + sx = -(x % pwidth) + sy = -(y % pheight) + for x in range(sx, width, pwidth): + for y in range(sy, height, pheight): + dstx = max(0, x) + dsty = max(0, y) + srcx = dstx - x + srcy = dsty - y + + w = min(pwidth - srcx, width - dstx) + h = min(pheight - srcy, height - dsty) + + pbuf.get_from_drawable(pmap, root.get_colormap(), + srcx, srcy, dstx, dsty, w, h) + + return pbuf + + + + +# +# Returns the ID of the background pixmap. +# +def get_wallpaper_id(): + global wid + try: + wid = root.property_get("_XROOTPMAP_ID", "PIXMAP")[2][0] + return int(wid) + + except: + #raise NotImplementedError + return int(wid) + + + +def watch_bg(observer): + _BGWATCHER.add_observer(observer) + + +#from BGWatcher import BGWatcher +#_BGWATCHER = BGWatcher() diff --git a/scal3/ui_gtk/dnd.py b/scal3/ui_gtk/dnd.py new file mode 100644 index 000000000..db055e3ca --- /dev/null +++ b/scal3/ui_gtk/dnd.py @@ -0,0 +1,38 @@ +def processDroppedDate(text, dtype): + ## data_type: "UTF8_STRING", "application/x-color", "text/uri-list", + if dtype=='UTF8_STRING': + if text.startswith('file://'): + path = core.urlToPath(text) + try: + t = os.stat(path).st_mtime ## modification time + except OSError: + print('Dropped invalid file "%s"'%path) + else: + y, m, d = localtime(t)[:3] + #print('Dropped file "%s", modification time: %s/%s/%s'%(path, y, m, d)) + return (y, m, d, core.DATE_GREG) + else: + date = ui.parseDroppedDate(text) + if date: + return date + (ui.dragRecMode,) + else: + ## Hot to deny dragged object (to return to it's first location) + ## FIXME + print('Dropped unknown text "%s"'%text) + #print(etime) + #context.drag_status(gdk.DragAction.DEFAULT, etime) + #context.drop_reply(False, etime) + #context.drag_abort(etime)##Segmentation fault + #context.drop_finish(False, etime) + #context.finish(False, True, etime) + #return True + elif dtype=='text/uri-list': + path = core.urlToPath(selection.data) + try: + t = os.stat(path).st_mtime ## modification time + except OSError: + print('Dropped invalid uri "%s"'%path) + return True + else: + return localtime(t)[:3] + (core.DATE_GREG,) + diff --git a/scal3/ui_gtk/drawing.py b/scal3/ui_gtk/drawing.py new file mode 100644 index 000000000..3965ac92a --- /dev/null +++ b/scal3/ui_gtk/drawing.py @@ -0,0 +1,351 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux +from os.path import join +from math import pi +from math import sin, cos +import re + +from scal3.path import * +from scal3.utils import toBytes +from scal3 import core +from scal3.locale_man import cutText, rtl +from scal3 import ui + +from gi.repository import cairo # as n_cairo +from gi.repository.PangoCairo import show_layout +from gi.repository import GdkPixbuf + +from scal3.ui_gtk import * +from scal3.ui_gtk.font_utils import * +from scal3.ui_gtk.color_utils import * + +if not ui.fontCustom: + ui.fontCustom = ui.fontDefault[:] + +colorCheckSvgTextChecked = open(join(rootDir, 'svg', 'color-check.svg')).read() +colorCheckSvgTextUnchecked = re.sub( + ']*?id="check"[^<>]*?/>', + '', + colorCheckSvgTextChecked, + flags=re.M | re.S, +) + +def setColor(cr, color): + ## arguments to set_source_rgb and set_source_rgba must be between 0 and 1 + if len(color)==3: + cr.set_source_rgb( + color[0] / 255.0, + color[1] / 255.0, + color[2] / 255.0, + ) + elif len(color)==4: + cr.set_source_rgba( + color[0] / 255.0, + color[1] / 255.0, + color[2] / 255.0, + color[3] / 255.0, + ) + else: + raise ValueError('bad color %s'%color) + + +def fillColor(cr, color): + setColor(cr, color) + cr.fill() + + +def newTextLayout( + widget, + text='', + font=None, + maxSize=None, + maximizeScale=0.6, + truncate=False, +): + ''' + None return value should be expected and handled, only if maxSize is given + ''' + layout = widget.create_pango_layout('') ## a Pango.Layout object + if not font: + font = ui.getFont() + layout.set_font_description(pfontEncode(font)) + if text: + layout.set_markup(text) + if maxSize: + layoutW, layoutH = layout.get_pixel_size() + ## + maxW, maxH = maxSize + maxW = float(maxW) + maxH = float(maxH) + if maxW <= 0: + return + if maxH <= 0: + minRat = 1.0 + else: + minRat = 1.01 * layoutH/maxH ## FIXME + if truncate: + if minRat > 1: + font[3] = int(font[3]/minRat) + layout.set_font_description(pfontEncode(font)) + layoutW, layoutH = layout.get_pixel_size() + if layoutW > 0: + char_w = float(layoutW)/len(text) + char_num = int(maxW//char_w) + while layoutW > maxW: + text = cutText(text, char_num) + if not text: + break + layout = widget.create_pango_layout(text) + layout.set_font_description(pfontEncode(font)) + layoutW, layoutH = layout.get_pixel_size() + char_num -= max(int((layoutW-maxW)//char_w), 1) + if char_num<0: + layout = None + break + else: + if maximizeScale > 0: + minRat = minRat/maximizeScale + if minRat < layoutW/maxW: + minRat = layoutW/maxW + if minRat > 1: + font[3] = int(font[3]/minRat) + layout.set_font_description(pfontEncode(font)) + return layout + +''' +def newLimitedWidthTextLayout(widget, text, width, font=None, truncate=True, markup=True): + if not font: + font = ui.getFont() + layout = widget.create_pango_layout('') + if markup: + layout.set_markup(text) + else: + layout.set_text(text) + layout.set_font_description(pfontEncode(font)) + if not layout: + return None + layoutW, layoutH = layout.get_pixel_size() + if layoutW > width: + if truncate: + char_w = layoutW/len(text) + char_num = int(width//char_w) + while layoutW > width: + text = cutText(text, char_num) + layout = widget.create_pango_layout(text) + layout.set_font_description(pfontEncode(font)) + layoutW, layoutH = layout.get_pixel_size() + char_num -= max(int((layoutW-width)//char_w), 1) + if char_num<0: + layout = None + break + else:## use smaller font + font2 = list(font) + while layoutW > width: + font2[3] = 0.9*font2[3]*width/layoutW + layout.set_font_description(pfontEncode(font2)) + layoutW, layoutH = layout.get_pixel_size() + #print(layoutW, width) + #print + return layout +''' + +def newColorCheckPixbuf(color, size, checked): + loader = GdkPixbuf.PixbufLoader.new_with_type('svg') + if checked: + data = colorCheckSvgTextChecked + else: + data = colorCheckSvgTextUnchecked + data = data.replace( + 'fill:#000000;', + 'fill:%s;'%rgbToHtmlColor(*color[:3]), + ) + data = toBytes(data) + loader.write(data) + loader.close() + pixbuf = loader.get_pixbuf() + return pixbuf + +def newDndDatePixbuf(ymd): + imagePath = join(rootDir, 'svg', 'dnd-date.svg') + loader = GdkPixbuf.PixbufLoader.new_with_type('svg') + data = open(imagePath).read() + data = data.replace('YYYY', '%.4d'%ymd[0]) + data = data.replace('MM', '%.2d'%ymd[1]) + data = data.replace('DD', '%.2d'%ymd[2]) + data = toBytes(data) + loader.write(data) + loader.close() + pixbuf = loader.get_pixbuf() + return pixbuf + +def newDndFontNamePixbuf(name): + imagePath = join(rootDir, 'svg', 'dnd-font.svg') + loader = GdkPixbuf.PixbufLoader.new_with_type('svg') + data = open(imagePath).read() + data = data.replace('FONTNAME', name) + data = toBytes(data) + loader.write(data) + loader.close() + pixbuf = loader.get_pixbuf() + return pixbuf + +def drawRoundedRect(cr, cx0, cy0, cw, ch, ro): + ro = min(ro, cw/2.0, ch/2.0) + cr.move_to(cx0+ro, cy0) + cr.line_to(cx0+cw-ro, cy0) + cr.arc(cx0+cw-ro, cy0+ro, ro, 3*pi/2, 2*pi) ## up right corner + cr.line_to(cx0+cw, cy0+ch-ro) + cr.arc(cx0+cw-ro, cy0+ch-ro, ro, 0, pi/2) ## down right corner + cr.line_to(cx0+ro, cy0+ch) + cr.arc(cx0+ro, cy0+ch-ro, ro, pi/2, pi) ## down left corner + cr.line_to(cx0, cy0+ro) + cr.arc(cx0+ro, cy0+ro, ro, pi, 3*pi/2) ## up left corner + cr.close_path() + + +def drawCursorBg(cr, cx0, cy0, cw, ch): + cursorRadius = ui.cursorRoundingFactor * min(cw, ch) * 0.5 + drawRoundedRect(cr, cx0, cy0, cw, ch, cursorRadius) + + +def drawOutlineRoundedRect(cr, cx0, cy0, cw, ch, ro, d): + ro = min(ro, cw/2.0, ch/2.0) + #a = min(cw, ch); ri = ro*(a-2*d)/a + ri = max(0, ro-d) + #print(ro, ri) + ######### Outline: + cr.move_to(cx0+ro, cy0) + cr.line_to(cx0+cw-ro, cy0) + cr.arc(cx0+cw-ro, cy0+ro, ro, 3*pi/2, 2*pi) ## up right corner + cr.line_to(cx0+cw, cy0+ch-ro) + cr.arc(cx0+cw-ro, cy0+ch-ro, ro, 0, pi/2) ## down right corner + cr.line_to(cx0+ro, cy0+ch) + cr.arc(cx0+ro, cy0+ch-ro, ro, pi/2, pi) ## down left corner + cr.line_to(cx0, cy0+ro) + cr.arc(cx0+ro, cy0+ro, ro, pi, 3*pi/2) ## up left corner + #### Inline: + if ri==0: + cr.move_to(cx0+d, cy0+d) + cr.line_to(cx0+d, cy0+ch-d) + cr.line_to(cx0+cw-d, cy0+ch-d) + cr.line_to(cx0+cw-d, cy0+d) + cr.line_to(cx0+d, cy0+d) + else: + cr.move_to(cx0+ro, cy0+d)## or line_to + cr.arc_negative(cx0+ro, cy0+ro, ri, 3*pi/2, pi) ## up left corner + cr.line_to(cx0+d, cy0+ch-ro) + cr.arc_negative(cx0+ro, cy0+ch-ro, ri, pi, pi/2) ## down left + cr.line_to(cx0+cw-ro, cy0+ch-d) + cr.arc_negative(cx0+cw-ro, cy0+ch-ro, ri, pi/2, 0) ## down right + cr.line_to(cx0+cw-d, cy0+ro) + cr.arc_negative(cx0+cw-ro, cy0+ro, ri, 2*pi, 3*pi/2) ## up right + cr.line_to(cx0+ro, cy0+d) + cr.close_path() + +def drawCursorOutline(cr, cx0, cy0, cw, ch): + cursorRadius = ui.cursorRoundingFactor * min(cw, ch) * 0.5 + cursorDia = ui.cursorDiaFactor * min(cw, ch) * 0.5 + drawOutlineRoundedRect(cr, cx0, cy0, cw, ch, cursorRadius, cursorDia) + + +def drawCircle(cr, cx, cy, r): + drawRoundedRect(cr, cx-r, cy-r, r*2, r*2, r) + +def drawCircleOutline(cr, cx, cy, r, d): + drawOutlineRoundedRect(cr, cx-r, cy-r, r*2, r*2, r, d) + +def goAngle(x0, y0, angle, length): + return x0 + cos(angle)*length, y0 + sin(angle)*length + +def drawLineLengthAngle(cr, xs, ys, length, angle, d): + xe, ye = goAngle(xs, ys, angle, length) + ## + x1, y1 = goAngle(xs, ys, angle-pi/2.0, d/2.0) + x2, y2 = goAngle(xs, ys, angle+pi/2.0, d/2.0) + x3, y3 = goAngle(xe, ye, angle+pi/2.0, d/2.0) + x4, y4 = goAngle(xe, ye, angle-pi/2.0, d/2.0) + ## + cr.move_to(x1, y1) + cr.line_to(x2, y2) + cr.line_to(x3, y3) + cr.line_to(x4, y4) + cr.close_path() + +def drawArcOutline(cr, xc, yc, r, d, a0, a1): + ''' + cr: cairo context + xc, yc: coordinates of center + r: outer radius + d: outline width (r - ri) + a0: start angle (radians) + a1: end angle (radians) + ''' + x1, y1 = goAngle(xc, yc, a0, r-d) + x2, y2 = goAngle(xc, yc, a1, r-d) + x3, y3 = goAngle(xc, yc, a1, r) + x4, y4 = goAngle(xc, yc, a0, r) + #### + cr.move_to(x1, y1) + cr.arc(xc, yc, r-d, a0, a1) + #cr.move_to(x2, y2) + cr.line_to(x3, y3) + cr.arc_negative(xc, yc, r, a1, a0) + #cr.move_to(x4, y4) + #cr.line_to(x1, y1) + + cr.close_path() + + + + + + +class Button: + def __init__(self, imageName, func, x, y, autoDir=True): + self.imageName = imageName + if imageName.startswith('gtk-'): + self.pixbuf = GdkPixbuf.Pixbuf.new_from_stock(imageName) + else: + self.pixbuf = GdkPixbuf.Pixbuf.new_from_file(join(pixDir, imageName)) + self.func = func + self.width = self.pixbuf.get_width() + self.height = self.pixbuf.get_height() + self.x = x + self.y = y + self.autoDir = autoDir + __repr__ = lambda self: 'Button(%r, %r, %r, %r, %r)'%(self.imageName, self.func.__name__, self.x, self.y, self.autoDir) + def getAbsPos(self, w, h): + x = self.x + y = self.y + if self.autoDir and rtl: + x = -x + if x<0: + x = w - self.width + x + if y<0: + y = h - self.height + y + return (x, y) + def draw(self, cr, w, h): + x, y = self.getAbsPos(w, h) + gdk.cairo_set_source_pixbuf(cr, self.pixbuf, x, y) + cr.rectangle(x, y, self.width, self.height) + cr.fill() + def contains(self, px, py, w, h): + x, y = self.getAbsPos(w, h) + return (x <= px < x+self.width and y <= py < y+self.height) + diff --git a/scal3/ui_gtk/event/__init__.py b/scal3/ui_gtk/event/__init__.py new file mode 100644 index 000000000..2ea07112a --- /dev/null +++ b/scal3/ui_gtk/event/__init__.py @@ -0,0 +1,47 @@ +__all__ = [ + 'rules', + 'notifiers', + 'occurrenceViews', +] + +############################################ + +from scal3.utils import myRaise +from scal3 import event_lib + + +modPrefix = 'scal3.ui_gtk.event' + + +def makeWidget(obj):## obj is an instance of Event, EventRule, EventNotifier or EventGroup + cls = obj.__class__ + try: + WidgetClass = cls.WidgetClass + except AttributeError: + try: + module = __import__( + '.'.join([ + modPrefix, + cls.tname, + cls.name, + ]), + fromlist=['WidgetClass'], + ) + WidgetClass = cls.WidgetClass = module.WidgetClass + except: + myRaise() + return + widget = WidgetClass(obj) + try: + widget.show_all() + except AttributeError: + widget.show() + widget.updateWidget()## FIXME + return widget + + +### Load accounts, groups and trash? FIXME +from os.path import join, isfile +from scal3.path import confDir + + diff --git a/scal3/ui_gtk/event/account/__init__.py b/scal3/ui_gtk/event/account/__init__.py new file mode 100644 index 000000000..5b8407fb3 --- /dev/null +++ b/scal3/ui_gtk/event/account/__init__.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- + +from scal3 import core +from scal3.locale_man import tr as _ + +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import IdComboBox, showError + +class BaseWidgetClass(gtk.VBox): + def __init__(self, account): + gtk.VBox.__init__(self) + self.account = account + ######## + self.sizeGroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Title')) + label.set_alignment(0, 0.5) + pack(hbox, label) + self.sizeGroup.add_widget(label) + self.titleEntry = gtk.Entry() + pack(hbox, self.titleEntry, 1, 1) + pack(self, hbox) + def updateWidget(self): + self.titleEntry.set_text(self.account.title) + def updateVars(self): + self.account.title = self.titleEntry.get_text() + + + +class AccountCombo(IdComboBox): + def __init__(self): + ls = gtk.ListStore(int, str) + gtk.ComboBox.__init__(self) + self.set_model(ls) + ### + cell = gtk.CellRendererText() + pack(self, cell, 1) + self.add_attribute(cell, 'text', 1) + ### + ls.append([-1, _('None')]) + for account in ui.eventAccounts: + if account.enable: + ls.append([account.id, account.title]) + ### + gtk.ComboBox.set_active(self, 0) + def get_active(self): + active = IdComboBox.get_active(self) + if active is -1: + active = None + return active + def set_active(self, active): + if active is None: + active = -1 + IdComboBox.set_active(self, active) + + + +class AccountGroupCombo(IdComboBox): + def __init__(self): + self.account = None + ### + ls = gtk.ListStore(str, str) + gtk.ComboBox.__init__(self) + self.set_model(ls) + ### + cell = gtk.CellRendererText() + pack(self, cell, 1) + self.add_attribute(cell, 'text', 1) + def setAccount(self, account): + self.account = account + self.updateList() + def updateList(self): + ls = self.get_model() + ls.clear() + if self.account: + for groupData in self.account.remoteGroups: + ls.append([ + str(groupData['id']), + groupData['title'], + ]) + + +class AccountGroupBox(gtk.HBox): + def __init__(self, accountCombo=None): + gtk.HBox.__init__(self) + self.combo = AccountGroupCombo() + pack(self, self.combo) + ## + button = gtk.Button( + #stock=gtk.STOCK_CONNECT, + ) + button.set_label(_('Fetch')) + button.connect('clicked', self.fetchClicked) + pack(self, button) + self.fetchButton = button + ## + label = gtk.Label() + label.set_alignment(0.1, 0.5) + pack(self, label, 1, 1) + self.msgLabel = label + ### + if accountCombo: + accountCombo.connect('changed', self.accountComboChanged) + def accountComboChanged(self, combo): + aid = combo.get_active() + if aid: + account = ui.eventAccounts[aid] + self.combo.setAccount(account) + def fetchClicked(self, obj=None): + combo = self.combo + account = combo.account + if not account: + self.msgLabel.set_label(_('No account selected')) + return + self.msgLabel.set_label(_('Fatching')) + while gtk.events_pending(): + gtk.main_iteration_do(False) + try: + account.fetchGroups() + except Exception as e: + self.msgLabel.set_label(_('Error')) + showError( + _('Error in fetching remote groups') + '\n' + str(e), + ui.eventManDialog, + ) + return + else: + self.msgLabel.set_label('') + account.save() + self.combo.updateList() + + + + + diff --git a/scal3/ui_gtk/event/account/google.py b/scal3/ui_gtk/event/account/google.py new file mode 100644 index 000000000..1d93b848d --- /dev/null +++ b/scal3/ui_gtk/event/account/google.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +from scal3 import core +from scal3.locale_man import tr as _ + +from scal3.ui_gtk.event.account import * + +class WidgetClass(BaseWidgetClass): + def __init__(self, account): + BaseWidgetClass.__init__(self, account) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Email')) + label.set_alignment(0, 0.5) + pack(hbox, label) + self.sizeGroup.add_widget(label) + self.emailEntry = gtk.Entry() + pack(hbox, self.emailEntry, 1, 1) + pack(self, hbox) + def updateWidget(self): + BaseWidgetClass.updateWidget(self) + self.emailEntry.set_text(self.account.email) + def updateVars(self): + BaseWidgetClass.updateVars(self) + self.account.email = self.emailEntry.get_text() + diff --git a/scal3/ui_gtk/event/account_op.py b/scal3/ui_gtk/event/account_op.py new file mode 100644 index 000000000..1d579f298 --- /dev/null +++ b/scal3/ui_gtk/event/account_op.py @@ -0,0 +1,81 @@ +from scal3.locale_man import tr as _ +from scal3 import event_lib +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import dialog_add_button +from scal3.ui_gtk.event import makeWidget + +class AccountEditorDialog(gtk.Dialog): + def __init__(self, account=None, **kwargs): + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Edit Account') if account else _('Add New Account')) + ### + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), gtk.ResponseType.CANCEL) + dialog_add_button(self, gtk.STOCK_OK, _('_OK'), gtk.ResponseType.OK) + ## + self.connect('response', lambda w, e: self.hide()) + ####### + self.account = account + self.activeWidget = None + ####### + hbox = gtk.HBox() + combo = gtk.ComboBoxText() + for cls in event_lib.classes.account: + combo.append_text(cls.desc) + pack(hbox, gtk.Label(_('Account Type'))) + pack(hbox, combo) + pack(hbox, gtk.Label(''), 1, 1) + pack(self.vbox, hbox) + #### + if self.account: + self.isNew = False + combo.set_active(event_lib.classes.account.names.index(self.account.name)) + else: + self.isNew = True + defaultAccountTypeIndex = 0 + combo.set_active(defaultAccountTypeIndex) + self.account = event_lib.classes.account[defaultAccountTypeIndex]() + self.activeWidget = None + combo.connect('changed', self.typeChanged) + self.comboType = combo + self.vbox.show_all() + self.typeChanged() + def dateModeChanged(self, combo): + pass + def typeChanged(self, combo=None): + if self.activeWidget: + self.activeWidget.updateVars() + self.activeWidget.destroy() + cls = event_lib.classes.account[self.comboType.get_active()] + account = cls() + if self.account: + account.copyFrom(self.account) + account.setId(self.account.id) + del self.account + if self.isNew: + account.title = cls.desc ## FIXME + self.account = account + self.activeWidget = makeWidget(account) + pack(self.vbox, self.activeWidget) + def run(self): + if self.activeWidget is None or self.account is None: + return None + if gtk.Dialog.run(self) != gtk.ResponseType.OK: + return None + self.activeWidget.updateVars() + self.account.save() + if self.isNew: + event_lib.lastIds.save() + else: + ui.eventAccounts[self.account.id] = self.account + self.destroy() + return self.account + + +class FetchRemoteGroupsDialog(gtk.Dialog): + def __init__(self, account, **kwargs): + gtk.Dialog.__init__(self, **kwargs) + self.account = account + + diff --git a/scal3/ui_gtk/event/bulk_edit.py b/scal3/ui_gtk/event/bulk_edit.py new file mode 100644 index 000000000..751e52f21 --- /dev/null +++ b/scal3/ui_gtk/event/bulk_edit.py @@ -0,0 +1,191 @@ +import natz + +from scal3.utils import myRaise +from scal3.locale_man import tr as _ + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import dialog_add_button +from scal3.ui_gtk.mywidgets import TextFrame +from scal3.ui_gtk.mywidgets.icon import IconSelectButton + +class EventsBulkEditDialog(gtk.Dialog): + def __init__(self, container, **kwargs): + from scal3.ui_gtk.mywidgets.tz_combo import TimeZoneComboBoxEntry + self._container = container + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Bulk Edit Events')) + #### + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), gtk.ResponseType.CANCEL) + dialog_add_button(self, gtk.STOCK_OK, _('_OK'), gtk.ResponseType.OK) + ## + self.connect('response', lambda w, e: self.hide()) + #### + try: + title = container.title + except AttributeError: + event_count = len(container) + msg = _('Here you are going to modify these %s events at once.'%event_count) + else: + msg = _('Here you are going to modify all events inside group "%s" at once.'%title) + msg += ' ' + msg += _('You better make a backup from you events before doing this. Just right click on group and select "Export" (or a full backup: menu File -> Export)') + msg += '\n\n' + label = gtk.Label(msg) + label.set_line_wrap(True) + pack(self.vbox, label) + #### + hbox = gtk.HBox() + self.iconRadio = gtk.RadioButton(label=_('Icon')) + pack(hbox, self.iconRadio, 1, 1) + self.summaryRadio = gtk.RadioButton(label=_('Summary'), group=self.iconRadio) + pack(hbox, self.summaryRadio, 1, 1) + self.descriptionRadio = gtk.RadioButton(label=_('Description'), group=self.iconRadio) + pack(hbox, self.descriptionRadio, 1, 1) + self.timeZoneRadio = gtk.RadioButton(label=_('Time Zone'), group=self.iconRadio) + pack(hbox, self.timeZoneRadio, 1, 1) + pack(self.vbox, hbox) + ### + self.iconRadio.connect('clicked', self.firstRadioChanged) + self.summaryRadio.connect('clicked', self.firstRadioChanged) + self.descriptionRadio.connect('clicked', self.firstRadioChanged) + self.timeZoneRadio.connect('clicked', self.firstRadioChanged) + #### + hbox = gtk.HBox() + self.iconChangeCombo = gtk.ComboBoxText() + self.iconChangeCombo.append_text('----') + self.iconChangeCombo.append_text(_('Change')) + self.iconChangeCombo.append_text(_('Change if empty')) + pack(hbox, self.iconChangeCombo) + pack(hbox, gtk.Label(' ')) + self.iconSelect = IconSelectButton() + try: + self.iconSelect.set_filename(container.icon) + except AttributeError: + pass + pack(hbox, self.iconSelect) + pack(hbox, gtk.Label(''), 1, 1) + pack(self.vbox, hbox) + self.iconHbox = hbox + #### + self.textVbox = gtk.VBox() + ### + hbox = gtk.HBox() + self.textChangeCombo = gtk.ComboBoxText() + self.textChangeCombo.append_text('----') + self.textChangeCombo.append_text(_('Add to beginning')) + self.textChangeCombo.append_text(_('Add to end')) + self.textChangeCombo.append_text(_('Replace text')) + self.textChangeCombo.connect('changed', self.textChangeComboChanged) + pack(hbox, self.textChangeCombo) + pack(hbox, gtk.Label(''), 1, 1) + ## CheckButton(_('Regexp')) + pack(self.textVbox, hbox) + ### + self.textInput1 = TextFrame() + pack(self.textVbox, self.textInput1, 1, 1) + ### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('with'))) + pack(hbox, gtk.Label(''), 1, 1) + pack(self.textVbox, hbox, 1, 1) + self.withHbox = hbox + ### + self.textInput2 = TextFrame() + pack(self.textVbox, self.textInput2, 1, 1) + #### + pack(self.vbox, self.textVbox, 1, 1) + #### + hbox = gtk.HBox() + self.timeZoneChangeCombo = gtk.ComboBoxText() + self.timeZoneChangeCombo.append_text('----') + self.timeZoneChangeCombo.append_text(_('Change')) + self.timeZoneChangeCombo.append_text(_('Change if empty')) + pack(hbox, self.timeZoneChangeCombo) + pack(hbox, gtk.Label(' ')) + self.timeZoneInput = TimeZoneComboBoxEntry() + pack(hbox, self.timeZoneInput) + pack(hbox, gtk.Label(''), 1, 1) + pack(self.vbox, hbox, 1, 1) + self.timeZoneHbox = hbox + #### + self.vbox.show_all() + self.iconRadio.set_active(True) + self.iconChangeCombo.set_active(0) + self.textChangeCombo.set_active(0) + self.firstRadioChanged() + def firstRadioChanged(self, w=None): + if self.iconRadio.get_active(): + self.iconHbox.show() + self.textVbox.hide() + self.timeZoneHbox.hide() + elif self.timeZoneRadio.get_active(): + self.iconHbox.hide() + self.textVbox.hide() + self.timeZoneHbox.show() + elif self.summaryRadio.get_active() or self.descriptionRadio.get_active(): + self.iconHbox.hide() + self.textChangeComboChanged() + self.timeZoneHbox.hide() + def textChangeComboChanged(self, w=None): + self.textVbox.show_all() + chType = self.textChangeCombo.get_active() + if chType==0: + self.textInput1.hide() + self.withHbox.hide() + self.textInput2.hide() + elif chType in (1, 2): + self.withHbox.hide() + self.textInput2.hide() + def doAction(self): + container = self._container + if self.iconRadio.get_active(): + chType = self.iconChangeCombo.get_active() + if chType!=0: + icon = self.iconSelect.get_filename() + for event in container: + if not (chType==2 and event.icon): + event.icon = icon + event.afterModify() + event.save() + elif self.timeZoneRadio.get_active(): + chType = self.timeZoneChangeCombo.get_active() + timeZone = self.timeZoneInput.get_text() + if chType!=0: + try: + natz.timezone(timeZone) + except: + myRaise('Invalid Time Zone "%s"'%timeZone) + else: + for event in container: + if not (chType==2 and event.timeZone): + event.timeZone = timeZone + event.afterModify() + event.save() + else: + chType = self.textChangeCombo.get_active() + if chType!=0: + text1 = self.textInput1.get_text() + text2 = self.textInput2.get_text() + if self.summaryRadio.get_active(): + for event in container: + if chType==1: + event.summary = text1 + event.summary + elif chType==2: + event.summary = event.summary + text1 + elif chType==3: + event.summary = event.summary.replace(text1, text2) + event.afterModify() + event.save() + elif self.descriptionRadio.get_active(): + for event in container: + if chType==1: + event.description = text1 + event.description + elif chType==2: + event.description = event.description + text1 + elif chType==3: + event.description = event.description.replace(text1, text2) + event.afterModify() + event.save() + + + diff --git a/scal3/ui_gtk/event/bulk_save_timezone.py b/scal3/ui_gtk/event/bulk_save_timezone.py new file mode 100644 index 000000000..721c01b5b --- /dev/null +++ b/scal3/ui_gtk/event/bulk_save_timezone.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import natz + +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import dialog_add_button + +class BulkSaveTimeZoneDialog(gtk.Dialog, **kwargs): + def __init__(self): + from scal3.ui_gtk.mywidgets.tz_combo import TimeZoneComboBoxEntry + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Time Zone')) + #### + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), gtk.ResponseType.CANCEL) + dialog_add_button(self, gtk.STOCK_OK, _('_OK'), gtk.ResponseType.OK) + ### + self.connect('response', self.onResponse) + #### + label = gtk.Label() + label.set_markup(''.join([ + _('"Time Zone" property is newly added to events')+'\n', + _('But this property needs to be saved for current events')+'\n', + _('Select the time zone for your current location')+'\n\n', + '', + _('If you have been in a different time zone while adding some of your event, you need to edit those events manually and change the time zone')+'\n', + _('Time zone for All-Day events will be disabled by default'), + '', + ])) + label.set_line_wrap(True) + pack(self.vbox, label, 1, 1) + #### + hbox = gtk.HBox() + self.timeZoneInput = TimeZoneComboBoxEntry() + pack(hbox, gtk.Label(''), 1, 1) + pack(hbox, self.timeZoneInput) + pack(hbox, gtk.Label(''), 1, 1) + hbox.set_border_width(20) + pack(self.vbox, hbox, 1, 1) + #### + self.errorLabel = gtk.Label() + pack(self.vbox, self.errorLabel, 1, 1) + #### + pack(self.vbox, gtk.Label(''), 1, 1) + #### + self.vbox.show_all() + def onResponse(self, dialog, responseId): + if responseId == gtk.ResponseType.OK: + timeZone = self.timeZoneInput.get_text() + try: + natz.timezone(timeZone) + except Exception as e: + self.errorLabel.set_text( + _('Time zone is invalid') + '\n' + str(e) + ) + else: + try: + for event in ui.iterAllEvents(): + event.timeZone = timeZone + event.afterModify() + event.save() + except Exception as e: + self.errorLabel.set_text( + str(e) + ) + else: + self.hide() + else: + self.hide() + while gtk.events_pending(): + gtk.main_iteration_do(False) + + +if __name__=='__main__': + BulkSaveTimeZoneDialog(parent=None).run() + diff --git a/scal3/ui_gtk/event/common.py b/scal3/ui_gtk/event/common.py new file mode 100644 index 000000000..2bec5369b --- /dev/null +++ b/scal3/ui_gtk/event/common.py @@ -0,0 +1,526 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + + +import os +from os.path import join, split + +from scal3.utils import toBytes +from scal3.utils import printError +from scal3.time_utils import durationUnitsAbs, durationUnitValues +from scal3.cal_types import calTypes +from scal3 import core +from scal3.core import myRaise +from scal3.locale_man import tr as _ +from scal3 import event_lib +from scal3 import ui + +from gi.repository import GObject +from gi.repository import GdkPixbuf + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import toolButtonFromStock, set_tooltip, labelStockMenuItem +from scal3.ui_gtk.utils import dialog_add_button, getStyleColor +from scal3.ui_gtk.drawing import newColorCheckPixbuf +from scal3.ui_gtk.mywidgets import TextFrame +from scal3.ui_gtk.mywidgets.icon import IconSelectButton +from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton +from scal3.ui_gtk.mywidgets.multi_spin.float_num import FloatSpinButton +from scal3.ui_gtk.event import makeWidget +from scal3.ui_gtk.event.utils import * + + +getGroupPixbuf = lambda group: newColorCheckPixbuf( + group.color, + 20, + group.enable, +) + +getGroupRow = lambda group: ( + group.id, + getGroupPixbuf(group), + group.title +) + + +class WidgetClass(gtk.VBox): + def __init__(self, event): + from scal3.ui_gtk.mywidgets.cal_type_combo import CalTypeCombo + from scal3.ui_gtk.mywidgets.tz_combo import TimeZoneComboBoxEntry + gtk.VBox.__init__(self) + self.event = event + ########### + hbox = gtk.HBox() + ### + pack(hbox, gtk.Label(_('Calendar Type'))) + combo = CalTypeCombo() + combo.set_active(calTypes.primary)## overwritten in updateWidget() + pack(hbox, combo) + pack(hbox, gtk.Label(''), 1, 1) + self.modeCombo = combo + ### + pack(self, hbox) + ########### + if event.isAllDay: + self.tzCheck = None + else: + hbox = gtk.HBox() + self.tzCheck = gtk.CheckButton(_('Time Zone')) + set_tooltip(self.tzCheck, _('For input times of event')) + pack(hbox, self.tzCheck) + combo = TimeZoneComboBoxEntry() + pack(hbox, combo) + pack(hbox, gtk.Label(''), 1, 1) + self.tzCombo = combo + pack(self, hbox) + self.tzCheck.connect('clicked', lambda check: self.tzCombo.set_sensitive(check.get_active())) + ########### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Summary'))) + self.summaryEntry = gtk.Entry() + pack(hbox, self.summaryEntry, 1, 1) + pack(self, hbox) + ########### + self.descriptionInput = TextFrame() + swin = gtk.ScrolledWindow() + swin.set_policy(gtk.PolicyType.AUTOMATIC, gtk.PolicyType.AUTOMATIC) + swin.add_with_viewport(self.descriptionInput) + ### + exp = gtk.Expander() + exp.set_expanded(True) + exp.set_label(_('Description')) + exp.add(swin) + pack(self, exp, 1, 1) + ########### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Icon')+':')) + self.iconSelect = IconSelectButton() + pack(hbox, self.iconSelect) + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + ########## + self.modeCombo.connect('changed', self.modeComboChanged)## right place? before updateWidget? FIXME + def focusSummary(self): + self.summaryEntry.select_region(0, -1) + self.summaryEntry.grab_focus() + def updateWidget(self): + #print('updateWidget', self.event.files) + self.modeCombo.set_active(self.event.mode) + if self.tzCheck: + self.tzCheck.set_active(self.event.timeZoneEnable) + self.tzCombo.set_sensitive(self.event.timeZoneEnable) + self.tzCombo.set_text(self.event.timeZone) + self.summaryEntry.set_text(self.event.summary) + self.descriptionInput.set_text(self.event.description) + self.iconSelect.set_filename(self.event.icon) + ##### + for attr in ('notificationBox', 'filesBox'): + try: + getattr(self, attr).updateWidget() + except AttributeError: + pass + ##### + self.modeComboChanged() + def updateVars(self): + self.event.mode = self.modeCombo.get_active() + if self.tzCheck: + self.event.timeZoneEnable = self.tzCheck.get_active() + self.event.timeZone = self.tzCombo.get_text() + else: + self.event.timeZoneEnable = False ## FIXME + self.event.summary = self.summaryEntry.get_text() + self.event.description = self.descriptionInput.get_text() + self.event.icon = self.iconSelect.get_filename() + ##### + for attr in ('notificationBox', 'filesBox'): + try: + getattr(self, attr).updateVars() + except AttributeError: + pass + ##### + def modeComboChanged(self, obj=None):## FIXME + pass + + +class FilesBox(gtk.VBox): + def __init__(self, event): + gtk.VBox.__init__(self) + self.event = event + self.vbox = gtk.VBox() + pack(self, self.vbox) + hbox = gtk.HBox() + pack(hbox, gtk.Label(''), 1, 1) + addButton = gtk.Button() + addButton.set_label(_('_Add File')) + addButton.set_image(gtk.Image.new_from_stock(gtk.STOCK_ADD, gtk.IconSize.BUTTON)) + addButton.connect('clicked', self.addClicked) + pack(hbox, addButton) + pack(self, hbox) + self.show_all() + self.newFiles = [] + def showFile(self, fname): + hbox = gtk.HBox() + link = gtk.LinkButton( + self.event.getUrlForFile(fname), + _('File') + ': ' + fname, + ) + pack(hbox, link) + pack(hbox, gtk.Label(''), 1, 1) + delButton = gtk.Button() + delButton.set_label(_('_Delete')) + delButton.set_image(gtk.Image.new_from_stock(gtk.STOCK_DELETE, gtk.IconSize.BUTTON)) + delButton.fname = fname + delButton.hbox = hbox + delButton.connect('clicked', self.delClicked) + pack(hbox, delButton) + pack(self.vbox, hbox) + hbox.show_all() + def addClicked(self, button): + fcd = gtk.FileChooserDialog( + buttons=( + toBytes(_('_OK')), gtk.ResponseType.OK, + toBytes(_('_Cancel')), gtk.ResponseType.CANCEL, + ), + title=_('Add File'), + ) + fcd.set_local_only(True) + fcd.connect('response', lambda w, e: fcd.hide()) + if fcd.run() == gtk.ResponseType.OK: + from shutil import copy + fpath = fcd.get_filename() + fname = split(fpath)[-1] + dstDir = self.event.filesDir + ## os.makedirs(dstDir, exist_ok=True)## only on new pythons FIXME + try: + os.makedirs(dstDir) + except: + myRaise() + copy(fpath, join(dstDir, fname)) + self.event.files.append(fname) + self.newFiles.append(fname) + self.showFile(fname) + def delClicked(self, button): + os.remove(join(self.event.filesDir, button.fname)) + try: + self.event.files.remove(button.fname) + except: + pass + button.hbox.destroy() + def removeNewFiles(self): + for fname in self.newFiles: + os.remove(join(self.event.filesDir, fname)) + self.newFiles = [] + def updateWidget(self): + for hbox in self.vbox.get_children(): + hbox.destroy() + for fname in self.event.files: + self.showFile(fname) + def updateVars(self):## FIXME + pass + + +class NotificationBox(gtk.Expander):## or NotificationBox FIXME + def __init__(self, event): + gtk.Expander.__init__(self) + self.set_label(_('Notification')) + self.event = event + self.hboxDict = {} + totalVbox = gtk.VBox() + ### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Notify')+' ')) + self.notifyBeforeInput = DurationInputBox() + pack(hbox, self.notifyBeforeInput, 0, 0) + pack(hbox, gtk.Label(' '+_('before event'))) + pack(hbox, gtk.Label(), 1, 1) + pack(totalVbox, hbox) + ### + for cls in event_lib.classes.notifier: + notifier = cls(self.event) + inputWidget = makeWidget(notifier) + if not inputWidget: + printError('notifier %s, inputWidget = %r'%(cls.name, inputWidget)) + continue + hbox = gtk.HBox() + cb = gtk.CheckButton(notifier.desc) + cb.inputWidget = inputWidget + cb.connect('clicked', lambda check: check.inputWidget.set_sensitive(check.get_active())) + cb.set_active(False) + pack(hbox, cb) + hbox.cb = cb + #pack(hbox, gtk.Label(''), 1, 1) + pack(hbox, inputWidget, 1, 1) + hbox.inputWidget = inputWidget + self.hboxDict[notifier.name] = hbox + pack(totalVbox, hbox) + self.add(totalVbox) + def updateWidget(self): + self.notifyBeforeInput.setDuration(*self.event.notifyBefore) + for hbox in self.hboxDict.values(): + hbox.cb.set_active(False) + hbox.inputWidget.set_sensitive(False) + for notifier in self.event.notifiers: + hbox = self.hboxDict[notifier.name] + hbox.cb.set_active(True) + hbox.inputWidget.set_sensitive(True) + hbox.inputWidget.notifier = notifier + hbox.inputWidget.updateWidget() + self.set_expanded(bool(self.event.notifiers)) + def updateVars(self): + self.event.notifyBefore = self.notifyBeforeInput.getDuration() + ### + notifiers = [] + for hbox in self.hboxDict.values(): + if hbox.cb.get_active(): + hbox.inputWidget.updateVars() + notifiers.append(hbox.inputWidget.notifier) + self.event.notifiers = notifiers + + +class DurationInputBox(gtk.HBox): + def __init__(self): + gtk.HBox.__init__(self) + ## + self.valueSpin = FloatSpinButton(0, 999, 1) + pack(self, self.valueSpin) + ## + combo = gtk.ComboBoxText() + for unitValue, unitName in durationUnitsAbs: + combo.append_text(_(' '+unitName.capitalize()+'s')) + combo.set_active(2) ## hour FIXME + pack(self, combo) + self.unitCombo = combo + def getDuration(self): + return self.valueSpin.get_value(), durationUnitValues[self.unitCombo.get_active()] + def setDuration(self, value, unit): + self.valueSpin.set_value(value) + self.unitCombo.set_active(durationUnitValues.index(unit)) + + +class StrListEditor(gtk.HBox): + def __init__(self, defaultValue=''): + self.defaultValue = defaultValue + ##### + gtk.HBox.__init__(self) + self.treev = gtk.TreeView() + self.treev.set_headers_visible(False) + self.trees = gtk.ListStore(str) + self.treev.set_model(self.trees) + ########## + cell = gtk.CellRendererText() + cell.set_property('editable', True) + col = gtk.TreeViewColumn('', cell, text=0) + self.treev.append_column(col) + #### + pack(self, self.treev, 1, 1) + ########## + toolbar = gtk.Toolbar() + toolbar.set_orientation(gtk.Orientation.VERTICAL) + #try:## DeprecationWarning #????????????? + #toolbar.set_icon_size(gtk.IconSize.SMALL_TOOLBAR) + ### no different (argument to set_icon_size does not affect) ????????? + #except: + # pass + size = gtk.IconSize.SMALL_TOOLBAR + ##no different(argument2 to image_new_from_stock does not affect) ????????? + #### gtk.IconSize.SMALL_TOOLBAR or gtk.IconSize.MENU + tb = toolButtonFromStock(gtk.STOCK_ADD, size) + set_tooltip(tb, _('Add')) + tb.connect('clicked', self.addClicked) + toolbar.insert(tb, -1) + #self.buttonAdd = tb + #### + tb = toolButtonFromStock(gtk.STOCK_GO_UP, size) + set_tooltip(tb, _('Move up')) + tb.connect('clicked', self.moveUpClicked) + toolbar.insert(tb, -1) + #### + tb = toolButtonFromStock(gtk.STOCK_GO_DOWN, size) + set_tooltip(tb, _('Move down')) + tb.connect('clicked', self.moveDownClicked) + toolbar.insert(tb, -1) + ####### + pack(self, toolbar) + def addClicked(self, button): + cur = self.treev.get_cursor() + if cur: + self.trees.insert(cur[0], [self.defaultValue]) + else: + self.trees.append([self.defaultValue]) + def moveUpClicked(self, button): + cur = self.treev.get_cursor() + if not cur: + return + i = cur[0] + t = self.trees + if i<=0 or i>=len(t): + gdk.beep() + return + t.swap(t.get_iter(i-1), t.get_iter(i)) + self.treev.set_cursor(i-1) + def moveDownClicked(self, button): + cur = self.treev.get_cursor() + if not cur: + return + i = cur[0] + t = self.trees + if i<0 or i>=len(t)-1: + gdk.beep() + return + t.swap(t.get_iter(i), t.get_iter(i+1)) + self.treev.set_cursor(i+1) + def setData(self, strList): + self.trees.clear() + for st in strList: + self.trees.append([st]) + getData = lambda self: [row[0] for row in self.trees] + + +class Scale10PowerComboBox(gtk.ComboBox): + def __init__(self): + ls = gtk.ListStore(int, str) + gtk.ComboBox.__init__(self) + self.set_model(ls) + ### + cell = gtk.CellRendererText() + pack(self, cell, True) + self.add_attribute(cell, 'text', 1) + ### + ls.append((1, _('Years'))) + ls.append((100, _('Centuries'))) + ls.append((1000, _('Thousand Years'))) + ls.append((1000**2, _('Million Years'))) + ls.append((1000**3, _('Billion (10^9) Years'))) + ### + self.set_active(0) + get_value = lambda self: self.get_model()[self.get_active()][0] + def set_value(self, value): + ls = self.get_model() + for i, row in enumerate(ls): + if row[0] == value: + self.set_active(i) + return + ls.append((value, _('%s Years')%_(value))) + self.set_active(len(ls)-1) + + +class GroupsTreeCheckList(gtk.TreeView): + def __init__(self): + gtk.TreeView.__init__(self) + self.trees = gtk.ListStore(int, bool, str)## groupId(hidden), enable, summary + self.set_model(self.trees) + self.set_headers_visible(False) + ### + cell = gtk.CellRendererToggle() + #cell.set_property('activatable', True) + cell.connect('toggled', self.enableCellToggled) + col = gtk.TreeViewColumn(_('Enable'), cell) + col.add_attribute(cell, 'active', 1) + #cell.set_active(True) + col.set_resizable(True) + self.append_column(col) + ### + col = gtk.TreeViewColumn(_('Title'), gtk.CellRendererText(), text=2) + col.set_resizable(True) + self.append_column(col) + ### + for group in ui.eventGroups: + self.trees.append([group.id, True, group.title]) + def enableCellToggled(self, cell, path): + i = int(path) + active = not cell.get_active() + self.trees[i][1] = active + cell.set_active(active) + getValue = lambda self: [row[0] for row in self.trees if row[1]] + def setValue(self, gids): + for row in self.trees: + row[1] = (row[0] in gids) + + + + +class SingleGroupComboBox(gtk.ComboBox): + def __init__(self): + ls = gtk.ListStore(int, GdkPixbuf.Pixbuf, str) + gtk.ComboBox.__init__(self) + self.set_model(ls) + ##### + cell = gtk.CellRendererPixbuf() + pack(self, cell) + self.add_attribute(cell, 'pixbuf', 1) + ### + cell = gtk.CellRendererText() + pack(self, cell, 1) + self.add_attribute(cell, 'text', 2) + ##### + self.updateItems() + def updateItems(self): + from scal3.ui_gtk.color_utils import gdkColorToRgb + ls = self.get_model() + activeGid = self.get_active() + ls.clear() + ### + for group in ui.eventGroups: + if not group.enable:## FIXME + continue + ls.append(getGroupRow(group)) + ### + #try: + gtk.ComboBox.set_active(self, 0) + #except: + # pass + if activeGid not in (None, -1): + try: + self.set_active(activeGid) + except ValueError: + pass + def get_active(self): + index = gtk.ComboBox.get_active(self) + if index in (None, -1): + return + gid = self.get_model()[index][0] + return gid + def set_active(self, gid): + ls = self.get_model() + for i, row in enumerate(ls): + if row[0] == gid: + gtk.ComboBox.set_active(self, i) + break + else: + raise ValueError('SingleGroupComboBox.set_active: Group ID %s is not in items'%gid) + +if __name__ == '__main__': + from pprint import pformat + dialog = gtk.Window() + dialog.vbox = gtk.VBox() + dialog.add(dialog.vbox) + #widget = ViewEditTagsHbox() + #widget = EventTagsAndIconSelect() + #widget = TagsListBox('task') + widget = SingleGroupComboBox() + pack(dialog.vbox, widget, 1, 1) + #dialog.vbox.show_all() + #dialog.resize(300, 500) + #dialog.run() + dialog.show_all() + gtk.main() + print(pformat(widget.getData())) + + + diff --git a/scal3/ui_gtk/event/editor.py b/scal3/ui_gtk/event/editor.py new file mode 100644 index 000000000..551941b31 --- /dev/null +++ b/scal3/ui_gtk/event/editor.py @@ -0,0 +1,130 @@ +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import dialog_add_button +from scal3.ui_gtk.utils import showInfo +from scal3.ui_gtk.event import makeWidget +from scal3.ui_gtk.event.utils import checkEventsReadOnly + +class EventEditorDialog(gtk.Dialog): + def __init__(self, event, typeChangable=True, isNew=False, useSelectedDate=False, **kwargs): + checkEventsReadOnly() + gtk.Dialog.__init__(self, **kwargs) + #self.set_transient_for(None) + #self.set_type_hint(gdk.WindowTypeHint.NORMAL) + self.isNew = isNew + #self.connect('delete-event', lambda obj, e: self.destroy()) + #self.resize(800, 600) + ### + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), gtk.ResponseType.CANCEL) + dialog_add_button(self, gtk.STOCK_OK, _('_OK'), gtk.ResponseType.OK) + ### + self.connect('response', lambda w, e: self.hide()) + ### + self.activeWidget = None + self._group = event.parent + self.eventTypeOptions = list(self._group.acceptsEventTypes) + #### + if not event.name in self.eventTypeOptions: + self.eventTypeOptions.append(event.name) + eventTypeIndex = self.eventTypeOptions.index(event.name) + #### + self.event = event + ####### + if isNew: + event.timeZone = str(core.localTz) + ####### + hbox = gtk.HBox() + pack(hbox, gtk.Label( + _('Group') + ': ' + self._group.title + )) + hbox.show_all() + pack(self.vbox, hbox) + ####### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Event Type'))) + if typeChangable: + combo = gtk.ComboBoxText() + for tmpEventType in self.eventTypeOptions: + combo.append_text(event_lib.classes.event.byName[tmpEventType].desc) + pack(hbox, combo) + #### + combo.set_active(eventTypeIndex) + #### + #self.activeWidget = makeWidget(event) + combo.connect('changed', self.typeChanged) + self.comboEventType = combo + else: + pack(hbox, gtk.Label(': '+event.desc)) + pack(hbox, gtk.Label(''), 1, 1) + hbox.show_all() + pack(self.vbox, hbox) + ##### + if useSelectedDate: + self.event.setJd(ui.cell.jd) + self.activeWidget = makeWidget(event) + if self.isNew: + self.activeWidget.focusSummary() + pack(self.vbox, self.activeWidget, 1, 1) + self.vbox.show() + def typeChanged(self, combo): + if self.activeWidget: + self.activeWidget.updateVars() + self.activeWidget.destroy() + eventType = self.eventTypeOptions[combo.get_active()] + if self.isNew: + self.event = self._group.createEvent(eventType) + else: + self.event = self._group.copyEventWithType(self.event, eventType) + self._group.updateCache(self.event)## needed? FIXME + self.activeWidget = makeWidget(self.event) + if self.isNew: + self.activeWidget.focusSummary() + pack(self.vbox, self.activeWidget) + #self.activeWidget.modeComboChanged()## apearantly not needed + def run(self): + #if not self.activeWidget: + # return None + if gtk.Dialog.run(self) != gtk.ResponseType.OK: + try: + filesBox = self.activeWidget.filesBox + except AttributeError: + pass + else: + filesBox.removeNewFiles() + return None + self.activeWidget.updateVars() + self.event.afterModify() + self.event.save() + event_lib.lastIds.save() + self.destroy() + ##### + if self.event.isSingleOccur: + occur = self.event.calcOccurrence(self.event.parent.startJd, self.event.parent.endJd) + if not occur: + showInfo(_('This event is outside of date range specified in it\'s group. You probably need to edit group \"%s\" and change \"Start\" or \"End\" values')%self.event.parent.title) + ##### + return self.event + +def addNewEvent(group, eventType, typeChangable=False, **kwargs): + event = group.createEvent(eventType) + if eventType=='custom':## FIXME + typeChangable = True + event = EventEditorDialog( + event, + typeChangable=typeChangable, + isNew=True, + **kwargs + ).run() + if event is None: + return + group.append(event) + group.save() + return event + + + diff --git a/scal3/ui_gtk/event/event/__init__.py b/scal3/ui_gtk/event/event/__init__.py new file mode 100644 index 000000000..f2c36999d --- /dev/null +++ b/scal3/ui_gtk/event/event/__init__.py @@ -0,0 +1,13 @@ +__all__ = [ + 'allDayTask', + 'custom', + 'dailyNote', + 'largeScale', + 'lifeTime', + 'task', + 'universityClass', + 'universityExam', + 'weekly', + 'yearly', +] + diff --git a/scal3/ui_gtk/event/event/allDayTask.py b/scal3/ui_gtk/event/event/allDayTask.py new file mode 100644 index 000000000..7de60a0fd --- /dev/null +++ b/scal3/ui_gtk/event/event/allDayTask.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3.cal_types import jd_to, to_jd +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton +from scal3.ui_gtk.mywidgets.multi_spin.date import DateButton +from scal3.ui_gtk.event import common + + +class WidgetClass(common.WidgetClass): + def __init__(self, event):## FIXME + common.WidgetClass.__init__(self, event) + ###### + sizeGroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ###### + hbox = gtk.HBox() + label = gtk.Label(_('Start')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.startDateInput = DateButton() + pack(hbox, self.startDateInput) + ## + pack(self, hbox) + ###### + hbox = gtk.HBox() + self.endTypeCombo = gtk.ComboBoxText() + for item in ('Duration', 'End'): + self.endTypeCombo.append_text(_(item)) + self.endTypeCombo.connect('changed', self.endTypeComboChanged) + sizeGroup.add_widget(self.endTypeCombo) + pack(hbox, self.endTypeCombo) + #### + self.durationBox = gtk.HBox() + self.durationSpin = IntSpinButton(1, 999) + pack(self.durationBox, self.durationSpin) + pack(self.durationBox, gtk.Label(_(' days'))) + pack(hbox, self.durationBox) + #### + self.endDateInput = DateButton() + pack(hbox, self.endDateInput) + #### + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + ############# + self.notificationBox = common.NotificationBox(event) + pack(self, self.notificationBox) + ############# + #self.filesBox = common.FilesBox(self.event) + #pack(self, self.filesBox) + def endTypeComboChanged(self, combo=None): + active = self.endTypeCombo.get_active() + if active==0:## duration + self.durationBox.show() + self.endDateInput.hide() + elif active==1:## end date + self.durationBox.hide() + self.endDateInput.show() + else: + raise RuntimeError + def updateWidget(self):## FIXME + common.WidgetClass.updateWidget(self) + mode = self.event.mode + ### + startJd = self.event.getJd() + self.startDateInput.set_value(jd_to(startJd, mode)) + ### + endType, endValue = self.event.getEnd() + if endType=='duration': + self.endTypeCombo.set_active(0) + self.durationSpin.set_value(endValue) + self.endDateInput.set_value(jd_to(self.event.getEndJd(), mode))## FIXME + elif endType=='date': + self.endTypeCombo.set_active(1) + self.endDateInput.set_value(endValue) + else: + raise RuntimeError + self.endTypeComboChanged() + def updateVars(self):## FIXME + common.WidgetClass.updateVars(self) + self.event.setStartDate(self.startDateInput.get_value()) + ### + active = self.endTypeCombo.get_active() + if active==0: + self.event.setEnd('duration', self.durationSpin.get_value()) + elif active==1: + self.event.setEnd( + 'date', + self.endDateInput.get_value(), + ) + def modeComboChanged(self, obj=None):## overwrite method from common.WidgetClass + newMode = self.modeCombo.get_active() + self.startDateInput.changeMode(self.event.mode, newMode) + self.endDateInput.changeMode(self.event.mode, newMode) + self.event.mode = newMode + + + diff --git a/scal3/ui_gtk/event/event/custom.py b/scal3/ui_gtk/event/event/custom.py new file mode 100644 index 000000000..6e28aa101 --- /dev/null +++ b/scal3/ui_gtk/event/event/custom.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + + +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.event import makeWidget +from scal3.ui_gtk.event import common + + +class WidgetClass(common.WidgetClass): + groups = [gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL), gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL)] + def __init__(self, event, autoCheck=True): + common.WidgetClass.__init__(self, event) + ################ + self.autoCheck = autoCheck + ###### + self.ruleAddBox = gtk.HBox() + self.warnLabel = gtk.Label() + self.warnLabel.modify_fg(gtk.StateType.NORMAL, gdk.Color(65535, 0, 0)) + self.warnLabel.set_alignment(0, 0.5) + #self.warnLabel.set_visible(False)## FIXME + ########### + self.rulesExp = gtk.Expander() + self.rulesExp.set_label(_('Rules')) + self.rulesExp.set_expanded(True) + self.rulesBox = gtk.VBox() + self.rulesExp.add(self.rulesBox) + pack(self, self.rulesExp) + ### + pack(self, self.ruleAddBox) + pack(self, self.warnLabel) + ### + self.notificationBox = common.NotificationBox(event) + pack(self, self.notificationBox) + ########### + self.addRuleModel = gtk.ListStore(str, str) + self.addRuleCombo = gtk.ComboBox() + self.addRuleCombo.set_model(self.addRuleModel) + ### + cell = gtk.CellRendererText() + pack(self.addRuleCombo, cell, True) + self.addRuleCombo.add_attribute(cell, 'text', 1) + ### + pack(self.ruleAddBox, gtk.Label(_('Add Rule')+':')) + pack(self.ruleAddBox, self.addRuleCombo) + pack(self.ruleAddBox, gtk.Label(''), 1, 1) + self.ruleAddButton = gtk.Button(stock=gtk.STOCK_ADD) + if ui.autoLocale: + self.ruleAddButton.set_label(_('_Add')) + self.ruleAddButton.set_image(gtk.Image.new_from_stock(gtk.STOCK_ADD, gtk.IconSize.BUTTON)) + pack(self.ruleAddBox, self.ruleAddButton) + ############# + #self.filesBox = common.FilesBox(self.event) + #pack(self, self.filesBox) + ############# + self.addRuleCombo.connect('changed', self.addRuleComboChanged) + self.ruleAddButton.connect('clicked', self.addClicked) + def makeRuleHbox(self, rule): + hbox = gtk.HBox(spacing=5) + lab = gtk.Label(rule.desc) + lab.set_alignment(0, 0.5) + pack(hbox, lab) + self.groups[rule.sgroup].add_widget(lab) + #pack(hbox, gtk.Label(''), 1, 1) + inputWidget = makeWidget(rule) + if not inputWidget: + print('failed to create inpout widget for rule %s'%rule.name) + return + if rule.expand: + pack(hbox, inputWidget, 1, 1) + else: + pack(hbox, inputWidget) + pack(hbox, gtk.Label(''), 1, 1) + #### + removeButton = gtk.Button(stock=gtk.STOCK_REMOVE) + if ui.autoLocale: + removeButton.set_label(_('_Remove')) + removeButton.set_image(gtk.Image.new_from_stock(gtk.STOCK_REMOVE, gtk.IconSize.BUTTON)) + removeButton.connect('clicked', self.removeButtonClicked, hbox)## FIXME + pack(hbox, removeButton) + #### + hbox.inputWidget = inputWidget + hbox.removeButton = removeButton + return hbox + def updateRulesWidget(self): + for hbox in self.rulesBox.get_children(): + hbox.destroy() + comboItems = [ruleClass.name for ruleClass in event_lib.classes.rule] + for rule in self.event: + hbox = self.makeRuleHbox(rule) + if not hbox: + continue + pack(self.rulesBox, hbox) + #hbox.show_all() + comboItems.remove(rule.name) + self.rulesBox.show_all() + for ruleName in comboItems: + self.addRuleModel.append((ruleName, event_lib.classes.rule.byName[ruleName].desc)) + self.addRuleComboChanged() + def updateRules(self): + self.event.clearRules() + for hbox in self.rulesBox.get_children(): + hbox.inputWidget.updateVars() + self.event.addRule(hbox.inputWidget.rule) + def updateWidget(self): + common.WidgetClass.updateWidget(self) + self.addRuleModel.clear() + self.updateRulesWidget() + self.notificationBox.updateWidget() + def updateVars(self): + common.WidgetClass.updateVars(self) + self.updateRules() + self.notificationBox.updateVars() + def modeComboChanged(self, obj=None):## overwrite method from common.WidgetClass + newMode = self.modeCombo.get_active() + for hbox in self.rulesBox.get_children(): + widget = hbox.inputWidget + if hasattr(widget, 'changeMode'): + widget.changeMode(newMode) + self.event.mode = newMode + def removeButtonClicked(self, button, hbox): + rule = hbox.inputWidget.rule + ok, msg = self.event.checkRulesDependencies(disabledRule=rule) + self.warnLabel.set_label(msg) + if not ok: + return + self.event.checkAndRemoveRule(rule) + #### + self.addRuleModel.append((rule.name, rule.desc)) + #### + hbox.destroy() + #self.rulesBox.remove(hbox) + self.addRuleComboChanged() + def addRuleComboChanged(self, combo=None): + ci = self.addRuleCombo.get_active() + if ci==None or ci<0: + return + newRuleName = self.addRuleModel[ci][0] + newRule = event_lib.classes.rule.byName[newRuleName](self.event) + ok, msg = self.event.checkRulesDependencies(newRule=newRule) + self.warnLabel.set_label(msg) + def addClicked(self, button): + ci = self.addRuleCombo.get_active() + if ci==None or ci<0: + return + ruleName = self.addRuleModel[ci][0] + rule = event_lib.classes.rule.byName[ruleName](self.event) + ok, msg = self.event.checkAndAddRule(rule) + if not ok: + return + hbox = self.makeRuleHbox(rule) + if not hbox: + return + pack(self.rulesBox, hbox) + del self.addRuleModel[ci] + n = len(self.addRuleModel) + if ci==n: + self.addRuleCombo.set_active(ci-1) + else: + self.addRuleCombo.set_active(ci) + hbox.show_all() + + diff --git a/scal3/ui_gtk/event/event/dailyNote.py b/scal3/ui_gtk/event/event/dailyNote.py new file mode 100644 index 000000000..20e500ddf --- /dev/null +++ b/scal3/ui_gtk/event/event/dailyNote.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from scal3.cal_types import convert +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.date import DateButton +from scal3.ui_gtk.event import common + + +class WidgetClass(common.WidgetClass): + def __init__(self, event): + common.WidgetClass.__init__(self, event) + ### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Date'))) + self.dateInput = DateButton() + pack(hbox, self.dateInput) + pack(self, hbox) + ############# + #self.filesBox = common.FilesBox(self.event) + #pack(self, self.filesBox) + def updateWidget(self): + common.WidgetClass.updateWidget(self) + self.dateInput.set_value(self.event.getDate()) + def updateVars(self): + common.WidgetClass.updateVars(self) + self.event.setDate(*self.dateInput.get_value()) + def modeComboChanged(self, obj=None):## overwrite method from common.WidgetClass + newMode = self.modeCombo.get_active() + self.dateInput.changeMode(self.event.mode, newMode) + self.event.mode = newMode + + + diff --git a/scal3/ui_gtk/event/event/largeScale.py b/scal3/ui_gtk/event/event/largeScale.py new file mode 100644 index 000000000..ebae2a58f --- /dev/null +++ b/scal3/ui_gtk/event/event/largeScale.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton +from scal3.ui_gtk.event import common + +maxStart = 999999 +maxDur = 99999 + +class WidgetClass(common.WidgetClass): + def __init__(self, event):## FIXME + common.WidgetClass.__init__(self, event) + ###### + sizeGroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ###### + hbox = gtk.HBox() + label = gtk.Label(_('Scale')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.scaleCombo = common.Scale10PowerComboBox() + pack(hbox, self.scaleCombo) + pack(self, hbox) + #### + hbox = gtk.HBox() + label = gtk.Label(_('Start')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.startSpin = IntSpinButton(-maxStart, maxStart) + self.startSpin.connect('changed', self.startSpinChanged) + pack(hbox, self.startSpin) + pack(self, hbox) + #### + hbox = gtk.HBox() + self.endRelCombo = gtk.ComboBoxText() + for item in ('Duration', 'End'): + self.endRelCombo.append_text(_(item)) + self.endRelCombo.connect('changed', self.endRelComboChanged) + sizeGroup.add_widget(self.endRelCombo) + pack(hbox, self.endRelCombo) + self.endSpin = IntSpinButton(-maxDur, maxDur) + pack(hbox, self.endSpin) + pack(self, hbox) + #### + self.endRelComboChanged() + def endRelComboChanged(self, combo=None): + rel = self.endRelCombo.get_active() + start = self.startSpin.get_value() + end = self.endSpin.get_value() + if rel==0:## reletive(duration) + self.endSpin.set_range(1, maxStart) + self.endSpin.set_value(max(1, end-start)) + elif rel==1:## absolute(end) + self.endSpin.set_range(start+1, maxStart) + self.endSpin.set_value(max(start+1, start+end)) + def startSpinChanged(self, spin=None): + if self.endRelCombo.get_active() == 1:## absolute(end) + self.endSpin.set_range(self.startSpin.get_value()+1, maxStart) + def updateWidget(self): + common.WidgetClass.updateWidget(self) + self.scaleCombo.set_value(self.event.scale) + self.startSpin.set_value(self.event.start) + self.endRelCombo.set_active(0 if self.event.endRel else 1) + self.endSpin.set_value(self.event.end) + def updateVars(self):## FIXME + common.WidgetClass.updateVars(self) + self.event.scale = self.scaleCombo.get_value() + self.event.start = self.startSpin.get_value() + self.event.endRel = (self.endRelCombo.get_active()==0) + self.event.end = self.endSpin.get_value() + + + +if __name__=='__main__': + combo = Scale10PowerComboBox() + combo.set_value(200) + win = gtk.Dialog(parent=None) + pack(win.vbox, combo) + win.vbox.show_all() + win.run() + print(combo.get_value()) + + + + diff --git a/scal3/ui_gtk/event/event/lifeTime.py b/scal3/ui_gtk/event/event/lifeTime.py new file mode 100644 index 000000000..5be838429 --- /dev/null +++ b/scal3/ui_gtk/event/event/lifeTime.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.date import DateButton +from scal3.ui_gtk.mywidgets.ymd import YearMonthDayBox +from scal3.ui_gtk.event import common + + +class WidgetClass(common.WidgetClass): + def __init__(self, event):## FIXME + common.WidgetClass.__init__(self, event) + ###### + sizeGroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ###### + try: + seperated = event.parent.showSeperatedYmdInputs + except AttributeError: + seperated = False + if seperated: + self.startDateInput = YearMonthDayBox() + self.endDateInput = YearMonthDayBox() + else: + self.startDateInput = DateButton() + self.endDateInput = DateButton() + ###### + hbox = gtk.HBox() + label = gtk.Label(_('Start')+': ') + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + pack(hbox, self.startDateInput) + pack(self, hbox) + ###### + hbox = gtk.HBox() + label = gtk.Label(_('End')+': ') + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + pack(hbox, self.endDateInput) + pack(self, hbox) + ############# + #self.filesBox = common.FilesBox(self.event) + #pack(self, self.filesBox) + def updateWidget(self): + common.WidgetClass.updateWidget(self) + self.startDateInput.set_value(self.event['start'].date) + self.endDateInput.set_value(self.event['end'].date) + def updateVars(self):## FIXME + common.WidgetClass.updateVars(self) + self.event['start'].setDate(self.startDateInput.get_value()) + self.event['end'].setDate(self.endDateInput.get_value()) + def modeComboChanged(self, obj=None):## overwrite method from common.WidgetClass + newMode = self.modeCombo.get_active() + self.startDateInput.changeMode(self.event.mode, newMode) + self.endDateInput.changeMode(self.event.mode, newMode) + self.event.mode = newMode + + + + + + + + + + + + diff --git a/scal3/ui_gtk/event/event/monthly.py b/scal3/ui_gtk/event/event/monthly.py new file mode 100644 index 000000000..fa02f45bf --- /dev/null +++ b/scal3/ui_gtk/event/event/monthly.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3.cal_types import jd_to +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.day import DaySpinButton +from scal3.ui_gtk.mywidgets.multi_spin.date import DateButton +from scal3.ui_gtk.mywidgets.multi_spin.hour_minute import HourMinuteButton +from scal3.ui_gtk.event import common + +class WidgetClass(common.WidgetClass): + def __init__(self, event):## FIXME + event.setJd(ui.cell.jd) + common.WidgetClass.__init__(self, event) + ###### + sizeGroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ###### + hbox = gtk.HBox() + label = gtk.Label(_('Start')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.startDateInput = DateButton() + pack(hbox, self.startDateInput) + ### + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + ###### + hbox = gtk.HBox() + label = gtk.Label(_('End')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.endDateInput = DateButton() + pack(hbox, self.endDateInput) + ### + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + ###### + hbox = gtk.HBox() + label = gtk.Label(_('Day of Month')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.daySpin = DaySpinButton() + pack(hbox, self.daySpin) + ### + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + ######### + hbox = gtk.HBox() + label = gtk.Label(_('Time')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + ## + self.dayTimeStartInput = HourMinuteButton() + self.dayTimeEndInput = HourMinuteButton() + ## + pack(hbox, self.dayTimeStartInput) + pack(hbox, gtk.Label(' ' + _('to') + ' ')) + pack(hbox, self.dayTimeEndInput) + pack(self, hbox) + ############# + #self.notificationBox = common.NotificationBox(event) + #pack(self, self.notificationBox) + ############# + #self.filesBox = common.FilesBox(self.event) + #pack(self, self.filesBox) + def updateWidget(self):## FIXME + common.WidgetClass.updateWidget(self) + mode = self.event.mode + ### + self.startDateInput.set_value(jd_to(self.event.getStartJd(), mode)) + self.endDateInput.set_value(jd_to(self.event.getEndJd(), mode)) + ### + self.daySpin.set_value(self.event.getDay()) + ### + timeRangeRule = self.event['dayTimeRange'] + self.dayTimeStartInput.set_value(timeRangeRule.dayTimeStart) + self.dayTimeEndInput.set_value(timeRangeRule.dayTimeEnd) + def updateVars(self):## FIXME + common.WidgetClass.updateVars(self) + self.event['start'].setDate(self.startDateInput.get_value()) + self.event['end'].setDate(self.endDateInput.get_value()) + self.event.setDay(self.daySpin.get_value()) + ### + self.event['dayTimeRange'].setRange( + self.dayTimeStartInput.get_value(), + self.dayTimeEndInput.get_value(), + ) + def modeComboChanged(self, obj=None):## overwrite method from common.WidgetClass + newMode = self.modeCombo.get_active() + self.startDateInput.changeMode(self.event.mode, newMode) + self.endDateInput.changeMode(self.event.mode, newMode) + self.event.mode = newMode + + + diff --git a/scal3/ui_gtk/event/event/task.py b/scal3/ui_gtk/event/event/task.py new file mode 100644 index 000000000..549c85075 --- /dev/null +++ b/scal3/ui_gtk/event/event/task.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.date import DateButton +from scal3.ui_gtk.mywidgets.multi_spin.time_b import TimeButton +from scal3.ui_gtk.event import common + +class WidgetClass(common.WidgetClass): + def __init__(self, event):## FIXME + common.WidgetClass.__init__(self, event) + ###### + sizeGroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ###### + hbox = gtk.HBox() + label = gtk.Label(_('Start')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.startDateInput = DateButton() + pack(hbox, self.startDateInput) + ## + pack(hbox, gtk.Label(' '+_('Time'))) + self.startTimeInput = TimeButton() + pack(hbox, self.startTimeInput) + ## + pack(self, hbox) + ###### + hbox = gtk.HBox() + self.endTypeCombo = gtk.ComboBoxText() + for item in ('Duration', 'End'): + self.endTypeCombo.append_text(_(item)) + self.endTypeCombo.connect('changed', self.endTypeComboChanged) + sizeGroup.add_widget(self.endTypeCombo) + pack(hbox, self.endTypeCombo) + #### + self.durationBox = common.DurationInputBox() + pack(hbox, self.durationBox, 1, 1) + #### + self.endDateHbox = gtk.HBox() + self.endDateInput = DateButton() + pack(self.endDateHbox, self.endDateInput) + ## + pack(self.endDateHbox, gtk.Label(' '+_('Time'))) + self.endTimeInput = TimeButton() + pack(self.endDateHbox, self.endTimeInput) + ## + pack(hbox, self.endDateHbox, 1, 1) + #### + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + ############# + self.notificationBox = common.NotificationBox(event) + pack(self, self.notificationBox) + ############# + #self.filesBox = common.FilesBox(self.event) + #pack(self, self.filesBox) + def endTypeComboChanged(self, combo=None): + active = self.endTypeCombo.get_active() + if active==0:## duration + self.durationBox.show() + self.endDateHbox.hide() + elif active==1:## end date + self.durationBox.hide() + self.endDateHbox.show() + else: + raise RuntimeError + def updateWidget(self):## FIXME + common.WidgetClass.updateWidget(self) + ### + startDate, startTime = self.event.getStart() + self.startDateInput.set_value(startDate) + self.startTimeInput.set_value(startTime) + ### + endType, values = self.event.getEnd() + if endType=='duration': + self.endTypeCombo.set_active(0) + self.durationBox.setDuration(*values) + self.endDateInput.set_value(startDate)## FIXME + self.endTimeInput.set_value(startTime)## FIXME + elif endType=='date': + self.endTypeCombo.set_active(1) + self.endDateInput.set_value(values[0]) + self.endTimeInput.set_value(values[1]) + else: + raise RuntimeError + self.endTypeComboChanged() + def updateVars(self):## FIXME + common.WidgetClass.updateVars(self) + self.event.setStart(self.startDateInput.get_value(), self.startTimeInput.get_value()) + ### + active = self.endTypeCombo.get_active() + if active==0: + self.event.setEnd('duration', *self.durationBox.getDuration()) + elif active==1: + self.event.setEnd( + 'date', + self.endDateInput.get_value(), + self.endTimeInput.get_value(), + ) + def modeComboChanged(self, obj=None):## overwrite method from common.WidgetClass + newMode = self.modeCombo.get_active() + self.startDateInput.changeMode(self.event.mode, newMode) + self.endDateInput.changeMode(self.event.mode, newMode) + self.event.mode = newMode + + + diff --git a/scal3/ui_gtk/event/event/universityClass.py b/scal3/ui_gtk/event/event/universityClass.py new file mode 100644 index 000000000..a124e81ff --- /dev/null +++ b/scal3/ui_gtk/event/event/universityClass.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import showError +from scal3.ui_gtk.mywidgets import TextFrame +from scal3.ui_gtk.mywidgets.multi_spin.option_box.hour_minute import HourMinuteButtonOption +from scal3.ui_gtk.mywidgets.icon import IconSelectButton +from scal3.ui_gtk.mywidgets.weekday_combo import WeekDayComboBox +from scal3.ui_gtk.event import common +from scal3.ui_gtk.event.rule.weekNumMode import WidgetClass as WeekNumModeWidgetClass + + + +class WidgetClass(gtk.VBox): + def __init__(self, event):## FIXME + gtk.VBox.__init__(self) + self.event = event + assert event.parent.name == 'universityTerm' ## FIXME + sizeGroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ##### + if not event.parent.courses: + showError(event.parent.noCourseError, ui.eventManDialog) + raise RuntimeError('No courses added') + self.courseIds = [] + self.courseNames = [] + combo = gtk.ComboBoxText() + for course in event.parent.courses: + self.courseIds.append(course[0]) + self.courseNames.append(course[1]) + combo.append_text(course[1]) + #combo.connect('changed', self.updateSummary) + self.courseCombo = combo + ## + hbox = gtk.HBox() + label = gtk.Label(_('Course')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + pack(hbox, combo) + ## + pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Week')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.weekNumModeCombo = WeekNumModeWidgetClass(event['weekNumMode']) + pack(hbox, self.weekNumModeCombo) + pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Week Day')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.weekDayCombo = WeekDayComboBox() + #self.weekDayCombo.connect('changed', self.updateSummary) + pack(hbox, self.weekDayCombo) + pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Time')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + ## + self.dayTimeStartCombo = HourMinuteButtonOption() + self.dayTimeEndCombo = HourMinuteButtonOption() + ## + #self.dayTimeStartCombo.get_child().set_direction(gtk.TextDirection.LTR) + #self.dayTimeEndCombo.get_child().set_direction(gtk.TextDirection.LTR) + ## + pack(hbox, self.dayTimeStartCombo) + pack(hbox, gtk.Label(' ' + _('to') + ' ')) + pack(hbox, self.dayTimeEndCombo) + pack(self, hbox) + ########### + #hbox = gtk.HBox() + #label = gtk.Label(_('Summary')) + #label.set_alignment(0, 0.5) + #sizeGroup.add_widget(label) + #pack(hbox, label) + #self.summaryEntry = gtk.Entry() + #pack(hbox, self.summaryEntry, 1, 1) + #pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Description')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.descriptionInput = TextFrame() + pack(hbox, self.descriptionInput, 1, 1) + pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Icon')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.iconSelect = IconSelectButton() + #print(join(pixDir, self.icon)) + pack(hbox, self.iconSelect) + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + ###### + self.notificationBox = common.NotificationBox(event) + pack(self, self.notificationBox) + ###### + #self.filesBox = common.FilesBox(self.event) + #pack(self, self.filesBox) + ###### + self.courseCombo.set_active(0) + #self.updateSummary() + def focusSummary(self): + pass + #def updateSummary(self, widget=None): + # courseIndex = self.courseCombo.get_active() + # summary = _('%s Class')%self.courseNames[courseIndex] + ' (' + self.weekDayCombo.get_active_text() + ')' + # self.summaryEntry.set_text(summary) + # self.event.summary = summary + def updateWidget(self):## FIXME + if self.event.courseId is None: + pass + else: + self.courseCombo.set_active(self.courseIds.index(self.event.courseId)) + ## + self.weekNumModeCombo.updateWidget() + weekDayList = self.event['weekDay'].weekDayList + if len(weekDayList)==1: + self.weekDayCombo.setValue(weekDayList[0])## FIXME + else: + self.weekDayCombo.set_active(0) + ## + self.dayTimeStartCombo.clear_history() + self.dayTimeEndCombo.clear_history() + for hm in reversed(self.event.parent.classTimeBounds): + for combo in (self.dayTimeStartCombo, self.dayTimeEndCombo): + combo.set_value(hm) + combo.add_history() + timeRangeRule = self.event['dayTimeRange'] + self.dayTimeStartCombo.set_value(timeRangeRule.dayTimeStart) + self.dayTimeEndCombo.set_value(timeRangeRule.dayTimeEnd) + #### + #self.summaryEntry.set_text(self.event.summary) + self.descriptionInput.set_text(self.event.description) + self.iconSelect.set_filename(self.event.icon) + #### + self.notificationBox.updateWidget() + #### + #self.filesBox.updateWidget() + def updateVars(self):## FIXME + courseIndex = self.courseCombo.get_active() + if courseIndex is None: + showError(_('No course is selected'), ui.eventManDialog) + raise RuntimeError('No courses is selected') + else: + self.event.courseId = self.courseIds[courseIndex] + ## + self.weekNumModeCombo.updateVars() + self.event['weekDay'].weekDayList = [self.weekDayCombo.getValue()]## FIXME + ## + self.event['dayTimeRange'].setRange( + self.dayTimeStartCombo.get_value(), + self.dayTimeEndCombo.get_value(), + ) + #### + #self.event.summary = self.summaryEntry.get_text() + self.event.description = self.descriptionInput.get_text() + self.event.icon = self.iconSelect.get_filename() + #### + self.notificationBox.updateVars() + self.event.updateSummary() + + + diff --git a/scal3/ui_gtk/event/event/universityExam.py b/scal3/ui_gtk/event/event/universityExam.py new file mode 100644 index 000000000..2fdfd5f00 --- /dev/null +++ b/scal3/ui_gtk/event/event/universityExam.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import showError +from scal3.ui_gtk.mywidgets import TextFrame +from scal3.ui_gtk.mywidgets.multi_spin.date import DateButton +from scal3.ui_gtk.mywidgets.multi_spin.hour_minute import HourMinuteButton +from scal3.ui_gtk.mywidgets.icon import IconSelectButton +from scal3.ui_gtk.event import common + +class WidgetClass(gtk.VBox): + def __init__(self, event):## FIXME + gtk.VBox.__init__(self) + self.event = event + assert event.parent.name == 'universityTerm' ## FIXME + sizeGroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ##### + if not event.parent.courses: + showError(event.parent.noCourseError, ui.eventManDialog) + raise RuntimeError('No courses added') + self.courseIds = [] + self.courseNames = [] + combo = gtk.ComboBoxText() + for course in event.parent.courses: + self.courseIds.append(course[0]) + self.courseNames.append(course[1]) + combo.append_text(course[1]) + #combo.connect('changed', self.updateSummary) + self.courseCombo = combo + ## + hbox = gtk.HBox() + label = gtk.Label(_('Course')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + pack(hbox, combo) + ## + pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Date')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.dateInput = DateButton() + pack(hbox, self.dateInput) + pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Time')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + ## + self.dayTimeStartCombo = HourMinuteButton() + self.dayTimeEndCombo = HourMinuteButton() + ## + #self.dayTimeStartCombo.get_child().set_direction(gtk.TextDirection.LTR) + #self.dayTimeEndCombo.get_child().set_direction(gtk.TextDirection.LTR) + ## + pack(hbox, self.dayTimeStartCombo) + pack(hbox, gtk.Label(' ' + _('to') + ' ')) + pack(hbox, self.dayTimeEndCombo) + pack(self, hbox) + ########### + #hbox = gtk.HBox() + #label = gtk.Label(_('Summary')) + #label.set_alignment(0, 0.5) + #sizeGroup.add_widget(label) + #pack(hbox, label) + #self.summaryEntry = gtk.Entry() + #pack(hbox, self.summaryEntry, 1, 1) + #pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Description')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.descriptionInput = TextFrame() + pack(hbox, self.descriptionInput, 1, 1) + pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Icon')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.iconSelect = IconSelectButton() + #print(join(pixDir, self.icon)) + pack(hbox, self.iconSelect) + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + ###### + self.notificationBox = common.NotificationBox(event) + pack(self, self.notificationBox) + ###### + #self.filesBox = common.FilesBox(self.event) + #pack(self, self.filesBox) + ###### + self.courseCombo.set_active(0) + #self.updateSummary() + def focusSummary(self): + pass + #def updateSummary(self, widget=None): + # courseIndex = self.courseCombo.get_active() + # summary = _('%s Exam')%self.courseNames[courseIndex] + # self.summaryEntry.set_text(summary) + # self.event.summary = summary + def updateWidget(self):## FIXME + if self.event.courseId is None: + pass + else: + self.courseCombo.set_active(self.courseIds.index(self.event.courseId)) + ## + self.dateInput.set_value(self.event.getDate()) + ## + timeRangeRule = self.event['dayTimeRange'] + self.dayTimeStartCombo.set_value(timeRangeRule.dayTimeStart) + self.dayTimeEndCombo.set_value(timeRangeRule.dayTimeEnd) + #### + #self.summaryEntry.set_text(self.event.summary) + self.descriptionInput.set_text(self.event.description) + self.iconSelect.set_filename(self.event.icon) + #### + self.notificationBox.updateWidget() + #### + #self.filesBox.updateWidget() + def updateVars(self):## FIXME + courseIndex = self.courseCombo.get_active() + if courseIndex is None: + showError(_('No course is selected'), ui.eventManDialog) + raise RuntimeError('No courses is selected') + else: + self.event.courseId = self.courseIds[courseIndex] + ## + self.event.setDate(*tuple(self.dateInput.get_value())) + ## + self.event['dayTimeRange'].setRange( + self.dayTimeStartCombo.get_value(), + self.dayTimeEndCombo.get_value(), + ) + #### + #self.event.summary = self.summaryEntry.get_text() + self.event.description = self.descriptionInput.get_text() + self.event.icon = self.iconSelect.get_filename() + #### + self.notificationBox.updateVars() + self.event.updateSummary() + def modeComboChanged(self, obj=None):## overwrite method from common.WidgetClass + newMode = self.modeCombo.get_active() + self.dateInput.changeMode(self.event.mode, newMode) + self.event.mode = newMode + + diff --git a/scal3/ui_gtk/event/event/weekly.py b/scal3/ui_gtk/event/event/weekly.py new file mode 100644 index 000000000..dc2041170 --- /dev/null +++ b/scal3/ui_gtk/event/event/weekly.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3.cal_types import jd_to +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton +from scal3.ui_gtk.mywidgets.multi_spin.date import DateButton +from scal3.ui_gtk.mywidgets.multi_spin.hour_minute import HourMinuteButton +from scal3.ui_gtk.event import common + +class WidgetClass(common.WidgetClass): + def __init__(self, event):## FIXME + common.WidgetClass.__init__(self, event) + ###### + sizeGroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ###### + hbox = gtk.HBox() + label = gtk.Label(_('Start')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.startDateInput = DateButton() + pack(hbox, self.startDateInput) + ### + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + ###### + hbox = gtk.HBox() + label = gtk.Label(_('Repeat Every ')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.weeksSpin = IntSpinButton(1, 99999) + pack(hbox, self.weeksSpin) + pack(hbox, gtk.Label(' '+_(' Weeks'))) + ### + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + ###### + hbox = gtk.HBox() + label = gtk.Label(_('End')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.endDateInput = DateButton() + pack(hbox, self.endDateInput) + ### + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + ######### + hbox = gtk.HBox() + label = gtk.Label(_('Time')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + ## + self.dayTimeStartInput = HourMinuteButton() + self.dayTimeEndInput = HourMinuteButton() + ## + pack(hbox, self.dayTimeStartInput) + pack(hbox, gtk.Label(' ' + _('to') + ' ')) + pack(hbox, self.dayTimeEndInput) + pack(self, hbox) + ############# + #self.notificationBox = common.NotificationBox(event) + #pack(self, self.notificationBox) + ############# + #self.filesBox = common.FilesBox(self.event) + #pack(self, self.filesBox) + def updateWidget(self):## FIXME + common.WidgetClass.updateWidget(self) + mode = self.event.mode + ### + self.startDateInput.set_value(jd_to(self.event.getStartJd(), mode)) + self.weeksSpin.set_value(self.event['cycleWeeks'].weeks) + self.endDateInput.set_value(jd_to(self.event.getEndJd(), mode)) + ### + timeRangeRule = self.event['dayTimeRange'] + self.dayTimeStartInput.set_value(timeRangeRule.dayTimeStart) + self.dayTimeEndInput.set_value(timeRangeRule.dayTimeEnd) + def updateVars(self):## FIXME + common.WidgetClass.updateVars(self) + self.event['start'].setDate(self.startDateInput.get_value()) + self.event['end'].setDate(self.endDateInput.get_value()) + self.event['cycleWeeks'].setData(self.weeksSpin.get_value()) + ### + self.event['dayTimeRange'].setRange( + self.dayTimeStartInput.get_value(), + self.dayTimeEndInput.get_value(), + ) + def modeComboChanged(self, obj=None):## overwrite method from common.WidgetClass + newMode = self.modeCombo.get_active() + self.startDateInput.changeMode(self.event.mode, newMode) + self.endDateInput.changeMode(self.event.mode, newMode) + self.event.mode = newMode + + + diff --git a/scal3/ui_gtk/event/event/yearly.py b/scal3/ui_gtk/event/event/yearly.py new file mode 100644 index 000000000..fa08df265 --- /dev/null +++ b/scal3/ui_gtk/event/event/yearly.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3.cal_types import calTypes, convert +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.month_combo import MonthComboBox +from scal3.ui_gtk.mywidgets.multi_spin.year import YearSpinButton +from scal3.ui_gtk.mywidgets.multi_spin.day import DaySpinButton +from scal3.ui_gtk.event import common + + +class WidgetClass(common.WidgetClass): + def __init__(self, event):## FIXME + common.WidgetClass.__init__(self, event) + ################ + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Month'))) + self.monthCombo = MonthComboBox() + self.monthCombo.build(event.mode) + pack(hbox, self.monthCombo) + pack(hbox, gtk.Label(''), 1, 1) + #pack(self, hbox) + ### + #hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Day'))) + self.daySpin = DaySpinButton() + pack(hbox, self.daySpin) + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + ### + hbox = gtk.HBox() + self.startYearCheck = gtk.CheckButton(_('Start Year')) + pack(hbox, self.startYearCheck) + self.startYearSpin = YearSpinButton() + pack(hbox, self.startYearSpin) + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + self.startYearCheck.connect('clicked', self.startYearCheckClicked) + #### + self.notificationBox = common.NotificationBox(event) + pack(self, self.notificationBox) + #### + #self.filesBox = common.FilesBox(self.event) + #pack(self, self.filesBox) + startYearCheckClicked = lambda self, obj=None: self.startYearSpin.set_sensitive(self.startYearCheck.get_active()) + def updateWidget(self):## FIXME + common.WidgetClass.updateWidget(self) + self.monthCombo.setValue(self.event.getMonth()) + self.daySpin.set_value(self.event.getDay()) + try: + startRule = self.event['start'] + except: + self.startYearCheck.set_active(False) + self.startYearSpin.set_value(self.event.getSuggestedStartYear()) + else: + self.startYearCheck.set_active(True) + self.startYearSpin.set_value(startRule.date[0]) + self.startYearCheckClicked() + def updateVars(self):## FIXME + common.WidgetClass.updateVars(self) + self.event.setMonth(self.monthCombo.getValue()) + self.event.setDay(int(self.daySpin.get_value())) + if self.startYearCheck.get_active(): + startRule = self.event.getAddRule('start') + startRule.date = (self.startYearSpin.get_value(), 1, 1, 0) + else: + try: + del self.event['start'] + except KeyError: + pass + def modeComboChanged(self, obj=None):## overwrite method from common.WidgetClass + newMode = self.modeCombo.get_active() + module = calTypes[newMode] + monthCombo = self.monthCombo + month = monthCombo.getValue() + monthCombo.build(newMode) + y2, m2, d2 = convert( + int(self.startYearSpin.get_value()), + month, + int(self.daySpin.get_value()), + self.event.mode, + newMode, + ) + self.startYearSpin.set_value(y2) + monthCombo.setValue(m2) + self.daySpin.set_value(d2) + self.event.mode = newMode + + + diff --git a/scal3/ui_gtk/event/export.py b/scal3/ui_gtk/event/export.py new file mode 100644 index 000000000..2946a5a0c --- /dev/null +++ b/scal3/ui_gtk/event/export.py @@ -0,0 +1,170 @@ +from os.path import join, split, splitext + +from scal3.path import deskDir +from scal3.json_utils import * +from scal3 import core +from scal3.core import DATE_GREG +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import dialog_add_button +from scal3.ui_gtk.mywidgets.dialog import MyDialog +from scal3.ui_gtk.event.common import GroupsTreeCheckList + + +class SingleGroupExportDialog(gtk.Dialog, MyDialog): + def __init__(self, group, **kwargs): + self._group = group + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Export Group')) + #### + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), gtk.ResponseType.CANCEL) + dialog_add_button(self, gtk.STOCK_OK, _('_OK'), gtk.ResponseType.OK) + self.connect('response', lambda w, e: self.hide()) + #### + hbox = gtk.HBox() + frame = gtk.Frame() + frame.set_label(_('Format')) + radioBox = gtk.VBox() + ## + self.radioIcs = gtk.RadioButton(label='iCalendar') + self.radioJsonCompact = gtk.RadioButton(label=_('Compact JSON (StarCalendar)'), group=self.radioIcs) + self.radioJsonPretty = gtk.RadioButton(label=_('Pretty JSON (StarCalendar)'), group=self.radioIcs) + ## + pack(radioBox, self.radioJsonCompact) + pack(radioBox, self.radioJsonPretty) + pack(radioBox, self.radioIcs) + ## + self.radioJsonCompact.set_active(True) + self.radioIcs.connect('clicked', self.formatRadioChanged) + self.radioJsonCompact.connect('clicked', self.formatRadioChanged) + self.radioJsonPretty.connect('clicked', self.formatRadioChanged) + ## + frame.add(radioBox) + pack(hbox, frame) + pack(hbox, gtk.Label(''), 1, 1) + pack(self.vbox, hbox) + ######## + self.fcw = gtk.FileChooserWidget(action=gtk.FileChooserAction.SAVE) + try: + self.fcw.set_current_folder(deskDir) + except AttributeError:## PyGTK < 2.4 + pass + pack(self.vbox, self.fcw, 1, 1) + #### + self.vbox.show_all() + self.formatRadioChanged() + def formatRadioChanged(self, widget=None): + fpath = self.fcw.get_filename() + if fpath: + fname_nox, ext = splitext(split(fpath)[1]) + else: + fname_nox, ext = '', '' + if not fname_nox: + fname_nox = core.fixStrForFileName(self._group.title) + if self.radioIcs.get_active(): + if ext != '.ics': + ext = '.ics' + else: + if ext != '.json': + ext = '.json' + self.fcw.set_current_name(fname_nox + ext) + def save(self): + fpath = self.fcw.get_filename() + if self.radioJsonCompact.get_active(): + text = dataToCompactJson(ui.eventGroups.exportData([self._group.id])) + open(fpath, 'wb').write(text) + elif self.radioJsonPretty.get_active(): + text = dataToPrettyJson(ui.eventGroups.exportData([self._group.id])) + open(fpath, 'wb').write(text) + elif self.radioIcs.get_active(): + ui.eventGroups.exportToIcs(fpath, [self._group.id]) + def run(self): + if gtk.Dialog.run(self)==gtk.ResponseType.OK: + self.waitingDo(self.save) + self.destroy() + + +class MultiGroupExportDialog(gtk.Dialog, MyDialog): + def __init__(self, **kwargs): + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Export')) + self.vbox.set_spacing(10) + #### + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), gtk.ResponseType.CANCEL) + dialog_add_button(self, gtk.STOCK_OK, _('_OK'), gtk.ResponseType.OK) + #### + hbox = gtk.HBox() + frame = gtk.Frame() + frame.set_label(_('Format')) + radioBox = gtk.VBox() + ## + self.radioIcs = gtk.RadioButton(label='iCalendar') + self.radioJsonCompact = gtk.RadioButton(label=_('Compact JSON (StarCalendar)'), group=self.radioIcs) + self.radioJsonPretty = gtk.RadioButton(label=_('Pretty JSON (StarCalendar)'), group=self.radioIcs) + ## + pack(radioBox, self.radioJsonCompact) + pack(radioBox, self.radioJsonPretty) + pack(radioBox, self.radioIcs) + ## + self.radioJsonCompact.set_active(True) + self.radioIcs.connect('clicked', self.formatRadioChanged) + self.radioJsonCompact.connect('clicked', self.formatRadioChanged) + self.radioJsonPretty.connect('clicked', self.formatRadioChanged) + ## + frame.add(radioBox) + pack(hbox, frame) + pack(hbox, gtk.Label(''), 1, 1) + pack(self.vbox, hbox) + ######## + hbox = gtk.HBox(spacing=2) + pack(hbox, gtk.Label(_('File')+':')) + self.fpathEntry = gtk.Entry() + self.fpathEntry.set_text(join(deskDir, 'events-%.4d-%.2d-%.2d'%core.getSysDate(DATE_GREG))) + pack(hbox, self.fpathEntry, 1, 1) + pack(self.vbox, hbox) + #### + self.groupSelect = GroupsTreeCheckList() + swin = gtk.ScrolledWindow() + swin.add(self.groupSelect) + swin.set_policy(gtk.PolicyType.AUTOMATIC, gtk.PolicyType.AUTOMATIC) + pack(self.vbox, swin, 1, 1) + #### + self.vbox.show_all() + self.formatRadioChanged() + self.resize(600, 600) + def formatRadioChanged(self, widget=None): + #self.dateRangeBox.set_visible(self.radioIcs.get_active()) + ### + fpath = self.fpathEntry.get_text() + if fpath: + fpath_nox, ext = splitext(fpath) + if fpath_nox: + if self.radioIcs.get_active(): + if ext != '.ics': + ext = '.ics' + else: + if ext != '.json': + ext = '.json' + self.fpathEntry.set_text(fpath_nox + ext) + def save(self): + fpath = self.fpathEntry.get_text() + activeGroupIds = self.groupSelect.getValue() + if self.radioIcs.get_active(): + ui.eventGroups.exportToIcs(fpath, activeGroupIds) + else: + data = ui.eventGroups.exportData(activeGroupIds) + ## what to do with all groupData['info'] s? FIXME + if self.radioJsonCompact.get_active(): + text = dataToCompactJson(data) + elif self.radioJsonPretty.get_active(): + text = dataToPrettyJson(data) + else: + raise RuntimeError + open(fpath, 'w').write(text) + def run(self): + if gtk.Dialog.run(self)==gtk.ResponseType.OK: + self.waitingDo(self.save) + self.destroy() + diff --git a/scal3/ui_gtk/event/group/__init__.py b/scal3/ui_gtk/event/group/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scal3/ui_gtk/event/group/base.py b/scal3/ui_gtk/event/group/base.py new file mode 100644 index 000000000..3211b97ad --- /dev/null +++ b/scal3/ui_gtk/event/group/base.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3 import core +from scal3.locale_man import tr as _ + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import set_tooltip +from scal3.ui_gtk.mywidgets import MyColorButton, TextFrame +from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton +from scal3.ui_gtk.mywidgets.icon import IconSelectButton +from scal3.ui_gtk.event import common + + +class BaseWidgetClass(gtk.VBox): + def __init__(self, group): + from scal3.ui_gtk.mywidgets.cal_type_combo import CalTypeCombo + gtk.VBox.__init__(self) + self.group = group + ######## + self.sizeGroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Title')) + label.set_alignment(0, 0.5) + pack(hbox, label) + self.sizeGroup.add_widget(label) + self.titleEntry = gtk.Entry() + pack(hbox, self.titleEntry, 1, 1) + pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Color')) + label.set_alignment(0, 0.5) + pack(hbox, label) + self.sizeGroup.add_widget(label) + self.colorButton = MyColorButton() + self.colorButton.set_use_alpha(True) ## FIXME + pack(hbox, self.colorButton) + pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Default Icon'))## FIXME + label.set_alignment(0, 0.5) + pack(hbox, label) + self.sizeGroup.add_widget(label) + self.iconSelect = IconSelectButton() + pack(hbox, self.iconSelect) + pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Default Calendar Type')) + label.set_alignment(0, 0.5) + pack(hbox, label) + self.sizeGroup.add_widget(label) + combo = CalTypeCombo() + pack(hbox, combo) + pack(hbox, gtk.Label(''), 1, 1) + self.modeCombo = combo + pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Show in Calendar')) + label.set_alignment(0, 0.5) + pack(hbox, label) + self.sizeGroup.add_widget(label) + self.showInDCalCheck = gtk.CheckButton(_('Day')) + self.showInWCalCheck = gtk.CheckButton(_('Week')) + self.showInMCalCheck = gtk.CheckButton(_('Month')) + pack(hbox, self.showInDCalCheck) + pack(hbox, gtk.Label(''), 1, 1) + pack(hbox, self.showInWCalCheck) + pack(hbox, gtk.Label(''), 1, 1) + pack(hbox, self.showInMCalCheck) + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Show in')) + label.set_alignment(0, 0.5) + pack(hbox, label) + self.sizeGroup.add_widget(label) + self.showInTimeLineCheck = gtk.CheckButton(_('Time Line')) + self.showInStatusIconCheck = gtk.CheckButton(_('Status Icon')) + pack(hbox, self.showInTimeLineCheck) + pack(hbox, gtk.Label(''), 1, 1) + pack(hbox, self.showInStatusIconCheck) + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Event Cache Size')) + label.set_alignment(0, 0.5) + pack(hbox, label) + self.sizeGroup.add_widget(label) + self.cacheSizeSpin = IntSpinButton(0, 9999) + pack(hbox, self.cacheSizeSpin) + pack(self, hbox) + ##### + hbox = gtk.HBox() + label = gtk.Label(_('Event Text Seperator')) + label.set_alignment(0, 0.5) + pack(hbox, label) + self.sizeGroup.add_widget(label) + self.sepInput = TextFrame() + pack(hbox, self.sepInput, 1, 1) + pack(self, hbox) + set_tooltip(hbox, _('Using to seperate Summary and Description when displaying event')) + ##### + #hbox = gtk.HBox() + #label = gtk.Label(_('Show Full Event Description')) + #label.set_alignment(0, 0.5) + #pack(hbox, label) + #self.sizeGroup.add_widget(label) + #self.showFullEventDescCheck = gtk.CheckButton('') + #pack(hbox, self.showFullEventDescCheck, 1, 1) + #pack(self, hbox) + ### + self.modeCombo.connect('changed', self.modeComboChanged)## right place? before updateWidget? FIXME + def updateWidget(self): + self.titleEntry.set_text(self.group.title) + self.colorButton.set_color(self.group.color) + self.iconSelect.set_filename(self.group.icon) + self.modeCombo.set_active(self.group.mode) + self.showInDCalCheck.set_active(self.group.showInDCal) + self.showInWCalCheck.set_active(self.group.showInWCal) + self.showInMCalCheck.set_active(self.group.showInMCal) + self.showInTimeLineCheck.set_active(self.group.showInTimeLine) + self.showInStatusIconCheck.set_active(self.group.showInStatusIcon) + self.cacheSizeSpin.set_value(self.group.eventCacheSize) + self.sepInput.set_text(self.group.eventTextSep) + #self.showFullEventDescCheck.set_active(self.group.showFullEventDesc) + def updateVars(self): + self.group.title = self.titleEntry.get_text() + self.group.color = self.colorButton.get_color() + self.group.icon = self.iconSelect.get_filename() + self.group.mode = self.modeCombo.get_active() + self.group.showInDCal = self.showInDCalCheck.get_active() + self.group.showInWCal = self.showInWCalCheck.get_active() + self.group.showInMCal = self.showInMCalCheck.get_active() + self.group.showInTimeLine = self.showInTimeLineCheck.get_active() + self.group.showInStatusIcon = self.showInStatusIconCheck.get_active() + self.group.eventCacheSize = int(self.cacheSizeSpin.get_value()) + self.group.eventTextSep = self.sepInput.get_text() + #self.group.showFullEventDesc = self.showFullEventDescCheck.get_active() + def modeComboChanged(self, obj=None): + pass + + + diff --git a/scal3/ui_gtk/event/group/editor.py b/scal3/ui_gtk/event/group/editor.py new file mode 100644 index 000000000..fd4540695 --- /dev/null +++ b/scal3/ui_gtk/event/group/editor.py @@ -0,0 +1,90 @@ +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import dialog_add_button +from scal3.ui_gtk.event import makeWidget +from scal3.ui_gtk.event.utils import checkEventsReadOnly + +class GroupEditorDialog(gtk.Dialog): + def __init__(self, group=None, **kwargs): + checkEventsReadOnly() + gtk.Dialog.__init__(self, **kwargs) + self.isNew = (group is None) + self.set_title(_('Add New Group') if self.isNew else _('Edit Group')) + #self.connect('delete-event', lambda obj, e: self.destroy()) + #self.resize(800, 600) + ### + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), gtk.ResponseType.CANCEL) + dialog_add_button(self, gtk.STOCK_OK, _('_OK'), gtk.ResponseType.OK) + self.connect('response', lambda w, e: self.hide()) + ####### + self.activeWidget = None + ####### + hbox = gtk.HBox() + combo = gtk.ComboBoxText() + for cls in event_lib.classes.group: + combo.append_text(cls.desc) + pack(hbox, gtk.Label(_('Group Type'))) + pack(hbox, combo) + pack(hbox, gtk.Label(''), 1, 1) + pack(self.vbox, hbox) + #### + if self.isNew: + self._group = event_lib.classes.group[event_lib.defaultGroupTypeIndex]() + combo.set_active(event_lib.defaultGroupTypeIndex) + else: + self._group = group + combo.set_active(event_lib.classes.group.names.index(group.name)) + self.activeWidget = None + combo.connect('changed', self.typeChanged) + self.comboType = combo + self.vbox.show_all() + self.typeChanged() + def dateModeChanged(self, combo): + pass + def getNewGroupTitle(self, baseTitle): + usedTitles = [group.title for group in ui.eventGroups] + if not baseTitle in usedTitles: + return baseTitle + i = 1 + while True: + newTitle = baseTitle + ' ' + _(i) + if newTitle in usedTitles: + i += 1 + else: + return newTitle + def typeChanged(self, combo=None): + if self.activeWidget: + self.activeWidget.updateVars() + self.activeWidget.destroy() + cls = event_lib.classes.group[self.comboType.get_active()] + group = cls() + if self.isNew: + group.setRandomColor() + if group.icon: + self._group.icon = group.icon + if not self.isNew: + group.copyFrom(self._group) + group.setId(self._group.id) + if self.isNew: + group.title = self.getNewGroupTitle(cls.desc) + self._group = group + self.activeWidget = makeWidget(group) + pack(self.vbox, self.activeWidget) + def run(self): + if self.activeWidget is None: + return None + if gtk.Dialog.run(self) != gtk.ResponseType.OK: + return None + self.activeWidget.updateVars() + self._group.save()## FIXME + if self.isNew: + event_lib.lastIds.save() + else: + ui.eventGroups[self._group.id] = self._group ## FIXME + self.destroy() + return self._group + diff --git a/scal3/ui_gtk/event/group/group.py b/scal3/ui_gtk/event/group/group.py new file mode 100644 index 000000000..764d3cf19 --- /dev/null +++ b/scal3/ui_gtk/event/group/group.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +from scal3.core import jd_to +from scal3.locale_man import tr as _ + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets import MyColorButton +from scal3.ui_gtk.mywidgets.multi_spin.date import DateButton +from scal3.ui_gtk.event.group.base import BaseWidgetClass +from scal3.ui_gtk.event.account import AccountCombo, AccountGroupBox + + +class WidgetClass(BaseWidgetClass): + def __init__(self, group): + BaseWidgetClass.__init__(self, group) + #### + hbox = gtk.HBox() + label = gtk.Label(_('Start')) + label.set_alignment(0, 0.5) + pack(hbox, label) + self.sizeGroup.add_widget(label) + self.startDateInput = DateButton() + pack(hbox, self.startDateInput) + pack(self, hbox) + ### + hbox = gtk.HBox() + label = gtk.Label(_('End')) + label.set_alignment(0, 0.5) + pack(hbox, label) + self.sizeGroup.add_widget(label) + self.endDateInput = DateButton() + pack(hbox, self.endDateInput) + pack(self, hbox) + ###### + exp = gtk.Expander() + exp.set_label(_('Online Service')) + vbox = gtk.VBox() + exp.add(vbox) + sizeGroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ## + hbox = gtk.HBox() + label = gtk.Label(_('Account')) + label.set_alignment(0, 0.5) + pack(hbox, label) + sizeGroup.add_widget(label) ## FIXME + self.accountCombo = AccountCombo() + pack(hbox, self.accountCombo) + pack(vbox, hbox) + ## + hbox = gtk.HBox() + label = gtk.Label(_('Remote Group')) + label.set_alignment(0, 0.5) + pack(hbox, label) + sizeGroup.add_widget(label) ## FIXME + accountGroupBox = AccountGroupBox(self.accountCombo) + pack(hbox, accountGroupBox, 1, 1) + pack(vbox, hbox) + self.accountGroupCombo = accountGroupBox.combo + ## + pack(self, exp) + def updateWidget(self): + BaseWidgetClass.updateWidget(self) + self.startDateInput.set_value(jd_to(self.group.startJd, self.group.mode)) + self.endDateInput.set_value(jd_to(self.group.endJd, self.group.mode)) + ### + if self.group.remoteIds: + aid, gid = self.group.remoteIds + else: + aid, gid = None, None + self.accountCombo.set_active(aid) + self.accountGroupCombo.set_active(gid) + def updateVars(self): + BaseWidgetClass.updateVars(self) + self.group.startJd = self.startDateInput.get_jd(self.group.mode) + self.group.endJd = self.endDateInput.get_jd(self.group.mode) + ### + aid = self.accountCombo.get_active() + if aid: + gid = self.accountGroupCombo.get_active() + self.group.remoteIds = aid, gid + else: + self.group.remoteIds = None + def modeComboChanged(self, obj=None): + newMode = self.modeCombo.get_active() + self.startDateInput.changeMode(self.group.mode, newMode) + self.endDateInput.changeMode(self.group.mode, newMode) + self.group.mode = newMode + + + diff --git a/scal3/ui_gtk/event/group/largeScale.py b/scal3/ui_gtk/event/group/largeScale.py new file mode 100644 index 000000000..769ed485d --- /dev/null +++ b/scal3/ui_gtk/event/group/largeScale.py @@ -0,0 +1,57 @@ +from scal3 import core +from scal3.locale_man import tr as _ + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton +from scal3.ui_gtk.event.group.base import BaseWidgetClass +from scal3.ui_gtk.event import common + + +maxStartEnd = 999999 + +class WidgetClass(BaseWidgetClass): + def __init__(self, group): + BaseWidgetClass.__init__(self, group) + ###### + sizeGroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ###### + hbox = gtk.HBox() + label = gtk.Label(_('Scale')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.scaleCombo = common.Scale10PowerComboBox() + pack(hbox, self.scaleCombo) + pack(self, hbox) + #### + hbox = gtk.HBox() + label = gtk.Label(_('Start')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.startSpin = IntSpinButton(-maxStartEnd, maxStartEnd) + pack(hbox, self.startSpin) + pack(self, hbox) + #### + hbox = gtk.HBox() + label = gtk.Label(_('End')) + label.set_alignment(0, 0.5) + sizeGroup.add_widget(label) + pack(hbox, label) + self.endSpin = IntSpinButton(-maxStartEnd, maxStartEnd) + pack(hbox, self.endSpin) + pack(self, hbox) + def updateWidget(self): + BaseWidgetClass.updateWidget(self) + self.scaleCombo.set_value(self.group.scale) + self.startSpin.set_value(self.group.getStartValue()) + self.endSpin.set_value(self.group.getEndValue()) + def updateVars(self): + BaseWidgetClass.updateVars(self) + self.group.scale = self.scaleCombo.get_value() + self.group.setStartValue(self.startSpin.get_value()) + self.group.setEndValue(self.endSpin.get_value()) + + + + diff --git a/scal3/ui_gtk/event/group/lifeTime.py b/scal3/ui_gtk/event/group/lifeTime.py new file mode 100644 index 000000000..0dd379b00 --- /dev/null +++ b/scal3/ui_gtk/event/group/lifeTime.py @@ -0,0 +1,25 @@ +from scal3 import core +from scal3.locale_man import tr as _ + +from scal3.ui_gtk import * +from scal3.ui_gtk.event.group.group import WidgetClass as NormalWidgetClass + + +class WidgetClass(NormalWidgetClass): + def __init__(self, group): + NormalWidgetClass.__init__(self, group) + #### + hbox = gtk.HBox() + self.showSeperatedYmdInputsCheck = gtk.CheckButton(_('Show Seperated Inputs for Year, Month and Day')) + pack(hbox, self.showSeperatedYmdInputsCheck) + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + def updateWidget(self): + NormalWidgetClass.updateWidget(self) + self.showSeperatedYmdInputsCheck.set_active(self.group.showSeperatedYmdInputs) + def updateVars(self): + NormalWidgetClass.updateVars(self) + self.group.showSeperatedYmdInputs = self.showSeperatedYmdInputsCheck.get_active() + + + diff --git a/scal3/ui_gtk/event/group/noteBook.py b/scal3/ui_gtk/event/group/noteBook.py new file mode 100644 index 000000000..91df43ba6 --- /dev/null +++ b/scal3/ui_gtk/event/group/noteBook.py @@ -0,0 +1,9 @@ +from scal3.ui_gtk.event.group.group import WidgetClass as NormalWidgetClass + + + +class WidgetClass(NormalWidgetClass): + pass + + + diff --git a/scal3/ui_gtk/event/group/taskList.py b/scal3/ui_gtk/event/group/taskList.py new file mode 100644 index 000000000..dd3837fba --- /dev/null +++ b/scal3/ui_gtk/event/group/taskList.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from scal3 import core +from scal3.locale_man import tr as _ + +from scal3.ui_gtk import * +from scal3.ui_gtk.event import common +from scal3.ui_gtk.event.group.group import WidgetClass as NormalWidgetClass + + +class WidgetClass(NormalWidgetClass): + def __init__(self, group): + NormalWidgetClass.__init__(self, group) + ### + hbox = gtk.HBox() + label = gtk.Label(_('Default Task Duration')) + label.set_alignment(0, 0.5) + pack(hbox, label) + self.sizeGroup.add_widget(label) + self.defaultDurationBox = common.DurationInputBox() + pack(hbox, self.defaultDurationBox) + pack(self, hbox) + def updateWidget(self):## FIXME + NormalWidgetClass.updateWidget(self) + self.defaultDurationBox.setDuration(*self.group.defaultDuration) + def updateVars(self): + NormalWidgetClass.updateVars(self) + self.group.defaultDuration = self.defaultDurationBox.getDuration() + + diff --git a/scal3/ui_gtk/event/group/universityTerm.py b/scal3/ui_gtk/event/group/universityTerm.py new file mode 100644 index 000000000..87edb8c9f --- /dev/null +++ b/scal3/ui_gtk/event/group/universityTerm.py @@ -0,0 +1,480 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import sys +from time import time as now + +from scal3.path import deskDir +from scal3.time_utils import hmEncode, hmDecode +from scal3 import core +from scal3.locale_man import tr as _ +from scal3.locale_man import numDecode + +from gi.repository import GObject + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.utils import toolButtonFromStock, set_tooltip +from scal3.ui_gtk.drawing import * +from scal3.ui_gtk.event.group.group import WidgetClass as NormalWidgetClass + + + +class CourseListEditor(gtk.HBox): + def __init__( + self, + term, + defaultCourseName=_('New Course'), + defaultCourseUnits=3, + enableScrollbars=False, + ): + self.term = term ## UniversityTerm obj + self.defaultCourseName = defaultCourseName + self.defaultCourseUnits = defaultCourseUnits + ##### + gtk.HBox.__init__(self) + self.treev = gtk.TreeView() + self.treev.set_headers_visible(True) + self.trees = gtk.ListStore(int, str, int) + self.treev.set_model(self.trees) + ########## + cell = gtk.CellRendererText() + cell.set_property('editable', True) + cell.connect('edited', self.courseNameEdited) + #cell.connect('editing-started', lambda cell0, editable, path: + # sys.stdout.write('editing-started %s\n'%path)) + #cell.connect('editing-canceled', lambda cell0:sys.stdout.write('editing-canceled\n')) + col = gtk.TreeViewColumn(_('Course Name'), cell, text=1) + self.treev.append_column(col) + ### + cell = gtk.CellRendererText() + cell.set_property('editable', True) + cell.connect('edited', self.courseUnitsEdited) + col = gtk.TreeViewColumn(_('Units'), cell, text=2) + self.treev.append_column(col) + #### + if enableScrollbars:## FIXME + swin = gtk.ScrolledWindow() + swin.add(self.treev) + swin.set_policy(gtk.PolicyType.NEVER, gtk.PolicyType.AUTOMATIC) + pack(self, swin, 1, 1) + else: + pack(self, self.treev, 1, 1) + ########## + toolbar = gtk.Toolbar() + toolbar.set_orientation(gtk.Orientation.VERTICAL) + #try:## DeprecationWarning #????????????? + #toolbar.set_icon_size(gtk.IconSize.SMALL_TOOLBAR) + ### no different (argument to set_icon_size does not affect) ????????? + #except: + # pass + size = gtk.IconSize.SMALL_TOOLBAR + ##no different(argument2 to image_new_from_stock does not affect) ????????? + #### gtk.IconSize.SMALL_TOOLBAR or gtk.IconSize.MENU + tb = toolButtonFromStock(gtk.STOCK_ADD, size) + set_tooltip(tb, _('Add')) + tb.connect('clicked', self.addClicked) + toolbar.insert(tb, -1) + #self.buttonAdd = tb + #### + tb = toolButtonFromStock(gtk.STOCK_DELETE, size) + set_tooltip(tb, _('Delete')) + tb.connect('clicked', self.deleteClicked) + toolbar.insert(tb, -1) + #self.buttonDel = tb + #### + tb = toolButtonFromStock(gtk.STOCK_GO_UP, size) + set_tooltip(tb, _('Move up')) + tb.connect('clicked', self.moveUpClicked) + toolbar.insert(tb, -1) + #### + tb = toolButtonFromStock(gtk.STOCK_GO_DOWN, size) + set_tooltip(tb, _('Move down')) + tb.connect('clicked', self.moveDownClicked) + toolbar.insert(tb, -1) + ####### + pack(self, toolbar) + def getSelectedIndex(self): + cur = self.treev.get_cursor() + try: + path, col = cur + index = path[0] + return index + except: + return None + def addClicked(self, button): + index = self.getSelectedIndex() + lastCourseId = max([1]+[row[0] for row in self.trees]) + row = [lastCourseId+1, self.defaultCourseName, self.defaultCourseUnits] + if index is None: + newIter = self.trees.append(row) + else: + newIter = self.trees.insert(index+1, row) + self.treev.set_cursor(self.trees.get_path(newIter)) + #col = self.treev.get_column(0) + #cell = col.get_cell_renderers()[0] + #cell.start_editing(...) ## FIXME + def deleteClicked(self, button): + index = self.getSelectedIndex() + if index is None: + return + del self.trees[index] + def moveUpClicked(self, button): + index = self.getSelectedIndex() + if index is None: + return + t = self.trees + if index<=0 or index>=len(t): + gdk.beep() + return + t.swap(t.get_iter(index-1), t.get_iter(index)) + self.treev.set_cursor(index-1) + def moveDownClicked(self, button): + index = self.getSelectedIndex() + if index is None: + return + t = self.trees + if index<0 or index>=len(t)-1: + gdk.beep() + return + t.swap(t.get_iter(index), t.get_iter(index+1)) + self.treev.set_cursor(index+1) + def courseNameEdited(self, cell, path, newText): + #print('courseNameEdited', newText) + index = int(path) + self.trees[index][1] = newText + def courseUnitsEdited(self, cell, path, newText): + index = int(path) + units = numDecode(newText) + self.trees[index][2] = units + def setData(self, rows): + self.trees.clear() + for row in rows: + self.trees.append(row) + getData = lambda self: [tuple(row) for row in self.trees] + + +class ClassTimeBoundsEditor(gtk.HBox): + def __init__(self, term): + self.term = term + ##### + gtk.HBox.__init__(self) + self.treev = gtk.TreeView() + self.treev.set_headers_visible(False) + self.trees = gtk.ListStore(str) + self.treev.set_model(self.trees) + ########## + cell = gtk.CellRendererText() + cell.set_property('editable', True) + cell.connect('edited', self.timeEdited) + col = gtk.TreeViewColumn(_('Time'), cell, text=0) + self.treev.append_column(col) + #### + pack(self, self.treev, 1, 1) + ########## + toolbar = gtk.Toolbar() + toolbar.set_orientation(gtk.Orientation.VERTICAL) + #try:## DeprecationWarning #????????????? + #toolbar.set_icon_size(gtk.IconSize.SMALL_TOOLBAR) + ### no different (argument to set_icon_size does not affect) ????????? + #except: + # pass + size = gtk.IconSize.SMALL_TOOLBAR + ##no different(argument2 to image_new_from_stock does not affect) ????????? + #### gtk.IconSize.SMALL_TOOLBAR or gtk.IconSize.MENU + tb = toolButtonFromStock(gtk.STOCK_ADD, size) + set_tooltip(tb, _('Add')) + tb.connect('clicked', self.addClicked) + toolbar.insert(tb, -1) + #self.buttonAdd = tb + #### + tb = toolButtonFromStock(gtk.STOCK_DELETE, size) + set_tooltip(tb, _('Delete')) + tb.connect('clicked', self.deleteClicked) + toolbar.insert(tb, -1) + #self.buttonDel = tb + ####### + pack(self, toolbar) + def getSelectedIndex(self): + cur = self.treev.get_cursor() + try: + path, col = cur + index = path[0] + return index + except: + return None + def addClicked(self, button): + index = self.getSelectedIndex() + row = ['00:00'] + if index is None: + newIter = self.trees.append(row) + else: + newIter = self.trees.insert(index+1, row) + self.treev.set_cursor(self.trees.get_path(newIter)) + def deleteClicked(self, button): + index = self.getSelectedIndex() + if index is None: + return + del self.trees[index] + def moveUpClicked(self, button): + index = self.getSelectedIndex() + if index is None: + return + t = self.trees + if index<=0 or index>=len(t): + gdk.beep() + return + t.swap(t.get_iter(index-1), t.get_iter(index)) + self.treev.set_cursor(index-1) + def moveDownClicked(self, button): + index = self.getSelectedIndex() + if index is None: + return + t = self.trees + if index<0 or index>=len(t)-1: + gdk.beep() + return + t.swap(t.get_iter(index), t.get_iter(index+1)) + self.treev.set_cursor(index+1) + def timeEdited(self, cell, path, newText): + index = int(path) + parts = newText.split(':') + h = numDecode(parts[0]) + m = numDecode(parts[1]) + hm = hmEncode((h, m)) + self.trees[index][0] = hm + #self.trees.sort()## FIXME + def setData(self, hmList): + self.trees.clear() + for hm in hmList: + self.trees.append([hmEncode(hm)]) + getData = lambda self: sorted( + [hmDecode(row[0]) for row in self.trees] + ) + +class WidgetClass(NormalWidgetClass): + def __init__(self, group): + NormalWidgetClass.__init__(self, group) + ##### + totalFrame = gtk.Frame() + totalFrame.set_label(group.desc) + totalVbox = gtk.VBox() + ### + expandHbox = gtk.HBox()## for courseList and classTimeBounds + ## + frame = gtk.Frame() + frame.set_label(_('Course List')) + self.courseListEditor = CourseListEditor(self.group) + self.courseListEditor.set_size_request(100, 150) + frame.add(self.courseListEditor) + pack(expandHbox, frame, 1, 1) + ## + frame = gtk.Frame()## FIXME + frame.set_label(_('Class Time Bounds')) + self.classTimeBoundsEditor = ClassTimeBoundsEditor(self.group) + self.classTimeBoundsEditor.set_size_request(50, 150) + frame.add(self.classTimeBoundsEditor) + pack(expandHbox, frame) + ## + pack(totalVbox, expandHbox, 1, 1) + ##### + totalFrame.add(totalVbox) + pack(self, totalFrame, 1, 1)## expand? FIXME + def updateWidget(self):## FIXME + NormalWidgetClass.updateWidget(self) + self.courseListEditor.setData(self.group.courses) + self.classTimeBoundsEditor.setData(self.group.classTimeBounds) + def updateVars(self): + NormalWidgetClass.updateVars(self) + ## + self.group.setCourses(self.courseListEditor.getData()) + self.group.classTimeBounds = self.classTimeBoundsEditor.getData() + + +@registerType +class WeeklyScheduleWidget(gtk.DrawingArea): + def __init__(self, term): + self.term = term + self.data = [] + #### + gtk.DrawingArea.__init__(self) + #self.connect('button-press-event', self.buttonPress) + self.connect('draw', self.onExposeEvent) + #self.connect('event', show_event) + def onExposeEvent(self, widget=None, event=None): + self.drawCairo(self.get_window().cairo_create()) + def drawCairo(self, cr): + if not self.data: + return + t0 = now() + w = self.get_allocation().width + h = self.get_allocation().height + cr.rectangle(0, 0, w, h) + fillColor(cr, ui.bgColor) + textColor = ui.textColor + gridColor = ui.mcalGridColor ## FIXME + ### + #classBounds = self.term.classTimeBounds + titles, tmfactors = self.term.getClassBoundsFormatted() + ### + weekDayLayouts = [] + weekDayLayoutsWidth = [] + for j in range(7): + layout = newTextLayout(self, core.getWeekDayN(j)) + layoutW, layoutH = layout.get_pixel_size() + weekDayLayouts.append(layout) + weekDayLayoutsWidth.append(layoutW) + leftMargin = max(weekDayLayoutsWidth) + 6 + ### + topMargin = 20 ## FIXME + ### Calc Coordinates: ycenters(list), dy(float) + ycenters = [ + topMargin + (h-topMargin)*(1.0+2*i)/14.0 + for i in range(7) + ] ## centers y + dy = (h-topMargin)/7.0 ## delta y + ### Draw grid + ## tmfactors includes 0 at the first, and 1 at the end + setColor(cr, gridColor) + ## + for i in range(7): + cr.rectangle(0, ycenters[i]-dy/2.0, w, 1) + cr.fill() + ## + for factor in tmfactors[:-1]: + x = leftMargin + factor*(w-leftMargin) + if rtl: x = w - x + cr.rectangle(x, 0, 1, h) + cr.fill() + ### + setColor(cr, textColor) + for i,title in enumerate(titles): + layout = newTextLayout(self, title) + layoutW, layoutH = layout.get_pixel_size() + ## + dx = (w - leftMargin) * (tmfactors[i+1] - tmfactors[i]) + if dx < layoutW: + continue + ## + factor = (tmfactors[i] + tmfactors[i+1])/2.0 + x = factor*(w-leftMargin) + leftMargin + if rtl: x = w - x + x -= layoutW/2.0 + ## + y = (topMargin-layoutH)/2.0 - 1 + ## + cr.move_to(x, y) + show_layout(cr, layout) + ### + for j in range(7): + layout = weekDayLayouts[j] + layoutW, layoutH = layout.get_pixel_size() + x = leftMargin/2.0 + if rtl: x = w - x + x -= layoutW/2.0 + ## + y = topMargin + (h-topMargin)*(j+0.5)/7.0 - layoutH/2.0 + ## + cr.move_to(x, y) + show_layout(cr, layout) + for j in range(7): + wd = (j+core.firstWeekDay)%7 + for i,dayData in enumerate(self.data[wd]): + textList = [] + for classData in dayData: + text = classData['name'] + if classData['weekNumMode']: + text += '(' + _(classData['weekNumMode'].capitalize()) + ')' + textList.append(text) + dx = (w - leftMargin) * (tmfactors[i+1] - tmfactors[i]) + layout = newTextLayout(self, '\n'.join(textList), maxSize=(dx, dy)) + layoutW, layoutH = layout.get_pixel_size() + ## + factor = (tmfactors[i] + tmfactors[i+1])/2.0 + x = factor*(w-leftMargin) + leftMargin + if rtl: x = w - x + x -= layoutW/2.0 + ## + y = topMargin + (h-topMargin)*(j+0.5)/7.0 - layoutH/2.0 + ## + cr.move_to(x, y) + show_layout(cr, layout) + + +class WeeklyScheduleWindow(gtk.Dialog): + def __init__(self, term, **kwargs): + self.term = term + gtk.Dialog.__init__(self, **kwargs) + self.resize(800, 500) + self.set_title(_('View Weekly Schedule')) + self.connect('delete-event', self.onDeleteEvent) + ##### + hbox = gtk.HBox() + self.currentWOnlyCheck = gtk.CheckButton(_('Current Week Only')) + self.currentWOnlyCheck.connect('clicked', lambda obj: self.updateWidget()) + pack(hbox, self.currentWOnlyCheck) + ## + pack(hbox, gtk.Label(''), 1, 1) + ## + button = gtk.Button(_('Export to ')+'SVG') + button.connect('clicked', self.exportToSvgClicked) + pack(hbox, button) + ## + pack(self.vbox, hbox) + ##### + self._widget = WeeklyScheduleWidget(term) + pack(self.vbox, self._widget, 1, 1) + ##### + self.vbox.show_all() + self.updateWidget() + def onDeleteEvent(self, win, gevent): + self.destroy() + return True + def updateWidget(self): + self._widget.data = self.term.getWeeklyScheduleData(self.currentWOnlyCheck.get_active()) + self._widget.queue_draw() + def exportToSvg(self, fpath): + x, y, w, h = self._widget.get_allocation() + fo = open(fpath, 'w') + surface = cairo.SVGSurface(fo, w, h) + cr0 = cairo.Context(surface) + cr = gdk.CairoContext(cr0) + #surface.set_device_offset(0, 0) + self._widget.drawCairo(cr) + surface.finish() + def exportToSvgClicked(self, obj=None): + fcd = gtk.FileChooserDialog(parent=self, action=gtk.FileChooserAction.SAVE) + fcd.set_current_folder(deskDir) + fcd.set_current_name(self.term.title + '.svg') + canB = fcd.add_button(gtk.STOCK_CANCEL, gtk.ResponseType.CANCEL) + saveB = fcd.add_button(gtk.STOCK_SAVE, gtk.ResponseType.OK) + if ui.autoLocale: + canB.set_label(_('_Cancel')) + canB.set_image(gtk.Image.new_from_stock(gtk.STOCK_CANCEL,gtk.IconSize.BUTTON)) + saveB.set_label(_('_Save')) + saveB.set_image(gtk.Image.new_from_stock(gtk.STOCK_SAVE,gtk.IconSize.BUTTON)) + if fcd.run()==gtk.ResponseType.OK: + self.exportToSvg(fcd.get_filename()) + fcd.destroy() + + +def viewWeeklySchedule(group): + WeeklyScheduleWindow(group, parent=ui.prefDialog).show() + + diff --git a/scal3/ui_gtk/event/group/vcs.py b/scal3/ui_gtk/event/group/vcs.py new file mode 100644 index 000000000..6ee323646 --- /dev/null +++ b/scal3/ui_gtk/event/group/vcs.py @@ -0,0 +1,45 @@ +from scal3 import core +from scal3.locale_man import tr as _ + +from scal3.ui_gtk import * +from scal3.ui_gtk.event.group.vcsEpochBase import VcsEpochBaseWidgetClass as BaseWidgetClass + + +class WidgetClass(BaseWidgetClass): + def __init__(self, group): + BaseWidgetClass.__init__(self, group) + #### + hbox = gtk.HBox() + label = gtk.Label(_('Commit Description')) + label.set_alignment(0, 0.5) + self.sizeGroup.add_widget(label) + pack(hbox, label) + ## + self.statCheck = gtk.CheckButton(_('Stat')) + pack(hbox, self.statCheck) + ## + pack(hbox, gtk.Label(' ')) + ## + self.authorCheck = gtk.CheckButton(_('Author')) + pack(hbox, self.authorCheck) + ## + pack(hbox, gtk.Label(' ')) + ## + self.shortHashCheck = gtk.CheckButton(_('Short Hash')) + pack(hbox, self.shortHashCheck) + ## + pack(self, hbox) + def updateWidget(self): + BaseWidgetClass.updateWidget(self) + self.authorCheck.set_active(self.group.showAuthor) + self.shortHashCheck.set_active(self.group.showShortHash) + self.statCheck.set_active(self.group.showStat) + def updateVars(self): + BaseWidgetClass.updateVars(self) + self.group.showAuthor = self.authorCheck.get_active() + self.group.showShortHash = self.shortHashCheck.get_active() + self.group.showStat = self.statCheck.get_active() + + + + diff --git a/scal3/ui_gtk/event/group/vcsBase.py b/scal3/ui_gtk/event/group/vcsBase.py new file mode 100644 index 000000000..3e83b9ce3 --- /dev/null +++ b/scal3/ui_gtk/event/group/vcsBase.py @@ -0,0 +1,44 @@ +from scal3 import core +from scal3.locale_man import tr as _ +from scal3.vcs_modules import vcsModuleNames + +from scal3.ui_gtk import * +from scal3.ui_gtk.event.group.group import WidgetClass as NormalWidgetClass + + + +class VcsBaseWidgetClass(NormalWidgetClass): + def __init__(self, group): + NormalWidgetClass.__init__(self, group) + ###### + hbox = gtk.HBox() + label = gtk.Label(_('VCS Type')) + label.set_alignment(0, 0.5) + self.sizeGroup.add_widget(label) + pack(hbox, label) + self.vcsTypeCombo = gtk.ComboBoxText() + for name in vcsModuleNames: + self.vcsTypeCombo.append_text(name)## descriptive name FIXME + pack(hbox, self.vcsTypeCombo) + pack(self, hbox) + ###### + hbox = gtk.HBox() + label = gtk.Label(_('Directory')) + label.set_alignment(0, 0.5) + self.sizeGroup.add_widget(label) + pack(hbox, label) + self.dirEntry = gtk.Entry() + pack(hbox, self.dirEntry) + ## + #self.dirBrowse = gtk.Button(_('Browse')) + pack(self, hbox) + def updateWidget(self): + NormalWidgetClass.updateWidget(self) + self.vcsTypeCombo.set_active(vcsModuleNames.index(self.group.vcsType)) + self.dirEntry.set_text(self.group.vcsDir) + def updateVars(self): + NormalWidgetClass.updateVars(self) + self.group.vcsType = vcsModuleNames[self.vcsTypeCombo.get_active()] + self.group.vcsDir = self.dirEntry.get_text() + + diff --git a/scal3/ui_gtk/event/group/vcsDailyStat.py b/scal3/ui_gtk/event/group/vcsDailyStat.py new file mode 100644 index 000000000..90a69326e --- /dev/null +++ b/scal3/ui_gtk/event/group/vcsDailyStat.py @@ -0,0 +1,12 @@ +from scal3 import core +from scal3.locale_man import tr as _ + +from gi.repository import Gtk as gtk + +from scal3.ui_gtk.event.group.vcsBase import VcsBaseWidgetClass as BaseWidgetClass + +class WidgetClass(BaseWidgetClass): + pass + + + diff --git a/scal3/ui_gtk/event/group/vcsEpochBase.py b/scal3/ui_gtk/event/group/vcsEpochBase.py new file mode 100644 index 000000000..4d798bd53 --- /dev/null +++ b/scal3/ui_gtk/event/group/vcsEpochBase.py @@ -0,0 +1,27 @@ +from scal3 import core +from scal3.locale_man import tr as _ + +from scal3.ui_gtk import * +from scal3.ui_gtk.event.group.vcsBase import VcsBaseWidgetClass + + +class VcsEpochBaseWidgetClass(VcsBaseWidgetClass): + def __init__(self, group): + VcsBaseWidgetClass.__init__(self, group) + ###### + hbox = gtk.HBox() + label = gtk.Label(_('Show Seconds')) + label.set_alignment(0, 0.5) + self.sizeGroup.add_widget(label) + pack(hbox, label) + pack(hbox, label) + self.showSecondsCheck = gtk.CheckButton('') + pack(hbox, self.showSecondsCheck) + pack(self, hbox) + def updateWidget(self): + VcsBaseWidgetClass.updateWidget(self) + self.showSecondsCheck.set_active(self.group.showSeconds) + def updateVars(self): + VcsBaseWidgetClass.updateVars(self) + self.group.showSeconds = self.showSecondsCheck.get_active() + diff --git a/scal3/ui_gtk/event/group/vcsTag.py b/scal3/ui_gtk/event/group/vcsTag.py new file mode 100644 index 000000000..cff06c17c --- /dev/null +++ b/scal3/ui_gtk/event/group/vcsTag.py @@ -0,0 +1,33 @@ +from scal3 import core +from scal3.locale_man import tr as _ + +from scal3.ui_gtk import * +from scal3.ui_gtk.event.group.vcsEpochBase import VcsEpochBaseWidgetClass as BaseWidgetClass + + +class WidgetClass(BaseWidgetClass): + def __init__(self, group): + BaseWidgetClass.__init__(self, group) + #### + hbox = gtk.HBox() + label = gtk.Label(_('Tag Description')) + label.set_alignment(0, 0.5) + self.sizeGroup.add_widget(label) + pack(hbox, label) + ## + self.statCheck = gtk.CheckButton(_('Stat')) + pack(hbox, self.statCheck) + ## + pack(self, hbox) + def updateWidget(self): + BaseWidgetClass.updateWidget(self) + self.statCheck.set_active(self.group.showStat) + def updateVars(self): + BaseWidgetClass.updateVars(self) + self.group.showStat = self.statCheck.get_active() + + + + + + diff --git a/scal3/ui_gtk/event/group/yearly.py b/scal3/ui_gtk/event/group/yearly.py new file mode 100644 index 000000000..3caf83423 --- /dev/null +++ b/scal3/ui_gtk/event/group/yearly.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from scal3 import core +from scal3.locale_man import tr as _ + +from scal3.ui_gtk import * +from scal3.ui_gtk.event import common +from scal3.ui_gtk.event.group.group import WidgetClass as NormalWidgetClass + + +class WidgetClass(NormalWidgetClass): + def __init__(self, group): + NormalWidgetClass.__init__(self, group) + ### + hbox = gtk.HBox() + label = gtk.Label(_('Show Date in Event Summary')) + label.set_alignment(0, 0.5) + pack(hbox, label) + self.sizeGroup.add_widget(label) + self.showDateCheck = gtk.CheckButton() + pack(hbox, self.showDateCheck) + pack(self, hbox) + def updateWidget(self):## FIXME + NormalWidgetClass.updateWidget(self) + self.showDateCheck.set_active(self.group.showDate) + def updateVars(self): + NormalWidgetClass.updateVars(self) + self.group.showDate = self.showDateCheck.get_active() + + diff --git a/scal3/ui_gtk/event/group_op.py b/scal3/ui_gtk/event/group_op.py new file mode 100644 index 000000000..05863ed0c --- /dev/null +++ b/scal3/ui_gtk/event/group_op.py @@ -0,0 +1,94 @@ +import natz + +from scal3.utils import myRaise +from scal3.locale_man import tr as _ + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import dialog_add_button + +class GroupSortDialog(gtk.Dialog): + def __init__(self, group, **kwargs): + self._group = group + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Sort Events')) + #### + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), gtk.ResponseType.CANCEL) + dialog_add_button(self, gtk.STOCK_OK, _('_OK'), gtk.ResponseType.OK) + ## + self.connect('response', lambda w, e: self.hide()) + #### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Sort events of group "%s"')%group.title)) + pack(hbox, gtk.Label(''), 1, 1) + pack(self.vbox, hbox) + ### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Based on')+' ')) + self.sortByNames = [] + self.sortByCombo = gtk.ComboBoxText() + sortByDefault, sortBys = group.getSortBys() + for item in sortBys: + self.sortByNames.append(item[0]) + self.sortByCombo.append_text(item[1]) + self.sortByCombo.set_active(self.sortByNames.index(sortByDefault))## FIXME + pack(hbox, self.sortByCombo) + self.reverseCheck = gtk.CheckButton(_('Descending')) + pack(hbox, self.reverseCheck) + pack(hbox, gtk.Label(''), 1, 1) + pack(self.vbox, hbox) + #### + self.vbox.show_all() + def run(self): + if gtk.Dialog.run(self)==gtk.ResponseType.OK: + self._group.sort( + self.sortByNames[self.sortByCombo.get_active()], + self.reverseCheck.get_active(), + ) + self._group.save() + return True + self.destroy() + + + +class GroupConvertModeDialog(gtk.Dialog): + def __init__(self, group, **kwargs): + from scal3.ui_gtk.mywidgets.cal_type_combo import CalTypeCombo + self._group = group + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Convert Calendar Type')) + #### + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), gtk.ResponseType.CANCEL) + dialog_add_button(self, gtk.STOCK_OK, _('_OK'), gtk.ResponseType.OK) + ## + self.connect('response', lambda w, e: self.hide()) + #### + hbox = gtk.HBox() + label = gtk.Label(_('This is going to convert calendar types of all events inside group \"%s\" to a specific type. This operation does not work for Yearly events and also some of Custom events. You have to edit those events manually to change calendar type.')%group.title) + label.set_line_wrap(True) + pack(hbox, label) + pack(hbox, gtk.Label(''), 1, 1) + pack(self.vbox, hbox) + ### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Calendar Type')+':')) + combo = CalTypeCombo() + combo.set_active(group.mode) + pack(hbox, combo) + pack(hbox, gtk.Label(''), 1, 1) + self.modeCombo = combo + pack(self.vbox, hbox) + #### + self.vbox.show_all() + def run(self): + if gtk.Dialog.run(self)==gtk.ResponseType.OK: + mode = self.modeCombo.get_active() + failedSummaryList = [] + for event in self._group: + if event.changeMode(mode): + event.save() + else: + failedSummaryList.append(event.summary) + if failedSummaryList:## FIXME + print(failedSummaryList) + self.destroy() + diff --git a/scal3/ui_gtk/event/import_event.py b/scal3/ui_gtk/event/import_event.py new file mode 100644 index 000000000..4e8c10248 --- /dev/null +++ b/scal3/ui_gtk/event/import_event.py @@ -0,0 +1,141 @@ +import sys + +from scal3.path import deskDir +from scal3.json_utils import * +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.wizard import WizardWindow + + + +class EventsImportWindow(WizardWindow): + def __init__(self, manager): + self.manager = manager + WizardWindow.__init__(self, _('Import Events')) + self.set_type_hint(gdk.WindowTypeHint.DIALOG) + #self.set_property('skip-taskbar-hint', True) + #self.set_modal(True) + #self.set_transient_for(manager) + #self.set_destroy_with_parent(True) + self.resize(400, 200) + class FirstStep(gtk.VBox): + def __init__(self, win): + gtk.VBox.__init__(self) + self.set_spacing(20) + self.win = win + self.buttons = ( + (_('Cancel'), self.cancelClicked), + (_('Next'), self.nextClicked), + ) + #### + hbox = gtk.HBox(spacing=10) + frame = gtk.Frame() + frame.set_label(_('Format')) + #frame.set_border_width(10) + radioBox = gtk.VBox(spacing=10) + radioBox.set_border_width(10) + ## + self.radioJson = gtk.RadioButton(label=_('JSON (StarCalendar)')) + #self.radioIcs = gtk.RadioButton(label='iCalendar', group=self.radioJson) + ## + pack(radioBox, self.radioJson) + #pack(radioBox, self.radioIcs) + ## + self.radioJson.set_active(True) + #self.radioJson.connect('clicked', self.formatRadioChanged) + ##self.radioIcs.connect('clicked', self.formatRadioChanged) + ## + frame.add(radioBox) + pack(hbox, frame, 0, 0, 10) + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + #### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('File')+':')) + self.fcb = gtk.FileChooserButton(_('Import: Select File')) + self.fcb.set_local_only(True) + self.fcb.set_current_folder(deskDir) + pack(hbox, self.fcb, 1, 1) + pack(self, hbox) + #### + self.show_all() + def run(self): + pass + def cancelClicked(self, obj): + self.win.destroy() + def nextClicked(self, obj): + fpath = self.fcb.get_filename() + if not fpath: + return + if self.radioJson.get_active(): + format = 'json' + #elif self.radioIcs.get_active(): + # format = 'ics' + else: + return + self.win.showStep(1, format, fpath) + class SecondStep(gtk.VBox): + def __init__(self, win): + gtk.VBox.__init__(self) + self.set_spacing(20) + self.win = win + self.buttons = ( + (_('Back'), self.backClicked), + (_('Close'), self.closeClicked), + ) + #### + self.textview = gtk.TextView() + pack(self, self.textview, 1, 1) + #### + self.show_all() + def redirectStdOutErr(self): + from scal3.ui_gtk.buffer import GtkBufferFile + t_table = gtk.TextTagTable() + tag_out = gtk.TextTag(name='output') + t_table.add(tag_out) + tag_err = gtk.TextTag(name='error') + t_table.add(tag_err) + self.buffer = gtk.TextBuffer(t_table) + self.textview.set_buffer(self.buffer) + self.out_fp = GtkBufferFile(self.buffer, tag_out) + sys.stdout = self.out_fp + self.err_fp = GtkBufferFile(self.buffer, tag_err) + sys.stderr = self.err_fp + def restoreStdOutErr(self): + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + def run(self, format, fpath): + self.redirectStdOutErr() + try: + if format=='json': + try: + text = open(fpath, 'rb').read() + except Exception as e: + sys.stderr.write(_('Error in reading file')+'\n%s\n'%e) + else: + try: + data = jsonToData(text) + except Exception as e: + sys.stderr.write(_('Error in loading JSON data')+'\n%s\n'%e) + else: + try: + newGroups = ui.eventGroups.importData(data) + except Exception as e: + sys.stderr.write(_('Error in importing events')+'\n%s\n'%e) + else: + for group in newGroups: + self.win.manager.appendGroupTree(group) + print(_('%s groups imported successfully')%_(len(newGroups))) + else: + raise ValueError('invalid format %r'%format) + finally: + self.restoreStdOutErr() + def backClicked(self, obj): + self.win.showStep(0) + def closeClicked(self, obj): + self.win.destroy() + stepClasses = [FirstStep, SecondStep] + diff --git a/scal3/ui_gtk/event/manager.py b/scal3/ui_gtk/event/manager.py new file mode 100644 index 000000000..180b9594e --- /dev/null +++ b/scal3/ui_gtk/event/manager.py @@ -0,0 +1,1354 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from time import time as now + +import os, sys +from os.path import join, dirname, split, splitext + +from scal3.path import * +from scal3 import core +from scal3.core import myRaise +from scal3 import locale_man +from scal3.locale_man import tr as _ +from scal3.locale_man import rtl +from scal3 import event_lib +from scal3 import ui + +from gi.repository import GdkPixbuf + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.utils import set_tooltip, dialog_add_button, confirm +from scal3.ui_gtk.utils import toolButtonFromStock, labelImageMenuItem, labelStockMenuItem +from scal3.ui_gtk.utils import pixbufFromFile, rectangleContainsPoint, getStyleColor +from scal3.ui_gtk.utils import showError, showInfo +from scal3.ui_gtk.color_utils import gdkColorToRgb +from scal3.ui_gtk.drawing import newColorCheckPixbuf +from scal3.ui_gtk import gtk_ud as ud +from scal3.ui_gtk.mywidgets.dialog import MyDialog +from scal3.ui_gtk.event import common +from scal3.ui_gtk.event.utils import * +from scal3.ui_gtk.event.editor import * +from scal3.ui_gtk.event.trash import TrashEditorDialog +from scal3.ui_gtk.event.export import SingleGroupExportDialog, MultiGroupExportDialog +from scal3.ui_gtk.event.import_event import EventsImportWindow +from scal3.ui_gtk.event.group_op import GroupSortDialog, GroupConvertModeDialog +from scal3.ui_gtk.event.account_op import FetchRemoteGroupsDialog +from scal3.ui_gtk.event.search_events import EventSearchWindow + +#print('Testing translator', __file__, _('About')) + + + + +@registerSignals +class EventManagerDialog(gtk.Dialog, MyDialog, ud.BaseCalObj):## FIXME + _name = 'eventMan' + desc = _('Event Manager') + def onShow(self, widget): + self.move(*ui.eventManPos) + self.onConfigChange() + def onDeleteEvent(self, obj, gevent): + ## onResponse is going to be called after onDeleteEvent + ## just return True, no need to do anything else + return True + def onResponse(self, dialog, response_id): + ui.eventManPos = self.get_position() + ui.saveLiveConf() + ### + self.hide() + self.emit('config-change') + #def findEventByPath(self, eid, path): + # groupIndex, eventIndex = path + # + def onConfigChange(self, *a, **kw): + ud.BaseCalObj.onConfigChange(self, *a, **kw) + ### + if not self.isLoaded: + if self.get_property('visible'): + self.waitingDo(self.reloadEvents)## FIXME + return + ### + for action, eid, gid, path in ui.eventDiff: + if action == '-': + try: + eventIter = self.eventsIter[eid] + except KeyError: + if gid in self.loadedGroupIds: + print('trying to delete non-existing event row, eid=%s, path=%s'%(eid, path)) + else: + self.trees.remove(eventIter) + elif action == '+': + if gid in self.loadedGroupIds: + parentIndex, eventIndex = path + #print(gid, self.loadedGroupIds, parentIndex) + parentIter = self.trees.get_iter((parentIndex,)) + event = ui.getEvent(gid, eid) + self.insertEventRow(parentIter, eventIndex, event) + elif action == 'e': + try: + eventIter = self.eventsIter[eid] + except KeyError: + if gid in self.loadedGroupIds: + print('trying to edit non-existing event row, eid=%s, path=%s'%(eid, path)) + else: + event = ui.getEvent(gid, eid) + self.updateEventRowByIter(event, eventIter) + ### + for gid in ui.changedGroups: + group = ui.eventGroups[gid] + groupIter = self.groupIterById[gid] + for i, value in enumerate(self.getGroupRow(group)): + self.trees.set_value(groupIter, i, value) + ui.changedGroups = [] + ### + for gid in ui.reloadGroups: + self.reloadGroupEvents(gid) + ui.reloadGroups = [] + ### + if ui.reloadTrash: + if self.trashIter: + self.trees.remove(self.trashIter) + self.appendTrash() + ui.reloadTrash = False + def __init__(self, **kwargs): + checkEventsReadOnly() ## FIXME + gtk.Dialog.__init__(self, **kwargs) + self.initVars() + ud.windowList.appendItem(self) + #### + self.syncing = None ## or a tuple of (groupId, statusText) + self.groupIterById = {} + self.trashIter = None + self.isLoaded = False + self.loadedGroupIds = set() + self.eventsIter = {} + #### + self.set_title(_('Event Manager')) + self.resize(600, 300) + self.connect('delete-event', self.onDeleteEvent) + self.set_transient_for(None) + self.set_type_hint(gdk.WindowTypeHint.NORMAL) + ## + dialog_add_button(self, gtk.STOCK_OK, _('_OK'), gtk.ResponseType.OK) + #self.connect('response', lambda w, e: self.hide()) + self.connect('response', self.onResponse) + self.connect('show', self.onShow) + ####### + self.searchWin = EventSearchWindow() + ####### + menubar = gtk.MenuBar() + #### + fileItem = MenuItem(_('_File')) + fileMenu = gtk.Menu() + fileItem.set_submenu(fileMenu) + menubar.append(fileItem) + ## + addGroupItem = MenuItem(_('Add New Group')) + addGroupItem.set_sensitive(not event_lib.readOnly) + addGroupItem.connect('activate', self.addGroupBeforeSelection) + ## or before selected group? FIXME + fileMenu.append(addGroupItem) + ## + searchItem = MenuItem(_('_Search Events'))## FIXME right place? + searchItem.connect('activate', self.mbarSearchClicked) + fileMenu.append(searchItem) + ## + exportItem = MenuItem(_('_Export')) + exportItem.connect('activate', self.mbarExportClicked) + fileMenu.append(exportItem) + ## + importItem = MenuItem(_('_Import')) + importItem.set_sensitive(not event_lib.readOnly) + importItem.connect('activate', self.mbarImportClicked) + fileMenu.append(importItem) + ## + orphanItem = MenuItem(_('Check for Orphan Events')) + orphanItem.set_sensitive(not event_lib.readOnly) + orphanItem.connect('activate', self.mbarOrphanClicked) + fileMenu.append(orphanItem) + #### + editItem = MenuItem(_('_Edit')) + if event_lib.readOnly: + editItem.set_sensitive(False) + else: + editMenu = gtk.Menu() + editItem.set_submenu(editMenu) + menubar.append(editItem) + ## + editEditItem = MenuItem(_('Edit')) + editEditItem.connect('activate', self.mbarEditClicked) + editMenu.append(editEditItem) + editMenu.connect('show', self.mbarEditMenuPopup) + self.mbarEditItem = editEditItem + ## + editMenu.append(gtk.SeparatorMenuItem()) + ## + cutItem = MenuItem(_('Cu_t')) + cutItem.connect('activate', self.mbarCutClicked) + editMenu.append(cutItem) + self.mbarCutItem = cutItem + ## + copyItem = MenuItem(_('_Copy')) + copyItem.connect('activate', self.mbarCopyClicked) + editMenu.append(copyItem) + self.mbarCopyItem = copyItem + ## + pasteItem = MenuItem(_('_Paste')) + pasteItem.connect('activate', self.mbarPasteClicked) + editMenu.append(pasteItem) + self.mbarPasteItem = pasteItem + ## + editMenu.append(gtk.SeparatorMenuItem()) + ## + dupItem = MenuItem(_('_Duplicate')) + dupItem.connect('activate', self.duplicateSelectedObj) + editMenu.append(dupItem) + self.mbarDupItem = dupItem + #### + viewItem = MenuItem(_('_View')) + viewMenu = gtk.Menu() + viewItem.set_submenu(viewMenu) + menubar.append(viewItem) + ## + collapseItem = MenuItem(_('Collapse All')) + collapseItem.connect('activate', self.collapseAllClicked) + viewMenu.append(collapseItem) + ## + expandItem = MenuItem(_('Expand All')) + expandItem.connect('activate', self.expandAllClicked) + viewMenu.append(expandItem) + ## + viewMenu.append(gtk.SeparatorMenuItem()) + ## + self.showDescItem = gtk.CheckMenuItem(_('Show _Description')) + self.showDescItem.set_active(ui.eventManShowDescription) + self.showDescItem.connect('toggled', self.showDescItemToggled) + viewMenu.append(self.showDescItem) + #### + #testItem = MenuItem(_('Test')) + #testMenu = gtk.Menu() + #testItem.set_submenu(testMenu) + #menubar.append(testItem) + ### + #item = MenuItem('') + #item.connect('activate', ) + #testMenu.append(item) + #### + menubar.show_all() + pack(self.vbox, menubar) + ####### + treeBox = gtk.HBox() + ##### + self.treev = gtk.TreeView() + self.treev.set_search_column(2) + #self.treev.set_headers_visible(False)## FIXME + #self.treev.get_selection().set_mode(gtk.SelectionMode.MULTIPLE)## FIXME + #self.treev.set_rubber_banding(gtk.SelectionMode.MULTIPLE)## FIXME + #self.treev.connect('realize', self.onTreeviewRealize) + self.treev.get_selection().connect('changed', self.treeviewCursorChanged)## FIXME + self.treev.connect('button-press-event', self.treeviewButtonPress) + self.treev.connect('row-activated', self.rowActivated) + self.treev.connect('key-press-event', self.keyPress) + ##### + swin = gtk.ScrolledWindow() + swin.add(self.treev) + swin.set_policy(gtk.PolicyType.AUTOMATIC, gtk.PolicyType.AUTOMATIC) + pack(treeBox, swin, 1, 1) + ### + toolbar = gtk.Toolbar() + toolbar.set_orientation(gtk.Orientation.VERTICAL) + size = gtk.IconSize.SMALL_TOOLBAR + ### + tb = toolButtonFromStock(gtk.STOCK_GO_UP, size) + set_tooltip(tb, _('Move up')) + tb.connect('clicked', self.moveUpByButton) + toolbar.insert(tb, -1) + ### + tb = toolButtonFromStock(gtk.STOCK_GO_DOWN, size) + set_tooltip(tb, _('Move down')) + tb.connect('clicked', self.moveDownByButton) + toolbar.insert(tb, -1) + ### + tb = toolButtonFromStock(gtk.STOCK_COPY, size) + set_tooltip(tb, _('Duplicate')) + tb.connect('clicked', self.duplicateSelectedObj) + toolbar.insert(tb, -1) + ### + pack(treeBox, toolbar) + ##### + pack(self.vbox, treeBox, 1, 1) + ####### + self.trees = gtk.TreeStore(int, GdkPixbuf.Pixbuf, str, str) + ## event: eid, event_icon, event_summary, event_description + ## group: gid, group_pixbuf, group_title, ?description + ## trash: -1, trash_icon, _('Trash'), '' + self.treev.set_model(self.trees) + ### + col = gtk.TreeViewColumn() + cell = gtk.CellRendererPixbuf() + pack(col, cell) + col.add_attribute(cell, 'pixbuf', 1) + col.set_property('expand', False) + self.treev.append_column(col) + ### + col = gtk.TreeViewColumn( + _('Summary'), + gtk.CellRendererText(), + text=2, + ) + col.set_resizable(True) + col.set_property('expand', True) + self.treev.append_column(col) + ### + self.colDesc = gtk.TreeViewColumn( + _('Description'), + gtk.CellRendererText(), + text=3, + ) + self.colDesc.set_property('expand', True) + if ui.eventManShowDescription: + self.treev.append_column(self.colDesc) + ### + #self.treev.set_search_column(2)## or 3 + ### + self.toPasteEvent = None ## (path, bool move) + ##### + self.sbar = gtk.Statusbar() + self.sbar.set_direction(gtk.TextDirection.LTR) + #self.sbar.set_has_resize_grip(False) + pack(self.vbox, self.sbar) + ##### + self.vbox.show_all() + def canPasteToGroup(self, group): + if self.toPasteEvent is None: + return False + if not group.acceptsEventTypes: + return False + ## check event type here? FIXME + return True + def checkEventToAdd(self, group, event): + if not group.checkEventToAdd(event): + showError( + _('Group type "%s" can not contain event type "%s"')%(group.desc, event.desc), + self, + ) + raise RuntimeError('Invalid event type for this group') + getGroupRow = lambda self, group:\ + common.getGroupRow(group) + ('',) + getEventRow = lambda self, event: ( + event.id, + pixbufFromFile(event.icon), + event.summary, + event.getShownDescription(), + ) + def appendEventRow(self, parentIter, event): + eventIter = self.trees.append(parentIter, self.getEventRow(event)) + self.eventsIter[event.id] = eventIter + return eventIter + def insertEventRow(self, parentIter, position, event): + eventIter = self.trees.insert(parentIter, position, self.getEventRow(event)) + self.eventsIter[event.id] = eventIter + return eventIter + def insertEventRowAfter(self, parentIter, siblingIter, event): + eventIter = self.trees.insert_after(parentIter, siblingIter, self.getEventRow(event)) + self.eventsIter[event.id] = eventIter + return eventIter + def insertGroup(self, position, group): + self.groupIterById[group.id] = groupIter = self.trees.insert( + None, + position, + self.getGroupRow(group), + ) + return groupIter + def appendGroupEvents(self, group, groupIter): + for event in group: + self.appendEventRow(groupIter, event) + self.loadedGroupIds.add(group.id) + def insertGroupTree(self, position, group): + groupIter = self.insertGroup(position, group) + if group.enable: + self.appendGroupEvents(group, groupIter) + def appendGroup(self, group): + self.groupIterById[group.id] = groupIter = self.trees.insert_before( + None, + self.trashIter, + self.getGroupRow(group), + ) + return groupIter + def appendGroupTree(self, group): + groupIter = self.appendGroup(group) + if group.enable: + self.appendGroupEvents(group, groupIter) + def appendTrash(self): + self.trashIter = self.trees.append(None, ( + -1, + pixbufFromFile(ui.eventTrash.icon), + ui.eventTrash.title, + '', + )) + for event in ui.eventTrash: + self.appendEventRow(self.trashIter, event) + def reloadGroupEvents(self, gid): + groupIter = self.groupIterById[gid] + assert self.trees.get_value(groupIter, 0) == gid + ## + self.removeIterChildren(groupIter) + ## + group = ui.eventGroups[gid] + if not gid in self.loadedGroupIds: + return + for event in group: + self.appendEventRow(groupIter, event) + def reloadEvents(self): + self.trees.clear() + self.appendTrash() + for group in ui.eventGroups: + self.appendGroupTree(group) + self.treeviewCursorChanged() + #### + ui.changedGroups = [] + ui.reloadGroups = [] + ui.reloadTrash = False + #### + self.isLoaded = True + def getObjsByPath(self, path): + obj_list = [] + for i in range(len(path)): + it = self.trees.get_iter(path[:i+1]) + obj_id = self.trees.get_value(it, 0) + if i==0: + if obj_id==-1: + obj_list.append(ui.eventTrash) + else: + obj_list.append(ui.eventGroups[obj_id]) + else: + obj_list.append(obj_list[i-1][obj_id]) + return obj_list + def genRightClickMenu(self, path): + ## how about multi-selection? FIXME + ## and Select _All menu item + obj_list = self.getObjsByPath(path) + #print(len(obj_list)) + menu = gtk.Menu() + if len(obj_list)==1: + group = obj_list[0] + if group.name == 'trash': + #print('right click on trash', group.title) + menu.add(eventWriteMenuItem( + 'Edit', + gtk.STOCK_EDIT, + self.editTrash, + )) + menu.add(eventWriteMenuItem( + 'Empty Trash', + gtk.STOCK_CLEAR, + self.emptyTrash, + )) + #menu.add(gtk.SeparatorMenuItem()) + #menu.add(eventWriteMenuItem( + # 'Add New Group', + # gtk.STOCK_NEW, + # self.addGroupBeforeSelection, + #))## FIXME + else: + #print('right click on group', group.title) + menu.add(eventWriteMenuItem( + 'Edit', + gtk.STOCK_EDIT, + self.editGroupFromMenu, + path, + )) + eventTypes = group.acceptsEventTypes + if eventTypes is None: + eventTypes = event_lib.classes.event.names + if len(eventTypes) > 3: + menu.add(eventWriteMenuItem( + _('Add Event'), + gtk.STOCK_ADD, + self.addGenericEventToGroupFromMenu, + path, + group, + )) + else: + for eventType in eventTypes: + #if eventType == 'custom':## FIXME + # desc = _('Add ') + _('Event') + #else: + label = _('Add ') + event_lib.classes.event.byName[eventType].desc + menu.add(eventWriteMenuItem( + label, + gtk.STOCK_ADD, + self.addEventToGroupFromMenu, + path, + group, + eventType, + label, + )) + pasteItem = eventWriteMenuItem( + 'Paste Event', + gtk.STOCK_PASTE, + self.pasteEventFromMenu, + path, + ) + menu.add(pasteItem) + pasteItem.set_sensitive(self.canPasteToGroup(group)) + ## + if group.remoteIds: + aid, remoteGid = group.remoteIds + account = ui.eventAccounts[aid] + if account.enable: + menu.add(gtk.SeparatorMenuItem()) + menu.add(eventWriteMenuItem( + 'Synchronize', + gtk.STOCK_CONNECT,## or gtk.STOCK_REFRESH FIXME + self.syncGroupFromMenu, + path, + account, + )) + #else:## FIXME + ## + menu.add(gtk.SeparatorMenuItem()) + #menu.add(eventWriteMenuItem( + # 'Add New Group', + # gtk.STOCK_NEW, + # self.addGroupBeforeGroup, + # path, + #))## FIXME + menu.add(eventWriteMenuItem( + 'Duplicate', + gtk.STOCK_COPY, + self.duplicateGroupFromMenu, + path, + )) + ### + dupAllItem = labelStockMenuItem( + 'Duplicate with All Events', + gtk.STOCK_COPY, + self.duplicateGroupWithEventsFromMenu, + path, + ) + menu.add(dupAllItem) + dupAllItem.set_sensitive(not event_lib.readOnly and bool(group.idList)) + ### + menu.add(gtk.SeparatorMenuItem()) + menu.add(eventWriteMenuItem( + 'Delete Group', + gtk.STOCK_DELETE, + self.deleteGroupFromMenu, + path, + )) + menu.add(gtk.SeparatorMenuItem()) + ## + #menu.add(eventWriteMenuItem( + # 'Move Up', + # gtk.STOCK_GO_UP, + # self.moveUpFromMenu, + # path, + #)) + #menu.add(eventWriteMenuItem( + # 'Move Down', + # gtk.STOCK_GO_DOWN, + # self.moveDownFromMenu, + # path, + #)) + ## + menu.add(labelStockMenuItem( + _('Export'), + gtk.STOCK_CONVERT, + self.groupExportFromMenu, + group, + )) + ### + sortItem = labelStockMenuItem( + _('Sort Events'), + gtk.STOCK_SORT_ASCENDING, + self.groupSortFromMenu, + path, + ) + menu.add(sortItem) + sortItem.set_sensitive(not event_lib.readOnly and bool(group.idList)) + ### + convertItem = labelStockMenuItem( + _('Convert Calendar Type'), + gtk.STOCK_CONVERT, + self.groupConvertModeFromMenu, + group, + ) + menu.add(convertItem) + convertItem.set_sensitive(not event_lib.readOnly and bool(group.idList)) + ### + for newGroupType in group.canConvertTo: + menu.add(eventWriteMenuItem( + _('Convert to %s')%event_lib.classes.group.byName[newGroupType].desc, + None, + self.groupConvertTo, + group, + newGroupType, + )) + ### + bulkItem = labelStockMenuItem( + _('Bulk Edit Events'), + gtk.STOCK_EDIT, + self.groupBulkEditFromMenu, + group, + path, + ) + menu.add(bulkItem) + bulkItem.set_sensitive(not event_lib.readOnly and bool(group.idList)) + ### + for actionName, actionFuncName in group.actions: + menu.add(eventWriteMenuItem( + _(actionName), + None, + self.groupActionClicked, + group, + actionFuncName, + )) + elif len(obj_list) == 2: + group, event = obj_list + #print('right click on event', event.summary) + if group.name != 'trash': + menu.add(eventWriteMenuItem( + 'Edit', + gtk.STOCK_EDIT, + self.editEventFromMenu, + path, + )) + #### + moveToItem = eventWriteMenuItem( + _('Move to %s')%'...', + None,## FIXME + ) + moveToMenu = gtk.Menu() + for new_group in ui.eventGroups: + if new_group.id == group.id: + continue + #if not new_group.enable:## FIXME + # continue + new_groupPath = self.trees.get_path(self.groupIterById[new_group.id]) + if event.name in new_group.acceptsEventTypes: + new_groupItem = ImageMenuItem() + new_groupItem.set_label(new_group.title) + ## + image = gtk.Image() + image.set_from_pixbuf(newColorCheckPixbuf(new_group.color, 20, True)) + new_groupItem.set_image(image) + ## + new_groupItem.connect( + 'activate', + self.moveEventToPathFromMenu, + path, + new_groupPath, + ) + ## + moveToMenu.add(new_groupItem) + moveToItem.set_submenu(moveToMenu) + menu.add(moveToItem) + #### + menu.add(gtk.SeparatorMenuItem()) + #### + menu.add(eventWriteMenuItem( + 'Cut', + gtk.STOCK_CUT, + self.cutEvent, + path, + )) + menu.add(eventWriteMenuItem( + 'Copy', + gtk.STOCK_COPY, + self.copyEvent, + path, + )) + ## + if group.name == 'trash': + menu.add(gtk.SeparatorMenuItem()) + menu.add(eventWriteMenuItem( + 'Delete', + gtk.STOCK_DELETE, + self.deleteEventFromTrash, + path, + )) + else: + pasteItem = eventWriteMenuItem( + 'Paste', + gtk.STOCK_PASTE, + self.pasteEventFromMenu, + path, + ) + menu.add(pasteItem) + pasteItem.set_sensitive(self.canPasteToGroup(group)) + ## + menu.add(gtk.SeparatorMenuItem()) + menu.add(labelImageMenuItem( + _('Move to %s')%ui.eventTrash.title, + ui.eventTrash.icon, + self.moveEventToTrashFromMenu, + path, + )) + else: + return + menu.show_all() + return menu + def openRightClickMenu(self, path, etime=None): + menu = self.genRightClickMenu(path) + if not menu: + return + if etime is None: + etime = gtk.get_current_event_time() + self.tmpMenu = menu + menu.popup(None, None, None, None, 3, etime) + #def onTreeviewRealize(self, gevent): + # #self.reloadEvents()## FIXME + # pass + def rowActivated(self, treev, path, col): + if len(path)==1: + if treev.row_expanded(path): + treev.collapse_row(path) + else: + treev.expand_row(path, False) + elif len(path)==2: + self.editEventByPath(path) + def keyPress(self, treev, gevent): + #from scal3.time_utils import getGtkTimeFromEpoch + #print(gevent.time-getGtkTimeFromEpoch(now())## FIXME) + #print(now()-gdk.CURRENT_TIME/1000.0) + ## gdk.CURRENT_TIME == 0## FIXME + ## gevent.time == gtk.get_current_event_time() ## OK + kname = gdk.keyval_name(gevent.keyval).lower() + if kname=='menu':## Simulate right click (key beside Right-Ctrl) + path = treev.get_cursor()[0] + if path: + menu = self.genRightClickMenu(path) + if not menu: + return + rect = treev.get_cell_area(path, treev.get_column(1)) + x = rect.x + if rtl: + x -= 100 + else: + x += 50 + dx, dy = treev.translate_coordinates(self, x, rect.y + rect.height) + foo, wx, wy = self.get_window().get_origin() + self.tmpMenu = menu + menu.popup( + None, + None, + lambda m, e: (wx+dx, wy+dy+20, True), + None, + 3, + gevent.time, + ) + elif kname=='delete': + self.moveSelectionToTrash() + else: + #print(kname) + return False + return True + def mbarExportClicked(self, obj): + MultiGroupExportDialog(parent=self).run() + def mbarImportClicked(self, obj): + EventsImportWindow(self).present() + def mbarSearchClicked(self, obj): + self.searchWin.present() + def _do_checkForOrphans(self): + newGroup = ui.eventGroups.checkForOrphans() + if newGroup is not None: + self.appendGroupTree(newGroup) + def mbarOrphanClicked(self, obj): + self.waitingDo(self._do_checkForOrphans) + def mbarEditMenuPopup(self, obj): + path = self.treev.get_cursor()[0] + selected = bool(path) + eventSelected = selected and len(path)==2 + ### + self.mbarEditItem.set_sensitive(selected) + self.mbarCutItem.set_sensitive(eventSelected) + self.mbarCopyItem.set_sensitive(eventSelected) + self.mbarDupItem.set_sensitive(selected) + ### + self.mbarPasteItem.set_sensitive( + selected and self.canPasteToGroup(self.getObjsByPath(path)[0]) + ) + def mbarEditClicked(self, obj): + path = self.treev.get_cursor()[0] + if not path: + return + if len(path)==1: + self.editGroupByPath(path) + elif len(path)==2: + self.editEventByPath(path) + def mbarCutClicked(self, obj): + path = self.treev.get_cursor()[0] + if not path: + return + if len(path)==2: + self.toPasteEvent = (path, True) + def mbarCopyClicked(self, obj): + path = self.treev.get_cursor()[0] + if not path: + return + if len(path)==2: + self.toPasteEvent = (path, False) + def mbarPasteClicked(self, obj): + path = self.treev.get_cursor()[0] + if not path: + return + self.pasteEventToPath(path) + collapseAllClicked = lambda self, obj: self.treev.collapse_all() + expandAllClicked = lambda self, obj: self.treev.expand_all() + def _do_showDescItemToggled(self): + active = self.showDescItem.get_active() + #self.showDescItem.set_active(active) + ui.eventManShowDescription = active + ui.saveLiveConf()## FIXME + if active: + self.treev.append_column(self.colDesc) + else: + self.treev.remove_column(self.colDesc) + def showDescItemToggled(self, obj=None): + self.waitingDo(self._do_showDescItemToggled) + def treeviewCursorChanged(self, selection=None): + path = self.treev.get_cursor()[0] + ## update eventInfoBox + #print('treeviewCursorChanged', path) + if not self.syncing: + text = '' + if path: + if len(path)==1: + group, = self.getObjsByPath(path) + if group.name == 'trash': + text = _('contains %s events')%_(len(group)) + else: + text = _('contains %s events and %s occurences')%( + _(len(group)), + _(group.occurCount), + ) + _(',') + ' ' + _('Group ID: %s')%_(group.id) + modified = group.modified + elif len(path)==2: + group, event = self.getObjsByPath(path) + text = _('Event ID: %s')%_(event.id) + modified = event.modified + text += '%s %s: %s'%( + _(','), + _('Last Modified'), + locale_man.textNumEncode(core.epochDateTimeEncode(modified)), + ) + try: + sbar = self.sbar + except AttributeError: + pass + else: + message_id = self.sbar.push(0, text) + return True + def _do_onGroupModify(self, group): + group.afterModify() + group.save()## FIXME + try: + if group.name == 'universityTerm':## FIXME + groupIter = self.groupIterById[group.id] + n = self.trees.iter_n_children(groupIter) + for i in range(n): + eventIter = self.trees.iter_nth_child(groupIter, i) + eid = self.trees.get(eventIter, 0)[0] + self.trees.set(eventIter, 2, group[eid].summary) + except: + myRaise() + def onGroupModify(self, group): + self.waitingDo(self._do_onGroupModify, group) + def treeviewButtonPress(self, treev, gevent): + pos_t = treev.get_path_at_pos(int(gevent.x), int(gevent.y)) + if not pos_t: + return + path, col, xRel, yRel = pos_t + if not path: + return + if gevent.button == 3: + self.openRightClickMenu(path, gevent.time) + elif gevent.button == 1: + if not col: + return + if not rectangleContainsPoint( + treev.get_cell_area(path, col), + gevent.x, + gevent.y, + ): + return + obj_list = self.getObjsByPath(path) + if len(obj_list) == 1:## group, not event + group = obj_list[0] + if group.name != 'trash': + cell = col.get_cells()[0] + try: + cell.get_property('pixbuf') + except: + pass + else: + group.enable = not group.enable + groupIter = self.trees.get_iter(path) + self.trees.set_value( + groupIter, + 1, + common.getGroupPixbuf(group), + ) + ui.eventGroups.save() + #group.save() + if group.enable and \ + self.trees.iter_n_children(groupIter) == 0 and \ + len(group) > 0: + for event in group: + self.appendEventRow(groupIter, event) + self.loadedGroupIds.add(group.id) + self.onGroupModify(group) + treev.set_cursor(path) + return True + def insertNewGroup(self, groupIndex): + from scal3.ui_gtk.event.group.editor import GroupEditorDialog + group = GroupEditorDialog(parent=self).run() + if group is None: + return + ui.eventGroups.insert(groupIndex, group) + ui.eventGroups.save() + beforeGroupIter = self.trees.get_iter((groupIndex,)) + self.groupIterById[group.id] = self.trees.insert_before( + #self.trees.get_iter_root(),## parent + self.trees.iter_parent(beforeGroupIter), + beforeGroupIter,## sibling + self.getGroupRow(group), + ) + self.onGroupModify(group) + self.loadedGroupIds.add(group.id) + def addGroupBeforeGroup(self, menu, path): + self.insertNewGroup(path[0]) + def addGroupBeforeSelection(self, obj=None): + path = self.treev.get_cursor()[0] + if path: + groupIndex = path[0] + else: + groupIndex = len(self.trees)-1 + self.insertNewGroup(groupIndex) + def duplicateGroup(self, path): + index, = path + group, = self.getObjsByPath(path) + newGroup = group.copy() + ui.duplicateGroupTitle(newGroup) + newGroup.afterModify() + newGroup.save() + ui.eventGroups.insert(index+1, newGroup) + ui.eventGroups.save() + self.groupIterById[newGroup.id] = self.trees.insert( + None, + index+1, + self.getGroupRow(newGroup), + ) + def duplicateGroupWithEvents(self, path): + index, = path + group, = self.getObjsByPath(path) + newGroup = group.deepCopy() + ui.duplicateGroupTitle(newGroup) + newGroup.save() + ui.eventGroups.insert(index+1, newGroup) + ui.eventGroups.save() + newGroupIter = self.groupIterById[newGroup.id] = self.trees.insert( + None, + index+1, + self.getGroupRow(newGroup), + ) + for event in newGroup: + self.appendEventRow(newGroupIter, event) + self.loadedGroupIds.add(newGroup.id) + def syncGroupFromMenu(self, menu, path, account): + index, = path + group, = self.getObjsByPath(path) + if not group.remoteIds: + return + aid, remoteGid = group.remoteIds + info = { + 'group': group.title, + 'account': account.title, + } + account.showError = showError + while gtk.events_pending(): + gtk.main_iteration_do(False) + #try: + self.waitingDo(account.sync, group, remoteGid) + ''' + except Exception as e: + showError( + _('Error in synchronizing group \"%(group)s\" with account \"%(account)s\"')%info + + '\n' + str(e), + self, + ) + else: + showInfo( + _('Successful synchronizing of group \"%(group)s\" with account \"%(account)s\"')%info, + self, + ) + ''' + self.reloadGroupEvents(group.id) + duplicateGroupFromMenu = lambda self, menu, path: self.duplicateGroup(path) + duplicateGroupWithEventsFromMenu = lambda self, menu, path: \ + self.duplicateGroupWithEvents(path) + def duplicateSelectedObj(self, button=None): + path = self.treev.get_cursor()[0] + if not path: + return + if len(path)==1: + self.duplicateGroup(path) + elif len(path)==2:## FIXME + self.toPasteEvent = (path, False) + self.pasteEventToPath(path) + def editGroupByPath(self, path): + from scal3.ui_gtk.event.group.editor import GroupEditorDialog + group, = self.getObjsByPath(path) + if group.name == 'trash': + self.editTrash() + else: + group = GroupEditorDialog(group, parent=self).run() + if group is None: + return + groupIter = self.trees.get_iter(path) + for i, value in enumerate(self.getGroupRow(group)): + self.trees.set_value(groupIter, i, value) + self.onGroupModify(group) + editGroupFromMenu = lambda self, menu, path: self.editGroupByPath(path) + def _do_deleteGroup(self, path, group): + trashedIds = group.idList + if core.eventTrashLastTop: + for eid in reversed(trashedIds): + event = group[eid] + self.insertEventRow(self.trashIter, 0, event) + else: + for eid in trashedIds: + event = group[eid] + self.appendEventRow(self.trashIter, event) + ui.deleteEventGroup(group) + self.trees.remove(self.trees.get_iter(path)) + def deleteGroup(self, path): + index, = path + group, = self.getObjsByPath(path) + eventCount = len(group) + if eventCount > 0: + if not confirm( + _('Press OK if you want to delete group "%s" and move its %s events to trash')%( + group.title, + _(eventCount), + ), + parent=self, + ): + return + self.waitingDo(self._do_deleteGroup, path, group) + deleteGroupFromMenu = lambda self, menu, path: self.deleteGroup(path) + def addEventToGroupFromMenu(self, menu, path, group, eventType, title): + event = addNewEvent( + group, + eventType, + title=title, + parent=self, + ) + if event is None: + return + groupIter = self.trees.get_iter(path) + if group.id in self.loadedGroupIds: + self.appendEventRow(groupIter, event) + self.treeviewCursorChanged() + def addGenericEventToGroupFromMenu(self, menu, path, group): + event = addNewEvent( + group, + group.acceptsEventTypes[0], + typeChangable=True, + title=_('Add Event'), + parent=self, + ) + if event is None: + return + groupIter = self.trees.get_iter(path) + if group.id in self.loadedGroupIds: + self.appendEventRow(groupIter, event) + self.treeviewCursorChanged() + def updateEventRow(self, event): + self.updateEventRowByIter( + event, + self.eventsIter[event.id], + ) + def updateEventRowByIter(self, event, eventIter): + for i, value in enumerate(self.getEventRow(event)): + self.trees.set_value(eventIter, i, value) + self.treeviewCursorChanged() + def editEventByPath(self, path): + from scal3.ui_gtk.event.editor import EventEditorDialog + group, event = self.getObjsByPath(path) + if group.name == 'trash':## FIXME + return + event = EventEditorDialog( + event, + title=_('Edit ')+event.desc, + parent=self, + ).run() + if event is None: + return + self.updateEventRow(event) + editEventFromMenu = lambda self, menu, path: self.editEventByPath(path) + def moveEventToPathFromMenu(self, menu, path, tarPath): + self.toPasteEvent = (path, True) + self.pasteEventToPath(tarPath, False) + def moveEventToTrash(self, path): + group, event = self.getObjsByPath(path) + if not confirmEventTrash(event, parent=self): + return + ui.moveEventToTrash(group, event) + self.trees.remove(self.trees.get_iter(path)) + if core.eventTrashLastTop: + self.insertEventRow(self.trashIter, 0, event) + else: + self.appendEventRow(self.trashIter, event) + moveEventToTrashFromMenu = lambda self, menu, path: self.moveEventToTrash(path) + def moveSelectionToTrash(self): + path = self.treev.get_cursor()[0] + if not path: + return + objs = self.getObjsByPath(path) + if len(path)==1: + self.deleteGroup(path) + elif len(path)==2: + self.moveEventToTrash(path) + def deleteEventFromTrash(self, menu, path): + trash, event = self.getObjsByPath(path) + trash.delete(event.id)## trash == ui.eventTrash + trash.save() + self.trees.remove(self.trees.get_iter(path)) + def removeIterChildren(self, _iter): + while True: + childIter = self.trees.iter_children(_iter) + if childIter is None: + break + self.trees.remove(childIter) + def emptyTrash(self, menu): + ui.eventTrash.empty() + self.removeIterChildren(self.trashIter) + self.treeviewCursorChanged() + def editTrash(self, obj=None): + TrashEditorDialog(parent=self).run() + self.trees.set_value( + self.trashIter, + 1, + pixbufFromFile(ui.eventTrash.icon), + ) + self.trees.set_value( + self.trashIter, + 2, + ui.eventTrash.title, + ) + def moveUp(self, path): + srcIter = self.trees.get_iter(path) + if len(path)==1: + if path[0]==0: + return + if self.trees.get_value(srcIter, 0)==-1: + return + tarIter = self.trees.get_iter((path[0]-1)) + self.trees.move_before(srcIter, tarIter) + ui.eventGroups.moveUp(path[0]) + ui.eventGroups.save() + elif len(path)==2: + parentObj, event = self.getObjsByPath(path) + parentLen = len(parentObj) + parentIndex, eventIndex = path + #print(eventIndex, parentLen) + if eventIndex > 0: + tarIter = self.trees.get_iter((parentIndex, eventIndex-1)) + self.trees.move_before(srcIter, tarIter)## or use self.trees.swap FIXME + parentObj.moveUp(eventIndex) + parentObj.save() + else: + ## move event to end of previous group + #if parentObj.name == 'trash': + # return + if parentIndex < 1: + return + newParentIter = self.trees.get_iter((parentIndex - 1)) + newParentId = self.trees.get_value(newParentIter, 0) + if newParentId==-1:## could not be! + return + newGroup = ui.eventGroups[newParentId] + self.checkEventToAdd(newGroup, event) + self.trees.remove(srcIter) + self.appendEventRow(newParentIter, event) + ### + parentObj.remove(event) + parentObj.save() + newGroup.append(event) + newGroup.save() + else: + raise RuntimeError('invalid tree path %s'%path) + newPath = self.trees.get_path(srcIter) + if len(path)==2: + self.treev.expand_to_path(newPath) + self.treev.set_cursor(newPath) + self.treev.scroll_to_cell(newPath) + def moveDown(self, path): + srcIter = self.trees.get_iter(path) + if len(path)==1: + if self.trees.get_value(srcIter, 0)==-1: + return + tarIter = self.trees.get_iter((path[0]+1)) + if self.trees.get_value(tarIter, 0)==-1: + return + self.trees.move_after(srcIter, tarIter)## or use self.trees.swap FIXME + ui.eventGroups.moveDown(path[0]) + ui.eventGroups.save() + elif len(path)==2: + parentObj, event = self.getObjsByPath(path) + parentLen = len(parentObj) + parentIndex, eventIndex = path + #print(eventIndex, parentLen) + if eventIndex < parentLen-1: + tarIter = self.trees.get_iter((parentIndex, eventIndex+1)) + self.trees.move_after(srcIter, tarIter) + parentObj.moveDown(eventIndex) + parentObj.save() + else: + ## move event to top of next group + if parentObj.name == 'trash': + return + newParentIter = self.trees.get_iter((parentIndex + 1)) + newParentId = self.trees.get_value(newParentIter, 0) + if newParentId==-1: + return + newGroup = ui.eventGroups[newParentId] + self.checkEventToAdd(newGroup, event) + self.trees.remove(srcIter) + srcIter = self.insertEventRow(newParentIter, 0, event) + ### + parentObj.remove(event) + parentObj.save() + newGroup.insert(0, event) + newGroup.save() + else: + raise RuntimeError('invalid tree path %s'%path) + newPath = self.trees.get_path(srcIter) + if len(path)==2: + self.treev.expand_to_path(newPath) + self.treev.set_cursor(newPath) + self.treev.scroll_to_cell(newPath) + moveUpFromMenu = lambda self, menu, path: self.moveUp(path) + moveDownFromMenu = lambda self, menu, path: self.moveDown(path) + def moveUpByButton(self, button): + path = self.treev.get_cursor()[0] + if not path: + return + self.moveUp(path) + def moveDownByButton(self, button): + path = self.treev.get_cursor()[0] + if not path: + return + self.moveDown(path) + def groupExportFromMenu(self, menu, group): + SingleGroupExportDialog(group, parent=self).run() + def groupSortFromMenu(self, menu, path): + index, = path + group, = self.getObjsByPath(path) + if GroupSortDialog(group, parent=self).run(): + if group.id in self.loadedGroupIds: + groupIter = self.trees.get_iter(path) + expanded = self.treev.row_expanded(path) + self.removeIterChildren(groupIter) + for event in group: + self.appendEventRow(groupIter, event) + if expanded: + self.treev.expand_row(path, False) + def groupConvertModeFromMenu(self, menu, group): + GroupConvertModeDialog(group, parent=self).run() + def _do_groupConvertTo(self, group, newGroupType): + idsCount = len(group.idList) + newGroup = ui.eventGroups.convertGroupTo(group, newGroupType) + ## reload it's events in tree? FIXME + ## summary and description haven't changed! + idsCount2 = len(newGroup.idList) + if idsCount2 != idsCount: + self.reloadGroupEvents(newGroup.id) + self.treeviewCursorChanged() + def groupConvertTo(self, menu, group, newGroupType): + self.waitingDo(self._do_groupConvertTo, group, newGroupType) + def _do_groupBulkEdit(self, dialog, group, path): + expanded = self.treev.row_expanded(path) + dialog.doAction() + dialog.destroy() + self.trees.remove(self.trees.get_iter(path)) + self.insertGroupTree(path[0], group) + if expanded: + self.treev.expand_row(path, False) + def groupBulkEditFromMenu(self, menu, group, path): + from scal3.ui_gtk.event.bulk_edit import EventsBulkEditDialog + dialog = EventsBulkEditDialog(group, parent=self) + if dialog.run()==gtk.ResponseType.OK: + self.waitingDo(self._do_groupBulkEdit, dialog, group, path) + def groupActionClicked(self, menu, group, actionFuncName): + func = getattr(group, actionFuncName) + self.waitingDo(func, parentWin=self) + def cutEvent(self, menu, path): + self.toPasteEvent = (path, True) + def copyEvent(self, menu, path): + self.toPasteEvent = (path, False) + pasteEventFromMenu = lambda self, menu, tarPath: self.pasteEventToPath(tarPath) + def pasteEventToPath(self, tarPath, doScroll=True): + if not self.toPasteEvent: + return + srcPath, move = self.toPasteEvent + srcGroup, srcEvent = self.getObjsByPath(srcPath) + tarGroup = self.getObjsByPath(tarPath)[0] + self.checkEventToAdd(tarGroup, srcEvent) + if len(tarPath)==1: + tarGroupIter = self.trees.get_iter(tarPath) + tarEventIter = None + tarEventIndex = len(tarGroup) + elif len(tarPath)==2: + tarGroupIter = self.trees.get_iter(tarPath[:1]) + tarEventIter = self.trees.get_iter(tarPath) + tarEventIndex = tarPath[1] + #### + if move: + srcGroup.remove(srcEvent) + srcGroup.save() + tarGroup.insert(tarEventIndex, srcEvent) + tarGroup.save() + self.trees.remove(self.trees.get_iter(srcPath)) + newEvent = srcEvent + else: + newEvent = srcEvent.copy() + newEvent.save() + tarGroup.insert(tarEventIndex, newEvent) + tarGroup.save() + #### + if tarEventIter: + newEventIter = self.insertEventRowAfter(tarGroupIter, tarEventIter, newEvent) + else: + newEventIter = self.appendEventRow(tarGroupIter, newEvent) + if doScroll: + self.treev.set_cursor(self.trees.get_path(newEventIter)) + self.toPasteEvent = None + #def selectAllEventInGroup(self, menu):## FIXME + # pass + #def selectAllEventInTrash(self, menu):## FIXME + # pass + def onDeleteEvent(self, obj, event): + self.hide() + return True + diff --git a/scal3/ui_gtk/event/notifier/__init__.py b/scal3/ui_gtk/event/notifier/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scal3/ui_gtk/event/notifier/alarm.py b/scal3/ui_gtk/event/notifier/alarm.py new file mode 100644 index 000000000..4e3eddf26 --- /dev/null +++ b/scal3/ui_gtk/event/notifier/alarm.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from subprocess import Popen, PIPE + +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * +#from scal3.ui_gtk import player + + +#class WidgetClass(player.PlayerBox): +# def __init__(self, notifier): +# self.notifier = notifier +# player.PlayerBox.__init__(self) +# def updateWidget(self): +# if self.notifier.alarmSound: +# self.openFile(self.notifier.alarmSound) +# def updateVars(self): +# self.notifier.alarmSound = self.getFile() + +class WidgetClass(gtk.FileChooserButton): + def __init__(self, notifier): + self.notifier = notifier + gtk.FileChooserButton.__init__(self, _('Select Sound')) + self.set_local_only(True) + def updateWidget(self): + if self.notifier.alarmSound: + self.set_filename(self.notifier.alarmSound) + def updateVars(self): + self.notifier.alarmSound = self.get_filename() + +def notifyWait(notifier, finishFunc): + if notifier.alarmSound and notifier.playerCmd: + Popen([notifier.playerCmd, notifier.alarmSound], stdout=PIPE, stderr=PIPE).communicate() + #finishFunc() + +def notify(notifier, finishFunc): + #import thread + #thread.start_new_thread(notifyWait, (notifier, finishFunc)) + finishFunc() + Popen([notifier.playerCmd, notifier.alarmSound], stdout=PIPE, stderr=PIPE) + +## event_lib.AlarmNotifier.WidgetClass = AlarmWidgetClass +## event_lib.AlarmNotifier.notify = notify + diff --git a/scal3/ui_gtk/event/notifier/command.py b/scal3/ui_gtk/event/notifier/command.py new file mode 100644 index 000000000..90ea99181 --- /dev/null +++ b/scal3/ui_gtk/event/notifier/command.py @@ -0,0 +1,3 @@ +from scal3 import core +from scal3.locale_man import tr as _ + diff --git a/scal3/ui_gtk/event/notifier/floatingMsg.py b/scal3/ui_gtk/event/notifier/floatingMsg.py new file mode 100644 index 000000000..93acc494b --- /dev/null +++ b/scal3/ui_gtk/event/notifier/floatingMsg.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3 import core +from scal3.locale_man import tr as _ + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.floatingMsg import * + +class WidgetClass(gtk.HBox): + def __init__(self, notifier): + self.notifier = notifier + ## + gtk.HBox.__init__(self) + ## [_] Fill Screen Width Speed [__] BG Color [__] Text Color [__] + ## + self.fillWidthCb = gtk.CheckButton(_('Fill Width')) + pack(self, self.fillWidthCb) + pack(self, gtk.Label(''), 1, 1) + ## + self.speedSpin = IntSpinButton(1, 999) + pack(self, gtk.Label(_('Speed'))) + pack(self, self.speedSpin) + pack(self, gtk.Label(''), 1, 1) + ## + self.bgColorButton = MyColorButton() + pack(self, gtk.Label(_('BG Color'))) + pack(self, self.bgColorButton) + pack(self, gtk.Label(''), 1, 1) + ## + self.textColorButton = MyColorButton() + pack(self, gtk.Label(_('Text Color'))) + pack(self, self.textColorButton) + pack(self, gtk.Label(''), 1, 1) + def updateWidget(self): + self.fillWidthCb.set_active(self.notifier.fillWidth) + self.speedSpin.set_value(self.notifier.speed) + self.bgColorButton.set_color(self.notifier.bgColor) + self.textColorButton.set_color(self.notifier.textColor) + def updateVars(self): + self.notifier.fillWidth = self.fillWidthCb.get_active() + self.notifier.speed = self.speedSpin.get_value() + self.notifier.bgColor = self.bgColorButton.get_color() + self.notifier.textColor = self.textColorButton.get_color() + +def notify(notifier, finishFunc):## FIXME + cls = FloatingMsg if notifier.fillWidth else NoFillFloatingMsgWindow + text = notifier.event.getText() + msg = cls( + text, + speed = notifier.speed, + bgColor = notifier.bgColor, + textColor = notifier.textColor, + finishFunc = finishFunc + ) + msg.show() + diff --git a/scal3/ui_gtk/event/notifier/windowMsg.py b/scal3/ui_gtk/event/notifier/windowMsg.py new file mode 100644 index 000000000..318b0df3d --- /dev/null +++ b/scal3/ui_gtk/event/notifier/windowMsg.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import imageFromFile + +class WidgetClass(gtk.Entry): + def __init__(self, notifier): + self.notifier = notifier + gtk.Entry.__init__(self) + def updateWidget(self): + self.set_text(self.notifier.extraMessage) + def updateVars(self): + self.notifier.extraMessage = self.get_text() + +def hideWindow(widget, dialog): + dialog.hide() + return True + +def notify(notifier, finishFunc):## FIXME + event = notifier.event + dialog = gtk.Dialog(parent=None) + #### + lines = [] + lines.append(event.getText()) + if notifier.extraMessage: + lines.append(notifier.extraMessage) + text = '\n'.join(lines) + #### + dialog.set_title(event.getText()) + #### + hbox = gtk.HBox(spacing=15) + hbox.set_border_width(10) + if event.icon: + pack(hbox, imageFromFile(event.icon)) + dialog.set_icon_from_file(event.icon) + label = gtk.Label(text) + label.set_selectable(True) + pack(hbox, label, 1, 1) + pack(dialog.vbox, hbox) + #### + okB = dialog.add_button(gtk.STOCK_OK, 3) + okB.connect('clicked', hideWindow, dialog) + if ui.autoLocale: + okB.set_label(_('_OK')) + okB.set_image(gtk.Image.new_from_stock(gtk.STOCK_OK, gtk.IconSize.BUTTON)) + #### + dialog.vbox.show_all() + dialog.connect('response', lambda w, e: finishFunc()) + dialog.present() + diff --git a/scal3/ui_gtk/event/occurrence_view.py b/scal3/ui_gtk/event/occurrence_view.py new file mode 100644 index 000000000..eab3137e0 --- /dev/null +++ b/scal3/ui_gtk/event/occurrence_view.py @@ -0,0 +1,309 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.utils import imageFromFile, labelStockMenuItem, labelImageMenuItem +from scal3.ui_gtk import gtk_ud as ud + + +@registerSignals +class DayOccurrenceView(gtk.ScrolledWindow, ud.BaseCalObj): + _name = 'eventDayView' + desc = _('Events of Day') + updateData = lambda self: self.updateDataByGroups(ui.eventGroups) + def __init__(self): + gtk.ScrolledWindow.__init__(self) + self.set_policy(gtk.PolicyType.NEVER, gtk.PolicyType.AUTOMATIC) + self.connect('size-allocate', self.onSizeRequest) + self.vbox = gtk.VBox(spacing=5) + self.add_with_viewport(self.vbox) + self.initVars() + self.maxHeight = 200 + self.showDesc = True + def onSizeRequest(self, widget, requisition): + #print('onSizeRequest', requisition.width, requisition.height) + requisition.height = min( + self.maxHeight,## FIXME + self.vbox.size_request().height + 2,## >=2 FIXME + ) + return True + def onDateChange(self, *a, **kw): + from scal3.ui_gtk.mywidgets.text_widgets import ReadOnlyLabel + ud.BaseCalObj.onDateChange(self, *a, **kw) + cell = ui.cell + ## destroy all VBox contents and add again + for hbox in self.vbox.get_children(): + hbox.destroy() + self.labels = []## we don't use it, just to prevent garbage collector from removing it + for occurData in cell.eventsData: + if not occurData['show'][0]: + continue + ## occurData['time'], occurData['text'], occurData['icon'] + text = ''.join(occurData['text']) if self.showDesc else occurData['text'][0] + ### + hbox = gtk.HBox(spacing=5) + if occurData['icon']: + pack(hbox, imageFromFile(occurData['icon'])) + if occurData['time']: + label = ReadOnlyLabel(occurData['time']) + self.labels.append(label) + label.set_direction(gtk.TextDirection.LTR) + label.connect('populate-popup', self.onEventLabelPopup, occurData) + pack(hbox, label) + pack(hbox, gtk.Label(' ')) + label = ReadOnlyLabel(text) + self.labels.append(label) + label.set_line_wrap(True) + label.set_use_markup(False)## should escape text if using markup FIXME + label.connect('populate-popup', self.onEventLabelPopup, occurData) + pack(hbox, label)## or 1, 1 (center) FIXME + pack(self.vbox, hbox) + pack(self.vbox, gtk.HSeparator()) + self.show_all() + self.vbox.show_all() + self.set_visible(bool(cell.eventsData)) + def moveEventToGroupFromMenu(self, item, event, prev_group, newGroup): + prev_group.remove(event) + prev_group.save() + ui.reloadGroups.append(prev_group.id) + ### + newGroup.append(event) + newGroup.save() + ### + ui.eventDiff.add('v', event) + ### + self.onConfigChange() + def copyOccurToGroupFromMenu(self, item, newGroup, newEventType, event, occurData): + newEvent = newGroup.createEvent(newEventType) + newEvent.copyFrom(event) + startEpoch, endEpoch = occurData['time_epoch'] + newEvent.setStartEpoch(startEpoch) + newEvent.setEnd('epoch', endEpoch) + newEvent.afterModify() + newEvent.save() + ### + newGroup.append(newEvent) + newGroup.save() + ui.eventDiff.add('+', newEvent) + ### + self.onConfigChange() + def onEventLabelPopup(self, label, menu, occurData): + from scal3.ui_gtk.event.utils import menuItemFromEventGroup + if event_lib.readOnly: + return + menu = gtk.Menu() + label.labelMenuAddCopyItems(menu) + #### + groupId, eventId = occurData['ids'] + event = ui.getEvent(groupId, eventId) + group = ui.eventGroups[groupId] + if not event.readOnly: + menu.add(gtk.SeparatorMenuItem()) + ### + winTitle = _('Edit ') + event.desc + menu.add(labelStockMenuItem( + winTitle, + gtk.STOCK_EDIT, + self.editEventClicked, + winTitle, + event, + groupId, + )) + ### + moveToItem = labelStockMenuItem( + _('Move to %s')%'...', + None,## FIXME + ) + moveToMenu = gtk.Menu() + for newGroup in ui.eventGroups: + if newGroup.id == group.id: + continue + if not newGroup.enable: + continue + if event.name in newGroup.acceptsEventTypes: + newGroupItem = menuItemFromEventGroup(newGroup) + newGroupItem.connect( + 'activate', + self.moveEventToGroupFromMenu, + event, + group, + newGroup, + ) + moveToMenu.add(newGroupItem) + moveToItem.set_submenu(moveToMenu) + menu.add(moveToItem) + ### + if not event.isSingleOccur: + newEventType = 'allDayTask' if occurData['is_allday'] else 'task' + copyOccurItem = labelStockMenuItem( + _('Copy as %s to...') % event_lib.classes.event.byName[newEventType].desc,## FIXME + None, + ) + copyOccurMenu = gtk.Menu() + for newGroup in ui.eventGroups: + if not newGroup.enable: + continue + if newEventType in newGroup.acceptsEventTypes: + newGroupItem = menuItemFromEventGroup(newGroup) + newGroupItem.connect( + 'activate', + self.copyOccurToGroupFromMenu, + newGroup, + newEventType, + event, + occurData, + ) + copyOccurMenu.add(newGroupItem) + copyOccurItem.set_submenu(copyOccurMenu) + menu.add(copyOccurItem) + ### + menu.add(gtk.SeparatorMenuItem()) + ### + menu.add(labelImageMenuItem( + _('Move to %s') % ui.eventTrash.title, + ui.eventTrash.icon, + self.moveEventToTrash, + event, + groupId, + )) + #### + menu.show_all() + label.tmpMenu = menu + menu.popup(None, None, None, None, 3, 0) + ui.updateFocusTime() + def editEventClicked(self, item, winTitle, event, groupId): + from scal3.ui_gtk.event.editor import EventEditorDialog + event = EventEditorDialog( + event, + title=winTitle, + #parent=self,## FIXME + ).run() + if event is None: + return + ui.eventDiff.add('e', event) + self.onConfigChange() + def moveEventToTrash(self, item, event, groupId): + from scal3.ui_gtk.event.utils import confirmEventTrash + if not confirmEventTrash(event, parent=ui.mainWin): + return + ui.moveEventToTrashFromOutside(ui.eventGroups[groupId], event) + self.onConfigChange() + + +class WeekOccurrenceView(gtk.TreeView): + updateData = lambda self: self.updateDataByGroups(ui.eventGroups) + def __init__(self, abrivateWeekDays=False): + self.abrivateWeekDays = abrivateWeekDays + self.absWeekNumber = core.getAbsWeekNumberFromJd(ui.cell.jd)## FIXME + gtk.TreeView.__init__(self) + self.set_headers_visible(False) + self.ls = gtk.ListStore(GdkPixbuf.Pixbuf, str, str, str)## icon, weekDay, time, text + self.set_model(self.ls) + ### + cell = gtk.CellRendererPixbuf() + col = gtk.TreeViewColumn(_('Icon'), cell) + col.add_attribute(cell, 'pixbuf', 0) + self.append_column(col) + ### + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('Week Day'), cell) + col.add_attribute(cell, 'text', 1) + col.set_resizable(True) + self.append_column(col) + ### + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('Time'), cell) + col.add_attribute(cell, 'text', 2) + col.set_resizable(True)## FIXME + self.append_column(col) + ### + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('Description'), cell) + col.add_attribute(cell, 'text', 3) + col.set_resizable(True) + self.append_column(col) + def updateWidget(self): + cells, wEventData = ui.cellCache.getWeekData(self.absWeekNumber) + self.ls.clear() + for item in wEventData: + if not item['show'][1]: + continue + self.ls.append( + pixbufFromFile(item['icon']), + core.weekDayNameAuto(self.abrivateWeekDays)[item['weekDay']], + item['time'], + item['text'], + ) + + +''' +class MonthOccurrenceView(gtk.TreeView, event_lib.MonthOccurrenceView): + updateData = lambda self: self.updateDataByGroups(ui.eventGroups) + def __init__(self): + event_lib.MonthOccurrenceView.__init__(self, ui.cell.jd) + gtk.TreeView.__init__(self) + self.set_headers_visible(False) + self.ls = gtk.ListStore(GdkPixbuf.Pixbuf, str, str, str)## icon, day, time, text + self.set_model(self.ls) + ### + cell = gtk.CellRendererPixbuf() + col = gtk.TreeViewColumn('', cell) + col.add_attribute(cell, 'pixbuf', 0) + self.append_column(col) + ### + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('Day'), cell) + col.add_attribute(cell, 'text', 1) + col.set_resizable(True) + self.append_column(col) + ### + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('Time'), cell) + col.add_attribute(cell, 'text', 2) + col.set_resizable(True)## FIXME + self.append_column(col) + ### + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('Description'), cell) + col.add_attribute(cell, 'text', 3) + col.set_resizable(True) + self.append_column(col) + def updateWidget(self): + self.updateData() + self.ls.clear()## FIXME + for item in self.data: + if not item['show'][2]: + continue + self.ls.append( + pixbufFromFile(item['icon']), + _(item['day']), + item['time'], + item['text'], + ) +''' + + + + diff --git a/scal3/ui_gtk/event/rule/__init__.py b/scal3/ui_gtk/event/rule/__init__.py new file mode 100644 index 000000000..6cc65232e --- /dev/null +++ b/scal3/ui_gtk/event/rule/__init__.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * + +''' +class MultiValueRule(gtk.HBox): + def __init__(self, rule, ValueWidgetClass): + self.rule = rule + self.ValueWidgetClass = ValueWidgetClass + ## + gtk.HBox.__init__(self) + self.widgetsBox = gtk.HBox() + pack(self, self.widgetsBox) + ## + self.removeButton = gtk.Button() + self.removeButton.set_image(gtk.Image.new_from_stock(gtk.STOCK_REMOVE, gtk.IconSize.MENU)) + self.removeButton.connect('clicked', self.removeLastWidget) + ## + + + ## + self.removeButton.hide()## FIXME + + def removeLastWidget(self, obj=None): + + def addWidget(self, obj=None): + widget = self.ValueWidgetClass() +''' + + diff --git a/scal3/ui_gtk/event/rule/cycleDays.py b/scal3/ui_gtk/event/rule/cycleDays.py new file mode 100644 index 000000000..e441f2437 --- /dev/null +++ b/scal3/ui_gtk/event/rule/cycleDays.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from scal3.ui_gtk.event.rule.numberSimpleRule import * diff --git a/scal3/ui_gtk/event/rule/cycleLen.py b/scal3/ui_gtk/event/rule/cycleLen.py new file mode 100644 index 000000000..7a6b83d3a --- /dev/null +++ b/scal3/ui_gtk/event/rule/cycleLen.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton +from scal3.ui_gtk.mywidgets.multi_spin.time_b import TimeButton + + +class WidgetClass(gtk.HBox): + def __init__(self, rule): + self.rule = rule + ### + gtk.HBox.__init__(self) + spin = IntSpinButton(0, 9999) + pack(self, spin) + self.spin = spin + ## + pack(self, gtk.Label(' '+_('days and')+' ')) + tbox = TimeButton() + pack(self, tbox) + self.tbox = tbox + def updateWidget(self): + self.spin.set_value(self.rule.days) + self.tbox.set_value(self.rule.extraTime) + def updateVars(self): + self.rule.days = self.spin.get_value() + self.rule.extraTime = self.tbox.get_value() + diff --git a/scal3/ui_gtk/event/rule/cycleWeeks.py b/scal3/ui_gtk/event/rule/cycleWeeks.py new file mode 100644 index 000000000..e441f2437 --- /dev/null +++ b/scal3/ui_gtk/event/rule/cycleWeeks.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from scal3.ui_gtk.event.rule.numberSimpleRule import * diff --git a/scal3/ui_gtk/event/rule/date.py b/scal3/ui_gtk/event/rule/date.py new file mode 100644 index 000000000..7dcc69ee5 --- /dev/null +++ b/scal3/ui_gtk/event/rule/date.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.date import DateButton + +class WidgetClass(DateButton): + def __init__(self, rule): + self.rule = rule + DateButton.__init__(self) + def updateWidget(self): + self.set_value(self.rule.date) + def updateVars(self): + self.rule.date = self.get_value() + def changeMode(self, mode): + curMode = self.rule.getMode() + if mode!=curMode: + y, m, d = self.get_value() + self.set_value(core.convert(y, m, d, curMode, mode)) diff --git a/scal3/ui_gtk/event/rule/dateTime.py b/scal3/ui_gtk/event/rule/dateTime.py new file mode 100644 index 000000000..8c94ca14a --- /dev/null +++ b/scal3/ui_gtk/event/rule/dateTime.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.date import DateButton +from scal3.ui_gtk.mywidgets.multi_spin.time_b import TimeButton + + +class WidgetClass(gtk.HBox): + def __init__(self, rule): + self.rule = rule + ### + gtk.ComboBox.__init__(self) + ### + self.dateInput = DateButton() + pack(self, self.dateInput) + ### + pack(self, gtk.Label(' '+_('Time'))) + self.timeInput = TimeButton() + pack(self, self.timeInput) + def updateWidget(self): + self.dateInput.set_value(self.rule.date) + self.timeInput.set_value(self.rule.time) + def updateVars(self): + self.rule.date = self.dateInput.get_value() + self.rule.time = self.timeInput.get_value() + def changeMode(self, mode): + curMode = self.rule.getMode() + if mode!=curMode: + y, m, d = self.dateInput.get_value() + self.dateInput.set_value(core.convert(y, m, d, curMode, mode)) diff --git a/scal3/ui_gtk/event/rule/day.py b/scal3/ui_gtk/event/rule/day.py new file mode 100644 index 000000000..390566ee2 --- /dev/null +++ b/scal3/ui_gtk/event/rule/day.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.num_ranges_entry import NumRangesEntry + +class WidgetClass(NumRangesEntry): + def __init__(self, rule): + self.rule = rule + NumRangesEntry.__init__(self, 1, 31, 10) + def updateWidget(self): + self.setValues(self.rule.values) + def updateVars(self): + self.rule.values = self.getValues() + + diff --git a/scal3/ui_gtk/event/rule/dayTime.py b/scal3/ui_gtk/event/rule/dayTime.py new file mode 100644 index 000000000..7010029b4 --- /dev/null +++ b/scal3/ui_gtk/event/rule/dayTime.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.time_b import TimeButton + +class WidgetClass(TimeButton): + def __init__(self, rule): + self.rule = rule + TimeButton.__init__(self) + def updateWidget(self): + self.set_value(self.rule.dayTime) + def updateVars(self): + self.rule.dayTime = self.get_value() + + diff --git a/scal3/ui_gtk/event/rule/dayTimeRange.py b/scal3/ui_gtk/event/rule/dayTimeRange.py new file mode 100644 index 000000000..86bd43d91 --- /dev/null +++ b/scal3/ui_gtk/event/rule/dayTimeRange.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.date import DateButton +from scal3.ui_gtk.mywidgets.multi_spin.time_b import TimeButton + + +class WidgetClass(gtk.HBox): + def __init__(self, rule): + self.rule = rule + ### + gtk.HBox.__init__(self) + ### + self.startTbox = TimeButton() + self.endTbox = TimeButton() + pack(self, self.startTbox) + pack(self, gtk.Label(' ' + _('to') + ' ')) + pack(self, self.endTbox) + def updateWidget(self): + self.startTbox.set_value(self.rule.dayTimeStart) + self.endTbox.set_value(self.rule.dayTimeEnd) + def updateVars(self): + self.rule.dayTimeStart = self.startTbox.get_value() + self.rule.dayTimeEnd = self.endTbox.get_value() + + diff --git a/scal3/ui_gtk/event/rule/duration.py b/scal3/ui_gtk/event/rule/duration.py new file mode 100644 index 000000000..b4036254a --- /dev/null +++ b/scal3/ui_gtk/event/rule/duration.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * +from scal3.ui_gtk.event import common + + +class WidgetClass(common.DurationInputBox): + def __init__(self, rule): + self.rule = rule + common.DurationInputBox.__init__(self) + def updateWidget(self): + self.setDuration(self.rule.value, self.rule.unit) + def updateVars(self): + self.rule.value, self.rule.unit = self.getDuration() + diff --git a/scal3/ui_gtk/event/rule/end.py b/scal3/ui_gtk/event/rule/end.py new file mode 100644 index 000000000..cba086c2d --- /dev/null +++ b/scal3/ui_gtk/event/rule/end.py @@ -0,0 +1 @@ +from scal3.ui_gtk.event.rule.dateTime import WidgetClass diff --git a/scal3/ui_gtk/event/rule/ex_dates.py b/scal3/ui_gtk/event/rule/ex_dates.py new file mode 100644 index 000000000..e11f801d7 --- /dev/null +++ b/scal3/ui_gtk/event/rule/ex_dates.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3 import core +from scal3.date_utils import dateEncode, dateDecode +from scal3.locale_man import tr as _ +from scal3.locale_man import textNumEncode, textNumDecode +from scal3 import event_lib +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import toolButtonFromStock, set_tooltip + +## FIXME +encode = lambda d: textNumEncode(dateEncode(d)) +decode = lambda s: dateDecode(textNumDecode(s)) +validate = lambda s: encode(decode(s)) + +class WidgetClass(gtk.HBox): + def __init__(self, rule): + self.rule = rule + gtk.HBox.__init__(self) + ### + self.countLabel = gtk.Label('') + pack(self, self.countLabel) + ### + self.trees = gtk.ListStore(str) + self.dialog = None + ### + self.editButton = gtk.Button(_('Edit')) + self.editButton.set_image(gtk.Image.new_from_stock(gtk.STOCK_EDIT, gtk.IconSize.BUTTON)) + self.editButton.connect('clicked', self.showDialog) + pack(self, self.editButton) + def updateCountLabel(self): + self.countLabel.set_label(' '*2 + _('%s items')%_(len(self.trees)) + ' '*2) + def createDialog(self): + if self.dialog: + return + print('----- toplevel', self.get_toplevel()) + self.dialog = gtk.Dialog( + title=self.rule.desc, + parent=self.get_toplevel(), + ) + ### + self.treev = gtk.TreeView() + self.treev.set_headers_visible(True) + self.treev.set_model(self.trees) + ## + cell = gtk.CellRendererText() + cell.set_property('editable', True) + cell.connect('edited', self.dateCellEdited) + col = gtk.TreeViewColumn(_('Date'), cell, text=0) + self.treev.append_column(col) + ## + toolbar = gtk.Toolbar() + toolbar.set_orientation(gtk.Orientation.VERTICAL) + size = gtk.IconSize.SMALL_TOOLBAR + ## + tb = toolButtonFromStock(gtk.STOCK_ADD, size) + set_tooltip(tb, _('Add')) + tb.connect('clicked', self.addClicked) + toolbar.insert(tb, -1) + #self.buttonAdd = tb + ## + tb = toolButtonFromStock(gtk.STOCK_DELETE, size) + set_tooltip(tb, _('Delete')) + tb.connect('clicked', self.deleteClicked) + toolbar.insert(tb, -1) + #self.buttonDel = tb + ## + tb = toolButtonFromStock(gtk.STOCK_GO_UP, size) + set_tooltip(tb, _('Move up')) + tb.connect('clicked', self.moveUpClicked) + toolbar.insert(tb, -1) + ## + tb = toolButtonFromStock(gtk.STOCK_GO_DOWN, size) + set_tooltip(tb, _('Move down')) + tb.connect('clicked', self.moveDownClicked) + toolbar.insert(tb, -1) + ## + dialogHbox = gtk.HBox() + pack(dialogHbox, self.treev, 1, 1) + pack(dialogHbox, toolbar) + pack(self.dialog.vbox, dialogHbox, 1, 1) + self.dialog.vbox.show_all() + self.dialog.resize(200, 300) + self.dialog.connect('response', lambda w, e: self.dialog.hide()) + ## + okButton = self.dialog.add_button(gtk.STOCK_OK, gtk.ResponseType.CANCEL) + if ui.autoLocale: + okButton.set_label(_('_OK')) + okButton.set_image(gtk.Image.new_from_stock(gtk.STOCK_OK, gtk.IconSize.BUTTON)) + def showDialog(self, w=None): + self.createDialog() + self.dialog.run() + self.updateCountLabel() + def dateCellEdited(self, cell, path, newText): + index = int(path) + self.trees[index][0] = validate(newText) + def getSelectedIndex(self): + cur = self.treev.get_cursor() + try: + path, col = cur + index = path[0] + return index + except: + return None + def addClicked(self, button): + index = self.getSelectedIndex() + mode = self.rule.getMode()## FIXME + row = [encode(core.getSysDate(mode))] + if index is None: + newIter = self.trees.append(row) + else: + newIter = self.trees.insert(index+1, row) + self.treev.set_cursor(self.trees.get_path(newIter)) + #col = self.treev.get_column(0) + #cell = col.get_cell_renderers()[0] + #cell.start_editing(...) ## FIXME + def deleteClicked(self, button): + index = self.getSelectedIndex() + if index is None: + return + del self.trees[index] + def moveUpClicked(self, button): + index = self.getSelectedIndex() + if index is None: + return + t = self.trees + if index<=0 or index>=len(t): + gdk.beep() + return + t.swap(t.get_iter(index-1), t.get_iter(index)) + self.treev.set_cursor(index-1) + def moveDownClicked(self, button): + index = self.getSelectedIndex() + if index is None: + return + t = self.trees + if index<0 or index>=len(t)-1: + gdk.beep() + return + t.swap(t.get_iter(index), t.get_iter(index+1)) + self.treev.set_cursor(index+1) + def updateWidget(self): + for date in self.rule.dates: + self.trees.append([encode(date)]) + self.updateCountLabel() + def updateVars(self): + dates = [] + for row in self.trees: + dates.append(decode(row[0])) + self.rule.setDates(dates) + + diff --git a/scal3/ui_gtk/event/rule/ex_day.py b/scal3/ui_gtk/event/rule/ex_day.py new file mode 100644 index 000000000..c30a16a1d --- /dev/null +++ b/scal3/ui_gtk/event/rule/ex_day.py @@ -0,0 +1 @@ +from scal3.ui_gtk.event.rule.day import * diff --git a/scal3/ui_gtk/event/rule/ex_month.py b/scal3/ui_gtk/event/rule/ex_month.py new file mode 100644 index 000000000..cb08a29db --- /dev/null +++ b/scal3/ui_gtk/event/rule/ex_month.py @@ -0,0 +1 @@ +from scal3.ui_gtk.event.rule.month import * diff --git a/scal3/ui_gtk/event/rule/ex_year.py b/scal3/ui_gtk/event/rule/ex_year.py new file mode 100644 index 000000000..8d6530383 --- /dev/null +++ b/scal3/ui_gtk/event/rule/ex_year.py @@ -0,0 +1 @@ +from scal3.ui_gtk.event.rule.year import * diff --git a/scal3/ui_gtk/event/rule/month.py b/scal3/ui_gtk/event/rule/month.py new file mode 100644 index 000000000..abf76d1ad --- /dev/null +++ b/scal3/ui_gtk/event/rule/month.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from scal3 import core +from scal3 import locale_man +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import set_tooltip + + +class WidgetClass(gtk.HBox): + def __init__(self, rule): + self.rule = rule + ### + gtk.HBox.__init__(self) + ### + self.buttons = [] + mode = self.rule.getMode() + for i in range(12): + b = gtk.ToggleButton(_(i+1)) + set_tooltip(b, locale_man.getMonthName(mode, i+1)) + pack(self, b) + self.buttons.append(b) + def updateWidget(self): + monthList = self.rule.getValuesPlain() + for i in range(12): + self.buttons[i].set_active((i+1) in monthList) + def updateVars(self): + monthList = [] + for i in range(12): + if self.buttons[i].get_active(): + monthList.append(i+1) + self.rule.setValuesPlain(monthList) + def changeMode(self, mode): + if mode!=self.rule.getMode(): + for i in range(12): + set_tooltip(self.buttons[i], locale_man.getMonthName(mode, i+1)) + + diff --git a/scal3/ui_gtk/event/rule/numberSimpleRule.py b/scal3/ui_gtk/event/rule/numberSimpleRule.py new file mode 100644 index 000000000..10da2f6a7 --- /dev/null +++ b/scal3/ui_gtk/event/rule/numberSimpleRule.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton + +class WidgetClass(IntSpinButton): + def __init__(self, rule): + self.rule = rule + IntSpinButton.__init__(self, 0, 999999) + def updateWidget(self): + self.set_value(self.rule.getData()) + def updateVars(self): + self.rule.setData(self.get_value()) + diff --git a/scal3/ui_gtk/event/rule/start.py b/scal3/ui_gtk/event/rule/start.py new file mode 100644 index 000000000..cba086c2d --- /dev/null +++ b/scal3/ui_gtk/event/rule/start.py @@ -0,0 +1 @@ +from scal3.ui_gtk.event.rule.dateTime import WidgetClass diff --git a/scal3/ui_gtk/event/rule/weekDay.py b/scal3/ui_gtk/event/rule/weekDay.py new file mode 100644 index 000000000..323b6ef15 --- /dev/null +++ b/scal3/ui_gtk/event/rule/weekDay.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * + + +class WidgetClass(gtk.HBox): + def __init__(self, rule): + self.rule = rule + ### + gtk.HBox.__init__(self) + self.set_homogeneous(True) + ls = [gtk.ToggleButton(item) for item in core.weekDayNameAb] + s = core.firstWeekDay + for i in range(7): + pack(self, ls[(s+i)%7], 1, 1) + self.cbList = ls + self.start = s + def setStart(self, s):## not used, FIXME + b = self + ls = self.cbList + for j in range(7):## or range(6) + b.reorder_child(ls[(s+j)%7], j) + self.start = s + def updateVars(self): + weekDayList = [] + cbl = self.cbList + for j in range(7): + if cbl[j].get_active(): + weekDayList.append(j) + self.rule.weekDayList = tuple(weekDayList) + def updateWidget(self): + cbl = self.cbList + for cb in cbl: + cb.set_active(False) + for j in self.rule.weekDayList: + cbl[j].set_active(True) + diff --git a/scal3/ui_gtk/event/rule/weekMonth.py b/scal3/ui_gtk/event/rule/weekMonth.py new file mode 100644 index 000000000..65a1a038c --- /dev/null +++ b/scal3/ui_gtk/event/rule/weekMonth.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.weekday_combo import WeekDayComboBox +from scal3.ui_gtk.mywidgets.month_combo import MonthComboBox + +class WidgetClass(gtk.HBox): + def __init__(self, rule): + self.rule = rule + ##### + gtk.HBox.__init__(self) + ### + combo = gtk.ComboBoxText() + for item in rule.wmIndexNames: + combo.append_text(item) + pack(self, combo) + self.nthCombo = combo + ### + combo = WeekDayComboBox() + pack(self, combo) + self.weekDayCombo = combo + ### + pack(self, gtk.Label(_(' of '))) + ### + combo = MonthComboBox(True) + combo.build(rule.getMode()) + pack(self, combo) + self.monthCombo = combo + def updateVars(self): + self.rule.wmIndex = self.nthCombo.get_active() + self.rule.weekDay = self.weekDayCombo.getValue() + self.rule.month = self.monthCombo.getValue() + def updateWidget(self): + self.nthCombo.set_active(self.rule.wmIndex) + self.weekDayCombo.setValue(self.rule.weekDay) + self.monthCombo.setValue(self.rule.month) + def changeMode(self, newMode): + self.monthCombo.build(newMode) + + + diff --git a/scal3/ui_gtk/event/rule/weekNumMode.py b/scal3/ui_gtk/event/rule/weekNumMode.py new file mode 100644 index 000000000..ac39c4270 --- /dev/null +++ b/scal3/ui_gtk/event/rule/weekNumMode.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * + +class WidgetClass(gtk.ComboBoxText): + def __init__(self, rule): + self.rule = rule + ### + gtk.ComboBoxText.__init__(self) + ### + self.append_text(_('Every Week')) + self.append_text(_('Odd Weeks')) + self.append_text(_('Even Weeks')) + self.set_active(0) + def updateWidget(self): + self.set_active(self.rule.weekNumMode) + def updateVars(self): + self.rule.weekNumMode = self.get_active() + + diff --git a/scal3/ui_gtk/event/rule/year.py b/scal3/ui_gtk/event/rule/year.py new file mode 100644 index 000000000..1763f32f6 --- /dev/null +++ b/scal3/ui_gtk/event/rule/year.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import event_lib + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.num_ranges_entry import NumRangesEntry + +class WidgetClass(NumRangesEntry): + def __init__(self, rule): + self.rule = rule + NumRangesEntry.__init__(self, 0, 9999, 10) + def updateWidget(self): + self.setValues(self.rule.values) + def updateVars(self): + self.rule.values = self.getValues() + def changeMode(self, mode): + curMode = self.rule.getMode() + if mode!=curMode: + self.setValues(self.rule.newModeValues(mode)) + diff --git a/scal3/ui_gtk/event/search_events.py b/scal3/ui_gtk/event/search_events.py new file mode 100644 index 000000000..7105fccfc --- /dev/null +++ b/scal3/ui_gtk/event/search_events.py @@ -0,0 +1,560 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3.path import * +from scal3.utils import cmp +from scal3.cal_types import calTypes +from scal3 import core +from scal3.core import jd_to_primary +from scal3.locale_man import tr as _ +from scal3.locale_man import rtl +from scal3 import event_lib +from scal3 import ui + +from gi.repository import GdkPixbuf + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.utils import pixbufFromFile, labelStockMenuItem, labelImageMenuItem +from scal3.ui_gtk.drawing import newColorCheckPixbuf +from scal3.ui_gtk.mywidgets import TextFrame +from scal3.ui_gtk.mywidgets.multi_spin.date_time import DateTimeButton +from scal3.ui_gtk.mywidgets.dialog import MyDialog +from scal3.ui_gtk import gtk_ud as ud +from scal3.ui_gtk.event.utils import confirmEventTrash +from scal3.ui_gtk.event.common import SingleGroupComboBox + + +@registerSignals +class EventSearchWindow(gtk.Window, MyDialog, ud.BaseCalObj): + def __init__(self, showDesc=False): + gtk.Window.__init__(self) + self.maximize() + self.initVars() + ud.windowList.appendItem(self) + ### + self.set_title(_('Search Events')) + self.connect('delete-event', self.closed) + self.connect('key-press-event', self.keyPress) + ### + self.vbox = gtk.VBox() + self.add(self.vbox) + ###### + frame = TextFrame() + frame.set_label(_('Text')) + frame.set_border_width(5) + pack(self.vbox, frame) + self.textInput = frame + ## + hbox = gtk.HBox() + self.textCSensCheck = gtk.CheckButton(_('Case Sensitive')) + self.textCSensCheck.set_active(False) ## FIXME + pack(hbox, self.textCSensCheck) + pack(self.vbox, hbox) + ###### + jd = core.getCurrentJd() + year, month, day = jd_to_primary(jd) + ###### + hbox = gtk.HBox() + frame = gtk.Frame() + frame.set_label(_('Time')) + frame.set_border_width(5) + vboxIn = gtk.VBox() + sgroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + #### + hboxIn = gtk.HBox() + ## + self.timeFromCheck = gtk.CheckButton(_('From')) + sgroup.add_widget(self.timeFromCheck) + pack(hboxIn, self.timeFromCheck) + pack(hboxIn, gtk.Label(' ')) + ## + self.timeFromInput = DateTimeButton() + self.timeFromInput.set_value(((year, 1, 1), (0, 0, 0))) + pack(hboxIn, self.timeFromInput) + ## + pack(vboxIn, hboxIn) + #### + hboxIn = gtk.HBox() + ## + self.timeToCheck = gtk.CheckButton(_('To')) + sgroup.add_widget(self.timeToCheck) + pack(hboxIn, self.timeToCheck) + pack(hboxIn, gtk.Label(' ')) + ## + self.timeToInput = DateTimeButton() + self.timeToInput.set_value(((year+1, 1, 1), (0, 0, 0))) + pack(hboxIn, self.timeToInput) + ## + pack(vboxIn, hboxIn) + ## + self.timeFromCheck.connect('clicked', self.updateTimeFromSensitive) + self.timeToCheck.connect('clicked', self.updateTimeToSensitive) + self.updateTimeFromSensitive() + self.updateTimeToSensitive() + #### + vboxIn.set_border_width(5) + frame.add(vboxIn) + pack(hbox, frame) + pack(hbox, gtk.Label(''), 1, 1) + pack(self.vbox, hbox) + ###### + hbox = gtk.HBox() + hbox.set_border_width(5) + self.modifiedFromCheck = gtk.CheckButton(_('Modified From')) + pack(hbox, self.modifiedFromCheck) + pack(hbox, gtk.Label(' ')) + self.modifiedFromInput = DateTimeButton() + self.modifiedFromInput.set_value(((year, 1, 1), (0, 0, 0))) + pack(hbox, self.modifiedFromInput) + ## + self.modifiedFromCheck.connect('clicked', self.updateModifiedFromSensitive) + self.updateModifiedFromSensitive() + pack(self.vbox, hbox) + ###### + hbox = gtk.HBox() + hbox.set_border_width(5) + self.typeCheck = gtk.CheckButton(_('Event Type')) + pack(hbox, self.typeCheck) + pack(hbox, gtk.Label(' ')) + ## + combo = gtk.ComboBoxText() + for cls in event_lib.classes.event: + combo.append_text(cls.desc) + combo.set_active(0) + pack(hbox, combo) + self.typeCombo = combo + ## + self.typeCheck.connect('clicked', self.updateTypeSensitive) + self.updateTypeSensitive() + pack(self.vbox, hbox) + ###### + hbox = gtk.HBox() + hbox.set_border_width(5) + self.groupCheck = gtk.CheckButton(_('Group')) + pack(hbox, self.groupCheck) + pack(hbox, gtk.Label(' ')) + self.groupCombo = SingleGroupComboBox() + pack(hbox, self.groupCombo) + ## + self.groupCheck.connect('clicked', self.updateGroupSensitive) + self.updateGroupSensitive() + pack(self.vbox, hbox) + ###### + bbox = gtk.HButtonBox() + bbox.set_layout(gtk.ButtonBoxStyle.START) + bbox.set_border_width(5) + searchButton = gtk.Button() + searchButton.set_label(_('_Search')) + searchButton.set_image(gtk.Image.new_from_stock(gtk.STOCK_FIND, gtk.IconSize.BUTTON)) + searchButton.connect('clicked', self.searchClicked) + bbox.add(searchButton) + pack(self.vbox, bbox) + ###### + treev = gtk.TreeView() + trees = gtk.TreeStore(int, int, str, GdkPixbuf.Pixbuf, str, str) + ## gid, eid, group_name, icon, summary, description + treev.set_model(trees) + treev.connect('button-press-event', self.treevButtonPress) + treev.connect('row-activated', self.rowActivated) + treev.connect('key-press-event', self.treevKeyPress) + treev.set_headers_clickable(True) + ### + self.colGroup = gtk.TreeViewColumn(_('Group'), gtk.CellRendererText(), text=2) + self.colGroup.set_resizable(True) + self.colGroup.set_sort_column_id(2) + self.colGroup.set_property('expand', False) + treev.append_column(self.colGroup) + ### + self.colIcon = gtk.TreeViewColumn() + cell = gtk.CellRendererPixbuf() + pack(self.colIcon, cell) + self.colIcon.add_attribute(cell, 'pixbuf', 3) + #self.colIcon.set_sort_column_id(3)## FIXME + self.colIcon.set_property('expand', False) + treev.append_column(self.colIcon) + ### + self.colSummary = gtk.TreeViewColumn(_('Summary'), gtk.CellRendererText(), text=4) + self.colSummary.set_resizable(True) + self.colSummary.set_sort_column_id(4) + self.colSummary.set_property('expand', True)## FIXME + treev.append_column(self.colSummary) + ### + self.colDesc = gtk.TreeViewColumn(_('Description'), gtk.CellRendererText(), text=5) + self.colDesc.set_sort_column_id(5) + self.colDesc.set_visible(showDesc) + self.colDesc.set_property('expand', True)## FIXME + treev.append_column(self.colDesc) + ### + trees.set_sort_func(2, self.sort_func_group) + ###### + swin = gtk.ScrolledWindow() + swin.set_policy(gtk.PolicyType.AUTOMATIC, gtk.PolicyType.AUTOMATIC) + swin.add(treev) + #### + vbox = gtk.VBox(spacing=5) + vbox.set_border_width(5) + ### + topHbox = gtk.HBox() + self.resultLabel = gtk.Label('') + pack(topHbox, self.resultLabel) + pack(topHbox, gtk.Label(''), 1, 1) + pack(vbox, topHbox) + #### + columnBox = gtk.HBox(spacing=5) + pack(columnBox, gtk.Label(_('Columns')+': ')) + ## + check = gtk.CheckButton(_('Group')) + check.set_active(True) + check.connect('clicked', lambda w: self.colGroup.set_visible(w.get_active())) + pack(columnBox, check) + ## + check = gtk.CheckButton(_('Icon')) + check.set_active(True) + check.connect('clicked', lambda w: self.colIcon.set_visible(w.get_active())) + pack(columnBox, check) + ## + check = gtk.CheckButton(_('Summary')) + check.set_active(True) + check.connect('clicked', lambda w: self.colSummary.set_visible(w.get_active())) + pack(columnBox, check) + ## + check = gtk.CheckButton(_('Description')) + check.set_active(showDesc) + check.connect('clicked', lambda w: self.colDesc.set_visible(w.get_active())) + pack(columnBox, check) + ## + pack(vbox, columnBox) + #### + pack(vbox, swin, 1, 1) + ## + frame = gtk.Frame() + frame.set_label(_('Search Results')) + frame.set_border_width(10) + frame.add(vbox) + ## + pack(self.vbox, frame, 1, 1) + ### + bbox2 = gtk.HButtonBox() + bbox2.set_layout(gtk.ButtonBoxStyle.END) + bbox2.set_border_width(10) + closeButton = gtk.Button() + closeButton.set_label(_('_Close')) + closeButton.set_image(gtk.Image.new_from_stock(gtk.STOCK_CLOSE, gtk.IconSize.BUTTON)) + closeButton.connect('clicked', self.closed) + bbox2.add(closeButton) + pack(self.vbox, bbox2) + ###### + self.treev = treev + self.trees = trees + self.vbox.show_all() + #self.maximize()## FIXME + ## FIXME + sort_func_group = lambda self, model, iter1, iter2: cmp( + ui.eventGroups.index(model.get(iter1, 0)[0]), + ui.eventGroups.index(model.get(iter2, 0)[0]), + ) + def updateTimeFromSensitive(self, obj=None): + self.timeFromInput.set_sensitive(self.timeFromCheck.get_active()) + def updateTimeToSensitive(self, obj=None): + self.timeToInput.set_sensitive(self.timeToCheck.get_active()) + def updateModifiedFromSensitive(self, obj=None): + self.modifiedFromInput.set_sensitive(self.modifiedFromCheck.get_active()) + def updateTypeSensitive(self, obj=None): + self.typeCombo.set_sensitive(self.typeCheck.get_active()) + def updateGroupSensitive(self, obj=None): + self.groupCombo.set_sensitive(self.groupCheck.get_active()) + def _do_search(self): + if self.groupCheck.get_active(): + groupIds = [ + self.groupCombo.get_active() + ] + else: + groupIds = ui.eventGroups.getEnableIds() + ### + conds = {} + if self.textCSensCheck.get_active(): + conds['text'] = self.textInput.get_text() + else: + conds['text_lower'] = self.textInput.get_text().lower() + if self.timeFromCheck.get_active(): + conds['time_from'] = self.timeFromInput.get_epoch(calTypes.primary) + if self.timeToCheck.get_active(): + conds['time_to'] = self.timeToInput.get_epoch(calTypes.primary) + if self.modifiedFromCheck.get_active(): + conds['modified_from'] = self.modifiedFromInput.get_epoch(calTypes.primary) + if self.typeCheck.get_active(): + index = self.typeCombo.get_active() + cls = event_lib.classes.event[index] + conds['type'] = cls.name + ### + self.trees.clear() + for gid in groupIds: + group = ui.eventGroups[gid] + for evData in group.search(conds): + self.trees.append(None, ( + group.id, + evData['id'], + group.title, + pixbufFromFile(evData['icon']), + evData['summary'], + evData['description'], + )) + self.resultLabel.set_label(_('Found %s events')%_(len(self.trees))) + def searchClicked(self, obj=None): + self.waitingDo(self._do_search) + def editEventByPath(self, path): + from scal3.ui_gtk.event.editor import EventEditorDialog + try: + gid = self.trees[path][0] + eid = self.trees[path][1] + except: + return + group = ui.eventGroups[gid] + event = group[eid] + event = EventEditorDialog( + event, + title=_('Edit ')+event.desc, + parent=self, + ).run() + if event is None: + return + ### + ui.eventDiff.add('e', event) + ### + eventIter = self.trees.get_iter(path) + self.trees.set_value(eventIter, 3, pixbufFromFile(event.icon)) + self.trees.set_value(eventIter, 4, event.summary) + self.trees.set_value(eventIter, 5, event.getShownDescription()) + def rowActivated(self, treev, path, col): + self.editEventByPath(path) + def editEventFromMenu(self, menu, path): + self.editEventByPath(path) + def moveEventToGroupFromMenu(self, menu, eventPath, event, old_group, new_group): + old_group.remove(event) + old_group.save() + new_group.append(event) + new_group.save() + ### + ui.eventDiff.add('v', event) + ## FIXME + ### + eventIter = self.trees.get_iter(eventPath) + self.trees.set_value(eventIter, 0, new_group.id) + self.trees.set_value(eventIter, 2, new_group.title) + def copyEventToGroupFromMenu(self, menu, eventPath, event, new_group): + new_event = event.copy() + new_event.save() + new_group.append(new_event) + new_group.save() + ### + ui.eventDiff.add('+', new_event) + ## FIXME + ### + eventIter = self.trees.get_iter(eventPath) + def moveEventToTrash(self, path): + try: + gid = self.trees[path][0] + eid = self.trees[path][1] + except: + return + group = ui.eventGroups[gid] + event = group[eid] + if not confirmEventTrash(event): + return + ui.moveEventToTrash(group, event) + ui.reloadTrash = True + ui.eventDiff.add('-', event) + self.trees.remove(self.trees.get_iter(path)) + moveEventToTrashFromMenu = lambda self, menu, path: self.moveEventToTrash(path) + def moveSelectionToTrash(self): + path = self.treev.get_cursor()[0] + if not path: + return + self.moveEventToTrash(path) + def getMoveToGroupSubMenu(self, path, group, event): + ## returns a MenuItem instance + item = labelStockMenuItem( + _('Move to %s')%'...', + None,## FIXME + ) + subMenu = gtk.Menu() + ### + for new_group in ui.eventGroups: + if new_group.id == group.id: + continue + #if not new_group.enable:## FIXME + # continue + if event.name in new_group.acceptsEventTypes: + new_groupItem = ImageMenuItem() + new_groupItem.set_label(new_group.title) + ## + image = gtk.Image() + image.set_from_pixbuf(newColorCheckPixbuf(new_group.color, 20, True)) + new_groupItem.set_image(image) + ## + new_groupItem.connect( + 'activate', + self.moveEventToGroupFromMenu, + path, + event, + group, + new_group, + ) + ## + subMenu.add(new_groupItem) + ## + item.set_submenu(subMenu) + return item + def getCopyToGroupSubMenu(self, path, event): + ## returns a MenuItem instance + item = labelStockMenuItem( + _('Copy to %s')%'...', + None,## FIXME + ) + subMenu = gtk.Menu() + ### + for new_group in ui.eventGroups: + #if not new_group.enable:## FIXME + # continue + if event.name in new_group.acceptsEventTypes: + new_groupItem = ImageMenuItem() + new_groupItem.set_label(new_group.title) + ## + image = gtk.Image() + image.set_from_pixbuf(newColorCheckPixbuf(new_group.color, 20, True)) + new_groupItem.set_image(image) + ## + new_groupItem.connect( + 'activate', + self.copyEventToGroupFromMenu, + path, + event, + new_group, + ) + ## + subMenu.add(new_groupItem) + ## + item.set_submenu(subMenu) + return item + def genRightClickMenu(self, path): + gid = self.trees[path][0] + eid = self.trees[path][1] + group = ui.eventGroups[gid] + event = group[eid] + ## + menu = gtk.Menu() + ## + menu.add(labelStockMenuItem( + 'Edit', + gtk.STOCK_EDIT, + self.editEventFromMenu, + path, + )) + ## + menu.add(self.getMoveToGroupSubMenu(path, group, event)) + menu.add(gtk.SeparatorMenuItem()) + menu.add(self.getCopyToGroupSubMenu(path, event)) + ## + menu.add(gtk.SeparatorMenuItem()) + menu.add(labelImageMenuItem( + _('Move to %s')%ui.eventTrash.title, + ui.eventTrash.icon, + self.moveEventToTrashFromMenu, + path, + )) + ## + menu.show_all() + return menu + def openRightClickMenu(self, path, etime=None): + menu = self.genRightClickMenu(path) + if not menu: + return + if etime is None: + etime = gtk.get_current_event_time() + self.tmpMenu = menu + menu.popup(None, None, None, None, 3, etime) + def treevButtonPress(self, widget, gevent): + pos_t = self.treev.get_path_at_pos(int(gevent.x), int(gevent.y)) + if not pos_t: + return + path, col, xRel, yRel = pos_t + #path, col = self.treev.get_cursor() ## FIXME + if not path: + return + if gevent.button==3: + self.openRightClickMenu(path, gevent.time) + return False + def treevKeyPress(self, treev, gevent): + #from scal3.time_utils import getGtkTimeFromEpoch + #print(gevent.time-getGtkTimeFromEpoch(now())## FIXME) + #print(now()-gdk.CURRENT_TIME/1000.0) + ## gdk.CURRENT_TIME == 0## FIXME + ## gevent.time == gtk.get_current_event_time() ## OK + kname = gdk.keyval_name(gevent.keyval).lower() + #print('treevKeyPress', kname) + if kname=='menu':## Simulate right click (key beside Right-Ctrl) + path = treev.get_cursor()[0] + if path: + menu = self.genRightClickMenu(path) + if not menu: + return + rect = treev.get_cell_area(path, treev.get_column(1)) + x = rect.x + if rtl: + x -= 100 + else: + x += 50 + dx, dy = treev.translate_coordinates(self, x, rect.y + rect.height) + wx, wy = self.get_window().get_origin() + self.tmpMenu = menu + menu.popup( + None, + None, + lambda m: (wx+dx, wy+dy+20, True), + None, + 3, + gevent.time, + ) + elif kname=='delete': + self.moveSelectionToTrash() + else: + #print(kname) + return False + return True + def clearResults(self): + self.trees.clear() + self.resultLabel.set_label('') + def closed(self, obj=None, gevent=None): + self.hide() + self.clearResults() + self.onConfigChange() + return True + def present(self): + self.groupCombo.updateItems() + gtk.Window.present(self) + def keyPress(self, arg, gevent): + kname = gdk.keyval_name(gevent.keyval).lower() + if kname == 'escape': + self.closed() + return True + return False + + + diff --git a/scal3/ui_gtk/event/tags.py b/scal3/ui_gtk/event/tags.py new file mode 100644 index 000000000..0b19336cb --- /dev/null +++ b/scal3/ui_gtk/event/tags.py @@ -0,0 +1,269 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3 import core +from scal3.core import pixDir, myRaise +from scal3.locale_man import tr as _ +from scal3 import event_lib +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import openWindow, dialog_add_button, hideList +from scal3.ui_gtk.mywidgets.icon import IconSelectButton + +#class EventCategorySelect(gtk.HBox): + +class EventTagsAndIconSelect(gtk.HBox): + def __init__(self): + gtk.HBox.__init__(self) + ######### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Category')+':')) + ##### + ls = gtk.ListStore(GdkPixbuf.Pixbuf, str) + combo = gtk.ComboBox() + combo.set_model(ls) + ### + cell = gtk.CellRendererPixbuf() + pack(combo, cell, False) + combo.add_attribute(cell, 'pixbuf', 0) + ### + cell = gtk.CellRendererText() + pack(combo, cell, True) + combo.add_attribute(cell, 'text', 1) + ### + ls.append([None, _('Custom')])## first or last FIXME + for item in ui.eventTags: + ls.append([ + GdkPixbuf.Pixbuf.new_from_file(item.icon) if item.icon else None, + item.desc + ]) + ### + self.customItemIndex = 0 ## len(ls)-1 + pack(hbox, combo) + self.typeCombo = combo + self.typeStore = ls + + ### + vbox = gtk.VBox() + pack(vbox, hbox) + pack(self, vbox) + ######### + iconLabel = gtk.Label(_('Icon')) + pack(hbox, iconLabel) + self.iconSelect = IconSelectButton() + pack(hbox, self.iconSelect) + tagsLabel = gtk.Label(_('Tags')) + pack(hbox, tagsLabel) + hbox3 = gtk.HBox() + self.tagButtons = [] + for item in ui.eventTags: + button = gtk.ToggleButton(item.desc) + button.tagName = item.name + self.tagButtons.append(button) + pack(hbox3, button) + self.swin = gtk.ScrolledWindow() + self.swin.set_policy(gtk.PolicyType.ALWAYS, gtk.PolicyType.NEVER)## horizontal AUTOMATIC or ALWAYS FIXME + self.swin.add_with_viewport(hbox3) + pack(self, self.swin, 1, 1) + self.customTypeWidgets = (iconLabel, self.iconSelect, tagsLabel, self.swin) + ######### + self.typeCombo.connect('changed', self.typeComboChanged) + self.connect('scroll-event', self.scrollEvent) + ######### + self.show_all() + hideList(self.customTypeWidgets) + def scrollEvent(self, widget, gevent): + self.swin.get_hscrollbar().emit('scroll-event', gevent) + def typeComboChanged(self, combo): + i = combo.get_active() + if i is None: + return + if i == self.customItemIndex: + showList(self.customTypeWidgets) + else: + hideList(self.customTypeWidgets) + def getData(self): + active = self.typeCombo.get_active() + if active in (-1, None): + icon = '' + tags = [] + else: + if active == self.customItemIndex: + icon = self.iconSelect.get_filename() + tags = [button.tagName for button in self.tagButtons if button.get_active()] + else: + item = ui.eventTags[active] + icon = item.icon + tags = [item.name] + return { + 'icon': icon, + 'tags': tags, + } + + +class TagsListBox(gtk.VBox): + ''' + [x] Only related tags tt: Show only tags related to this event type + Sort by: + Name + Usage + + + Related to this event type (first) + Most used (first) + Most used for this event type (first) + ''' + def __init__(self, eventType=''):## '' == 'custom' + gtk.VBox.__init__(self) + #### + self.eventType = eventType + ######## + if eventType: + hbox = gtk.HBox() + self.relatedCheck = gtk.CheckButton(_('Only related tags')) + set_tooltip(self.relatedCheck, _('Show only tags related to this event type')) + self.relatedCheck.set_active(True) + self.relatedCheck.connect('clicked', self.optionsChanged) + pack(hbox, self.relatedCheck) + pack(hbox, gtk.Label(''), 1, 1) + pack(self, hbox) + ######## + treev = gtk.TreeView() + trees = gtk.ListStore(str, bool, str, int, str)## name(hidden), enable, desc, usage(hidden), usage(locale) + treev.set_model(trees) + ### + cell = gtk.CellRendererToggle() + #cell.set_property('activatable', True) + cell.connect('toggled', self.enableCellToggled) + col = gtk.TreeViewColumn(_('Enable'), cell) + col.add_attribute(cell, "active", 1) + #cell.set_active(False) + col.set_resizable(True) + col.set_sort_column_id(1) + col.set_sort_indicator(True) + treev.append_column(col) + ### + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('Name'), cell, text=2)## really desc, not name + col.set_resizable(True) + col.set_sort_column_id(2) + col.set_sort_indicator(True) + treev.append_column(col) + ### + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('Usage'), cell, text=4) + #col.set_resizable(True) + col.set_sort_column_id(3) ## previous column (hidden and int) + col.set_sort_indicator(True) + treev.append_column(col) + ### + swin = gtk.ScrolledWindow() + swin.add(treev) + swin.set_policy(gtk.PolicyType.AUTOMATIC, gtk.PolicyType.AUTOMATIC) + pack(self, swin, 1, 1) + #### + self.treeview = treev + self.treestore = trees + #### + #ui.updateEventTagsUsage()## FIXME + #for (i, tagObj) in enumerate(ui.eventTags): tagObj.usage = i*10 ## for testing + self.optionsChanged() + self.show_all() + def optionsChanged(self, widget=None, tags=[]): + if not tags: + tags = self.getData() + tagObjList = ui.eventTags + if self.eventType: + if self.relatedCheck.get_active(): + tagObjList = [t for t in tagObjList if self.eventType in t.eventTypes] + self.treestore.clear() + for t in tagObjList: + self.treestore.append(( + t.name, + t.name in tags, ## True or False + t.desc, + t.usage, + _(t.usage) + )) + def enableCellToggled(self, cell, path): + i = int(path) + active = not cell.get_active() + self.treestore[i][1] = active + cell.set_active(active) + def getData(self): + tags = [] + for row in self.treestore: + if row[1]: + tags.append(row[0]) + return tags + def setData(self, tags): + self.optionsChanged(tags=tags) + + + +class TagEditorDialog(gtk.Dialog): + def __init__(self, eventType='', **kwargs): + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Tags')) + self.set_transient_for(None) + self.tags = [] + self.tagsBox = TagsListBox(eventType) + pack(self.vbox, self.tagsBox, 1, 1) + #### + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), gtk.ResponseType.CANCEL) + dialog_add_button(self, gtk.STOCK_OK, _('_OK'), gtk.ResponseType.OK) + #### + self.vbox.show_all() + self.getData = self.tagsBox.getData + self.setData = self.tagsBox.setData + + + + +class ViewEditTagsHbox(gtk.HBox): + def __init__(self, eventType=''): + gtk.HBox.__init__(self) + self.tags = [] + pack(self, gtk.Label(_('Tags')+': ')) + self.tagsLabel = gtk.Label('') + pack(self, self.tagsLabel, 1, 1) + self.dialog = TagEditorDialog(eventType, parent=self.get_toplevel()) + self.dialog.connect('response', self.dialogResponse) + self.editButton = gtk.Button() + self.editButton.set_label(_('_Edit')) + self.editButton.set_image(gtk.Image.new_from_stock(gtk.STOCK_EDIT, gtk.IconSize.BUTTON)) + self.editButton.connect('clicked', self.editButtonClicked) + pack(self, self.editButton) + self.show_all() + def editButtonClicked(self, widget): + openWindow(self.dialog) + def dialogResponse(self, dialog, resp): + #print('dialogResponse', dialog, resp) + if resp==gtk.ResponseType.OK: + self.setData(dialog.getData()) + dialog.hide() + def setData(self, tags): + self.tags = tags + self.dialog.setData(tags) + sep = _(',') + ' ' + self.tagsLabel.set_label(sep.join([ui.eventTagsDesc[tag] for tag in tags])) + getData = lambda self: self.tags + + diff --git a/scal3/ui_gtk/event/trash.py b/scal3/ui_gtk/event/trash.py new file mode 100644 index 000000000..3c4159ad1 --- /dev/null +++ b/scal3/ui_gtk/event/trash.py @@ -0,0 +1,60 @@ +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import dialog_add_button +from scal3.ui_gtk.mywidgets.icon import IconSelectButton +from scal3.ui_gtk.event.utils import checkEventsReadOnly + + +class TrashEditorDialog(gtk.Dialog): + def __init__(self, **kwargs): + checkEventsReadOnly() + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Edit Trash')) + #self.connect('delete-event', lambda obj, e: self.destroy()) + #self.resize(800, 600) + ### + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), gtk.ResponseType.CANCEL) + dialog_add_button(self, gtk.STOCK_OK, _('_OK'), gtk.ResponseType.OK) + ## + self.connect('response', lambda w, e: self.hide()) + ####### + self.trash = ui.eventTrash + ## + sizeGroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ####### + hbox = gtk.HBox() + label = gtk.Label(_('Title')) + label.set_alignment(0, 0.5) + pack(hbox, label) + sizeGroup.add_widget(label) + self.titleEntry = gtk.Entry() + pack(hbox, self.titleEntry, 1, 1) + pack(self.vbox, hbox) + #### + hbox = gtk.HBox() + label = gtk.Label(_('Icon')) + label.set_alignment(0, 0.5) + pack(hbox, label) + sizeGroup.add_widget(label) + self.iconSelect = IconSelectButton() + pack(hbox, self.iconSelect) + pack(hbox, gtk.Label(''), 1, 1) + pack(self.vbox, hbox) + #### + self.vbox.show_all() + self.updateWidget() + def run(self): + if gtk.Dialog.run(self)==gtk.ResponseType.OK: + self.updateVars() + self.destroy() + def updateWidget(self): + self.titleEntry.set_text(self.trash.title) + self.iconSelect.set_filename(self.trash.icon) + def updateVars(self): + self.trash.title = self.titleEntry.get_text() + self.trash.icon = self.iconSelect.filename + self.trash.save() + diff --git a/scal3/ui_gtk/event/utils.py b/scal3/ui_gtk/event/utils.py new file mode 100644 index 000000000..4d6151718 --- /dev/null +++ b/scal3/ui_gtk/event/utils.py @@ -0,0 +1,39 @@ +from scal3.locale_man import tr as _ +from scal3 import core +from scal3 import event_lib + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import confirm, showError, labelStockMenuItem +from scal3.ui_gtk.drawing import newColorCheckPixbuf + + +confirmEventTrash = lambda event, parent=None: confirm( + _('Press OK if you want to move event "%s" to trash')%event.summary, + parent=parent, +) + + +def checkEventsReadOnly(doException=True): + if event_lib.readOnly: + error = 'Events are Read-Only because they are locked by another StarCalendar 3.x process' + showError(_(error)) + if doException: + raise RuntimeError(error) + return False + return True + +def eventWriteMenuItem(*args, **kwargs): + item = labelStockMenuItem(*args, **kwargs) + item.set_sensitive(not event_lib.readOnly) + return item + +def menuItemFromEventGroup(group): + item = ImageMenuItem() + item.set_label(group.title) + ## + image = gtk.Image() + image.set_from_pixbuf(newColorCheckPixbuf(group.color, 20, True)) + item.set_image(image) + return item + + diff --git a/scal3/ui_gtk/export.py b/scal3/ui_gtk/export.py new file mode 100644 index 000000000..0205100fc --- /dev/null +++ b/scal3/ui_gtk/export.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import os, sys + +from scal3.cal_types import calTypes +from scal3 import core +from scal3 import locale_man +from scal3.locale_man import tr as _ +from scal3 import ui +from scal3.monthcal import getMonthStatus, getCurrentMonthStatus +from scal3.export import exportToHtml + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import openWindow, dialog_add_button +from scal3.ui_gtk.mywidgets.multi_spin.date import DateButton +from scal3.ui_gtk.mywidgets.multi_spin.time_b import TimeButton +from scal3.ui_gtk.mywidgets.multi_spin.year_month import YearMonthButton + + +#gdkColorToHtml = lambda color: '#%.2x%.2x%.2x'%(color.red/256, color.green/256, color.blue/256) + + + +class ExportDialog(gtk.Dialog): + def __init__(self, **kwargs): + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Export to %s')%'HTML') + ## parent=None FIXME + #self.set_has_separator(False) + ######## + hbox = gtk.HBox(spacing=2) + pack(hbox, gtk.Label(_('Month Range'))) + combo = gtk.ComboBoxText() + for t in ('Current Month', 'Whole Current Year', 'Custom'): + combo.append_text(_(t)) + pack(hbox, combo) + pack(hbox, gtk.Label(''), 1, 1) + self.combo = combo + ### + hbox2 = gtk.HBox(spacing=2) + pack(hbox2, gtk.Label(_('from month'))) + self.ymBox0 = YearMonthButton() + pack(hbox2, self.ymBox0) + pack(hbox2, gtk.Label(''), 1, 1) + pack(hbox2, gtk.Label(_('to month'))) + self.ymBox1 = YearMonthButton() + pack(hbox2, self.ymBox1) + pack(hbox, hbox2, 1, 1) + self.hbox2 = hbox2 + combo.set_active(0) + pack(self.vbox, hbox) + ######## + self.fcw = gtk.FileChooserWidget(action=gtk.FileChooserAction.SAVE) + pack(self.vbox, self.fcw, 1, 1) + self.vbox.set_focus_child(self.fcw)## FIXME + self.vbox.show_all() + combo.connect('changed', self.comboChanged) + ## + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), 1, self.onDelete) + dialog_add_button(self, gtk.STOCK_SAVE, _('_Save'), 2, self.save) + ## + self.connect('delete-event', self.onDelete) + try: + self.fcw.set_current_folder(core.deskDir) + except AttributeError:## PyGTK < 2.4 + pass + def comboChanged(self, widget=None, ym=None): + i = self.combo.get_active() + if ym==None: + ym = (ui.cell.year, ui.cell.month) + if i==0: + self.fcw.set_current_name('calendar-%.4d-%.2d.html'%ym) + self.hbox2.hide() + elif i==1: + self.fcw.set_current_name('calendar-%.4d.html'%ym[0]) + self.hbox2.hide() + else:#elif i==2 + self.fcw.set_current_name('calendar.html') + self.hbox2.show() + ## select_region(0, -4) ## FIXME + def onDelete(self, widget=None, event=None):## hide(close) File Chooser Dialog + self.hide() + return True + def save(self, widget=None): + self.get_window().set_cursor(gdk.Cursor.new(gdk.CursorType.WATCH)) + while gtk.events_pending(): + gtk.main_iteration_do(False) + path = self.fcw.get_filename() + if path in (None, ''): + return + print('Exporting to html file "%s"'%path) + i = self.combo.get_active() + months = [] + module = calTypes.primaryModule() + if i==0: + s = getCurrentMonthStatus() + months = [s] + title = '%s %s'%(locale_man.getMonthName(calTypes.primary, s.month, s.year), _(s.year)) + elif i==1: + for i in range(1, 13): + months.append(getMonthStatus(ui.cell.year, i)) + title = '%s %s'%(_('Calendar'), _(ui.cell.year)) + elif i==2: + y0, m0 = self.ymBox0.get_value() + y1, m1 = self.ymBox1.get_value() + for ym in range(y0*12+m0-1, y1*12+m1): + y, m = divmod(ym, 12) + m += 1 + months.append(getMonthStatus(y, m)) + title = _('Calendar') + exportToHtml(path, months, title) + self.get_window().set_cursor(gdk.Cursor.new(gdk.CursorType.LEFT_PTR)) + self.hide() + def showDialog(self, year, month): + self.comboChanged(ym=(year, month)) + self.ymBox0.set_value((year, month)) + self.ymBox1.set_value((year, month)) + self.resize(1, 1) + openWindow(self) + ''' + def exportSvg(self, path, monthList):## FIXME + ## monthList is a list of tuples (year, month) + #import cairo + hspace = 20 + mcal = ui.mainWin.mcal + x, y, w, h0 = mcal.get_allocation() + n = len(monthList) + h = n*h0 + (n-1)*hspace + fo = open(path+'.svg', 'w') + surface = cairo.SVGSurface(fo, w, h) + cr0 = cairo.Context(surface) + cr = gdk.CairoContext(cr0) + year = ui.cell.year + month = ui.cell.month + day = self.mcal.day + ui.mainWin.show() ## ?????????????? + for i in range(n): + surface.set_device_offset(0, i*(h0+hspace)) + mcal.dateChange((monthList[i][0], monthList[i][1], 1)) + mcal.drawAll(cr=cr, cursor=False) + mcal.queue_draw() + ui.mainWin.dateChange((year, month, day)) + surface.finish() + ''' + + + +class ExportToIcsDialog(gtk.Dialog): + def __init__(self, saveIcsFunc, defaultFileName, **kwargs): + self.saveIcsFunc = saveIcsFunc + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Export to %s')%'iCalendar') + ## parent=None FIXME + #self.set_has_separator(False) + ######## + hbox = gtk.HBox(spacing=2) + pack(hbox, gtk.Label(_('From')+' ')) + self.startDateInput = DateButton() + pack(hbox, self.startDateInput) + pack(hbox, gtk.Label(' '+_('To')+' ')) + self.endDateInput = DateButton() + pack(hbox, self.endDateInput) + pack(self.vbox, hbox) + #### + year, month, day = ui.todayCell.dates[calTypes.primary] + self.startDateInput.set_value((year, 1, 1, 0)) + self.endDateInput.set_value((year+1, 1, 1, 0)) + ######## + self.fcw = gtk.FileChooserWidget(action=gtk.FileChooserAction.SAVE) + pack(self.vbox, self.fcw, 1, 1) + self.vbox.set_focus_child(self.fcw)## FIXME + self.vbox.show_all() + ## + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), 1, self.onDelete) + dialog_add_button(self, gtk.STOCK_SAVE, _('_Save'), 2, self.save) + ## + self.connect('delete-event', self.onDelete) + self.fcw.connect('file-activated', self.save)## not working FIXME + ## + try: + self.fcw.set_current_folder(core.deskDir) + except AttributeError:## PyGTK < 2.4 + pass + if not defaultFileName.endswith('.ics'): + defaultFileName += '.ics' + self.fcw.set_current_name(defaultFileName) + def onDelete(self, widget=None, event=None):## hide(close) File Chooser Dialog + self.destroy() + return True + def save(self, widget=None): + self.get_window().set_cursor(gdk.Cursor.new(gdk.CursorType.WATCH)) + while gtk.events_pending(): + gtk.main_iteration_do(False) + path = self.fcw.get_filename() + if path in (None, ''): + return + print('Exporting to ics file "%s"'%path) + self.saveIcsFunc( + path, + core.primary_to_jd(*self.startDateInput.get_value()), + core.primary_to_jd(*self.endDateInput.get_value()), + ) + self.get_window().set_cursor(gdk.Cursor.new(gdk.CursorType.LEFT_PTR)) + self.destroy() + + diff --git a/scal3/ui_gtk/font_utils.py b/scal3/ui_gtk/font_utils.py new file mode 100644 index 000000000..c8dcaa8c1 --- /dev/null +++ b/scal3/ui_gtk/font_utils.py @@ -0,0 +1,39 @@ +from gi.repository import Pango as pango +W_NORMAL = pango.Weight.NORMAL +S_NORMAL = pango.Style.NORMAL +BOLD = pango.Weight.BOLD +ITALIC = pango.Style.ITALIC + +#import pango +#W_NORMAL = pango.WEIGHT_NORMAL +#S_NORMAL = pango.STYLE_NORMAL +#BOLD = pango.WEIGHT_BOLD +#ITALIC = pango.STYLE_ITALIC + +pfontDecode = lambda pfont: [ + pfont.get_family(), + pfont.get_weight()==BOLD, + pfont.get_style()==ITALIC, + pfont.get_size()/1024, +] + +def pfontEncode(font): + pfont = pango.FontDescription() + pfont.set_family(font[0]) + pfont.set_weight(BOLD if font[1] else W_NORMAL) + pfont.set_style(ITALIC if font[2] else S_NORMAL) + pfont.set_size(int(font[3]*1024)) + return pfont + +gfontDecode = lambda gfont: pfontDecode(pango.FontDescription(gfont))## gfont is a string like "Terafik 12" + +gfontEncode = lambda font: pfontEncode(font).to_string() + +getFontFamilyList = lambda widget: sorted( + ( + f.get_name() for f in widget.get_pango_context().list_families() + ), + key=str.upper, +) + + diff --git a/scal3/ui_gtk/gtk_ud.py b/scal3/ui_gtk/gtk_ud.py new file mode 100644 index 000000000..066fddc6b --- /dev/null +++ b/scal3/ui_gtk/gtk_ud.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +## The low-level module for gtk ui dependent stuff (classes/functions/settings) +## ud = ui dependent +## upper the "ui" module + +import time +from os.path import join + +from scal3.path import * +from scal3.utils import myRaise +from scal3.json_utils import * +from scal3.locale_man import rtl +from scal3 import core +from scal3 import ui +from scal3.format_time import compileTmFormat + +from gi.overrides.GObject import Object + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.font_utils import gfontDecode, pfontEncode + + +############################################################ + +sysConfPath = join(sysConfDir, 'ui-gtk.json') + +confPath = join(confDir, 'ui-gtk.json') + +confParams = ( + 'dateFormat', + 'clockFormat', + #'adjustTimeCmd', +) + + +def loadConf(): + loadModuleJsonConf(__name__) + updateFormatsBin() + +def saveConf(): + saveModuleJsonConf(__name__) + +############################################################ + +@registerSignals +class BaseCalObj(Object): + _name = '' + desc = '' + loaded = True + customizable = False + signals = [ + ('config-change', []), + ('date-change', []), + ] + def initVars(self): + self.items = [] + self.enable = True + def onConfigChange(self, sender=None, emit=True): + if emit: + self.emit('config-change') + for item in self.items: + if item.enable and item is not sender: + item.onConfigChange(emit=False) + def onDateChange(self, sender=None, emit=True): + if emit: + self.emit('date-change') + for item in self.items: + if item.enable and item is not sender: + item.onDateChange(emit=False) + def __getitem__(self, key): + for item in self.items: + if item._name == key: + return item + def connectItem(self, item): + item.connect('config-change', self.onConfigChange) + item.connect('date-change', self.onDateChange) + #def insertItem(self, index, item): + # self.items.insert(index, item) + # self.connectItem(item) + def appendItem(self, item): + self.items.append(item) + self.connectItem(item) + def replaceItem(self, itemIndex, item): + self.items[itemIndex] = item + self.connectItem(item) + def moveItemUp(self, i): + self.items.insert(i-1, self.items.pop(i)) + def addItemWidget(self, i): + pass + def showHide(self): + try: + func = self.show if self.enable else self.hide + except AttributeError: + try: + self.set_visible(self.enable) + except AttributeError: + pass + else: + func() + for item in self.items: + item.showHide() + + +class IntegatedWindowList(BaseCalObj): + _name = 'windowList' + desc = 'Window List' + def __init__(self): + Object.__init__(self) + self.initVars() + def onConfigChange(self, *a, **ka): + ui.cellCache.clear() + settings.set_property( + 'gtk-font-name', + pfontEncode(ui.getFont()).to_string(), + ) + #### + BaseCalObj.onConfigChange(self, *a, **ka) + self.onDateChange() + +#################################################### + +windowList = IntegatedWindowList() + +########### + +if rtl: + gtk.Widget.set_default_direction(gtk.TextDirection.RTL) + +gtk.Window.set_default_icon_from_file(ui.logo) + +settings = gtk.Settings.get_default() + +## ui.timeout_initial = settings.get_property('gtk-timeout-initial') ## == 200 FIXME +## ui.timeout_repeat = settings.get_property('gtk-timeout-repeat') ## == 20 too small!! FIXME + + +ui.initFonts(gfontDecode(settings.get_property('gtk-font-name'))) +ui.fontDefaultInit = ui.fontDefault + +########### +textDirDict = { + 'ltr': gtk.TextDirection.LTR, + 'rtl': gtk.TextDirection.RTL, + 'auto': gtk.TextDirection.NONE, +} + +iconSizeList = [ + ('Menu', gtk.IconSize.MENU), + ('Small Toolbar', gtk.IconSize.SMALL_TOOLBAR), + ('Button', gtk.IconSize.BUTTON), + ('Large Toolbar', gtk.IconSize.LARGE_TOOLBAR), + ('DND', gtk.IconSize.DND), + ('Dialog', gtk.IconSize.DIALOG), +] ## in size order +iconSizeDict = dict(iconSizeList) + +############################## + +#if ui.fontCustomEnable:## FIXME +# settings.set_property('gtk-font-name', fontCustom) + + +dateFormat = '%Y/%m/%d' +clockFormat = '%X' ## '%T', '%X' (local), '%T', '%m:%d' + +dateFormatBin = None +clockFormatBin = None + +def updateFormatsBin(): + global dateFormatBin, clockFormatBin + dateFormatBin = compileTmFormat(dateFormat) + clockFormatBin = compileTmFormat(clockFormat) + +############################## + +def setDefault_adjustTimeCmd(): + global adjustTimeCmd + for cmd in ('gksudo', 'kdesudo', 'gksu', 'gnomesu', 'kdesu'): + if os.path.isfile('/usr/bin/%s'%cmd): + adjustTimeCmd = [ + cmd, + join(rootDir, 'scripts', 'run'), + 'scal3/ui_gtk/adjust_dtime.py' + ] + break + +## user should be able to configure this in Preferences +adjustTimeCmd = '' +setDefault_adjustTimeCmd() + +############################## + +mainToolbarData = { + 'items': [], + 'iconSize': 'Large Toolbar', + 'style': 'Icon', + 'buttonsBorder': 0, +} + +wcalToolbarData = { + 'items': [ + ('mainMenu', True), + ('backward4', False), + ('backward', True), + ('today', True), + ('forward', True), + ('forward4', False), + ], + 'iconSize': 'Button', + 'style': 'Icon', + 'buttonsBorder': 0, +} + + + +########################################################### + +try: + wcalToolbarData = ui.ud__wcalToolbarData ## loaded from jsom +except AttributeError: + pass + +try: + mainToolbarData = ui.ud__mainToolbarData ## loaded from jsom +except AttributeError: + pass + + +loadConf() + +setDefault_adjustTimeCmd()## FIXME + +############################################################ + +rootWindow = gdk.get_default_root_window() ## Good Place????? +##import atexit +##atexit.register(rootWindow.set_cursor, gdk.Cursor.new(gdk.CursorType.LEFT_PTR)) ## ????????????????????? +#rootWindow.set_cursor(cursor=gdk.Cursor.new(gdk.CursorType.WATCH)) ## ??????????????????? +screenW = rootWindow.get_width() +screenH = rootWindow.get_height() + + + diff --git a/scal3/ui_gtk/hijri.py b/scal3/ui_gtk/hijri.py new file mode 100644 index 000000000..73104d85f --- /dev/null +++ b/scal3/ui_gtk/hijri.py @@ -0,0 +1,259 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +## Islamic (Hijri) calendar: http://en.wikipedia.org/wiki/Islamic_calendar + +import os +from os.path import isfile + +from scal3.cal_types import calTypes, jd_to, to_jd +from scal3.cal_types.hijri import monthDb, monthName +from scal3.date_utils import monthPlus +from scal3 import core +from scal3.locale_man import rtl, dateLocale +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.date import DateButton +from scal3.ui_gtk.utils import dialog_add_button, toolButtonFromStock, set_tooltip +from scal3.ui_gtk import gtk_ud as ud + +hijriMode = calTypes.names.index('hijri') + +def getCurrentYm(): + y, m, d = ui.todayCell.dates[hijriMode] + return y*12 + m-1 + +class EditDbDialog(gtk.Dialog): + def __init__(self, **kwargs): + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Tune Hijri Monthes')) + self.connect('delete-event', self.onDeleteEvent) + ############ + self.altMode = 0 + self.altModeDesc = 'Gregorian' + ############ + hbox = gtk.HBox() + self.topLabel = gtk.Label() + pack(hbox, self.topLabel) + self.startDateInput = DateButton() + self.startDateInput.set_editable(False)## FIXME + self.startDateInput.connect('changed', lambda widget: self.updateEndDates()) + pack(hbox, self.startDateInput) + pack(self.vbox, hbox) + ############################ + treev = gtk.TreeView() + trees = gtk.ListStore(int, str, str, int, str)## ym, yearShown, monthShown, monthLenCombo, endDateShown + treev.set_model(trees) + #treev.get_selection().connect('changed', self.plugTreevCursorChanged) + #treev.connect('row-activated', self.plugTreevRActivate) + #treev.connect('button-press-event', self.plugTreevButtonPress) + ### + swin = gtk.ScrolledWindow() + swin.add(treev) + swin.set_policy(gtk.PolicyType.AUTOMATIC, gtk.PolicyType.AUTOMATIC) + ###### + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('Year'), cell, text=1) + treev.append_column(col) + ###### + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('Month'), cell, text=2) + treev.append_column(col) + ###### + cell = gtk.CellRendererCombo() + mLenModel = gtk.ListStore(int) + mLenModel.append([29]) + mLenModel.append([30]) + cell.set_property('model', mLenModel) + #cell.set_property('has-entry', False) + cell.set_property('editable', True) + cell.set_property('text-column', 0) + cell.connect('edited', self.monthLenCellEdited) + col = gtk.TreeViewColumn(_('Month Length'), cell, text=3) + treev.append_column(col) + ###### + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('End Date'), cell, text=4) + treev.append_column(col) + ###### + toolbar = gtk.Toolbar() + toolbar.set_orientation(gtk.Orientation.VERTICAL) + size = gtk.IconSize.SMALL_TOOLBAR + ### + tb = toolButtonFromStock(gtk.STOCK_ADD, size) + set_tooltip(tb, _('Add')) + tb.connect('clicked', self.addClicked) + toolbar.insert(tb, -1) + ### + tb = toolButtonFromStock(gtk.STOCK_DELETE, size) + set_tooltip(tb, _('Delete')) + tb.connect('clicked', self.delClicked) + toolbar.insert(tb, -1) + ###### + self.treev = treev + self.trees = trees + ##### + mainHbox = gtk.HBox() + pack(mainHbox, swin, 1, 1) + pack(mainHbox, toolbar) + pack(self.vbox, mainHbox, 1, 1) + ###### + dialog_add_button(self, gtk.STOCK_OK, _('_OK'), gtk.ResponseType.OK) + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), gtk.ResponseType.CANCEL) + ## + resetB = self.add_button(gtk.STOCK_UNDO, gtk.ResponseType.NONE) + resetB.set_label(_('_Reset to Defaults')) + resetB.set_image(gtk.Image.new_from_stock(gtk.STOCK_UNDO, gtk.IconSize.BUTTON)) + resetB.connect('clicked', self.resetToDefaults) + ## + self.connect('response', self.onResponse) + #print(dir(self.get_action_area())) + #self.get_action_area().set_homogeneous(False) + ###### + self.vbox.show_all() + def resetToDefaults(self, widget): + if isfile(monthDb.userDbPath): + os.remove(monthDb.userDbPath) + monthDb.load() + self.updateWidget() + return True + def addClicked(self, obj=None): + last = self.trees[-1] + ## 0 ym + ## 1 yearLocale + ## 2 monthLocale + ## 3 mLen + ## 4 endDate = '' + ym = last[0] + 1 + mLen = 59 - last[3] + year, month0 = divmod(ym, 12) + self.trees.append(( + ym, + _(year), + _(monthName[month0]), + mLen, + '', + )) + self.updateEndDates() + self.selectLastRow() + def selectLastRow(self): + lastPath = (len(self.trees)-1,) + self.treev.scroll_to_cell(lastPath) + self.treev.set_cursor(lastPath) + def delClicked(self, obj=None): + if len(self.trees) > 1: + del self.trees[-1] + self.selectLastRow() + def updateWidget(self): + #for index, module in calTypes.iterIndexModule(): + # if module.name != 'hijri': + for mode in calTypes.active: + modeDesc = calTypes[mode].desc + if not 'hijri' in modeDesc.lower(): + self.altMode = mode + self.altModeDesc = modeDesc + break + self.topLabel.set_label(_('Start')+': '+dateLocale(*monthDb.startDate)+' '+_('Equals to')+' %s'%_(self.altModeDesc)) + self.startDateInput.set_value(jd_to(monthDb.startJd, self.altMode)) + ########### + selectYm = getCurrentYm() - 1 ## previous month + selectIndex = None + self.trees.clear() + for index, ym, mLen in monthDb.getMonthLenList(): + if ym == selectYm: + selectIndex = index + year, month0 = divmod(ym, 12) + self.trees.append([ + ym, + _(year), + _(monthName[month0]), + mLen, + '', + ]) + self.updateEndDates() + ######## + if selectIndex is not None: + self.treev.scroll_to_cell(str(selectIndex)) + self.treev.set_cursor(str(selectIndex)) + def updateEndDates(self): + y, m, d = self.startDateInput.get_value() + jd0 = to_jd(y, m, d, self.altMode) - 1 + for row in self.trees: + mLen = row[3] + jd0 += mLen + row[4] = dateLocale(*jd_to(jd0, self.altMode)) + def monthLenCellEdited(self, combo, path_string, new_text): + editIndex = int(path_string) + mLen = int(new_text) + if not mLen in (29, 30): + return + mLenPrev = self.trees[editIndex][3] + delta = mLen - mLenPrev + if delta == 0: + return + n = len(self.trees) + self.trees[editIndex][3] = mLen + if delta==1: + for i in range(editIndex+1, n): + if self.trees[i][3] == 30: + self.trees[i][3] = 29 + break + elif delta==-1: + for i in range(editIndex+1, n): + if self.trees[i][3] == 29: + self.trees[i][3] = 30 + break + self.updateEndDates() + def updateVars(self): + y, m, d = self.startDateInput.get_value() + monthDb.endJd = monthDb.startJd = to_jd(y, m, d, self.altMode) + monthDb.monthLenByYm = {} + for row in self.trees: + ym = row[0] + mLen = row[3] + monthDb.monthLenByYm[ym] = mLen + monthDb.endJd += mLen + monthDb.save() + def run(self): + monthDb.load() + self.updateWidget() + self.treev.grab_focus() + gtk.Dialog.run(self) + def onResponse(self, dialog, response_id): + if response_id==gtk.ResponseType.OK: + self.updateVars() + self.destroy() + elif response_id==gtk.ResponseType.CANCEL: + self.destroy() + return True + def onDeleteEvent(self, dialog, gevent): + self.destroy() + return True + +def tuneHijriMonthes(widget=None): + dialog = EditDbDialog(parent=ui.prefDialog) + dialog.resize(400, 400) + dialog.run() + +if __name__=='__main__': + tuneHijriMonthes() + + diff --git a/scal3/ui_gtk/import_config_2to3.py b/scal3/ui_gtk/import_config_2to3.py new file mode 100755 index 000000000..e017b35e7 --- /dev/null +++ b/scal3/ui_gtk/import_config_2to3.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +## +## Copyright (C) Saeed Rasooli +## +## 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. Or on Debian systems, from /usr/share/common-licenses/GPL +## If not, see . + +APP_DESC = 'StarCalendar' + +import os, shutil +from os.path import dirname +from os.path import join, isfile, isdir + +from scal3.path import * +from scal3.import_config_2to3 import * +from scal3.locale_man import langDict, langDefault + +from scal3.ui_gtk import * + +langConfDir = join(rootDir, 'conf', 'defaults') + +gtk.Window.set_default_icon_from_file(join(pixDir, 'starcal.png')) + +langNameList = [] +langCodeList = [] + + +win = gtk.Dialog( + title=APP_DESC+' 3.x - First Run', + buttons=( + gtk.STOCK_OK, + gtk.ResponseType.OK, + gtk.STOCK_CANCEL, + gtk.ResponseType.CANCEL, + ) +) +langHbox = gtk.HBox() +pack(langHbox, gtk.Label('Select Language:')) + + +importCheckb = None +oldVersion = getOldVersion() +if oldVersion:## and '2.2.0' <= oldVersion < '2.5.0':## FIXME + importCheckb = gtk.CheckButton('Import configurations from %s %s'%(APP_DESC, oldVersion)) + importCheckb.connect('clicked', lambda cb: langHbox.set_sensitive(not cb.get_active())) + importCheckb.set_active(True) + pack(win.vbox, importCheckb) + + +langCombo = gtk.ComboBoxText() + +for langObj in langDict.values(): + langNameList.append(langObj.name) + langCodeList.append(langObj.code) + langCombo.append_text(langObj.name) + + +if langDefault and (langDefault in langCodeList): + langCombo.set_active(langCodeList.index(langDefault)) +else: + langCombo.set_active(0) + +pack(langHbox, langCombo, 1, 1) +pack(win.vbox, langHbox) + +pbarHbox = gtk.HBox() +pbar = gtk.ProgressBar() +pack(pbarHbox, pbar, 1, 1) +pack(win.vbox, pbarHbox) + + +win.vbox.show_all() + +if win.run()==gtk.ResponseType.OK: + #print('RESPONSE OK') + if importCheckb and importCheckb.get_active(): + importCheckb.set_sensitive(False) + langHbox.set_sensitive(False) + win.get_action_area().set_sensitive(False) + for frac in importConfigIter(): + pbar.set_fraction(frac) + percent = frac * 100 + text = '%.1f%%'%percent ## FIXME + pbar.set_text(text) + while gtk.events_pending(): + gtk.main_iteration_do(False) + else: + i = langCombo.get_active() + langCode = langCodeList[i] + thisLangConfDir = join(langConfDir, langCode) + #print('Setting language', langCode) + if not os.path.isdir(confDir): + os.mkdir(confDir, 0o755) + if os.path.isdir(thisLangConfDir): + for fname in os.listdir(thisLangConfDir): + src_path = join(thisLangConfDir, fname) + if not isfile(src_path): + continue + dst_path = join(confDir, fname) + #print(src_path) + shutil.copy(src_path, dst_path) + else: + open(join(confDir, 'locale.json'), 'w').write( + dataToPrettyJson({ + 'lang': langCode, + }) + ) + +win.destroy() + +if not os.path.isdir(confDir): + os.mkdir(confDir, 0o755) + + diff --git a/scal3/ui_gtk/listener.py b/scal3/ui_gtk/listener.py new file mode 100644 index 000000000..04410330d --- /dev/null +++ b/scal3/ui_gtk/listener.py @@ -0,0 +1,49 @@ +import time +from time import localtime +from time import time as now + +from scal3.time_utils import getUtcOffsetCurrent +from scal3 import core +from scal3 import ui + +from gi.repository.GObject import timeout_add, timeout_add_seconds + +from scal3.ui_gtk import gtk + +dayLen = 24*3600 + +class DateChangeListener: + def __init__(self, timeout=1): + self.timeout = timeout ## seconds + self.receivers = [] + self.gdate = localtime()[:3] + self.check() + def add(self, receiver): + self.receivers.append(receiver) + def check(self): + tm = now() + gdate = localtime(tm)[:3] + if gdate!=self.gdate: + self.gdate = gdate + ui.todayCell = ui.cellCache.getTodayCell() + for obj in self.receivers: + obj.onCurrentDateChange(gdate) + #timeout_add_seconds(int(dayLen-(tm+getUtcOffsetCurrent())%dayLen)+1, self.check) + timeout_add_seconds(self.timeout, self.check) + if ui.mainWin: + ui.mainWin.statusIconUpdateTooltip() + +#class TimeChangeListener: + +dateChange = DateChangeListener() +#timeChange = TimeChangeListener() + +if __name__=='__main__': + from gi.repository import GLib as glib + class TestRec: + def onCurrentDateChange(self, date): + print('current date changed to %s/%s/%s'%date) + dateChange.add(TestRec()) + glib.MainLoop().run() + + diff --git a/scal3/ui_gtk/mainwin_items/__init__.py b/scal3/ui_gtk/mainwin_items/__init__.py new file mode 100644 index 000000000..a04b5a6b8 --- /dev/null +++ b/scal3/ui_gtk/mainwin_items/__init__.py @@ -0,0 +1,15 @@ +from scal3 import core +from scal3.locale_man import tr as _ + +mainWinItemsDesc = { + 'eventDayView': _('Events of Day'), + 'labelBox': _('Year & Month Labels'), + 'monthCal': _('Month Calendar'), + 'pluginsText': _('Plugins Text'), + 'seasonPBar': _('Season Progress Bar'), + 'statusBar': _('Status Bar'), + 'toolbar': _('Toolbar'), + 'weekCal': _('Week Calendar'), + 'winContronller': _('Window Controller'), +} + diff --git a/scal3/ui_gtk/mainwin_items/dayCal.py b/scal3/ui_gtk/mainwin_items/dayCal.py new file mode 100644 index 000000000..f34d36199 --- /dev/null +++ b/scal3/ui_gtk/mainwin_items/dayCal.py @@ -0,0 +1,317 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from time import localtime +from time import time as now + +import sys, os +from math import sqrt + +from scal3.utils import myRaise +from scal3.cal_types import calTypes +from scal3 import core +from scal3.core import log +from scal3.locale_man import rtl, rtlSgn +from scal3.locale_man import tr as _ +from scal3 import ui +from scal3.monthcal import getCurrentMonthStatus + +from gi.repository import GdkPixbuf + +from scal3.ui_gtk import * +from scal3.ui_gtk.drawing import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk import gtk_ud as ud +from scal3.ui_gtk.customize import CustomizableCalObj +from scal3.ui_gtk.cal_base import CalBase + +class DayCalTypeParamBox(gtk.HBox): + def __init__(self, cal, index, mode, params, sgroupLabel, sgroupFont): + from scal3.ui_gtk.mywidgets.multi_spin.float_num import FloatSpinButton + from scal3.ui_gtk.mywidgets import MyFontButton, MyColorButton + gtk.HBox.__init__(self) + self.cal = cal + self.index = index + self.mode = mode + ###### + label = gtk.Label(_(calTypes[mode].desc)+' ') + label.set_alignment(0, 0.5) + pack(self, label) + sgroupLabel.add_widget(label) + ### + pack(self, gtk.Label(''), 1, 1) + pack(self, gtk.Label(_('position'))) + ### + spin = FloatSpinButton(-999, 999, 1) + self.spinX = spin + pack(self, spin) + ### + spin = FloatSpinButton(-999, 999, 1) + self.spinY = spin + pack(self, spin) + #### + pack(self, gtk.Label(''), 1, 1) + ### + fontb = MyFontButton(cal) + self.fontb = fontb + pack(self, fontb) + sgroupFont.add_widget(fontb) + #### + colorb = MyColorButton() + self.colorb = colorb + pack(self, colorb) + #### + self.set(params) + #### + self.spinX.connect('changed', self.onChange) + self.spinY.connect('changed', self.onChange) + fontb.connect('font-set', self.onChange) + colorb.connect('color-set', self.onChange) + get = lambda self: { + 'pos': (self.spinX.get_value(), self.spinY.get_value()), + 'font': self.fontb.get_font_name(), + 'color': self.colorb.get_color() + } + def set(self, data): + self.spinX.set_value(data['pos'][0]) + self.spinY.set_value(data['pos'][1]) + self.fontb.set_font_name(data['font']) + self.colorb.set_color(data['color']) + def onChange(self, obj=None, event=None): + ui.dcalTypeParams[self.index] = self.get() + self.cal.queue_draw() + + + + + +@registerSignals +class CalObj(gtk.DrawingArea, CalBase): + _name = 'dayCal' + desc = _('Day Calendar') + myKeys = CalBase.myKeys + ( + 'up', 'down', + 'right', 'left', + 'page_up', + 'k', 'p', + 'page_down', + 'j', 'n', + #'end', + 'f10', 'm', + ) + def heightSpinChanged(self, spin): + v = spin.get_value() + self.set_property('height-request', v) + ui.dcalHeight = v + def updateTypeParamsWidget(self): + try: + vbox = self.typeParamsVbox + except AttributeError: + return + for child in vbox.get_children(): + child.destroy() + ### + n = len(calTypes.active) + while len(ui.dcalTypeParams) < n: + ui.dcalTypeParams.append({ + 'pos': (0, 0), + 'font': ui.getFont(3.0), + 'color': ui.textColor, + }) + sgroupLabel = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + sgroupFont = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + for i, mode in enumerate(calTypes.active): + #try: + params = ui.dcalTypeParams[i] + #except IndexError: + ## + hbox = DayCalTypeParamBox(self, i, mode, params, sgroupLabel, sgroupFont) + pack(vbox, hbox) + ### + vbox.show_all() + def __init__(self): + gtk.DrawingArea.__init__(self) + self.add_events(gdk.EventMask.ALL_EVENTS_MASK) + self.initCal() + self.set_property('height-request', ui.dcalHeight) + ###################### + #self.kTime = 0 + ###################### + self.connect('draw', self.drawAll) + self.connect('button-press-event', self.buttonPress) + #self.connect('screen-changed', self.screenChanged) + self.connect('scroll-event', self.scroll) + def optionsWidgetCreate(self): + from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton + from scal3.ui_gtk.pref_utils import CheckPrefItem, ColorPrefItem + if self.optionsWidget: + return + self.optionsWidget = gtk.VBox() + #### + hbox = gtk.HBox() + spin = IntSpinButton(1, 9999) + spin.set_value(ui.dcalHeight) + spin.connect('changed', self.heightSpinChanged) + pack(hbox, gtk.Label(_('Height'))) + pack(hbox, spin) + pack(self.optionsWidget, hbox) + ######## + frame = gtk.Frame() + frame.set_label(_('Calendars')) + self.typeParamsVbox = gtk.VBox() + frame.add(self.typeParamsVbox) + frame.show_all() + pack(self.optionsWidget, frame) + self.optionsWidget.show_all() + self.updateTypeParamsWidget()## FIXME + def drawAll(self, widget=None, cr=None, cursor=True): + #gevent = gtk.get_current_event() + w = self.get_allocation().width + h = self.get_allocation().height + if not cr: + cr = self.get_window().cairo_create() + #cr.set_line_width(0)#?????????????? + #cr.scale(0.5, 0.5) + cr.rectangle(0, 0, w, h) + fillColor(cr, ui.bgColor) + ##### + c = ui.cell + x0 = 0 + y0 = 0 + dx = w + dy = h + ######## + iconList = c.getDayEventIcons() + if iconList: + iconsN = len(iconList) + scaleFact = 3.0 / sqrt(iconsN) + fromRight = 0 + for index, icon in enumerate(iconList): + ## if len(iconList) > 1 ## FIXME + try: + pix = GdkPixbuf.Pixbuf.new_from_file(icon) + except: + myRaise(__file__) + continue + pix_w = pix.get_width() + pix_h = pix.get_height() + ## right buttom corner ????????????????????? + x1 = (x0 + dx)/scaleFact - fromRight - pix_w # right side + y1 = (y0 + dy/2.0)/scaleFact - pix_h/2.0 # middle + cr.scale(scaleFact, scaleFact) + gdk.cairo_set_source_pixbuf(cr, pix, x1, y1) + cr.rectangle(x1, y1, pix_w, pix_h) + cr.fill() + cr.scale(1.0/scaleFact, 1.0/scaleFact) + fromRight += pix_w + #### Drawing numbers inside every cell + #cr.rectangle( + # x0-dx/2.0+1, + # y0-self.dy/2.0+1, + # dx-1, + # dy-1, + #) + mode = calTypes.primary + params = ui.dcalTypeParams[0] + daynum = newTextLayout(self, _(c.dates[mode][2], mode), params['font']) + fontw, fonth = daynum.get_pixel_size() + if c.holiday: + setColor(cr, ui.holidayColor) + else: + setColor(cr, params['color']) + cr.move_to( + x0 + dx/2.0 - fontw/2.0 + params['pos'][0], + y0 + dy/2.0 - fonth/2.0 + params['pos'][1], + ) + show_layout(cr, daynum) + #### + for mode, params in ui.getActiveDayCalParams()[1:]: + daynum = newTextLayout(self, _(c.dates[mode][2], mode), params['font']) + fontw, fonth = daynum.get_pixel_size() + setColor(cr, params['color']) + cr.move_to( + x0 + dx/2.0 - fontw/2.0 + params['pos'][0], + y0 + dy/2.0 - fonth/2.0 + params['pos'][1], + ) + show_layout(cr, daynum) + def buttonPress(self, obj, gevent): + ## FIXME + pass + def jdPlus(self, p): + ui.jdPlus(p) + self.onDateChange() + def keyPress(self, arg, gevent): + print('keyPress') + if CalBase.keyPress(self, arg, gevent): + return True + kname = gdk.keyval_name(gevent.keyval).lower() + print('keyPress', kname) + #if kname.startswith('alt'): + # return True + ## How to disable Alt+Space of metacity ????????????????????? + if kname=='up': + self.jdPlus(-1) + elif kname=='down': + self.jdPlus(1) + elif kname=='right': + if rtl: + self.jdPlus(-1) + else: + self.jdPlus(1) + elif kname=='left': + if rtl: + self.jdPlus(1) + else: + self.jdPlus(-1) + elif kname in ('page_up', 'k', 'p'): + self.jdPlus(-1)## FIXME + elif kname in ('page_down', 'j', 'n'): + self.jdPlus(1)## FIXME + #elif kname in ('f10', 'm'):## FIXME + # if gevent.get_state() & gdk.ModifierType.SHIFT_MASK: + # # Simulate right click (key beside Right-Ctrl) + # self.emit('popup-cell-menu', gevent.time, *self.getCellPos()) + # else: + # self.emit('popup-main-menu', gevent.time, *self.getMainMenuPos()) + else: + return False + return True + def scroll(self, widget, gevent): + d = getScrollValue(gevent) + if d=='up': + self.jdPlus(-1) + elif d=='down': + self.jdPlus(1) + else: + return False + def onDateChange(self, *a, **kw): + CustomizableCalObj.onDateChange(self, *a, **kw) + self.queue_draw() + def onConfigChange(self, *a, **kw): + CustomizableCalObj.onConfigChange(self, *a, **kw) + self.updateTypeParamsWidget() + + + + + + + + + diff --git a/scal3/ui_gtk/mainwin_items/eventDayView.py b/scal3/ui_gtk/mainwin_items/eventDayView.py new file mode 100644 index 000000000..a0242d5aa --- /dev/null +++ b/scal3/ui_gtk/mainwin_items/eventDayView.py @@ -0,0 +1,35 @@ +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * + +from scal3.ui_gtk.customize import CustomizableCalObj +from scal3.ui_gtk.event.occurrence_view import DayOccurrenceView + +#@registerSignals +class CalObj(DayOccurrenceView, CustomizableCalObj):## FIXME + def __init__(self): + DayOccurrenceView.__init__(self) + self.maxHeight = ui.eventViewMaxHeight + def optionsWidgetCreate(self): + from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton + if self.optionsWidget: + return + self.optionsWidget = gtk.HBox() + ### + hbox = gtk.HBox() + spin = IntSpinButton(1, 9999) + spin.set_value(ui.eventViewMaxHeight) + spin.connect('changed', self.heightSpinChanged) + pack(hbox, gtk.Label(_('Maximum Height'))) + pack(hbox, spin) + pack(self.optionsWidget, hbox) + ### + self.optionsWidget.show_all() + def heightSpinChanged(self, spin): + v = spin.get_value() + self.maxHeight = ui.eventViewMaxHeight = v + self.queue_resize() + diff --git a/scal3/ui_gtk/mainwin_items/labelBox.py b/scal3/ui_gtk/mainwin_items/labelBox.py new file mode 100644 index 000000000..c21f1e7eb --- /dev/null +++ b/scal3/ui_gtk/mainwin_items/labelBox.py @@ -0,0 +1,482 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from time import time as now + +from scal3.cal_types import calTypes +from scal3 import core +from scal3.locale_man import getMonthName, rtl +from scal3.locale_man import tr as _ +from scal3 import ui + +from gi.repository import GObject as gobject + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.utils import set_tooltip, setClipboard, get_menu_width +from scal3.ui_gtk.drawing import newTextLayout, setColor +from scal3.ui_gtk.mywidgets.button import ConButton +from scal3.ui_gtk import gtk_ud as ud +from scal3.ui_gtk.customize import CustomizableCalObj + +class BaseLabel(gtk.EventBox): + highlightColor = (176, 176, 176) + def __init__(self): + gtk.EventBox.__init__(self) + ########## + #self.menu.connect('map', lambda obj: self.drag_highlight()) + #self.menu.connect('unmap', lambda obj: self.drag_unhighlight()) + ######### + #self.connect('enter-notify-event', self.highlight) + #self.connect('leave-notify-event', self.unhighlight)## FIXME + def highlight(self, widget=None, event=None): + #self.drag_highlight() + if self.get_window()==None: + return + cr = self.get_window().cairo_create() + setColor(cr, self.highlightColor) + #print(tuple(self.get_allocation()), tuple(self.label.get_allocation())) + w = self.get_allocation().width + h = self.get_allocation().height + cr.rectangle(0, 0, w, 1) + cr.fill() + cr.rectangle(0, h-1, w, 1) + cr.fill() + cr.rectangle(0, 0, 1, h) + cr.fill() + cr.rectangle(w-1, 0, 1, h) + cr.fill() + cr.clip() + def unhighlight(self, widget=None, event=None): + #self.drag_unhighlight() + if self.get_window()==None: + return + w = self.get_allocation().width + h = self.get_allocation().height + self.get_window().clear_area(0, 0, w, 1) + self.get_window().clear_area(0, h-1, w, 1) + self.get_window().clear_area(0, 0, 1, h) + self.get_window().clear_area(w-1, 0, 1, h) + + +@registerSignals +class MonthLabel(BaseLabel, ud.BaseCalObj): + getItemStr = lambda self, i: _(i+1, fillZero=2) + getActiveStr = lambda self, s: '%s'%(ui.menuActiveLabelColor, s) + #getActiveStr = lambda self, s: '%s'%s + def __init__(self, mode, active=0): + BaseLabel.__init__(self) + #self.set_border_width(1)#??????????? + self.initVars() + self.mode = mode + self.label = gtk.Label() + self.label.set_use_markup(True) + self.add(self.label) + self.menu = gtk.Menu() + self.menu.set_border_width(0) + self.menuLabels = [] + self.connect('button-press-event', self.buttonPress) + self.active = active + self.setActive(active) + def createMenuLabels(self): + if self.menuLabels: + return + for i in range(12): + if ui.monthRMenuNum: + text = '%s: %s'%(self.getItemStr(i), _(getMonthName(self.mode, i+1))) + else: + text = _(getMonthName(self.mode, i+1)) + if i==self.active: + text = self.getActiveStr(text) + item = MenuItem() + label = item.get_child() + label.set_label(text) + #label.set_justify(gtk.Justification.LEFT) + label.set_alignment(0, 0.5) + label.set_use_markup(True) + item.set_right_justified(True) ##????????? + item.connect('activate', self.itemActivate, i) + self.menu.append(item) + self.menuLabels.append(label) + self.menu.show_all() + def setActive(self, active): + ## (Performance) update menu here, or make menu entirly before popup ???????????????? + s = getMonthName(self.mode, active+1) + s2 = getMonthName(self.mode, self.active+1) + if self.menuLabels: + if ui.monthRMenuNum: + self.menuLabels[self.active].set_label( + '%s: %s'%( + self.getItemStr(self.active), + s2, + ) + ) + self.menuLabels[active].set_label(self.getActiveStr('%s: %s'%(self.getItemStr(active), s))) + else: + self.menuLabels[self.active].set_label(s2) + self.menuLabels[active].set_label(self.getActiveStr(s)) + if ui.boldYmLabel: + self.label.set_label('%s'%s) + else: + self.label.set_label(s) + self.active = active + def changeMode(self, mode): + self.mode = mode + if ui.boldYmLabel: + self.label.set_label('%s'%getMonthName(self.mode, self.active+1)) + else: + self.label.set_label(getMonthName(self.mode, self.active+1)) + for i in range(12): + if ui.monthRMenuNum: + s = '%s: %s'%(self.getItemStr(i), getMonthName(self.mode, i+1)) + else: + s = getMonthName(self.mode, i+1) + if i==self.active: + s = self.getActiveStr(s) + self.menuLabels[i].set_label(s) + def itemActivate(self, item, index): + y, m, d = ui.cell.dates[self.mode] + m = index + 1 + ui.changeDate(y, m, d, self.mode) + self.onDateChange() + def buttonPress(self, widget, gevent): + if gevent.button==3: + self.createMenuLabels() + foo, x, y = self.get_window().get_origin() + ## foo == 1 FIXME + y += self.get_allocation().height + #if rtl: + # x = x - get_menu_width(self.menu) + self.get_allocation().width + #x -= 7 ## ????????? because of menu padding + ## align menu to center: + x -= int((get_menu_width(self.menu) - self.get_allocation().width)//2) + self.menu.popup(None, None, lambda widget, menu: (x, y, True), None, gevent.button, gevent.time) + ui.updateFocusTime() + return True + else: + return False + def onDateChange(self, *a, **ka): + ud.BaseCalObj.onDateChange(self, *a, **ka) + self.setActive(ui.cell.dates[self.mode][1]-1) + + + +@registerSignals +class IntLabel(BaseLabel): + #getActiveStr = lambda self, s: '%s'%s + getActiveStr = lambda self, s: '%s'%(ui.menuActiveLabelColor, s) + signals = [ + ('changed', [int]), + ] + def __init__(self, height=9, active=0): + BaseLabel.__init__(self) + #self.set_border_width(1)#??????????? + self.height = height + #self.delay = delay + if ui.boldYmLabel: + s = '%s'%_(active) + else: + s = _(active) + self.label = gtk.Label(s) + self.label.set_use_markup(True) + self.add(self.label) + self.menu = None + self.connect('button-press-event', self.buttonPress) + self.active = active + self.setActive(active) + self.start = 0 + self.remain = 0 + self.ymPressTime = 0 + self.etime = 0 + self.step = 0 + def setActive(self, active): + if ui.boldYmLabel: + self.label.set_label('%s'%_(active)) + else: + self.label.set_label(_(active)) + self.active = active + def createMenu(self): + if self.menu: + return + self.menu = gtk.Menu() + self.menuLabels = [] + self.menu.connect('scroll-event', self.menuScroll) + ########## + item = gtk.MenuItem() + arrow = gtk.Arrow(gtk.ArrowType.UP, gtk.ShadowType.IN) + item.add(arrow) + arrow.set_property('height-request', 10) + #item.set_border_width(0) + #item.set_property('height-request', 10) + #print(item.style_get_property('horizontal-padding') ## OK) + ###??????????????????????????????????? + #item.config('horizontal-padding'=0) + #style = item.get_style() + #style.set_property('horizontal-padding', 0) + #item.set_style(style) + self.menu.append(item) + item.connect('select', self.arrowSelect, -1) + item.connect('deselect', self.arrowDeselect) + item.connect('activate', lambda wid: False) + ########## + for i in range(self.height): + item = MenuItem() + label = item.get_child() + label.set_use_markup(True) + item.connect('activate', self.itemActivate, i) + self.menu.append(item) + self.menuLabels.append(label) + ########## + item = gtk.MenuItem() + arrow = gtk.Arrow(gtk.ArrowType.DOWN, gtk.ShadowType.IN) + arrow.set_property('height-request', 10) + item.add(arrow) + self.menu.append(item) + item.connect('select', self.arrowSelect, 1) + item.connect('deselect', self.arrowDeselect) + ########## + self.menu.show_all() + def updateMenu(self, start=None): + self.createMenu() + if start==None: + start = self.active - self.height//2 + self.start = start + for i in range(self.height): + if start+i==self.active: + self.menuLabels[i].set_label(self.getActiveStr(_(start+i))) + else: + self.menuLabels[i].set_label(_(start+i)) + def itemActivate(self, widget, item): + self.setActive(self.start+item) + self.emit('changed', self.start+item) + def buttonPress(self, widget, gevent): + if gevent.button==3: + self.updateMenu() + foo, x, y = self.get_window().get_origin() + y += self.get_allocation().height + x -= 7 ## ????????? because of menu padding + ## align menu to center: + x -= int((get_menu_width(self.menu) - self.get_allocation().width)//2) + self.menu.popup(None, None, lambda widget, menu: (x, y, True), None, gevent.button, gevent.time) + ui.updateFocusTime() + return True + else: + return False + def arrowSelect(self, item, plus): + self.remain = plus + gobject.timeout_add(int(ui.labelMenuDelay*1000), self.arrowRemain, plus) + def arrowDeselect(self, item): + self.remain = 0 + def arrowRemain(self, plus): + t = now() + #print(t-self.etime) + if self.remain==plus: + if t-self.etime1: + self.step = 0 + return False + else: + self.step += 1 + self.etime = t #?????????? + return True + else: + self.updateMenu(self.start+plus) + self.etime = t + return True + else: + return False + def menuScroll(self, widget, gevent): + d = getScrollValue(gevent) + if d=='up': + self.updateMenu(self.start-1) + elif d=='down': + self.updateMenu(self.start+1) + else: + return False + + +@registerSignals +class YearLabel(IntLabel, ud.BaseCalObj): + signals = ud.BaseCalObj.signals + def __init__(self, mode, **kwargs): + IntLabel.__init__(self, **kwargs) + self.initVars() + self.mode = mode + self.connect('changed', self.onChanged) + def onChanged(self, label, item): + mode = self.mode + y, m, d = ui.cell.dates[mode] + ui.changeDate(item, m, d, mode) + self.onDateChange() + def changeMode(self, mode): + self.mode = mode + #self.onDateChange() + def onDateChange(self, *a, **ka): + ud.BaseCalObj.onDateChange(self, *a, **ka) + self.setActive(ui.cell.dates[self.mode][0]) + + +def newSmallNoFocusButton(stock, func, tooltip=''): + arrow = ConButton() + arrow.set_relief(2) + arrow.set_can_focus(False) + arrow.set_image(gtk.Image.new_from_stock(stock, gtk.IconSize.SMALL_TOOLBAR)) + arrow.connect('con-clicked', func) + if tooltip: + set_tooltip(arrow, tooltip) + return arrow + +class YearLabelButtonBox(gtk.HBox, ud.BaseCalObj): + def __init__(self, mode, **kwargs): + gtk.HBox.__init__(self) + self.initVars() + ### + pack(self, + newSmallNoFocusButton(gtk.STOCK_REMOVE, self.prevClicked, _('Previous Year')), + 0, + 0, + 0, + ) + ### + self.label = YearLabel(mode, **kwargs) + pack(self, self.label) + ### + pack(self, + newSmallNoFocusButton(gtk.STOCK_ADD, self.nextClicked, _('Next Year')), + 0, + 0, + 0, + ) + def prevClicked(self, button): + ui.yearPlus(-1) + self.label.onDateChange() + def nextClicked(self, button): + ui.yearPlus(1) + self.label.onDateChange() + changeMode = lambda self, mode: self.label.changeMode(mode) + +class MonthLabelButtonBox(gtk.HBox, ud.BaseCalObj): + def __init__(self, mode, **kwargs): + gtk.HBox.__init__(self) + self.initVars() + ### + pack(self, + newSmallNoFocusButton(gtk.STOCK_REMOVE, self.prevClicked, _('Previous Month')), + 0, + 0, + 0, + ) + ### + self.label = MonthLabel(mode, **kwargs) + pack(self, self.label) + ### + pack(self, + newSmallNoFocusButton(gtk.STOCK_ADD, self.nextClicked, _('Next Month')), + 0, + 0, + 0, + ) + def prevClicked(self, button): + ui.monthPlus(-1) + self.label.onDateChange() + def nextClicked(self, button): + ui.monthPlus(1) + self.label.onDateChange() + changeMode = lambda self, mode: self.label.changeMode(mode) + + +@registerSignals +class CalObj(gtk.HBox, CustomizableCalObj): + _name = 'labelBox' + desc = _('Year & Month Labels') + def __init__(self): + gtk.HBox.__init__(self) + self.initVars() + #self.set_border_width(2) + def onConfigChange(self, *a, **kw): + CustomizableCalObj.onConfigChange(self, *a, **kw) + ##### + for child in self.get_children(): + child.destroy() + ### + monthLabels = [] + mode = calTypes.primary + ## + box = YearLabelButtonBox(mode) + pack(self, box) + self.appendItem(box.label) + ## + pack(self, gtk.VSeparator(), 1, 1) + ## + box = MonthLabelButtonBox(mode) + pack(self, box) + self.appendItem(box.label) + monthLabels.append(box.label) + #### + for i, mode in list(enumerate(calTypes.active))[1:]: + pack(self, gtk.VSeparator(), 1, 1) + label = YearLabel(mode) + pack(self, label) + self.appendItem(label) + ############### + label = gtk.Label('') + label.set_property('width-request', 5) + pack(self, label) + ############### + label = MonthLabel(mode) + pack(self, label) + monthLabels.append(label) + self.appendItem(label) + #### + ## updateTextWidth + lay = newTextLayout(self) + for label in monthLabels: + wm = 0 + for m in range(12): + name = getMonthName(label.mode, m) + if ui.boldYmLabel: + lay.set_markup('%s'%name) + else: + lay.set_text(name) ## OR lay.set_markup + w = lay.get_pixel_size()[0] + if w > wm: + wm = w + label.set_property('width-request', wm) + ##### + self.show_all() + ##### + self.onDateChange() + + +if __name__=='__main__': + win = gtk.Dialog(parent=None) + box = CalObj() + win.add_events( + gdk.EventMask.POINTER_MOTION_MASK | gdk.EventMask.FOCUS_CHANGE_MASK | gdk.EventMask.BUTTON_MOTION_MASK | + gdk.EventMask.BUTTON_PRESS_MASK | gdk.EventMask.BUTTON_RELEASE_MASK | gdk.EventMask.SCROLL_MASK | + gdk.EventMask.KEY_PRESS_MASK | gdk.EventMask.VISIBILITY_NOTIFY_MASK | gdk.EventMask.EXPOSURE_MASK + ) + pack(win.vbox, box, 1, 1) + win.vbox.show_all() + win.resize(600, 50) + win.set_title(box.desc) + box.onConfigChange() + win.run() + + diff --git a/scal3/ui_gtk/mainwin_items/monthCal.py b/scal3/ui_gtk/mainwin_items/monthCal.py new file mode 100644 index 000000000..afd28ec2f --- /dev/null +++ b/scal3/ui_gtk/mainwin_items/monthCal.py @@ -0,0 +1,586 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from time import localtime +from time import time as now + +import sys, os +from math import sqrt + +from scal3.utils import myRaise +from scal3.cal_types import calTypes +from scal3 import core +from scal3.core import log +from scal3.locale_man import rtl, rtlSgn +from scal3.locale_man import tr as _ +from scal3 import ui +from scal3.monthcal import getCurrentMonthStatus + +from gi.repository import GdkPixbuf + +from scal3.ui_gtk import * +from scal3.ui_gtk.drawing import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk import gtk_ud as ud +from scal3.ui_gtk.customize import CustomizableCalObj +from scal3.ui_gtk.cal_base import CalBase +#from scal3.ui_gtk import desktop +#from scal3.ui_gtk import wallpaper + + +class MonthCalTypeParamBox(gtk.HBox): + def getCellPagePlus(self, cell, plus): + return ui.getMonthPlus(cell, plus) + def __init__(self, cal, index, mode, params, sgroupLabel, sgroupFont): + from scal3.ui_gtk.mywidgets.multi_spin.float_num import FloatSpinButton + from scal3.ui_gtk.mywidgets import MyFontButton, MyColorButton + gtk.HBox.__init__(self) + self.cal = cal + self.index = index + self.mode = mode + ###### + label = gtk.Label(_(calTypes[mode].desc)+' ') + label.set_alignment(0, 0.5) + pack(self, label) + sgroupLabel.add_widget(label) + ### + pack(self, gtk.Label(''), 1, 1) + pack(self, gtk.Label(_('position'))) + ### + spin = FloatSpinButton(-99, 99, 1) + self.spinX = spin + pack(self, spin) + ### + spin = FloatSpinButton(-99, 99, 1) + self.spinY = spin + pack(self, spin) + #### + pack(self, gtk.Label(''), 1, 1) + ### + fontb = MyFontButton(cal) + self.fontb = fontb + pack(self, fontb) + sgroupFont.add_widget(fontb) + #### + colorb = MyColorButton() + self.colorb = colorb + pack(self, colorb) + #### + self.set(params) + #### + self.spinX.connect('changed', self.onChange) + self.spinY.connect('changed', self.onChange) + fontb.connect('font-set', self.onChange) + colorb.connect('color-set', self.onChange) + get = lambda self: { + 'pos': (self.spinX.get_value(), self.spinY.get_value()), + 'font': self.fontb.get_font_name(), + 'color': self.colorb.get_color() + } + def set(self, data): + self.spinX.set_value(data['pos'][0]) + self.spinY.set_value(data['pos'][1]) + self.fontb.set_font_name(data['font']) + self.colorb.set_color(data['color']) + def onChange(self, obj=None, event=None): + ui.mcalTypeParams[self.index] = self.get() + self.cal.queue_draw() + +@registerSignals +class CalObj(gtk.DrawingArea, CalBase): + _name = 'monthCal' + desc = _('Month Calendar') + cx = [0, 0, 0, 0, 0, 0, 0] + myKeys = CalBase.myKeys + ( + 'up', 'down', + 'right', 'left', + 'page_up', + 'k', 'p', + 'page_down', + 'j', 'n', + 'end', + 'f10', 'm', + ) + def heightSpinChanged(self, spin): + v = spin.get_value() + self.set_property('height-request', v) + ui.mcalHeight = v + def leftMarginSpinChanged(self, spin): + ui.mcalLeftMargin = spin.get_value() + self.queue_draw() + def topMarginSpinChanged(self, spin): + ui.mcalTopMargin = spin.get_value() + self.queue_draw() + def updateTypeParamsWidget(self): + try: + vbox = self.typeParamsVbox + except AttributeError: + return + for child in vbox.get_children(): + child.destroy() + ### + n = len(calTypes.active) + while len(ui.mcalTypeParams) < n: + ui.mcalTypeParams.append({ + 'pos': (0, 0), + 'font': ui.getFont(0.6), + 'color': ui.textColor, + }) + sgroupLabel = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + sgroupFont = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + for i, mode in enumerate(calTypes.active): + #try: + params = ui.mcalTypeParams[i] + #except IndexError: + ## + hbox = MonthCalTypeParamBox(self, i, mode, params, sgroupLabel, sgroupFont) + pack(vbox, hbox) + ### + vbox.show_all() + def __init__(self): + gtk.DrawingArea.__init__(self) + self.add_events(gdk.EventMask.ALL_EVENTS_MASK) + self.initCal() + self.set_property('height-request', ui.mcalHeight) + ###################### + #self.kTime = 0 + ###################### + self.connect('draw', self.drawAll) + self.connect('button-press-event', self.buttonPress) + #self.connect('screen-changed', self.screenChanged) + self.connect('scroll-event', self.scroll) + ###################### + #self.updateTextWidth() + def optionsWidgetCreate(self): + from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton + from scal3.ui_gtk.pref_utils import CheckPrefItem, ColorPrefItem + if self.optionsWidget: + return + self.optionsWidget = gtk.VBox() + #### + hbox = gtk.HBox() + spin = IntSpinButton(1, 9999) + spin.set_value(ui.mcalHeight) + spin.connect('changed', self.heightSpinChanged) + pack(hbox, gtk.Label(_('Height'))) + pack(hbox, spin) + pack(self.optionsWidget, hbox) + #### + hbox = gtk.HBox(spacing=3) + ## + pack(hbox, gtk.Label(_('Left Margin'))) + spin = IntSpinButton(0, 99) + spin.set_value(ui.mcalLeftMargin) + spin.connect('changed', self.leftMarginSpinChanged) + pack(hbox, spin) + ## + pack(hbox, gtk.Label(_('Top'))) + spin = IntSpinButton(0, 99) + spin.set_value(ui.mcalTopMargin) + spin.connect('changed', self.topMarginSpinChanged) + pack(hbox, spin) + ## + pack(hbox, gtk.Label(''), 1, 1) + pack(self.optionsWidget, hbox) + ######## + hbox = gtk.HBox(spacing=3) + #### + item = CheckPrefItem(ui, 'mcalGrid', _('Grid')) + item.updateWidget() + gridCheck = item._widget + pack(hbox, gridCheck) + gridCheck.item = item + #### + colorItem = ColorPrefItem(ui, 'mcalGridColor', True) + colorItem.updateWidget() + pack(hbox, colorItem._widget) + gridCheck.colorb = colorItem._widget + gridCheck.connect('clicked', self.gridCheckClicked) + colorItem._widget.item = colorItem + colorItem._widget.connect('color-set', self.gridColorChanged) + colorItem._widget.set_sensitive(ui.mcalGrid) + #### + pack(self.optionsWidget, hbox) + ######## + frame = gtk.Frame() + frame.set_label(_('Calendars')) + self.typeParamsVbox = gtk.VBox() + frame.add(self.typeParamsVbox) + frame.show_all() + pack(self.optionsWidget, frame) + self.optionsWidget.show_all() + self.updateTypeParamsWidget()## FIXME + def drawAll(self, widget=None, cr=None, cursor=True): + #gevent = gtk.get_current_event() + #?????? Must enhance (only draw few cells, not all cells) + self.calcCoord() + w = self.get_allocation().width + h = self.get_allocation().height + if not cr: + cr = self.get_window().cairo_create() + #cr.set_line_width(0)#?????????????? + #cr.scale(0.5, 0.5) + wx = ui.winX + wy = ui.winY + if ui.bgUseDesk:## FIXME + ### ????????????????? Need for mainWin !!!!! + coord = self.translate_coordinates(self, wx, wy) + if len(coord)==2: + from scal3.ui_gtk import desktop + x0, y0 = coord + try: + bg = desktop.get_wallpaper(x0, y0, w, h) + except: + print('Could not get wallpaper!') + myRaise(__file__) + #os.popen('gnome-settings-daemon') + ui.bgUseDesk = False ##?????????????????? + #if ui.prefDialog + # ui.prefDialog.checkDeskBg.set_active(False)##?????????????????? + else: + gdk.cairo_set_source_pixbuf(cr, bg, 0, 0, 0) + cr.paint() + #else: + # print(coord) + cr.rectangle(0, 0, w, h) + fillColor(cr, ui.bgColor) + status = getCurrentMonthStatus() + #################################### Drawing Border + if ui.mcalTopMargin>0: + ##### Drawing border top background + ##menuBgColor == borderColor ##??????????????? + cr.rectangle(0, 0, w, ui.mcalTopMargin) + fillColor(cr, ui.borderColor) + ######## Drawing weekDays names + setColor(cr, ui.borderTextColor) + dx = 0 + wdayAb = (self.wdaysWidth > w) + for i in range(7): + wday = newTextLayout(self, core.getWeekDayAuto(i, wdayAb)) + try: + fontw, fonth = wday.get_pixel_size() + except: + myRaise(__file__) + fontw, fonth = wday.get_pixel_size() + cr.move_to( + self.cx[i]-fontw/2.0, + (ui.mcalTopMargin-fonth)/2.0-1, + ) + show_layout(cr, wday) + ######## Drawing "Menu" label + setColor(cr, ui.menuTextColor) + text = newTextLayout(self, _('Menu')) + fontw, fonth = text.get_pixel_size() + if rtl: + cr.move_to( + w-(ui.mcalLeftMargin+fontw)/2.0 - 3, + (ui.mcalTopMargin-fonth)/2.0 - 1, + ) + else: + cr.move_to( + (ui.mcalLeftMargin-fontw)/2.0, + (ui.mcalTopMargin-fonth)/2.0 - 1, + ) + show_layout(cr, text) + if ui.mcalLeftMargin>0: + ##### Drawing border left background + if rtl: + cr.rectangle( + w - ui.mcalLeftMargin, + ui.mcalTopMargin, + ui.mcalLeftMargin, + h - ui.mcalTopMargin, + ) + else: + cr.rectangle( + 0, + ui.mcalTopMargin, + ui.mcalLeftMargin, + h - ui.mcalTopMargin, + ) + fillColor(cr, ui.borderColor) + ##### Drawing week numbers + setColor(cr, ui.borderTextColor) + for i in range(6): + lay = newTextLayout(self, _(status.weekNum[i])) + fontw, fonth = lay.get_pixel_size() + if rtl: + cr.move_to( + w - (ui.mcalLeftMargin+fontw)/2.0, + self.cy[i]-fonth/2.0 + 2, + ) + else: + cr.move_to( + (ui.mcalLeftMargin-fontw)/2.0, + self.cy[i]-fonth/2.0 + 2, + ) + show_layout(cr, lay) + selectedCellPos = ui.cell.monthPos + if ui.todayCell.inSameMonth(ui.cell): + tx, ty = ui.todayCell.monthPos ## today x and y + x0 = self.cx[tx] - self.dx/2.0 + y0 = self.cy[ty] - self.dy/2.0 + cr.rectangle(x0, y0, self.dx, self.dy) + fillColor(cr, ui.todayCellColor) + for yPos in range(6): + for xPos in range(7): + c = status[yPos][xPos] + x0 = self.cx[xPos] + y0 = self.cy[yPos] + cellInactive = (c.month != ui.cell.month) + cellHasCursor = (cursor and (xPos, yPos) == selectedCellPos) + if cellHasCursor: + ##### Drawing Cursor + cx0 = x0 - self.dx/2.0 + 1 + cy0 = y0 - self.dy/2.0 + 1 + cw = self.dx - 1 + ch = self.dy - 1 + ######### Circular Rounded + drawCursorBg(cr, cx0, cy0, cw, ch) + fillColor(cr, ui.cursorBgColor) + ######## end of Drawing Cursor + if not cellInactive: + iconList = c.getMonthEventIcons() + if iconList: + iconsN = len(iconList) + scaleFact = 1.0 / sqrt(iconsN) + fromRight = 0 + for index, icon in enumerate(iconList): + ## if len(iconList) > 1 ## FIXME + try: + pix = GdkPixbuf.Pixbuf.new_from_file(icon) + except: + myRaise(__file__) + continue + pix_w = pix.get_width() + pix_h = pix.get_height() + ## right buttom corner ????????????????????? + x1 = (x0 + self.dx/2.0)/scaleFact - fromRight - pix_w # right side + y1 = (y0 + self.dy/2.0)/scaleFact - pix_h # buttom side + cr.scale(scaleFact, scaleFact) + gdk.cairo_set_source_pixbuf(cr, pix, x1, y1) + cr.rectangle(x1, y1, pix_w, pix_h) + cr.fill() + cr.scale(1.0/scaleFact, 1.0/scaleFact) + fromRight += pix_w + #### Drawing numbers inside every cell + #cr.rectangle( + # x0-self.dx/2.0+1, + # y0-self.dy/2.0+1, + # self.dx-1, + # self.dy-1, + #) + mode = calTypes.primary + params = ui.mcalTypeParams[0] + daynum = newTextLayout(self, _(c.dates[mode][2], mode), params['font']) + fontw, fonth = daynum.get_pixel_size() + if cellInactive: + setColor(cr, ui.inactiveColor) + elif c.holiday: + setColor(cr, ui.holidayColor) + else: + setColor(cr, params['color']) + cr.move_to( + x0 - fontw/2.0 + params['pos'][0], + y0 - fonth/2.0 + params['pos'][1], + ) + show_layout(cr, daynum) + if not cellInactive: + for mode, params in ui.getActiveMonthCalParams()[1:]: + daynum = newTextLayout(self, _(c.dates[mode][2], mode), params['font']) + fontw, fonth = daynum.get_pixel_size() + setColor(cr, params['color']) + cr.move_to( + x0 - fontw/2.0 + params['pos'][0], + y0 - fonth/2.0 + params['pos'][1], + ) + show_layout(cr, daynum) + if cellHasCursor: + ##### Drawing Cursor Outline + cx0 = x0-self.dx/2.0+1 + cy0 = y0-self.dy/2.0+1 + cw = self.dx-1 + ch = self.dy-1 + ######### Circular Rounded + drawCursorOutline(cr, cx0, cy0, cw, ch) + fillColor(cr, ui.cursorOutColor) + ##### end of Drawing Cursor Outline + ################ end of drawing cells + ##### drawGrid + if ui.mcalGrid: + setColor(cr, ui.mcalGridColor) + for i in range(7): + cr.rectangle(self.cx[i]+rtlSgn()*self.dx/2.0, 0, 1, h) + cr.fill() + for i in range(6): + cr.rectangle(0, self.cy[i]-self.dy/2.0, w, 1) + cr.fill() + return False + def updateTextWidth(self): + ### update width of week days names to understand that should be synopsis or no + lay = newTextLayout(self) + wm = 0 ## max width + for i in range(7): + lay.set_markup(core.weekDayName[i]) + w = lay.get_pixel_size()[0] ## ???????? + #w = lay.get_pixel_extents()[0] ## ???????? + #print(w,) + if w > wm: + wm = w + self.wdaysWidth = wm*7 + ui.mcalLeftMargin ## ???????? + #self.wdaysWidth = wm*7*0.7 + ui.mcalLeftMargin ## ???????? + #print('max =', wm, ' wdaysWidth =', self.wdaysWidth) + def buttonPress(self, obj, gevent): + ## self.winActivate() #????????? + b = gevent.button + x, y, = self.get_pointer() + # foo, x, y, flags = self.get_window().get_pointer() + self.pointer = (x, y) + if b==2: + return False + xPos = -1 + yPos = -1 + for i in range(7): + if abs(x-self.cx[i]) <= self.dx/2.0: + xPos = i + break + for i in range(6): + if abs(y-self.cy[i]) <= self.dy/2.0: + yPos = i + break + status = getCurrentMonthStatus() + if yPos == -1 or xPos == -1: + self.emit('popup-main-menu', gevent.time, gevent.x, gevent.y) + elif yPos >= 0 and xPos >= 0: + cell = status[yPos][xPos] + self.changeDate(*cell.dates[calTypes.primary]) + if gevent.type==TWO_BUTTON_PRESS: + self.emit('2button-press') + if b == 3 and cell.month == ui.cell.month:## right click on a normal cell + #self.emit('popup-cell-menu', gevent.time, *self.getCellPos()) + self.emit('popup-cell-menu', gevent.time, gevent.x, gevent.y) + return True + def calcCoord(self):## calculates coordidates (x and y of cells centers) + w = self.get_allocation().width + h = self.get_allocation().height + if rtl: + self.cx = [ + (w - ui.mcalLeftMargin) * (13.0 - 2*i) / 14.0 + for i in range(7) + ] ## centers x + else: + self.cx = [ + ui.mcalLeftMargin + (w-ui.mcalLeftMargin) * (1.0 + 2*i) / 14.0 + for i in range(7) + ] ## centers x + self.cy = [ + ui.mcalTopMargin + (h - ui.mcalTopMargin) * (1.0 + 2*i) / 12.0 + for i in range(6) + ] ## centers y + self.dx = (w - ui.mcalLeftMargin) / 7.0 ## delta x + self.dy = (h - ui.mcalTopMargin) / 6.0 ## delta y + def monthPlus(self, p): + ui.monthPlus(p) + self.onDateChange() + def keyPress(self, arg, gevent): + print('keyPress') + if CalBase.keyPress(self, arg, gevent): + return True + kname = gdk.keyval_name(gevent.keyval).lower() + print('keyPress', kname) + #if kname.startswith('alt'): + # return True + ## How to disable Alt+Space of metacity ????????????????????? + if kname=='up': + self.jdPlus(-7) + elif kname=='down': + self.jdPlus(7) + elif kname=='right': + if rtl: + self.jdPlus(-1) + else: + self.jdPlus(1) + elif kname=='left': + if rtl: + self.jdPlus(1) + else: + self.jdPlus(-1) + elif kname=='end': + self.changeDate( + ui.cell.year, + ui.cell.month, + core.getMonthLen(ui.cell.year, ui.cell.month, calTypes.primary), + ) + elif kname in ('page_up', 'k', 'p'): + self.monthPlus(-1) + elif kname in ('page_down', 'j', 'n'): + self.monthPlus(1) + elif kname in ('f10', 'm'): + if gevent.get_state() & gdk.ModifierType.SHIFT_MASK: + # Simulate right click (key beside Right-Ctrl) + self.emit('popup-cell-menu', gevent.time, *self.getCellPos()) + else: + self.emit('popup-main-menu', gevent.time, *self.getMainMenuPos()) + else: + return False + return True + def scroll(self, widget, gevent): + d = getScrollValue(gevent) + if d=='up': + self.jdPlus(-7) + elif d=='down': + self.jdPlus(7) + else: + return False + getCellPos = lambda self: ( + int(self.cx[ui.cell.monthPos[0]]), + int(self.cy[ui.cell.monthPos[1]] + self.dy/2.0), + ) + def getMainMenuPos(self):## FIXME + if rtl: + return ( + int(self.get_allocation().width - ui.mcalLeftMargin/2.0), + int(ui.mcalTopMargin/2.0), + ) + else: + return ( + int(ui.mcalLeftMargin/2.0), + int(ui.mcalTopMargin/2.0), + ) + def onDateChange(self, *a, **kw): + CustomizableCalObj.onDateChange(self, *a, **kw) + self.queue_draw() + def onConfigChange(self, *a, **kw): + CustomizableCalObj.onConfigChange(self, *a, **kw) + self.updateTextWidth() + self.updateTypeParamsWidget() + + + + +if __name__=='__main__': + win = gtk.Dialog(parent=None) + cal = CalObj() + win.add_events(gdk.EventMask.ALL_EVENTS_MASK) + pack(win.vbox, cal, 1, 1) + win.vbox.show_all() + win.resize(600, 400) + win.set_title(cal.desc) + win.run() + diff --git a/scal3/ui_gtk/mainwin_items/pluginsText.py b/scal3/ui_gtk/mainwin_items/pluginsText.py new file mode 100644 index 000000000..22792d062 --- /dev/null +++ b/scal3/ui_gtk/mainwin_items/pluginsText.py @@ -0,0 +1,73 @@ +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.customize import CustomizableCalObj + +@registerSignals +class CalObj(gtk.VBox, CustomizableCalObj): + _name = 'pluginsText' + desc = _('Plugins Text') + def __init__(self): + from scal3.ui_gtk.mywidgets.text_widgets import ReadOnlyTextView + gtk.VBox.__init__(self) + self.initVars() + #### + self.textview = ReadOnlyTextView() + self.textview.set_wrap_mode(gtk.WrapMode.WORD) + self.textview.set_justification(gtk.Justification.CENTER) + self.textbuff = self.textview.get_buffer() + ## + self.expander = gtk.Expander() + self.expander.connect('activate', self.expanderExpanded) + if ui.pluginsTextInsideExpander: + self.expander.add(self.textview) + pack(self, self.expander) + self.expander.set_expanded(ui.pluginsTextIsExpanded) + self.textview.show() + else: + pack(self, self.textview) + def optionsWidgetCreate(self): + if self.optionsWidget: + return + self.optionsWidget = gtk.HBox() + self.enableExpanderCheckb = gtk.CheckButton(_('Inside Expander')) + self.enableExpanderCheckb.set_active(ui.pluginsTextInsideExpander) + self.enableExpanderCheckb.connect('clicked', lambda check: self.setEnableExpander(check.get_active())) + self.setEnableExpander(ui.pluginsTextInsideExpander) + pack(self.optionsWidget, self.enableExpanderCheckb) + #### + self.optionsWidget.show_all() + def expanderExpanded(self, exp): + ui.pluginsTextIsExpanded = not exp.get_expanded() + ui.saveLiveConf() + getWidget = lambda self: self.expander if ui.pluginsTextInsideExpander else self.textview + def setText(self, text): + if text: + self.textbuff.set_text(text) + self.getWidget().show() + else:## elif self.get_property('visible') + self.textbuff.set_text('')## forethought + self.getWidget().hide() + def setEnableExpander(self, enable): + #print('setEnableExpander', enable) + if enable: + if not ui.pluginsTextInsideExpander: + self.remove(self.textview) + self.expander.add(self.textview) + pack(self, self.expander) + self.expander.show_all() + else: + if ui.pluginsTextInsideExpander: + self.expander.remove(self.textview) + self.remove(self.expander) + pack(self, self.textview) + self.textview.show() + ui.pluginsTextInsideExpander = enable + self.onDateChange() + def onDateChange(self, *a, **kw): + CustomizableCalObj.onDateChange(self, *a, **kw) + self.setText(ui.cell.pluginsText) + diff --git a/scal3/ui_gtk/mainwin_items/seasonPBar.py b/scal3/ui_gtk/mainwin_items/seasonPBar.py new file mode 100644 index 000000000..a871ad9b6 --- /dev/null +++ b/scal3/ui_gtk/mainwin_items/seasonPBar.py @@ -0,0 +1,35 @@ +from scal3 import core +from scal3.locale_man import tr as _ +from scal3.locale_man import rtl, textNumEncode +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.customize import CustomizableCalObj + +@registerSignals +class CalObj(gtk.ProgressBar, CustomizableCalObj): + _name = 'seasonPBar' + desc = _('Season Progress Bar') + def __init__(self): + gtk.ProgressBar.__init__(self) + self.initVars() + def onDateChange(self, *a, **kw): + from scal3.season import getSeasonNamePercentFromJd + CustomizableCalObj.onDateChange(self, *a, **kw) + name, frac = getSeasonNamePercentFromJd(ui.cell.jd) + if rtl: + percent = '%d%%'%(frac*100) + else: + percent = '%%%d'%(frac*100) + self.set_text( + _(name) + + ' - ' + + textNumEncode( + percent, + changeDot=True, + ) + ) + self.set_fraction(frac) + + diff --git a/scal3/ui_gtk/mainwin_items/statusBar.py b/scal3/ui_gtk/mainwin_items/statusBar.py new file mode 100644 index 000000000..4c5306aea --- /dev/null +++ b/scal3/ui_gtk/mainwin_items/statusBar.py @@ -0,0 +1,48 @@ +from scal3.cal_types import calTypes +from scal3 import core +from scal3.locale_man import tr as _ +from scal3.locale_man import rtl +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.datelabel import DateLabel +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk import gtk_ud as ud +from scal3.ui_gtk.customize import CustomizableCalObj + +@registerSignals +class CalObj(gtk.HBox, CustomizableCalObj): + _name = 'statusBar' + desc = _('Status Bar') + def __init__(self): + from scal3.ui_gtk.mywidgets.resize_button import ResizeButton + gtk.HBox.__init__(self) + self.initVars() + self.labelBox = gtk.HBox() + pack(self, self.labelBox, 1, 1) + resizeB = ResizeButton(ui.mainWin) + pack(self, resizeB, 0, 0) + if rtl: + self.set_direction(gtk.TextDirection.LTR) + self.labelBox.set_direction(gtk.TextDirection.LTR) + def onConfigChange(self, *a, **kw): + CustomizableCalObj.onConfigChange(self, *a, **kw) + ### + for label in self.labelBox.get_children(): + label.destroy() + ### + for mode in calTypes.active: + label = DateLabel(None) + label.mode = mode + pack(self.labelBox, label, 1) + self.show_all() + ### + self.onDateChange() + def onDateChange(self, *a, **kw): + CustomizableCalObj.onDateChange(self, *a, **kw) + for i, label in enumerate(self.labelBox.get_children()): + text = ui.cell.format(ud.dateFormatBin, label.mode) + if i==0: + text = '%s'%text + label.set_label(text) + diff --git a/scal3/ui_gtk/mainwin_items/toolbar.py b/scal3/ui_gtk/mainwin_items/toolbar.py new file mode 100644 index 000000000..883a0fac8 --- /dev/null +++ b/scal3/ui_gtk/mainwin_items/toolbar.py @@ -0,0 +1,34 @@ +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk import gtk_ud as ud +from scal3.ui_gtk.toolbar import ToolbarItem, CustomizableToolbar + +@registerSignals +class CalObj(CustomizableToolbar): + defaultItems = [ + ToolbarItem('today', 'home', 'goToday', 'Select Today', 'Today'), + ToolbarItem('date', 'index', 'selectDateShow', 'Select Date...', 'Date'), + ToolbarItem('customize', 'edit', 'customizeShow'), + ToolbarItem('preferences', 'preferences', 'prefShow'), + ToolbarItem('add', 'add', 'eventManShow', 'Event Manager', 'Event'), + ToolbarItem('export', 'convert', 'exportClicked', _('Export to %s')%'HTML', 'Export'), + ToolbarItem('about', 'about', 'aboutShow', _('About ')+core.APP_DESC, 'About'), + ToolbarItem('quit', 'quit', 'quit'), + ] + defaultItemsDict = dict([(item._name, item) for item in defaultItems]) + def __init__(self): + CustomizableToolbar.__init__(self, ui.mainWin, vertical=False) + if not ud.mainToolbarData['items']: + ud.mainToolbarData['items'] = [(item._name, True) for item in self.defaultItems] + self.setData(ud.mainToolbarData) + if ui.mainWin: + self.connect('button-press-event', ui.mainWin.childButtonPress) + def updateVars(self): + CustomizableToolbar.updateVars(self) + ud.mainToolbarData = self.getData() + + diff --git a/scal3/ui_gtk/mainwin_items/weekCal.py b/scal3/ui_gtk/mainwin_items/weekCal.py new file mode 100644 index 000000000..dedd02020 --- /dev/null +++ b/scal3/ui_gtk/mainwin_items/weekCal.py @@ -0,0 +1,976 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import time +from time import time as now + +import sys, os + +from scal3.utils import myRaise +from scal3 import core +from scal3.locale_man import tr as _ +from scal3.locale_man import rtl + +from scal3.cal_types import calTypes + +from scal3 import core +from scal3 import ui + +from gi.repository import GdkPixbuf + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.drawing import * +from scal3.ui_gtk.mywidgets.font_family_combo import FontFamilyCombo + +from scal3.ui_gtk import gtk_ud as ud + +from scal3.ui_gtk.cal_base import CalBase +from scal3.ui_gtk.customize import CustomizableCalObj, CustomizableCalBox +from scal3.ui_gtk.toolbar import ToolbarItem, CustomizableToolbar + +def show_event(widget, gevent): + print(type(widget), gevent.type.value_name)#, gevent.get_value()#, gevent.send_event + + +class ColumnBase(CustomizableCalObj): + customizeWidth = False + customizeFont = False + autoButtonPressHandler = True + ## + getWidthAttr = lambda self: 'wcal_%s_width'%self._name + getWidthValue = lambda self: getattr(ui, self.getWidthAttr(), None) + setWidthValue = lambda self, value: setattr(ui, self.getWidthAttr(), value) + setWidthWidget = lambda self, value: self.set_property('width-request', value) + ## + getFontAttr = lambda self: 'wcalFont_%s'%self._name + getFontValue = lambda self: getattr(ui, self.getFontAttr(), None) + def onConfigChange(self, *a, **kw): + CustomizableCalObj.onConfigChange(self, *a, **kw) + if self.customizeWidth: + self.setWidthWidget(self.getWidthValue()) + def widthSpinChanged(self, spin): + if self._name: + value = spin.get_value() + self.setWidthValue(value) + self.setWidthWidget(value) + def fontFamilyComboChanged(self, combo): + if self._name: + setattr(ui, self.getFontAttr(), combo.get_value()) + self.onDateChange() + def optionsWidgetCreate(self): + from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton + if self.optionsWidget: + return + self.optionsWidget = gtk.VBox() + #### + if self.customizeWidth: + value = self.getWidthValue() + ### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Width'))) + spin = IntSpinButton(0, 999) + pack(hbox, spin) + spin.set_value(value) + spin.connect('changed', self.widthSpinChanged) + pack(self.optionsWidget, hbox) + #### + if self.customizeFont: + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Font Family'))) + combo = FontFamilyCombo(hasAuto=True) + combo.set_value(self.getFontValue()) + pack(hbox, combo) + combo.connect('changed', self.fontFamilyComboChanged) + pack(self.optionsWidget, hbox) + #### + self.optionsWidget.show_all() + + + +class Column(gtk.DrawingArea, ColumnBase): + colorizeHolidayText = False + showCursor = False + truncateText = False + def __init__(self, wcal): + gtk.DrawingArea.__init__(self) + self.add_events(gdk.EventMask.ALL_EVENTS_MASK) + self.initVars() + #self.connect('button-press-event', self.buttonPress) + #self.connect('event', show_event) + self.wcal = wcal + def getContext(self): + return self.get_window().cairo_create() + def drawBg(self, cr): + w = self.get_allocation().width + h = self.get_allocation().height + cr.rectangle(0, 0, w, h) + fillColor(cr, ui.bgColor) + rowH = h/7.0 + for i in range(7): + c = self.wcal.status[i] + y0 = i*rowH + if c.jd == ui.todayCell.jd: + cr.rectangle(0, i*rowH, w, rowH) + fillColor(cr, ui.todayCellColor) + if self.showCursor and c.jd == ui.cell.jd: + drawCursorBg(cr, 0, y0, w, rowH) + fillColor(cr, ui.cursorBgColor) + if ui.wcalGrid: + w = self.get_allocation().width + h = self.get_allocation().height + setColor(cr, ui.wcalGridColor) + ### + cr.rectangle(w-1, 0, 1, h) + cr.fill() + ### + for i in range(1, 7): + cr.rectangle(0, i*rowH, w, 1) + cr.fill() + def drawCursorFg(self, cr): + w = self.get_allocation().width + h = self.get_allocation().height + rowH = h/7.0 + for i in range(7): + c = self.wcal.status[i] + y0 = i*rowH + if self.showCursor and c.jd == ui.cell.jd: + drawCursorOutline(cr, 0, y0, w, rowH) + fillColor(cr, ui.cursorOutColor) + def drawTextList(self, cr, textData, font=None): + w = self.get_allocation().width + h = self.get_allocation().height + ### + rowH = h/7.0 + itemW = w - ui.wcalPadding + if font is None: + fontName = self.getFontValue() + fontSize = ui.getFont()[-1] ## FIXME + font = [fontName, False, False, fontSize] if fontName else None + for i in range(7): + data = textData[i] + if data: + linesN = len(data) + lineH = rowH/linesN + lineI = 0 + if len(data[0]) < 2: + print(self._name) + for line, color in data: + layout = newTextLayout( + self, + text=line, + font=font, + maxSize=(itemW, lineH), + maximizeScale=ui.wcalTextSizeScale, + truncate=self.truncateText, + ) + if not layout: + continue + layoutW, layoutH = layout.get_pixel_size() + layoutX = (w-layoutW)/2.0 + layoutY = i*rowH + (lineI+0.5)*lineH - layoutH/2.0 + cr.move_to(layoutX, layoutY) + if self.colorizeHolidayText and self.wcal.status[i].holiday: + color = ui.holidayColor + if not color: + color = ui.textColor + setColor(cr, color) + show_layout(cr, layout) + lineI += 1 + def buttonPress(self, widget, gevent): + return False + def onDateChange(self, *a, **kw): + CustomizableCalObj.onDateChange(self, *a, **kw) + self.queue_draw() + + +class MainMenuToolbarItem(ToolbarItem): + def __init__(self): + ToolbarItem.__init__(self, 'mainMenu', None, '', _('Main Menu'), enableTooltip=False) + self.connect('clicked', self.onClicked) + self.updateImage() + def optionsWidgetCreate(self): + from scal3.ui_gtk.mywidgets.icon import IconSelectButton + if self.optionsWidget: + return + self.optionsWidget = gtk.VBox() + ### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Icon')+' ')) + self.iconSelect = IconSelectButton() + self.iconSelect.set_filename(ui.wcal_toolbar_mainMenu_icon) + self.iconSelect.connect('changed', self.onIconChanged) + pack(hbox, self.iconSelect) + pack(hbox, gtk.Label(''), 1, 1) + pack(self.optionsWidget, hbox) + self.optionsWidget.show_all() + def updateImage(self): + from scal3.ui_gtk.utils import imageFromFile + self.set_property('label-widget', imageFromFile(ui.wcal_toolbar_mainMenu_icon)) + self.show_all() + def getMenuPos(self): + wcal = self.get_parent().get_parent() + w = self.get_allocation().width + h = self.get_allocation().height + x0, y0 = self.translate_coordinates(wcal, 0, 0) + return ( + x0 if rtl else x0+w, + y0+h, + ) + def onClicked(self, widget=None): + x, y = self.getMenuPos() + self.get_parent().get_parent().emit( + 'popup-main-menu', + 0, + x, + y, + ) + def onIconChanged(self, widget, icon): + if not icon: + icon = ui.wcal_toolbar_mainMenu_icon_default + self.iconSelect.set_filename(icon) + ui.wcal_toolbar_mainMenu_icon = icon + self.updateImage() + + +class WeekNumToolbarItem(ToolbarItem): + def __init__(self): + ToolbarItem.__init__(self, 'weekNum', None, self.onClicked, ('Week Number')) + self.label = gtk.Label() + self.label.set_direction(gtk.TextDirection.LTR) + self.set_property('label-widget', self.label) + def updateLabel(self): + if ui.wcal_toolbar_weekNum_negative: + n = ui.cell.weekNumNeg + else: + n = ui.cell.weekNum + self.label.set_label(_(n)) + def onDateChange(self, *a, **ka): + ToolbarItem.onDateChange(self, *a, **ka) + self.updateLabel() + def onClicked(self, *a): + ui.wcal_toolbar_weekNum_negative = not ui.wcal_toolbar_weekNum_negative + self.updateLabel() + ui.saveLiveConf() + + +@registerSignals +class ToolbarColumn(CustomizableToolbar, ColumnBase): + autoButtonPressHandler = False + defaultItems = [ + MainMenuToolbarItem(), + WeekNumToolbarItem(), + ToolbarItem('backward4', 'goto_top', 'goBackward4', 'Backward 4 Weeks'), + ToolbarItem('backward', 'go_up', 'goBackward', 'Previous Week'), + ToolbarItem('today', 'home', 'goToday', 'Today'), + ToolbarItem('forward', 'go_down', 'goForward', 'Next Week'), + ToolbarItem('forward4', 'goto_bottom', 'goForward4', 'Forward 4 Weeks'), + ] + defaultItemsDict = dict([(item._name, item) for item in defaultItems]) + def __init__(self, wcal): + CustomizableToolbar.__init__(self, wcal, True, True) + if not ud.wcalToolbarData['items']: + ud.wcalToolbarData['items'] = [(item._name, True) for item in self.defaultItems] + self.setData(ud.wcalToolbarData) + def updateVars(self): + CustomizableToolbar.updateVars(self) + ud.wcalToolbarData = self.getData() + + +@registerSignals +class WeekDaysColumn(Column): + _name = 'weekDays' + desc = _('Week Days') + colorizeHolidayText = True + showCursor = True + customizeWidth = True + customizeFont = True + def __init__(self, wcal): + Column.__init__(self, wcal) + self.connect('draw', self.onExposeEvent) + def onExposeEvent(self, widget=None, event=None): + cr = self.getContext() + self.drawBg(cr) + self.drawTextList( + cr, + [ + [ + (core.getWeekDayN(i), ''), + ] + for i in range(7) + ], + ) + self.drawCursorFg(cr) + + +@registerSignals +class PluginsTextColumn(Column): + _name = 'pluginsText' + desc = _('Plugins Text') + expand = True + customizeFont = True + truncateText = False + def __init__(self, wcal): + Column.__init__(self, wcal) + self.connect('draw', self.onExposeEvent) + def onExposeEvent(self, widget=None, event=None): + cr = self.getContext() + self.drawBg(cr) + self.drawTextList( + cr, + [ + [ + (line, '') for line in self.wcal.status[i].pluginsText.split('\n') + ] + for i in range(7) + ] + ) + + +@registerSignals +class EventsIconColumn(Column): + _name = 'eventsIcon' + desc = _('Events Icon') + maxPixH = 26.0 + maxPixW = 26.0 + customizeWidth = True + def __init__(self, wcal): + Column.__init__(self, wcal) + self.connect('draw', self.onExposeEvent) + def onExposeEvent(self, widget=None, event=None): + cr = self.getContext() + self.drawBg(cr) + ### + w = self.get_allocation().width + h = self.get_allocation().height + ### + rowH = h/7.0 + itemW = w - ui.wcalPadding + for i in range(7): + c = self.wcal.status[i] + iconList = c.getWeekEventIcons() + if not iconList: + continue + n = len(iconList) + scaleFact = min( + 1.0, + h / self.maxPixH, + w / (n * self.maxPixW), + ) + x0 = (w/scaleFact - (n-1)*self.maxPixW) / 2.0 + y0 = (2*i + 1) * h / (14.0*scaleFact) + if rtl: + iconList.reverse()## FIXME + for iconIndex, icon in enumerate(iconList): + try: + pix = GdkPixbuf.Pixbuf.new_from_file(icon) + except: + myRaise(__file__) + continue + pix_w = pix.get_width() + pix_h = pix.get_height() + x1 = x0 + iconIndex*self.maxPixW - pix_w/2.0 + y1 = y0 - pix_h/2.0 + cr.scale(scaleFact, scaleFact) + gdk.cairo_set_source_pixbuf(cr, pix, x1, y1) + cr.rectangle(x1, y1, pix_w, pix_h) + cr.fill() + cr.scale(1.0/scaleFact, 1.0/scaleFact) + + +@registerSignals +class EventsCountColumn(Column): + _name = 'eventsCount' + desc = _('Events Count') + customizeWidth = True + def __init__(self, wcal): + Column.__init__(self, wcal) + self.expand = ui.wcal_eventsCount_expand + ## + self.connect('draw', self.onExposeEvent) + def optionsWidgetCreate(self): + if self.optionsWidget: + return + Column.optionsWidgetCreate(self) + ##### + hbox = gtk.HBox() + check = gtk.CheckButton(_('Expand')) + check.set_active(ui.wcal_eventsCount_expand) + check.connect('clicked', self.expandCheckClicked) + pack(hbox, check) + pack(hbox, gtk.Label(''), 1, 1) + pack(self.optionsWidget, hbox) + self.optionsWidget.show_all() + def expandCheckClicked(self, check): + active = check.get_active() + self.expand = ui.wcal_eventsCount_expand = active + self.wcal.set_child_packing(self, active, active, 0, gtk.PACK_START) + self.queue_draw() + def getDayTextData(self, i): + n = len(self.wcal.status[i].eventsData) + ## item['show'][1] FIXME + if n > 0: + line = _('%s events')%_(n) + else: + line = '' + return [ + (line, None), + ] + def onExposeEvent(self, widget=None, event=None): + cr = self.getContext() + self.drawBg(cr) + ### + w = self.get_allocation().width + h = self.get_allocation().height + ### + self.drawTextList( + cr, + [ + self.getDayTextData(i) + for i in range(7) + ], + ) + + +@registerSignals +class EventsTextColumn(Column): + _name = 'eventsText' + desc = _('Events Text') + expand = True + customizeFont = True + truncateText = False + def __init__(self, wcal): + Column.__init__(self, wcal) + self.connect('draw', self.onExposeEvent) + def optionsWidgetCreate(self): + if self.optionsWidget: + return + Column.optionsWidgetCreate(self) + ##### + hbox = gtk.HBox() + check = gtk.CheckButton(_('Show Description')) + check.set_active(ui.wcal_eventsText_showDesc) + pack(hbox, check) + pack(hbox, gtk.Label(''), 1, 1) + check.connect('clicked', self.descCheckClicked) + pack(self.optionsWidget, hbox) + ## + hbox = gtk.HBox() + check = gtk.CheckButton(_('Colorize')) + check.set_active(ui.wcal_eventsText_colorize) + pack(hbox, check) + pack(hbox, gtk.Label(''), 1, 1) + check.connect('clicked', self.colorizeCheckClicked) + pack(self.optionsWidget, hbox) + ## + self.optionsWidget.show_all() + def descCheckClicked(self, check): + ui.wcal_eventsText_showDesc = check.get_active() + self.queue_draw() + def colorizeCheckClicked(self, check): + ui.wcal_eventsText_colorize = check.get_active() + self.queue_draw() + def getDayTextData(self, i): + from scal3.xml_utils import escape + data = [] + for item in self.wcal.status[i].eventsData: + if not item['show'][1]: + continue + line = ''.join(item['text']) if ui.wcal_eventsText_showDesc else item['text'][0] + line = escape(line) + if item['time']: + line = item['time'] + ' ' + line + color = item['color'] if ui.wcal_eventsText_colorize else '' + data.append((line, color)) + return data + def onExposeEvent(self, widget=None, event=None): + cr = self.getContext() + self.drawBg(cr) + self.drawTextList( + cr, + [ + self.getDayTextData(i) + for i in range(7) + ], + ) + + +@registerSignals +class EventsBoxColumn(Column): + _name = 'eventsBox' + desc = _('Events Box') + expand = True ## FIXME + customizeFont = True + def __init__(self, wcal): + self.boxes = None + self.padding = 2 + self.timeWidth = 7*24*3600 + self.boxEditing = None + ##### + Column.__init__(self, wcal) + ##### + self.connect('realize', lambda w: self.updateData()) + self.connect('draw', self.onExposeEvent) + def updateData(self): + from scal3.time_utils import getEpochFromJd + from scal3.ui_gtk import timeline_box as tbox + self.timeStart = getEpochFromJd(self.wcal.status[0].jd) + self.pixelPerSec = float(self.get_allocation().height) / self.timeWidth ## pixel/second + self.borderTm = 0 ## tbox.boxMoveBorder / self.pixelPerSec ## second + self.boxes = tbox.calcEventBoxes( + self.timeStart, + self.timeStart + self.timeWidth, + self.pixelPerSec, + self.borderTm, + ) + def onDateChange(self, *a, **kw): + Column.onDateChange(self, *a, **kw) + self.updateData() + self.queue_draw() + def onConfigChange(self, *a, **kw): + Column.onConfigChange(self, *a, **kw) + self.updateData() + self.queue_draw() + def drawBox(self, cr, box): + from scal3.ui_gtk import timeline_box as tbox + ### + x = box.y + y = box.x + w = box.h + h = box.w + ### + tbox.drawBoxBG(cr, box, x, y, w, h) + tbox.drawBoxText(cr, box, x, y, w, h, self) + def onExposeEvent(self, widget=None, event=None): + cr = self.getContext() + self.drawBg(cr) + if not self.boxes: + return + ### + w = self.get_allocation().width + h = self.get_allocation().height + ### + for box in self.boxes: + box.setPixelValues( + self.timeStart, + self.pixelPerSec, + self.padding, + w - 2*self.padding, + ) + self.drawBox(cr, box) + + +class WcalTypeParamBox(gtk.HBox): + def __init__(self, wcal, index, mode, params, sgroupLabel, sgroupFont): + from scal3.ui_gtk.mywidgets import MyFontButton + gtk.HBox.__init__(self) + self.wcal = wcal + self.index = index + self.mode = mode + ###### + label = gtk.Label(_(calTypes[mode].desc)+' ') + label.set_alignment(0, 0.5) + pack(self, label) + sgroupLabel.add_widget(label) + ### + self.fontCheck = gtk.CheckButton(_('Font')) + pack(self, gtk.Label(''), 1, 1) + pack(self, self.fontCheck) + ### + self.fontb = MyFontButton(wcal) + pack(self, self.fontb) + sgroupFont.add_widget(self.fontb) + #### + self.set(params) + #### + self.fontCheck.connect('clicked', self.onChange) + self.fontb.connect('font-set', self.onChange) + get = lambda self: { + 'font': self.fontb.get_font_name() if self.fontCheck.get_active() else None, + } + def set(self, data): + font = data['font'] + self.fontCheck.set_active(bool(font)) + if not font: + font = ui.getFont() + self.fontb.set_font_name(font) + def onChange(self, obj=None, event=None): + ui.wcalTypeParams[self.index] = self.get() + self.wcal.queue_draw() + +@registerSignals +class DaysOfMonthColumn(Column): + colorizeHolidayText = True + showCursor = True + def __init__(self, wcal, cgroup, mode, index): + Column.__init__(self, wcal) + self.cgroup = cgroup + self.mode = mode + self.index = index + ### + self.connect('draw', self.onExposeEvent) + def onExposeEvent(self, widget=None, event=None): + cr = self.getContext() + self.drawBg(cr) + try: + font = ui.wcalTypeParams[self.index]['font'] + except: + font = None + self.drawTextList( + cr, + [ + [ + ( + _(self.wcal.status[i].dates[self.mode][2], self.mode), + '', + ) + ] + for i in range(7) + ], + font=font, + ) + self.drawCursorFg(cr) + +@registerSignals +class DaysOfMonthColumnGroup(gtk.HBox, CustomizableCalBox, ColumnBase): + _name = 'daysOfMonth' + desc = _('Days of Month') + customizeWidth = True + updateDir = lambda self: self.set_direction(ud.textDirDict[ui.wcal_daysOfMonth_dir]) + def __init__(self, wcal): + gtk.HBox.__init__(self) + self.initVars() + self.wcal = wcal + self.updateCols() + self.updateDir() + self.show() + def optionsWidgetCreate(self): + from scal3.ui_gtk.mywidgets.direction_combo import DirectionComboBox + if self.optionsWidget: + return + ColumnBase.optionsWidgetCreate(self) + ### + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Direction'))) + combo = DirectionComboBox() + pack(hbox, combo) + combo.setValue(ui.wcal_daysOfMonth_dir) + combo.connect('changed', self.dirComboChanged) + pack(self.optionsWidget, hbox) + #### + frame = gtk.Frame() + frame.set_label(_('Calendars')) + self.typeParamsVbox = gtk.VBox() + frame.add(self.typeParamsVbox) + frame.show_all() + pack(self.optionsWidget, frame) + self.updateTypeParamsWidget()## FIXME + #### + self.optionsWidget.show_all() + def setWidthWidget(self, value):## overwrites method from ColumnBase + for child in self.get_children(): + child.set_property('width-request', value) + def dirComboChanged(self, combo): + ui.wcal_daysOfMonth_dir = combo.getValue() + self.updateDir() + def updateCols(self): + #self.foreach(gtk.DrawingArea.destroy)## Couses crash tray icon in gnome3 + #self.foreach(lambda child: self.remove(child))## Couses crash tray icon in gnome3 + ######## + columns = self.get_children() + n = len(columns) + n2 = len(calTypes.active) + width = self.getWidthValue() + if n > n2: + for i in range(n2, n): + columns[i].destroy() + elif n < n2: + for i in range(n, n2): + col = DaysOfMonthColumn(self.wcal, self, 0, i) + pack(self, col) + columns.append(col) + for i, mode in enumerate(calTypes.active): + col = columns[i] + col.mode = mode + col.show() + col.set_property('width-request', width) + def updateTypeParamsWidget(self): + try: + vbox = self.typeParamsVbox + except AttributeError: + return + for child in vbox.get_children(): + child.destroy() + ### + n = len(calTypes.active) + while len(ui.wcalTypeParams) < n: + ui.wcalTypeParams.append({ + 'font': None, + }) + sgroupLabel = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + sgroupFont = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + for i, mode in enumerate(calTypes.active): + #try: + params = ui.wcalTypeParams[i] + #except IndexError: + ## + hbox = WcalTypeParamBox(self.wcal, i, mode, params, sgroupLabel, sgroupFont) + pack(vbox, hbox) + ### + vbox.show_all() + def onConfigChange(self, *a, **ka): + ColumnBase.onConfigChange(self, *a, **ka) + self.updateCols() + self.updateTypeParamsWidget() + + + +@registerSignals +class CalObj(gtk.HBox, CustomizableCalBox, ColumnBase, CalBase): + _name = 'weekCal' + desc = _('Week Calendar') + myKeys = CalBase.myKeys + ( + 'up', 'down', + 'page_up', + 'k', 'p', + 'page_down', + 'j', 'n', + 'end', + 'f10', 'm', + ) + signals = CalBase.signals + def getCellPagePlus(self, cell, plus): + return ui.cellCache.getCell(cell.jd + 7*plus) + def __init__(self): + gtk.HBox.__init__(self) + self.add_events(gdk.EventMask.ALL_EVENTS_MASK) + self.initCal() + self.set_property('height-request', ui.wcalHeight) + self.windowToItemDict = {} + ###################### + self.connect('scroll-event', self.scroll) + ### + self.connect('button-press-event', self.buttonPress) + ##### + defaultItems = [ + ToolbarColumn(self), + WeekDaysColumn(self), + PluginsTextColumn(self), + EventsIconColumn(self), + EventsCountColumn(self), + EventsTextColumn(self), + EventsBoxColumn(self), + DaysOfMonthColumnGroup(self), + ] + defaultItemsDict = dict([(item._name, item) for item in defaultItems]) + itemNames = list(defaultItemsDict.keys()) + for name, enable in ui.wcalItems: + try: + item = defaultItemsDict[name] + except KeyError: + print('weekCal item %s does not exist'%name) + else: + item.enable = enable + self.appendItem(item) + itemNames.remove(name) + for name in itemNames: + item = defaultItemsDict[name] + item.enable = False + self.appendItem(item) + def optionsWidgetCreate(self): + from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton + from scal3.ui_gtk.mywidgets.multi_spin.float_num import FloatSpinButton + from scal3.ui_gtk.pref_utils import CheckPrefItem, ColorPrefItem + if self.optionsWidget: + return + ColumnBase.optionsWidgetCreate(self) + ##### + hbox = gtk.HBox() + spin = IntSpinButton(1, 9999) + spin.set_value(ui.wcalHeight) + spin.connect('changed', self.heightSpinChanged) + pack(hbox, gtk.Label(_('Height'))) + pack(hbox, spin) + pack(self.optionsWidget, hbox) + ### + hbox = gtk.HBox() + spin = FloatSpinButton(0.01, 1, 2) + spin.set_value(ui.wcalTextSizeScale) + spin.connect('changed', self.textSizeScaleSpinChanged) + pack(hbox, gtk.Label(_('Text Size Scale'))) + pack(hbox, spin) + pack(self.optionsWidget, hbox) + ######## + hbox = gtk.HBox(spacing=3) + #### + item = CheckPrefItem(ui, 'wcalGrid', _('Grid')) + item.updateWidget() + gridCheck = item._widget + pack(hbox, gridCheck) + gridCheck.item = item + #### + colorItem = ColorPrefItem(ui, 'wcalGridColor', True) + colorItem.updateWidget() + pack(hbox, colorItem._widget) + gridCheck.colorb = colorItem._widget + gridCheck.connect('clicked', self.gridCheckClicked) + colorItem._widget.item = colorItem + colorItem._widget.connect('color-set', self.gridColorChanged) + colorItem._widget.set_sensitive(ui.wcalGrid) + #### + pack(self.optionsWidget, hbox) + ### + self.optionsWidget.show_all() + def heightSpinChanged(self, spin): + v = spin.get_value() + self.set_property('height-request', v) + ui.wcalHeight = v + def textSizeScaleSpinChanged(self, spin): + ui.wcalTextSizeScale = spin.get_value() + self.queue_draw() + def updateVars(self): + CustomizableCalBox.updateVars(self) + ui.wcalItems = self.getItemsData() + def updateStatus(self): + from scal3.weekcal import getCurrentWeekStatus + self.status = getCurrentWeekStatus() + def onConfigChange(self, *a, **kw): + self.updateStatus() + ColumnBase.onConfigChange(self, *a, **kw) + self.queue_draw() + def onDateChange(self, *a, **kw): + self.updateStatus() + CustomizableCalBox.onDateChange(self, *a, **kw) + self.queue_draw() + #for item in self.items: + # item.queue_draw() + def goBackward4(self, obj=None): + self.jdPlus(-28) + def goBackward(self, obj=None): + self.jdPlus(-7) + def goForward(self, obj=None): + self.jdPlus(7) + def goForward4(self, obj=None): + self.jdPlus(28) + def buttonPress(self, widget, gevent): + col_win = gevent.get_window() + col = None + for item in self.items: + if col_win == item.get_window(): + col = item + break + if not col: + return False + if not col.autoButtonPressHandler: + return False + ### + b = gevent.button + #x, y, mask = col_win.get_pointer() + x, y = self.get_pointer() + #y += 10 + ### + i = int(gevent.y * 7.0 / self.get_allocation().height) + cell = self.status[i] + self.gotoJd(cell.jd) + if gevent.type==TWO_BUTTON_PRESS: + self.emit('2button-press') + if b == 3: + self.emit('popup-cell-menu', gevent.time, x, y) + return True + def keyPress(self, arg, gevent): + print('keyPress') + if CalBase.keyPress(self, arg, gevent): + return True + kname = gdk.keyval_name(gevent.keyval).lower() + print('keyPress', kname) + if kname=='up': + self.jdPlus(-1) + elif kname=='down': + self.jdPlus(1) + elif kname=='end': + self.gotoJd(self.status[-1].jd) + elif kname in ('page_up', 'k', 'p'): + self.jdPlus(-7) + elif kname in ('page_down', 'j', 'n'): + self.jdPlus(7) + elif kname in ('f10', 'm'): + if gevent.state & gdk.ModifierType.SHIFT_MASK: + # Simulate right click (key beside Right-Ctrl) + self.emit('popup-cell-menu', gevent.time, *self.getCellPos()) + else: + self.emit('popup-main-menu', gevent.time, *self.getMainMenuPos()) + else: + return False + return True + def scroll(self, widget, gevent): + d = getScrollValue(gevent) + if d=='up': + self.jdPlus(-1) + elif d=='down': + self.jdPlus(1) + else: + return False + getCellPos = lambda self: ( + int(self.get_allocation().width / 2.0), + (ui.cell.weekDayIndex+1) * self.get_allocation().height / 7.0, + ) + def getToolbar(self): + for item in self.items: + if item.enable and item._name == 'toolbar': + return item + def getMainMenuPos(self): + toolbar = self.getToolbar() + if toolbar: + for item in toolbar.items: + if item.enable and item._name == 'mainMenu': + return item.getMenuPos() + if rtl: + return self.get_allocation().width, 0 + else: + return 0, 0 + + + + + + + + + +if __name__=='__main__': + win = gtk.Dialog(parent=None) + cal = CalObj() + win.add_events(gdk.EventMask.ALL_EVENTS_MASK) + pack(win.vbox, cal, 1, 1) + win.vbox.show_all() + win.resize(600, 400) + win.set_title(cal.desc) + cal.onConfigChange() + win.run() + + + + + + + + + + + diff --git a/scal3/ui_gtk/mainwin_items/winContronller.py b/scal3/ui_gtk/mainwin_items/winContronller.py new file mode 100644 index 000000000..ce5a2645b --- /dev/null +++ b/scal3/ui_gtk/mainwin_items/winContronller.py @@ -0,0 +1,150 @@ +from scal3.path import * +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import set_tooltip +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.customize import CustomizableCalObj, CustomizableCalBox + +@registerSignals +class WinConButton(gtk.EventBox, CustomizableCalObj): + expand = False + imageName = '' + imageNameFocus = '' + imageNameInactive = 'button-inactive.png' + def __init__(self, controller, size=23): + gtk.EventBox.__init__(self) + self.initVars() + ### + self.size = size + self.controller = controller + CustomizableCalObj.initVars(self) + self.build() + ### + if ui.mainWin: + self.connect('button-press-event', ui.mainWin.childButtonPress) + ### + self.show_all() + def onClicked(self, gWin, gevent): + raise NotImplementedError + setImage = lambda self, imName: self.im.set_from_file('%s/wm/%s'%(pixDir, imName)) + setFocus = lambda self, focus:\ + self.setImage(self.imageNameFocus if focus else self.imageName) + setInactive = lambda self: self.setImage(self.imageNameInactive) + def build(self): + self.im = gtk.Image() + self.setFocus(False) + self.im.set_size_request(self.size, self.size) + self.add(self.im) + self.connect('enter-notify-event', self.enterNotify) + self.connect('leave-notify-event', self.leaveNotify) + self.connect('button-press-event', self.buttonPress) + self.connect('button-release-event', self.buttonRelease) + set_tooltip(self, self.desc)## FIXME + def enterNotify(self, widget, gevent): + self.setFocus(True) + def leaveNotify(self, widget, gevent): + if self.controller.winFocused: + self.setFocus(False) + else: + self.setInactive() + return False + def buttonPress(self, widget, gevent): + self.setFocus(False) + return True + def onClicked(self, gWin, gevent): + pass + def buttonRelease(self, button, gevent): + if gevent.button==1: + self.onClicked(self.controller.gWin, gevent) + return False + +class WinConButtonMin(WinConButton): + _name = 'min' + desc = _('Minimize Window') + imageName = 'button-min.png' + imageNameFocus = 'button-min-focus.png' + def onClicked(self, gWin, gevent): + if ui.winTaskbar: + gWin.iconify() + else: + gWin.emit('delete-event', gdk.Event(gevent)) + +class WinConButtonMax(WinConButton): + _name = 'max' + desc = _('Maximize Window') + imageName = 'button-max.png' + imageNameFocus = 'button-max-focus.png' + def onClicked(self, gWin, gevent): + if gWin.isMaximized: + gWin.unmaximize() + gWin.isMaximized = False + else: + gWin.maximize() + gWin.isMaximized = True + +class WinConButtonClose(WinConButton): + _name = 'close' + desc = _('Close Window') + imageName = 'button-close.png' + imageNameFocus = 'button-close-focus.png' + def onClicked(self, gWin, gevent): + gWin.emit('delete-event', gdk.Event()) + +class WinConButtonSep(WinConButton): + _name = 'sep' + desc = _('Seperator') + expand = True + def build(self): + pass + def setFocus(self, focus): + pass + def setInactive(self): + pass + +## Stick +## Above +## Below + +## What is "GTK Window Decorator" ?????????? +@registerSignals +class CalObj(gtk.HBox, CustomizableCalBox): + _name = 'winContronller' + desc = _('Window Controller') + buttonClassList = (WinConButtonMin, WinConButtonMax, WinConButtonClose, WinConButtonSep) + buttonClassDict = dict([(cls._name, cls) for cls in buttonClassList]) + def __init__(self): + gtk.HBox.__init__(self, spacing=ui.winControllerSpacing) + self.set_property('height-request', 15) + self.set_direction(gtk.TextDirection.LTR)## FIXME + self.initVars() + ########### + for bname, enable in ui.winControllerButtons: + button = self.buttonClassDict[bname](self) + button.enable = enable + self.appendItem(button) + self.set_property('can-focus', True) + ################## + self.gWin = ui.mainWin + if self.gWin: + self.gWin.winCon = self ## dirty FIXME + ##gWin.connect('focus-in-event', self.windowFocusIn) + ##gWin.connect('focus-out-event', self.windowFocusOut) + self.winFocused = True + def windowFocusIn(self, widget=None, event=None): + for b in self.items: + b.setFocus(False) + self.winFocused = True + return False + def windowFocusOut(self, widget=None, event=None): + for b in self.items: + b.setInactive() + self.winFocused = False + return False + def updateVars(self): + CustomizableCalBox.updateVars(self) + ui.winControllerButtons = self.getItemsData() + + diff --git a/scal3/ui_gtk/mywidgets/__init__.py b/scal3/ui_gtk/mywidgets/__init__.py new file mode 100644 index 000000000..fafb53ced --- /dev/null +++ b/scal3/ui_gtk/mywidgets/__init__.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# Also avalable in /usr/share/common-licenses/LGPL on Debian systems +# or /usr/share/licenses/common/LGPL/license.txt on ArchLinux + +import sys, os +from time import time as now +from time import localtime + +from gi.repository import GObject +from gi.repository import GdkPixbuf + +from scal3.ui_gtk import * +from scal3.ui_gtk.font_utils import * +from scal3.ui_gtk.color_utils import * +from scal3.ui_gtk.utils import buffer_get_text +from scal3.ui_gtk.drawing import newDndFontNamePixbuf + +def myRaise(): + i = sys.exc_info() + try: + print(('line %s: %s: %s'%(i[2].tb_lineno, i[0].__name__, i[1]))) + except: + print(i) + +def show_event(widget, gevent): + print(type(widget), gevent.type.value_name)#, gevent.send_event + +time_rem = lambda: int(1000*(1.01-now()%1)) + + +class MyFontButton(gtk.FontButton): + def __init__(self, parent): + gtk.FontButton.__init__(self) + ########## + self.drag_source_set( + gdk.ModifierType.MODIFIER_MASK, + (), + gdk.DragAction.COPY, + ) + self.drag_source_add_text_targets() + self.connect('drag-data-get', self.dragDataGet) + self.connect('drag-begin', self.dragBegin, parent) + self.drag_dest_set( + gtk.DestDefaults.ALL, + (), + gdk.DragAction.COPY, + ) + self.drag_dest_add_text_targets() + self.connect('drag-data-received', self.dragDataRec) + def dragDataGet(self, fontb, context, selection, target_id, etime): + #print('fontButtonDragDataGet') + selection.set_text(gfontEncode(fontb.get_font_name())) + return True + def dragDataRec(self, fontb, context, x, y, selection, target_id, etime): + #dtype = selection.get_data_type() + #print(dtype ## UTF8_STRING) + text = selection.get_text() + #\print('fontButtonDragDataRec text=', text) + if text: + pfont = Pango.FontDescription(text) + if pfont.get_family() and pfont.get_size() > 0: + gtk.FontButton.set_font_name(fontb, text) + return True + def dragBegin(self, fontb, context, parent): + #print('fontBottonDragBegin'## caled before dragCalDataGet) + fontName = gtk.FontButton.get_font_name(self) + pbuf = newDndFontNamePixbuf(fontName) + w = pbuf.get_width() + h = pbuf.get_height() + gtk.drag_set_icon_pixbuf(context, pbuf, w/2, -10) + return True + get_font_name = lambda self: gfontDecode(gtk.FontButton.get_font_name(self)) + def set_font_name(self, font): + if isinstance(font, str):## For compatibility + gtk.FontButton.set_font_name(self, font) + else: + gtk.FontButton.set_font_name(self, gfontEncode(font)) + + + +class MyColorButton(gtk.ColorButton): ## for tooltip text + def __init__(self): + gtk.ColorButton.__init__(self) + self.connect('color-set', self.update_tooltip) + def update_tooltip(self, colorb=None): + try: + r, g, b = self.get_color() + a = self.get_alpha() + if self.get_use_alpha(): + text = '%s\n%s\n%s\n%s'%(r, g, b, a) + else: + text = '%s\n%s\n%s'%(r, g, b) + ##self.get_tooltip_window().set_direction(gtk.TextDirection.LTR) + ##print(self.get_tooltip_window()) + self.set_tooltip_text(text) ##???????????????? Right to left + #self.tt_label.set_label(text)##???????????? Dosent work + ##self.set_tooltip_window(self.tt_win) + except AttributeError:## Old PyGTK + pass + #myRaise(__file__) + def set_color(self, color):## color is a tuple of (r, g, b) + if len(color)==3: + r, g, b = color + gtk.ColorButton.set_color(self, rgbToGdkColor(*color)) + self.set_alpha(255) + elif len(color)==4: + gtk.ColorButton.set_color(self, rgbToGdkColor(*color[:3])) + gtk.ColorButton.set_alpha(self, color[3]*257) + else: + raise ValueError + self.update_tooltip() + def set_alpha(self, alpha):## alpha in range(256) + if alpha==None: + alpha = 255 + gtk.ColorButton.set_alpha(self, alpha*257) + self.update_tooltip() + def get_color(self): + color = gtk.ColorButton.get_color(self) + return ( + int(color.red/257), + int(color.green/257), + int(color.blue/257), + ) + get_alpha = lambda self: int(gtk.ColorButton.get_alpha(self)/257) + + +class TextFrame(gtk.Frame): + def __init__(self): + gtk.Frame.__init__(self) + self.set_border_width(4) + #### + self.textview = gtk.TextView() + self.textview.set_wrap_mode(gtk.WrapMode.WORD) + self.add(self.textview) + #### + self.buff = self.textview.get_buffer() + set_text = lambda self, text: self.buff.set_text(text) + get_text = lambda self: buffer_get_text(self.buff) + + +if __name__=='__main__': + d = gtk.Dialog(parent=None) + clock = FClockLabel() + clock.start() + pack(d.vbox, clock, 1, 1) + d.connect('delete-event', lambda widget, event: gtk.main_quit()) + d.show_all() + gtk.main() + + + + diff --git a/scal3/ui_gtk/mywidgets/button.py b/scal3/ui_gtk/mywidgets/button.py new file mode 100644 index 000000000..1dc6c6b22 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/button.py @@ -0,0 +1,57 @@ +from time import time as now +import sys + +from scal3 import ui + +from gi.repository import GObject +from gi.repository.GObject import timeout_add + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk import gtk_ud as ud + + +class ConButtonBase: + def __init__(self): + self.pressTm = 0 + self.remain = False + ### + self.connect('pressed', self.onPress) + self.connect('released', self.onRelease) + doTrigger = lambda self: self.emit('con-clicked') + def onPress(self, widget, event=None): + self.pressTm = now() + self.remain = True + self.doTrigger() + timeout_add(ui.timeout_initial, self.onPressRemain, self.doTrigger) + def onPressRemain(self, func): + if self.remain and now()-self.pressTm>=ui.timeout_repeat/1000.0: + func() + timeout_add(ui.timeout_repeat, self.onPressRemain, self.doTrigger) + def onRelease(self, widget, event=None): + self.remain = False + + +@registerSignals +class ConButton(gtk.Button, ConButtonBase): + signals =[ + ('con-clicked', []), + ] + def __init__(self, *args, **kwargs): + gtk.Button.__init__(self, *args, **kwargs) + ConButtonBase.__init__(self) + + + + +if __name__=='__main__': + win = gtk.Dialog(parent=None) + button = ConButton('Press') + button.connect('con-clicked', lambda obj: sys.stdout.write('%.4f\n'%now())) + pack(win.vbox, button, 1, 1) + win.vbox.show_all() + win.resize(100, 100) + win.run() + + + diff --git a/scal3/ui_gtk/mywidgets/cal_type_combo.py b/scal3/ui_gtk/mywidgets/cal_type_combo.py new file mode 100644 index 000000000..cc65c3a65 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/cal_type_combo.py @@ -0,0 +1,19 @@ +from scal3.cal_types import calTypes +from scal3.locale_man import tr as _ + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import IdComboBox + +class CalTypeCombo(IdComboBox): + def __init__(self):## , showInactive=True FIXME + ls = gtk.ListStore(int, str) + gtk.ComboBox.__init__(self) + self.set_model(ls) + ### + cell = gtk.CellRendererText() + pack(self, cell, True) + self.add_attribute(cell, 'text', 1) + ### + for i, mod in calTypes.iterIndexModule(): + ls.append([i, _(mod.desc)]) + diff --git a/scal3/ui_gtk/mywidgets/clock.py b/scal3/ui_gtk/mywidgets/clock.py new file mode 100644 index 000000000..2de0c0160 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/clock.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# Also avalable in /usr/share/common-licenses/LGPL on Debian systems +# or /usr/share/licenses/common/LGPL/license.txt on ArchLinux +import time +from time import localtime, strftime +from time import time as now + +from gi.repository.GObject import timeout_add +from gi.repository import GdkPixbuf + +from scal3.ui_gtk import * +from scal3.ui_gtk.font_utils import * + +time_rem = lambda: int(1000*(1.01-now()%1)) + +class ClockLabel(gtk.Label): + def __init__(self, bold=False, seconds=True, selectable=False): + gtk.Label.__init__(self) + self.set_use_markup(True) + self.set_selectable(selectable) + self.bold = bold + self.seconds = seconds + self.running = False + #self.connect('button-press-event', self.button_press) + self.start()#??? + def start(self): + self.running = True + self.update() + def update(self): + if self.running: + timeout_add(time_rem(), self.update) + if self.seconds: + l = '%.2d:%.2d:%.2d'%tuple(localtime()[3:6]) + else: + l = '%.2d:%.2d'%tuple(localtime()[3:5]) + if self.bold: + l = '%s'%l + self.set_label(l) + def stop(self): + self.running = False + #def button_press(self, obj, gevent): + # if gevent.button == 3: + + +class FClockLabel(gtk.Label): + def __init__(self, format='%T', local=True, selectable=False): + '''format is a string that used in strftime(), it can contains markup that apears in GtkLabel + for example format can be "%T" + local is bool. if True, use Local time. and if False, use GMT time. + selectable is bool that passes to GtkLabel''' + gtk.Label.__init__(self) + self.set_use_markup(True) + self.set_selectable(selectable) + self.set_direction(gtk.TextDirection.LTR) + self.format = format + self.local = local + self.running = False + #self.connect('button-press-event', self.button_press) + self.start()#??? + def start(self): + self.running = True + self.update() + def update(self): + if self.running: + timeout_add(time_rem(), self.update) + if self.local: + self.set_label(strftime(self.format)) + else: + self.set_label(strftime(self.format, time.gmtime())) + def stop(self): + self.running = False + + + +class FClockWidget(gtk.DrawingArea): ## Time is in Local + def __init__(self, format='%T', selectable=False): + '''format is a string that used in strftime(), it can contains markup that apears in GtkLabel + for example format can be "%T" + local is bool. if True, use Local time. and if False, use GMT time. + selectable is bool that passes to GtkLabel''' + gtk.DrawingArea.__init__(self) + self.set_direction(gtk.TextDirection.LTR) + self.format = format + self.running = False + #self.connect('button-press-event', self.button_press) + self.start()#??? + def start(self): + self.running = True + self.update() + def update(self): + if self.running: + timeout_add(time_rem(), self.update) + self.set_label(strftime(self.format)) + def stop(self): + self.running = False + def set_label(self, text): + if self.get_window()==None: + return + self.get_window().clear() + cr = self.get_window().cairo_create() + cr.set_source_color(gdk.Color(0,0,0)) + lay = self.create_pango_layout(text) + show_layout(cr, lay) + w, h = lay.get_pixel_size() + cr.clip() + self.set_size_request(w, h) + """ + textLay = self.create_pango_layout('') ## markup + textLay.set_markup(text) + textLay.set_font_description(Pango.FontDescription(ui.getFont())) + w, h = textLay.get_pixel_size() + pixbuf = GdkPixbuf.Pixbuf(GdkPixbuf.Colorspace.RGB, True, 8, w, h) + pixbuf = pixbuf.add_alpha(True, '0','0','0') + pmap, mask = pixbuf.render_pixmap_and_mask(alpha_threshold=127) ## pixmap is also a drawable + pmap.draw_layout(pmap.new_gc(), 0, 0, textLay, statusIconTextColor)#, statusIconBgColor) + self.clear() + #self.set_from_image(pmap.get_image(0, 0, w, h), mask) + self.set_from_pixmap(pmap, mask) + """ + + diff --git a/scal3/ui_gtk/mywidgets/datelabel.py b/scal3/ui_gtk/mywidgets/datelabel.py new file mode 100644 index 000000000..3491c0de3 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/datelabel.py @@ -0,0 +1,42 @@ +from scal3.utils import toStr +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3 import ui +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import setClipboard + +class DateLabel(gtk.Label): + def __init__(self, text=None): + gtk.Label.__init__(self, text) + self.set_selectable(True) + #self.set_cursor_visible(False)## FIXME + self.set_can_focus(False) + self.set_use_markup(True) + self.connect('populate-popup', self.popupPopulate) + #### + self.menu = gtk.Menu() + ## + itemCopyAll = ImageMenuItem(_('Copy _All')) + itemCopyAll.set_image(gtk.Image.new_from_stock(gtk.STOCK_COPY, gtk.IconSize.MENU)) + itemCopyAll.connect('activate', self.copyAll) + self.menu.add(itemCopyAll) + ## + itemCopy = ImageMenuItem(_('_Copy')) + itemCopy.set_image(gtk.Image.new_from_stock(gtk.STOCK_COPY, gtk.IconSize.MENU)) + itemCopy.connect('activate', self.copy) + self.itemCopy = itemCopy + self.menu.add(itemCopy) + ## + self.menu.show_all() + def popupPopulate(self, label, menu): + self.itemCopy.set_sensitive(self.get_property('cursor-position') > self.get_property('selection-bound'))## FIXME + self.menu.popup(None, None, None, None, 3, 0) + ui.updateFocusTime() + def copy(self, item): + start = self.get_property('selection-bound') + end = self.get_property('cursor-position') + setClipboard(toStr(self.get_text())[start:end]) + copyAll = lambda self, label: setClipboard(self.get_text()) + diff --git a/scal3/ui_gtk/mywidgets/dialog.py b/scal3/ui_gtk/mywidgets/dialog.py new file mode 100644 index 000000000..57254efb5 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/dialog.py @@ -0,0 +1,27 @@ +from scal3 import core +from scal3.ui_gtk import * + +class MyDialog: + def startWaiting(self): + self.queue_draw() + self.vbox.set_sensitive(False) + self.get_window().set_cursor(gdk.Cursor.new(gdk.CursorType.WATCH)) + while gtk.events_pending(): + gtk.main_iteration_do(False) + def endWaiting(self): + self.get_window().set_cursor(gdk.Cursor.new(gdk.CursorType.LEFT_PTR)) + self.vbox.set_sensitive(True) + def waitingDo(self, func, *args, **kwargs): + self.startWaiting() + if core.debugMode: + func(*args, **kwargs) + self.endWaiting() + else: + try: + func(*args, **kwargs) + except Exception as e: + raise e + finally: + self.endWaiting() + + diff --git a/scal3/ui_gtk/mywidgets/direction_combo.py b/scal3/ui_gtk/mywidgets/direction_combo.py new file mode 100644 index 000000000..6b8f22ab5 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/direction_combo.py @@ -0,0 +1,25 @@ +from scal3.ui_gtk import * + +class DirectionComboBox(gtk.ComboBox): + keys = ['ltr', 'rtl', 'auto'] + descs = [ + _('Left to Right'), + _('Right to Left'), + _('Auto'), + ] + def __init__(self): + ls = gtk.ListStore(str) + gtk.ComboBox.__init__(self) + self.set_model(ls) + ### + cell = gtk.CellRendererText() + pack(self, cell, True) + self.add_attribute(cell, 'text', 0) + ### + for d in self.descs: + ls.append([d]) + self.set_active(0) + getValue = lambda self: self.keys[self.get_active()] + def setValue(self, value): + self.set_active(self.keys.index(value)) + diff --git a/scal3/ui_gtk/mywidgets/floatingMsg.py b/scal3/ui_gtk/mywidgets/floatingMsg.py new file mode 100644 index 000000000..0cc9cf392 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/floatingMsg.py @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import time +from time import time as now + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.drawing import * +from scal3.ui_gtk.mywidgets import MyColorButton +from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton + + +rootWin = gdk.get_default_root_window() +screenWidth = rootWin.get_width() + +@registerType +class FloatingMsg(gtk.DrawingArea): + def on_realize(self, widget): + self.animateStart() + def __init__(self, text, + speed=100, + bgColor=(255, 255, 0), + textColor=(0, 0, 0), + refreshTime=10, + finishFunc=None, + finishOnClick=True, + createWindow=True): + gtk.DrawingArea.__init__(self) + ## speed: pixels per second + self.speed = speed + self.bgColor = bgColor + self.textColor = textColor + self.refreshTime = refreshTime + self.finishFunc = finishFunc + self.isFinished = False + if finishOnClick: + self.connect('button-press-event', self.finish) + ######## + if isinstance(text, str): + text = text.decode('utf8') + lines = [] + for line in text.split('\n'): + line = line.strip() + if line: + lines.append(line) + self.linesNum = len(lines) + self.layoutList = [newTextLayout(self, line) for line in lines] + self.rtlList = [self.isRtl(lines[i], self.layoutList[i]) + for i in range(self.linesNum)] + self.index = 0 + self.height = 30 + ######## + self.connect('draw', self.onExposeEvent) + self.connect('realize', self.on_realize) + ######## + if createWindow: + self.win = gtk.Window(gtk.WindowType.POPUP)#gtk.WindowType.POPUP ## ???????????????? + self.win.add(self) + self.win.set_decorated(False) + self.win.set_property('skip-taskbar-hint', True) + self.win.set_keep_above(True) + else: + self.win = False + def isRtl(self, line, layout): + for i in range(len(line)): + if layout.index_to_pos(i)[2] != 0: + return (layout.index_to_pos(i)[2] < 0) + return False + def updateLine(self): + self.layout = self.layoutList[self.index] + self.rtl = self.rtlList[self.index] + self.rtlSign = 1 if self.rtl else -1 + size = self.layout.get_pixel_size() + self.height = size[1] + self.set_size_request(screenWidth, self.height) + if self.win!=None: + self.win.resize(screenWidth, self.height) + self.textWidth = size[0] + self.startXpos = -self.textWidth if self.rtl else screenWidth + self.xpos = self.startXpos + def finish(self, w=None, e=None): + self.isFinished = True + self.win.destroy() + self.destroy() + if self.finishFunc: + self.finishFunc() + def onExposeEvent(self, widget, gevent): + cr = self.cr = self.get_window().cairo_create() + ####### + cr.rectangle(0, 0, screenWidth, self.height) + setColor(cr, self.bgColor) + cr.fill() + ####### + cr.move_to(self.xpos, 0) + setColor(cr, self.textColor) + show_layout(cr, self.layout) + def animateStart(self): + self.updateLine() + self.startTime = now() + self.animateUpdate() + def animateUpdate(self): + if self.isFinished: + return + GObject.timeout_add(self.refreshTime, self.animateUpdate) + self.xpos = self.startXpos + (now()-self.startTime)*self.speed*self.rtlSign + if self.xpos>screenWidth or self.xpos<-self.textWidth: + if self.index >= self.linesNum-1: + self.finish() + return + else: + self.index += 1 + self.updateLine() + self.queue_draw() + def show(self): + gtk.DrawingArea.show(self) + self.win.show() + + +@registerType +class MyLabel(gtk.DrawingArea): + def __init__(self, bgColor, textColor): + gtk.DrawingArea.__init__(self) + self.bgColor = bgColor + self.textColor = textColor + self.connect('draw', self.onExposeEvent) + def set_label(self, text): + self.text = text + self.layout = newTextLayout(self, text) + size = self.layout.get_pixel_size() + self.height = size[1] + self.width = size[0] + self.set_size_request(self.width, self.height) + self.rtl = self.isRtl() + self.rtlSign = 1 if self.rtl else -1 + def onExposeEvent(self, widget, gevent): + cr = self.cr = self.get_window().cairo_create() + ####### + cr.rectangle(0, 0, self.width, self.height) + setColor(cr, self.bgColor) + cr.fill() + ####### + cr.move_to(0, 0) + setColor(cr, self.textColor) + show_layout(cr, self.layout) + def isRtl(self): + for i in range(len(self.text)): + if self.layout.index_to_pos(i)[2] != 0: + return (self.layout.index_to_pos(i)[2] < 0) + return False + + +@registerType +class NoFillFloatingMsgWindow(gtk.Window): + def __init__(self, text, + speed=100, + bgColor=(255, 255, 0), + textColor=(0, 0, 0), + refreshTime=10, + finishFunc=None, + finishOnClick=True): + gtk.Window.__init__(self) + self.set_type_hint(gtk.WindowType.POPUP)#gtk.WindowType.POPUP ## ???????????????? + self.set_decorated(False) + self.set_property('skip-taskbar-hint', True) + self.set_keep_above(True) + self.label = MyLabel(bgColor, textColor) + self.add(self.label) + self.label.show() + ## speed: pixels per second + self.speed = speed + self.refreshTime = refreshTime + self.finishFunc = finishFunc + self.isFinished = False + if finishOnClick: + self.connect('button-press-event', self.finish) + ######## + if isinstance(text, str): + text = text.decode('utf8') + text = text.replace('\\n', '\n').replace('\\t', '\t') + lines = [] + for line in text.split('\n'): + line = line.strip() + if line: + lines.append(line) + self.linesNum = len(lines) + self.lines = lines + self.index = 0 + ######## + self.connect('realize', lambda widget: self.animateStart()) + def updateLine(self): + self.label.set_label(self.lines[self.index]) + self.startXpos = -self.label.width if self.label.rtl else screenWidth + self.startTime = now() + def finish(self, w=None, e=None): + self.isFinished = True + self.destroy() + if self.finishFunc: + self.finishFunc() + def animateStart(self): + self.updateLine() + self.animateUpdate() + def animateUpdate(self): + if self.isFinished: + return + GObject.timeout_add(self.refreshTime, self.animateUpdate) + xpos = int(self.startXpos + (now()-self.startTime)*self.speed*self.label.rtlSign) + self.move(xpos, 0) + self.resize(1, 1) + if xpos>screenWidth or xpos<-self.label.width: + if self.index >= self.linesNum-1: + self.finish() + return + else: + self.index += 1 + self.updateLine() + + + + +if __name__=='__main__': + import sys + if len(sys.argv)<2: + sys.exit(1) + text = ' '.join(sys.argv[1:]) + msg = NoFillFloatingMsgWindow(text, speed=200, finishFunc=gtk.main_quit) + #msg = FloatingMsg(text, speed=200, finishFunc=gtk.main_quit) + msg.show() + gtk.main() + + diff --git a/scal3/ui_gtk/mywidgets/font_family_combo.py b/scal3/ui_gtk/mywidgets/font_family_combo.py new file mode 100644 index 000000000..09af5552a --- /dev/null +++ b/scal3/ui_gtk/mywidgets/font_family_combo.py @@ -0,0 +1,54 @@ +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from gi.repository import GObject + +from scal3.ui_gtk import * +from scal3.ui_gtk.font_utils import getFontFamilyList + + +class FontFamilyCombo(gtk.ComboBox): + def __init__(self, hasAuto=False): + gtk.ComboBox.__init__(self) + ls = gtk.ListStore(str, str) + self.set_model(ls) + ### + cell = gtk.CellRendererText() + pack(self, cell, True) + self.add_attribute(cell, 'text', 1) + ### + if hasAuto: + ls.append((None, _('Auto'))) + for fontName in getFontFamilyList(self): + ls.append((fontName, _(fontName)))## translate font name in GUI? FIXME + ### + self.set_active(0) + if not hasAuto: + self.set_value(ui.fontDefault[0]) + ### + #self.set_property('has-entry', 1) + #self.set_property('entry-text-column', 0) + def get_value(self): + i = self.get_active() + if i is None: + return None + return self.get_model()[i][0] + def set_value(self, fontName): + ls = self.get_model() + for i in range(len(ls)): + if ls[i][0]==fontName: + self.set_active(i) + break + +if __name__=='__main__': + d = gtk.Dialog(parent=None) + combo = FontFamilyCombo(1) + pack(d.vbox, combo, 1, 1) + d.vbox.show_all() + d.run() + print(combo.get_value()) + + + + diff --git a/scal3/ui_gtk/mywidgets/icon.py b/scal3/ui_gtk/mywidgets/icon.py new file mode 100644 index 000000000..39ebaa056 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/icon.py @@ -0,0 +1,88 @@ +from os.path import join + +from scal3.path import * +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.utils import labelStockMenuItem + + +@registerSignals +class IconSelectButton(gtk.Button): + signals = [ + ('changed', [str]), + ] + def __init__(self, filename=''): + gtk.Button.__init__(self) + self.image = gtk.Image() + self.add(self.image) + self.dialog = gtk.FileChooserDialog( + title=_('Select Icon File'), + action=gtk.FileChooserAction.OPEN, + ) + okB = self.dialog.add_button(gtk.STOCK_OK, gtk.ResponseType.OK) + cancelB = self.dialog.add_button(gtk.STOCK_CANCEL, gtk.ResponseType.CANCEL) + clearB = self.dialog.add_button(gtk.STOCK_CLEAR, gtk.ResponseType.REJECT) + if ui.autoLocale: + cancelB.set_label(_('_Cancel')) + cancelB.set_image(gtk.Image.new_from_stock(gtk.STOCK_CANCEL,gtk.IconSize.BUTTON)) + okB.set_label(_('_OK')) + okB.set_image(gtk.Image.new_from_stock(gtk.STOCK_OK,gtk.IconSize.BUTTON)) + clearB.set_label(_('Clear')) + clearB.set_image(gtk.Image.new_from_stock(gtk.STOCK_CLEAR,gtk.IconSize.BUTTON)) + ### + menu = gtk.Menu() + self.menu = menu + menu.add(labelStockMenuItem(_('None'), None, self.menuItemActivate, '')) + for item in ui.eventTags: + icon = item.icon + if icon: + menuItem = ImageMenuItem(item.desc) + menuItem.set_image(gtk.Image.new_from_file(icon)) + menuItem.connect('activate', self.menuItemActivate, icon) + menu.add(menuItem) + menu.show_all() + ### + self.dialog.connect('file-activated', self.fileActivated) + self.dialog.connect('response', self.dialogResponse) + #self.connect('clicked', lambda button: button.dialog.run()) + self.connect('button-press-event', self.buttonPressEvent) + ### + self.set_filename(filename) + def buttonPressEvent(self, widget, gevent): + b = gevent.button + if b==1: + self.dialog.run() + elif b==3: + self.menu.popup(None, None, None, None, b, gevent.time) + menuItemActivate = lambda self, widget, icon: self.set_filename(icon) + def dialogResponse(self, dialog, response=0): + dialog.hide() + if response == gtk.ResponseType.OK: + fname = dialog.get_filename() + elif response == gtk.ResponseType.REJECT: + fname = '' + else: + return + self.set_filename(fname) + self.emit('changed', fname) + def fileActivated(self, dialog): + fname = dialog.get_filename() + self.filename = fname + self.image.set_from_file(self.filename) + self.emit('changed', fname) + self.dialog.hide() + get_filename = lambda self: self.filename + def set_filename(self, filename): + if filename is None: + filename = '' + self.dialog.set_filename(filename) + self.filename = filename + if not filename: + self.image.set_from_file(join(pixDir, 'empty.png')) + else: + self.image.set_from_file(filename) + diff --git a/scal3/ui_gtk/mywidgets/month_combo.py b/scal3/ui_gtk/mywidgets/month_combo.py new file mode 100644 index 000000000..e8c5441e9 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/month_combo.py @@ -0,0 +1,38 @@ +from scal3 import core +from scal3 import locale_man +from scal3.locale_man import tr as _ +from scal3.ui_gtk import * + +class MonthComboBox(gtk.ComboBox): + def __init__(self, includeEvery=False): + self.includeEvery = includeEvery + ### + ls = gtk.ListStore(str) + gtk.ComboBox.__init__(self) + self.set_model(ls) + ### + cell = gtk.CellRendererText() + pack(self, cell, True) + self.add_attribute(cell, 'text', 0) + def build(self, mode): + active = self.get_active() + ls = self.get_model() + ls.clear() + if self.includeEvery: + ls.append([_('Every Month')]) + for m in range(1, 13): + ls.append([locale_man.getMonthName(mode, m)]) + if active is not None: + self.set_active(active) + def getValue(self): + a = self.get_active() + if self.includeEvery: + return a + else: + return a + 1 + def setValue(self, value): + if self.includeEvery: + self.set_active(value) + else: + self.set_active(value - 1) + diff --git a/scal3/ui_gtk/mywidgets/multi_spin/__init__.py b/scal3/ui_gtk/mywidgets/multi_spin/__init__.py new file mode 100644 index 000000000..3408ea324 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/multi_spin/__init__.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# Also avalable in /usr/share/common-licenses/LGPL on Debian systems +# or /usr/share/licenses/common/LGPL/license.txt on ArchLinux + +import sys, os +from time import localtime +from time import time as now + +from scal3.utils import toStr +from scal3.cal_types import to_jd, jd_to +from scal3 import locale_man +from scal3.locale_man import tr as _ +from scal3.locale_man import rtl +from scal3.mywidgets.multi_spin import * ## FIXME +from scal3 import ui + +from gi.repository.GObject import timeout_add + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * + +@registerSignals +class MultiSpinButton(gtk.SpinButton): + signals = [ + ('first-min', []), + ('first-max', []), + ] + def __init__(self, sep, fields, arrow_select=True, page_inc=10): + gtk.SpinButton.__init__(self) + #### + sep = toStr(sep) + self.field = ContainerField(sep, *fields) + self.arrow_select = arrow_select + self.set_editable(True) + ### + self.digs = locale_man.getDigits() + ### + #### + self.set_direction(gtk.TextDirection.LTR) ## self is a gtk.Entry + self.set_width_chars(self.field.getMaxWidth()) + #print(self.__class__.__name__, 'value=', value) + gtk.SpinButton.set_value(self, 0) + gtk.SpinButton.set_range(self, -2, 2) + self.set_digits(0) + self.set_increments(1, page_inc) + #self.connect('activate', lambda obj: self.update()) + self.connect('activate', self._entry_activate) + self.connect('key-press-event', self._key_press) + self.connect('scroll-event', self._scroll) + self.connect('button-press-event', self._button_press) + self.connect('button-release-event', self._button_release) + self.connect('output', lambda obj: True)##Disable auto-numeric-validating(the entry text is not a numebr) + #### + #self.select_region(0, 0) + def _entry_activate(self, widget): + #print('_entry_activate', self.get_text()) + self.update() + #print(self.get_text()) + return True + def get_value(self): + self.field.setText(self.get_text()) + return self.field.getValue() + def set_value(self, value): + if isinstance(value, (int, float)): + gtk.SpinButton.set_value(self, value) + pos = self.get_position() + self.field.setValue(value) + self.set_text(self.field.getText()) + self.set_position(pos) + def update(self): + pos = self.get_position() + self.field.setText(toStr(self.get_text())) + self.set_text(self.field.getText()) + self.set_position(pos) + def insertText(self, s, clearSeceltion=True): + selection = self.get_selection_bounds() + if selection and clearSeceltion: + start, end = selection + text = toStr(self.get_text()) + text = text[:start] + s + text[end:] + self.set_text(text) + self.set_position(start+len(s)) + else: + pos = self.get_position() + self.insert_text(s, pos) + self.set_position(pos + len(s)) + def entry_plus(self, p): + self.update() + pos = self.get_position() + self.field.getFieldAt(toStr(self.get_text()), self.get_position()).plus(p) + self.set_text(self.field.getText()) + self.set_position(pos) + def _key_press(self, widget, gevent): + kval = gevent.keyval + kname = gdk.keyval_name(kval).lower() + size = len(self.field) + sep = self.field.sep + step_inc, page_inc = self.get_increments() + if kname in ('up', 'down', 'page_up', 'page_down', 'left', 'right'): + if not self.get_editable(): + return True + if kname in ('left', 'right'): + return False + #if not self.arrow_select: + # return False + #shift = { + # 'left': -1, + # 'right': 1 + #}[kname] + #FIXME + else: + p = { + 'up': step_inc, + 'down': -step_inc, + 'page_up': page_inc, + 'page_down': -page_inc, + }[kname] + self.entry_plus(p) + #from scal3.utils import strFindNth + #if fieldIndex==0: + # i1 = 0 + #else: + # i1 = strFindNth(text, sep, fieldIndex) + len(sep) + #i2 = strFindNth(text, sep, fieldIndex+1) + ##self.grab_focus() + #self.select_region(i1, i2) + return True + #elif kname=='return':## Enter + # self.update() + # ##self.emit('activate') + # return True + elif ord('0') <= kval <= ord('9'): + self.insertText(self.digs[kval-ord('0')]) + return True + elif 'kp_0' <= kname <= 'kp_9': + self.insertText(self.digs[int(kname[-1])]) + return True + elif kname in ( + 'period', 'kp_decimal', + ): + self.insertText(locale_man.getNumSep()) + return True + else: + #print(kname, kval) + return False + def _button_press(self, widget, gevent): + gwin = gevent.window + r = self.get_allocation() + ##print(gwin.get_property('name'))## TypeError: object of type `GdkX11Window' does not have property `name' + #print('allocation', r.width, r.height) + #print(gevent.x, gevent.y) + #print(gwin.get_position()) + #print(dir(gwin)) + if not self.has_focus(): + self.grab_focus() + if self.get_editable(): + self.update() + #height = self.size_request().height + get_size = lambda gw: (gw.get_width(), gw.get_height()) + step_inc, page_inc = self.get_increments() + gwin_list = self.get_window().get_children() + gwin_index = gwin_list.index(gwin) + gwin_size = get_size(gwin) + button_type = None ## '+', '-' + try: + if gwin_size == get_size(gwin_list[gwin_index + 1]): + button_type = '+' + except IndexError: + pass + if gwin_index > 0: + if gwin_size == get_size(gwin_list[gwin_index - 1]): + button_type = '-' + #print('_button_press', button_type) + if button_type == '+': + if gevent.button==1: + self._arrow_press(step_inc) + elif gevent.button==2: + self._arrow_press(page_inc) + return True + elif button_type == '-': + if gevent.button==1: + self._arrow_press(-step_inc) + else: + self._arrow_press(-page_inc) + return True + #elif button_type == 'text':## TEXT ENTRY + # if gevent.type==TWO_BUTTON_PRESS: + # pass ## FIXME + # ## select the numeric part containing cursor + # #return True + return False + def _scroll(self, widget, gevent): + d = getScrollValue(gevent) + if d in ('up', 'down'): + if not self.has_focus(): + self.grab_focus() + if self.get_editable(): + plus = (1 if d=='up' else -1) * self.get_increments()[0] + self.entry_plus(plus) + else: + return False + return True + #def _move_cursor(self, obj, step, count, extend_selection): + ## force_select + #print'_entry_move_cursor', count, extend_selection + def _arrow_press(self, plus): + self.pressTm = now() + self._remain = True + timeout_add(ui.timeout_initial, self._arrow_remain, plus) + self.entry_plus(plus) + def _arrow_remain(self, plus): + if self.get_editable() and self._remain and now()-self.pressTm>=ui.timeout_repeat/1000.0: + self.entry_plus(plus) + timeout_add(ui.timeout_repeat, self._arrow_remain, plus) + def _button_release(self, widget, gevent): + self._remain = False + """## ???????????????????????????????? + def _arrow_enter_notify(self, gtkWin): + if gtkWin!=None: + print('_arrow_enter_notify') + gtkWin.set_background(gdk.Color(65535, 0, 0)) + gtkWin.show() + def _arrow_leave_notify(self, gtkWin): + if gtkWin!=None: + print('_arrow_leave_notify') + gtkWin.set_background(gdk.Color(65535, 65535, 65535)) + #""" + + +class SingleSpinButton(MultiSpinButton): + def __init__(self, field, **kwargs): + MultiSpinButton.__init__( + self, + ' ', + (field,), + **kwargs + ) + if isinstance(field, NumField): + gtk.SpinButton.set_range(self, field._min, field._max) + def set_range(self, _min, _max): + gtk.SpinButton.set_range(self, _min, _max) + get_value = lambda self: MultiSpinButton.get_value(self)[0] + + + + + + diff --git a/scal3/ui_gtk/mywidgets/multi_spin/date.py b/scal3/ui_gtk/mywidgets/multi_spin/date.py new file mode 100644 index 000000000..09e181290 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/multi_spin/date.py @@ -0,0 +1,31 @@ +from time import localtime + +from scal3.cal_types import to_jd, jd_to +from scal3.mywidgets.multi_spin import YearField, MonthField, DayField +from scal3.ui_gtk.mywidgets.multi_spin import MultiSpinButton + + +class DateButton(MultiSpinButton): + def __init__(self, date=None, **kwargs): + MultiSpinButton.__init__( + self, + '/', + ( + YearField(), + MonthField(), + DayField(), + ), + **kwargs + ) + if date==None: + date = localtime()[:3] + self.set_value(date) + def get_jd(self, mode): + y, m, d = self.get_value() + return to_jd(y, m, d, mode) + changeMode = lambda self, fromMode, toMode: self.set_value(jd_to(self.get_jd(fromMode), toMode)) + def setMaxDay(self, _max): + self.field.children[2].setMax(_max) + self.update() + + diff --git a/scal3/ui_gtk/mywidgets/multi_spin/date_time.py b/scal3/ui_gtk/mywidgets/multi_spin/date_time.py new file mode 100644 index 000000000..30646501d --- /dev/null +++ b/scal3/ui_gtk/mywidgets/multi_spin/date_time.py @@ -0,0 +1,41 @@ +from time import localtime + +from scal3.cal_types import to_jd +from scal3.mywidgets.multi_spin import ContainerField, YearField, MonthField, DayField, HourField, Z60Field +from scal3.ui_gtk.mywidgets.multi_spin import MultiSpinButton + +class DateTimeButton(MultiSpinButton): + def __init__(self, date_time=None, **kwargs): + MultiSpinButton.__init__( + self, + ' ', + ( + ContainerField( + '/', + YearField(), + MonthField(), + DayField(), + ), + ContainerField( + ':', + HourField(), + Z60Field(), + Z60Field(), + ), + ), + #StrConField('seconds'), + **kwargs + ) + if date_time==None: + date_time = localtime()[:6] + self.set_value(date_time) + def get_epoch(self, mode): + from scal3.time_utils import getEpochFromJhms + date, hms = self.get_value() + return getEpochFromJhms( + to_jd(date[0], date[1], date[2], mode), + *hms + ) + + + diff --git a/scal3/ui_gtk/mywidgets/multi_spin/day.py b/scal3/ui_gtk/mywidgets/multi_spin/day.py new file mode 100644 index 000000000..a31fcf539 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/multi_spin/day.py @@ -0,0 +1,11 @@ +from scal3.mywidgets.multi_spin import DayField +from scal3.ui_gtk.mywidgets.multi_spin import SingleSpinButton + +class DaySpinButton(SingleSpinButton): + def __init__(self, **kwargs): + SingleSpinButton.__init__( + self, + DayField(pad=0), + **kwargs + ) + diff --git a/scal3/ui_gtk/mywidgets/multi_spin/float_num.py b/scal3/ui_gtk/mywidgets/multi_spin/float_num.py new file mode 100644 index 000000000..84c78ed2a --- /dev/null +++ b/scal3/ui_gtk/mywidgets/multi_spin/float_num.py @@ -0,0 +1,13 @@ +from scal3.mywidgets.multi_spin import FloatField +from scal3.ui_gtk.mywidgets.multi_spin import SingleSpinButton + +class FloatSpinButton(SingleSpinButton): + def __init__(self, _min, _max, digits, **kwargs): + if digits < 1: + raise ValueError('FloatSpinButton: invalid digits=%r'%digits) + SingleSpinButton.__init__( + self, + FloatField(_min, _max, digits), + **kwargs + ) + diff --git a/scal3/ui_gtk/mywidgets/multi_spin/hour_minute.py b/scal3/ui_gtk/mywidgets/multi_spin/hour_minute.py new file mode 100644 index 000000000..a2f6855ba --- /dev/null +++ b/scal3/ui_gtk/mywidgets/multi_spin/hour_minute.py @@ -0,0 +1,27 @@ +from time import localtime + +from scal3.mywidgets.multi_spin import HourField, Z60Field +from scal3.ui_gtk.mywidgets.multi_spin import MultiSpinButton + +class HourMinuteButton(MultiSpinButton): + def __init__(self, hm=None, **kwargs): + MultiSpinButton.__init__( + self, + ':', + ( + HourField(), + Z60Field(), + ), + **kwargs + ) + if hm==None: + hm = localtime()[3:5] + self.set_value(hm) + get_value = lambda self: MultiSpinButton.get_value(self) + [0] + def set_value(self, value): + if isinstance(value, int): + value = [value, 0] + else: + value = value[:2] + MultiSpinButton.set_value(self, value) + diff --git a/scal3/ui_gtk/mywidgets/multi_spin/integer.py b/scal3/ui_gtk/mywidgets/multi_spin/integer.py new file mode 100644 index 000000000..4f24db5a7 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/multi_spin/integer.py @@ -0,0 +1,16 @@ +from scal3.ui_gtk import * +from scal3.mywidgets.multi_spin import IntField +from scal3.ui_gtk.mywidgets.multi_spin import SingleSpinButton + +class IntSpinButton(SingleSpinButton): + def __init__(self, _min, _max, **kwargs): + SingleSpinButton.__init__( + self, + IntField(_min, _max), + **kwargs + ) + def set_range(self, _min, _max): + SingleSpinButton.set_range(self, _min, _max) + self.field.children[0].setRange(_min, _max) + self.set_text(self.field.getText()) + diff --git a/scal3/ui_gtk/mywidgets/multi_spin/option_box/__init__.py b/scal3/ui_gtk/mywidgets/multi_spin/option_box/__init__.py new file mode 100644 index 000000000..94b9acb85 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/multi_spin/option_box/__init__.py @@ -0,0 +1,64 @@ +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.mywidgets.multi_spin import MultiSpinButton + +@registerSignals +class MultiSpinOptionBox(gtk.HBox): + signals = [ + ('activate', []) + ] + def _entry_activate(self, widget): + #self.spin.update() #????? + #self.add_history() + self.emit('activate') + return False + def __init__(self, sep, fields, spacing=0, is_hbox=False, hist_size=10, **kwargs): + if not is_hbox: + gtk.HBox.__init__(self, spacing=spacing) + self.spin = MultiSpinButton(sep, fields, **kwargs) + pack(self, self.spin, 1, 1) + self.hist_size = hist_size + self.option = gtk.Button() + self.option.add(gtk.Arrow(gtk.ArrowType.DOWN, gtk.ShadowType.IN)) + pack(self, self.option, 1, 1) + self.menu = gtk.Menu() + #self.menu.show() + self.option.connect('button-press-event', self.option_pressed) + self.menuItems = [] + #self.option.set_sensitive(False) #??????? + #self.spin._entry_activate = self._entry_activate + self.spin.connect('activate', self._entry_activate) + self.get_value = self.spin.get_value + self.set_value = self.spin.set_value + def option_pressed(self, widget, gevent): + #x, y, w, h = self.option. + self.menu.popup(None, None, None, None, gevent.button, gevent.time) + def add_history(self): + self.spin.update() + text = self.spin.get_text() + found = -1 + n = len(self.menuItems) + for i in range(n): + if self.menuItems[i].text==text: + found = i + break + if found>-1: + self.menu.remove(self.menuItems.pop(found)) + else: + n += 1 + #m.prepend([text])#self.combo.prepend_text(text) + item = MenuItem(text) + item.connect('activate', lambda obj: self.spin.set_text(text)) + item.text = text + self.menu.add(item) + self.menu.reorder_child(item, 0) + if n > self.hist_size: + self.menu.remove(self.menuItems.pop(n-1)) + self.menu.show_all() + #self.option.set_sensitive(True) #??????? + def clear_history(self): + for item in self.menu.get_children(): + self.menu.remove(item) + + + diff --git a/scal3/ui_gtk/mywidgets/multi_spin/option_box/date.py b/scal3/ui_gtk/mywidgets/multi_spin/option_box/date.py new file mode 100644 index 000000000..f31829386 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/multi_spin/option_box/date.py @@ -0,0 +1,24 @@ +from time import localtime + +from scal3.mywidgets.multi_spin import YearField, MonthField, DayField +from scal3.ui_gtk.mywidgets.multi_spin.option_box import MultiSpinOptionBox + +class DateButtonOption(MultiSpinOptionBox): + def __init__(self, date=None, **kwargs): + MultiSpinOptionBox.__init__( + self, + '/', + ( + YearField(), + MonthField(), + DayField(), + ), + **kwargs + ) + if date==None: + date = localtime()[:3] + self.set_value(date) + def setMaxDay(self, _max): + self.spin.field.children[2].setMax(_max) + self.spin.update() + diff --git a/scal3/ui_gtk/mywidgets/multi_spin/option_box/hour_minute.py b/scal3/ui_gtk/mywidgets/multi_spin/option_box/hour_minute.py new file mode 100644 index 000000000..beb4388f8 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/multi_spin/option_box/hour_minute.py @@ -0,0 +1,27 @@ +from time import localtime + +from scal3.mywidgets.multi_spin import HourField, Z60Field +from scal3.ui_gtk.mywidgets.multi_spin.option_box import MultiSpinOptionBox + +class HourMinuteButtonOption(MultiSpinOptionBox): + def __init__(self, hm=None, **kwargs): + MultiSpinOptionBox.__init__( + self, + ':', + ( + HourField(), + Z60Field(), + ), + **kwargs + ) + if hm==None: + hm = localtime()[3:5] + self.set_value(hm) + get_value = lambda self: MultiSpinOptionBox.get_value(self) + [0] + def set_value(self, value): + if isinstance(value, int): + value = [value, 0] + else: + value = value[:2] + MultiSpinOptionBox.set_value(self, value) + diff --git a/scal3/ui_gtk/mywidgets/multi_spin/tests.py b/scal3/ui_gtk/mywidgets/multi_spin/tests.py new file mode 100644 index 000000000..837983a64 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/multi_spin/tests.py @@ -0,0 +1,38 @@ +from scal3.ui_gtk import * + +def getDateTimeWidget(): + from scal3.ui_gtk.mywidgets.multi_spin.date_time import DateTimeButton + btn = DateTimeButton() + btn.set_value((2011, 1, 1)) + return btn + +def getIntWidget(): + from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton + btn = IntSpinButton(0, 99) + btn.set_value(12) + return btn + +def getFloatWidget(): + from scal3.ui_gtk.mywidgets.multi_spin.float_num import FloatSpinButton + btn = FloatSpinButton(-3.3, 5.5, 1) + btn.set_value(3.67) + return btn + +def getFloatBuiltinWidget(): + btn = gtk.SpinButton() + btn.set_range(0, 90) + btn.set_digits(2) + btn.set_increments(0.01, 1) + return btn + + +if __name__=='__main__': + d = gtk.Dialog(parent=None) + btn = getFloatWidget() + btn.set_editable(True) + pack(d.vbox, btn, 1, 1) + d.vbox.show_all() + d.run() + print(btn.get_value()) + + diff --git a/scal3/ui_gtk/mywidgets/multi_spin/time_b.py b/scal3/ui_gtk/mywidgets/multi_spin/time_b.py new file mode 100644 index 000000000..379c88ee1 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/multi_spin/time_b.py @@ -0,0 +1,30 @@ +from time import localtime + +from scal3.mywidgets.multi_spin import HourField, Z60Field +from scal3.ui_gtk.mywidgets.multi_spin import MultiSpinButton + +class TimeButton(MultiSpinButton): + def __init__(self, hms=None, **kwargs): + MultiSpinButton.__init__( + self, + ':', + ( + HourField(), + Z60Field(), + Z60Field(), + ), + **kwargs + ) + if hms==None: + hms = localtime()[3:6] + self.set_value(hms) + def get_seconds(self): + h, m, s = self.get_value() + return h*3600 + m*60 + s + def set_seconds(self, seconds): + day, s = divmod(seconds, 86400) ## do what with "day" ????? + h, s = divmod(s, 3600) + m, s = divmod(s, 60) + self.set_value((h, m, s)) + ##return day + diff --git a/scal3/ui_gtk/mywidgets/multi_spin/timer.py b/scal3/ui_gtk/mywidgets/multi_spin/timer.py new file mode 100644 index 000000000..c28eb3b97 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/multi_spin/timer.py @@ -0,0 +1,56 @@ +from time import localtime + +from gobject import timeout_add + +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.mywidgets.multi_spin.time_b import TimeButton + +@registerSignals +class TimerButton(TimeButton): + signals = [ + ('time-elapse', []), + ] + def __init__(self, **kwargs): + TimeButton.__init__(self, **kwargs) + #self.timer = False + #self.clock = False + self.delay = 1.0 # timer delay + self.tPlus = -1 # timer plus (step) + self.elapse = 0 + def timer_start(self): + self.clock = False + self.timer = True + #self.delay = 1.0 # timer delay + #self.tPlus = -1 # timer plus (step) + #self.elapse = 0 + ######### + self.tOff = now()*self.tPlus - self.get_seconds() + self.set_editable(False) + self.timer_update() + def timer_stop(self): + self.timer = False + self.set_editable(True) + def timer_update(self): + if not self.timer: + return + sec = int(now()*self.tPlus - self.tOff) + self.set_seconds(sec) + if self.tPlus*(sec-self.elapse) >= 0: + self.emit('time-elapse') + self.timer_stop() + else: + timeout_add(int(self.delay*1000), self.timer_update) + def clock_start(self): + self.timer = False + self.clock = True + self.set_editable(False) + self.clock_update() + def clock_stop(self): + self.clock = False + self.set_editable(True) + def clock_update(self): + if self.clock: + timeout_add(time_rem(), self.clock_update) + self.set_value(localtime()[3:6]) + + diff --git a/scal3/ui_gtk/mywidgets/multi_spin/year.py b/scal3/ui_gtk/mywidgets/multi_spin/year.py new file mode 100644 index 000000000..74e1d4de3 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/multi_spin/year.py @@ -0,0 +1,11 @@ +from scal3.mywidgets.multi_spin import YearField +from scal3.ui_gtk.mywidgets.multi_spin import SingleSpinButton + +class YearSpinButton(SingleSpinButton): + def __init__(self, **kwargs): + SingleSpinButton.__init__( + self, + YearField(), + **kwargs + ) + diff --git a/scal3/ui_gtk/mywidgets/multi_spin/year_month.py b/scal3/ui_gtk/mywidgets/multi_spin/year_month.py new file mode 100644 index 000000000..35840dd4e --- /dev/null +++ b/scal3/ui_gtk/mywidgets/multi_spin/year_month.py @@ -0,0 +1,22 @@ +from time import localtime + +from scal3.mywidgets.multi_spin import YearField, MonthField +from scal3.ui_gtk.mywidgets.multi_spin import MultiSpinButton + +class YearMonthButton(MultiSpinButton): + def __init__(self, date=None, **kwargs): + MultiSpinButton.__init__( + self, + '/', + ( + YearField(), + MonthField(), + ), + **kwargs + ) + if date==None: + date = localtime()[:2] + self.set_value(date) + + + diff --git a/scal3/ui_gtk/mywidgets/num_ranges_entry.py b/scal3/ui_gtk/mywidgets/num_ranges_entry.py new file mode 100644 index 000000000..2dc96e340 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/num_ranges_entry.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# Also avalable in /usr/share/common-licenses/LGPL on Debian systems +# or /usr/share/licenses/common/LGPL/license.txt on ArchLinux + +import sys, os, time + +from scal3.utils import toBytes, toStr +from scal3.utils import numRangesEncode, numRangesDecode +from scal3 import core +from scal3 import locale_man +from scal3.locale_man import tr as _ +from scal3.locale_man import numDecode, textNumEncode, textNumDecode + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * + +def myRaise(): + i = sys.exc_info() + try: + print(('line %s: %s: %s'%(i[2].tb_lineno, i[0].__name__, i[1]))) + except: + print(i) + +@registerType +class NumRangesEntry(gtk.Entry): + def __init__(self, _min, _max, page_inc=10): + self._min = _min + self._max = _max + self.digs = locale_man.digits[locale_man.langSh] + self.page_inc = page_inc + #### + gtk.Entry.__init__(self) + self.connect('key-press-event', self.keyPress) + self.set_direction(gtk.TextDirection.LTR) + self.set_alignment(0.5) + def insertText(self, s, clearSeceltion=True): + selection = self.get_selection_bounds() + if selection and clearSeceltion: + start, end = selection + text = toStr(self.get_text()) + text = text[:start] + s + text[end:] + self.set_text(text) + self.set_position(start+len(s)) + else: + pos = self.get_position() + self.insert_text(s, pos) + self.set_position(pos + len(s)) + def numPlus(self, plus): + pos = self.get_position() + text = toStr(self.get_text()) + n = len(text) + commaI = text.rfind(',', 0, pos) + if commaI == -1: + startI = 0 + else: + if text[commaI+1]==' ': + startI = commaI + 2 + else: + startI = commaI + 1 + nextCommaI = text.find(',', pos) + if nextCommaI == -1: + endI = n + else: + endI = nextCommaI + dashI = text.find('-', startI, endI) + if dashI != -1: + #print('dashI=%r'%dashI) + if pos < dashI: + endI = dashI + else: + startI = dashI + 1 + thisNumStr = text[startI:endI] + #print(startI, endI, thisNumStr) + if thisNumStr: + thisNum = numDecode(thisNumStr) + newNum = thisNum + plus + if not self._min <= newNum <= self._max: + return + else: + if plus > 0: + newNum = self._max + else: + newNum = self._min + newNumStr = _(newNum) + newText = text[:startI] + newNumStr + text[endI:] + self.set_text(newText) + #print('new end index', endI - len(thisNumStr) + len(newNumStr)) + self.set_position(pos) + self.select_region( + startI, + endI - len(thisNumStr) + len(newNumStr), + ) + def keyPress(self, obj, gevent): + kval = gevent.keyval + kname = gdk.keyval_name(gevent.keyval).lower() + #print(kval, kname) + if kname in ( + 'tab', 'escape', 'backspace', 'delete', 'insert', + 'home', 'end', + 'control_l', 'control_r', + 'iso_next_group', + ): + return False + elif kname == 'return': + self.validate() + return False + elif kname=='up': + self.numPlus(1) + elif kname=='down': + self.numPlus(-1) + elif kname=='page_up': + self.numPlus(self.page_inc) + elif kname=='page_down': + self.numPlus(-self.page_inc) + elif kname=='left': + return False## FIXME + elif kname=='right': + return False## FIXME + #elif kname in ('braceleft', 'bracketleft'): + # self.insertText(u'[') + #elif kname in ('braceright', 'bracketright'): + # self.insertText(u']') + elif kname in ('comma', 'arabic_comma'): + self.insertText(', ', False) + elif kname=='minus': + pos = self.get_position() + text = toStr(self.get_text()) + n = len(text) + if pos==n: + start = numDecode(text.split(',')[-1].strip()) + self.insertText('-' + _(start + 2), False) + else: + self.insertText('-', False) + elif ord('0') <= kval <= ord('9'): + self.insertText(self.digs[kval-ord('0')]) + else: + uniVal = gdk.keyval_to_unicode(kval) + #print('uniVal=%r'%uniVal) + if uniVal!=0: + ch = chr(uniVal) + #print('ch=%r'%ch) + if ch in self.digs: + self.insertText(ch) + if gevent.get_state() & gdk.ModifierType.CONTROL_MASK:## Shortcuts like Ctrl + [A, C, X, V] + return False + else: + print(kval, kname) + return True + getValues = lambda self: numRangesDecode(textNumDecode(self.get_text())) + setValues = lambda self, values: self.set_text( + textNumEncode(numRangesEncode(values), changeSpecialChars=False) + ) + validate = lambda self: self.setValues(self.getValues()) + + + +if __name__=='__main__': + from scal3 import core + ### + entry = NumRangesEntry(0, 9999) + win = gtk.Dialog(parent=None) + win.vbox.add(entry) + win.vbox.show_all() + win.resize(100, 40) + win.run() + + + diff --git a/scal3/ui_gtk/mywidgets/resize_button.py b/scal3/ui_gtk/mywidgets/resize_button.py new file mode 100644 index 000000000..9803b0a29 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/resize_button.py @@ -0,0 +1,33 @@ +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import imageFromFile + + + +class ResizeButton(gtk.EventBox): + def __init__(self, win, edge=gdk.WindowEdge.SOUTH_EAST): + gtk.EventBox.__init__(self) + self.win = win + self.edge = edge + ### + self.image = imageFromFile('resize-small.png') + self.add(self.image) + self.connect('button-press-event', self.buttonPress) + def buttonPress(self, obj, gevent): + self.win.begin_resize_drag( + self.edge, + gevent.button, + int(gevent.x_root), + int(gevent.y_root), + gevent.time, + ) + + + + + + + + + + + diff --git a/scal3/ui_gtk/mywidgets/text_widgets.py b/scal3/ui_gtk/mywidgets/text_widgets.py new file mode 100644 index 000000000..6b5fce841 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/text_widgets.py @@ -0,0 +1,78 @@ +from scal3.utils import toStr +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import labelStockMenuItem, setClipboard, buffer_get_text + +class ReadOnlyTextWidget: + copyAll = lambda self, item: setClipboard(toStr(self.get_text())) + def has_selection(): + raise NotImplementedError + def labelMenuAddCopyItems(self, menu): + menu.add(labelStockMenuItem( + 'Copy _All', + gtk.STOCK_COPY, + self.copyAll, + )) + #### + itemCopy = labelStockMenuItem( + '_Copy', + gtk.STOCK_COPY, + self.copy, + ) + if not self.has_selection(): + itemCopy.set_sensitive(False) + menu.add(itemCopy) + def onPopup(self, widget, menu): + menu = gtk.Menu() + self.labelMenuAddCopyItems(menu) + #### + menu.show_all() + self.tmpMenu = menu + menu.popup(None, None, None, None, 3, 0) + ui.updateFocusTime() + + +class ReadOnlyLabel(gtk.Label, ReadOnlyTextWidget): + get_cursor_position = lambda self: self.get_property('cursor-position') + get_selection_bound = lambda self: self.get_property('selection-bound') + def has_selection(self): + return self.get_cursor_position() != self.get_selection_bound() + def copy(self, item): + bound = self.get_selection_bound() + cursor = self.get_cursor_position() + start = min(bound, cursor) + end = max(bound, cursor) + setClipboard(toStr(self.get_text())[start:end]) + def __init__(self, *args, **kwargs): + gtk.Label.__init__(self, *args, **kwargs) + self.set_selectable(True)## to be selectable, with visible cursor + self.connect('populate-popup', self.onPopup)## FIXME + + + +class ReadOnlyTextView(gtk.TextView, ReadOnlyTextWidget): + get_text = lambda self: buffer_get_text(self.get_buffer()) + get_cursor_position = lambda self: self.get_buffer().get_property('cursor-position') + def has_selection(self): + buf = self.get_buffer() + try: + start_iter, end_iter = buf.get_selection_bounds() + except ValueError: + return False + else: + return True + def copy(self, item): + buf = self.get_buffer() + start_iter, end_iter = buf.get_selection_bounds() + setClipboard(toStr(buf.get_text(start_iter, end_iter, True))) + def __init__(self, *args, **kwargs): + gtk.TextView.__init__(self, *args, **kwargs) + self.set_editable(False) + self.set_cursor_visible(False) + self.connect('populate-popup', self.onPopup)## FIXME + + + diff --git a/scal3/ui_gtk/mywidgets/tz_combo.py b/scal3/ui_gtk/mywidgets/tz_combo.py new file mode 100644 index 000000000..d14d72555 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/tz_combo.py @@ -0,0 +1,70 @@ +from os.path import join + +from scal3.path import rootDir +from scal3.json_utils import jsonToOrderedData +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * + +class TimeZoneComboBoxEntry(gtk.HBox): + def __init__(self): + from natz.tree import getZoneInfoTree + gtk.HBox.__init__(self) + model = gtk.TreeStore(str, bool) + self.c = gtk.ComboBoxText.new_with_entry() + #gtk.ComboBoxText.__init__(self) + self.c.set_model(model) + self.c.set_entry_text_column(0) + first_cell = self.c.get_cells()[0] + self.c.add_attribute(first_cell, 'text', 0) + self.c.add_attribute(first_cell, 'sensitive', 1) + self.c.connect('changed', self.onChanged) + child = self.c.get_child() + child.set_text(str(core.localTz)) + #self.set_text(str(core.localTz)) ## FIXME + ### + self.get_text = child.get_text + #self.get_text = self.c.get_active_text ## FIXME + self.set_text = child.set_text + ##### + recentIter = model.append(None, [ + _('Recent...'), + False, + ]) + for tz_name in ui.localTzHist: + model.append(recentIter, [tz_name, True]) + ### + self.appendOrderedDict( + None, + getZoneInfoTree(), + ) + def appendOrderedDict(self, parentIter, dct): + model = self.c.get_model() + for key, value in dct.items(): + if isinstance(value, dict): + itr = model.append(parentIter, [key, False]) + self.appendOrderedDict(itr, value) + else: + itr = model.append(parentIter, [key, True]) + def onChanged(self, widget): + model = self.c.get_model() + itr = self.c.get_active_iter() + if itr is None: + return + path = model.get_path(itr) + parts = [] + if path[0] == 0: + text = model.get(itr, 0)[0] + else: + for i in range(len(path)): + parts.append( + model.get( + model.get_iter(path[:i+1]), + 0, + )[0] + ) + text = '/'.join(parts) + self.set_text(text) + diff --git a/scal3/ui_gtk/mywidgets/weekday_combo.py b/scal3/ui_gtk/mywidgets/weekday_combo.py new file mode 100644 index 000000000..80d792fbb --- /dev/null +++ b/scal3/ui_gtk/mywidgets/weekday_combo.py @@ -0,0 +1,22 @@ +from scal3 import core +from scal3.locale_man import tr as _ +from scal3.ui_gtk import * + +class WeekDayComboBox(gtk.ComboBox): + def __init__(self): + ls = gtk.ListStore(str) + gtk.ComboBox.__init__(self) + self.set_model(ls) + self.firstWeekDay = core.firstWeekDay + ### + cell = gtk.CellRendererText() + pack(self, cell, True) + self.add_attribute(cell, 'text', 0) + ### + for i in range(7): + ls.append([core.weekDayName[(i+self.firstWeekDay)%7]]) + self.set_active(0) + getValue = lambda self: (self.firstWeekDay + self.get_active()) % 7 + def setValue(self, value): + self.set_active((value-self.firstWeekDay)%7) + diff --git a/scal3/ui_gtk/mywidgets/ymd.py b/scal3/ui_gtk/mywidgets/ymd.py new file mode 100644 index 000000000..2fe993621 --- /dev/null +++ b/scal3/ui_gtk/mywidgets/ymd.py @@ -0,0 +1,68 @@ +from scal3.cal_types import calTypes +from scal3 import core +from scal3.core import getMonthLen +from scal3.locale_man import tr as _ + +from scal3.ui_gtk import * +from scal3.ui_gtk.mywidgets.multi_spin.year import YearSpinButton +from scal3.ui_gtk.mywidgets.multi_spin.day import DaySpinButton + +class YearMonthDayBox(gtk.HBox): + def __init__(self): + gtk.HBox.__init__(self, spacing=4) + self.mode = calTypes.primary + #### + pack(self, gtk.Label(_('Year'))) + self.spinY = YearSpinButton() + pack(self, self.spinY) + #### + pack(self, gtk.Label(_('Month'))) + comboMonth = gtk.ComboBoxText() + module = calTypes[self.mode] + for i in range(12): + comboMonth.append_text(_(module.getMonthName(i+1, None)))## year=None means all months + comboMonth.set_active(0) + pack(self, comboMonth) + self.comboMonth = comboMonth + #### + pack(self, gtk.Label(_('Day'))) + self.spinD = DaySpinButton() + pack(self, self.spinD) + self.comboMonthConn = comboMonth.connect('changed', self.comboMonthChanged) + self.spinY.connect('changed', self.comboMonthChanged) + def set_mode(self, mode): + self.comboMonth.disconnect(self.comboMonthConn) + self.mode = mode + module = calTypes[mode] + combo = self.comboMonth + combo.remove_all() + for i in range(12): + combo.append_text(_(module.getMonthName(i+1))) + self.spinD.set_range(1, module.maxMonthLen) + self.comboMonthConn = self.comboMonth.connect('changed', self.comboMonthChanged) + def changeMode(self, mode, newMode):## FIXME naming standard? + self.set_mode(newMode) + def set_value(self, date): + y, m, d = date + self.spinY.set_value(y) + self.comboMonth.set_active(m-1) + self.spinD.set_value(d) + get_value = lambda self: ( + self.spinY.get_value(), + self.comboMonth.get_active() + 1, + self.spinD.get_value(), + ) + def comboMonthChanged(self, widget=None): + self.spinD.set_range(1, getMonthLen( + self.spinY.get_value(), + self.comboMonth.get_active() + 1, + self.mode, + )) + + + + + + + + diff --git a/scal3/ui_gtk/player.py b/scal3/ui_gtk/player.py new file mode 100644 index 000000000..6651b6424 --- /dev/null +++ b/scal3/ui_gtk/player.py @@ -0,0 +1,388 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# Based on program "pygme-0.0.6", writen by Vinay Reddy +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from time import sleep +import sys, os, re + +from gi.repository import GObject as gobject + +from scal3.ui_gtk import * + + +## Control +SEEK_TIME_SMALL = 10 # in seconds + +## Mplayer +STATUS_UPDATE_TIMEOUT = 1000 +VOLUME_STEP = 5 + +class MPlayer: + pbox, mplayerIn, mplayerOut = None, None, None + eofHandle, statusQuery = 0, 0 + paused = False + isVidOnTop = False + mplayerOptions = None + playTime = None + def __init__(self, pbox): + self.pbox = pbox + + # Play the specified file + def play(self, path): + print('File path: ', path) + mplayerOptions = self.pbox.mplayerOptions + + if self.pbox.isvidontop: + mplayerOptions = '-ontop ' + mplayerOptions + + cmd = 'mplayer ' + mplayerOptions + ' -quiet -slave \'' + path + '\''# 2>/dev/null' + self.mplayerIn, self.mplayerOut = os.popen2(cmd) #open pipes + + try: + import fcntl + except ImportError: + pass + else: + #set mplayerOut to non-blocking mode + fcntl.fcntl(self.mplayerOut, fcntl.F_SETFL, os.O_NONBLOCK) + + self.startHandleEof() + self.startStatusQuery() + #if self.mplayerIn!=None: + # self.pbox.seekBar.set_sensitive(True) + # self.pbox.fcb.set_sensitive(False) + + + # Get the length of file, format it and place it in playtime + def getLength(self): + self.cmd('get_time_length') + sleep(0.1) + + status = None + try: # get the last line of output + for status in self.mplayerOut: + if not status: break + except: + pass + if not status or not status.startswith('ANS_LENGTH='): + return True + + length = int(float(status.replace('ANS_LENGTH=', '').strip())) + h = length/3600 + m = (length % 3600)/60 + s = (length % 60) + if h: + self.playTime = '%d:%.2d:%.2d' % (h, m, s) + else: + self.playTime = '%d:%.2d' % (m, s) + print(self.playTime) + + # Toggle between play and pause + def pause(self): + if not self.mplayerIn: + return + if self.cmd('pause'): + if self.paused: + self.startStatusQuery() + else: + self.stopStatusQuery() + self.paused = not self.paused + else: + self.stopStatusQuery() + self.paused = False + + # Seek by the amount specified (in seconds) + def seek(self, amount, mode = 0): + if not self.mplayerIn: + return False + self.cmd('seek %s %s'%(amount, mode)) + self.queryStatus() + + # Set volume using aumix + def setVolume(self, value): + if self.pbox.adjustvol: + command = 'aumix -v %s'%value + else: + command = 'aumix -w %s'%value + + try: + os.popen(command) + except Exception as message: + print('Cannot set volume: %s'%message) + + # Change volume by the amount specified + # Changing the adjustment automatically updates + # the range widget and increases the vol + def stepVolume(self, increase): + if increase: + self.pbox.volAdj.value += VOLUME_STEP + if self.pbox.volAdj.value > 100: + self.pbox.volAdj.value = 100 + + else: + if self.pbox.volAdj.value <= VOLUME_STEP: + self.pbox.volAdj.value = 0 + else: + self.pbox.volAdj.value -= VOLUME_STEP + # Close mplayer + def close(self): + if self.paused: + self.pause() + if not self.mplayerIn: + return + self.stopStatusQuery() + self.stopEofHandler() + self.cmd('quit') # It doesn't matter if false is returned + try: + self.mplayerIn.close() + self.mplayerOut.close() + except: + pass + self.mplayerIn, self.mplayerOut = None, None + self.playTime = None + self.pbox.seekAdj.value = 0 + #self.pbox.seekBar.set_sensitive(False) + #self.pbox.fcb.set_sensitive(True) + def cmd(self, command): + if not self.mplayerIn: + return False + try: + self.mplayerIn.write(command + '\n') + self.mplayerIn.flush() + except: + return False + return True + + # Get current playing position in song + def queryStatus(self): + if not self.playTime: + self.getLength() + self.cmd('get_percent_pos') + sleep(0.05) # allow time for output + + status = None + try: # get the last line of output + for status in self.mplayerOut: + if not status: break + except: + pass + + if not status or not status.startswith('ANS_PERCENT_POSITION='): + return True + + self.pbox.seekAdj.value = int(status.replace('ANS_PERCENT_POSITION=', '')) + + return True + + # Handle EOF in mplayerOut + def handleEof(self, source, condition): + self.stopStatusQuery() + self.mplayerIn, self.mplayerOut = None, None + self.pbox.seekAdj.value = 0 + + # Handle EOF (basically, a connection Hung Up in mplayerOut) + def startHandleEof(self): + self.eofHandle = gobject.io_add_watch(self.mplayerOut, gobject.IO_HUP, self.handleEof) + + # Stop looking for IO_HUP in mplayerOut + def stopEofHandler(self): + gobject.source_remove(self.eofHandle) + + # Call a function periodically to fetch status + def startStatusQuery(self): + print('start') + self.statusQuery = gobject.timeout_add(STATUS_UPDATE_TIMEOUT, self.queryStatus) + + # Stop calling the function that fetches status periodically + def stopStatusQuery(self): + gobject.source_remove(self.statusQuery) + + +class PlayerBox(gtk.HBox): + adjustvol = 0 + vollevel0 = 100 + vollevel1 = 50 + mplayerOptions = '-geometry 50:50' + key_pause = 65 + key_stop = 39 + key_seekback = 100 + key_seekforward = 102 + key_volinc = 63 + key_voldec = 112 + isvidontop = False + #continuous = True + #cycle = False + #ontop = 28 + #isvidontop = False + ############### + forbid = [102, 100] + def __init__(self, hasVol=False): + gtk.HBox.__init__(self) + self.fcb = gtk.FileChooserButton(title='Select Sound') + self.fcb.set_local_only(True) + self.fcb.set_property('width-request', 150) + pack(self, self.fcb) + self.mplayer = MPlayer(self) + self.connect('key-press-event', self.divert) + self.connect('destroy', lambda self, *args: self.mplayer.close()) ## FIXME + #self.connect('destroy', lambda obj, event=None: self.mplayer.close())#?????????? + ##self.toolbar.connect('key-press-event', self.toolbarKey)#?????????? + ############## + self.playPauseBut = gtk.Button() + self.playPauseBut.set_image(gtk.Image.new_from_stock(gtk.STOCK_MEDIA_PLAY,gtk.IconSize.SMALL_TOOLBAR)) + self.playPauseBut.connect('clicked', self.playPause) + pack(self, self.playPauseBut) + ####### + stopBut = gtk.Button() + stopBut.set_image(gtk.Image.new_from_stock(gtk.STOCK_MEDIA_STOP,gtk.IconSize.SMALL_TOOLBAR)) + stopBut.connect('clicked', self.stop) + pack(self, stopBut) + ############## + self.seekAdj = gtk.Adjustment(0, 0, 100, 1, 10, 0) + #self.seekAdj.connect('value_changed', self.seekAdjChanged)#?????????????????? + self.seekBar = gtk.HScale(self.seekAdj) + self.seekBar.set_value_pos(gtk.PositionType.TOP) + self.seekBar.set_sensitive(False) + self.seekBar.connect('key-press-event', self.divert) + self.seekBar.set_draw_value(False) + #self.seekBar.connect('format-value', self.displaySongString) + self.seekBar.connect('button-release-event', self.seek) + pack(self, self.seekBar, 1, 1, 5) + ################ + self.hasVol = hasVol + if hasVol: + if self.adjustvol: + self.volAdj = gtk.Adjustment(self.vollevel1, 0, 100, 5, 10, 0) + self.mplayer.setVolume(self.vollevel1) + else: + self.volAdj = gtk.Adjustment(self.vollevel0, 0, 100, 5, 10, 0) + self.mplayer.setVolume(self.vollevel0) + self.volAdj.connect('value_changed', self.setVolume) + scale = gtk.HScale(self.volAdj) + scale.set_size_request(50, -1) + scale.set_value_pos(gtk.PositionType.TOP) + scale.connect('format-value', self.displayVolString) + scale.connect('key-press-event', self.divert) + pack(self, scale, False, False, 5) + def divert(self, widget, gevent): + key = gevent.hardware_keycode + if key == self.key_seekback: # left arrow, seek + self.mplayer.seek(-SEEK_TIME_SMALL) + elif key == self.key_seekforward: # right arrow, seek forward + self.mplayer.seek(SEEK_TIME_SMALL) + elif key == self.key_volinc: # *, increase volume + if self.hasVol: + self.mplayer.stepVolume(True) + elif key == self.key_voldec: # /, decrease volume + if self.hasVol: + self.mplayer.stepVolume(False) + elif key == self.key_pause: # space bar, pause + self.mplayer.pause() + else: + return False + def displaySongString(self, seekBar, value): + if self.mplayer.playTime: + return str(int(value)) + '% of ' + self.mplayer.playTime + elif self.mplayer.mplayerIn: + return str(int(value)) + '% of ' #+ self.playlist.getCurrentSongTime() + else: + return str(int(value)) + '%' + def seek(self, widget, gevent):# Seek on changing the seekBar + #print('seek', self.seekAdj.value, self.mplayer.mplayerIn) + if not self.mplayer.mplayerIn: + print('abc') + sleep(0.05) + self.seekAdj.value = 100 + #self.playPauseBut.set_image(gtk.Image.new_from_stock(gtk.STOCK_MEDIA_PLAY,gtk.IconSize.SMALL_TOOLBAR)) + else: + self.mplayer.seek(int(self.seekAdj.value), 1) + ## Return formatted volume string + displayVolString = lambda self, scale, value: 'Volume: ' + str(int(value)) + '%' + def setVolume(self, adj):# Set volume when the volume range widget is changed + self.mplayer.setVolume(int(adj.value)) + if self.adjustvol: + self.vollevel1 = int(adj.value) + self.mplayer.setVolume(self.vollevel1) + else: + self.vollevel0 = int(adj.value) + self.mplayer.setVolume(self.vollevel0) + def playPause(self, button=None): + icon = gtk.STOCK_MEDIA_PLAY + if self.mplayer.mplayerIn: + if not self.mplayer.paused: + icon = gtk.STOCK_MEDIA_PAUSE + self.mplayer.pause() + else: + icon = gtk.STOCK_MEDIA_PAUSE + path = self.fcb.get_filename() + if path==None: + return + self.mplayer.play(path) + self.playPauseBut.set_image(gtk.Image.new_from_stock(icon, gtk.IconSize.SMALL_TOOLBAR)) + playing = bool(self.mplayer.mplayerIn) + self.fcb.set_sensitive(not playing) + self.seekBar.set_sensitive(playing) + def stop(self, button):# Stop mplayer if it's running + self.mplayer.close() + self.playPauseBut.set_image(gtk.Image.new_from_stock(gtk.STOCK_MEDIA_PLAY,gtk.IconSize.SMALL_TOOLBAR)) + self.fcb.set_sensitive(self.mplayer.mplayerIn==None) + self.seekBar.set_sensitive(self.mplayer.mplayerIn!=None) + def decVol(self, widget): + self.mplayer.stepVolume(False) + def incVol(self, widget): + self.mplayer.stepVolume(True) + def toolbarKey(self, widget, gevent):# Prevent the down and up keys from taking control out of the toolbar + keycode = gevent.hardware_keycode + if keycode in [98, 104]: + return True + return False + def quit(self, event = None): + self.mplayer.close() + gtk.main_quit() + def openFile(self, path, startPlaying=True): + self.fcb.set_filename(path) + if startPlaying: + self.playPause() + #self.mplayer.play(path) + #self.playPauseBut.set_image(gtk.Image.new_from_stock(gtk.STOCK_MEDIA_PAUSE,gtk.IconSize.SMALL_TOOLBAR)) + #self.fcb.set_sensitive(self.mplayer.mplayerIn==None) + #self.seekBar.set_sensitive(self.mplayer.mplayerIn!=None) + getFile = lambda self: self.fcb.get_filename() + + + +if __name__=='__main__': + window = gtk.Window(gtk.WindowType.TOPLEVEL) + window.set_title('Simple PyGTK Interface for MPlayer') + mainVbox = gtk.VBox(False, 0) + pbox = PlayerBox() + pack(mainVbox, pbox) + window.connect('destroy', pbox.quit) + window.add(mainVbox) + mainVbox.show_all() + window.show() + if len(sys.argv)>1: + pbox.openFile(sys.argv[1]) + gtk.main() + + + + + diff --git a/scal3/ui_gtk/pref_utils.py b/scal3/ui_gtk/pref_utils.py new file mode 100644 index 000000000..0c79ac10c --- /dev/null +++ b/scal3/ui_gtk/pref_utils.py @@ -0,0 +1,755 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import sys, os +from os.path import join, split, isabs + +from scal3.path import * +from scal3.utils import toBytes, toStr +from scal3.cal_types import calTypes +from scal3 import core +from scal3 import locale_man +from scal3.locale_man import langDict, langSh, rtl +from scal3.locale_man import tr as _ +from scal3 import startup +from scal3 import ui + +from gi.repository import GdkPixbuf + +from scal3.ui_gtk import * +from scal3.ui_gtk.font_utils import * +from scal3.ui_gtk.color_utils import * +from scal3.ui_gtk.utils import * + + +from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton +from scal3.ui_gtk.mywidgets.multi_spin.float_num import FloatSpinButton + + + +## (VAR_NAME, bool, CHECKBUTTON_TEXT) ## CheckButton +## (VAR_NAME, list, LABEL_TEXT, (ITEM1, ITEM2, ...)) ## ComboBox +## (VAR_NAME, int, LABEL_TEXT, MIN, MAX) ## SpinButton +## (VAR_NAME, float, LABEL_TEXT, MIN, MAX, DIGITS) ## SpinButton +class ModuleOptionItem: + def __init__(self, module, opt): + t = opt[1] + self.opt = opt ## needed?? + self.module = module + self.type = t + self.var_name = opt[0] + hbox = gtk.HBox() + if t==bool: + w = gtk.CheckButton(_(opt[2])) + self.get_value = w.get_active + self.set_value = w.set_active + elif t==list: + pack(hbox, gtk.Label(_(opt[2]))) + w = gtk.ComboBoxText() ### or RadioButton + for s in opt[3]: + w.append_text(_(s)) + self.get_value = w.get_active + self.set_value = w.set_active + elif t==int: + pack(hbox, gtk.Label(_(opt[2]))) + w = IntSpinButton(opt[3], opt[4]) + self.get_value = w.get_value + self.set_value = w.set_value + elif t==float: + pack(hbox, gtk.Label(_(opt[2]))) + w = FloatSpinButton(opt[3], opt[4], opt[5]) + self.get_value = w.get_value + self.set_value = w.set_value + else: + raise RuntimeError('bad option type "%s"'%t) + pack(hbox, w) + self._widget = hbox + #### + self.updateVar = lambda: setattr(self.module, self.var_name, self.get_value()) + self.updateWidget = lambda: self.set_value(getattr(self.module, self.var_name)) + + +## ('button', LABEL, CLICKED_MODULE_NAME, CLICKED_FUNCTION_NAME) +class ModuleOptionButton: + def __init__(self, opt): + funcName = opt[2] + clickedFunc = getattr(__import__('scal3.ui_gtk.%s'%opt[1], fromlist=[funcName]), funcName) + hbox = gtk.HBox() + button = gtk.Button(_(opt[0])) + button.connect('clicked', clickedFunc) + pack(hbox, button) + self._widget = hbox + def updateVar(self): + pass + def updateWidget(self): + pass + + + +class PrefItem(): + ## self.__init__, self.module, self.varName, self._widget + ## self.varName an string containing the name of variable + ## set self.module=None if varName is name of a global variable in this module + def get(self): + raise NotImplementedError + def set(self, value): + raise NotImplementedError + updateVar = lambda self: setattr(self.module, self.varName, self.get()) + updateWidget = lambda self: self.set(getattr(self.module, self.varName)) + ## def confStr(self):## REMOVED + ## repr of a utf8 string (by Python 2) is not utf8 and not readable correctly from Python 3 + ## no simple way you can fix that for strings nested inside other structures + ## yet another reason to switch to JSON for config files + + +class ComboTextPrefItem(PrefItem): + def makeWidget(self): + return gtk.ComboBoxText() + def __init__(self, module, varName, items=[]):## items is a list of strings + self.module = module + self.varName = varName + w = self.makeWidget() + self._widget = w + for s in items: + w.append_text(s) + get = lambda self: self._widget.get_active() + set = lambda value: self._widget.set_active(value) + #def set(self, value): + # print('ComboTextPrefItem.set', value) + # self._widget.set_active(int(value)) + +class FontFamilyPrefItem(ComboTextPrefItem): + def makeWidget(self): + from scal3.ui_gtk.mywidgets.font_family_combo import FontFamilyCombo + return FontFamilyCombo(True) + get = lambda self: self._widget.get_value() + set = lambda self, value: self._widget.set_value(value) + +class ComboEntryTextPrefItem(PrefItem): + def __init__(self, module, varName, items=[]):## items is a list of strings + self.module = module + self.varName = varName + w = gtk.ComboBoxText.new_with_entry() + self._widget = w + for s in items: + w.append_text(s) + child = w.get_child() + self.get = child.get_text + self.set = child.set_text + +class ComboImageTextPrefItem(PrefItem): + def __init__(self, module, varName, items=[]):## items is a list of pairs (imagePath, text) + self.module = module + self.varName = varName + ### + ls = gtk.ListStore(GdkPixbuf.Pixbuf, str) + combo = gtk.ComboBox() + combo.set_model(ls) + ### + cell = gtk.CellRendererPixbuf() + pack(combo, cell, False) + combo.add_attribute(cell, 'pixbuf', 0) + ### + cell = gtk.CellRendererText() + pack(combo, cell, True) + combo.add_attribute(cell, 'text', 1) + ### + self._widget = combo + self.ls = ls + for (imPath, label) in items: + self.append(imPath, label) + self.get = combo.get_active + self.set = combo.set_active + def append(self, imPath, label): + if imPath: + if not isabs(imPath): + imPath = join(pixDir, imPath) + pix = GdkPixbuf.Pixbuf.new_from_file(imPath) + else: + pix = None + self.ls.append([pix, label]) + + +class FontPrefItem(PrefItem):##???????????? + def __init__(self, module, varName, parent): + from scal3.ui_gtk.mywidgets import MyFontButton + self.module = module + self.varName = varName + w = MyFontButton(parent) + self._widget = w + self.get = w.get_font_name## FIXME + self.set = w.set_font_name## FIXME + +class CheckPrefItem(PrefItem): + def __init__(self, module, varName, label='', tooltip=None): + self.module = module + self.varName = varName + w = gtk.CheckButton(label) + if tooltip!=None: + set_tooltip(w, tooltip) + self._widget = w + self.get = w.get_active + self.set = w.set_active + def syncSensitive(self, widget, reverse=False): + self._sensitiveWidget = widget + self._sensitiveReverse = reverse + self._widget.connect('show', self.syncSensitiveUpdate) + self._widget.connect('clicked', self.syncSensitiveUpdate) + def syncSensitiveUpdate(self, myWidget): + active = myWidget.get_active() + if self._sensitiveReverse: + active = not active + self._sensitiveWidget.set_sensitive(active) + + + +class ColorPrefItem(PrefItem): + def __init__(self, module, varName, useAlpha=False): + from scal3.ui_gtk.mywidgets import MyColorButton + self.module = module + self.varName = varName + w = MyColorButton() + w.set_use_alpha(useAlpha) + self.useAlpha = useAlpha + self._widget = w + self.set = w.set_color + def get(self): + #if self.useAlpha: + alpha = self._widget.get_alpha() + if alpha==None: + return self._widget.get_color() + else: + return self._widget.get_color() + (alpha,) + def set(self, color): + if self.useAlpha: + if len(color)==3: + self._widget.set_color(color) + self._widget.set_alpha(255) + elif len(color)==4: + self._widget.set_color(color[:3]) + self._widget.set_alpha(color[3]) + else: + raise ValueError + else: + self._widget.set_color(color) + +class SpinPrefItem(PrefItem): + def __init__(self, module, varName, _min, _max, digits=1): + self.module = module + self.varName = varName + if digits==0: + w = IntSpinButton(_min, _max) + else: + w = FloatSpinButton(_min, _max, digits) + self._widget = w + self.get = w.get_value + self.set = w.set_value + +class WidthHeightPrefItem(PrefItem): + def __init__(self, module, varName, _max): + _min = 0 + self.module = module + self.varName = varName + ### + self.widthItem = IntSpinButton(_min, _max) + self.heightItem = IntSpinButton(_min, _max) + ### + hbox = self._widget = gtk.HBox() + pack(hbox, gtk.Label(_('Width')+':')) + pack(hbox, self.widthItem) + pack(hbox, gtk.Label(' ')) + pack(hbox, gtk.Label(_('Height')+':')) + pack(hbox, self.heightItem) + def get(self): + return ( + int(self.widthItem.get_value()), + int(self.heightItem.get_value()), + ) + def set(self, value): + w, h = value + self.widthItem.set_value(w) + self.heightItem.set_value(h) + + +class FileChooserPrefItem(PrefItem): + def __init__(self, module, varName, title='Select File', currentFolder='', defaultVarName=None): + self.module = module + self.varName = varName + ### + dialog = gtk.FileChooserDialog(title, action=gtk.FileChooserAction.OPEN) + dialog_add_button(dialog, gtk.STOCK_CANCEL, _('_Cancel'), gtk.ResponseType.CANCEL, None) + dialog_add_button(dialog, gtk.STOCK_OK, _('_OK'), gtk.ResponseType.OK, None) + w = gtk.FileChooserButton(dialog) + w.set_local_only(True) + if currentFolder: + w.set_current_folder(currentFolder) + ### + self.defaultVarName = defaultVarName + if defaultVarName: + dialog_add_button(dialog, gtk.STOCK_UNDO, _('_Revert'), gtk.ResponseType.NONE, self.revertClicked) + ### + self._widget = w + self.get = w.get_filename + self.set = w.set_filename + def revertClicked(self, button): + defaultValue = getattr(self.module, self.defaultVarName) + setattr( + self.module, + self.varName, + defaultValue, + ) + self.set(defaultValue) + + +class RadioListPrefItem(PrefItem): + def __init__(self, vertical, module, varName, texts, label=None): + self.num = len(texts) + self.module = module + self.varName = varName + if vertical: + box = gtk.VBox() + else: + box = gtk.HBox() + self._widget = box + self.radios = [gtk.RadioButton(label=_(s)) for s in texts] + first = self.radios[0] + if label!=None: + pack(box, gtk.Label(label)) + pack(box, gtk.Label(''), 1, 1) + pack(box, first) + for r in self.radios[1:]: + pack(box, gtk.Label(''), 1, 1) + pack(box, r) + r.set_group(first) + pack(box, gtk.Label(''), 1, 1) ## FIXME + def get(self): + for i in range(self.num): + if self.radios[i].get_active(): + return i + def set(self, index): + self.radios[index].set_active(True) + +class RadioHListPrefItem(RadioListPrefItem): + def __init__(self, *args, **kwargs): + RadioListPrefItem.__init__(self, False, *args, **kwargs) + +class RadioVListPrefItem(RadioListPrefItem): + def __init__(self, *args, **kwargs): + RadioListPrefItem.__init__(self, True, *args, **kwargs) + + +class ListPrefItem(PrefItem): + def __init__(self, vertical, module, varName, items=[]): + self.module = module + self.varName = varName + if vertical: + box = gtk.VBox() + else: + box = gtk.HBox() + for item in items: + pack(box, item._widget) + self.num = len(items) + self.items = items + self._widget = box + get = lambda self: [item.get() for item in self.items] + def set(self, valueL): + for i in range(self.num): + self.items[i].set(valueL[i]) + def append(self, item): + pack(self._widget, item._widget) + self.items.append(item) + + +class HListPrefItem(ListPrefItem): + def __init__(self, *args, **kwargs): + ListPrefItem.__init__(self, False, *args, **kwargs) + +class VListPrefItem(ListPrefItem): + def __init__(self, *args, **kwargs): + ListPrefItem.__init__(self, True, *args, **kwargs) + + +class WeekDayCheckListPrefItem(PrefItem): + def __init__(self, module, varName, vertical=False, homo=True, abbreviateNames=True): + self.module = module + self.varName = varName + if vertical: + box = gtk.VBox() + else: + box = gtk.HBox() + box.set_homogeneous(homo) + nameList = core.weekDayNameAb if abbreviateNames else core.weekDayName + ls = [gtk.ToggleButton(item) for item in nameList] + s = core.firstWeekDay + for i in range(7): + pack(box, ls[(s+i)%7], 1, 1) + self.cbList = ls + self._widget = box + self.start = s + def setStart(self, s): + b = self._widget + ls = self.cbList + for j in range(7):## or range(6) + b.reorder_child(ls[(s+j)%7], j) + self.start = s + def get(self): + value = [] + cbl = self.cbList + for j in range(7): + if cbl[j].get_active(): + value.append(j) + return value + def set(self, value): + cbl = self.cbList + for cb in cbl: + cb.set_active(False) + for j in value: + cbl[j].set_active(True) + + +''' +class ToolbarIconSizePrefItem(PrefItem): + def __init__(self, module, varName): + self.module = module + self.varName = varName + #### + self._widget = gtk.ComboBoxText() + for item in ud.iconSizeList: + self._widget.append_text(item[0]) + get = lambda self: ud.iconSizeList[self._widget.get_active()][0] + def set(self, value): + for (i, item) in enumerate(ud.iconSizeList): + if item[0]==value: + self._widget.set_active(i) + return +''' + +############################################################ + +class LangPrefItem(PrefItem): + def __init__(self): + self.module = locale_man + self.varName = 'lang' + ### + ls = gtk.ListStore(GdkPixbuf.Pixbuf, str) + combo = gtk.ComboBox() + combo.set_model(ls) + ### + cell = gtk.CellRendererPixbuf() + pack(combo, cell, False) + combo.add_attribute(cell, 'pixbuf', 0) + ### + cell = gtk.CellRendererText() + pack(combo, cell, True) + combo.add_attribute(cell, 'text', 1) + ### + self._widget = combo + self.ls = ls + self.append(join(pixDir, 'computer.png'), _('System Setting')) + for (key, data) in langDict.items(): + self.append(data.flag, data.name) + def append(self, imPath, label): + if imPath=='': + pix = None + else: + if not isabs(imPath): + imPath = join(pixDir, imPath) + pix = GdkPixbuf.Pixbuf.new_from_file(imPath) + self.ls.append([pix, label]) + def get(self): + i = self._widget.get_active() + if i==0: + return '' + else: + return langDict.keyList[i-1] + def set(self, value): + if value=='': + self._widget.set_active(0) + else: + try: + i = langDict.keyList.index(value) + except ValueError: + print('language %s in not in list!'%value) + self._widget.set_active(0) + else: + self._widget.set_active(i+1) + #def updateVar(self): + # lang = + +class CheckStartupPrefItem():## FIXME + def __init__(self): + w = gtk.CheckButton(_('Run on session startup')) + set_tooltip(w, 'Run on startup of Gnome, KDE, Xfce, LXDE, ...\nFile: %s'%startup.comDesk) + self._widget = w + self.get = w.get_active + self.set = w.set_active + def updateVar(self): + if self.get(): + if not startup.addStartup(): + self.set(False) + else: + try: + startup.removeStartup() + except: + pass + def updateWidget(self): + self.set(startup.checkStartup()) + +class AICalsTreeview(gtk.TreeView): + def __init__(self): + gtk.TreeView.__init__(self) + self.set_headers_clickable(False) + self.set_model(gtk.ListStore(str, str)) + ### + self.enable_model_drag_source( + gdk.ModifierType.BUTTON1_MASK, + [ + ('row', gtk.TargetFlags.SAME_APP, self.dragId), + ], + gdk.DragAction.MOVE, + ) + self.enable_model_drag_dest( + [ + ('row', gtk.TargetFlags.SAME_APP, self.dragId), + ], + gdk.DragAction.MOVE, + ) + self.connect('drag-data-get', self.dragDataGet) + self.connect('drag_data_received', self.dragDataReceived) + #### + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(self.title, cell, text=1) + col.set_resizable(True) + self.append_column(col) + self.set_search_column(1) + def dragDataGet(self, treev, context, selection, dragId, etime): + path, col = treev.get_cursor() + if path is None: + return + self.dragPath = path + return True + def dragDataReceived(self, treev, context, x, y, selection, dragId, etime): + srcTreev = context.get_source_widget() + if not isinstance(srcTreev, AICalsTreeview): + return + srcDragId = srcTreev.dragId + model = treev.get_model() + dest = treev.get_dest_row_at_pos(x, y) + if srcDragId == self.dragId: + path, col = treev.get_cursor() + if path==None: + return + i = path[0] + if dest is None: + model.move_after(model.get_iter(i), model.get_iter(len(model)-1)) + elif dest[1] in (gtk.TreeViewDropPosition.BEFORE, gtk.TreeViewDropPosition.INTO_OR_BEFORE): + model.move_before(model.get_iter(i), model.get_iter(dest[0][0])) + else: + model.move_after(model.get_iter(i), model.get_iter(dest[0][0])) + else: + smodel = srcTreev.get_model() + sIter = smodel.get_iter(srcTreev.dragPath) + row = [smodel.get(sIter, j)[0] for j in range(2)] + smodel.remove(sIter) + if dest is None: + model.append(row) + elif dest[1] in (gtk.TreeViewDropPosition.BEFORE, gtk.TreeViewDropPosition.INTO_OR_BEFORE): + model.insert_before(model.get_iter(dest[0]), row) + else: + model.insert_after(model.get_iter(dest[0]), row) + def makeSwin(self): + swin = gtk.ScrolledWindow() + swin.add(self) + swin.set_policy(gtk.PolicyType.AUTOMATIC, gtk.PolicyType.AUTOMATIC) + swin.set_property('width-request', 200) + return swin + + +class ActiveCalsTreeView(AICalsTreeview): + isActive = True + title = _('Active') + dragId = 100 + +class InactiveCalsTreeView(AICalsTreeview): + isActive = False + title = _('Inactive') + dragId = 101 + + + +class AICalsPrefItem(): + def __init__(self): + self._widget = gtk.HBox() + size = gtk.IconSize.SMALL_TOOLBAR + ###### + toolbar = gtk.Toolbar() + toolbar.set_orientation(gtk.Orientation.VERTICAL) + ######## + treev = ActiveCalsTreeView() + treev.connect('row-activated', self.activeTreevRActivate) + treev.connect('focus-in-event', self.activeTreevFocus) + treev.get_selection().connect('changed', self.activeTreevSelectionChanged) + ### + pack(self._widget, treev.makeSwin()) + #### + self.activeTreev = treev + self.activeTrees = treev.get_model() + ######## + toolbar = gtk.Toolbar() + toolbar.set_orientation(gtk.Orientation.VERTICAL) + #### + tb = gtk.ToolButton() + tb.set_direction(gtk.TextDirection.LTR) + tb.action = '' + self.leftRightButton = tb + set_tooltip(tb, _('Activate/Inactivate')) + tb.connect('clicked', self.leftRightClicked) + toolbar.insert(tb, -1) + #### + tb = toolButtonFromStock(gtk.STOCK_GO_UP, size) + set_tooltip(tb, _('Move up')) + tb.connect('clicked', self.upClicked) + toolbar.insert(tb, -1) + ## + tb = toolButtonFromStock(gtk.STOCK_GO_DOWN, size) + set_tooltip(tb, _('Move down')) + tb.connect('clicked', self.downClicked) + toolbar.insert(tb, -1) + ## + pack(self._widget, toolbar) + ######## + treev = InactiveCalsTreeView() + treev.connect('row-activated', self.inactiveTreevRActivate) + treev.connect('focus-in-event', self.inactiveTreevFocus) + treev.get_selection().connect('changed', self.inactiveTreevSelectionChanged) + ### + pack(self._widget, treev.makeSwin()) + #### + self.inactiveTreev = treev + self.inactiveTrees = treev.get_model() + ######## + def setLeftRight(self, isRight): + tb = self.leftRightButton + if isRight is None: + tb.set_label_widget(None) + tb.action = '' + else: + tb.set_label_widget( + gtk.Image.new_from_stock( + gtk.STOCK_GO_FORWARD if isRight ^ rtl else gtk.STOCK_GO_BACK, + gtk.IconSize.SMALL_TOOLBAR, + ) + ) + tb.action = 'inactivate' if isRight else 'activate' + tb.show_all() + def activeTreevFocus(self, treev, gevent=None): + self.setLeftRight(True) + def inactiveTreevFocus(self, treev, gevent=None): + self.setLeftRight(False) + def leftRightClicked(self, obj=None): + tb = self.leftRightButton + if tb.action == 'activate': + path, col = self.inactiveTreev.get_cursor() + if path: + self.activateIndex(path[0]) + elif tb.action == 'inactivate': + if len(self.activeTrees) > 1: + path, col = self.activeTreev.get_cursor() + if path: + self.inactivateIndex(path[0]) + def getCurrentTreeview(self): + tb = self.leftRightButton + if tb.action == 'inactivate': + return self.activeTreev + elif tb.action == 'activate': + return self.inactiveTreev + else: + return + def upClicked(self, obj=None): + treev = self.getCurrentTreeview() + if not treev: + return + path, col = treev.get_cursor() + if path: + i = path[0] + s = treev.get_model() + if i > 0: + s.swap(s.get_iter(i-1), s.get_iter(i)) + treev.set_cursor(i-1) + def downClicked(self, obj=None): + treev = self.getCurrentTreeview() + if not treev: + return + path, col = treev.get_cursor() + if path: + i = path[0] + s = treev.get_model() + if i < len(s)-1: + s.swap(s.get_iter(i), s.get_iter(i+1)) + treev.set_cursor(i+1) + def inactivateIndex(self, index): + self.inactiveTrees.prepend(list(self.activeTrees[index])) + del self.activeTrees[index] + self.inactiveTreev.set_cursor(0) + try: + self.activeTreev.set_cursor(min(index, len(self.activeTrees)-1)) + except: + pass + self.inactiveTreev.grab_focus()## FIXME + def activateIndex(self, index): + self.activeTrees.append(list(self.inactiveTrees[index])) + del self.inactiveTrees[index] + self.activeTreev.set_cursor(len(self.activeTrees)-1)## FIXME + try: + self.inactiveTreev.set_cursor(min(index, len(self.inactiveTrees)-1)) + except: + pass + self.activeTreev.grab_focus()## FIXME + def activeTreevSelectionChanged(self, selection): + if selection.count_selected_rows() > 0: + self.setLeftRight(True) + else: + self.setLeftRight(None) + def inactiveTreevSelectionChanged(self, selection): + if selection.count_selected_rows() > 0: + self.setLeftRight(False) + else: + self.setLeftRight(None) + def activeTreevRActivate(self, treev, path, col): + self.inactivateIndex(path[0]) + def inactiveTreevRActivate(self, treev, path, col): + self.activateIndex(path[0]) + def updateVar(self): + calTypes.activeNames = [row[0] for row in self.activeTrees] + calTypes.inactiveNames = [row[0] for row in self.inactiveTrees] + calTypes.update() + def updateWidget(self): + self.activeTrees.clear() + self.inactiveTrees.clear() + ## + for mode in calTypes.active: + module = calTypes[mode] + self.activeTrees.append([module.name, _(module.desc)]) + ## + for mode in calTypes.inactive: + module = calTypes[mode] + self.inactiveTrees.append([module.name, _(module.desc)]) + + + diff --git a/scal3/ui_gtk/preferences.py b/scal3/ui_gtk/preferences.py new file mode 100644 index 000000000..fd5e69372 --- /dev/null +++ b/scal3/ui_gtk/preferences.py @@ -0,0 +1,1224 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from time import localtime + +import sys, os +from os.path import join + +from scal3.path import * +from scal3.utils import myRaise +from scal3.cal_types import calTypes +from scal3 import core +from scal3 import locale_man +from scal3.locale_man import langSh +from scal3.locale_man import tr as _ +from scal3 import plugin_man +from scal3 import ui +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import * +from scal3.ui_gtk import gtk_ud as ud +from scal3.ui_gtk.pref_utils import * + + + +class PrefDialog(gtk.Dialog): + def __init__(self, statusIconMode, **kwargs): + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Preferences')) + self.connect('delete-event', self.onDelete) + #self.set_has_separator(False) + #self.set_skip_taskbar_hint(True) + ### + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), 1, self.cancel) + dialog_add_button(self, gtk.STOCK_APPLY, _('_Apply'), 2, self.apply) + okB = dialog_add_button(self, gtk.STOCK_OK, _('_OK'), 3, self.ok, tooltip=_('Apply and Close')) + okB.grab_default()## FIXME + #okB.grab_focus()## FIXME + ############################################## + self.localePrefItems = [] + self.corePrefItems = [] + self.uiPrefItems = [] + self.gtkPrefItems = [] ## FIXME + ##### + self.prefPages = [] + ################################ Tab 1 (General) ############################################ + vbox = gtk.VBox() + vbox.label = _('_General') + vbox.icon = 'preferences-other.png' + self.prefPages.append(vbox) + hbox = gtk.HBox(spacing=3) + pack(hbox, gtk.Label(_('Language'))) + itemLang = LangPrefItem() + self.localePrefItems.append(itemLang) + ### + pack(hbox, itemLang._widget) + if langSh!='en': + pack(hbox, gtk.Label('Language')) + pack(vbox, hbox) + ########################## + hbox = gtk.HBox() + frame = gtk.Frame() + frame.set_label(_('Calendar Types')) + itemCals = AICalsPrefItem() + self.corePrefItems.append(itemCals) + frame.add(itemCals._widget) + pack(hbox, frame) + pack(hbox, gtk.Label(''), 1, 1) + hbox.set_border_width(5) + #frame.set_border_width(5) + pack(vbox, hbox, 1, 1) + ########################## + if statusIconMode!=1: + hbox = gtk.HBox(spacing=3) + item = CheckStartupPrefItem() + self.uiPrefItems.append(item) + pack(hbox, item._widget, 1, 1) + pack(vbox, hbox) + ######################## + item = CheckPrefItem(ui, 'showMain', _('Show main window on start')) + self.uiPrefItems.append(item) + pack(vbox, item._widget) + ########################## + item = CheckPrefItem(ui, 'winTaskbar', _('Window in Taskbar')) + self.uiPrefItems.append(item) + hbox = gtk.HBox(spacing=3) + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + ########### + pack(vbox, hbox) + ########################## + try: + from gi.repository import AppIndicator3 as appIndicator + except ImportError: + pass + else: + item = CheckPrefItem(ui, 'useAppIndicator', _('Use AppIndicator')) + self.uiPrefItems.append(item) + hbox = gtk.HBox(spacing=3) + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + pack(vbox, hbox) + ########################## + hbox = gtk.HBox(spacing=3) + pack(hbox, gtk.Label(_('Show Digital Clock:'))) + pack(hbox, gtk.Label(''), 1, 1) + #item = CheckPrefItem(ui, 'showDigClockTb', _('On Toolbar'))## FIXME + #self.uiPrefItems.append(item) + #pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + if statusIconMode==1: + item = CheckPrefItem(ui, 'showDigClockTr', _('On Applet'), 'Panel Applet') + else: + item = CheckPrefItem(ui, 'showDigClockTr', _('On Status Icon'), 'Notification Area') + self.uiPrefItems.append(item) + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + pack(vbox, hbox) + ################################ Tab 2 (Appearance) ########################################### + vbox = gtk.VBox() + vbox.label = _('A_ppearance') + vbox.icon = 'preferences-desktop-theme.png' + self.prefPages.append(vbox) + ######## + hbox = gtk.HBox(spacing=2) + ### + customCheckItem = CheckPrefItem(ui, 'fontCustomEnable', _('Application Font')) + self.uiPrefItems.append(customCheckItem) + pack(hbox, customCheckItem._widget) + ### + customItem = FontPrefItem(ui, 'fontCustom', self) + self.uiPrefItems.append(customItem) + pack(hbox, customItem._widget) + pack(hbox, gtk.Label(''), 1, 1) + customCheckItem.syncSensitive(customItem._widget) + pack(vbox, hbox) + ########################### Theme ##################### + hbox = gtk.HBox(spacing=3) + item = CheckPrefItem(ui, 'bgUseDesk', _('Use Desktop Background')) + self.uiPrefItems.append(item) + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + pack(vbox, hbox) + ##################### + hbox = gtk.HBox(spacing=3) + lab = gtk.Label('%s: '%_('Colors')) + lab.set_use_markup(True) + pack(hbox, lab) + pack(hbox, gtk.Label(''), 1, 1) + ### + pack(hbox, gtk.Label(_('Background'))) + item = ColorPrefItem(ui, 'bgColor', True) + self.uiPrefItems.append(item) + self.colorbBg = item._widget ## FIXME + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + ### + pack(hbox, gtk.Label(_('Border'))) + item = ColorPrefItem(ui, 'borderColor', True) + self.uiPrefItems.append(item) + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + ### + pack(hbox, gtk.Label(_('Cursor'))) + item = ColorPrefItem(ui, 'cursorOutColor', False) + self.uiPrefItems.append(item) + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + ### + pack(hbox, gtk.Label(_('Cursor BG'))) + item = ColorPrefItem(ui, 'cursorBgColor', True) + self.uiPrefItems.append(item) + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + ### + pack(hbox, gtk.Label(_('Today'))) + item = ColorPrefItem(ui, 'todayCellColor', True) + self.uiPrefItems.append(item) + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + ### + pack(vbox, hbox) + #################### + hbox = gtk.HBox(spacing=3) + lab = gtk.Label('%s: '%_('Font Colors')) + lab.set_use_markup(True) + pack(hbox, lab) + pack(hbox, gtk.Label(''), 1, 1) + #### + pack(hbox, gtk.Label(_('Normal'))) + item = ColorPrefItem(ui, 'textColor', False) + self.uiPrefItems.append(item) + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + ### + pack(hbox, gtk.Label(_('Holiday'))) + item = ColorPrefItem(ui, 'holidayColor', False) + self.uiPrefItems.append(item) + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + ### + pack(hbox, gtk.Label(_('Inactive Day'))) + item = ColorPrefItem(ui, 'inactiveColor', True) + self.uiPrefItems.append(item) + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + #### + pack(hbox, gtk.Label(_('Border'))) + item = ColorPrefItem(ui, 'borderTextColor', False) + self.uiPrefItems.append(item) + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + #### + pack(vbox, hbox) + ################### + hbox = gtk.HBox(spacing=1) + label = gtk.Label('%s:'%_('Cursor')) + label.set_use_markup(True) + pack(hbox, label) + pack(hbox, gtk.Label(''), 1, 1) + pack(hbox, gtk.Label(_('Diameter Factor'))) + item = SpinPrefItem(ui, 'cursorDiaFactor', 0, 1, 2) + self.uiPrefItems.append(item) + pack(hbox, item._widget) + ### + pack(hbox, gtk.Label(''), 1, 1) + pack(hbox, gtk.Label(_('Rounding Factor'))) + item = SpinPrefItem(ui, 'cursorRoundingFactor', 0, 1, 2) + self.uiPrefItems.append(item) + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + ### + pack(vbox, hbox) + ################### + exp = gtk.Expander() + label = gtk.Label('%s'%_('Status Icon')) + label.set_use_markup(True) + exp.set_label_widget(label) + expVbox = gtk.VBox(spacing=1) + exp.add(expVbox) + exp.set_expanded(True) + sgroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + #### + hbox = gtk.HBox(spacing=1) + pack(hbox, gtk.Label(' ')) + label = gtk.Label(_('Normal Days')) + sgroup.add_widget(label) + pack(hbox, label) + item = FileChooserPrefItem( + ui, + 'statusIconImage', + title=_('Select Icon'), + currentFolder=pixDir, + defaultVarName='statusIconImageDefault', + ) + self.uiPrefItems.append(item) + pack(hbox, item._widget, 1, 1) + pack(expVbox, hbox) + #### + hbox = gtk.HBox(spacing=1) + pack(hbox, gtk.Label(' ')) + label = gtk.Label(_('Holidays')) + sgroup.add_widget(label) + pack(hbox, label) + item = FileChooserPrefItem( + ui, + 'statusIconImageHoli', + title=_('Select Icon'), + currentFolder=pixDir, + defaultVarName='statusIconImageHoliDefault', + ) + self.uiPrefItems.append(item) + pack(hbox, item._widget, 1, 1) + pack(expVbox, hbox) + #### + hbox = gtk.HBox(spacing=1) + pack(hbox, gtk.Label(' ')) + checkItem = CheckPrefItem( + ui, + 'statusIconFontFamilyEnable', + label=_('Change font family to'), + #tooltip=_('In SVG files'), + ) + self.uiPrefItems.append(checkItem) + #sgroup.add_widget(checkItem._widget) + pack(hbox, checkItem._widget) + item = FontFamilyPrefItem( + ui, + 'statusIconFontFamily', + ) + self.uiPrefItems.append(item) + pack(hbox, item._widget, 1, 1) + pack(expVbox, hbox) + #### + hbox = gtk.HBox(spacing=1) + pack(hbox, gtk.Label(' ')) + checkItem = CheckPrefItem( + ui, + 'statusIconFixedSizeEnable', + label=_('Fixed Size'), + #tooltip=_(''), + ) + self.uiPrefItems.append(checkItem) + #sgroup.add_widget(checkItem._widget) + pack(hbox, checkItem._widget) + pack(hbox, gtk.Label(' ')) + item = WidthHeightPrefItem( + ui, + 'statusIconFixedSizeWH', + 999, + ) + self.uiPrefItems.append(item) + pack(hbox, item._widget, 1, 1) + pack(expVbox, hbox) + ######## + checkItem.syncSensitive(item._widget, reverse=False) + #### + pack(vbox, exp) + ################################ Tab 3 (Advanced) ########################################### + vbox = gtk.VBox() + vbox.label = _('A_dvanced') + vbox.icon = 'applications-system.png' + self.prefPages.append(vbox) + ###### + sgroup = gtk.SizeGroup(gtk.SizeGroupMode.HORIZONTAL) + ###### + hbox = gtk.HBox(spacing=5) + label = gtk.Label(_('Date Format')) + label.set_alignment(0, 0.5) + pack(hbox, label) + sgroup.add_widget(label) + #pack(hbox, gtk.Label(''), 1, 1) + item = ComboEntryTextPrefItem(ud, 'dateFormat', ( + '%Y/%m/%d', + '%Y-%m-%d', + '%y/%m/%d', + '%y-%m-%d', + '%OY/%Om/%Od', + '%OY-%Om-%Od', + '%m/%d', + '%m/%d/%Y', + )) + self.gtkPrefItems.append(item) + pack(hbox, item._widget, 1, 1) + pack(vbox, hbox) + ### + hbox = gtk.HBox(spacing=5) + #pack(hbox, gtk.Label(''), 1, 1) + label = gtk.Label(_('Digital Clock Format')) + label.set_alignment(0, 0.5) + pack(hbox, label) + sgroup.add_widget(label) + item = ComboEntryTextPrefItem(ud, 'clockFormat', ( + '%T', + '%X', + '%Y/%m/%d - %T', + '%OY/%Om/%Od - %X', + '%Y/%m/%d - %T', + '%T', + '%X', + '%H:%M', + '%H:%M', + '%OY/%Om/%Od,%X' + '%OY/%Om/%Od,%X', + '%X', + '%OH:%OM', + '%OH:%OM', + )) + self.gtkPrefItems.append(item) + pack(hbox, item._widget, 1, 1) + pack(vbox, hbox) + ###### + hbox = gtk.HBox(spacing=5) + label = gtk.Label(_('Days maximum cache size')) + label.set_alignment(0, 0.5) + pack(hbox, label) + ##sgroup.add_widget(label) + item = SpinPrefItem(ui, 'maxDayCacheSize', 100, 9999, 0) + self.uiPrefItems.append(item) + pack(hbox, item._widget) + pack(vbox, hbox) + vbox4 = vbox + ######## + hbox = gtk.HBox(spacing=3) + pack(hbox, gtk.Label(_('First day of week'))) + ##item = ComboTextPrefItem( ## FIXME + self.comboFirstWD = gtk.ComboBoxText() + for item in core.weekDayName: + self.comboFirstWD.append_text(item) + self.comboFirstWD.append_text(_('Automatic')) + self.comboFirstWD.connect('changed', self.comboFirstWDChanged) + pack(hbox, self.comboFirstWD) + pack(vbox, hbox) + ######### + hbox0 = gtk.HBox(spacing=0) + pack(hbox0, gtk.Label(_('Holidays')+' ')) + item = WeekDayCheckListPrefItem(core, 'holidayWeekDays') + self.corePrefItems.append(item) + self.holiWDItem = item ## Holiday Week Days Item + pack(hbox0, item._widget, 1, 1) + pack(vbox, hbox0) + ######### + hbox = gtk.HBox(spacing=3) + pack(hbox, gtk.Label(_('First week of year containts'))) + combo = gtk.ComboBoxText() + texts = [_('First %s of year')%name for name in core.weekDayName]+[_('First day of year')] + texts[4] += ' (ISO 8601)' ## FIXME + for text in texts: + combo.append_text(text) + #combo.append_text(_('Automatic'))## (as Locale)## FIXME + pack(hbox, combo) + pack(hbox, gtk.Label(''), 1, 1) + pack(vbox, hbox) + self.comboWeekYear = combo + ######### + hbox = gtk.HBox(spacing=3) + item = CheckPrefItem(locale_man, 'enableNumLocale', _('Numbers Localization')) + self.localePrefItems.append(item) + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + pack(vbox, hbox) + ################################################## + ################################ + options = [] + for mod in calTypes: + for opt in mod.options: + if opt[0]=='button': + try: + optl = ModuleOptionButton(opt[1:]) + except: + myRaise() + continue + else: + optl = ModuleOptionItem(mod, opt) + options.append(optl) + pack(vbox, optl._widget) + self.moduleOptions = options + ################################ Tab 4 (Plugins) ############################################ + vbox = gtk.VBox() + vbox.label = _('_Plugins') + vbox.icon = 'preferences-plugin.png' + self.prefPages.append(vbox) + ##### + ##pluginsTextStatusIcon: + hbox = gtk.HBox() + if statusIconMode==1: + item = CheckPrefItem(ui, 'pluginsTextStatusIcon', _('Show in applet (for today)')) + else: + item = CheckPrefItem(ui, 'pluginsTextStatusIcon', _('Show in Status Icon (for today)')) + self.uiPrefItems.append(item) + pack(hbox, item._widget) + pack(hbox, gtk.Label(''), 1, 1) + pack(vbox, hbox) + ##### + treev = gtk.TreeView() + treev.set_headers_clickable(True) + trees = gtk.ListStore(int, bool, bool, str) + treev.set_model(trees) + treev.enable_model_drag_source(gdk.ModifierType.BUTTON1_MASK, [('row', gtk.TargetFlags.SAME_WIDGET, 0)], gdk.DragAction.MOVE) + treev.enable_model_drag_dest([('row', gtk.TargetFlags.SAME_WIDGET, 0)], gdk.DragAction.MOVE) + treev.connect('drag_data_received', self.plugTreevDragReceived) + treev.get_selection().connect('changed', self.plugTreevCursorChanged) + treev.connect('row-activated', self.plugTreevRActivate) + treev.connect('button-press-event', self.plugTreevButtonPress) + ### + #treev.drag_source_set_icon_stock(gtk.STOCK_CLOSE) + #treev.drag_source_add_text_targets() + #treev.drag_source_add_uri_targets() + #treev.drag_source_unset() + ### + swin = gtk.ScrolledWindow() + swin.add(treev) + swin.set_policy(gtk.PolicyType.AUTOMATIC, gtk.PolicyType.AUTOMATIC) + ###### + cell = gtk.CellRendererToggle() + #cell.set_property('activatable', True) + cell.connect('toggled', self.plugTreeviewCellToggled) + col = gtk.TreeViewColumn(_('Enable'), cell) + col.add_attribute(cell, 'active', 1) + #cell.set_active(False) + col.set_resizable(True) + col.set_property('expand', False) + treev.append_column(col) + ###### + cell = gtk.CellRendererToggle() + #cell.set_property('activatable', True) + cell.connect('toggled', self.plugTreeviewCellToggled2) + col = gtk.TreeViewColumn(_('Show Date'), cell) + col.add_attribute(cell, 'active', 2) + #cell.set_active(False) + col.set_resizable(True) + col.set_property('expand', False) + treev.append_column(col) + ###### + #cell = gtk.CellRendererText() + #col = gtk.TreeViewColumn(_('File Name'), cell, text=2) + #col.set_resizable(True) + #treev.append_column(col) + #treev.set_search_column(1) + ###### + cell = gtk.CellRendererText() + #cell.set_property('wrap-mode', gtk.WrapMode.WORD) + #cell.set_property('editable', True) + #cell.set_property('wrap-width', 200) + col = gtk.TreeViewColumn(_('Title'), cell, text=3) + #treev.connect('draw', self.plugTreevExpose) + #self.plugTitleCell = cell + #self.plugTitleCol = col + #col.set_resizable(True)## No need! + col.set_property('expand', True) + treev.append_column(col) + ###### + #for i in xrange(len(core.plugIndex)): + # x = core.plugIndex[i] + # trees.append([x[0], x[1], x[2], core.allPlugList[x[0]].title]) + ###### + self.plugTreeview = treev + self.plugTreestore = trees + ####################### + hbox = gtk.HBox() + vboxPlug = gtk.VBox() + pack(vboxPlug, swin, 1, 1) + pack(hbox, vboxPlug, 1, 1) + ### + hboxBut = gtk.HBox() + ### + button = gtk.Button(_('_About Plugin')) + button.set_image(gtk.Image.new_from_stock(gtk.STOCK_ABOUT, gtk.IconSize.BUTTON)) + button.set_sensitive(False) + button.connect('clicked', self.plugAboutClicked) + self.plugButtonAbout = button + pack(hboxBut, button) + pack(hboxBut, gtk.Label(''), 1, 1) + ### + button = gtk.Button(_('C_onfigure Plugin')) + button.set_image(gtk.Image.new_from_stock(gtk.STOCK_PREFERENCES, gtk.IconSize.BUTTON)) + button.set_sensitive(False) + button.connect('clicked', self.plugConfClicked) + self.plugButtonConf = button + pack(hboxBut, button) + pack(hboxBut, gtk.Label(''), 1, 1) + ### + pack(vboxPlug, hboxBut) + ### + toolbar = gtk.Toolbar() + toolbar.set_orientation(gtk.Orientation.VERTICAL) + #try:## DeprecationWarning ## FIXME + #toolbar.set_icon_size(gtk.IconSize.SMALL_TOOLBAR) + ### no different (argument to set_icon_size does not affect) FIXME + #except: + # pass + size = gtk.IconSize.SMALL_TOOLBAR + ##no different(argument2 to image_new_from_stock does not affect) FIXME + ######## gtk.IconSize.SMALL_TOOLBAR or gtk.IconSize.MENU + tb = toolButtonFromStock(gtk.STOCK_GOTO_TOP, size) + set_tooltip(tb, _('Move to top')) + tb.connect('clicked', self.plugTreeviewTop) + toolbar.insert(tb, -1) + ######## + tb = toolButtonFromStock(gtk.STOCK_GO_UP, size) + set_tooltip(tb, _('Move up')) + tb.connect('clicked', self.plugTreeviewUp) + toolbar.insert(tb, -1) + ######### + tb = toolButtonFromStock(gtk.STOCK_GO_DOWN, size) + set_tooltip(tb, _('Move down')) + tb.connect('clicked', self.plugTreeviewDown) + toolbar.insert(tb, -1) + ######## + tb = toolButtonFromStock(gtk.STOCK_GOTO_BOTTOM, size) + set_tooltip(tb, _('Move to bottom')) + tb.connect('clicked', self.plugTreeviewBottom) + toolbar.insert(tb, -1) + ########## + tb = toolButtonFromStock(gtk.STOCK_ADD, size) + set_tooltip(tb, _('Add')) + #tb.connect('clicked', lambda obj: self.plugAddDialog.run()) + tb.connect('clicked', self.plugAddClicked) + #if len(self.plugAddItems)==0: + # tb.set_sensitive(False) + toolbar.insert(tb, -1) + self.plugButtonAdd = tb + ########### + tb = toolButtonFromStock(gtk.STOCK_DELETE, size) + set_tooltip(tb, _('Delete')) + tb.connect('clicked', self.plugTreeviewDel) + toolbar.insert(tb, -1) + ########### + pack(hbox, toolbar) + ##### + ''' + vpan = gtk.VPaned() + vpan.add1(hbox) + vbox2 = gtk.VBox() + pack(vbox2, gtk.Label('Test Label')) + vpan.add2(vbox2) + vpan.set_position(100) + pack(vbox, vpan) + ''' + pack(vbox, hbox, 1, 1) + ########################## + d = gtk.Dialog(parent=self) + d.set_transient_for(self) + ## dialog.set_transient_for(parent) makes the window on top of parent and at the center point of parent + ## but if you call dialog.show() or dialog.present(), the parent is still active(clickabel widgets) before closing child "dialog" + ## you may call dialog.run() to realy make it transient for parent + #d.set_has_separator(False) + d.connect('delete-event', self.plugAddDialogClose) + d.set_title(_('Add Plugin')) + ### + dialog_add_button(d, gtk.STOCK_CANCEL, _('_Cancel'), 1, self.plugAddDialogClose) + dialog_add_button(d, gtk.STOCK_OK, _('_OK'), 2, self.plugAddDialogOK) + ### + treev = gtk.TreeView() + trees = gtk.ListStore(str) + treev.set_model(trees) + #treev.enable_model_drag_source(gdk.ModifierType.BUTTON1_MASK, [('', 0, 0, 0)], gdk.DragAction.MOVE)## FIXME + #treev.enable_model_drag_dest([('', 0, 0, 0)], gdk.DragAction.MOVE)## FIXME + treev.connect('drag_data_received', self.plugTreevDragReceived) + treev.connect('row-activated', self.plugAddTreevRActivate) + #### + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('Title'), cell, text=0) + #col.set_resizable(True)# no need when have only one column! + treev.append_column(col) + #### + swin = gtk.ScrolledWindow() + swin.add(treev) + swin.set_policy(gtk.PolicyType.AUTOMATIC, gtk.PolicyType.AUTOMATIC) + pack(d.vbox, swin, 1, 1) + d.vbox.show_all() + self.plugAddDialog = d + self.plugAddTreeview = treev + self.plugAddTreestore = trees + ############# + ##treev.set_resize_mode(gtk.RESIZE_IMMEDIATE) + ##self.plugAddItems = [] + ####################################### Tab 5 (Accounts) + vbox = gtk.VBox() + vbox.label = _('Accounts') + vbox.icon = 'web-settings.png' + self.prefPages.append(vbox) + ##### + treev = gtk.TreeView() + treev.set_headers_clickable(True) + trees = gtk.ListStore(int, bool, str)## id (hidden), enable, title + treev.set_model(trees) + treev.enable_model_drag_source(gdk.ModifierType.BUTTON1_MASK, [('row', gtk.TargetFlags.SAME_WIDGET, 0)], gdk.DragAction.MOVE) + treev.enable_model_drag_dest([('row', gtk.TargetFlags.SAME_WIDGET, 0)], gdk.DragAction.MOVE) + treev.connect('row-activated', self.accountsTreevRActivate) + treev.connect('button-press-event', self.accountsTreevButtonPress) + ### + swin = gtk.ScrolledWindow() + swin.add(treev) + swin.set_policy(gtk.PolicyType.AUTOMATIC, gtk.PolicyType.AUTOMATIC) + ###### + cell = gtk.CellRendererToggle() + #cell.set_property('activatable', True) + cell.connect('toggled', self.accountsTreeviewCellToggled) + col = gtk.TreeViewColumn(_('Enable'), cell) + col.add_attribute(cell, 'active', 1) + #cell.set_active(False) + col.set_resizable(True) + col.set_property('expand', False) + treev.append_column(col) + ###### + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(_('Title'), cell, text=2) + #col.set_resizable(True)## No need! + col.set_property('expand', True) + treev.append_column(col) + ###### + self.accountsTreeview = treev + self.accountsTreestore = trees + ####################### + hbox = gtk.HBox() + vboxPlug = gtk.VBox() + pack(vboxPlug, swin, 1, 1) + pack(hbox, vboxPlug, 1, 1) + ### + toolbar = gtk.Toolbar() + toolbar.set_orientation(gtk.Orientation.VERTICAL) + #try:## DeprecationWarning ## FIXME + #toolbar.set_icon_size(gtk.IconSize.SMALL_TOOLBAR) + ### no different (argument to set_icon_size does not affect) FIXME + #except: + # pass + size = gtk.IconSize.SMALL_TOOLBAR + ##no different(argument2 to image_new_from_stock does not affect) FIXME + ######## gtk.IconSize.SMALL_TOOLBAR or gtk.IconSize.MENU + tb = toolButtonFromStock(gtk.STOCK_EDIT, size) + set_tooltip(tb, _('Edit')) + tb.connect('clicked', self.accountsEditClicked) + toolbar.insert(tb, -1) + ########### + tb = toolButtonFromStock(gtk.STOCK_ADD, size) + set_tooltip(tb, _('Add')) + tb.connect('clicked', self.accountsAddClicked) + toolbar.insert(tb, -1) + ########### + tb = toolButtonFromStock(gtk.STOCK_DELETE, size) + set_tooltip(tb, _('Delete')) + tb.connect('clicked', self.accountsDelClicked) + toolbar.insert(tb, -1) + ########## + tb = toolButtonFromStock(gtk.STOCK_GO_UP, size) + set_tooltip(tb, _('Move up')) + tb.connect('clicked', self.accountsUpClicked) + toolbar.insert(tb, -1) + ######### + tb = toolButtonFromStock(gtk.STOCK_GO_DOWN, size) + set_tooltip(tb, _('Move down')) + tb.connect('clicked', self.accountsDownClicked) + toolbar.insert(tb, -1) + ########### + pack(hbox, toolbar) + pack(vbox, hbox, 1, 1) + ################################################################################################### + notebook = gtk.Notebook() + self.notebook = notebook + ##################################### + for vbox in self.prefPages: + l = gtk.Label(vbox.label) + l.set_use_underline(True) + vb = gtk.VBox(spacing=3) + pack(vb, imageFromFile(vbox.icon)) + pack(vb, l) + vb.show_all() + notebook.append_page(vbox, vb) + try: + notebook.set_tab_reorderable(vbox, True) + except AttributeError: + pass + ####################### + #notebook.set_property('homogeneous', True)## not in gtk3 FIXME + #notebook.set_property('tab-border', 5)## not in gtk3 FIXME + #notebook.set_property('tab-hborder', 15)## not in gtk3 FIXME + pack(self.vbox, notebook) + self.vbox.show_all() + for i in ui.prefPagesOrder: + try: + j = ui.prefPagesOrder[i] + except IndexError: + continue + notebook.reorder_child(self.prefPages[i], j) + def comboFirstWDChanged(self, combo): + f = self.comboFirstWD.get_active() ## 0 means Sunday + if f==7: ## auto + try: + f = core.getLocaleFirstWeekDay() + except: + pass + ## core.firstWeekDay will be later = f + self.holiWDItem.setStart(f) + def onDelete(self, obj=None, data=None): + self.hide() + return True + def ok(self, widget): + self.hide() + self.apply() + def cancel(self, widget=None): + self.hide() + self.updatePrefGui() + return True + getAllPrefItems = lambda self: self.moduleOptions + self.localePrefItems + self.corePrefItems +\ + self.uiPrefItems + self.gtkPrefItems + def apply(self, widget=None): + from scal3.ui_gtk.font_utils import gfontDecode + ####### FIXME + #print('fontDefault = %s'%ui.fontDefault) + ui.fontDefault = gfontDecode(ud.settings.get_property('gtk-font-name')) + #print('fontDefault = %s'%ui.fontDefault) + ############################################## Updating pref variables + for opt in self.getAllPrefItems(): + opt.updateVar() + ###### DB Manager (Plugin Manager) + index = [] + for row in self.plugTreestore: + plugI = row[0] + enable = row[1] + show_date = row[2] + index.append(plugI) + plug = core.allPlugList[plugI] + if plug.loaded: + try: + plug.enable = enable + plug.show_date = show_date + except: + core.myRaise(__file__) + print(i, core.plugIndex) + else: + if enable: + plug = plugin_man.loadPlugin(plug.file, enable=True) + if plug: + assert plug.loaded + core.allPlugList[plugI] = plug + core.plugIndex = index + core.updatePlugins() + ###### + first = self.comboFirstWD.get_active() + if first==7: + core.firstWeekDayAuto = True + try: + core.firstWeekDay = core.getLocaleFirstWeekDay() + except: + pass + else: + core.firstWeekDayAuto = False + core.firstWeekDay = first + ###### + mode = self.comboWeekYear.get_active() + if mode==8: + core.weekNumberModeAuto = True + core.weekNumberMode = core.getLocaleweekNumberMode() + else: + core.weekNumberModeAuto = False + core.weekNumberMode = mode + ###### + ui.cellCache.clear() ## Very important, specially when calTypes.primary will be changed + ###### + ud.updateFormatsBin() + #################################################### Saving Preferences + for mod in calTypes: + mod.save() + ##################### Saving locale config + locale_man.saveConf() + ##################### Saving core config + core.version = core.VERSION + core.saveConf() + del core.version + ##################### Saving ui config + ui.prefPagesOrder = tuple([ + self.notebook.page_num(page) for page in self.prefPages + ]) + ui.saveConf() + ##################### Saving gtk_ud config + ud.saveConf() + ################################################### Updating GUI + ud.windowList.onConfigChange() + if ui.mainWin: + """ + if ui.bgUseDesk and ui.bgColor[3]==255: + msg = gtk.MessageDialog(buttons=gtk.ButtonsType.OK_CANCEL, message_format=_( + 'If you want to have a transparent calendar (and see your desktop),'+\ + 'change the opacity of calendar background color!')) + if msg.run()==gtk.ResponseType.OK: + self.colorbBg.emit('clicked') + msg.destroy() + """ + if ui.checkNeedRestart(): + d = gtk.Dialog( + title=_('Need Restart '+core.APP_DESC), + parent=self, + flags=gtk.DialogFlags.MODAL | gtk.DialogFlags.DESTROY_WITH_PARENT, + buttons=(gtk.STOCK_CANCEL, 0), + ) + d.set_keep_above(True) + label = gtk.Label(_('Some preferences need for restart %s to apply.'%core.APP_DESC)) + label.set_line_wrap(True) + pack(d.vbox, label) + resBut = d.add_button(_('_Restart'), 1) + resBut.set_image(gtk.Image.new_from_stock(gtk.STOCK_REFRESH,gtk.IconSize.BUTTON)) + resBut.grab_default() + d.vbox.show_all() + if d.run()==1: + core.restart() + else: + d.destroy() + def updatePrefGui(self):############### Updating Pref Gui (NOT MAIN GUI) + for opt in self.getAllPrefItems(): + opt.updateWidget() + ############################### + if core.firstWeekDayAuto: + self.comboFirstWD.set_active(7) + else: + self.comboFirstWD.set_active(core.firstWeekDay) + if core.weekNumberModeAuto: + self.comboWeekYear.set_active(8) + else: + self.comboWeekYear.set_active(core.weekNumberMode) + ###### Plugin Manager + self.plugTreestore.clear() + for row in core.getPluginsTable(): + self.plugTreestore.append(row) + self.plugAddItems = [] + self.plugAddTreestore.clear() + for (i, title) in core.getDeletedPluginsTable(): + self.plugAddItems.append(i) + self.plugAddTreestore.append([title]) + self.plugButtonAdd.set_sensitive(True) + ###### Accounts + self.accountsTreestore.clear() + for account in ui.eventAccounts: + self.accountsTreestore.append([account.id, account.enable, account.title]) + #def plugTreevExpose(self, widget, gevent): + #self.plugTitleCell.set_property('wrap-width', self.plugTitleCol.get_width()+2) + def plugTreevCursorChanged(self, selection): + cur = self.plugTreeview.get_cursor()[0] + if cur==None: + return + i = cur[0] + j = self.plugTreestore[i][0] + plug = core.allPlugList[j] + self.plugButtonAbout.set_sensitive(plug.about!=None) + self.plugButtonConf.set_sensitive(plug.hasConfig) + def plugAboutClicked(self, obj=None): + from scal3.ui_gtk.about import AboutDialog + cur = self.plugTreeview.get_cursor()[0] + if cur==None: + return + i = cur[0] + j = self.plugTreestore[i][0] + plug = core.allPlugList[j] + if hasattr(plug, 'open_about'): + return plug.open_about() + if plug.about==None: + return + about = AboutDialog( + name='',## FIXME + title=_('About Plugin'),## _('About ')+plug.title + authors=plug.authors, + comments=plug.about, + ) + about.set_transient_for(self) + about.connect('delete-event', lambda w, e: w.destroy()) + about.connect('response', lambda w, e: w.destroy()) + #about.set_resizable(True) + #about.vbox.show_all()## OR about.vbox.show_all() ; about.run() + openWindow(about)## FIXME + def plugConfClicked(self, obj=None): + cur = self.plugTreeview.get_cursor()[0] + if cur==None: + return + i = cur[0] + j = self.plugTreestore[i][0] + plug = core.allPlugList[j] + if not plug.hasConfig: + return + plug.open_configure() + def plugExportToIcsClicked(self, menu, plug): + from scal3.ui_gtk.export import ExportToIcsDialog + ExportToIcsDialog(plug.exportToIcs, plug.title).run() + def plugTreevRActivate(self, treev, path, col): + if col.get_title()==_('Title'):## FIXME + self.plugAboutClicked(None) + def plugTreevButtonPress(self, widget, gevent): + b = gevent.button + if b==3: + cur = self.plugTreeview.get_cursor()[0] + if cur: + i = cur[0] + j = self.plugTreestore[i][0] + plug = core.allPlugList[j] + menu = gtk.Menu() + ## + item = labelStockMenuItem('_About', gtk.STOCK_ABOUT, self.plugAboutClicked) + item.set_sensitive(bool(plug.about)) + menu.add(item) + ## + item = labelStockMenuItem('_Configure', gtk.STOCK_PREFERENCES, self.plugConfClicked) + item.set_sensitive(plug.hasConfig) + menu.add(item) + ## + menu.add(labelImageMenuItem(_('Export to %s')%'iCalendar', 'ical-32.png', self.plugExportToIcsClicked, plug)) + ## + menu.show_all() + self.tmpMenu = menu + menu.popup(None, None, None, None, 3, gevent.time) + return True + return False + def plugAddClicked(self, button): + ## FIXME + ## Reize window to show all texts + #self.plugAddTreeview.columns_autosize() ## FIXME + r, x, y, w, h = self.plugAddTreeview.get_column(0).cell_get_size() + #print(r[2], r[3], x, y, w, h) + self.plugAddDialog.resize(w+30, 75 + 30*len(self.plugAddTreestore)) + ############### + self.plugAddDialog.run() + #self.plugAddDialog.present() + #self.plugAddDialog.show() + def plugAddDialogClose(self, obj, gevent=None): + self.plugAddDialog.hide() + return True + def plugTreeviewCellToggled(self, cell, path): + i = int(path) + #cur = self.plugTreeview.get_cursor()[0] + #if cur==None or i!=cur[0]:## FIXME + # return + active = not cell.get_active() + self.plugTreestore[i][1] = active + cell.set_active(active) + def plugTreeviewCellToggled2(self, cell, path): + i = int(path) + #cur = self.plugTreeview.get_cursor()[0] + #if cur==None or i!=cur[0]:## FIXME + # return + active = not cell.get_active() + self.plugTreestore[i][2] = active + cell.set_active(active) + def plugTreeviewTop(self, button): + cur = self.plugTreeview.get_cursor()[0] + if cur==None: + return + i = cur[0] + t = self.plugTreestore + if i<=0 or i>=len(t): + gdk.beep() + return + t.prepend(list(t[i])) + t.remove(t.get_iter(i+1)) + self.plugTreeview.set_cursor(0) + def plugTreeviewBottom(self, button): + cur = self.plugTreeview.get_cursor()[0] + if cur==None: + return + i = cur[0] + t = self.plugTreestore + if i<0 or i>=len(t)-1: + gdk.beep() + return + t.append(list(t[i])) + t.remove(t.get_iter(i)) + self.plugTreeview.set_cursor(len(t)-1) + def plugTreeviewUp(self, button): + cur = self.plugTreeview.get_cursor()[0] + if cur==None: + return + i = cur[0] + t = self.plugTreestore + if i<=0 or i>=len(t): + gdk.beep() + return + t.swap(t.get_iter(i-1), t.get_iter(i)) + self.plugTreeview.set_cursor(i-1) + def plugTreeviewDown(self, button): + cur = self.plugTreeview.get_cursor()[0] + if cur==None: + return + i = cur[0] + t = self.plugTreestore + if i<0 or i>=len(t)-1: + gdk.beep() + return + t.swap(t.get_iter(i), t.get_iter(i+1)) + self.plugTreeview.set_cursor(i+1) + def plugTreevDragReceived(self, treev, context, x, y, selec, info, etime): + t = treev.get_model() #self.plugAddTreestore + cur = treev.get_cursor()[0] + if cur==None: + return + i = cur[0] + dest = treev.get_dest_row_at_pos(x, y) + if dest == None: + t.move_after(t.get_iter(i), t.get_iter(len(t)-1)) + elif dest[1] in (gtk.TreeViewDropPosition.BEFORE, gtk.TreeViewDropPosition.INTO_OR_BEFORE): + t.move_before(t.get_iter(i), t.get_iter(dest[0][0])) + else: + t.move_after(t.get_iter(i), t.get_iter(dest[0][0])) + def plugTreeviewDel(self, button): + cur = self.plugTreeview.get_cursor()[0] + if cur==None: + return + i = cur[0] + t = self.plugTreestore + n = len(t) + if i<0 or i>=n: + gdk.beep() + return + j = t[i][0] + t.remove(t.get_iter(i)) + ### j + self.plugAddItems.append(j) + title = core.allPlugList[j].title + self.plugAddTreestore.append([title]) + if core.debugMode: + print('deleting %s'%title) + self.plugButtonAdd.set_sensitive(True) + if n>1: + self.plugTreeview.set_cursor(min(n-2, i)) + def plugAddDialogOK(self, obj): + cur = self.plugAddTreeview.get_cursor()[0] + if cur==None: + gdk.beep() + return + i = cur[0] + j = self.plugAddItems[i] + cur2 = self.plugTreeview.get_cursor()[0] + if cur2==None: + pos = len(self.plugTreestore) + else: + pos = cur2[0]+1 + self.plugTreestore.insert(pos, [j, True, False, core.allPlugList[j].title]) + self.plugAddTreestore.remove(self.plugAddTreestore.get_iter(i)) + self.plugAddItems.pop(i) + self.plugAddDialog.hide() + self.plugTreeview.set_cursor(pos)### pos==1- ## FIXME + def plugAddTreevRActivate(self, treev, path, col): + self.plugAddDialogOK(None)## FIXME + def editAccount(self, index): + from scal3.ui_gtk.event.account_op import AccountEditorDialog + accountId = self.accountsTreestore[index][0] + account = ui.eventAccounts[accountId] + if not account.loaded: + showError(_('Account must be enabled before editing'), self) + return + account = AccountEditorDialog(account, parent=self).run() + if account is None: + return + account.save() + ui.eventAccounts.save() + self.accountsTreestore[index][2] = account.title + def accountsEditClicked(self, button): + cur = self.accountsTreeview.get_cursor()[0] + if cur==None: + return + index = cur[0] + self.editAccount(index) + def accountsAddClicked(self, button): + from scal3.ui_gtk.event.account_op import AccountEditorDialog + account = AccountEditorDialog(parent=self).run() + if account is None: + return + account.save() + ui.eventAccounts.append(account) + ui.eventAccounts.save() + self.accountsTreestore.append([account.id, account.enable, account.title]) + def accountsDelClicked(self, button): + cur = self.accountsTreeview.get_cursor()[0] + if cur==None: + return + index = cur[0] + accountId = self.accountsTreestore[index][0] + account = ui.eventAccounts[accountId] + if not confirm(_('Do you want to delete account "%s"')%account.title, parent=self): + return + ui.eventAccounts.delete(account) + del self.accountsTreestore[index] + def accountsUpClicked(self, button): + cur = self.accountsTreeview.get_cursor()[0] + if cur==None: + return + index = cur[0] + t = self.accountsTreestore + if index<=0 or index>=len(t): + gdk.beep() + return + ui.eventAccounts.moveUp(index) + ui.eventAccounts.save() + t.swap(t.get_iter(index-1), t.get_iter(index)) + self.accountsTreeview.set_cursor(index-1) + def accountsDownClicked(self, button): + cur = self.accountsTreeview.get_cursor()[0] + if cur==None: + return + index = cur[0] + t = self.accountsTreestore + if index<0 or index>=len(t)-1: + gdk.beep() + return + ui.eventAccounts.moveDown(index) + ui.eventAccounts.save() + t.swap(t.get_iter(index), t.get_iter(index+1)) + self.accountsTreeview.set_cursor(index+1) + def accountsTreevRActivate(self, treev, path, col): + index = path[0] + self.editAccount(index) + def accountsTreevButtonPress(self, widget, gevent): + b = gevent.button + if b==3: + cur = self.accountsTreeview.get_cursor()[0] + if cur: + index = cur[0] + accountId = self.accountsTreestore[index][0] + account = ui.eventAccounts[accountId] + menu = gtk.Menu() + ## + ## FIXME + ## + #menu.show_all() + #self.tmpMenu = menu + #menu.popup(None, None, None, None, 3, gevent.time) + return True + return False + def accountsTreeviewCellToggled(self, cell, path): + index = int(path) + active = not cell.get_active() + ### + accountId = self.accountsTreestore[index][0] + account = ui.eventAccounts[accountId] + if not account.loaded:## it's a dummy account + if active: + account = ui.eventAccounts.replaceDummyObj(account) + if account is None: + return + account.enable = active + account.save() + ### + self.accountsTreestore[index][1] = active + cell.set_active(active) + + + +if __name__=='__main__': + dialog = PrefDialog(0) + dialog.updatePrefGui() + dialog.run() + + diff --git a/scal3/ui_gtk/selectdate.py b/scal3/ui_gtk/selectdate.py new file mode 100644 index 000000000..970fa474b --- /dev/null +++ b/scal3/ui_gtk/selectdate.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import os, sys + +from scal3.cal_types import calTypes, convert +from scal3 import core +from scal3.core import getMonthName +from scal3.locale_man import tr as _ +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.utils import openWindow, dialog_add_button +from scal3.ui_gtk.mywidgets.cal_type_combo import CalTypeCombo +from scal3.ui_gtk.mywidgets.multi_spin.option_box.date import DateButtonOption +from scal3.ui_gtk.mywidgets.ymd import YearMonthDayBox + + + +@registerSignals +class SelectDateDialog(gtk.Dialog): + signals = [ + ('response-date', [int, int, int]), + ] + def __init__(self, **kwargs): + gtk.Dialog.__init__(self, **kwargs) + self.set_title(_('Select Date...')) + #self.set_has_separator(False) + #self.set_skip_taskbar_hint(True) + self.connect('delete-event', self.hideMe) + self.mode = calTypes.primary + ###### Reciving dropped day! + self.drag_dest_set( + gtk.DestDefaults.ALL, + (), + gdk.DragAction.COPY, + ) + self.drag_dest_add_text_targets() + self.connect('drag-data-received', self.dragRec) + ###### + hb0 = gtk.HBox(spacing=4) + pack(hb0, gtk.Label(_('Date Mode'))) + combo = CalTypeCombo() + combo.set_active(self.mode) + pack(hb0, combo) + pack(self.vbox, hb0) + ####################### + hbox = gtk.HBox(spacing=5) + rb1 = gtk.RadioButton.new_with_label(None, '') + rb1.num = 1 + pack(hbox, rb1) + self.ymdBox = YearMonthDayBox() + pack(hbox, self.ymdBox) + pack(self.vbox, hbox) + ######## + hb2 = gtk.HBox(spacing=4) + pack(hb2, gtk.Label('yyyy/mm/dd')) + dateInput = DateButtonOption(hist_size=16) + pack(hb2, dateInput) + rb2 = gtk.RadioButton.new_with_label_from_widget(rb1, '') + rb2.num = 2 + #rb2.set_group([rb1]) + hb2i = gtk.HBox(spacing=5) + pack(hb2i, rb2) + pack(hb2i, hb2) + pack(self.vbox, hb2i) + ####### + dialog_add_button(self, gtk.STOCK_CANCEL, _('_Cancel'), 2, self.hideMe) + dialog_add_button(self, gtk.STOCK_OK, _('_OK'), 1, self.ok) + ####### + self.comboMode = combo + self.dateInput = dateInput + self.radio1 = rb1 + self.radio2 = rb2 + self.hbox2 = hb2 + ####### + combo.connect ('changed', self.comboModeChanged) + rb1.connect_after('clicked', self.radioChanged) + rb2.connect_after('clicked', self.radioChanged) + dateInput.connect('activate', self.ok) + self.radioChanged() + ####### + self.vbox.show_all() + self.resize(1, 1) + def dragRec(self, obj, context, x, y, selection, target_id, etime): + text = selection.get_text() + if text==None: + return + date = ui.parseDroppedDate(text) + if date==None: + print('selectDateDialog: dropped text "%s"'%text) + return + print('selectDateDialog: dropped date: %d/%d/%d'%date) + mode = self.comboMode.get_active() + if mode!=ui.dragGetMode: + date = convert(date[0], date[1], date[2], ui.dragGetMode, mode) + self.dateInput.set_value(date) + self.dateInput.add_history() + return True + def show(self): + ## Show a window that ask the date and set on the calendar + mode = calTypes.primary + y, m, d = ui.cell.dates[mode] + self.set_mode(mode) + self.set(y, m, d) + openWindow(self) + def hideMe(self, widget, event=None): + self.hide() + return True + def set(self, y, m, d): + self.ymdBox.set_value((y, m, d)) + self.dateInput.set_value((y, m, d)) + self.dateInput.add_history() + def set_mode(self, mode): + self.mode = mode + module = calTypes[mode] + self.comboMode.set_active(mode) + self.ymdBox.set_mode(mode) + self.dateInput.setMaxDay(module.maxMonthLen) + def comboModeChanged(self, widget=None): + pMode = self.mode + pDate = self.get() + mode = self.comboMode.get_active() + module = calTypes[mode] + if pDate==None: + y, m, d = ui.cell.dates[mode] + else: + y0, m0, d0 = pDate + y, m, d = convert(y0, m0, d0, pMode, mode) + self.ymdBox.set_mode(mode) + self.dateInput.setMaxDay(module.maxMonthLen) + self.set(y, m, d) + self.mode = mode + def get(self): + mode = self.comboMode.get_active() + if self.radio1.get_active(): + y0, m0, d0 = self.ymdBox.get_value() + elif self.radio2.get_active(): + y0, m0, d0 = self.dateInput.get_value() + return (y0, m0, d0) + def ok(self, widget): + mode = self.comboMode.get_active() + if mode==None: + return + get = self.get() + if get==None: + return + y0, m0, d0 = get + if mode==calTypes.primary: + y, m, d = (y0, m0, d0) + else: + y, m, d = convert(y0, m0, d0, mode, calTypes.primary) + #if not core.validDate(mode, y, m, d): + # print('bad date', mode, y, m, d) + # return + self.emit('response-date', y, m, d) + self.hide() + self.dateInput.set_value((y0, m0, d0)) + self.dateInput.add_history() + def radioChanged(self, widget=None): + if self.radio1.get_active(): + self.ymdBox.set_sensitive(True) + self.hbox2.set_sensitive(False) + else: + self.ymdBox.set_sensitive(False) + self.hbox2.set_sensitive(True) + + + + diff --git a/scal3/ui_gtk/simplemonthcal.py b/scal3/ui_gtk/simplemonthcal.py new file mode 100644 index 000000000..e69de29bb diff --git a/scal3/ui_gtk/starcal-static.py b/scal3/ui_gtk/starcal-static.py new file mode 100644 index 000000000..dc913b0a3 --- /dev/null +++ b/scal3/ui_gtk/starcal-static.py @@ -0,0 +1 @@ +## a static bitmap monthly calendar (like a printed calendar) diff --git a/scal3/ui_gtk/starcal.py b/scal3/ui_gtk/starcal.py new file mode 100755 index 000000000..0b4a691bb --- /dev/null +++ b/scal3/ui_gtk/starcal.py @@ -0,0 +1,1054 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import sys + +if sys.version_info[0] != 3: + print('Run this script with Python 3.x') + sys.exit(1) + +from time import time as now +from time import localtime +import os +import os.path +from os.path import join, dirname, isdir + +sys.path.insert(0, dirname(dirname(dirname(__file__)))) + +from scal3.path import * +from scal3.utils import myRaise + +if not isdir(confDir): + from scal3.utils import restartLow + try: + __import__('scal3.ui_gtk.import_config_2to3') + except: + myRaise() + if not isdir(confDir): + os.mkdir(confDir, 0o755) + else: + restartLow() + +from scal3.utils import versionLessThan +from scal3.cal_types import calTypes +from scal3 import core + +from scal3 import locale_man +from scal3.locale_man import rtl, lang ## import scal3.locale_man after core +#_ = locale_man.loadTranslator(False)## FIXME +from scal3.locale_man import tr as _ +from scal3 import event_lib +from scal3 import ui + +import gi.repository.GObject as gobject +from gi.repository import GdkPixbuf + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import registerSignals +from scal3.ui_gtk.utils import * +#from scal3.ui_gtk.color_utils import rgbToGdkColor +from scal3.ui_gtk import listener +from scal3.ui_gtk import gtk_ud as ud +from scal3.ui_gtk.customize import DummyCalObj, CustomizableCalBox +from scal3.ui_gtk.event.utils import checkEventsReadOnly + +ui.uiName = 'gtk' + + +mainWinItemsDesc = { + 'dayCal': _('Day Calendar'), + 'eventDayView': _('Events of Day'), + 'labelBox': _('Year & Month Labels'), + 'monthCal': _('Month Calendar'), + 'pluginsText': _('Plugins Text'), + 'seasonPBar': _('Season Progress Bar'), + 'statusBar': _('Status Bar'), + 'toolbar': _('Toolbar'), + 'weekCal': _('Week Calendar'), + 'winContronller': _('Window Controller'), +} + + + + +#def show_event(widget, gevent): +# print(type(widget), gevent.type.value_name, gevent.get_value())#, gevent.send_event + + +def liveConfChanged(): + tm = now() + if tm-ui.lastLiveConfChangeTime > ui.saveLiveConfDelay: + gobject.timeout_add(int(ui.saveLiveConfDelay*1000), ui.saveLiveConfLoop) + ui.lastLiveConfChangeTime = tm + + +# How to define icon of custom stock???????????? +#gtk.stock_add(( +#('gtk-evolution', 'E_volution', gdk.ModifierType.BUTTON1_MASK, 0, 'gtk20') + + + + + + + + +@registerSignals +class MainWinVbox(gtk.VBox, CustomizableCalBox): + _name = 'mainWin' + desc = _('Main Window') + params = ( + 'ui.mainWinItems', + 'ui.winControllerButtons', + 'ui.mcalHeight', + 'ui.mcalLeftMargin', + 'ui.mcalTopMargin', + 'ui.mcalTypeParams', + 'ui.mcalGrid', + 'ui.mcalGridColor', + 'ui.wcalHeight', + 'ui.wcalTextSizeScale', + 'ui.wcalItems', + 'ui.wcalGrid', + 'ui.wcalGridColor', + 'ud.wcalToolbarData', + 'ui.wcal_toolbar_mainMenu_icon', + 'ui.wcal_weekDays_width', + 'ui.wcalFont_weekDays', + 'ui.wcalFont_pluginsText', + 'ui.wcal_eventsIcon_width', + 'ui.wcal_eventsText_showDesc', + 'ui.wcal_eventsText_colorize', + 'ui.wcalFont_eventsText', + 'ui.wcal_daysOfMonth_dir', + 'ui.wcalTypeParams', + 'ui.wcal_daysOfMonth_width', + 'ui.wcal_eventsCount_expand', + 'ui.wcal_eventsCount_width', + 'ui.wcalFont_eventsBox', + 'ui.dcalHeight', + 'ui.dcalTypeParams', + 'ui.pluginsTextInsideExpander', + 'ud.mainToolbarData', + ) + def __init__(self): + gtk.VBox.__init__(self) + self.initVars() + def updateVars(self): + CustomizableCalBox.updateVars(self) + ui.mainWinItems = self.getItemsData() + def keyPress(self, arg, gevent): + CustomizableCalBox.keyPress(self, arg, gevent) + return True ## FIXME + def switchWcalMcal(self): + wi = None + mi = None + for i, item in enumerate(self.items): + if item._name == 'weekCal': + wi = i + elif item._name == 'monthCal': + mi = i + wcal, mcal = self.items[wi], self.items[mi] + wcal.enable, mcal.enable = mcal.enable, wcal.enable + ## FIXME + #self.reorder_child(wcal, mi) + #self.reorder_child(mcal, wi) + #self.items[wi], self.items[mi] = mcal, wcal + self.showHide() + self.onDateChange() + + +@registerSignals +#class MainWin(gtk.ApplicationWindow, ud.IntegratedCalObj): +class MainWin(gtk.Window, ud.BaseCalObj): + _name = 'mainWin' + desc = _('Main Window') + timeout = 1 ## second + setMinHeight = lambda self: self.resize(ui.winWidth, 2) + #def maximize(self): + # pass + def __init__(self, statusIconMode=2): + #from gi.repository import Gio + #self.app = gtk.Application(application_id="apps.starcal") + #self.app.register(Gio.Cancellable.new()) + #gtk.ApplicationWindow.__init__(self, application=self.app) + gtk.Window.__init__(self) + self.add_events(gdk.EventMask.ALL_EVENTS_MASK) + self.initVars() + ud.windowList.appendItem(self) + ui.mainWin = self + ################## + ## statusIconMode: + ## ('none', 'none') + ## ('statusIcon', 'normal') + ## ('applet', 'gnome') + ## ('applet', 'kde') + ## + ## 0: none (simple window) + ## 1: applet + ## 2: standard status icon + self.statusIconMode = statusIconMode + ### + #ui.eventManDialog = None + #ui.timeLineWin = None + ### + #ui.weekCalWin = WeekCalWindow() + #ud.windowList.appendItem(ui.weekCalWin) + ### + self.dayInfoDialog = None + #print('windowList.items', [item._name for item in ud.windowList.items]) + ########### + ##self.connect('window-state-event', selfStateEvent) + self.set_title('%s %s'%(core.APP_DESC, core.VERSION)) + #self.connect('main-show', lambda arg: self.present()) + #self.connect('main-hide', lambda arg: self.hide()) + self.set_decorated(False) + self.set_property('skip-taskbar-hint', not ui.winTaskbar) ## self.set_skip_taskbar_hint ## FIXME + self.set_role('starcal') + #self.set_focus_on_map(True)#???????? + #self.set_type_hint(gdk.WindowTypeHint.NORMAL) + #self.connect('realize', self.onRealize) + self.set_default_size(ui.winWidth, 1) + try: + self.move(ui.winX, ui.winY) + except: + pass + ############################################################# + self.connect('focus-in-event', self.focusIn, 'Main') + self.connect('focus-out-event', self.focusOut, 'Main') + self.connect('button-press-event', self.buttonPress) + self.connect('key-press-event', self.keyPress) + self.connect('configure-event', self.configureEvent) + self.connect('destroy', self.quit) + ############################################################# + """ + #self.add_events(gdk.EventMask.VISIBILITY_NOTIFY_MASK) + #self.connect('frame-event', show_event) + ## Compiz does not send configure-event(or any event) when MOVING window(sends in last point, + ## when moving completed) + #self.connect('drag-motion', show_event) + ud.rootWindow.set_events(... + ud.rootWindow.add_filter(self.onRootWinEvent) + #self.realize() + #gdk.flush() + #self.configureEvent(None, None) + #self.connect('drag-motion', show_event) + ###################### + ## ???????????????????????????????????????????????? + ## when button is down(before button-release-event), motion-notify-event does not recived! + """ + ################################################################## + self.focus = False + #self.focusOutTime = 0 + #self.clockTr = None + ############################################################################ + self.winCon = None + ############ + self.vbox = MainWinVbox() + ui.checkMainWinItems() + itemsPkg = 'scal3.ui_gtk.mainwin_items' + for (name, enable) in ui.mainWinItems: + #print(name, enable) + if enable: + try: + module = __import__( + '.'.join([ + itemsPkg, + name, + ]), + fromlist=['CalObj'], + ) + CalObj = module.CalObj + except: + myRaise() + continue + item = CalObj() + item.enable = enable + item.connect('size-allocate', self.childSizeAllocate) + #modify_bg_all(item, gtk.StateType.NORMAL, rgbToGdkColor(*ui.bgColor)) + else: + desc = mainWinItemsDesc[name] + item = DummyCalObj(name, desc, itemsPkg, True) + self.vbox.appendItem(item) + self.appendItem(self.vbox) + self.vbox.show() + self.customizeDialog = None + ####### + self.add(self.vbox) + #################### + self.isMaximized = False + #################### + #ui.prefDialog = None + self.exportDialog = None + self.selectDateDialog = None + ############### Building About Dialog + self.aboutDialog = None + ############### + self.menuMain = None + ##### + check = gtk.CheckMenuItem(label=_('_On Top')) + check.set_use_underline(True) + check.connect('activate', self.keepAboveClicked) + check.set_active(ui.winKeepAbove) + self.set_keep_above(ui.winKeepAbove) + self.checkAbove = check + ##### + check = gtk.CheckMenuItem(label=_('_Sticky')) + check.set_use_underline(True) + check.connect('activate', self.stickyClicked) + check.set_active(ui.winSticky) + if ui.winSticky: + self.stick() + self.checkSticky = check + ############################################################ + self.statusIconInit() + listener.dateChange.add(self) + #if self.statusIconMode!=1: + # gobject.timeout_add_seconds(self.timeout, self.statusIconUpdate) + ######### + self.connect('delete-event', self.onDeleteEvent) + ######################################### + for plug in core.allPlugList: + if plug.external: + try: + plug.set_dialog(self) + except AttributeError: + pass + ########################### + self.onConfigChange() + #ud.rootWindow.set_cursor(gdk.Cursor.new(gdk.CursorType.LEFT_PTR)) + #def mainWinStateEvent(self, obj, gevent): + #print(dir(event)) + #print(gevent.new_window_state) + #self.event = event + def childSizeAllocate(self, cal, req): + self.setMinHeight() + def selectDateResponse(self, widget, y, m, d): + ui.changeDate(y, m, d) + self.onDateChange() + def keyPress(self, arg, gevent): + kname = gdk.keyval_name(gevent.keyval).lower() + #print(now(), 'MainWin.keyPress', kname) + if kname=='escape': + self.onEscape() + elif kname=='f1': + self.aboutShow() + elif kname in ('insert', 'plus', 'kp_add'): + self.eventManShow() + elif kname in ('q', 'arabic_dad'):## FIXME + self.quit() + else: + self.vbox.keyPress(arg, gevent) + return True ## FIXME + def focusIn(self, widegt, event, data=None): + self.focus = True + if self.winCon and self.winCon.enable: + self.winCon.windowFocusIn() + def focusOut(self, widegt, event, data=None): + ## called 0.0004 sec (max) after focusIn (if switched between two windows) + dt = now()-ui.focusTime + #print('focusOut', dt) + if dt > 0.05: ## FIXME + self.focus = False + gobject.timeout_add(2, self.focusOutDo) + def focusOutDo(self): + if not self.focus:# and t-self.focusOutTime>0.002: + ab = self.checkAbove.get_active() + self.set_keep_above(ab) + if self.winCon and self.winCon.enable: + self.winCon.windowFocusOut() + return False + + """ + def checkResize(self, widget, req): + if ui.mcalHeight != req.height:# and ui.winWidth==req.width: + if req.height==0: + req.height=1 + ui.mcalHeight = req.height + """ + def configureEvent(self, widget, gevent): + wx, wy = self.get_position() + maxPosDelta = max(abs(ui.winX-wx), abs(ui.winY-wy)) + #print(wx, wy) + ww, wh = self.get_size() + #if ui.bgUseDesk and maxPosDelta > 1:## FIXME + # self.queue_draw() + if self.get_property('visible'): + ui.winX, ui.winY = (wx, wy) + ui.winWidth = ww + liveConfChanged() + return False + def buttonPress(self, obj, gevent): + print('buttonPress') + b = gevent.button + #print('buttonPress', b) + if b==3: + self.menuMainCreate() + self.menuMain.popup(None, None, None, None, 3, gevent.time) + ui.updateFocusTime() + elif b==1: + self.begin_move_drag(gevent.button, int(gevent.x_root), int(gevent.y_root), gevent.time) + return False + def childButtonPress(self, widget, gevent): + b = gevent.button + #print(dir(gevent)) + #foo, x, y, mask = gevent.get_window().get_pointer() + #x, y = self.get_pointer() + x, y = gevent.x_root, gevent.y_root + if b == 1: + self.begin_move_drag(gevent.button, x, y, gevent.time) + return True + elif b == 3: + self.menuMainCreate() + if rtl: + x -= get_menu_width(self.menuMain) + self.menuMain.popup(None, None, lambda m, e: (x, y, True), None, 3, gevent.time) + ui.updateFocusTime() + return True + return False + def startResize(self, widget, gevent): + if self.menuMain: + self.menuMain.hide() + self.begin_resize_drag( + gdk.WindowEdge.SOUTH_EAST, + gevent.button, + int(gevent.x_root), + int(gevent.y_root), + gevent.time, + ) + return True + def changeDate(self, year, month, day): + ui.changeDate(year, month, day) + self.onDateChange() + goToday = lambda self, obj=None: self.changeDate(*core.getSysDate(calTypes.primary)) + def onDateChange(self, *a, **kw): + #print('MainWin.onDateChange') + ud.BaseCalObj.onDateChange(self, *a, **kw) + #for j in range(len(core.plugIndex)):##???????????????????? + # try: + # core.allPlugList[core.plugIndex[j]].date_change(*date) + # except AttributeError: + # pass + self.setMinHeight() + for j in range(len(core.plugIndex)): + try: + core.allPlugList[core.plugIndex[j]].date_change_after(*date) + except AttributeError: + pass + #print('Occurence Time: max=%e, avg=%e'%(ui.Cell.ocTimeMax, ui.Cell.ocTimeSum/ui.Cell.ocTimeCount)) + def getEventAddToMenuItem(self): + from scal3.ui_gtk.drawing import newColorCheckPixbuf + addToItem = labelStockMenuItem('_Add to', gtk.STOCK_ADD) + if event_lib.readOnly: + addToItem.set_sensitive(False) + return addToItem + menu2 = gtk.Menu() + ## + for group in ui.eventGroups: + if not group.enable: + continue + if not group.showInCal():## FIXME + continue + eventTypes = group.acceptsEventTypes + if not eventTypes: + continue + item2 = ImageMenuItem() + item2.set_label(group.title) + ## + image = gtk.Image() + if group.icon: + image.set_from_file(group.icon) + else: + image.set_from_pixbuf(newColorCheckPixbuf(group.color, 20, True)) + item2.set_image(image) + ## + if len(eventTypes)==1: + item2.connect('activate', self.addToGroupFromMenu, group, eventTypes[0]) + else: + menu3 = gtk.Menu() + for eventType in eventTypes: + eventClass = event_lib.classes.event.byName[eventType] + item3 = ImageMenuItem() + item3.set_label(eventClass.desc) + icon = eventClass.getDefaultIcon() + if icon: + item3.set_image(imageFromFile(icon)) + item3.connect('activate', self.addToGroupFromMenu, group, eventType) + menu3.add(item3) + menu3.show_all() + item2.set_submenu(menu3) + menu2.add(item2) + ## + menu2.show_all() + addToItem.set_submenu(menu2) + return addToItem + def menuCellPopup(self, widget, etime, x, y): + menu = gtk.Menu() + #### + menu.add(labelStockMenuItem('_Copy Date', gtk.STOCK_COPY, self.copyDate)) + menu.add(labelStockMenuItem('Day Info', gtk.STOCK_INFO, self.dayInfoShow)) + menu.add(self.getEventAddToMenuItem()) + menu.add(gtk.SeparatorMenuItem()) + menu.add(labelStockMenuItem('Select _Today', gtk.STOCK_HOME, self.goToday)) + menu.add(labelStockMenuItem('Select _Date...', gtk.STOCK_INDEX, self.selectDateShow)) + if widget._name in ('weekCal', 'monthCal'): + menu.add(labelStockMenuItem( + 'Switch to ' + ('Month Calendar' if widget._name=='weekCal' else 'Week Calendar'), + gtk.STOCK_REDO, + self.switchWcalMcal, + )) + if os.path.isfile('/usr/bin/evolution'):##?????????????????? + menu.add(labelImageMenuItem('In E_volution', 'evolution-18.png', ui.dayOpenEvolution)) + #if os.path.isfile('/usr/bin/sunbird'):##?????????????????? + # menu.add(labelImageMenuItem('In _Sunbird', 'sunbird-18.png', ui.dayOpenSunbird)) + #### + moreMenu = gtk.Menu() + moreMenu.add(labelStockMenuItem('_Customize', gtk.STOCK_EDIT, self.customizeShow)) + moreMenu.add(labelStockMenuItem('_Preferences', gtk.STOCK_PREFERENCES, self.prefShow)) + moreMenu.add(labelStockMenuItem('_Event Manager', gtk.STOCK_ADD, self.eventManShow)) + moreMenu.add(labelImageMenuItem('Time Line', 'timeline-18.png', self.timeLineShow)) + #moreMenu.add(labelImageMenuItem('Week Calendar', 'weekcal-18.png', self.weekCalShow)) + moreMenu.add(labelStockMenuItem(_('Export to %s')%'HTML', gtk.STOCK_CONVERT, self.exportClicked)) + moreMenu.add(labelStockMenuItem('_About', gtk.STOCK_ABOUT, self.aboutShow)) + if self.statusIconMode!=1: + moreMenu.add(labelStockMenuItem('_Quit', gtk.STOCK_QUIT, self.quit)) + ## + moreMenu.show_all() + moreItem = MenuItem(_('More')) + moreItem.set_submenu(moreMenu) + #moreItem.show_all() + menu.add(moreItem) + #### + menu.show_all() + dx, dy = widget.translate_coordinates(self, x, y) + foo, wx, wy = self.get_window().get_origin() + x = wx+dx + y = wy+dy + if rtl: + x -= get_menu_width(menu) + #### + etime = gtk.get_current_event_time() + #print('menuCellPopup', x, y, etime) + self.menuCell = menu ## without this line, the menu was not showing up, WTF?!! + menu.popup(None, None, lambda m, e: (x, y, True), None, 3, etime) + ui.updateFocusTime() + def menuMainCreate(self): + if self.menuMain: + return + menu = gtk.Menu() + #### + item = ImageMenuItem(_('Resize')) + item.set_image(imageFromFile('resize.png')) + item.connect('button-press-event', self.startResize) + menu.add(item) + ####### + menu.add(self.checkAbove) + menu.add(self.checkSticky) + ####### + menu.add(labelStockMenuItem('Select _Today', gtk.STOCK_HOME, self.goToday)) + menu.add(labelStockMenuItem('Select _Date...', gtk.STOCK_INDEX, self.selectDateShow)) + menu.add(labelStockMenuItem('Day Info', gtk.STOCK_INFO, self.dayInfoShow)) + menu.add(labelStockMenuItem('_Customize', gtk.STOCK_EDIT, self.customizeShow)) + menu.add(labelStockMenuItem('_Preferences', gtk.STOCK_PREFERENCES, self.prefShow)) + #menu.add(labelStockMenuItem('_Add Event', gtk.STOCK_ADD, ui.addCustomEvent)) + menu.add(labelStockMenuItem('_Event Manager', gtk.STOCK_ADD, self.eventManShow)) + menu.add(labelImageMenuItem('Time Line', 'timeline-18.png', self.timeLineShow)) + #menu.add(labelImageMenuItem('Week Calendar', 'weekcal-18.png', self.weekCalShow)) + menu.add(labelStockMenuItem(_('Export to %s')%'HTML', gtk.STOCK_CONVERT, self.exportClicked)) + menu.add(labelStockMenuItem('_About', gtk.STOCK_ABOUT, self.aboutShow)) + if self.statusIconMode!=1: + menu.add(labelStockMenuItem('_Quit', gtk.STOCK_QUIT, self.quit)) + menu.show_all() + self.menuMain = menu + def menuMainPopup(self, widget, etime, x, y): + self.menuMainCreate() + if etime == 0: + etime = gtk.get_current_event_time() + menu = self.menuMain + dx, dy = widget.translate_coordinates(self, x, y) + foo, wx, wy = self.get_window().get_origin() + x = wx+dx + y = wy+dy + if rtl: + x -= get_menu_width(menu) + #print('menuMainPopup', x, y, etime) + menu.popup(None, None, lambda m, e: (x, y, True), None, 3, etime) + ui.updateFocusTime() + def addToGroupFromMenu(self, menu, group, eventType): + from scal3.ui_gtk.event.editor import addNewEvent + #print('addToGroupFromMenu', group.title, eventType) + title = _('Add ') + event_lib.classes.event.byName[eventType].desc + event = addNewEvent( + group, + eventType, + useSelectedDate=True, + title=title, + parent=self, + ) + if event is None: + return + ui.eventDiff.add('+', event) + self.onConfigChange() + def prefUpdateBgColor(self, cal): + if ui.prefDialog: + ui.prefDialog.colorbBg.set_color(ui.bgColor) + #else:## FIXME + ui.saveLiveConf() + def keepAboveClicked(self, check): + act = check.get_active() + self.set_keep_above(act) + ui.winKeepAbove = act + ui.saveLiveConf() + def stickyClicked(self, check): + if check.get_active(): + self.stick() + ui.winSticky = True + else: + self.unstick() + ui.winSticky = False + ui.saveLiveConf() + def copyDate(self, obj=None, event=None): + setClipboard(ui.cell.format(ud.dateFormatBin)) + def copyDateToday(self, obj=None, event=None): + setClipboard(ui.todayCell.format(ud.dateFormatBin)) + def copyTime(self, obj=None, event=None): + setClipboard(ui.todayCell.format(ud.clockFormatBin, tm=localtime()[3:6])) + """ + def updateToolbarClock(self): + if ui.showDigClockTb: + if self.clock==None: + from scal3.ui_gtk.mywidgets.clock import FClockLabel + self.clock = FClockLabel(ud.clockFormat) + pack(self.toolbBox, self.clock) + self.clock.show() + else: + self.clock.format = ud.clockFormat + else: + if self.clock!=None: + self.clock.destroy() + self.clock = None + def updateStatusIconClock(self, checkStatusIconMode=True): + if checkStatusIconMode and self.statusIconMode!=2: + return + if ui.showDigClockTr: + if self.clockTr==None: + from scal3.ui_gtk.mywidgets.clock import FClockLabel + self.clockTr = FClockLabel(ud.clockFormat) + try: + pack(self.statusIconHbox, self.clockTr) + except AttributeError: + self.clockTr.destroy() + self.clockTr = None + else: + self.clockTr.show() + else: + self.clockTr.format = ud.clockFormat + else: + if self.clockTr!=None: + self.clockTr.destroy() + self.clockTr = None + """ + #weekCalShow = lambda self, obj=None, data=None: openWindow(ui.weekCalWin) + def statusIconInit(self): + if self.statusIconMode==2: + useAppIndicator = ui.useAppIndicator + if useAppIndicator: + try: + from gi.repository import AppIndicator3 as appIndicator + except ImportError: + useAppIndicator = False + if useAppIndicator: + from scal3.ui_gtk.starcal_appindicator import IndicatorStatusIconWrapper + self.sicon = IndicatorStatusIconWrapper(self) + else: + self.sicon = gtk.StatusIcon() + ##self.sicon.set_blinking(True) ## for Alarms ## some problem with gnome-shell + #self.sicon.set_name('starcal') + ## Warning: g_object_notify: object class `GtkStatusIcon' has no property named `name' + self.sicon.set_title(core.APP_DESC) + self.sicon.set_visible(True)## is needed ???????? + self.sicon.connect('button-press-event', self.statusIconButtonPress) + self.sicon.connect('activate', self.statusIconClicked) + self.sicon.connect('popup-menu', self.statusIconPopup) + #self.sicon.set_from_stock(gtk.STOCK_HOME) + else: + self.sicon = None + getMainWinMenuItem = lambda self: labelMenuItem('Main Window', self.statusIconClicked) + getStatusIconPopupItems = lambda self: [ + labelStockMenuItem('Copy _Time', gtk.STOCK_COPY, self.copyTime), + labelStockMenuItem('Copy _Date', gtk.STOCK_COPY, self.copyDateToday), + labelStockMenuItem('Ad_just System Time', gtk.STOCK_PREFERENCES, self.adjustTime), + #labelStockMenuItem('_Add Event', gtk.STOCK_ADD, ui.addCustomEvent),## FIXME + labelStockMenuItem(_('Export to %s')%'HTML', gtk.STOCK_CONVERT, self.exportClickedStatusIcon), + labelStockMenuItem('_Preferences', gtk.STOCK_PREFERENCES, self.prefShow), + labelStockMenuItem('_Customize', gtk.STOCK_EDIT, self.customizeShow), + labelStockMenuItem('_Event Manager', gtk.STOCK_ADD, self.eventManShow), + labelImageMenuItem('Time Line', 'timeline-18.png', self.timeLineShow), + labelStockMenuItem('_About', gtk.STOCK_ABOUT, self.aboutShow), + gtk.SeparatorMenuItem(), + labelStockMenuItem('_Quit', gtk.STOCK_QUIT, self.quit), + ] + def statusIconPopup(self, sicon, button, etime): + menu = gtk.Menu() + if os.sep == '\\': + from scal3.ui_gtk.windows import setupMenuHideOnLeave + setupMenuHideOnLeave(menu) + items = self.getStatusIconPopupItems() + # items.insert(0, self.getMainWinMenuItem())## FIXME + geo = self.sicon.get_geometry() ## Returns None on windows, why??? + if geo is None:## windows, taskbar is on buttom(below) + items.reverse() + get_pos_func = None + else: + #print(dir(geo)) + y1 = geo.index(1) + y = gtk.StatusIcon.position_menu(menu, self.sicon)[1] + if y1: + if self.sicon.is_embedded(): + self.hide() + else: + self.quit() + return True + def onEscape(self): + #ui.winX, ui.winY = self.get_position()## FIXME gives bad position sometimes + #liveConfChanged() + #print(ui.winX, ui.winY) + if self.statusIconMode==0: + self.quit() + elif self.statusIconMode>1: + if self.sicon.is_embedded(): + self.hide() + def quit(self, widget=None, event=None): + try: + ui.saveLiveConf() + except: + myRaise() + if self.statusIconMode>1 and self.sicon: + self.sicon.set_visible(False) ## needed for windows ## before or after main_quit ? + self.destroy() + ###### + core.stopRunningThreads() + ###### + return gtk.main_quit() + def adjustTime(self, widget=None, event=None): + from subprocess import Popen + Popen(ud.adjustTimeCmd) + def aboutShow(self, obj=None, data=None): + if not self.aboutDialog: + from scal3.ui_gtk.about import AboutDialog + dialog = AboutDialog( + name=core.APP_DESC, + version=core.VERSION, + title=_('About ')+core.APP_DESC, + authors=[_(line) for line in open(join(rootDir, 'authors-dialog')).read().splitlines()], + comments=core.aboutText, + license=core.licenseText, + website=core.homePage, + parent=self, + ) + ## add Donate button ## FIXME + dialog.connect('delete-event', self.aboutHide) + dialog.connect('response', self.aboutHide) + #dialog.set_logo(GdkPixbuf.Pixbuf.new_from_file(ui.logo)) + #dialog.set_skip_taskbar_hint(True) + self.aboutDialog = dialog + openWindow(self.aboutDialog) + def aboutHide(self, widget, arg=None):## arg maybe an event, or response id + self.aboutDialog.hide() + return True + def prefShow(self, obj=None, data=None): + if not ui.prefDialog: + from scal3.ui_gtk.preferences import PrefDialog + ui.prefDialog = PrefDialog(self.statusIconMode, parent=self) + ui.prefDialog.updatePrefGui() + openWindow(ui.prefDialog) + def eventManCreate(self): + checkEventsReadOnly() ## FIXME + if not ui.eventManDialog: + from scal3.ui_gtk.event.manager import EventManagerDialog + ui.eventManDialog = EventManagerDialog(parent=self) + def eventManShow(self, obj=None, data=None): + self.eventManCreate() + openWindow(ui.eventManDialog) + def addCustomEvent(self, obj=None): + self.eventManCreate() + ui.eventManDialog.addCustomEvent() + def timeLineShow(self, obj=None, data=None): + if not ui.timeLineWin: + from scal3.ui_gtk.timeline import TimeLineWindow + ui.timeLineWin = TimeLineWindow() + openWindow(ui.timeLineWin) + def selectDateShow(self, widget=None): + if not self.selectDateDialog: + from scal3.ui_gtk.selectdate import SelectDateDialog + self.selectDateDialog = SelectDateDialog(parent=self) + self.selectDateDialog.connect('response-date', self.selectDateResponse) + self.selectDateDialog.show() + def dayInfoShow(self, widget=None): + if not self.dayInfoDialog: + from scal3.ui_gtk.day_info import DayInfoDialog + self.dayInfoDialog = DayInfoDialog(parent=self) + self.dayInfoDialog.onDateChange() + openWindow(self.dayInfoDialog) + def customizeDialogCreate(self): + if not self.customizeDialog: + from scal3.ui_gtk.customize_dialog import CustomizeDialog + self.customizeDialog = CustomizeDialog(self.vbox) + def switchWcalMcal(self, widget=None): + self.customizeDialogCreate() + self.vbox.switchWcalMcal() + self.customizeDialog.updateTreeEnableChecks() + self.customizeDialog.save() + def customizeShow(self, obj=None, data=None): + self.customizeDialogCreate() + openWindow(self.customizeDialog) + def exportShow(self, year, month): + if not self.exportDialog: + from scal3.ui_gtk.export import ExportDialog + self.exportDialog = ExportDialog(parent=self) + self.exportDialog.showDialog(year, month) + def exportClicked(self, widget=None): + self.exportShow(ui.cell.year, ui.cell.month) + def exportClickedStatusIcon(self, widget=None, event=None): + year, month, day = core.getSysDate(calTypes.primary) + self.exportShow(year, month) + def onConfigChange(self, *a, **kw): + ud.BaseCalObj.onConfigChange(self, *a, **kw) + #self.set_property('skip-taskbar-hint', not ui.winTaskbar) ## self.set_skip_taskbar_hint ## FIXME + ## skip-taskbar-hint need to restart ro be applied + #self.updateToolbarClock()## FIXME + #self.updateStatusIconClock() + self.statusIconUpdate() + + +###########################################################################3 + + +#core.COMMAND = sys.argv[0] ## OR __file__ ## ???????? + + +gtk.init_check(sys.argv) + +clickWebsite = lambda widget, url: core.openUrl(url) +try: + gtk.link_button_set_uri_hook(clickWebsite) +except:## old PyGTK (older than 2.10) + pass + +try: + gtk.about_dialog_set_url_hook(clickWebsite) +except:## old PyGTK (older than 2.10) + pass + +for plug in core.allPlugList: + if hasattr(plug, 'onCurrentDateChange'): + listener.dateChange.add(plug) + + +""" +themeDir = join(rootDir, 'themes') +theme = 'Dark' # 'Default +if theme!=None: + gtkrc = join(themeDir, theme, 'gtkrc') + try: + #gtk.rc_set_default_files([gtkrc]) + gtk.rc_parse(gtkrc) + #gtk.rc_reparse_all() + #exec(open(join(themeDir, theme, 'starcalrc')).read()) + except: + myRaise(__file__) +""" + + + + + +def main(): + ''' + try: + import psyco + except ImportError: + print('Warning: module "psyco" not found. It could speed up execution.') + psyco_found=False + else: + psyco.full() + print('Using module "psyco" to speed up execution.') + psyco_found=True''' + statusIconMode = 2 + action = '' + if ui.showMain: + action = 'show' + if len(sys.argv)>1: + if sys.argv[1] in ('--no-tray-icon', '--no-status-icon'): + statusIconMode = 0 + action = 'show' + elif sys.argv[1]=='--hide': + action = '' + elif sys.argv[1]=='--show': + action = 'show' + #elif sys.argv[1]=='--html':#???????????? + # action = 'html' + #elif sys.argv[1]=='--svg':#???????????? + # action = 'svg' + ############################### + ui.init() + ############################### + checkEventsReadOnly(False) + ## right place? FIXME + if core.BRANCH == 'master' and versionLessThan(event_lib.info.version, '2.3.0'): + from scal3.ui_gtk.event.bulk_save_timezone import BulkSaveTimeZoneDialog + BulkSaveTimeZoneDialog(parent=None).run() + event_lib.info.updateAndSave() + ############################### + mainWin = MainWin(statusIconMode=statusIconMode) + #if action=='html': + # mainWin.exportHtml('calendar.html') ## exportHtml(path, months, title) + # sys.exit(0) + #elif action=='svg': + # mainWin.export.exportSvg('%s/2010-01.svg'%core.deskDir, [(2010, 1)]) + # sys.exit(0) + if action=='show' or not mainWin.sicon: + mainWin.present() + ##ud.rootWindow.set_cursor(gdk.Cursor.new(gdk.CursorType.LEFT_PTR))#??????????? + #mainWin.app.run(None) + ex = 0 + try: + ex = gtk.main() + except KeyboardInterrupt: + print('You pressed Control+C, Goodbye') + core.stopRunningThreads() + except Exception as e: + core.stopRunningThreads() + raise e + return ex + + +if __name__ == '__main__': + sys.exit(main()) + diff --git a/scal3/ui_gtk/starcal_appindicator.py b/scal3/ui_gtk/starcal_appindicator.py new file mode 100644 index 000000000..c7695cb3e --- /dev/null +++ b/scal3/ui_gtk/starcal_appindicator.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import sys +import os +from os.path import dirname +sys.path.insert(0, dirname(dirname(dirname(__file__)))) + +from scal3.path import * +from scal3 import core +from scal3 import locale_man +from scal3.locale_man import tr as _ + +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import CopyLabelMenuItem + +from gi.repository import AppIndicator3 as appindicator + +class IndicatorStatusIconWrapper: + imPath = join(tmpDir, APP_NAME+'-indicator-%s.png'%os.getuid())## FIXME + def __init__(self, mainWin): + self.mainWin = mainWin + self.c = appindicator.Indicator.new( + APP_NAME,## app id + '',## icon + appindicator.IndicatorCategory.APPLICATION_STATUS, + ) + self.c.set_status(appindicator.IndicatorStatus.ACTIVE) + #self.c.set_attention_icon("new-messages-red") + ###### + self.create_menu() + ''' + def create_menu_simple(self): + menu = gtk.Menu() + ### + for item in [self.mainWin.getMainWinMenuItem()] + self.mainWin.getStatusIconPopupItems(): + item.show() + menu.add(item) + ### + #if locale_man.rtl: + #menu.set_direction(gtk.TextDirection.RTL) + self.c.set_menu(menu) + ''' + def create_menu(self): + menu = gtk.Menu() + self.menuItems = [] ## just to prevent GC from removing custom objects for items + #### + for line in self.mainWin.getStatusIconTooltip().split('\n'): + item = CopyLabelMenuItem(line) + self.menuItems.append(item) + item.show() + menu.append(item) + #### + item = self.mainWin.getMainWinMenuItem() + self.menuItems.append(item) + item.show() + menu.append(item) + #### + submenu = gtk.Menu() + for item in self.mainWin.getStatusIconPopupItems(): + self.menuItems.append(item) + item.show() + submenu.add(item) + sitem = MenuItem(label=_('More')) + sitem.set_submenu(submenu) + sitem.show() + menu.append(sitem) + self.c.set_menu(menu) + def set_from_file(self, fpath): + self.c.set_icon('') + self.c.set_icon(fpath) + self.create_menu() + def set_from_pixbuf(self, pbuf): + ## https://bugs.launchpad.net/ubuntu/+source/indicator-application/+bug/533439 + #pbuf.scale_simple(22, 22, GdkPixbuf.InterpType.HYPER) + pbuf.savev(self.imPath, 'png', [], []) + self.set_from_file(self.imPath) + #def __del__(self): + # os.remove(self.imPath) + is_embedded = lambda self: True ## FIXME + def set_visible(self, visible):## FIXME + pass + def set_tooltip_text(self, text): + #self.c.set_label_guide(text) + pass + + + diff --git a/scal3/ui_gtk/timeline.py b/scal3/ui_gtk/timeline.py new file mode 100644 index 000000000..6eab0136c --- /dev/null +++ b/scal3/ui_gtk/timeline.py @@ -0,0 +1,573 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import time +from time import time as now + +import math +from math import pi + +from scal3.utils import iceil +from scal3 import core +from scal3.core import myRaise +from scal3.locale_man import tr as _ +from scal3.locale_man import rtl +from scal3 import ui +from scal3.timeline import * + +from gi.repository.GObject import timeout_add +from gi.repository.GLib import source_remove + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.font_utils import pfontEncode +from scal3.ui_gtk.utils import labelStockMenuItem, labelImageMenuItem +from scal3.ui_gtk.drawing import setColor, fillColor, newTextLayout, Button +from scal3.ui_gtk import gtk_ud as ud +#from scal3.ui_gtk import preferences +from scal3.ui_gtk.timeline_box import * + +import scal3.ui_gtk.event.manager + + + +def show_event(widget, gevent): + print(type(widget), gevent.type.value_name, gevent.get_value())#, gevent.send_event + + +@registerSignals +class TimeLine(gtk.DrawingArea, ud.BaseCalObj): + _name = 'timeLine' + desc = _('Time Line') + def centerToNow(self): + self.stopMovingAnim() + self.timeStart = now() - self.timeWidth/2.0 + def centerToNowClicked(self, arg=None): + self.centerToNow() + self.queue_draw() + def __init__(self, closeFunc): + gtk.DrawingArea.__init__(self) + self.add_events(gdk.EventMask.ALL_EVENTS_MASK) + self.initVars() + ### + self.closeFunc = closeFunc + self.connect('draw', self.onExposeEvent) + self.connect('scroll-event', self.onScroll) + self.connect('button-press-event', self.buttonPress) + self.connect('motion-notify-event', self.motionNotify) + self.connect('button-release-event', self.buttonRelease) + self.connect('key-press-event', self.keyPress) + #self.connect('event', show_event) + self.currentTime = now() + self.timeWidth = dayLen + self.timeStart = self.currentTime - self.timeWidth/2.0 + self.buttons = [ + Button('home.png', self.centerToNowClicked, 1, -1, False), + Button('resize-small.png', self.startResize, -1, -1, False), + Button('exit.png', closeFunc, 35, -1, False) + ] + ## zoom in and zoom out buttons FIXME + self.data = None + ######## + self.movingLastPress = 0 + self.movingV = 0 + self.movingF = 0 + ####### + self.boxEditing = None + ## or (editType, box, x0, t0) + ## editType=0 moving + ## editType=-1 resizing to left + ## editType=+1 resizing to right + def currentTimeUpdate(self, restart=False, draw=True): + if restart: + try: + source_remove(self.timeUpdateSourceId) + except AttributeError: + pass + try: + pixelPerSec = self.pixelPerSec + except AttributeError: + pixelPerSec = 1 + seconds = iceil(0.4/pixelPerSec) + tm = now() + #print('time=%.2f'%(tm%100), 'pixelPerSec=%.1f'%pixelPerSec, 'seconds=%d'%seconds) + self.timeUpdateSourceId = timeout_add( + int(1000*(seconds + 0.01 - tm%1)), + self.currentTimeUpdate, + ) + self.currentTime = int(tm) + if draw and self.get_parent(): + if self.get_parent().get_visible() and \ + self.timeStart <= tm <= self.timeStart + self.timeWidth + 1: + #print('%.2f'%(tm%100), 'currentTimeUpdate: DRAW') + self.queue_draw() + def updateData(self): + width = self.get_allocation().width + self.pixelPerSec = float(width) / self.timeWidth ## pixel/second + self.borderTm = boxMoveBorder / self.pixelPerSec ## second + self.data = calcTimeLineData( + self.timeStart, + self.timeWidth, + self.pixelPerSec, + self.borderTm, + ) + def drawTick(self, cr, tick, maxTickHeight): + tickH = tick.height + tickW = tick.width + tickH = min(tickH, maxTickHeight) + ### + tickX = tick.pos - tickW/2.0 + tickY = 1 + cr.rectangle(tickX, tickY, tickW, tickH) + try: + fillColor(cr, tick.color) + except: + print('error in fill, x=%.2f, y=%.2f, w=%.2f, h=%.2f'%(tickX, tickY, tickW, tickH)) + ### + font = [ + fontFamily, + False, + False, + tick.fontSize, + ] + #layout = newLimitedWidthTextLayout( + # self, + # tick.label, + # tick.maxLabelWidth, + # font=font, + # truncate=truncateTickLabel, + #)## FIXME + layout = newTextLayout( + self, + text=tick.label, + font=font, + maxSize=(tick.maxLabelWidth, 0), + maximizeScale=1.0, + truncate=truncateTickLabel, + )## FIXME + if layout: + #layout.set_auto_dir(0)## FIXME + #print('layout.get_auto_dir() = %s'%layout.get_auto_dir()) + layoutW, layoutH = layout.get_pixel_size() + layoutX = tick.pos - layoutW/2.0 + layoutY = tickH*labelYRatio + try: + cr.move_to(layoutX, layoutY) + except: + print('error in move_to, x=%.2f, y=%.2f'%(layoutX, layoutY)) + else: + show_layout(cr, layout)## with the same tick.color + def drawBox(self, cr, box): + d = box.lineW + x = box.x + w = box.w + y = box.y + h = box.h + ### + drawBoxBG(cr, box, x, y, w, h) + drawBoxBorder(cr, box, x, y, w, h) + drawBoxText(cr, box, x, y, w, h, self) + def drawBoxEditingHelperLines(self, cr): + if not self.boxEditing: + return + editType, event, box, x0, t0 = self.boxEditing + setColor(cr, fgColor) + d = editingBoxHelperLineWidth + cr.rectangle( + box.x, + 0, + d, + box.y, + ) + cr.fill() + cr.rectangle( + box.x + box.w - d, + 0, + d, + box.y, + ) + cr.fill() + def drawAll(self, cr): + width = self.get_allocation().width + height = self.get_allocation().height + pixelPerSec = self.pixelPerSec + dayPixel = dayLen * pixelPerSec ## pixel + maxTickHeight = maxTickHeightRatio * height + ##### + cr.rectangle(0, 0, width, height) + fillColor(cr, bgColor) + ##### + setColor(cr, holidayBgBolor) + for x in self.data['holidays']: + cr.rectangle(x, 0, dayPixel, height) + cr.fill() + ##### + for tick in self.data['ticks']: + self.drawTick(cr, tick, maxTickHeight) + ###### + beforeBoxH = maxTickHeight ## FIXME + maxBoxH = height - beforeBoxH + for box in self.data['boxes']: + box.setPixelValues(self.timeStart, pixelPerSec, beforeBoxH, maxBoxH) + self.drawBox(cr, box) + self.drawBoxEditingHelperLines(cr) + ###### Draw Current Time Marker + dt = self.currentTime - self.timeStart + if 0 <= dt <= self.timeWidth: + setColor(cr, currentTimeMarkerColor) + cr.rectangle( + dt*pixelPerSec - currentTimeMarkerWidth/2.0, + 0, + currentTimeMarkerWidth, + currentTimeMarkerHeightRatio * self.get_allocation().height + ) + cr.fill() + ###### + for button in self.buttons: + button.draw(cr, width, height) + def onExposeEvent(self, widget=None, event=None): + #t0 = now() + if not self.boxEditing: + self.updateData() + self.currentTimeUpdate(restart=True, draw=False) + #t1 = now() + self.drawAll(self.get_window().cairo_create()) + #t2 = now() + #print('drawing time / data calc time: %.2f'%((t2-t1)/(t1-t0))) + def onScroll(self, widget, gevent): + d = getScrollValue(gevent) + #print('onScroll', d) + if gevent.get_state() & gdk.ModifierType.CONTROL_MASK: + self.zoom( + d=='up', + scrollZoomStep, + float(gevent.x) / self.get_allocation().width, + ) + else: + self.movingUserEvent( + direction=(-1 if d=='up' else 1), + )## FIXME + self.queue_draw() + return True + def buttonPress(self, obj, gevent): + x = gevent.x + y = gevent.y + w = self.get_allocation().width + h = self.get_allocation().height + if gevent.button==1: + for button in self.buttons: + if button.contains(x, y, w, h): + button.func(gevent) + return True + #### + for box in self.data['boxes']: + if not box.hasBorder: + continue + if not box.ids: + continue + if not box.contains(x, y): + continue + gid, eid = box.ids + group = ui.eventGroups[gid] + event = group[eid] + #### + top = y - box.y + left = x - box.x + right = box.x + box.w - x + minA = min(boxMoveBorder, top, left, right) + editType = None + if top == minA: + editType = 0 + t0 = event.getStartEpoch() + self.get_window().set_cursor(gdk.Cursor.new(gdk.CursorType.FLEUR)) + elif right == minA: + editType = 1 + t0 = event.getEndEpoch() + self.get_window().set_cursor(gdk.Cursor.new(gdk.CursorType.RIGHT_SIDE)) + elif left == minA: + editType = -1 + t0 = event.getStartEpoch() + self.get_window().set_cursor(gdk.Cursor.new(gdk.CursorType.LEFT_SIDE)) + if editType is not None: + self.boxEditing = (editType, event, box, x, t0) + return True + elif gevent.button==3: + for box in self.data['boxes']: + if not box.ids: + continue + if not box.contains(x, y): + continue + gid, eid = box.ids + group = ui.eventGroups[gid] + event = group[eid] + #### + menu = gtk.Menu() + ## + if not event.readOnly: + winTitle = _('Edit') + ' ' + event.desc + menu.add(labelStockMenuItem( + winTitle, + gtk.STOCK_EDIT, + self.editEventClicked, + winTitle, + event, + gid, + )) + ## + winTitle = _('Edit') + ' ' + group.desc + menu.add(labelStockMenuItem( + winTitle, + gtk.STOCK_EDIT, + self.editGroupClicked, + winTitle, + group, + )) + ## + menu.add(gtk.SeparatorMenuItem()) + ## + menu.add(labelImageMenuItem( + _('Move to %s')%ui.eventTrash.title, + ui.eventTrash.icon, + self.moveEventToTrash, + group, + event, + )) + ## + menu.show_all() + self.tmpMenu = menu + menu.popup(None, None, None, None, 3, gevent.time) + return False + def motionNotify(self, obj, gevent): + if self.boxEditing: + editType, event, box, x0, t0 = self.boxEditing + t1 = t0 + (gevent.x - x0)/self.pixelPerSec + if editType==0: + event.modifyPos(t1) + elif editType==1: + if t1-box.t0 > 2*boxMoveBorder/self.pixelPerSec: + event.modifyEnd(t1) + elif editType==-1: + if box.t1-t1 > 2*boxMoveBorder/self.pixelPerSec: + event.modifyStart(t1) + box.t0 = max( + event.getStartEpoch(), + self.timeStart - self.borderTm, + ) + box.t1 = min( + event.getEndEpoch(), + self.timeStart + self.timeWidth + self.borderTm, + ) + self.queue_draw() + def buttonRelease(self, obj, gevent): + if self.boxEditing: + editType, event, box, x0, t0 = self.boxEditing + event.afterModify() + event.save() + self.boxEditing = None + self.get_window().set_cursor(gdk.Cursor.new(gdk.CursorType.LEFT_PTR)) + self.queue_draw() + def onConfigChange(self, *a, **kw): + ud.BaseCalObj.onConfigChange(self, *a, **kw) + self.queue_draw() + def editEventClicked(self, menu, winTitle, event, gid): + from scal3.ui_gtk.event.editor import EventEditorDialog + event = EventEditorDialog( + event, + title=winTitle, + #parent=self,## FIXME + ).run() + if event is None: + return + ui.eventDiff.add('e', event) + self.onConfigChange() + def editGroupClicked(self, menu, winTitle, group): + from scal3.ui_gtk.event.group.editor import GroupEditorDialog + group = GroupEditorDialog( + group, + parent=self.get_toplevel(), + ).run() + if group is not None: + group.afterModify() + group.save()## FIXME + ui.changedGroups.append(group.id) + ud.windowList.onConfigChange() + self.queue_draw() + def moveEventToTrash(self, menu, group, event): + from scal3.ui_gtk.event.utils import confirmEventTrash + if not confirmEventTrash(event): + return + eventIndex = group.index(event.id) + ui.moveEventToTrashFromOutside(group, event) + self.onConfigChange() + def startResize(self, gevent): + self.get_parent().begin_resize_drag( + gdk.WindowEdge.SOUTH_EAST, + gevent.button, + int(gevent.x_root), + int(gevent.y_root), + gevent.time, + ) + def zoom(self, zoomIn, stepFact, posFact): + zoomValue = 1.0/stepFact if zoomIn else stepFact + self.timeStart += self.timeWidth * (1-zoomValue) * posFact + self.timeWidth *= zoomValue + keyboardZoom = lambda self, zoomIn: self.zoom(zoomIn, keyboardZoomStep, 0.5) + def keyPress(self, arg, gevent): + k = gdk.keyval_name(gevent.keyval).lower() + #print('%.3f'%now()) + if k in ('space', 'home'): + self.centerToNow() + elif k=='right': + self.movingUserEvent( + direction=1, + smallForce=(gevent.get_state() & gdk.ModifierType.SHIFT_MASK), + ) + elif k=='left': + self.movingUserEvent( + direction=-1, + smallForce=(gevent.get_state() & gdk.ModifierType.SHIFT_MASK), + ) + elif k=='down': + self.stopMovingAnim() + elif k in ('q', 'escape'): + self.closeFunc() + #elif k=='end': + # pass + #elif k=='page_up': + # pass + #elif k=='page_down': + # pass + #elif k=='menu':# Simulate right click (key beside Right-Ctrl) + # #self.emit('popup-cell-menu', gevent.time, *self.getCellPos()) + #elif k in ('f10','m'): # F10 or m or M + # if gevent.get_state() & gdk.ModifierType.SHIFT_MASK: + # # Simulate right click (key beside Right-Ctrl) + # self.emit('popup-cell-menu', gevent.time, *self.getCellPos()) + # else: + # self.emit('popup-main-menu', gevent.time, *self.getMainMenuPos()) + elif k in ('plus', 'equal', 'kp_add'): + self.keyboardZoom(True) + elif k in ('minus', 'kp_subtract'): + self.keyboardZoom(False) + else: + #print(k) + return False + self.queue_draw() + return True + def movingUserEvent(self, direction=1, smallForce=False): + if enableAnimation: + tm = now() + #dtEvent = tm - self.movingLastPress + self.movingLastPress = tm + ''' + We should call a new updateMovingAnim if: + last key press has bin timeout, OR + force direction has been change, OR + its currently still (no speed and no force) + ''' + if self.movingF*direction < 0 or self.movingF*self.movingV==0: + ## or dtEvent > movingKeyTimeout + self.movingF = direction * \ + (movingHandSmallForce if smallForce else movingHandForce) + self.movingV += movingV0 * direction + self.updateMovingAnim(self.movingF, tm, tm, self.movingV, self.movingF) + else: + self.timeStart += direction * movingStaticStep * \ + self.timeWidth/float(self.get_allocation().width) + def updateMovingAnim(self, f1, t0, t1, v0, a1): + t2 = now() + f = self.movingF + if f!=f1: + return + v1 = self.movingV + if f==0 and v1==0: + return + timeout = movingKeyTimeoutFirst if t2-t0= timeout:## Stopping + f = self.movingF = 0 + if v1 > 0: + a2 = f - movingFrictionForce + elif v1 < 0: + a2 = f + movingFrictionForce + else: + a2 = f + if a2 != a1: + return self.updateMovingAnim(f, t2, t2, v1, a2) + v2 = v0 + a2*(t2-t0) + if v2 > movingMaxSpeed: + v2 = movingMaxSpeed + elif v2 < -movingMaxSpeed: + v2 = -movingMaxSpeed + if f==0 and v1*v2 <= 0: + self.movingV = 0 + return + timeout_add(movingUpdateTime, self.updateMovingAnim, f, t0, t2, v0, a2) + self.movingV = v2 + self.timeStart += v2 * (t2-t1) * self.timeWidth/float(self.get_allocation().width) + self.queue_draw() + def stopMovingAnim(self):## stop moving immudiatly + self.movingF = 0 + self.movingV = 0 + + +@registerSignals +class TimeLineWindow(gtk.Window, ud.BaseCalObj): + _name = 'timeLineWin' + desc = _('Time Line') + def __init__(self): + gtk.Window.__init__(self) + self.initVars() + ud.windowList.appendItem(self) + ### + self.resize(ud.screenW, 150) + self.move(0, 0) + self.set_title(_('Time Line')) + self.set_decorated(False) + self.connect('delete-event', self.closeClicked) + self.connect('button-press-event', self.buttonPress) + ### + self.tline = TimeLine(self.closeClicked) + self.connect('key-press-event', self.tline.keyPress) + self.add(self.tline) + self.tline.show() + self.appendItem(self.tline) + def closeClicked(self, arg=None, event=None): + if ui.mainWin: + self.hide() + else: + gtk.main_quit()## FIXME + return True + def buttonPress(self, obj, gevent): + if gevent.button==1: + self.begin_move_drag(gevent.button, int(gevent.x_root), int(gevent.y_root), gevent.time) + return True + return False + + +if __name__=='__main__': + win = TimeLineWindow() + #win.tline.timeWidth = 100 * minYearLenSec # 2 * 10**17 + #win.tline.timeStart = now() - win.tline.timeWidth # -10**17 + win.show() + gtk.main() + + + + diff --git a/scal3/ui_gtk/timeline_box.py b/scal3/ui_gtk/timeline_box.py new file mode 100644 index 000000000..2c000e251 --- /dev/null +++ b/scal3/ui_gtk/timeline_box.py @@ -0,0 +1,138 @@ +from scal3.utils import toStr +from scal3 import ui +from scal3.timeline import * + +from scal3.ui_gtk.drawing import * + + +def drawBoxBG(cr, box, x, y, w, h): + d = box.lineW + cr.rectangle(x, y, w, h) + if d == 0: + fillColor(cr, box.color) + return + try: + alpha = box.color[3] + except IndexError: + alpha = 255 + try: + fillColor(cr, ( + box.color[0], + box.color[1], + box.color[2], + int(alpha*boxInnerAlpha), + )) + except cairo.Error: + return + ### + cr.set_line_width(0) + cr.move_to(x, y) + cr.line_to(x+w, y) + cr.line_to(x+w, y+h) + cr.line_to(x, y+h) + cr.line_to(x, y) + cr.line_to(x+d, y) + cr.line_to(x+d, y+h-d) + cr.line_to(x+w-d, y+h-d) + cr.line_to(x+w-d, y+d) + cr.line_to(x+d, y+d) + cr.close_path() + fillColor(cr, box.color) + + +def drawBoxBorder(cr, box, x, y, w, h): + if box.hasBorder: + if w > 2*boxMoveBorder and h > boxMoveBorder: + b = boxMoveBorder + bd = boxMoveLineW + #cr.set_line_width(bd) + cr.move_to(x+b-bd, y+h) + cr.line_to(x+b-bd, y+b-bd) + cr.line_to(x+w-b+bd, y+b-bd) + cr.line_to(x+w-b+bd, y+h) + cr.line_to(x+w-b, y+h) + cr.line_to(x+w-b, y+b) + cr.line_to(x+b, y+b) + cr.line_to(x+b, y+h) + cr.close_path() + fillColor(cr, box.color) + ### + bds = 0.7 * bd + cr.move_to(x, y) + cr.line_to(x+bds, y) + cr.line_to(x+b+bds, y+b) + cr.line_to(x+b, y+b+bds) + cr.line_to(x, y+bds) + cr.close_path() + fillColor(cr, box.color) + ## + cr.move_to(x+w, y) + cr.line_to(x+w-bds, y) + cr.line_to(x+w-b-bds, y+b) + cr.line_to(x+w-b, y+b+bds) + cr.line_to(x+w, y+bds) + cr.close_path() + fillColor(cr, box.color) + else: + box.hasBorder = False + + +def drawBoxText(cr, box, x, y, w, h, widget): + ## now draw the text + ## how to find the best font size based in the box's width and height, + ## and font family? FIXME + ## possibly write in many lines? or just in one line and wrap if needed? + if box.text: + #print(box.text) + textW = 0.9 * w + textH = 0.9 * h + textLen = len(toStr(box.text)) + #print('textLen=%s'%textLen) + avgCharW = float(textW if rotateBoxLabel == 0 else max(textW, textH)) / textLen + if avgCharW > 3:## FIXME + font = list(ui.getFont()) + layout = widget.create_pango_layout(box.text) ## a Pango.Layout object + layout.set_font_description(pfontEncode(font)) + layoutW, layoutH = layout.get_pixel_size() + #print('orig font size: %s'%font[3]) + normRatio = min( + float(textW)/layoutW, + float(textH)/layoutH, + ) + rotateRatio = min( + float(textW)/layoutH, + float(textH)/layoutW, + ) + if rotateBoxLabel != 0 and rotateRatio > normRatio: + font[3] *= max(normRatio, rotateRatio) + layout.set_font_description(pfontEncode(font)) + layoutW, layoutH = layout.get_pixel_size() + fillColor(cr, fgColor)## before cr.move_to + #print('x=%s, y=%s, w=%s, h=%s, layoutW=%s, layoutH=%s'\) + # %(x,y,w,h,layoutW,layoutH) + cr.move_to( + x + (w - rotateBoxLabel*layoutH)/2.0, + y + (h + rotateBoxLabel*layoutW)/2.0, + ) + cr.rotate(-rotateBoxLabel*pi/2) + show_layout(cr, layout) + try: + cr.rotate(rotateBoxLabel*pi/2) + except: + print('counld not rotate by %s*pi/2 = %s'%( + rotateBoxLabel, + rotateBoxLabel*pi/2, + )) + else: + font[3] *= normRatio + layout.set_font_description(pfontEncode(font)) + layoutW, layoutH = layout.get_pixel_size() + fillColor(cr, fgColor)## before cr.move_to + cr.move_to( + x + (w-layoutW)/2.0, + y + (h-layoutH)/2.0, + ) + show_layout(cr, layout) + + + diff --git a/scal3/ui_gtk/tinycal.py b/scal3/ui_gtk/tinycal.py new file mode 100644 index 000000000..00f65be78 --- /dev/null +++ b/scal3/ui_gtk/tinycal.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import sys, os + +from math import pi +import os.path + +from scal3.path import * +from scal3.locale_man import tr as _ +from scal3.locale_man import rtl, rtlSgn +from scal3 import core +from scal3.core import myRaise, getMonthName, getMonthLen +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.font_utils import pfontEncode +from scal3.ui_gtk.color_utils import rgbToGdkColor +from scal3.ui_gtk.drawing import fillColor, newTextLayout + + +class WeekStatus(list): + ## int year + ## int weeknum + ## list[list] dates + def __init__(self): + list.__init__(self) + +class TextObject(): + def __init__(self, parent, x, y, color, font, center=True): + self.parent = parent + self.x = x + self.y = y + self.resizable = False + ############### + self.color = color + self.layout = widget.create_pango_layout('') + if font: + self.setFont(font) + self.center = center ## ??????????????????? + #self.xAlign = 0.5 + #self.yAlign = 0.5 + def draw(self, cr): + if self.center: + w, h = self.layout.get_pixel_size() + cr.move_to(self.x - w/2.0, self.y - h/2.0) + else: + cr.move_to(self.x, self.y) + fillColor(cr, self.color) + show_layout(cr, self.layout) + def setFont(self, font): + self.layout.set_font_description(pfontEncode(font)) + def getText(self): + raise NotImplementedError + def contains(self, px, py): + w, h = self.layout.get_pixel_size() + if self.center: + return -w/2.0 <= px-self.x < w/2.0 \ + and -h/2.0 <= py-self.y < h/2.0 + else: + return 0 <= px-self.x < w \ + and 0 <= py-self.y < h + +class YearObject(TextObject): + def __init__(self, parent, mode, x=0, y=0, color=(0,0,0), font=None): + TextObject.__init__(self, parent, x, y, color, font) + self.mode = mode + getText = lambda self: _(self.parent.dates[self.mode][2], self.mode) + +class MonthObject(TextObject): + def __init__(self, parent, mode, x=0, y=0, color=(0,0,0), font=None): + TextObject.__init__(self, parent, x, y, color, font) + self.mode = mode + getText = lambda self: _(self.parent.dates[self.mode][1], self.mode) + +class MonthNameObject(TextObject): + def __init__(self, parent, mode, x=0, y=0, color=(0,0,0), font=None): + TextObject.__init__(self, parent, x, y, color, font) + self.mode = mode + getText = lambda self: getMonthName(self.mode, self.parent.dates[self.mode][1]) + +class PlainStrObject(TextObject): + def __init__(self, parent, text='', x=0, y=0, color=(0,0,0), font=None): + TextObject.__init__(self, parent, x, y, color, font) + self.text = text + getText = lambda self: self.text + + + +class TinyCal(gtk.Window): + def __init__(self): + gtk.Window.__init__(self) + self.set_title(core.APP_DESC+' Tiny') + self.set_decorated(False) + self.set_property('skip-taskbar-hint', None) + self.set_role(core.APP_NAME+'-tiny') + ################## + self.objects = [] + def startEditing(self): + pass + def endEditing(self): + pass + + + + + + + + + + + + + + + + + + + + diff --git a/scal3/ui_gtk/toolbar.py b/scal3/ui_gtk/toolbar.py new file mode 100644 index 000000000..ca1378bab --- /dev/null +++ b/scal3/ui_gtk/toolbar.py @@ -0,0 +1,207 @@ +from time import time as now + +from scal3.utils import myRaise +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from gi.repository import GObject as gobject + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.utils import set_tooltip +from scal3.ui_gtk import gtk_ud as ud +from scal3.ui_gtk.customize import CustomizableCalObj + + + +@registerSignals +class ToolbarItem(gtk.ToolButton, CustomizableCalObj): + def __init__(self, name, stockName, method, desc='', shortDesc='', enableTooltip=True): + #print('ToolbarItem', name, stockName, method, desc, text) + self.method = method + ###### + if not desc: + desc = name.capitalize() + ## + if not shortDesc: + shortDesc = desc + ## + desc = _(desc) + shortDesc = _(shortDesc) + ###### + gtk.ToolButton.__init__(self) + self.set_icon_widget( + gtk.Image.new_from_stock( + getattr(gtk, 'STOCK_%s'%(stockName.upper())), + gtk.IconSize.DIALOG, + ) if stockName else None, + #shortDesc, + ) + self.set_label(shortDesc) + self._name = name + self.desc = desc + #self.shortDesc = shortDesc## FIXME + self.initVars() + if enableTooltip: + set_tooltip(self, desc)## FIXME + self.set_is_important(True)## FIXME + show = lambda self: self.show_all() + + +#@registerSignals +class CustomizableToolbar(gtk.Toolbar, CustomizableCalObj): + _name = 'toolbar' + desc = _('Toolbar') + #signals = CustomizableCalObj.signals + [ + # ('popup-main-menu', [int, int, int]), + #] + styleList = ( + ## Gnome's naming is not exactly the best here + ## And Gnome's order of options is also different from Gtk's enum + 'Icon',## 'icons', 'Icons only' + 'Text',## 'text', 'Text only' + 'Text below Icon',## 'both', 'Text below items' + 'Text beside Icon',## 'both-horiz', 'Text beside items' + ) + defaultItems = [] + defaultItemsDict = {} + def __init__(self, funcOwner, vertical=False, onPressContinue=False): + from scal3.ui_gtk.mywidgets.multi_spin.integer import IntSpinButton + gtk.Toolbar.__init__(self) + self.funcOwner = funcOwner + self.set_orientation(gtk.Orientation.VERTICAL if vertical else gtk.Orientation.HORIZONTAL) + self.add_events(gdk.EventMask.POINTER_MOTION_MASK) + self.onPressContinue = onPressContinue + ### + self.connect('button-press-event', self.buttonPress) + ### + optionsWidget = gtk.VBox() + ## + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Style'))) + self.styleCombo = gtk.ComboBoxText() + for item in self.styleList: + self.styleCombo.append_text(_(item)) + pack(hbox, self.styleCombo) + pack(optionsWidget, hbox) + ## + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Icon Size'))) + self.iconSizeCombo = gtk.ComboBoxText() + for (i, item) in enumerate(ud.iconSizeList): + self.iconSizeCombo.append_text(_(item[0])) + pack(hbox, self.iconSizeCombo) + pack(optionsWidget, hbox) + self.iconSizeHbox = hbox + ## + hbox = gtk.HBox() + pack(hbox, gtk.Label(_('Buttons Border'))) + self.buttonsBorderSpin = IntSpinButton(0, 99) + pack(hbox, self.buttonsBorderSpin) + pack(optionsWidget, hbox) + ## + self.initVars(optionsWidget=optionsWidget) + self.iconSizeCombo.connect('changed', self.iconSizeComboChanged) + self.styleCombo.connect('changed', self.styleComboChanged) + self.buttonsBorderSpin.connect('changed', self.buttonsBorderSpinChanged) + #self.styleComboChanged() + ## + #print('toolbar state', self.get_state()## STATE_NORMAL) + #self.set_state(gtk.StateType.ACTIVE)## FIXME + #self.set_property('border-width', 0) + #style = self.get_style() + #style.border_width = 10 + #self.set_style(style) + getIconSizeName = lambda self: ud.iconSizeList[self.iconSizeCombo.get_active()][0] + setIconSizeName = lambda self, size_name: self.set_icon_size(ud.iconSizeDict[size_name]) + ## gtk.Toolbar.set_icon_size was previously Deprecated, but it's not Deprecated now!! + def setButtonsBorder(self, bb): + for item in self.items: + item.set_border_width(bb) + def iconSizeComboChanged(self, combo=None): + self.setIconSizeName(self.getIconSizeName()) + def styleComboChanged(self, combo=None): + style = self.styleCombo.get_active() + self.set_style(style) + #self.showHide()## FIXME + self.iconSizeHbox.set_sensitive(style!=1) + def buttonsBorderSpinChanged(self, spin=None): + self.setButtonsBorder(self.buttonsBorderSpin.get_value()) + def moveItemUp(self, i): + button = self.items[i] + self.remove(button) + self.insert(button, i-1) + self.items.insert(i-1, self.items.pop(i)) + #def insertItem(self, item, pos): + # CustomizableCalObj.insertItem(self, pos, item) + # gtk.Toolbar.insert(self, item, pos) + # item.show() + def appendItem(self, item): + CustomizableCalObj.appendItem(self, item) + gtk.Toolbar.insert(self, item, -1) + if item.enable: + item.show() + getData = lambda self: { + 'items': self.getItemsData(), + 'iconSize': self.getIconSizeName(), + 'style': self.styleList[self.styleCombo.get_active()], + 'buttonsBorder': self.buttonsBorderSpin.get_value(), + } + def setupItemSignals(self, item): + if item.method: + if isinstance(item.method, str): + func = getattr(self.funcOwner, item.method) + else: + func = item.method + if self.onPressContinue: + child = item.get_child() + child.connect('button-press-event', lambda obj, ev: self.itemPress(func)) + child.connect('button-release-event', self.itemRelease) + else: + item.connect('clicked', func) + def setData(self, data): + for (name, enable) in data['items']: + try: + item = self.defaultItemsDict[name] + except KeyError: + myRaise() + else: + item.enable = enable + self.setupItemSignals(item) + self.appendItem(item) + ### + iconSize = data['iconSize'] + for (i, item) in enumerate(ud.iconSizeList): + if item[0]==iconSize: + self.iconSizeCombo.set_active(i) + self.setIconSizeName(iconSize) + ### + styleNum = self.styleList.index(data['style']) + self.styleCombo.set_active(styleNum) + self.set_style(styleNum) + ### + bb = data.get('buttonsBorder', 0) + self.buttonsBorderSpin.set_value(bb) + self.setButtonsBorder(bb) + ### + def itemPress(self, func): + self.lastPressTime = now() + self.remain = True + func() + gobject.timeout_add(ui.timeout_initial, self.itemPressRemain, func) + def itemPressRemain(self, func): + if self.remain and now()-self.lastPressTime>=ui.timeout_repeat/1000.0: + func() + gobject.timeout_add(ui.timeout_repeat, self.itemPressRemain, func) + def itemRelease(self, widget, event=None): + self.remain = False + def buttonPress(self, obj, gevent): + if ui.mainWin: + if gevent.button==1: + ui.mainWin.begin_move_drag(gevent.button, int(gevent.x_root), int(gevent.y_root), gevent.time) + elif gevent.button==3: + ui.mainWin.menuMainPopup(self, gevent.time, gevent.x, gevent.y) + #self.emit('popup-main-menu', gevent.time, gevent.x, gevent.y) + return False + diff --git a/scal3/ui_gtk/tree_utils.py b/scal3/ui_gtk/tree_utils.py new file mode 100644 index 000000000..fc231850f --- /dev/null +++ b/scal3/ui_gtk/tree_utils.py @@ -0,0 +1,7 @@ +tree_path_split = lambda p: [int(x) for x in p.split(':')] + +def getTreeviewPathStr(path): + if not path: + return None + return '/'.join([str(x) for x in path]) + diff --git a/scal3/ui_gtk/utils.py b/scal3/ui_gtk/utils.py new file mode 100644 index 000000000..4337212e4 --- /dev/null +++ b/scal3/ui_gtk/utils.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from time import time as now +import os +from os.path import join, isabs +from subprocess import Popen + +from scal3.utils import myRaise +from scal3.utils import toBytes, toStr +from scal3.json_utils import * +from scal3.path import pixDir, rootDir +from scal3.cal_types import calTypes +from scal3 import core +from scal3.locale_man import tr as _ +from scal3 import ui + +from gi.repository.GObject import timeout_add +from gi.repository import GdkPixbuf + +from scal3.ui_gtk import * + + + +def hideList(widgets): + for w in widgets: + w.hide() + +def showList(widgets): + for w in widgets: + w.show() + + +def set_tooltip(widget, text): + try: + widget.set_tooltip_text(text)## PyGTK 2.12 or above + except AttributeError: + try: + widget.set_tooltip(gtk.Tooltips(), text) + except: + myRaise(__file__) + +buffer_get_text = lambda b: b.get_text(b.get_start_iter(), b.get_end_iter(), True) + +def setClipboard(text, clipboard=None): + if not clipboard: + clipboard = gtk.Clipboard.get(gdk.SELECTION_CLIPBOARD) + clipboard.set_text( + toStr(text), + len(toBytes(text)), + ) + #clipboard.store() ## ?????? No need! + +def imageFromFile(path):## the file must exist + if not isabs(path): + path = join(pixDir, path) + im = gtk.Image() + try: + im.set_from_file(path) + except: + myRaise() + return im + +def pixbufFromFile(path):## the file may not exist + if not path: + return None + if not isabs(path): + path = join(pixDir, path) + try: + return GdkPixbuf.Pixbuf.new_from_file(path) + except: + myRaise() + return None + +def toolButtonFromStock(stock, size): + tb = gtk.ToolButton() + tb.set_icon_widget(gtk.Image.new_from_stock(stock, size)) + return tb + +def labelStockMenuItem(label, stock=None, func=None, *args): + item = ImageMenuItem(_(label)) + item.set_use_underline(True) + if stock: + item.set_image(gtk.Image.new_from_stock(stock, gtk.IconSize.MENU)) + if func: + item.connect('activate', func, *args) + return item + +def labelImageMenuItem(label, image, func=None, *args): + item = ImageMenuItem(_(label)) + item.set_use_underline(True) + item.set_image(imageFromFile(image)) + if func: + item.connect('activate', func, *args) + return item + +def labelMenuItem(label, func=None, *args): + item = MenuItem(_(label)) + if func: + item.connect('activate', func, *args) + return item + +getStyleColor = lambda widget, state=gtk.StateType.NORMAL:\ + widget.get_style_context().get_color(state) + + +def modify_bg_all(widget, state, gcolor): + print(widget.__class__.__name__) + widget.modify_bg(state, gcolor) + try: + children = widget.get_children() + except AttributeError: + pass + else: + for child in children: + modify_bg_all(child, state, gcolor) + + +rectangleContainsPoint = lambda r, x, y: r.x <= x < r.x + r.width and r.y <= y < r.y + r.height + +def dialog_add_button(dialog, stock, label, resId, onClicked=None, tooltip=''): + b = dialog.add_button(stock, resId) + if ui.autoLocale: + if label: + b.set_label(label) + b.set_image(gtk.Image.new_from_stock(stock, gtk.IconSize.BUTTON)) + if onClicked: + b.connect('clicked', onClicked) + if tooltip: + set_tooltip(b, tooltip) + return b + +def confirm(msg, parent=None): + win = gtk.MessageDialog( + parent=parent, + flags=0, + type=gtk.MessageType.INFO, + buttons=gtk.ButtonsType.NONE, + message_format=msg, + ) + dialog_add_button(win, gtk.STOCK_CANCEL, _('_Cancel'), gtk.ResponseType.CANCEL) + dialog_add_button(win, gtk.STOCK_OK, _('_OK'), gtk.ResponseType.OK) + ok = win.run() == gtk.ResponseType.OK + win.destroy() + return ok + +def showMsg(msg, parent, msg_type): + win = gtk.MessageDialog( + parent=parent, + flags=0, + type=msg_type, + buttons=gtk.ButtonsType.NONE, + message_format=msg, + ) + dialog_add_button(win, gtk.STOCK_CLOSE, _('_Close'), gtk.ResponseType.OK) + win.run() + win.destroy() + +def showError(msg, parent=None): + showMsg(msg, parent, gtk.MessageType.ERROR) + +def showInfo(msg, parent=None): + showMsg(msg, parent, gtk.MessageType.INFO) + +def openWindow(win): + win.set_keep_above(ui.winKeepAbove) + win.present() + +def get_menu_width(menu): + ''' + #print(menu.has_screen()) + #menu.show_all() + #menu.realize() + print( + menu.get_border_width(), + max_item_width, + menu.get_allocation().width, + menu.size_request().width, + menu.get_size_request()[0], + menu.get_preferred_width(), + #menu.do_get_preferred_width(), + menu.get_preferred_size()[0].width, + menu.get_preferred_size()[1].width, + ) + ''' + w = menu.get_allocation().width + if w > 1: + #print(w-max(item.size_request().width for item in menu.get_children())) + return w + items = menu.get_children() + if items: + mw = max(item.size_request().width for item in items) + return mw + 56 ## FIXME + return 0 + + +class IdComboBox(gtk.ComboBox): + def set_active(self, _id): + ls = self.get_model() + for i in range(len(ls)): + if ls[i][0]==_id: + gtk.ComboBox.set_active(self, i) + return + def get_active(self): + i = gtk.ComboBox.get_active(self) + if i is None: + return + try: + return self.get_model()[i][0] + except IndexError: + return + +class CopyLabelMenuItem(MenuItem): + def __init__(self, label): + MenuItem.__init__(self) + self.set_label(label) + self.connect('activate', self.on_activate) + def on_activate(self, item): + setClipboard(self.get_property('label')) + + +if __name__=='__main__': + diolog = gtk.Dialog(parent=None) + w = TimeZoneComboBoxEntry() + pack(diolog.vbox, w) + diolog.vbox.show_all() + diolog.run() + + diff --git a/scal3/ui_gtk/windows.py b/scal3/ui_gtk/windows.py new file mode 100644 index 000000000..c766afbac --- /dev/null +++ b/scal3/ui_gtk/windows.py @@ -0,0 +1,10 @@ +def setupMenuHideOnLeave(menu): + def menuLeaveNotify(m, e): + t0 = now() + if t0-m.lastLeaveNotify < 0.001: + timeout_add(500, m.hide) + m.lastLeaveNotify = t0 + menu.lastLeaveNotify = 0 + menu.connect('leave-notify-event', menuLeaveNotify) + + diff --git a/scal3/ui_gtk/wizard.py b/scal3/ui_gtk/wizard.py new file mode 100644 index 000000000..f8302154c --- /dev/null +++ b/scal3/ui_gtk/wizard.py @@ -0,0 +1,53 @@ +from scal3.ui_gtk import * +from scal3.ui_gtk.utils import hideList + +class WizardWindow(gtk.Window): + stepClasses = [] + def __init__(self, title): + gtk.Window.__init__(self) + self.set_title(title) + self.connect('delete-event', lambda obj, e: self.destroy()) + self.connect('key-press-event', self.keyPress) + self.vbox = gtk.VBox() + self.add(self.vbox) + #### + self.steps = [] + for cls in self.stepClasses: + step = cls(self) + self.steps.append(step) + pack(self.vbox, step, 1, 1) + self.stepIndex = 0 + #### + self.buttonBox = gtk.HButtonBox() + self.buttonBox.set_layout(gtk.ButtonBoxStyle.END) + self.buttonBox.set_spacing(15) + self.buttonBox.set_border_width(15) + pack(self.vbox, self.buttonBox) + #### + self.showStep(0) + self.vbox.show() + #self.vbox.pack_end( + #print(id(self.get_action_area())) + def keyPress(self, arg, gevent): + kname = gdk.keyval_name(gevent.keyval).lower() + if kname=='escape': + self.destroy() + return True + def showStep(self, stepIndex, *args): + step = self.steps[stepIndex] + step.run(*args) + hideList(self.steps) + step.show() + self.stepIndex = stepIndex + ### + bbox = self.buttonBox + for child in bbox.get_children(): + child.destroy() + for label, func in step.buttons: + #print(label, func) + button = gtk.Button(label) + button.connect('clicked', func) + bbox.add(button) + #pack(bbox, button) + bbox.show_all() + diff --git a/scal3/ui_gtk/year_wheel.py b/scal3/ui_gtk/year_wheel.py new file mode 100644 index 000000000..8a12c2d9f --- /dev/null +++ b/scal3/ui_gtk/year_wheel.py @@ -0,0 +1,316 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import sys + +import math +from math import pi, sin, cos + +from scal3.cal_types import calTypes, to_jd, jd_to +from scal3 import core +from scal3 import locale_man +from scal3.locale_man import tr as _ +from scal3.locale_man import getMonthName +from scal3.season import getSpringJdAfter + +from scal3 import ui + +from scal3.ui_gtk import * +from scal3.ui_gtk.decorators import * +from scal3.ui_gtk.drawing import * +from scal3.ui_gtk import gtk_ud as ud + + +@registerSignals +class YearWheel(gtk.DrawingArea, ud.BaseCalObj): + _name = 'yearWheel' + desc = _('Year Wheel') + ### + scrollRotateDegree = 1 + ### + bgColor = (0, 0, 0, 255) + wheelBgColor = (255, 255, 255, 30) + lineColor = (255, 255, 255, 50) + yearStartLineColor = (255, 255, 0, 255) + lineWidth = 2.0 + textColor = (255, 255, 255, 255) + innerCircleRatio = 0.6 + ### + todayIndicatorEnable = True + todayIndicatorColor = (255, 0, 0, 255) + todayIndicatorWidth = 0.5 + ### + centerR = 3 + centerColor = (255, 0, 0, 255) + ### + springColor = (0, 255, 0, 15) + summerColor = (255, 0, 0, 15) + autumnColor = (255, 255, 0, 15) + winterColor = (0, 0, 255, 15) + ### + def __init__(self, closeFunc): + gtk.DrawingArea.__init__(self) + self.add_events(gdk.EventMask.ALL_EVENTS_MASK) + self.initVars() + ### + self.angleOffset = 0.0 + ### + #self.closeFunc = closeFunc + self.connect('draw', self.onDraw) + self.connect('scroll-event', self.onScroll) + self.connect('button-press-event', self.onButtonPress) + #self.connect('motion-notify-event', self.onMotionNotify) + #self.connect('button-release-event', self.onButtonRelease) + #self.connect('key-press-event', self.onKeyPress) + #self.connect('event', show_event) + + def onDraw(self, widget=None, event=None): + cr = self.get_window().cairo_create() + width = float(self.get_allocation().width) + height = float(self.get_allocation().height) + dia = min(width, height) + maxR = float(dia) / 2.0 + minR = self.innerCircleRatio * maxR + x0 = (width - dia) / 2.0 + y0 = (height - dia) / 2.0 + cx = x0 + maxR + cy = y0 + maxR + #### + #self.angleOffset + #self.bgColor + #self.wheelBgColor + #self.lineColor + #self.lineWidth + #self.textColor + #### + cr.rectangle(0, 0, width, height) + fillColor(cr, self.bgColor) + #### + calsN = len(calTypes.active) + deltaR = (maxR - minR) / float(calsN) + mode0 = calTypes.active[0] + jd0 = to_jd(ui.todayCell.year, 1, 1, mode0) + yearLen = calTypes.primaryModule().avgYearLen + angle0 = self.angleOffset * pi / 180.0 - pi/2.0 + avgDeltaAngle = 2*pi / 12 + #### + if self.todayIndicatorEnable: + drawLineLengthAngle( + cr, + cx, + cy, + maxR,## FIXME + angle0 + 2.0*pi*(ui.todayCell.jd - jd0)/yearLen, + self.todayIndicatorWidth, + ) + fillColor(cr, self.todayIndicatorColor) + #### + drawCircle(cr, cx, cy, self.centerR) + fillColor(cr, self.centerColor) + #### + drawCircleOutline(cr, cx, cy, maxR, maxR-minR) + fillColor(cr, self.wheelBgColor) + #### + spinngJd = getSpringJdAfter(jd0) + springAngle = angle0 + 2.0*pi*(spinngJd - jd0)/yearLen ## radians + for index, color in enumerate(( + self.springColor, + self.summerColor, + self.autumnColor, + self.winterColor, + )): + drawArcOutline( + cr, + cx, + cy, + maxR, + maxR-minR, + springAngle + index * pi/2.0, + springAngle + (index+1) * pi/2.0, + ) + fillColor(cr, color) + #### + for index, mode in enumerate(calTypes.active): + dr = index * deltaR + r = maxR - dr + cx0 = x0 + dr + cy0 = y0 + dr + ### + drawCircleOutline(cr, cx, cy, r, self.lineWidth) + fillColor(cr, self.lineColor) + #### + year0, month0, day0 = jd_to(jd0, mode) + ym0 = year0*12 + (month0-1) + cr.set_line_width(self.lineWidth) + for ym in range(ym0, ym0 + 12): + year, month = divmod(ym, 12) ; month += 1 + jd = to_jd(year, month, 1, mode) + angle = angle0 + 2.0*pi*(jd - jd0)/yearLen ## radians + #angleD = angle * 180 / pi + #print('mode=%s, year=%s, month=%s, angleD=%.1f'%(mode, year, month, angleD)) + d = self.lineWidth + sepX, sepY = goAngle( + cx, + cy, + angle, + r - d*0.2,## FIXME + ) + drawLineLengthAngle( + cr, + sepX, + sepY, + deltaR - d*0.2,## FIXME + angle + pi, + d, + ) + fillColor( + cr, + self.yearStartLineColor if month==1 else self.lineColor, + ) + ### + layoutMaxW = (r - deltaR) * 2.0 * pi / 12.0 + layoutMaxH = deltaR + layout = newTextLayout( + self, + text=getMonthName(mode, month, year), + maxSize=(layoutMaxW, layoutMaxH), + maximizeScale=0.6, + truncate=False, + ) + layoutW, layoutH = layout.get_pixel_size() + centerAngle = angle + avgDeltaAngle/2.0 + lx, ly = goAngle( + cx, + cy, + centerAngle, + (r - deltaR/3.0), + ) + lx, ly = goAngle( + lx, + ly, + angle - pi/2.0, + layoutW / 2.0, + ) + lx, ly = goAngle( + lx, + ly, + angle, + layoutH / 2.0, + ) + cr.move_to( + lx, + ly, + ) + #cr.save() + rotateAngle = centerAngle + pi/2.0 + cr.rotate(rotateAngle) + setColor(cr, self.textColor) ; show_layout(cr, layout) + cr.rotate(-rotateAngle) + #cr.restore() + ##### + drawCircleOutline(cr, cx, cy, minR, self.lineWidth) + fillColor(cr, self.lineColor) + ### + + + + def onScroll(self, widget, gevent): + d = getScrollValue(gevent) + #print('onScroll', d) + self.angleOffset += (-1 if d=='up' else 1) * self.scrollRotateDegree + self.queue_draw() + return True + def onKeyPress(self, arg, gevent): + return False + def onButtonPress(self, obj, gevent): + x = gevent.x + y = gevent.y + w = self.get_allocation().width + h = self.get_allocation().height + #if gevent.button==1: + # #self.begin_move_drag(gevent.button, int(gevent.x_root), int(gevent.y_root), gevent.time) + # #return True + #elif gevent.button==3: + # pass + return False + + +@registerSignals +class YearWheelWindow(gtk.Window, ud.BaseCalObj): + _name = 'yearWheelWin' + desc = _('Year Wheel') + def __init__(self): + gtk.Window.__init__(self) + self.initVars() + ud.windowList.appendItem(self) + ### + size = min(ud.screenW, ud.screenH) * 0.9 + self.resize(size, size) + self.move( + (ud.screenW-size)/2.0, + (ud.screenH-size)/2.0, + ) + self.set_title(self.desc) + self.set_decorated(False) + self.connect('delete-event', self.closeClicked) + self.connect('button-press-event', self.onButtonPress) + ### + self._widget = YearWheel(self.closeClicked) + self.connect('key-press-event', self._widget.onKeyPress) + self.add(self._widget) + self._widget.show() + self.appendItem(self._widget) + def closeClicked(self, arg=None, event=None): + if ui.mainWin: + self.hide() + else: + self.destroy() + core.stopRunningThreads() + gtk.main_quit() + return True + def onButtonPress(self, obj, gevent): + if gevent.button==1: + self.begin_move_drag(gevent.button, int(gevent.x_root), int(gevent.y_root), gevent.time) + return True + return False + + + +if __name__=='__main__': + #locale_man.langActive = '' + #_ = locale_man.loadTranslator() + ui.init() + win = YearWheelWindow() + win.show() + gtk.main() + + + + + + + + + + + + + + diff --git a/scal3/utils.py b/scal3/utils.py new file mode 100644 index 000000000..89b1d26aa --- /dev/null +++ b/scal3/utils.py @@ -0,0 +1,331 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +import sys, os +from math import floor, ceil + +from scal3.lib import OrderedDict + +try: + from collections import Iterable +except ImportError: + class Iterable: + + def __iter__(self): + raise NotImplementedError + + @classmethod + def __subclasshook__(cls, C): + if cls is Iterable: + if any('__iter__' in B.__dict__ for B in C.__mro__): + return True + return NotImplemented + +try: + from collections import Iterator +except ImportError: + class Iterator(Iterable): + + def __next__(self): + raise StopIteration + + __iter__ = lambda self: self + + @classmethod + def __subclasshook__(cls, C): + if cls is Iterator: + if (any('__next__' in B.__dict__ for B in C.__mro__) and + any('__iter__' in B.__dict__ for B in C.__mro__)): + return True + return NotImplemented + + + + +ifloor = lambda x: int(floor(x)) +iceil = lambda x: int(ceil(x)) + +def arange(start, stop, step): + l = [] + x = start + stop -= 0.000001 + while x < stop: + l.append(x) + x += step + return l + +toBytes = lambda s: s.encode('utf8') if isinstance(s, str) else bytes(s) +toStr = lambda s: str(s, 'utf8') if isinstance(s, bytes) else str(s) + +cmp = lambda a, b: 0 if a==b else (1 if a>b else -1) + +def versionLessThan(v0, v1): + if v0=='': + if v1=='': + return 0 + else: + return -1 + elif v1=='': + return 1 + return [ int(p) for p in v0.split('.') ] < [ int(p) for p in v1.split('.') ] + +def printError(text): + sys.stderr.write('%s\n'%text) + +class FallbackLogger: + def __init__(self): + pass + def error(self, text): + sys.stderr.write('ERROR: %s\n'%text) + def warning(self, text): + print('WARNING: %s'%text) + def debug(self, text): + print(text) + +def myRaise(File=None): + i = sys.exc_info() + typ, value, tback = sys.exc_info() + text = 'line %s: %s: %s\n'%(tback.tb_lineno, typ.__name__, value) + if File: + text = 'File "%s", '%File + text + sys.stderr.write(text) + +def myRaiseTback(): + import traceback + typ, value, tback = sys.exc_info() + sys.stderr.write("".join(traceback.format_exception(typ, value, tback))) + +restartLow = lambda: os.execl( + sys.executable, + sys.executable, + *sys.argv +)## will not return from the function + +class StrOrderedDict(dict): + ## A dict from strings to objects, with ordered keys + ## and some looks like a list + def __init__(self, arg=[], reorderOnModify=True): + self.reorderOnModify = reorderOnModify + if isinstance(arg, (list, tuple)): + self.keyList = [item[0] for item in arg] + elif isinstance(arg, dict): + self.keyList = sorted(arg.keys()) + else: + raise TypeError('StrOrderedDict: bad type for first argument: %s'%type(arg)) + dict.__init__(self, arg) + keys = lambda self: self.keyList + values = lambda self: [dict.__getitem__(self, key) for key in self.keyList] + items = lambda self: [(key, dict.__getitem__(self, key)) for key in self.keyList] + def __getitem__(self, arg): + if isinstance(arg, int): + return dict.__getitem__(self, self.keyList[arg]) + elif isinstance(arg, str): + return dict.__getitem__(self, arg) + elif isinstance(arg, slice):## not tested FIXME + return StrOrderedDict([ + (key, dict.__getitem__(self, key)) \ + for key in self.keyList.__getitem__(arg) + ]) + else: + raise ValueError('Bad type argument given to StrOrderedDict.__getitem__: %s'%type(arg)) + def __setitem__(self, arg, value): + if isinstance(arg, int): + dict.__setitem__(self, self.keyList[arg], value) + elif isinstance(arg, str): + if arg in self.keyList:## Modifying value for an existing key + if reorderOnModify: + self.keyList.remove(arg) + self.keyList.append(arg) + #elif isinstance(arg, slice):## ???????????? is not tested + # #assert isinstance(value, StrOrderedDict) + # if isinstance(value, StrOrderedDict): + # for key in self.keyList.__getitem__(arg): + else: + self.keyList.append(arg) + dict.__setitem__(self, arg, value) + else: + raise ValueError('Bad type argument given to StrOrderedDict.__setitem__: %s' + %type(item)) + def __delitem__(self, arg): + if isinstance(arg, int): + self.keyList.__delitem__(arg) + dict.__delitem__(self, self.keyList[arg]) + elif isinstance(arg, str): + self.keyList.remove(arg) + dict.__delitem__(self, arg) + elif isinstance(arg, slice):## ???????????? is not tested + for key in self.keyList.__getitem__(arg): + dict.__delitem__(self, key) + self.keyList.__delitem__(arg) + else: + raise ValueError('Bad type argument given to StrOrderedDict.__delitem__: %s'%type(arg)) + pop = lambda self, key: self.__delitem__(key) + def clear(self): + self.keyList = [] + dict.clear(self) + def append(self, key, value): + assert isinstance(key, str) and not key in self.keyList + self.keyList.append(key) + dict.__setitem__(self, key, value) + def insert(self, index, key, value): + assert isinstance(key, str) and not key in self.keyList + self.keyList.insert(index, key) + dict.__setitem__(self, key, value) + def sort(self, attr=None): + if attr==None: + self.keyList.sort() + else: + self.keyList.sort(key=lambda k: getattr(dict.__getitem__(self, k), attr)) + __iter__ = lambda self: self.keyList.__iter__() + def iteritems(self):## OR lambda self: self.items().__iter__() + for key in self.keyList:## OR self.keyList.__iter__() + yield (key, dict.__getitem__(self, key)) + __str__ = lambda self: 'StrOrderedDict(%r)'%self.items() + #'StrOrderedDict{' + ', '.join([repr(k)+':'+repr(self[k]) for k in self.keyList]) + '}' + __repr__ = lambda self: 'StrOrderedDict(%r)'%self.items() + + +class NullObj:## a fully transparent object + def __setattr__(self, attr, value): + pass + __getattr__ = lambda self, attr: self + __call__ = lambda self, *args, **kwargs: self + __str__ = lambda self: '' + __repr__ = lambda self: '' + __int__ = lambda self: 0 + + +int_split = lambda s: [int(x) for x in s.split()] + +s_join = lambda l: ' '.join([str(x) for x in l]) + + +def cleanCacheDict(cache, maxSize, currentValue): + n = len(cache) + if n >= maxSize > 2: + keys = sorted(cache.keys()) + if keys[n//2] < currentValue: + rm = keys[0] + else: + rm = keys[-1] + cache.pop(rm) + +def urlToPath(url): + if len(url)<7: + return url + if url[:7]!='file://': + return url + path = url[7:] + if path[-2:]=='\r\n': + path = path[:-2] + elif path[-1]=='\r': + path = path[:-1] + ## here convert html unicode symbols to utf8 string: + if not '%' in path: + return path + path2 = '' + n = len(path) + i = 0 + while i 1: + values.append(( + int(pparts[0]), + int(pparts[1]), + )) + except: + myRaise() + return values + +def inputDate(msg): + while True: + try: + date = input(msg) + except KeyboardInterrupt: + return + if date.lower() == 'q': + return + try: + return dateDecode(date) + except Exception as e: + print(str(e)) + +def inputDateJd(msg): + date = inputDate(msg) + if date: + y, m, d = date + return to_jd(y, m, d, DATE_GREG) + + +#if __name__=='__main__': +# print(findNearestNum([1, 2, 4, 6, 3, 7], 3.6)) + + + diff --git a/scal3/vcs_modules/__init__.py b/scal3/vcs_modules/__init__.py new file mode 100644 index 000000000..5a01bed43 --- /dev/null +++ b/scal3/vcs_modules/__init__.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +from scal3.utils import myRaise +from scal3.utils import toStr +from scal3.time_utils import getEpochFromJd + +def encodeShortStat(files_changed, insertions, deletions): + parts = [] + if files_changed == 1: + parts.append('1 file changed') + else: + parts.append('%d files changed'%files_changed) + if insertions > 0: + parts.append('%d insertions(+)'%insertions) + if deletions > 0: + parts.append('%d deletions(-)'%deletions) + return ', '.join(parts) + +def getCommitListFromEst(obj, startJd, endJd, format_rev_id=None): + ''' + returns a list of (epoch, rev_id) tuples + ''' + startEpoch = getEpochFromJd(startJd) + endEpoch = getEpochFromJd(endJd) + ### + data = [] + for t0, t1, rev_id, dt in obj.est.search(startEpoch, endEpoch): + if format_rev_id: + rev_id = format_rev_id(obj.repo, rev_id) + data.append((t0, rev_id)) + data.sort(reverse=True) + return data + + +vcsModuleNames = [ + 'git', + 'hg', + 'bzr', +] + + + + + + diff --git a/scal3/vcs_modules/bzr.py b/scal3/vcs_modules/bzr.py new file mode 100644 index 000000000..f5b9d181c --- /dev/null +++ b/scal3/vcs_modules/bzr.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from difflib import SequenceMatcher + +from scal3.utils import NullObj +from scal3.time_utils import getEpochFromJd +from scal3.vcs_modules import encodeShortStat, getCommitListFromEst +from scal3.event_search_tree import EventSearchTree + +from bzrlib.bzrdir import BzrDir +from bzrlib.diff import DiffText +from bzrlib import revision as _mod_revision +from bzrlib.osutils import split_lines + +def prepareObj(obj): + tree, branch, repo, relpath = \ + BzrDir.open_containing_tree_branch_or_repository(obj.vcsDir) + obj.branch = branch + obj.repo = repo + ### + obj.est = EventSearchTree() + obj.firstRev = None + obj.lastRev = None + for rev_id, depth, revno, end_of_merge in \ + branch.iter_merge_sorted_revisions(direction='forward'): + rev = obj.repo.get_revision(rev_id) + epoch = rev.timestamp + obj.est.add(epoch, epoch, rev_id) + if not obj.firstRev: + obj.firstRev = rev + obj.lastRev = rev + + +def clearObj(obj): + obj.branch = None + obj.repo = None + obj.est = EventSearchTree() + +def getCommitList(obj, startJd, endJd): + ''' + returns a list of (epoch, rev_id) tuples + ''' + return getCommitListFromEst( + obj, + startJd, + endJd, + ) + + +def getCommitInfo(obj, rev_id): + rev = obj.repo.get_revision(rev_id) + lines = rev.message.split('\n') + return { + 'epoch': rev.timestamp, + 'author': rev.committer, + 'shortHash': rev_id, + 'summary': lines[0], + 'description': '\n'.join(lines[1:]), + } + + +def getShortStat(obj, old_rev_id, rev_id): + repo = obj.repo + return getShortStatByTrees( + repo, + repo.revision_tree(old_rev_id), + repo.revision_tree(rev_id), + ) + +def getShortStatByTrees(repo, old_tree, tree): + files_changed = 0 + insertions = 0 + deletions = 0 + #### + tree.lock_read() + for file_id, (old_path, new_path), changed_content,\ + versioned, parent, name, (old_kind, new_kind), executable in tree.iter_changes(old_tree): + if changed_content: + #for kind in (old_kind, new_kind): + # if not kind in (None, 'file', 'symlink', 'directory'): + # print('kind', old_kind, new_kind) + if new_kind in ('file', 'symlink'): + files_changed += 1 + text = tree.get_file_text(file_id) + if not '\x00' in text[:1024]:## FIXME + if old_kind == None: + insertions += len(split_lines(text)) + elif old_kind in ('file', 'symlink'): + old_text = old_tree.get_file_text(file_id) + seq = SequenceMatcher( + None, + split_lines(old_text), + split_lines(text), + ) + for op, i1, i2, j1, j2 in seq.get_opcodes(): + if op == 'equal': + continue + #if not op in ('insert', 'delete', 'replace'): + # print('op', op) + insertions += (j2 - j1) + deletions += (i2 - i1) + elif new_kind == None: + if old_kind in ('file', 'symlink'): + files_changed += 1 + old_text = old_tree.get_file_text(file_id) + if not '\x00' in old_text[:1024]:## FIXME + deletions += len(split_lines(old_text)) + return files_changed, insertions, deletions + + +def getCommitShortStat(obj, rev_id): + ''' + returns (files_changed, insertions, deletions) + ''' + repo = obj.repo + rev = repo.get_revision(rev_id) + tree = repo.revision_tree(rev_id) + try: + old_rev_id = rev.parent_ids[0] + except IndexError: + old_rev_id = _mod_revision.NULL_REVISION + return getShortStatByTrees( + repo, + repo.revision_tree(old_rev_id), + tree, + ) + +## returns str +getCommitShortStatLine = lambda obj, rev_id: encodeShortStat(*getCommitShortStat(obj, rev_id)) + + +def getTagList(obj, startJd, endJd): + ''' + returns a list of (epoch, tag_name) tuples + ''' + if not obj.repo: + return [] + startEpoch = getEpochFromJd(startJd) + endEpoch = getEpochFromJd(endJd) + ### + data = [] + for tag, rev_id in obj.branch.tags.get_tag_dict().items(): + rev = obj.repo.get_revision(rev_id) + epoch = rev.timestamp + if startEpoch <= epoch < endEpoch: + data.append(( + epoch, + tag, + )) + data.sort() + return data + +def getTagShortStat(obj, prevTag, tag): + ''' + returns (files_changed, insertions, deletions) + ''' + repo = obj.repo + td = obj.branch.tags.get_tag_dict() + return getShortStatByTrees( + repo, + repo.revision_tree(td[prevTag] if prevTag else None), + repo.revision_tree(td[tag]), + ) + + +## returns str +getTagShortStatLine = lambda obj, prevTag, tag:\ + encodeShortStat(*getTagShortStat(obj, prevTag, tag)) + +getFirstCommitEpoch = lambda obj: obj.firstRev.timestamp + +getLastCommitEpoch = lambda obj: obj.lastRev.timestamp + +def getLastCommitIdUntilJd(obj, jd): + untilEpoch = getEpochFromJd(jd) + last = obj.est.getLastBefore(untilEpoch) + if not last: + return + t0, t1, rev_id = last + return str(obj.repo.get_revision(rev_id)) + + + + diff --git a/scal3/vcs_modules/git.py b/scal3/vcs_modules/git.py new file mode 100644 index 000000000..b87844e0b --- /dev/null +++ b/scal3/vcs_modules/git.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from os.path import join +from subprocess import Popen, PIPE + +from scal3.utils import toStr +from scal3.time_utils import getEpochFromJd, encodeJd + +def prepareObj(obj): + pass + +def clearObj(obj): + pass + + +def decodeStatLine(line): + if not line: + return 0, 0, 0 + files_changed, insertions, deletions = 0, 0, 0 + for section in line.split(','): + parts = section.strip().split(' ') + if len(parts) < 2: + continue + try: + num = int(parts[0]) + except: + print('bad section: %r, stat line=%r'%(section, line)) + else: + action = parts[-1].strip() + if 'changed' in action: + files_changed = num + elif 'insertions' in action: + insertions = num + elif 'deletions' in action: + deletions = num + return files_changed, insertions, deletions + +def getCommitList(obj, startJd=None, endJd=None): + ''' + returns a list of (epoch, commit_id) tuples + ''' + cmd = [ + 'git', + '--git-dir', join(obj.vcsDir, '.git'), + 'log', + '--format=%ct %H',## or '--format=%ct %H' + ] + if startJd is not None: + cmd += [ + '--since', + encodeJd(startJd), + ] + if endJd is not None: + cmd += [ + '--until', + encodeJd(endJd), + ] + data = [] + for line in Popen(cmd, stdout=PIPE).stdout: + line = toStr(line) + parts = line.strip().split(' ') + data.append(( + int(parts[0]), + parts[1], + )) + return data + + +def getCommitInfo(obj, commid_id): + cmd = [ + 'git', + '--git-dir', join(obj.vcsDir, '.git'), + 'log', + '-1', + '--format=%at\n%cn <%ce>\n%h\n%s', + commid_id, + ] + parts = Popen(cmd, stdout=PIPE).stdout.read().strip().split('\n') + if not parts: + return + return { + 'epoch': int(parts[0]), + 'author': parts[1], + 'shortHash': parts[2], + 'summary': parts[3], + 'description': '\n'.join(parts[4:]), + } + +def getShortStatLine(obj, prevId, thisId): + ''' + returns str + ''' + cmd = [ + 'git', + '--git-dir', join(obj.vcsDir, '.git'), + 'diff', + '--shortstat', + prevId, + thisId, + ] + return toStr(Popen(cmd, stdout=PIPE).stdout.read().strip()) + +getShortStat = lambda obj, prevId, thisId: decodeStatLine(getShortStatLine(obj, prevId, thisId)) + + +def getCommitShortStatLine(obj, commit_id): + ''' + returns str + ''' + lines = Popen([ + 'git', + '--git-dir', join(obj.vcsDir, '.git'), + 'log', + '--shortstat', + '-1', + '--pretty=format:', + commit_id, + ], stdout=PIPE).stdout.readlines() + if lines: + return lines[-1].strip() + return '' + + +## returns (files_changed, insertions, deletions) +getCommitShortStat = lambda obj, commit_id: decodeStatLine(getCommitShortStatLine(obj.vcsDir, commit_id)) + +def getTagList(obj, startJd, endJd): + ''' + returns a list of (epoch, tag_name) tuples + ''' + startEpoch = getEpochFromJd(startJd) + endEpoch = getEpochFromJd(endJd) + cmd = [ + 'git', + '--git-dir', join(obj.vcsDir, '.git'), + 'tag', + ] + data = [] + for line in Popen(cmd, stdout=PIPE).stdout: + tag = line.strip() + if not tag: + continue + line = Popen([ + 'git', + '--git-dir', join(obj.vcsDir, '.git'), + 'log', + '-1', + tag, + '--pretty=%ct', + ], stdout=PIPE).stdout.read().strip() + epoch = int(line) + if epoch < startEpoch: + continue + if epoch >= endEpoch: + break + data.append(( + epoch, + tag, + )) + return data + +getTagShortStatLine = lambda obj, prevTag, tag: getShortStatLine(obj, prevTag, tag) + +getFirstCommitEpoch = lambda obj: int( + Popen([ + 'git', + '--git-dir', join(obj.vcsDir, '.git'), + 'rev-list', + '--max-parents=0', + 'HEAD', + '--format=%ct', + ], stdout=PIPE).stdout.readlines()[1].strip() +) + + +getLastCommitEpoch = lambda obj: int(Popen([ + 'git', + '--git-dir', join(obj.vcsDir, '.git'), + 'log', + '-1', + '--format=%ct', +], stdout=PIPE).stdout.read().strip()) + + +getLastCommitIdUntilJd = lambda obj, jd: Popen([ + 'git', + '--git-dir', join(obj.vcsDir, '.git'), + 'log', + '--until', encodeJd(jd), + '-1', + '--format=%H', +], stdout=PIPE).stdout.read().strip() + + + diff --git a/scal3/vcs_modules/hg.py b/scal3/vcs_modules/hg.py new file mode 100644 index 000000000..8dbe6a9ad --- /dev/null +++ b/scal3/vcs_modules/hg.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +from scal3.time_utils import getEpochFromJd +from scal3.vcs_modules import encodeShortStat, getCommitListFromEst +from scal3.event_search_tree import EventSearchTree + +import mercurial.ui +from mercurial.localrepo import localrepository +from mercurial.patch import diff, diffstatdata, diffstatsum +from mercurial.util import iterlines + +def prepareObj(obj): + obj.repo = localrepository(mercurial.ui.ui(), obj.vcsDir) + ### + obj.est = EventSearchTree() + for rev_id in obj.repo.changelog: + epoch = obj.repo[rev_id].date()[0] + obj.est.add(epoch, epoch, rev_id) + +def clearObj(obj): + obj.repo = None + obj.est = EventSearchTree() + + +## returns a list of (epoch, commit_id) tuples +getCommitList = lambda obj, startJd, endJd: getCommitListFromEst( + obj, + startJd, + endJd, + lambda repo, rev_id: str(repo[rev_id]) +) + + +def getCommitInfo(obj, commid_id): + ctx = obj.repo[commid_id] + lines = ctx.description().split('\n') + return { + 'epoch': ctx.date()[0], + 'author': ctx.user(), + 'shortHash': str(ctx), + 'summary': lines[0], + 'description': '\n'.join(lines[1:]), + } + + +def getShortStat(obj, node1, node2):## SLOW FIXME + repo = obj.repo + ## if not node1 ## FIXME + stats = diffstatdata( + iterlines( + diff( + repo, + str(node1), + str(node2), + ) + ) + ) + maxname, maxtotal, insertions, deletions, hasbinary = diffstatsum(stats) + return len(stats), insertions, deletions + + +def getCommitShortStat(obj, commit_id): + ''' + returns (files_changed, insertions, deletions) + ''' + ctx = obj.repo[commit_id] + return getShortStat( + obj, + ctx.p1(), + ctx, + ) + + +## returns str +getCommitShortStatLine = lambda obj, commit_id:\ + encodeShortStat(*getCommitShortStat(obj, commit_id)) + + +def getTagList(obj, startJd, endJd): + ''' + returns a list of (epoch, tag_name) tuples + ''' + if not obj.repo: + return [] + startEpoch = getEpochFromJd(startJd) + endEpoch = getEpochFromJd(endJd) + ### + data = [] + for tag, unkown in obj.repo.tagslist(): + if tag == 'tip': + continue + epoch = obj.repo[tag].date()[0] + if startEpoch <= epoch < endEpoch: + data.append(( + epoch, + tag, + )) + data.sort() + return data + +def getTagShortStat(obj, prevTag, tag): + repo = obj.repo + return getShortStat( + obj, + repo[prevTag if prevTag else 0], + repo[tag], + ) + + +## returns str +getTagShortStatLine = lambda obj, prevTag, tag: encodeShortStat(*getTagShortStat(obj, prevTag, tag)) + +getFirstCommitEpoch = lambda obj: obj.repo[0].date()[0] + +getLastCommitEpoch = lambda obj: obj.repo[len(obj.repo)-1].date()[0] + +def getLastCommitIdUntilJd(obj, jd): + untilEpoch = getEpochFromJd(jd) + last = obj.est.getLastBefore(untilEpoch) + if not last: + return + t0, t1, rev_id = last + return str(obj.repo[rev_id]) + + diff --git a/scal3/weekcal.py b/scal3/weekcal.py new file mode 100644 index 000000000..5b4ee5a97 --- /dev/null +++ b/scal3/weekcal.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Saeed Rasooli +# +# 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 . +# Also avalable in /usr/share/common-licenses/GPL on Debian systems +# or /usr/share/licenses/common/GPL3/license.txt on ArchLinux + +#from scal3.locale_man import tr as _ + +from scal3 import core +from scal3.core import myRaise, getMonthName, getMonthLen, pixDir + +from scal3 import ui + +pluginName = 'WeekCal' + +class WeekStatus(list): + ## list (of 7 cells) + def __init__(self, cellCache, absWeekNumber): + self.absWeekNumber = absWeekNumber + startJd = core.getStartJdOfAbsWeekNumber(absWeekNumber) + endJd = startJd + 7 + #self.startJd = startJd + #self.startDate = core.jd_to_primary(self.startJd) + #self.weekNumberOfYear = core.getWeekNumber(*self.startDate) + ######### + #list.__init__(self, [cellCache.getCell(jd) for jd in range(startJd, endJd)]) + list.__init__(self, []) + for jd in range(startJd, endJd): + #print('WeekStatus', jd) + self.append(cellCache.getCell(jd)) + allCells = lambda self: self + +def setParamsFunc(cell): + cell.absWeekNumber, cell.weekDayIndex = core.getWeekDateFromJd(cell.jd) + + +getWeekStatus = lambda absWeekNumber: ui.cellCache.getCellGroup(pluginName, absWeekNumber) +getCurrentWeekStatus = lambda: ui.cellCache.getCellGroup(pluginName, ui.cell.absWeekNumber) + +######################## +ui.cellCache.registerPlugin(pluginName, setParamsFunc, WeekStatus) + + + + diff --git a/scal3/windows.py b/scal3/windows.py new file mode 100644 index 000000000..fa0d555a5 --- /dev/null +++ b/scal3/windows.py @@ -0,0 +1,19 @@ +import os +from os.path import join +from scal3.core import APP_NAME + +winStartupRelPath = r'\Microsoft\Windows\Start Menu\Programs\Startup' +winStartupDir = os.getenv('APPDATA') + winStartupRelPath +#winStartupDirSys = os.getenv('ALLUSERSPROFILE') + winStartupRelPath +winStartupFile = join(winStartupDir, APP_NAME+'.lnk') + + +def winMakeShortcut(srcPath, dstPath, iconPath=None): + from win32com.client import Dispatch + shell = Dispatch('WScript.Shell') + shortcut = shell.CreateShortCut(dstPath) + shortcut.Targetpath = srcPath + #shortcut.WorkingDirectory = ... + shortcut.save() + + diff --git a/scal3/xml_utils.py b/scal3/xml_utils.py new file mode 100644 index 000000000..36a664802 --- /dev/null +++ b/scal3/xml_utils.py @@ -0,0 +1,31 @@ +#from xml.sax.saxutils import escape, unescape +def escape(data, entities={}): + """Escape &, <, and > in a string of data. + + You can escape other strings of data by passing a dictionary as + the optional entities parameter. The keys and values must all be + strings; each key will be replaced with its corresponding value. + """ + + # must do ampersand first + data = data.replace("&", "&") + data = data.replace(">", ">") + data = data.replace("<", "<") + if entities: + data = __dict_replace(data, entities) + return data + +def unescape(data, entities={}): + """Unescape &, <, and > in a string of data. + + You can unescape other strings of data by passing a dictionary as + the optional entities parameter. The keys and values must all be + strings; each key will be replaced with its corresponding value. + """ + data = data.replace("<", "<") + data = data.replace(">", ">") + if entities: + data = __dict_replace(data, entities) + # must do ampersand last + return data.replace("&", "&") + diff --git a/scripts/assert_python3 b/scripts/assert_python3 new file mode 100755 index 000000000..acc11f9da --- /dev/null +++ b/scripts/assert_python3 @@ -0,0 +1,14 @@ +#!/bin/bash +cd /usr/bin +if [ ! -f python3 ] ; then + echo "python3 command not found" + for V in 3.3 3.2 3.1 3.0 ; do + if [ -f "python$V" ] ; then + ln -s "python$V" python3 + echo "LINKING /usr/bin/python$V TO /usr/bin/python3" + break + fi + done +fi +cd - > /dev/null + diff --git a/scripts/bson_to_json.py b/scripts/bson_to_json.py new file mode 100755 index 000000000..978905989 --- /dev/null +++ b/scripts/bson_to_json.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +import sys +from os.path import splitext + +import json +from bson import BSON + +for fname_json in sys.argv[1:]: + fname, ext = splitext(fname_json) + bson_s = open(fname_json, 'rb').read() + data = BSON.decode(bson_s) + open(fname + '.json', 'w').write(json.dumps(data)) + + + diff --git a/scripts/compact_json.py b/scripts/compact_json.py new file mode 100755 index 000000000..226024f9b --- /dev/null +++ b/scripts/compact_json.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +import sys, json + +for fname in sys.argv[1:]: + data = json.loads(open(fname).read()) + jstr = json.dumps(data, sort_keys=True, separators=(',', ':')) + open(fname, 'w').write(jstr) + diff --git a/scripts/compact_json_all b/scripts/compact_json_all new file mode 100755 index 000000000..3cdc41ddf --- /dev/null +++ b/scripts/compact_json_all @@ -0,0 +1,5 @@ +#!/bin/bash +#!/bin/bash +myDir=`dirname "$0"` +find ~/.starcal3 -name '*.json' -print -exec "$myDir/compact_json.py" '{}' \; + diff --git a/scripts/json_to_bson.py b/scripts/json_to_bson.py new file mode 100755 index 000000000..47baf5505 --- /dev/null +++ b/scripts/json_to_bson.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +import sys +from os.path import splitext + +import json +from bson import BSON + +for fname_json in sys.argv[1:]: + fname, ext = splitext(fname_json) + json_s = open(fname_json).read() + data = json.loads(json_s) + open(fname + '.bson', 'wb').write(bytes(BSON.encode(data))) + + + + diff --git a/scripts/load-zoneinfo-tree.py b/scripts/load-zoneinfo-tree.py new file mode 100755 index 000000000..05ad69b86 --- /dev/null +++ b/scripts/load-zoneinfo-tree.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +import os + +try: + import json +except ImportError: + import simplejson as json + +dataToPrettyJson = lambda data: json.dumps(data, sort_keys=False, indent=4) + + +if __name__=='__main__': + zoneTree = getZoneInfoTree( + ['usr', 'share', 'zoneinfo'] + ) + #open('zoneinfo-tree.json', 'w').write( + # dataToPrettyJson(zoneTree).replace(' \n', '\n') + #) + diff --git a/scripts/post_install.py b/scripts/post_install.py new file mode 100755 index 000000000..cb64639fb --- /dev/null +++ b/scripts/post_install.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# post install script +import os, shutil + +pkgName = 'starcal3' + + + + +""" +from gi.repository import Gtk as gtk +d = gtk.Dialog() +okB = d.add_button(gtk.STOCK_OK, 1) +okB.connect('clicked', lambda obj: d.hide()) +pack(d.vbox, gtk.Label('StarCalendar post-install configuration')) +d.set_title('%s postinst'%pkgName) + + +check3 = gtk.CheckButton('Copy shortcut(x-desktop) file to Desktop') +check3.set_active(True) +pack(d.vbox, check3) + +d.vbox.show_all() +d.set_keep_above(True) +d.run() + +if check3.get_active():""" + +lines = open('/etc/passwd').read().split('\n') +for line in lines: + if line=='': + continue + line = line.replace(',' ,'') + parts = line.split(':') + uid = int(parts[2]) + username = parts[0] + if uid<1000: + continue + if username=='nobody': + continue + gid = int(parts[3]) + home = parts[5] + target = '%s/Desktop/%s.desktop'%(home, pkgName) + try: + shutil.copy('/usr/share/applications/%s.desktop'%pkgName, target) + except: + continue + print('Copying x-desktop file to %s\'s Desktop'%username) + os.chown(target, uid, gid) + + diff --git a/scripts/pre_remove.py b/scripts/pre_remove.py new file mode 100755 index 000000000..4e69da860 --- /dev/null +++ b/scripts/pre_remove.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# pre remove script + +pkgName = 'starcal3' +import os + + +lines = open('/etc/passwd').read().split('\n') +for line in lines: + if line=='': + continue + line = line.replace(',' ,'') + parts = line.split(':') + home = parts[5] + try: + os.remove('%s/Desktop/%s.desktop'%(home, pkgName)) + except: + continue + print('Removing x-desktop file from %s\'s Desktop'%parts[0]) + diff --git a/scripts/pretty_json.py b/scripts/pretty_json.py new file mode 100755 index 000000000..200bad943 --- /dev/null +++ b/scripts/pretty_json.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +import sys, json + +for fname in sys.argv[1:]: + data = json.loads(open(fname).read()) + jstr = json.dumps(data, sort_keys=True, indent=2) + open(fname, 'w').write(jstr) + diff --git a/scripts/pretty_json_all b/scripts/pretty_json_all new file mode 100755 index 000000000..7373835b7 --- /dev/null +++ b/scripts/pretty_json_all @@ -0,0 +1,5 @@ +#!/bin/bash +#!/bin/bash +myDir=`dirname "$0"` +find ~/.starcal3 -name '*.json' -print -exec "$myDir/pretty_json.py" '{}' \; + diff --git a/scripts/py2json.py b/scripts/py2json.py new file mode 100755 index 000000000..800bbd50b --- /dev/null +++ b/scripts/py2json.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +import sys +try: + import json +except ImportError: + import simplejson as json + +from os.path import splitext +from collections import OrderedDict + +dataToPrettyJson = lambda data, ensure_ascii=False: json.dumps( + data, + sort_keys=False, + indent=2, + ensure_ascii=ensure_ascii, +) + + +for fpath_py in sys.argv[1:]: + text_py = open(fpath_py).read() + data = OrderedDict() + exec(text_py, {}, data) + text_json = dataToPrettyJson(data) + fpath_nox = splitext(fpath_py)[0] + fpath_json = fpath_nox + '.json' + open(fpath_json, 'w').write(text_json) + + + + diff --git a/scripts/run b/scripts/run new file mode 100755 index 000000000..5e17417b0 --- /dev/null +++ b/scripts/run @@ -0,0 +1,12 @@ +#!/bin/bash +myPath="$0" +if [ "${myPath:0:2}" == "./" ] ; then + myPath=$PWD${myPath:1} +elif [ "${myPath:0:1}" != "/" ] ; then + myPath=$PWD/$myPath +fi +myDir=`dirname "$myPath"` +rootDir=`dirname "$myDir"` +cd "$rootDir" +PYTHONPATH=$PYTHONPATH:$rootDir python3 "$@" +cd - > /dev/null diff --git a/scripts/run.pyw b/scripts/run.pyw new file mode 100755 index 000000000..7c9594562 --- /dev/null +++ b/scripts/run.pyw @@ -0,0 +1,7 @@ +#!/usr/bin/python3 +import sys +from os.path import dirname, join +rootDir = dirname(dirname(__file__)) +sys.path.append(rootDir) +execfile(join(rootDir, sys.argv[1])) + diff --git a/scripts/run_abs b/scripts/run_abs new file mode 100755 index 000000000..97d879778 --- /dev/null +++ b/scripts/run_abs @@ -0,0 +1,10 @@ +#!/bin/bash +myPath="$0" +if [ "${myPath:0:2}" == "./" ] ; then + myPath=$PWD${myPath:1} +elif [ "${myPath:0:1}" != "/" ] ; then + myPath=$PWD/$myPath +fi +myDir=`dirname "$myPath"` +rootDir=`dirname "$myDir"` +PYTHONPATH=$PYTHONPATH:$rootDir python3 "$@" diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..785d03a38 --- /dev/null +++ b/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup + +setup(name='YourAppName', + version='1.0', + description='OpenShift App', + author='Your Name', + author_email='example@example.com', + url='http://www.python.org/sigs/distutils-sig/', +# install_requires=['Django>=1.3'], + ) diff --git a/starcal b/starcal new file mode 100755 index 000000000..15987befc --- /dev/null +++ b/starcal @@ -0,0 +1,2 @@ +#!/bin/bash +python3 `dirname "$0"`/scal3/ui_gtk/starcal.py "$@" diff --git a/starcal.pyw b/starcal.pyw new file mode 100644 index 000000000..7ab7c41ff --- /dev/null +++ b/starcal.pyw @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +from os.path import dirname, join +execfile(join(dirname(__file__), 'scal3', 'ui_gtk', 'starcal.py')) diff --git a/status-icons/ambiance-green.svg b/status-icons/ambiance-green.svg new file mode 100644 index 000000000..567c6dbec --- /dev/null +++ b/status-icons/ambiance-green.svg @@ -0,0 +1,88 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + TX + + diff --git a/status-icons/ambiance-red.svg b/status-icons/ambiance-red.svg new file mode 100644 index 000000000..b97c62131 --- /dev/null +++ b/status-icons/ambiance-red.svg @@ -0,0 +1,88 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + TX + + diff --git a/status-icons/ambiance.svg b/status-icons/ambiance.svg new file mode 100644 index 000000000..9649872d2 --- /dev/null +++ b/status-icons/ambiance.svg @@ -0,0 +1,79 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + TX + diff --git a/status-icons/black-circle.svg b/status-icons/black-circle.svg new file mode 100644 index 000000000..a44191e2b --- /dev/null +++ b/status-icons/black-circle.svg @@ -0,0 +1,150 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + TX + diff --git a/status-icons/dark-blue.svg b/status-icons/dark-blue.svg new file mode 100644 index 000000000..aa824b1c1 --- /dev/null +++ b/status-icons/dark-blue.svg @@ -0,0 +1,184 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TX + + diff --git a/status-icons/dark-green.svg b/status-icons/dark-green.svg new file mode 100644 index 000000000..b2b7ab178 --- /dev/null +++ b/status-icons/dark-green.svg @@ -0,0 +1,184 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TX + + diff --git a/status-icons/dark-red.svg b/status-icons/dark-red.svg new file mode 100644 index 000000000..b57809f8b --- /dev/null +++ b/status-icons/dark-red.svg @@ -0,0 +1,251 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TX + + diff --git a/status-icons/radiance-green.svg b/status-icons/radiance-green.svg new file mode 100644 index 000000000..8e7bc59b6 --- /dev/null +++ b/status-icons/radiance-green.svg @@ -0,0 +1,88 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + TX + + diff --git a/status-icons/radiance-red.svg b/status-icons/radiance-red.svg new file mode 100644 index 000000000..9bd77244d --- /dev/null +++ b/status-icons/radiance-red.svg @@ -0,0 +1,88 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + TX + + diff --git a/status-icons/radiance.svg b/status-icons/radiance.svg new file mode 100644 index 000000000..5d28684b7 --- /dev/null +++ b/status-icons/radiance.svg @@ -0,0 +1,79 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + TX + diff --git a/status-icons/ubuntu-dark.svg b/status-icons/ubuntu-dark.svg new file mode 100644 index 000000000..acfbc89d7 --- /dev/null +++ b/status-icons/ubuntu-dark.svg @@ -0,0 +1,131 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + TX + + + + + diff --git a/status-icons/ubuntu-light.svg b/status-icons/ubuntu-light.svg new file mode 100644 index 000000000..a6b72e3f8 --- /dev/null +++ b/status-icons/ubuntu-light.svg @@ -0,0 +1,131 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + TX + + + + + diff --git a/status-icons/white-blue.svg b/status-icons/white-blue.svg new file mode 100644 index 000000000..4c0f89085 --- /dev/null +++ b/status-icons/white-blue.svg @@ -0,0 +1,178 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TX + + diff --git a/status-icons/white-green.svg b/status-icons/white-green.svg new file mode 100644 index 000000000..7dfee289f --- /dev/null +++ b/status-icons/white-green.svg @@ -0,0 +1,181 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TX + + + diff --git a/status-icons/white-red.svg b/status-icons/white-red.svg new file mode 100644 index 000000000..ebd08e520 --- /dev/null +++ b/status-icons/white-red.svg @@ -0,0 +1,178 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TX + + diff --git a/svg/color-check.svg b/svg/color-check.svg new file mode 100644 index 000000000..c394ecec8 --- /dev/null +++ b/svg/color-check.svg @@ -0,0 +1,65 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/svg/dnd-date.svg b/svg/dnd-date.svg new file mode 100644 index 000000000..5153d49a8 --- /dev/null +++ b/svg/dnd-date.svg @@ -0,0 +1,51 @@ + + + + + + + + + image/svg+xml + + + + + + + + YYYY/MM/DD + + diff --git a/svg/dnd-font.svg b/svg/dnd-font.svg new file mode 100644 index 000000000..c594ce70c --- /dev/null +++ b/svg/dnd-font.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + FONTNAME + diff --git a/tools/kalzium-elements-discovery.py b/tools/kalzium-elements-discovery.py new file mode 100644 index 000000000..66b4a8b26 --- /dev/null +++ b/tools/kalzium-elements-discovery.py @@ -0,0 +1,73 @@ +import sys +from xml.etree.ElementTree import XML, tostring + +sys.path.append('/starcal2') + +from scal3 import event_lib +from scal3 import ui + + +tree = XML(open('/usr/share/apps/libkdeedu/data/elements.xml').read()) + + +def decodeAtomElement(atom): + data = {'id': atom.attrib['id']} + for el in atom: + ref = el.attrib['dictRef'] + if ref.startswith('bo:'): + ref = ref[3:] + if ref in ('name', 'atomicNumber', 'discoveryDate', 'discoveryCountry', 'discoverers'):## 'symbol', + try: + data[ref] = el.attrib['value'] + except KeyError: + data[ref] = el.text.strip() + #assert data['id'] == data['symbol'] + return data + + +def createDiscoveryEvent(group, atom): + if not 'discoveryDate' in atom: + print('no discoveryDate for %s'%atom['id']) + return + discoveryDate = int(atom['discoveryDate']) + if discoveryDate < 1: + print('empty discoveryDate (%r) for %s'%(atom['discoveryDate'], atom['id'])) + return + description = atom['name'] + if 'discoverers' in atom: + description += ', by %s'%atom['discoverers'].replace(';', ',') + event = group.createEvent('largeScale') + event.setData({ + 'calType': 'gregorian', + 'summary': 'Element Discovery: %s'%atom['id'], + 'description': description, + 'scale': 1, + 'start': discoveryDate, + 'duration': 1, + }) + #print(event.id) + return event + + +if __name__=='__main__': + ui.eventGroups.load() + group = event_lib.LargeScaleGroup() + group.setData({ + 'calType': 'gregorian', + 'color': [255, 0, 0], + 'title': 'Elements Discovery', + + }) + for atom in tree: + if not atom.tag.endswith('atom'): + continue + atomData = decodeAtomElement(atom) + event = createDiscoveryEvent(group, atomData) + if event: + event.save() + group.append(event) + group.save() + ui.eventGroups.append(group) + ui.eventGroups.save() + + diff --git a/tools/wikipedia-fa-events/1-download.py b/tools/wikipedia-fa-events/1-download.py new file mode 100644 index 000000000..00d717775 --- /dev/null +++ b/tools/wikipedia-fa-events/1-download.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +import sys, os, subprocess +from os.path import join, isfile + +sys.path.append('/usr/share/starcal2') + +from scal3 import core +from scal3.locale_man import tr as _ + +rawUrlBase = 'http://fa.wikipedia.org/w/index.php?title=%s_%s&action=raw' +saveDir = 'wikipedia-fa-events' + +#skipExisingFiles = True + +jalaliMonthLen = (31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 30) +jalaliMonthName = ('Farvardin','Ordibehesht','Khordad','Teer','Mordad','Shahrivar', + 'Mehr','Aban','Azar','Dey','Bahman','Esfand') + + +for month in range(1, 13): + for day in range(1, jalaliMonthLen[month-1]+1): + direc = join(saveDir, str(month)) + fpath = join(direc, str(day)) + #if skipExisingFiles and isfile(fpath): + # continue + try: + os.makedirs(direc) + except: + pass + url = rawUrlBase%(_(day), _(jalaliMonthName[month-1])) + subprocess.call(['wget', '-c', url, '-O', fpath]) + + + + + + + + diff --git a/tools/wikipedia-fa-events/2-parse.py b/tools/wikipedia-fa-events/2-parse.py new file mode 100644 index 000000000..bb82f4d7b --- /dev/null +++ b/tools/wikipedia-fa-events/2-parse.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys, os, re, json +from os.path import join + +faDigs = ('۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹', '٫') + +ignoreCategories = ('تعطیلات', 'جستارهای وابسته', 'منابع') + +getPrettyJson = lambda data: json.dumps(data, sort_keys=True, indent=4) + +def numFaDecode(numFa): + if isinstance(numFa, str): + numFa = numFa.decode('utf8') + numStr = '' + for c in numFa: + numStr += str(faDigs.index(c)) + return int(numStr) + +def cleanRawText(text): + text = text.strip()## .replace(']]', '').replace('[[', '') + for part in re.findall('\[\[.*?\]\]', text): + part2 = part.split('|')[-1] + text = text.replace(part, part2) + return text.replace('[[', '').replace(']]', '').replace("'''", '')\ + .replace('٬ ', '، ')\ + .replace('ي', 'ی')\ + .replace('ك', 'ک') + + +def parseFile(fpath, month, day): + #print(fpath, month, day) + category = '' + data = [] + for line in open(fpath).read().split('\n'): + if line.startswith('== '): + category = line[2:-2].strip() + if category in ignoreCategories: + category = '' + continue + if category and line.startswith('* '): + try: + yearFa = line.split('[[')[1].split(']]')[0] + year = numFaDecode(yearFa) + except: + continue + textStart = line.find('-') + #print(textStart) + if textStart<0: + continue + text = cleanRawText(line[textStart+1:]) + #print('text=%s'%text) + data.append({ + 'date': (year, month, day), + 'category': category, + 'text': text, + }) + return data + + +def parseAllFiles(direc): + data = [] + for monthFname in os.listdir(direc): + if monthFname.endswith('~'): + continue + #try: + month = int(monthFname) + #except ValueError, e: + # continue + mDirec = join(direc, monthFname) + for dayFname in os.listdir(mDirec): + if dayFname.endswith('~'): + continue + #try: + day = int(dayFname) + #except ValueError: + # continue + data += parseFile(join(mDirec, dayFname), month, day) + data.sort() + return data + +def writeToTabfile(data, fpath): + lines = [] + for event in data: + lines.append('%s\t%s\t%s'%( + '%.4d/%.2d/%.2d'%tuple(event['date']), + event['category'], + event['text'], + )) + open(fpath, 'w').write('\n'.join(lines)) + + + +if __name__=='__main__': + from pprint import pprint + data = parseAllFiles('wikipedia-fa-events') + writeToTabfile(data, 'wikipedia-fa.tab') + #print(getPrettyJson(data)) + #pprint(data) + + + + diff --git a/tools/wikipedia-fa-events/3-starcal2-import.py b/tools/wikipedia-fa-events/3-starcal2-import.py new file mode 100644 index 000000000..c8854e0c1 --- /dev/null +++ b/tools/wikipedia-fa-events/3-starcal2-import.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +import os, sys + +sys.path.append('/starcal2') + +from scal3.date_utils import dateDecode +from scal3.core import to_jd, jd_to, convert, moduleNames +from scal3 import event_lib +from scal3 import ui + +dataToPrettyJson = lambda data: json.dumps(data, sort_keys=True, indent=2) + +DATE_GREG = moduleNames.index('gregorian') +DATE_JALALI = moduleNames.index('jalali') + +ui.eventGroups.load() + +groupTitlePrefix = 'ویکی‌پدیا - ' +newGroupsDict = {} + +def getGroupByTitle(title): + global newGroupsDict + try: + return newGroupsDict[title] + except KeyError: + group = event_lib.NoteBook() + group.setData({ + 'calType': 'jalali', + 'color': [255, 255, 0], + 'title': title, + + }) + newGroupsDict[title] = group + ui.eventGroups.append(group) + return group + + + + +for line in open('wikipedia-fa.tab'): + line = line.strip() + if not line: + continue + parts = line.split('\t') + if len(parts)==4: + date_str, category, summary, description = parts + elif len(parts)==3: + date_str, category, summary = parts + description = '' + else: + print('BAD LINE', line) + continue + year, month, day = dateDecode(date_str) + group = getGroupByTitle(groupTitlePrefix + category) + event = group.createEvent('dailyNote') + event.setDate(year, month, day) + event.summary = summary + event.description = description + group.append(event) + event.save() + + +for group in newGroupsDict.values(): + group.save() +ui.eventGroups.save() + + + + + + diff --git a/uninstall b/uninstall new file mode 100755 index 000000000..bf7fb348f --- /dev/null +++ b/uninstall @@ -0,0 +1,82 @@ +#!/bin/bash + +function printUsage { + echo "Usage: $0 [--prefix=/usr/local]" +} + +pkgName=starcal3 + +options=`getopt -o 'v' --long 'prefix:,verbose' -n "$0" -- "$@"` +if [ $? != 0 ] ; then + printUsage + exit 1 +fi +eval set -- "$options" ## Note the quotes around $options are essential! +options="" + + +prefix="" +verbose="" + +while true ; do + case "$1" in + --prefix) prefix="$2" ; shift 2 ;; + -v|--verbose) verbose="yes" ; shift 1 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +if [ -n "$1" ] ; then ## extra arguments + printUsage + exit 1 +fi + +if [ -z "$prefix" ] ; then ## prefix is empty (not been set) + prefix=/usr/local + for p in /usr/local /usr ; do + if [ -d $p/share/$pkgName ] ; then + prefix=$p + break + fi + done +else + n=${#prefix} + if [ ${prefix:n-1:1} = / ] ; then + prefix=${prefix::-1} + fi +fi + +shareDir="${prefix}/share" + +if [ -z $pkgName ] ; then ## do not f*** the system if pkgName was empty amiss! + echo "Internal Error! pkgName=''" + exit 1 +fi + +##================== Starting to remove files ===================== + +if [ -n "$verbose" ] ; then ## no output (note that quotation around $verbose is needed) + RM="rm -Rfv" +else + RM="rm -Rf" +fi + + +if [ -d "${shareDir}/$pkgName" ] ; then + $RM "${shareDir}/$pkgName" +fi + +if [ -d "${shareDir}/doc/$pkgName" ] ; then + $RM "${shareDir}/doc/$pkgName" +fi + +## what about the /etc/$pkgName and /var/log/$pkgName ## FIXME + +$RM /etc/init.d/*${pkgName}d* /etc/rc.d/*${pkgName}d* 2>/dev/null ## FIXME +$RM $shareDir/$pkgName/icons/hicolor/*/apps/$pkgName.png 2>/dev/null +$RM $prefix/bin/$pkgName $prefix/bin/$pkgName-qt 2>/dev/null +$RM $shareDir/applications/$pkgName.desktop 2>/dev/null +$RM $prefix/share/locale/*/LC_MESSAGES/$pkgName.mo 2>/dev/null + + diff --git a/update-perm b/update-perm new file mode 100755 index 000000000..037581ea5 --- /dev/null +++ b/update-perm @@ -0,0 +1,33 @@ +#!/bin/bash + +cd "`dirname \"$0\"`" + +#chmod -R 755 . + +#find . -type f -exec chmod 644 '{}' \; + +chmod 755 scripts/* + +chmod 755 'starcal' + +chmod 755 'scal3/ui_gtk/starcal.py' + +chmod 755 'install' +chmod 755 'install-archlinux' +chmod 755 'install-debian' +chmod 755 'install-fedora' +chmod 755 'install-suse' +chmod 755 'install-ubuntu' +chmod 755 'uninstall' +chmod 755 'update-perm' + +chmod 755 'locale.d/compile' +chmod 755 'locale.d/install' +chmod 755 'locale.d/make-template' + +chmod 755 'scal3/get_version.py' +chmod 755 'scal3/import_config_2to3.py' + +chmod 755 'scal3/ui_gtk/adjust_dtime.py' +chmod 755 'scal3/ui_gtk/arch-enable-locale.py' +chmod 755 'scal3/ui_gtk/import_config_2to3.py' diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 000000000..c443581a6 --- /dev/null +++ b/wsgi.py @@ -0,0 +1,307 @@ +#!/usr/bin/python +import os + +virtenv = os.environ['OPENSHIFT_PYTHON_DIR'] + '/virtenv/' +virtualenv = os.path.join(virtenv, 'bin/activate_this.py') +try: + execfile(virtualenv, dict(__file__=virtualenv)) +except IOError: + pass +# +# IMPORTANT: Put any additional includes below this line. If placed above this +# line, it's possible required libraries won't be in your searchable path +# + +def application(environ, start_response): + + ctype = 'text/plain' + if environ['PATH_INFO'] == '/health': + response_body = "1" + elif environ['PATH_INFO'] == '/env': + response_body = ['%s: %s' % (key, value) + for key, value in sorted(environ.items())] + response_body = '\n'.join(response_body) + else: + ctype = 'text/html' + response_body = ''' + + + + + Welcome to OpenShift + + + +
+
+

Welcome to your Python application on OpenShift

+
+ +
+
+
+

Deploying code changes

+

OpenShift uses the Git version control system for your source code, and grants you access to it via the Secure Shell (SSH) protocol. In order to upload and download code to your application you need to give us your public SSH key. You can upload it within the web console or install the RHC command line tool and run rhc setup to generate and upload your key automatically.

+ +

Working in your local Git repository

+

If you created your application from the command line and uploaded your SSH key, rhc will automatically download a copy of that source code repository (Git calls this 'cloning') to your local system.

+ +

If you created the application from the web console, you'll need to manually clone the repository to your local system. Copy the application's source code Git URL and then run:

+ +
$ git clone <git_url> <directory_to_create>
+
+# Within your project directory
+# Commit your changes and push to OpenShift
+
+$ git commit -a -m 'Some commit message'
+$ git push
+ + +
+ +
+
+ +

Managing your application

+ +

Web Console

+

You can use the OpenShift web console to enable additional capabilities via cartridges, add collaborator access authorizations, designate custom domain aliases, and manage domain memberships.

+ +

Command Line Tools

+

Installing the OpenShift RHC client tools allows you complete control of your cloud environment. Read more on how to manage your application from the command line in our User Guide. +

+ +

Development Resources

+ + +
+
+ + +
+ +''' + + status = '200 OK' + response_headers = [('Content-Type', ctype), ('Content-Length', str(len(response_body)))] + # + start_response(status, response_headers) + return [response_body] + +# +# Below for testing only +# +if __name__ == '__main__': + from wsgiref.simple_server import make_server + httpd = make_server('localhost', 8051, application) + # Wait for a single request, serve it and quit. + httpd.handle_request() diff --git a/zoneinfo-tree.json b/zoneinfo-tree.json new file mode 100644 index 000000000..db8ff3392 --- /dev/null +++ b/zoneinfo-tree.json @@ -0,0 +1,569 @@ +{ + "Etc": { + "GMT": "", + "GMT+0": "", + "GMT+1": "", + "GMT+10": "", + "GMT+11": "", + "GMT+12": "", + "GMT+2": "", + "GMT+3": "", + "GMT+4": "", + "GMT+5": "", + "GMT+6": "", + "GMT+7": "", + "GMT+8": "", + "GMT+9": "", + "GMT-0": "", + "GMT-1": "", + "GMT-10": "", + "GMT-11": "", + "GMT-12": "", + "GMT-13": "", + "GMT-14": "", + "GMT-2": "", + "GMT-3": "", + "GMT-4": "", + "GMT-5": "", + "GMT-6": "", + "GMT-7": "", + "GMT-8": "", + "GMT-9": "", + "GMT0": "", + "Greenwich": "", + "UCT": "", + "UTC": "", + "Universal": "", + "Zulu": "" + }, + "Africa": { + "Abidjan": "", + "Accra": "", + "Addis_Ababa": "", + "Algiers": "", + "Asmara": "", + "Asmera": "", + "Bamako": "", + "Bangui": "", + "Banjul": "", + "Bissau": "", + "Blantyre": "", + "Brazzaville": "", + "Bujumbura": "", + "Cairo": "", + "Casablanca": "", + "Ceuta": "", + "Conakry": "", + "Dakar": "", + "Dar_es_Salaam": "", + "Djibouti": "", + "Douala": "", + "El_Aaiun": "", + "Freetown": "", + "Gaborone": "", + "Harare": "", + "Johannesburg": "", + "Juba": "", + "Kampala": "", + "Khartoum": "", + "Kigali": "", + "Kinshasa": "", + "Lagos": "", + "Libreville": "", + "Lome": "", + "Luanda": "", + "Lubumbashi": "", + "Lusaka": "", + "Malabo": "", + "Maputo": "", + "Maseru": "", + "Mbabane": "", + "Mogadishu": "", + "Monrovia": "", + "Nairobi": "", + "Ndjamena": "", + "Niamey": "", + "Nouakchott": "", + "Ouagadougou": "", + "Porto-Novo": "", + "Sao_Tome": "", + "Timbuktu": "", + "Tripoli": "", + "Tunis": "", + "Windhoek": "" + }, + "America": { + "Adak": "", + "Anchorage": "", + "Anguilla": "", + "Antigua": "", + "Araguaina": "", + "Argentina": { + "Buenos_Aires": "", + "Catamarca": "", + "ComodRivadavia": "", + "Cordoba": "", + "Jujuy": "", + "La_Rioja": "", + "Mendoza": "", + "Rio_Gallegos": "", + "Salta": "", + "San_Juan": "", + "San_Luis": "", + "Tucuman": "", + "Ushuaia": "" + }, + "Aruba": "", + "Asuncion": "", + "Atikokan": "", + "Atka": "", + "Bahia": "", + "Bahia_Banderas": "", + "Barbados": "", + "Belem": "", + "Belize": "", + "Blanc-Sablon": "", + "Boa_Vista": "", + "Bogota": "", + "Boise": "", + "Buenos_Aires": "", + "Cambridge_Bay": "", + "Campo_Grande": "", + "Cancun": "", + "Caracas": "", + "Catamarca": "", + "Cayenne": "", + "Cayman": "", + "Chicago": "", + "Chihuahua": "", + "Coral_Harbour": "", + "Cordoba": "", + "Costa_Rica": "", + "Creston": "", + "Cuiaba": "", + "Curacao": "", + "Danmarkshavn": "", + "Dawson": "", + "Dawson_Creek": "", + "Denver": "", + "Detroit": "", + "Dominica": "", + "Edmonton": "", + "Eirunepe": "", + "El_Salvador": "", + "Ensenada": "", + "Fort_Wayne": "", + "Fortaleza": "", + "Glace_Bay": "", + "Godthab": "", + "Goose_Bay": "", + "Grand_Turk": "", + "Grenada": "", + "Guadeloupe": "", + "Guatemala": "", + "Guayaquil": "", + "Guyana": "", + "Halifax": "", + "Havana": "", + "Hermosillo": "", + "Indiana": { + "Indianapolis": "", + "Knox": "", + "Marengo": "", + "Petersburg": "", + "Tell_City": "", + "Vevay": "", + "Vincennes": "", + "Winamac": "" + }, + "Indianapolis": "", + "Inuvik": "", + "Iqaluit": "", + "Jamaica": "", + "Jujuy": "", + "Juneau": "", + "Kentucky": { + "Louisville": "", + "Monticello": "" + }, + "Knox_IN": "", + "Kralendijk": "", + "La_Paz": "", + "Lima": "", + "Los_Angeles": "", + "Louisville": "", + "Lower_Princes": "", + "Maceio": "", + "Managua": "", + "Manaus": "", + "Marigot": "", + "Martinique": "", + "Matamoros": "", + "Mazatlan": "", + "Mendoza": "", + "Menominee": "", + "Merida": "", + "Metlakatla": "", + "Mexico_City": "", + "Miquelon": "", + "Moncton": "", + "Monterrey": "", + "Montevideo": "", + "Montreal": "", + "Montserrat": "", + "Nassau": "", + "New_York": "", + "Nipigon": "", + "Nome": "", + "Noronha": "", + "North_Dakota": { + "Beulah": "", + "Center": "", + "New_Salem": "" + }, + "Ojinaga": "", + "Panama": "", + "Pangnirtung": "", + "Paramaribo": "", + "Phoenix": "", + "Port-au-Prince": "", + "Port_of_Spain": "", + "Porto_Acre": "", + "Porto_Velho": "", + "Puerto_Rico": "", + "Rainy_River": "", + "Rankin_Inlet": "", + "Recife": "", + "Regina": "", + "Resolute": "", + "Rio_Branco": "", + "Rosario": "", + "Santa_Isabel": "", + "Santarem": "", + "Santiago": "", + "Santo_Domingo": "", + "Sao_Paulo": "", + "Scoresbysund": "", + "Shiprock": "", + "Sitka": "", + "St_Barthelemy": "", + "St_Johns": "", + "St_Kitts": "", + "St_Lucia": "", + "St_Thomas": "", + "St_Vincent": "", + "Swift_Current": "", + "Tegucigalpa": "", + "Thule": "", + "Thunder_Bay": "", + "Tijuana": "", + "Toronto": "", + "Tortola": "", + "Vancouver": "", + "Virgin": "", + "Whitehorse": "", + "Winnipeg": "", + "Yakutat": "", + "Yellowknife": "" + }, + "Antarctica": { + "Casey": "", + "Davis": "", + "DumontDUrville": "", + "Macquarie": "", + "Mawson": "", + "McMurdo": "", + "Palmer": "", + "Rothera": "", + "South_Pole": "", + "Syowa": "", + "Vostok": "" + }, + "Arctic": { + "Longyearbyen": "" + }, + "Asia": { + "Aden": "", + "Almaty": "", + "Amman": "", + "Anadyr": "", + "Aqtau": "", + "Aqtobe": "", + "Ashgabat": "", + "Ashkhabad": "", + "Baghdad": "", + "Bahrain": "", + "Baku": "", + "Bangkok": "", + "Beirut": "", + "Bishkek": "", + "Brunei": "", + "Calcutta": "", + "Choibalsan": "", + "Chongqing": "", + "Chungking": "", + "Colombo": "", + "Dacca": "", + "Damascus": "", + "Dhaka": "", + "Dili": "", + "Dubai": "", + "Dushanbe": "", + "Gaza": "", + "Harbin": "", + "Hebron": "", + "Ho_Chi_Minh": "", + "Hong_Kong": "", + "Hovd": "", + "Irkutsk": "", + "Istanbul": "", + "Jakarta": "", + "Jayapura": "", + "Jerusalem": "", + "Kabul": "", + "Kamchatka": "", + "Karachi": "", + "Kashgar": "", + "Kathmandu": "", + "Katmandu": "", + "Khandyga": "", + "Kolkata": "", + "Krasnoyarsk": "", + "Kuala_Lumpur": "", + "Kuching": "", + "Kuwait": "", + "Macao": "", + "Macau": "", + "Magadan": "", + "Makassar": "", + "Manila": "", + "Muscat": "", + "Nicosia": "", + "Novokuznetsk": "", + "Novosibirsk": "", + "Omsk": "", + "Oral": "", + "Phnom_Penh": "", + "Pontianak": "", + "Pyongyang": "", + "Qatar": "", + "Qyzylorda": "", + "Rangoon": "", + "Riyadh": "", + "Riyadh87": "", + "Riyadh88": "", + "Riyadh89": "", + "Saigon": "", + "Sakhalin": "", + "Samarkand": "", + "Seoul": "", + "Shanghai": "", + "Singapore": "", + "Taipei": "", + "Tashkent": "", + "Tbilisi": "", + "Tehran": "", + "Tel_Aviv": "", + "Thimbu": "", + "Thimphu": "", + "Tokyo": "", + "Ujung_Pandang": "", + "Ulaanbaatar": "", + "Ulan_Bator": "", + "Urumqi": "", + "Ust-Nera": "", + "Vientiane": "", + "Vladivostok": "", + "Yakutsk": "", + "Yekaterinburg": "", + "Yerevan": "" + }, + "Atlantic": { + "Azores": "", + "Bermuda": "", + "Canary": "", + "Cape_Verde": "", + "Faeroe": "", + "Faroe": "", + "Jan_Mayen": "", + "Madeira": "", + "Reykjavik": "", + "South_Georgia": "", + "St_Helena": "", + "Stanley": "" + }, + "Australia": { + "ACT": "", + "Adelaide": "", + "Brisbane": "", + "Broken_Hill": "", + "Canberra": "", + "Currie": "", + "Darwin": "", + "Eucla": "", + "Hobart": "", + "LHI": "", + "Lindeman": "", + "Lord_Howe": "", + "Melbourne": "", + "NSW": "", + "North": "", + "Perth": "", + "Queensland": "", + "South": "", + "Sydney": "", + "Tasmania": "", + "Victoria": "", + "West": "", + "Yancowinna": "" + }, + "Brazil": { + "Acre": "", + "DeNoronha": "", + "East": "", + "West": "" + }, + "Canada": { + "Atlantic": "", + "Central": "", + "East-Saskatchewan": "", + "Eastern": "", + "Mountain": "", + "Newfoundland": "", + "Pacific": "", + "Saskatchewan": "", + "Yukon": "" + }, + "Chile": { + "Continental": "", + "EasterIsland": "" + }, + "Europe": { + "Amsterdam": "", + "Andorra": "", + "Athens": "", + "Belfast": "", + "Belgrade": "", + "Berlin": "", + "Bratislava": "", + "Brussels": "", + "Bucharest": "", + "Budapest": "", + "Busingen": "", + "Chisinau": "", + "Copenhagen": "", + "Dublin": "", + "Gibraltar": "", + "Guernsey": "", + "Helsinki": "", + "Isle_of_Man": "", + "Istanbul": "", + "Jersey": "", + "Kaliningrad": "", + "Kiev": "", + "Lisbon": "", + "Ljubljana": "", + "London": "", + "Luxembourg": "", + "Madrid": "", + "Malta": "", + "Mariehamn": "", + "Minsk": "", + "Monaco": "", + "Moscow": "", + "Nicosia": "", + "Oslo": "", + "Paris": "", + "Podgorica": "", + "Prague": "", + "Riga": "", + "Rome": "", + "Samara": "", + "San_Marino": "", + "Sarajevo": "", + "Simferopol": "", + "Skopje": "", + "Sofia": "", + "Stockholm": "", + "Tallinn": "", + "Tirane": "", + "Tiraspol": "", + "Uzhgorod": "", + "Vaduz": "", + "Vatican": "", + "Vienna": "", + "Vilnius": "", + "Volgograd": "", + "Warsaw": "", + "Zagreb": "", + "Zaporozhye": "", + "Zurich": "" + }, + "Indian": { + "Antananarivo": "", + "Chagos": "", + "Christmas": "", + "Cocos": "", + "Comoro": "", + "Kerguelen": "", + "Mahe": "", + "Maldives": "", + "Mauritius": "", + "Mayotte": "", + "Reunion": "" + }, + "Mexico": { + "BajaNorte": "", + "BajaSur": "", + "General": "" + }, + "Mideast": { + "Riyadh87": "", + "Riyadh88": "", + "Riyadh89": "" + }, + "Pacific": { + "Apia": "", + "Auckland": "", + "Chatham": "", + "Chuuk": "", + "Easter": "", + "Efate": "", + "Enderbury": "", + "Fakaofo": "", + "Fiji": "", + "Funafuti": "", + "Galapagos": "", + "Gambier": "", + "Guadalcanal": "", + "Guam": "", + "Honolulu": "", + "Johnston": "", + "Kiritimati": "", + "Kosrae": "", + "Kwajalein": "", + "Majuro": "", + "Marquesas": "", + "Midway": "", + "Nauru": "", + "Niue": "", + "Norfolk": "", + "Noumea": "", + "Pago_Pago": "", + "Palau": "", + "Pitcairn": "", + "Pohnpei": "", + "Ponape": "", + "Port_Moresby": "", + "Rarotonga": "", + "Saipan": "", + "Samoa": "", + "Tahiti": "", + "Tarawa": "", + "Tongatapu": "", + "Truk": "", + "Wake": "", + "Wallis": "", + "Yap": "" + } +} \ No newline at end of file