diff options
author | toma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2009-11-25 17:56:58 +0000 |
---|---|---|
committer | toma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2009-11-25 17:56:58 +0000 |
commit | 460c52653ab0dcca6f19a4f492ed2c5e4e963ab0 (patch) | |
tree | 67208f7c145782a7e90b123b982ca78d88cc2c87 /kalarm | |
download | tdepim-460c52653ab0dcca6f19a4f492ed2c5e4e963ab0.tar.gz tdepim-460c52653ab0dcca6f19a4f492ed2c5e4e963ab0.zip |
Copy the KDE 3.5 branch to branches/trinity for new KDE 3.5 features.
BUG:215923
git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/kdepim@1054174 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'kalarm')
172 files changed, 43493 insertions, 0 deletions
diff --git a/kalarm/ACKNOWLEDGEMENTS b/kalarm/ACKNOWLEDGEMENTS new file mode 100644 index 000000000..0131c5dbf --- /dev/null +++ b/kalarm/ACKNOWLEDGEMENTS @@ -0,0 +1,26 @@ +Thank you to the following people who have contributed significantly to KAlarm +whether by giving advice and opinions, suggesting improvements, or giving their +time and effort to help fix faults: + +Cornelius Schumacher for his advice and opinions on various issues. + +Bill Dimm for helping to track down several faults, and useful suggestions for +extra facilities. + +Martin Köbele for code to send mail automatically via KMail. + +Wolfgang Jeltsch for investigating the corruption of deferred alarms. + +Bill Pfeffer for several suggestions for improvements. + +Serg ("Frr") for useful suggestions for extra facilities. + +Thorsten Schnebeck for helping to track down a tricky time zone problem. + +Ray ("thesun") for suggestions and problem reporting. + +Kyle Maus for helping to track down some recurrence problems. + +m.eik for contributing RPMs. + +Daniel Eckl for helping to fix some calendar problems in a beta issue. diff --git a/kalarm/AUTHORS b/kalarm/AUTHORS new file mode 100644 index 000000000..5f36e497b --- /dev/null +++ b/kalarm/AUTHORS @@ -0,0 +1,11 @@ +KAlarm author: +David Jarvie <software@astrojar.org.uk> + +Alarm daemon author: +David Jarvie <software@astrojar.org.uk> + +Original KOrganizer alarm daemon author: +Preston Brown <pbrown@kde.org> + +KOrganizer alarm daemon maintainer: +Cornelius Schumacher <schumacher@kde.org>
\ No newline at end of file diff --git a/kalarm/BUGS b/kalarm/BUGS new file mode 100644 index 000000000..ff0c38397 --- /dev/null +++ b/kalarm/BUGS @@ -0,0 +1,17 @@ +Known bugs in KAlarm +==================== + +The following bugs are known to exist in KAlarm: + +-- Bug 79247: Summer/winter time changes (daylight savings time changes), are + not handled well. KAlarm needs to use time zones in scheduling alarms instead + of a simple local time which it currently uses. This requires improved + timezone handling in kdelibs and libkcal. This is fixed in KAlarm version 1.9. + +The following bugs appear to be KAlarm bugs but are actually due to other software: + +-- System tray icon has an opaque background (KDE bug 122988) + In the system tray, the KAlarm icon sometimes does not have a transparent + background, but instead displays inside an opaque square set to the KDE window + background colour. This is due to a bug in the QtCurve style, and can be fixed + by choosing a different style. diff --git a/kalarm/COPYING b/kalarm/COPYING new file mode 100644 index 000000000..2e3f0efad --- /dev/null +++ b/kalarm/COPYING @@ -0,0 +1,341 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + + 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 +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) 19yy <name of author> + + 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 2 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, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) 19yy name of author + Gnomovision 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, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This 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 Library General +Public License instead of this License. diff --git a/kalarm/Changelog b/kalarm/Changelog new file mode 100644 index 000000000..6c51c621e --- /dev/null +++ b/kalarm/Changelog @@ -0,0 +1,657 @@ +KAlarm Change Log + +=== Version 1.5.5 --- 21 January 2009 === +- Require a real double click to accept the selected template in pick list. +- Make mouse wheel work, and fix highlighting, for left-hand time spinbox + buttons in Plastik style. +- Ensure alarm windows show on top of full-screen windows. +- Fix failure to update alarms in KOrganizer when Kontact is running but + Kontact's calendar component is not loaded. +- Fix inability to change or cancel alarm deferral times. +- Fix invalid alarm remaining in calendar when pre-alarm action failure message + is acknowledged before the alarm is deferred. +- Prevent corrupt alarms if deferral reinstates from archived alarm instead of + from the displaying calendar. +- Ignore events in calendar without usable alarms, which prevents them getting + stuck in the alarm list). +- Prevent defer dialogue date being set outside the allowed range. +- Cancel screensaver when an alarm is displayed. + +=== Version 1.5.4 (KDE 3.5.10) --- 18 August 2008 === +- Show background colour selector for file display alarms. +- Display default font correctly in alarm edit dialogue sample text. +- Expand font selection control when dialogue is expanded. +- Fix potential crash when KAlarm quits, if birthday dialogue was opened. + +=== Version 1.5.3 --- 22 May 2008 === +- In New From Template menu, show list of template names in sorted order. +- Fix recurrence count being lost when using alarm templates. +- Prevent invalid negative values appearing in 'Time from now' edit field. +- Fix time shown in alarm edit dialogue for recurring alarms. +- Fix recurrence count shown in alarm edit dialogue once alarm has triggered. +- Fix Find not working with a new search text after a failed search. +- Display correct error message when a search fails. +- Prevent user changing font/colour dialogue when editing read-only alarms. + +=== Version 1.5.2 --- 13 February 2008 === +- Prevent repetition duration error message when saving alarm which never recurs. + +=== Version 1.5.1 (KDE 3.5.9) --- 13 February 2008 === +- Fix inability to set up sub-repetitions for simple yearly recurrences. + +=== Version 1.5.0 --- 27 January 2008 === +- Replace simple repetitions with recurrence sub-repetitions, to save confusion. +- Add option to enter reminder times in minutes, in addition to hours/minutes. +- Replace alarm edit dialogue background colour selector with font/colour sample. +- Rearrange yearly recurrence controls to reduce alarm edit dialogue height. +- Store email unique IDs instead of names in email alarms to prevent problems if + email IDs are renamed. + +=== Version 1.4.22 --- 27 January 2008 === +- Fix error "Sender verify failed (in reply to RCPT TO command)" using sendmail + on some systems, by adding envelope sender address to emails. +- Fix OpenSolaris build error. + +=== Version 1.4.21 --- 19 December 2007 === +- Remember last used main window show/hide options instead of setting them in + Preferences dialogue. +- Make the Menu key work in the alarm list. +- Fix crash when saving preferences, if 'xterm' is not installed in the system. +- Prevent multiple identical error messages being displayed for the same alarm. + +=== Version 1.4.20 --- 18 November 2007 === +- Fix deferral of non-recurring alarms not working. +- Fix loss of reminder details in archive when alarm has had a reminder deferred. +- Fix inability to reactivate deleted alarms which still have repetitions to go. +- Fix incorrect interpretation of --late-cancel weekly parameter on command line. + +=== Version 1.4.19 --- 11 November 2007 === +- Fix KAlarm hanging and freezing the system for a while, especially on startup. +- Fix next occurrence time set after editing alarm, when it's a sub-repetition. +- Prevent error messages while typing date value, until user finishes entering it. + +=== Version 1.4.18 --- 2 November 2007 === +- Fix failure to trigger some recurring date-only alarms (e.g. after suspend-resume). +- Fix date-only alarms triggering every minute from midnight to start-of-day time. +- Simplify recurrence text shown in alarm edit dialogue Alarm tab when possible. +- Prevent error after browsing for command log file, due to file:// prefix. + +=== Version 1.4.17 (KDE 3.5.8) --- 8 October 2007 === +- Allow time-from-now values up to 999 hours to be entered. +- Fix incorrect email headers resulting in failure to send some emails. + +=== Version 1.4.16a --- 12 September 2007 === +- Fix failure to retrieve font and colour settings for display alarms. +- Disable reminder etc. controls for at-login recurrence in alarm edit dialogue. + +=== Version 1.4.15 --- 7 September 2007 === +- Fix deferrals of recurring alarms not triggering correctly. +- Fix failure to archive details of repetitions within a recurrence. +- Enable/disable "Show expired alarms" action when preferences change. + +=== Version 1.4.14 --- 5 August 2007 === +- Fix handling of exception dates in recurrences. +- In sound file dialogue change Play button to a Stop button while playing a file. + +=== Version 1.4.13 --- 18 May 2007 === +- Fix time value in templates not being stored. +- Expand time spin boxes to make room for all digits. +- Make Preferences dialogue non-modal. + +=== Version 1.4.12 (KDE 3.5.7) --- 11 May 2007 === +- Display advance reminders for each occurrence of recurring alarms. +- Fix Undo of deletion of active alarms. +- Disable simple repetition controls if repetitions can't fit between recurrences. +- Make the system tray tooltip take account of alarm repetitions. +- Show repetition & special action status by button states in alarm edit dialogue. +- Fix reminder alarms displaying very big numbers for how long until alarm is due. +- Fix KMail omitting attachments from email alarms (if KMail is the email client). + +=== Version 1.4.11 --- 16 April 2007 === +- Prevent pre-alarm actions being executed multiple times when alarm is triggered. +- Prevent alarm daemon triggering alarms multiple times. +- Only execute pre-alarm actions once (not for reminders or deferrals). +- Only execute post-alarm actions once when alarm is finally acknowledged (after + any deferrals), and not after reminders. +- Show file name as a tooltip on sound type combo box when "file" is selected. + +=== Version 1.4.10 --- 3 March 2007 === +- Add play button to sound file selection dialogue. +- Prevent simple repetitions triggering again when KAlarm is restarted. +- Fix recurring alarms being triggered on exception days. +- Fix start-of-day time being ignored for date-only alarms. +- Disable Defer button in new message window when deferral limit has been reached. +- Fix failure to save "Execute in terminal window" option in Preferences dialogue. +- Ensure up-to-date menus are displayed if user has a customised toolbar. + +=== Version 1.4.9 (KDE 3.5.6) --- 3 January 2007 === +- Minor changes. + +=== Version 1.4.8 --- 28 December 2006 === +- Fix Find always using first search text entered even after entering a new one. + +=== Version 1.4.7 --- 14 December 2006 === +- Fix crash saving Preferences dialogue (due to command alarm terminal setting). + +=== Version 1.4.6 --- 30 November 2006 === +- Fix crash if an alarm triggers while user is deleting it. +- Fix "Start alarm monitoring at login" value shown in preferences dialogue. +- Fix deselecting "Start alarm monitoring at login" when daemon not running. +- Fix editing of 29th February alarm options for non-leap years. +- Tidy up preferences dialogue Run mode options. +- Tidy up alarm edit/preferences dialogue sound type options into a combo box. +- Add context help for sound file fade options. + +=== Version 1.4.5 (KDE 3.5.5) --- 29 September 2006 === +- Improve alarm edit dialogue layout (Reminder controls moved to below Time box). + +=== Version 1.4.4 --- 11 July 2006 === +- Use an alarm's previous deferral time interval as default for its next deferral. + +=== Version 1.4.3 (KDE 3.5.4) --- 11 July 2006 === +- Add facility to import alarms from other calendar files. +- Fix Defer dialog time interval maximum to match maximum date/time value. +- Fix crash when a deferred expired recurring alarm is edited from message window. +- Fix crash when a message is redisplayed after login. +- Prevent inapplicable 'Unable to speak' error when alarm redisplayed after login. +- Save main window column order changes to use on restart (except message column). + +=== Version 1.3.10 (KDE 3.5.3) --- 22 May 2006 === +- Add DCOP calls and command line options to display the edit alarm dialogue. +- Add Select All and Deselect actions & shortcuts for import birthdays list. +- Make system tray icon appear in non-KDE window managers. +- Output error message if deleting copy of alarm from KOrganizer fails. +- Fix corruption of alarms displayed at logout and then deferred after login. +- Fix reminder time not being saved in alarm templates. +- Fix erroneous date adjustment of start of recurrence when saving alarm. +- Fix crash when --play command line option is used, if compiled without aRts. +- Don't show disabled alarms in system tray tooltip alarm list. + +=== Version 1.3.9 (KDE 3.5.2) --- 7 March 2006 === +- Notify daemon by DCOP that alarm has been processed: to prevent alarm loss, and + to prevent defunct kalarm processes when run mode is on-demand. +- Add Select All and Deselect actions & shortcuts for alarm and template lists. + +=== Version 1.3.8 --- 24 January 2006 === +- Fix kalarmd hang when triggering late alarm and KAlarm run mode is on-demand. + +=== Version 1.3.7 --- 22 January 2006 === +- Fix column widths when main window is resized, if columns have been reordered. + +=== Version 1.3.6 (KDE 3.5.1) --- 10 January 2006 === +- Make autoclose of message windows work. +- Fix New From Template not creating alarm if template contents are not changed. +- Ensure that day and month names translations are independent of locale calendar. +- Display alarm message windows within current screen in multi-head systems. +- Reduce size of Preferences dialog to fit in 1024x768 screen. + +=== Version 1.3.5 --- 14 December 2005 === +- Fix email attachments being forgotten when saving alarms. +- Fix toolbar configuration being lost after quitting KAlarm. + +=== Version 1.3.4 (KDE 3.5) --- 30 October 2005 === +- Fix incorrect recurrence frequency in Alarm Edit dialogue's Alarm tab. + +=== Version 1.3.3 --- 22 September 2005 === +- Add day-of-week selection to daily recurrence dialog. + +=== Version 1.3.2 (KDE 3.5 beta 1) --- 10 September 2005 === +- Add option to show alarms in KOrganizer's active calendar. +- Add option for email text alarms to locate the email in KMail. +- When email alarm triggers and KMail isn't running, start KMail and send mail + automatically instead of opening KMail composer window. +- Provide per-alarm option for yearly February 29th recurrences. +- Wait longer (20 seconds) before reporting alarm daemon registration failed. +- Minimise KMix window if KMix is started by KAlarm when displaying a message. +- Fix Plastik style 'enabled' indication for time spinbox left-hand buttons. +- Prevent message windows always being full screen after a big message is shown. +- Prevent message windows being initially larger than the desktop. +- Prevent message windows initially overlapping the KDE panel. +- Prevent session restoration displaying main windows which should be hidden. +- Fix alarms getting stuck if due during a daylight savings clock change. +- Change --volume command line option short form to -V (-v is used by --version). +- Fix reported shell errors when output from command alarm is discarded. +- Use 'KAlarm' untranslated in calendar product ID, to cater for locale changes. + +=== Version 1.3.1 --- 30 May 2005 === +- Add Undo/Redo facility for alarm edit/creation/deletion/reactivation. +- Add text search facility. +- Add option to speak alarm messages (if speech synthesis is installed). +- Add command line option --speak. +- Add 'New alarm from template' menu option and toolbar button. +- Add 'Time from now' option in alarm templates. +- Add fade option for playing sound files. +- Add option to log command alarm output to a file. +- Add Edit button to alarm message window to allow the alarm to be edited. +- Enable drag and drop of alarms to other applications. +- Email drag-and-drop from KMail (KDE >= 3.5) now presets alarm edit dialog with + full From/To/Cc/Subject headers and body text. + +=== Version 1.2.8 (KDE 3.4.1) --- 9 May 2005 === +- Fix failure to enable "Reminder for first recurrence only" checkbox. + +=== Version 1.2.7 --- 20 April 2005 === +- Use a sensible default for terminal window command in Preferences dialog. +- Validate terminal window command entered in Preferences dialog. +- Fix date range no longer being validated in Defer dialog. +- Don't ignore Sound setting in Preferences dialog Edit tab. +- Reset sound volume (if it was set) as soon as audio file playing is complete. +- Don't start KMix when an alarm is displayed if no sound volume is specified. +- Add command script and execute-in-terminal options to DCOP interface. + +=== Version 1.2.6 (KDE 3.4) --- 22 February 2005 === +- Pop up message windows far from cursor to avoid accidental acknowledgement. +- Start KMix if not already running, for setting alarm sound level. +- Fix alarms not triggering if IDs are duplicated in different calendar files. +- Improve validation when reading configuration file values. + +=== Version 1.2.5 (KDE 3.4 beta2) --- 21 January 2005 === +- Prevent multiple "Failed to start Alarm Daemon" error messages at startup. +- Fix missing left border for time spinboxes in Plastik style. + +=== Version 1.2.4 (KDE 3.4 beta1) --- 9 January 2005 === +- Provide option to enter a script for a command alarm, instead of a command line. +- Add option to run command alarms in terminal windows. +- Accept drag and drop of KAddressBook entries to alarm edit dialog email fields. +- Drag and drop now inserts text where appropriate, rather than replacing it. +- Display correct controls after loading a template in alarm edit dialog. + +=== Version 1.2.3 --- 7 December 2004 === +- Put alarm type icons in a separate, sortable, column in alarm list. +- Align times in alarm list. +- Fix crash when the last recurrence of an alarm is reached. +- Fix random limit on expired alarm discard time if stepping with spinbox buttons. +- Fix dialog layouts for right-to-left languages. +- Fix time spin box layout for right-to-left languages. + +=== Version 1.2.2 --- 27 November 2004 === +- Make alarm daemon (kalarmd) exclusive to KAlarm. +- Move control options for alarm daemon into KAlarm preferences dialog. +- Allow user to specify the late-cancellation period for an alarm. +- Add option to automatically close window after late-cancellation period. +- Add facility to enable and disable individual alarms. +- Add simple repetition facility, including repetition within a recurrence. +- Add option to pick a KMail identity to use as sender of email alarms. +- Add option to copy emails sent via sendmail, to KMail sent-mail folder. +- Show scheduled times, not reminder times, in alarm list and system tray tooltip. +- Make time edit controls use 12-hour clock when that is the user's default. +- Also fill in alarm edit dialog email fields when email is dropped onto KAlarm. +- New revised DCOP request interface (old interface still kept for compatibility). +- Make detection of email message display alarms independent of language. +- Use KMix whenever possible to set hardware sound volume. +- Limit range of entered date/time to valid values in deferral dialogue. +- Prevent kalarm failing to register with kalarmd except when really necessary. +- Fix time-to-alarm column in main window not always updating every minute. + +=== Version 1.1.7 (KDE 3.3.2) --- 27 November 2004 === +- Fix KAlarm button on message windows to make it always display main window. +- Show scheduled times, not reminder times, in alarm list and system tray tooltip. +- Fix time-to-alarm column in main window not always updating every minute. + +=== Version 1.1.6 (KDE 3.3.1) --- 30 September 2004 === +- Prevent crash, and output error message, if menu creation fails. +- Unsuppress Quit warning message box if default answer is Cancel quit. +- Prevent blind copy to self of email alarms via KMail when bcc is deselected. + +=== Version 1.1.5 --- 1 September 2004 === +- Show erroneous control in alarm edit dialog when an error message is displayed. +- Make alarm edit dialog always appear on current desktop. +- Make weekly/monthly/yearly recurrences scheduled from command line correspond + correctly to the start date. +- Fix start date for monthly/yearly recurrences scheduled from the command line. +- Fix DCOP triggerEvent() call to not reschedule alarm if it isn't due yet. + +=== Version 1.1.4 --- 21 August 2004 === +- Fix errors when altering or cancelling deferrals of expired recurrences. + +=== Version 1.1.3 (KDE 3.3) --- 28 July 2004 === +- Fix dialog sizing the first time KAlarm is run. + +=== Version 1.1.2 (KDE 3.3 beta2) --- 11 July 2004 === +- Fix hangup in interactions with alarm daemon introduced in version 1.1.1. +- Only tick Alarms Enabled menu items once alarms have actually been enabled. +- Fix build for "./configure --without-arts". + +=== Version 1.1.1 (KDE 3.3 beta1) --- 20 June 2004 === +- Output error message and disable alarms if can't register with alarm daemon. +- Exit if error in alarm calendar name configuration. +- Fix bug where sound file is selected even when Cancel is pressed. + +=== Version 1.1.0 --- 1 June 2004 === +- Add facility to define alarm templates. +- Add facility to specify pre- and post-alarm shell command actions. +- Add option to play sound file repeatedly until alarm window is closed. +- Add volume control for playing sound file. +- Add 'stop sound' button to alarm message window when sound file is played. +- Rename command line option --sound to --play, add option --play-repeat. +- Add command line option --volume. +- Add 'Configure Shortcuts' and 'Configure Toolbars' menu options in main window. +- After creating/editing alarm, prompt to re-enable alarms if currently disabled. +- Middle mouse button over system tray icon displays new alarm dialog. +- Add option to display a reminder once only before the first alarm recurrence. +- Display time-to-alarm in reminder message window. +- For message texts which are truncated in main window, show full text in tooltip. +- Allow time of day to be entered in format HHMM in time spin boxes. +- Allow hour to be omitted when colon format time is entered in time spin boxes. +- Add "Don't ask again" option to alarm deletion confirmation prompt. +- Prevent expired alarm calendar purges clashing with other alarm actions. +- Fix initial recurrence date/time for weekly/monthly/yearly recurrences. +- Fix yearly recurrences of the last day in the month. +- Disable yearly recurrence's month checkboxes depending on selected day of month. +- Update which time columns are displayed in alarm list when Preferences change. +- Don't store audio/reminder details in email/command alarms. +- Don't store email details in message/file/command alarms. +- Don't close message windows when quit is selected. +- Fix "Warn before quitting" configuration option. +- Don't redisplay error message windows on session restoration. +- Remove obsolete --displayEvent command line option (replaced by --triggerEvent). +- Remove obsolete pre-version 0.7 DCOP calls. + +=== Version 1.0.7 --- 2 May 2004 === +- Fix scheduleCommand() and scheduleEmail() DCOP handling. +- Make KAlarm build for "./configure --without-arts". +- Fix email body text not being saved in email alarms. +- Fix loss of --exec command line arguments. +- Remove wasted vertical space from message windows. + +=== Version 1.0.6 (KDE 3.2.2) --- 26 March 2004 === +- Make the Quit menu item in main window quit the program. +- Update time entry field after editing as soon as mouse cursor leaves it. +- Cancel deferral if reminder is set before it, to prevent it becoming stuck. +- Prevent undeleted recurring alarms being triggered immediately. +- Don't allow alarms to be undeleted if they are completely expired. + +=== Version 1.0.5 (KDE 3.2.1) --- 24 February 2004 === +- Fix whatsThis text on bottom row of alarm list. + +=== Version 1.0.4 --- 22 February 2004 === +- Fix freeze at login when multiple alarms trigger. +- Show all audio file types in sound file chooser dialogue. + +=== Version 1.0.3 --- 15 February 2004 === +- Prevent email alarms from being sent if no 'From' address is configured. +- Omit 'Bcc' when sending email alarms if no 'Bcc' address is configured. +- Fix freeze when starting the alarm daemon. +- Fix memory leaks displaying dialogs. +- Fix scheduleCommand() and scheduleEmail() DCOP handling. +- Fix errors saving expired alarm calendar. + +=== Version 1.0.2 (KDE 3.2) --- 29 January 2004 === +- Prevent editing alarm and saving without changes from deleting the alarm. + +=== Version 1.0.1 --- 4 January 2004 === +- Fix failure to see alarms if KAlarm is reactivated while restoring session. + +=== Version 1.0.0 --- 7 December 2003 === +- Allow entered start date for timed recurrence events to be earlier than now. +- Prevent attempted entry of recurrence end date earlier than start date or today. +- Fix error displaying time of expired repeat-at-login alarms. +- Fix memory leak when sending emails with attachments. +- Fix error trying to send emails with very small attachments. +- Eliminate duplicate reload-calendar calls to alarm daemon. + +=== Version 0.9.6 (KDE 3.2 beta1) --- 7 November 2003 === +- Add option to choose foreground colour for alarm messages. +- Create new alarm by dragging KMail email onto main window or system tray icon. +- Set initial recurrence defaults to correspond to alarm start date. +- Add option for how February 29th recurrences are handled in non-leap years. +- Monthly/yearly recurrence edit: adhere to user preference for start day of week. +- Eliminate multiple confirmation prompts when deleting multiple alarms. +- Eliminate duplicate alarms in system tray tooltip. +- Fix crash after reporting error opening calendar file. +- Fix wrong status in system tray icon if KAlarm starts up with alarms disabled. +- Fix wrong number of days in Time-to-alarm column in main window. +- Fix omission of deferred alarms from system tray tooltip. + +=== Version 0.9.5 --- 3 September 2003 === +- Add option for non-modal alarm message windows. +- Add option to display a notification when an email alarm queues an email. +- Emails via KMail are sent without opening composer window, if KMail is running. +- Provide separate configuration for 'From' and 'Bcc' addresses for email alarms. +- Add exceptions to recurrence specification. +- Add multiple month selection to yearly recurrence. +- Add day of month selection in yearly recurrence. +- Add last day of month option in monthly and yearly recurrences. +- Add 2nd - 5th last week of month options in monthly and yearly recurrences. +- Add filename completion to file and command alarm edit fields. +- Display alarms-disabled indication in system tray tooltip. +- Enable file alarms to display image files. +- Fix file alarms not dislaying some text files, and improve HTML file display. +- Fix loss of changes to attachment list after editing email alarms. +- Fix wrong recurrence end date being displayed when editing an existing alarm. + +=== Version 0.9.4 --- 3 July 2003 === +- Add time-to-alarm display option to main alarm list. +- Add option to list next 24 hours' alarms in system tray tooltip. +- Create new alarm by dragging text or URL onto main window or system tray icon. +- Display reasons for failure to send an email. +- Allow editing of the list of message colours. +- Edit new alarm by context menu or double click on white space in alarm list. +- Add show expired alarms option to preferences dialog. +- Display HTML files correctly in file display alarms. + +=== Version 0.9.3 --- 4 March 2003 === +- Add preferences option to set default sound file for the Edit Alarm dialog. +- Fix display of "Invalid date" message before Edit Alarm dialog displays. + +=== Version 0.9.2 --- 28 February 2003 === +- Option to set font for individual alarm messages. +- Allow multiple alarm selection in the main window. +- KAlarm icon in alarm message window selects the alarm in the main window. +- In Edit Alarm dialog, move all recurrence edit controls into Recurrence tab. +- Add quit warning message option to preferences dialog. +- Add "New Alarm" option to system tray context menu. +- Disallow command alarms when KDE is running in kiosk mode. +- Revised storage of beep, font, colour and program arguments in calendar file. +- Always save alarms in iCalendar format (but vCalendar may still be read). +- Add reminder, recurrence and font parameters to DCOP calls. +- Fix failure to enable alarms when running in on-demand mode. + +=== Version 0.9.1 --- 16 January 2003 === +- Add option to set advance reminders for display alarms. +- In run-in-system-tray mode, warn that alarms will be disabled before quitting. +- Fix monthly and yearly recurrences on nth Monday etc. of the month. +- Fix yearly recurrences on February 29th. +- Fix recurrence start times stored in expired calendar file. +- Fix extra empty events being stored in expired calendar file. + +=== Version 0.9.0 --- 3 January 2003 === +- Add facility to import birthdays from KAddressBook +- Add option to send an email instead of displaying an alarm message. +- Add option to store and view expired alarms. +- Add copy, view and undelete actions (as applicable) for the selected alarm. +- In alarm message window, message text can be copied to clipboard using mouse. +- Allow message text to be scrolled in alarm message window if too big to fit. +- Shift key with left mouse button steps time edit arrows by 5 minutes/6 hours. +- Report failure to run command alarm (bash, ksh shells only). +- Retain repeat-at-login status on alarm deferral. +- Restore alarm messages which were displayed before KAlarm was killed or crashed. +- Store alarm data in the calendar file in a more standard way. +- Alarm message defer dialog: update recurrence deferral time limit in real time. +- Weekly recurrence edit: adhere to user preference for start day of week. +- Use standard action icons. + +=== Version 0.8.5 (KDE 3.1.1) --- 21 February 2003 === +- Fix monthly and yearly recurrences on nth Monday etc. of the month. +- Fix yearly recurrences on February 29th. +- Fix failure to enable alarms when running in on-demand mode. + +=== Version 0.8.4 (KDE 3.1) --- 8 January 2003 === +- Make KAlarm icon in message window bring main window to current desktop. +- Fix detection of KDE desktop. +- Fix entry of yearly recurrences on a specified date in the year. + +=== Version 0.8.3 --- 9 November 2002 === +- Fix no system tray icon being displayed. +- Fix multiple system tray icons being displayed. +- Fix alarms being missed after changing "Disable alarms when not running" status. + +=== Version 0.8.2 --- 2 November 2002 === +- Fix audio files not playing. + +=== Version 0.8.1 --- 1 November 2002 === +- Adhere to KDE single/double click setting when clicking on alarm list. +- Fix possible loss of alarms if KAlarm has previously used another calendar file. +- Fix coordination between "At time" and "After time" values when they change. +- Always remove alarm deferral even when next recurrence triggers instead. +- When alarm triggers, replace any existing repeat-at-login alarm message window. +- Fix deselection of Sound not working after selecting a sound file. +- Fix display of hour spin buttons in time edit spin boxes. +- Prevent time edit spin box buttons from selecting the text. +- Clean up previous alarm list highlight properly when a new alarm is selected. +- Set sensible initial focus when edit alarm dialog pages are displayed. +- Fix Quit duplicate entry in system tray context menu. + +=== Version 0.8 (KDE 3.1 beta2) --- 16 September 2002 === +- Move recurrence edit to separate tab in alarm dialog (now fits 800x600 display). +- Add accelerator keys in dialogs. +- Provide date picker for entering dates. + +=== Version 0.7.5 --- 1 September 2002 === +- Add preferences options to choose default settings for the Edit Alarm dialog. +- Fix right-to-left character sets not being displayed in message edit control. +- Make "Help -> Report Bug" use the KDE bug system (bug #43250). +- Fix session restoration not occurring. + +=== Version 0.7.4 (KDE 3.1 beta1) --- 5 August 2002 === +- Add option to prompt for confirmation on alarm deletion. +- Add option to prompt for confirmation on alarm acknowedgement. +- Display KAlarm handbook Preferences section when Help clicked in config dialog. +- Correctly adjust wrong summer times stored by version 0.5.7 (KDE 3.0.0). + +=== Version 0.7.3 --- 24 July 2002 === +- Fix loss of alarm times after saving pre-version 0.7 calendar file. +- Fix main alarm list display of hours or hours/minutes repeat interval. +- Display KAlarm handbook when Help clicked in configuration dialog. + +=== Version 0.7.2 --- 2 July 2002 === +- Fix reading wrong alarm times from pre-version 0.7 calendar file. +- Partially fix loss of alarm times after saving pre-version 0.7 calendar file. + +=== Version 0.7.1 --- 29 June 2002 === +- Prevent duplicate message windows from being displayed. +- Make Close button on message window not the default button to reduce chance + of accidental acknowledgement. +- Fix non-ASCII message texts being saved as question marks. +- Fix memory leak with recurrences. + +=== Version 0.7.0 --- 15 June 2002 === +- Add option to play audio file when message is displayed. +- Add daily, weekly, monthly, annual recurrences. +- Allow deferring only up to next scheduled repetition time. +- Don't defer repetitions when an alarm is deferred. +- Make regular repetition and repeat-at-login mutually exclusive. +- Double click on alarm in main window opens alarm edit dialog. +- Change Reset Daemon menu option to Refresh Alarms. +- Save and restore window sizes. + +=== Version 0.6.4 --- 8 May 2002 === +- Make click on system tray icon always bring KAlarm to top on current desktop. +- Fix alarms not being triggered (depending on time zone). + +=== Version 0.6.0 --- 8 March 2002 === +- Add option to execute a command instead of displaying an alarm message. +- Add Try button to alarm message edit dialog. +- Add icons in the alarm list to indicate each alarm's type. +- Display error message if a file to be displayed is not a text file. +- Reduce chance of lost late-cancel alarms when daemon check interval is reduced. +- Rename command line option --displayEvent to --triggerEvent. +- Rename DCOP function displayMessage() to triggerEvent(). +- Rename DCOP function cancelMessage() to cancelEvent(). + +=== Version 0.5.8 (KDE 3.0.5A) --- 23 November 2002 === +- Fix detection of KDE desktop. + +=== Version 0.5.8 (KDE 3.0.5) --- 4 October 2002 === +- Fix possible loss of alarms if KAlarm has previously used another calendar file. + +=== Version 0.5.8 (KDE 3.0.4) --- 18 August 2002 === +- Make "Help -> Report Bug" use the KDE bug system (bug #43250). +- Fix right-to-left character sets not being displayed in message edit control. + +=== Version 0.5.8 (KDE 3.0.3) --- 5 August 2002 === +- Adjust wrong summer times stored by version 0.5.7 (KDE 3.0.0). +- Display KAlarm handbook when Help clicked in configuration dialog. +- Make Close button on message window not the default button to reduce chance + of accidental acknowledgement. +- Fix session restoration often not occurring at login. + +=== Version 0.5.7 (KDE 3.0.1) --- 9 May 2002 === +- Use local time for alarm times instead of using a time zone. +- Make click on system tray icon always bring KAlarm to top on current desktop. + +=== Version 0.5.7 (KDE 3.0) --- 17 March 2002 === +- Show system tray icon on deferring command line-initiated message (run-in- + system-tray mode). +- Associate main window with system tray icon when displayed from message window. +- Don't start KAlarm at login, until it has been run for the first time. +- Add startup notification to kalarm.desktop. +- Prevent open main window from cancelling KDE session shutdown. +- Fix failure to display messages after daemon is restarted (run-on-demand mode). +- Fix possible failure to display command line-initiated message. +- Fix crash in some circumstances on changing run mode to run-on-demand. +- Fix crash on clicking KAlarm icon in command line-initiated message window. +- Fix crash on deferring alarm in command line-initiated message window. +- Fix duplication of repeat-at-login alarms at login. +- Fix error displaying text file messages. + +=== Version 0.5.4 --- 7 February 2002 === +- Fix extra window being displayed in session restoration. + +=== Version 0.5.2 --- 31 January 2002 === +- Fix session restore crash if in 'run continuously in system tray' mode. + +=== Version 0.5.1 --- 30 January 2002 === +- Change configuration defaults. + +=== Version 0.5 --- 29 January 2002 === +- Incorporate system tray icon into KAlarm, add --tray option. +- Add 'run continuously in system tray' operating mode. +- Don't use alarm daemon GUI application. +- Add enable/disable alarms option to main window menu. +- Add show/hide system tray icon option to main window menu. +- Add toolbar. +- Rename alarm dialog Set Alarm button to OK. +- Rename message window OK button to Close. +- Remove keyboard accelerator for Reset Daemon. +- Fix magnified system tray icon. +- Include README, etc. files in installation. + +=== Version 0.4 --- 22 December 2001 === +- Modify to use split alarm daemon/alarm daemon GUI. +- Prevent a command line error exiting all open KAlarm windows. +- Ensure the program exits after starting with --stop or --reset options. + +=== Version 0.3.5 --- 5 December 2001 === +- Add option to repeat alarms at login. +- Add context help button to main window and message window. +- Fix occasional crash on displaying non-repeating alarms. +- Fix possible failure to display alarms at login. +- Fix blank title bar when main window restored at login. +- Fix alarms not deleted from main window when displayed at login. +- Fix handling of zero-length calendar file. +- Improve error messages. +- Make documentation files installation dependent on KDE version. + +=== Version 0.3.1 --- 20 November 2001 === +- Fix build fault when using ./configure --enable-final + +=== Version 0.3 --- 4 November 2001 === +- Add option to display a file's contents instead of specifying a message. +- Add dialog option to set an alarm's time as an interval from the current time. +- Add defer option to alarm message window. +- Provide button in alarm message window to activate KAlarm. +- Make dialogs modal only for their parent window. + +=== Version 0.2 --- 20 October 2001 === +- Implement repeating alarms. +- Add extra pair of arrow buttons to time spinbox to change the hour. +- Fix sorting by colour column. +- Better What's This? texts for the main window. +- Remove -r, -s short options (use --reset, --stop instead). + +=== Version 0.1.1 --- 1 September 2001 === +- Fix documentation not being created by build. + +=== Version 0.1 --- 31 August 2001 === +- Initial release. diff --git a/kalarm/INSTALL b/kalarm/INSTALL new file mode 100644 index 000000000..34cab87ab --- /dev/null +++ b/kalarm/INSTALL @@ -0,0 +1,40 @@ +Setting up KAlarm on non-KDE desktops +===================================== + +Although KAlarm is a KDE application and requires KDE to be installed on your +system, you can still use it while running other desktops or window managers. + +In order to have alarms monitored and displayed automatically from one login +session to the next, the alarm daemon (which monitors the alarm list and tells +KAlarm to display each alarm when it becomes due) must be run automatically +when you graphically log in or otherwise start X. If you are running the KDE +desktop, the KAlarm installation process sets this up for you. + +If you want to use KAlarm with Gnome or another non-KDE window manager, you +have two alternatives: + +1) If your desktop environment/window manager performs session restoration, +ensure that the alarm daemon (kalarmd) is included in the session restoration, +and that after login or restarting X the alarm daemon is running with a +'-session' command line option, e.g. + + kalarmd -session 117f000002000100176495700000008340018 + +You can use the 'ps' command to check this. + +2) If you cannot use session restoration to start the alarm daemon correctly, +you must configure the following command to be run whenever you graphically log +in or start X: + + kalarmd --login + +If your desktop environment or window manager has a facility to configure +programs to be run at login, you can use that facility. Otherwise, you need to +add the command to an appropriate script which is run after X is started. + +If you can specify an order to start the applications, start kalarm first, then +kalarmd. + + +If you can send me details on how to set up the daemon for any particular +window manager, I will include these in the next version of KAlarm. diff --git a/kalarm/Makefile.am b/kalarm/Makefile.am new file mode 100644 index 000000000..6d7c5658a --- /dev/null +++ b/kalarm/Makefile.am @@ -0,0 +1,76 @@ +#KDE_OPTIONS = nofinal + +SUBDIRS = kalarmd lib pixmaps + +INCLUDES= -I$(top_srcdir) -I$(top_srcdir)/kalarm \ + -I$(top_srcdir)/kalarm/lib \ + -I$(top_builddir)/libkcal/libical/src \ + -I$(top_srcdir)/libkmime \ + -I$(top_srcdir)/certmanager/lib $(all_includes) + +bin_PROGRAMS = kalarm +# Don't change the order of the kalarm_SOURCES files without first checking +# that it builds with --enable-final !!! +kalarm_SOURCES = birthdaydlg.cpp main.cpp alarmevent.cpp editdlg.cpp \ + fontcolour.cpp soundpicker.cpp sounddlg.cpp alarmcalendar.cpp daemon.cpp undo.cpp \ + kalarmapp.cpp mainwindowbase.cpp mainwindow.cpp messagewin.cpp \ + preferences.cpp prefdlg.cpp kalarmiface.skel \ + traywindow.cpp dcophandler.cpp alarmguiiface.skel \ + recurrenceedit.cpp karecurrence.cpp deferdlg.cpp functions.cpp \ + fontcolourbutton.cpp alarmtimewidget.cpp \ + specialactions.cpp reminder.cpp startdaytimer.cpp \ + eventlistviewbase.cpp alarmlistview.cpp kamail.cpp timeselector.cpp \ + templatelistview.cpp templatepickdlg.cpp templatedlg.cpp \ + templatemenuaction.cpp latecancel.cpp repetition.cpp alarmtext.cpp \ + emailidcombo.cpp find.cpp pickfileradio.cpp calendarcompat.cpp +kalarm_LDFLAGS = $(all_libraries) $(KDE_RPATH) +if include_ARTS +ARTSLIB = -lartskde +endif +kalarm_LDADD = $(top_builddir)/kalarm/lib/libkalarm.la \ + $(top_builddir)/libkcal/libkcal.la \ + $(top_builddir)/libkmime/libkmime.la \ + $(top_builddir)/kalarm/kalarmd/libkalarmd.la \ + $(top_builddir)/libkdepim/libkdepim.la \ + $(top_builddir)/libkpimidentities/libkpimidentities.la \ + -lkabc -lkutils $(LIB_KDEUI) $(ARTSLIB) $(LIBXTST) +noinst_HEADERS = alarmcalendar.h alarmevent.h alarmlistview.h alarmtext.h \ + alarmtimewidget.h birthdaydlg.h daemon.h dcophandler.h deferdlg.h \ + editdlg.h editdlgprivate.h emailidcombo.h eventlistviewbase.h find.h \ + fontcolour.h fontcolourbutton.h functions.h kalarm.h kalarmapp.h \ + kamail.h karecurrence.h latecancel.h mainwindow.h mainwindowbase.h \ + messagewin.h pickfileradio.h prefdlg.h preferences.h recurrenceedit.h \ + recurrenceeditprivate.h reminder.h repetition.h sounddlg.h \ + soundpicker.h specialactions.h startdaytimer.h templatedlg.h \ + templatelistview.h templatemenuaction.h templatepickdlg.h timeselector.h \ + traywindow.h undo.h calendarcompat.h + +METASOURCES = AUTO + +alarmguiiface_DIR = $(top_srcdir)/kalarm/kalarmd + +autostart_DATA = kalarm.tray.desktop +autostartdir = $(datadir)/autostart + +xdg_apps_DATA = kalarm.desktop + +install-data-local: + $(mkinstalldirs) $(DESTDIR)$(kde_appsdir)/Applications + $(INSTALL_DATA) $(srcdir)/uninstall.desktop $(DESTDIR)$(kde_appsdir)/Applications/kalarm.desktop + +KDE_ICON = AUTO + + +rcdir = $(kde_datadir)/kalarm +rc_DATA = kalarmui.rc + +messages: rc.cpp + $(EXTRACTRC) *.rc >> rc.cpp + $(XGETTEXT) *.h *.cpp lib/*.h lib/*.cpp kalarmd/*.cpp -o $(podir)/kalarm.pot + +DOXYGEN_REFERENCES = libkcal +include $(top_srcdir)/admin/Doxyfile.am + +calendarcompat.o alarmcalendar.o: ../libkcal/libical/src/libical/ical.h +daemon.o: kalarmd/alarmdaemoniface_stub.h + diff --git a/kalarm/README b/kalarm/README new file mode 100644 index 000000000..e52cf40b2 --- /dev/null +++ b/kalarm/README @@ -0,0 +1,60 @@ +KAlarm + +KAlarm is a personal alarm message, command and email scheduler. It lets you +set up personal alarm messages which pop up on the screen at the chosen time, +or you can schedule commands to be executed or emails to be sent. + +When setting up or modifying an alarm, the options available let you: + * choose whether the alarm should display a text message, display a text file, + execute a command, or send an email. + * configure the alarm to recur on an hours/minutes, daily, weekly, monthly or + annual basis, or set it to trigger every time you log in. + * specify that a reminder should be displayed in advance of the main alarm + time(s). + * choose a colour and font for displaying the alarm message. + * specify an audible beep or a sound file to play when the message is + displayed. + * choose whether or not the alarm should be cancelled if it can't be triggered + at its scheduled time. An alarm can only be triggered while you are logged + in and running a graphical environment. If you choose not to cancel the + alarm if it can't be triggered at the correct time, it will be triggered + when you eventually log in. + +In addition to its graphical mode, alarms may also be scheduled from the +command line, or via DCOP calls from other programs. + +See the INSTALL file for installation instructions. + + +USE ON NON-KDE DESKTOPS + +For details on the requirements for setting up KAlarm to run on non-KDE +desktops, see the INSTALL file. + + +PROGRAM STATUS + +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 2 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 COPYING file for more details. + + +BUGS: + +Bugs can be reported to the KDE bug tracking system (http://bugs.kde.org). +There you can also get information about the status of older bugs. +KAlarm, as most other KDE programs, offers a menu item "Report Bug" in the +Help menu for convenient reporting of bugs or feature requests. + + +COMMENTS AND SUGGESTIONS: + +Please send your comments and suggestions to +David Jarvie <software@astrojar.org.uk>. diff --git a/kalarm/alarmcalendar.cpp b/kalarm/alarmcalendar.cpp new file mode 100644 index 000000000..85c4f7bcf --- /dev/null +++ b/kalarm/alarmcalendar.cpp @@ -0,0 +1,908 @@ +/* + * alarmcalendar.cpp - KAlarm calendar file access + * Program: kalarm + * Copyright © 2001-2006,2009 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" +#include <unistd.h> +#include <time.h> + +#include <qfile.h> +#include <qtextstream.h> +#include <qregexp.h> +#include <qtimer.h> + +#include <klocale.h> +#include <kmessagebox.h> +#include <kstandarddirs.h> +#include <kstaticdeleter.h> +#include <kconfig.h> +#include <kaboutdata.h> +#include <kio/netaccess.h> +#include <kfileitem.h> +#include <ktempfile.h> +#include <kfiledialog.h> +#include <dcopclient.h> +#include <kdebug.h> + +extern "C" { +#include <libical/ical.h> +} + +#include <libkcal/vcaldrag.h> +#include <libkcal/vcalformat.h> +#include <libkcal/icalformat.h> + +#include "calendarcompat.h" +#include "daemon.h" +#include "functions.h" +#include "kalarmapp.h" +#include "mainwindow.h" +#include "preferences.h" +#include "startdaytimer.h" +#include "alarmcalendar.moc" + +using namespace KCal; + +QString AlarmCalendar::icalProductId() +{ + return QString::fromLatin1("-//K Desktop Environment//NONSGML " KALARM_NAME " %1//EN").arg(KAlarm::currentCalendarVersionString()); +} + +static const KAEvent::Status eventTypes[AlarmCalendar::NCALS] = { + KAEvent::ACTIVE, KAEvent::EXPIRED, KAEvent::DISPLAYING, KAEvent::TEMPLATE +}; +static const QString calendarNames[AlarmCalendar::NCALS] = { + QString::fromLatin1("calendar.ics"), + QString::fromLatin1("expired.ics"), + QString::fromLatin1("displaying.ics"), + QString::fromLatin1("template.ics") +}; +static KStaticDeleter<AlarmCalendar> calendarDeleter[AlarmCalendar::NCALS]; // ensure that the calendar destructors are called + +AlarmCalendar* AlarmCalendar::mCalendars[NCALS] = { 0, 0, 0, 0 }; + + +/****************************************************************************** +* Initialise the alarm calendars, and ensure that their file names are different. +* There are 4 calendars: +* 1) A user-independent one containing the active alarms; +* 2) A historical one containing expired alarms; +* 3) A user-specific one which contains details of alarms which are currently +* being displayed to that user and which have not yet been acknowledged; +* 4) One containing alarm templates. +* Reply = true if success, false if calendar name error. +*/ +bool AlarmCalendar::initialiseCalendars() +{ + KConfig* config = kapp->config(); + config->setGroup(QString::fromLatin1("General")); + QString activeKey = QString::fromLatin1("Calendar"); + QString expiredKey = QString::fromLatin1("ExpiredCalendar"); + QString templateKey = QString::fromLatin1("TemplateCalendar"); + QString displayCal, activeCal, expiredCal, templateCal; + calendarDeleter[ACTIVE].setObject(mCalendars[ACTIVE], createCalendar(ACTIVE, config, activeCal, activeKey)); + calendarDeleter[EXPIRED].setObject(mCalendars[EXPIRED], createCalendar(EXPIRED, config, expiredCal, expiredKey)); + calendarDeleter[DISPLAY].setObject(mCalendars[DISPLAY], createCalendar(DISPLAY, config, displayCal)); + calendarDeleter[TEMPLATE].setObject(mCalendars[TEMPLATE], createCalendar(TEMPLATE, config, templateCal, templateKey)); + + QString errorKey1, errorKey2; + if (activeCal == displayCal) + errorKey1 = activeKey; + else if (expiredCal == displayCal) + errorKey1 = expiredKey; + else if (templateCal == displayCal) + errorKey1 = templateKey; + if (!errorKey1.isNull()) + { + kdError(5950) << "AlarmCalendar::initialiseCalendars(): '" << errorKey1 << "' calendar name = display calendar name\n"; + QString file = config->readPathEntry(errorKey1); + KAlarmApp::displayFatalError(i18n("%1: file name not permitted: %2").arg(errorKey1).arg(file)); + return false; + } + if (activeCal == expiredCal) + { + errorKey1 = activeKey; + errorKey2 = expiredKey; + } + else if (activeCal == templateCal) + { + errorKey1 = activeKey; + errorKey2 = templateKey; + } + else if (expiredCal == templateCal) + { + errorKey1 = expiredKey; + errorKey2 = templateKey; + } + if (!errorKey1.isNull()) + { + kdError(5950) << "AlarmCalendar::initialiseCalendars(): calendar names clash: " << errorKey1 << ", " << errorKey2 << endl; + KAlarmApp::displayFatalError(i18n("%1, %2: file names must be different").arg(errorKey1).arg(errorKey2)); + return false; + } + if (!mCalendars[ACTIVE]->valid()) + { + QString path = mCalendars[ACTIVE]->path(); + kdError(5950) << "AlarmCalendar::initialiseCalendars(): invalid name: " << path << endl; + KAlarmApp::displayFatalError(i18n("Invalid calendar file name: %1").arg(path)); + return false; + } + return true; +} + +/****************************************************************************** +* Create an alarm calendar instance. +* If 'configKey' is non-null, the calendar will be converted to ICal format. +*/ +AlarmCalendar* AlarmCalendar::createCalendar(CalID type, KConfig* config, QString& writePath, const QString& configKey) +{ + static QRegExp vcsRegExp(QString::fromLatin1("\\.vcs$")); + static QString ical = QString::fromLatin1(".ics"); + + if (configKey.isNull()) + { + writePath = locateLocal("appdata", calendarNames[type]); + return new AlarmCalendar(writePath, type); + } + else + { + QString readPath = config->readPathEntry(configKey, locateLocal("appdata", calendarNames[type])); + writePath = readPath; + writePath.replace(vcsRegExp, ical); + return new AlarmCalendar(readPath, type, writePath, configKey); + } +} + +/****************************************************************************** +* Terminate access to all calendars. +*/ +void AlarmCalendar::terminateCalendars() +{ + for (int i = 0; i < NCALS; ++i) + { + calendarDeleter[i].destructObject(); + mCalendars[i] = 0; + } +} + +/****************************************************************************** +* Return a calendar, opening it first if not already open. +* Reply = calendar instance +* = 0 if calendar could not be opened. +*/ +AlarmCalendar* AlarmCalendar::calendarOpen(CalID id) +{ + AlarmCalendar* cal = mCalendars[id]; + if (!cal->mPurgeDays) + return 0; // all events are automatically purged from the calendar + if (cal->open()) + return cal; + kdError(5950) << "AlarmCalendar::calendarOpen(" << calendarNames[id] << "): open error\n"; + return 0; +} + +/****************************************************************************** +* Find and return the event with the specified ID. +* The calendar searched is determined by the calendar identifier in the ID. +*/ +const KCal::Event* AlarmCalendar::getEvent(const QString& uniqueID) +{ + if (uniqueID.isEmpty()) + return 0; + CalID calID; + switch (KAEvent::uidStatus(uniqueID)) + { + case KAEvent::ACTIVE: calID = ACTIVE; break; + case KAEvent::TEMPLATE: calID = TEMPLATE; break; + case KAEvent::EXPIRED: calID = EXPIRED; break; + case KAEvent::DISPLAYING: calID = DISPLAY; break; + default: + return 0; + } + AlarmCalendar* cal = calendarOpen(calID); + if (!cal) + return 0; + return cal->event(uniqueID); +} + + +/****************************************************************************** +* Constructor. +* If 'icalPath' is non-null, the file will be always be saved in ICal format. +* If 'configKey' is also non-null, that config file entry will be updated when +* the file is saved in ICal format. +*/ +AlarmCalendar::AlarmCalendar(const QString& path, CalID type, const QString& icalPath, + const QString& configKey) + : mCalendar(0), + mConfigKey(icalPath.isNull() ? QString::null : configKey), + mType(eventTypes[type]), + mPurgeDays(-1), // default to not purging + mOpen(false), + mPurgeDaysQueued(-1), + mUpdateCount(0), + mUpdateSave(false) +{ + mUrl.setPath(path); // N.B. constructor mUrl(path) doesn't work with UNIX paths + mICalUrl.setPath(icalPath.isNull() ? path : icalPath); + mVCal = (icalPath.isNull() || path != icalPath); // is the calendar in ICal or VCal format? +} + +AlarmCalendar::~AlarmCalendar() +{ + close(); +} + +/****************************************************************************** +* Open the calendar file if not already open, and load it into memory. +*/ +bool AlarmCalendar::open() +{ + if (mOpen) + return true; + if (!mUrl.isValid()) + return false; + + kdDebug(5950) << "AlarmCalendar::open(" << mUrl.prettyURL() << ")\n"; + if (!mCalendar) + mCalendar = new CalendarLocal(QString::fromLatin1("UTC")); + mCalendar->setLocalTime(); // write out using local time (i.e. no time zone) + + // Check for file's existence, assuming that it does exist when uncertain, + // to avoid overwriting it. + if (!KIO::NetAccess::exists(mUrl, true, MainWindow::mainMainWindow())) + { + // The calendar file doesn't yet exist, so create it + if (create()) + load(); + } + else + { + // Load the existing calendar file + if (load() == 0) + { + if (create()) // zero-length file - create a new one + load(); + } + } + if (!mOpen) + { + delete mCalendar; + mCalendar = 0; + } + return mOpen; +} + +/****************************************************************************** +* Private method to create a new calendar file. +* It is always created in iCalendar format. +*/ +bool AlarmCalendar::create() +{ + if (mICalUrl.isLocalFile()) + return saveCal(mICalUrl.path()); + else + { + KTempFile tmpFile; + return saveCal(tmpFile.name()); + } +} + +/****************************************************************************** +* Load the calendar file into memory. +* Reply = 1 if success +* = 0 if zero-length file exists. +* = -1 if failure to load calendar file +* = -2 if instance uninitialised. +*/ +int AlarmCalendar::load() +{ + if (!mCalendar) + return -2; + + kdDebug(5950) << "AlarmCalendar::load(): " << mUrl.prettyURL() << endl; + QString tmpFile; + if (!KIO::NetAccess::download(mUrl, tmpFile, MainWindow::mainMainWindow())) + { + kdError(5950) << "AlarmCalendar::load(): Load failure" << endl; + KMessageBox::error(0, i18n("Cannot open calendar:\n%1").arg(mUrl.prettyURL())); + return -1; + } + kdDebug(5950) << "AlarmCalendar::load(): --- Downloaded to " << tmpFile << endl; + mCalendar->setTimeZoneId(QString::null); // default to the local time zone for reading + bool loaded = mCalendar->load(tmpFile); + mCalendar->setLocalTime(); // write using local time (i.e. no time zone) + if (!loaded) + { + // Check if the file is zero length + KIO::NetAccess::removeTempFile(tmpFile); + KIO::UDSEntry uds; + KIO::NetAccess::stat(mUrl, uds, MainWindow::mainMainWindow()); + KFileItem fi(uds, mUrl); + if (!fi.size()) + return 0; // file is zero length + kdError(5950) << "AlarmCalendar::load(): Error loading calendar file '" << tmpFile << "'" << endl; + KMessageBox::error(0, i18n("Error loading calendar:\n%1\n\nPlease fix or delete the file.").arg(mUrl.prettyURL())); + // load() could have partially populated the calendar, so clear it out + mCalendar->close(); + delete mCalendar; + mCalendar = 0; + return -1; + } + if (!mLocalFile.isEmpty()) + KIO::NetAccess::removeTempFile(mLocalFile); // removes it only if it IS a temporary file + mLocalFile = tmpFile; + + CalendarCompat::fix(*mCalendar, mLocalFile); // convert events to current KAlarm format for when calendar is saved + mOpen = true; + return 1; +} + +/****************************************************************************** +* Reload the calendar file into memory. +*/ +bool AlarmCalendar::reload() +{ + if (!mCalendar) + return false; + kdDebug(5950) << "AlarmCalendar::reload(): " << mUrl.prettyURL() << endl; + close(); + bool result = open(); + return result; +} + +/****************************************************************************** +* Save the calendar from memory to file. +* If a filename is specified, create a new calendar file. +*/ +bool AlarmCalendar::saveCal(const QString& newFile) +{ + if (!mCalendar || !mOpen && newFile.isNull()) + return false; + + kdDebug(5950) << "AlarmCalendar::saveCal(\"" << newFile << "\", " << mType << ")\n"; + QString saveFilename = newFile.isNull() ? mLocalFile : newFile; + if (mVCal && newFile.isNull() && mUrl.isLocalFile()) + saveFilename = mICalUrl.path(); + if (!mCalendar->save(saveFilename, new ICalFormat)) + { + kdError(5950) << "AlarmCalendar::saveCal(" << saveFilename << "): failed.\n"; + KMessageBox::error(0, i18n("Failed to save calendar to\n'%1'").arg(mICalUrl.prettyURL())); + return false; + } + + if (!mICalUrl.isLocalFile()) + { + if (!KIO::NetAccess::upload(saveFilename, mICalUrl, MainWindow::mainMainWindow())) + { + kdError(5950) << "AlarmCalendar::saveCal(" << saveFilename << "): upload failed.\n"; + KMessageBox::error(0, i18n("Cannot upload calendar to\n'%1'").arg(mICalUrl.prettyURL())); + return false; + } + } + + if (mVCal) + { + // The file was in vCalendar format, but has now been saved in iCalendar format. + // Save the change in the config file. + if (!mConfigKey.isNull()) + { + KConfig* config = kapp->config(); + config->setGroup(QString::fromLatin1("General")); + config->writePathEntry(mConfigKey, mICalUrl.path()); + config->sync(); + } + mUrl = mICalUrl; + mVCal = false; + } + + mUpdateSave = false; + emit calendarSaved(this); + return true; +} + +/****************************************************************************** +* Delete any temporary file at program exit. +*/ +void AlarmCalendar::close() +{ + if (!mLocalFile.isEmpty()) + { + KIO::NetAccess::removeTempFile(mLocalFile); // removes it only if it IS a temporary file + mLocalFile = ""; + } + if (mCalendar) + { + mCalendar->close(); + delete mCalendar; + mCalendar = 0; + } + mOpen = false; +} + +/****************************************************************************** +* Import alarms from an external calendar and merge them into KAlarm's calendar. +* The alarms are given new unique event IDs. +* Parameters: parent = parent widget for error message boxes +* Reply = true if all alarms in the calendar were successfully imported +* = false if any alarms failed to be imported. +*/ +bool AlarmCalendar::importAlarms(QWidget* parent) +{ + KURL url = KFileDialog::getOpenURL(QString::fromLatin1(":importalarms"), + QString::fromLatin1("*.vcs *.ics|%1").arg(i18n("Calendar Files")), parent); + if (url.isEmpty()) + { + kdError(5950) << "AlarmCalendar::importAlarms(): Empty URL" << endl; + return false; + } + if (!url.isValid()) + { + kdDebug(5950) << "AlarmCalendar::importAlarms(): Invalid URL" << endl; + return false; + } + kdDebug(5950) << "AlarmCalendar::importAlarms(" << url.prettyURL() << ")" << endl; + + bool success = true; + QString filename; + bool local = url.isLocalFile(); + if (local) + { + filename = url.path(); + if (!KStandardDirs::exists(filename)) + { + kdDebug(5950) << "AlarmCalendar::importAlarms(): File '" << url.prettyURL() << "' not found" << endl; + KMessageBox::error(parent, i18n("Could not load calendar '%1'.").arg(url.prettyURL())); + return false; + } + } + else + { + if (!KIO::NetAccess::download(url, filename, MainWindow::mainMainWindow())) + { + kdError(5950) << "AlarmCalendar::importAlarms(): Download failure" << endl; + KMessageBox::error(parent, i18n("Cannot download calendar:\n%1").arg(url.prettyURL())); + return false; + } + kdDebug(5950) << "--- Downloaded to " << filename << endl; + } + + // Read the calendar and add its alarms to the current calendars + CalendarLocal cal(QString::fromLatin1("UTC")); + cal.setLocalTime(); // write out using local time (i.e. no time zone) + success = cal.load(filename); + if (!success) + { + kdDebug(5950) << "AlarmCalendar::importAlarms(): error loading calendar '" << filename << "'" << endl; + KMessageBox::error(parent, i18n("Could not load calendar '%1'.").arg(url.prettyURL())); + } + else + { + CalendarCompat::fix(cal, filename); + bool saveActive = false; + bool saveExpired = false; + bool saveTemplate = false; + AlarmCalendar* active = activeCalendar(); + AlarmCalendar* expired = expiredCalendar(); + AlarmCalendar* templat = 0; + AlarmCalendar* acal; + Event::List events = cal.rawEvents(); + for (Event::List::ConstIterator it = events.begin(); it != events.end(); ++it) + { + const Event* event = *it; + if (event->alarms().isEmpty() || !KAEvent(*event).valid()) + continue; // ignore events without alarms, or usable alarms + KAEvent::Status type = KAEvent::uidStatus(event->uid()); + switch (type) + { + case KAEvent::ACTIVE: + acal = active; + saveActive = true; + break; + case KAEvent::EXPIRED: + acal = expired; + saveExpired = true; + break; + case KAEvent::TEMPLATE: + if (!templat) + templat = templateCalendarOpen(); + acal = templat; + saveTemplate = true; + break; + default: + continue; + } + if (!acal) + continue; + + Event* newev = new Event(*event); + + // If there is a display alarm without display text, use the event + // summary text instead. + if (type == KAEvent::ACTIVE && !newev->summary().isEmpty()) + { + const Alarm::List& alarms = newev->alarms(); + for (Alarm::List::ConstIterator ait = alarms.begin(); ait != alarms.end(); ++ait) + { + Alarm* alarm = *ait; + if (alarm->type() == Alarm::Display && alarm->text().isEmpty()) + alarm->setText(newev->summary()); + } + newev->setSummary(QString::null); // KAlarm only uses summary for template names + } + // Give the event a new ID and add it to the calendar + newev->setUid(KAEvent::uid(CalFormat::createUniqueId(), type)); + if (!acal->mCalendar->addEvent(newev)) + success = false; + } + + // Save any calendars which have been modified + if (saveActive) + active->saveCal(); + if (saveExpired) + expired->saveCal(); + if (saveTemplate) + templat->saveCal(); + } + if (!local) + KIO::NetAccess::removeTempFile(filename); + return success; +} + +/****************************************************************************** +* Flag the start of a group of calendar update calls. +* The purpose is to avoid multiple calendar saves during a group of operations. +*/ +void AlarmCalendar::startUpdate() +{ + ++mUpdateCount; +} + +/****************************************************************************** +* Flag the end of a group of calendar update calls. +* The calendar is saved if appropriate. +*/ +bool AlarmCalendar::endUpdate() +{ + if (mUpdateCount > 0) + --mUpdateCount; + if (!mUpdateCount) + { + if (mUpdateSave) + return saveCal(); + } + return true; +} + +/****************************************************************************** +* Save the calendar, or flag it for saving if in a group of calendar update calls. +*/ +bool AlarmCalendar::save() +{ + if (mUpdateCount) + { + mUpdateSave = true; + return true; + } + else + return saveCal(); +} + +#if 0 +/****************************************************************************** +* If it is VCal format, convert the calendar URL to ICal and save the new URL +* in the config file. +*/ +void AlarmCalendar::convertToICal() +{ + if (mVCal) + { + if (!mConfigKey.isNull()) + { + KConfig* config = kapp->config(); + config->setGroup(QString::fromLatin1("General")); + config->writePathEntry(mConfigKey, mICalUrl.path()); + config->sync(); + } + mUrl = mICalUrl; + mVCal = false; + } +} +#endif + +/****************************************************************************** +* Set the number of days to keep alarms. +* Alarms which are older are purged immediately, and at the start of each day. +*/ +void AlarmCalendar::setPurgeDays(int days) +{ + if (days != mPurgeDays) + { + int oldDays = mPurgeDays; + mPurgeDays = days; + if (mPurgeDays <= 0) + StartOfDayTimer::disconnect(this); + if (oldDays < 0 || days >= 0 && days < oldDays) + { + // Alarms are now being kept for less long, so purge them + if (open()) + slotPurge(); + } + else if (mPurgeDays > 0) + startPurgeTimer(); + } +} + +/****************************************************************************** +* Called at the start of each day by the purge timer. +* Purge all events from the calendar whose end time is longer ago than 'mPurgeDays'. +*/ +void AlarmCalendar::slotPurge() +{ + purge(mPurgeDays); + startPurgeTimer(); +} + +/****************************************************************************** +* Purge all events from the calendar whose end time is longer ago than +* 'daysToKeep'. All events are deleted if 'daysToKeep' is zero. +*/ +void AlarmCalendar::purge(int daysToKeep) +{ + if (mPurgeDaysQueued < 0 || daysToKeep < mPurgeDaysQueued) + mPurgeDaysQueued = daysToKeep; + + // Do the purge once any other current operations are completed + theApp()->processQueue(); +} + +/****************************************************************************** +* This method must only be called from the main KAlarm queue processing loop, +* to prevent asynchronous calendar operations interfering with one another. +* +* Purge all events from the calendar whose end time is longer ago than 'daysToKeep'. +* All events are deleted if 'daysToKeep' is zero. +* The calendar must already be open. +*/ +void AlarmCalendar::purgeIfQueued() +{ + if (mPurgeDaysQueued >= 0) + { + if (open()) + { + kdDebug(5950) << "AlarmCalendar::purgeIfQueued(" << mPurgeDaysQueued << ")\n"; + bool changed = false; + QDate cutoff = QDate::currentDate().addDays(-mPurgeDaysQueued); + Event::List events = mCalendar->rawEvents(); + for (Event::List::ConstIterator it = events.begin(); it != events.end(); ++it) + { + Event* kcalEvent = *it; + if (!mPurgeDaysQueued || kcalEvent->created().date() < cutoff) + { + mCalendar->deleteEvent(kcalEvent); + changed = true; + } + } + if (changed) + { + saveCal(); + emit purged(); + } + mPurgeDaysQueued = -1; + } + } +} + + +/****************************************************************************** +* Start the purge timer to expire at the start of the next day (using the user- +* defined start-of-day time). +*/ +void AlarmCalendar::startPurgeTimer() +{ + if (mPurgeDays > 0) + StartOfDayTimer::connect(this, SLOT(slotPurge())); +} + +/****************************************************************************** +* Add the specified event to the calendar. +* If it is the active calendar and 'useEventID' is false, a new event ID is +* created. In all other cases, the event ID is taken from 'event'. +* 'event' is updated with the actual event ID. +* Reply = the KCal::Event as written to the calendar. +*/ +Event* AlarmCalendar::addEvent(KAEvent& event, bool useEventID) +{ + if (!mOpen) + return 0; + QString id = event.id(); + Event* kcalEvent = new Event; + if (mType == KAEvent::ACTIVE) + { + if (id.isEmpty()) + useEventID = false; + if (!useEventID) + event.setEventID(kcalEvent->uid()); + } + else + { + if (id.isEmpty()) + id = kcalEvent->uid(); + useEventID = true; + } + if (useEventID) + { + id = KAEvent::uid(id, mType); + event.setEventID(id); + kcalEvent->setUid(id); + } + event.updateKCalEvent(*kcalEvent, false, (mType == KAEvent::EXPIRED), true); + mCalendar->addEvent(kcalEvent); + event.clearUpdated(); + return kcalEvent; +} + +/****************************************************************************** +* Update the specified event in the calendar with its new contents. +* The event retains the same ID. +*/ +void AlarmCalendar::updateEvent(const KAEvent& evnt) +{ + if (mOpen) + { + Event* kcalEvent = event(evnt.id()); + if (kcalEvent) + { + evnt.updateKCalEvent(*kcalEvent); + evnt.clearUpdated(); + if (mType == KAEvent::ACTIVE) + Daemon::savingEvent(evnt.id()); + return; + } + } + if (mType == KAEvent::ACTIVE) + Daemon::eventHandled(evnt.id(), false); +} + +/****************************************************************************** +* Delete the specified event from the calendar, if it exists. +* The calendar is then optionally saved. +*/ +bool AlarmCalendar::deleteEvent(const QString& eventID, bool saveit) +{ + if (mOpen) + { + Event* kcalEvent = event(eventID); + if (kcalEvent) + { + mCalendar->deleteEvent(kcalEvent); + if (mType == KAEvent::ACTIVE) + Daemon::savingEvent(eventID); + if (saveit) + return save(); + return true; + } + } + if (mType == KAEvent::ACTIVE) + Daemon::eventHandled(eventID, false); + return false; +} + +/****************************************************************************** +* Emit a signal to indicate whether the calendar is empty. +*/ +void AlarmCalendar::emitEmptyStatus() +{ + emit emptyStatus(events().isEmpty()); +} + +/****************************************************************************** +* Return the event with the specified ID. +*/ +KCal::Event* AlarmCalendar::event(const QString& uniqueID) +{ + return mCalendar ? mCalendar->event(uniqueID) : 0; +} + +/****************************************************************************** +* Return all events in the calendar which contain usable alarms. +*/ +KCal::Event::List AlarmCalendar::events() +{ + if (!mCalendar) + return KCal::Event::List(); + KCal::Event::List list = mCalendar->rawEvents(); + KCal::Event::List::Iterator it = list.begin(); + while (it != list.end()) + { + KCal::Event* event = *it; + if (event->alarms().isEmpty() || !KAEvent(*event).valid()) + it = list.remove(it); + else + ++it; + } + return list; +} + +/****************************************************************************** +* Return all events which have alarms falling within the specified time range. +*/ +Event::List AlarmCalendar::eventsWithAlarms(const QDateTime& from, const QDateTime& to) +{ + kdDebug(5950) << "AlarmCalendar::eventsWithAlarms(" << from.toString() << " - " << to.toString() << ")\n"; + Event::List evnts; + QDateTime dt; + Event::List allEvents = events(); // ignore events without usable alarms + for (Event::List::ConstIterator it = allEvents.begin(); it != allEvents.end(); ++it) + { + Event* e = *it; + bool recurs = e->doesRecur(); + int endOffset = 0; + bool endOffsetValid = false; + const Alarm::List& alarms = e->alarms(); + for (Alarm::List::ConstIterator ait = alarms.begin(); ait != alarms.end(); ++ait) + { + Alarm* alarm = *ait; + if (alarm->enabled()) + { + if (recurs) + { + if (alarm->hasTime()) + dt = alarm->time(); + else + { + // The alarm time is defined by an offset from the event start or end time. + // Find the offset from the event start time, which is also used as the + // offset from the recurrence time. + int offset = 0; + if (alarm->hasStartOffset()) + offset = alarm->startOffset().asSeconds(); + else if (alarm->hasEndOffset()) + { + if (!endOffsetValid) + { + endOffset = e->hasDuration() ? e->duration() : e->hasEndDate() ? e->dtStart().secsTo(e->dtEnd()) : 0; + endOffsetValid = true; + } + offset = alarm->endOffset().asSeconds() + endOffset; + } + // Adjust the 'from' date/time and find the next recurrence at or after it + QDateTime pre = from.addSecs(-offset - 1); + if (e->doesFloat() && pre.time() < Preferences::startOfDay()) + pre = pre.addDays(-1); // today's recurrence (if today recurs) is still to come + dt = e->recurrence()->getNextDateTime(pre); + if (!dt.isValid()) + continue; + dt = dt.addSecs(offset); + } + } + else + dt = alarm->time(); + if (dt >= from && dt <= to) + { + kdDebug(5950) << "AlarmCalendar::events() '" << e->summary() + << "': " << dt.toString() << endl; + evnts.append(e); + break; + } + } + } + } + return evnts; +} diff --git a/kalarm/alarmcalendar.h b/kalarm/alarmcalendar.h new file mode 100644 index 000000000..87abc2d13 --- /dev/null +++ b/kalarm/alarmcalendar.h @@ -0,0 +1,114 @@ +/* + * alarmcalendar.h - KAlarm calendar file access + * Program: kalarm + * Copyright © 2001-2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef ALARMCALENDAR_H +#define ALARMCALENDAR_H + +#include <qobject.h> +#include <kurl.h> +#include <libkcal/calendarlocal.h> +#include "alarmevent.h" + +class KConfig; + + +/** Provides read and write access to calendar files. + * Either vCalendar or iCalendar files may be read, but the calendar is saved + * only in iCalendar format to avoid information loss. + */ +class AlarmCalendar : public QObject +{ + Q_OBJECT + public: + virtual ~AlarmCalendar(); + bool valid() const { return mUrl.isValid(); } + KAEvent::Status type() const { return mType; } + bool open(); + int load(); + bool reload(); + bool save(); + void close(); + void startUpdate(); + bool endUpdate(); + KCal::Event* event(const QString& uniqueID); + KCal::Event::List events(); + KCal::Event::List eventsWithAlarms(const QDateTime& from, const QDateTime& to); + KCal::Event* addEvent(KAEvent&, bool useEventID = false); + void updateEvent(const KAEvent&); + bool deleteEvent(const QString& eventID, bool save = false); + void emitEmptyStatus(); + void purgeAll() { purge(0); } + void setPurgeDays(int days); + void purgeIfQueued(); // must only be called from KAlarmApp::processQueue() + bool isOpen() const { return mOpen; } + QString path() const { return mUrl.prettyURL(); } + QString urlString() const { return mUrl.url(); } + + static QString icalProductId(); + static bool initialiseCalendars(); + static void terminateCalendars(); + static AlarmCalendar* activeCalendar() { return mCalendars[ACTIVE]; } + static AlarmCalendar* expiredCalendar() { return mCalendars[EXPIRED]; } + static AlarmCalendar* displayCalendar() { return mCalendars[DISPLAY]; } + static AlarmCalendar* templateCalendar() { return mCalendars[TEMPLATE]; } + static AlarmCalendar* activeCalendarOpen() { return calendarOpen(ACTIVE); } + static AlarmCalendar* expiredCalendarOpen() { return calendarOpen(EXPIRED); } + static AlarmCalendar* displayCalendarOpen() { return calendarOpen(DISPLAY); } + static AlarmCalendar* templateCalendarOpen() { return calendarOpen(TEMPLATE); } + static bool importAlarms(QWidget*); + static const KCal::Event* getEvent(const QString& uniqueID); + + enum CalID { ACTIVE, EXPIRED, DISPLAY, TEMPLATE, NCALS }; + + signals: + void calendarSaved(AlarmCalendar*); + void purged(); + void emptyStatus(bool empty); + + private slots: + void slotPurge(); + + private: + AlarmCalendar(const QString& file, CalID, const QString& icalFile = QString::null, + const QString& configKey = QString::null); + bool create(); + bool saveCal(const QString& newFile = QString::null); + void purge(int daysToKeep); + void startPurgeTimer(); + static AlarmCalendar* createCalendar(CalID, KConfig*, QString& writePath, const QString& configKey = QString::null); + static AlarmCalendar* calendarOpen(CalID); + + static AlarmCalendar* mCalendars[NCALS]; // the calendars + + KCal::CalendarLocal* mCalendar; + KURL mUrl; // URL of current calendar file + KURL mICalUrl; // URL of iCalendar file + QString mLocalFile; // calendar file, or local copy if it's a remote file + QString mConfigKey; // config file key for this calendar's URL + KAEvent::Status mType; // what type of events the calendar file is for + int mPurgeDays; // how long to keep alarms, 0 = don't keep, -1 = keep indefinitely + bool mOpen; // true if the calendar file is open + int mPurgeDaysQueued; // >= 0 to purge the calendar when called from KAlarmApp::processLoop() + int mUpdateCount; // nesting level of group of calendar update calls + bool mUpdateSave; // save() was called while mUpdateCount > 0 + bool mVCal; // true if calendar file is in VCal format +}; + +#endif // ALARMCALENDAR_H diff --git a/kalarm/alarmevent.cpp b/kalarm/alarmevent.cpp new file mode 100644 index 000000000..28b0a55a4 --- /dev/null +++ b/kalarm/alarmevent.cpp @@ -0,0 +1,3488 @@ +/* + * alarmevent.cpp - represents calendar alarms and events + * Program: kalarm + * Copyright © 2001-2009 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <stdlib.h> +#include <time.h> +#include <ctype.h> +#include <qcolor.h> +#include <qregexp.h> + +#include <klocale.h> +#include <kdebug.h> + +#include "alarmtext.h" +#include "functions.h" +#include "kalarmapp.h" +#include "kamail.h" +#include "preferences.h" +#include "alarmcalendar.h" +#include "alarmevent.h" +using namespace KCal; + + +const QCString APPNAME("KALARM"); + +// KAlarm version which first used the current calendar/event format. +// If this changes, KAEvent::convertKCalEvents() must be changed correspondingly. +// The string version is the KAlarm version string used in the calendar file. +QString KAEvent::calVersionString() { return QString::fromLatin1("1.5.0"); } +int KAEvent::calVersion() { return KAlarm::Version(1,5,0); } + +// Custom calendar properties. +// Note that all custom property names are prefixed with X-KDE-KALARM- in the calendar file. +// - Event properties +static const QCString NEXT_RECUR_PROPERTY("NEXTRECUR"); // X-KDE-KALARM-NEXTRECUR property +static const QCString REPEAT_PROPERTY("REPEAT"); // X-KDE-KALARM-REPEAT property +// - General alarm properties +static const QCString TYPE_PROPERTY("TYPE"); // X-KDE-KALARM-TYPE property +static const QString FILE_TYPE = QString::fromLatin1("FILE"); +static const QString AT_LOGIN_TYPE = QString::fromLatin1("LOGIN"); +static const QString REMINDER_TYPE = QString::fromLatin1("REMINDER"); +static const QString REMINDER_ONCE_TYPE = QString::fromLatin1("REMINDER_ONCE"); +static const QString ARCHIVE_REMINDER_ONCE_TYPE = QString::fromLatin1("ONCE"); +static const QString TIME_DEFERRAL_TYPE = QString::fromLatin1("DEFERRAL"); +static const QString DATE_DEFERRAL_TYPE = QString::fromLatin1("DATE_DEFERRAL"); +static const QString DISPLAYING_TYPE = QString::fromLatin1("DISPLAYING"); // used only in displaying calendar +static const QString PRE_ACTION_TYPE = QString::fromLatin1("PRE"); +static const QString POST_ACTION_TYPE = QString::fromLatin1("POST"); +static const QCString NEXT_REPEAT_PROPERTY("NEXTREPEAT"); // X-KDE-KALARM-NEXTREPEAT property +// - Display alarm properties +static const QCString FONT_COLOUR_PROPERTY("FONTCOLOR"); // X-KDE-KALARM-FONTCOLOR property +// - Email alarm properties +static const QCString EMAIL_ID_PROPERTY("EMAILID"); // X-KDE-KALARM-EMAILID property +// - Audio alarm properties +static const QCString VOLUME_PROPERTY("VOLUME"); // X-KDE-KALARM-VOLUME property +static const QCString SPEAK_PROPERTY("SPEAK"); // X-KDE-KALARM-SPEAK property + +// Event categories +static const QString DATE_ONLY_CATEGORY = QString::fromLatin1("DATE"); +static const QString EMAIL_BCC_CATEGORY = QString::fromLatin1("BCC"); +static const QString CONFIRM_ACK_CATEGORY = QString::fromLatin1("ACKCONF"); +static const QString LATE_CANCEL_CATEGORY = QString::fromLatin1("LATECANCEL;"); +static const QString AUTO_CLOSE_CATEGORY = QString::fromLatin1("LATECLOSE;"); +static const QString TEMPL_AFTER_TIME_CATEGORY = QString::fromLatin1("TMPLAFTTIME;"); +static const QString KMAIL_SERNUM_CATEGORY = QString::fromLatin1("KMAIL:"); +static const QString KORGANIZER_CATEGORY = QString::fromLatin1("KORG"); +static const QString DEFER_CATEGORY = QString::fromLatin1("DEFER;"); +static const QString ARCHIVE_CATEGORY = QString::fromLatin1("SAVE"); +static const QString ARCHIVE_CATEGORIES = QString::fromLatin1("SAVE:"); +static const QString LOG_CATEGORY = QString::fromLatin1("LOG:"); +static const QString xtermURL = QString::fromLatin1("xterm:"); + +// Event status strings +static const QString DISABLED_STATUS = QString::fromLatin1("DISABLED"); + +static const QString EXPIRED_UID = QString::fromLatin1("-exp-"); +static const QString DISPLAYING_UID = QString::fromLatin1("-disp-"); +static const QString TEMPLATE_UID = QString::fromLatin1("-tmpl-"); +static const QString KORGANIZER_UID = QString::fromLatin1("-korg-"); + +struct AlarmData +{ + const Alarm* alarm; + QString cleanText; // text or audio file name + uint emailFromId; + EmailAddressList emailAddresses; + QString emailSubject; + QStringList emailAttachments; + QFont font; + QColor bgColour, fgColour; + float soundVolume; + float fadeVolume; + int fadeSeconds; + int startOffsetSecs; + bool speak; + KAAlarm::SubType type; + KAAlarmEventBase::Type action; + int displayingFlags; + bool defaultFont; + bool reminderOnceOnly; + bool isEmailText; + bool commandScript; + int repeatCount; + int repeatInterval; + int nextRepeat; +}; +typedef QMap<KAAlarm::SubType, AlarmData> AlarmMap; + +static void setProcedureAlarm(Alarm*, const QString& commandLine); + + +/*============================================================================= += Class KAEvent += Corresponds to a KCal::Event instance. +=============================================================================*/ + +inline void KAEvent::set_deferral(DeferType type) +{ + if (type) + { + if (!mDeferral) + ++mAlarmCount; + } + else + { + if (mDeferral) + --mAlarmCount; + } + mDeferral = type; +} + +inline void KAEvent::set_reminder(int minutes) +{ + if (minutes && !mReminderMinutes) + ++mAlarmCount; + else if (!minutes && mReminderMinutes) + --mAlarmCount; + mReminderMinutes = minutes; + mArchiveReminderMinutes = 0; +} + +inline void KAEvent::set_archiveReminder() +{ + if (mReminderMinutes) + --mAlarmCount; + mArchiveReminderMinutes = mReminderMinutes; + mReminderMinutes = 0; +} + + +void KAEvent::copy(const KAEvent& event) +{ + KAAlarmEventBase::copy(event); + mTemplateName = event.mTemplateName; + mAudioFile = event.mAudioFile; + mPreAction = event.mPreAction; + mPostAction = event.mPostAction; + mStartDateTime = event.mStartDateTime; + mSaveDateTime = event.mSaveDateTime; + mAtLoginDateTime = event.mAtLoginDateTime; + mDeferralTime = event.mDeferralTime; + mDisplayingTime = event.mDisplayingTime; + mDisplayingFlags = event.mDisplayingFlags; + mReminderMinutes = event.mReminderMinutes; + mArchiveReminderMinutes = event.mArchiveReminderMinutes; + mDeferDefaultMinutes = event.mDeferDefaultMinutes; + mRevision = event.mRevision; + mAlarmCount = event.mAlarmCount; + mDeferral = event.mDeferral; + mLogFile = event.mLogFile; + mCommandXterm = event.mCommandXterm; + mKMailSerialNumber = event.mKMailSerialNumber; + mCopyToKOrganizer = event.mCopyToKOrganizer; + mReminderOnceOnly = event.mReminderOnceOnly; + mMainExpired = event.mMainExpired; + mArchiveRepeatAtLogin = event.mArchiveRepeatAtLogin; + mArchive = event.mArchive; + mTemplateAfterTime = event.mTemplateAfterTime; + mEnabled = event.mEnabled; + mUpdated = event.mUpdated; + delete mRecurrence; + if (event.mRecurrence) + mRecurrence = new KARecurrence(*event.mRecurrence); + else + mRecurrence = 0; +} + +/****************************************************************************** + * Initialise the KAEvent from a KCal::Event. + */ +void KAEvent::set(const Event& event) +{ + // Extract status from the event + mEventID = event.uid(); + mRevision = event.revision(); + mTemplateName = QString::null; + mLogFile = QString::null; + mTemplateAfterTime = -1; + mBeep = false; + mSpeak = false; + mEmailBcc = false; + mCommandXterm = false; + mCopyToKOrganizer = false; + mConfirmAck = false; + mArchive = false; + mReminderOnceOnly = false; + mAutoClose = false; + mArchiveRepeatAtLogin = false; + mArchiveReminderMinutes = 0; + mDeferDefaultMinutes = 0; + mLateCancel = 0; + mKMailSerialNumber = 0; + mBgColour = QColor(255, 255, 255); // missing/invalid colour - return white background + mFgColour = QColor(0, 0, 0); // and black foreground + mDefaultFont = true; + mEnabled = true; + clearRecur(); + bool ok; + bool dateOnly = false; + const QStringList cats = event.categories(); + for (unsigned int i = 0; i < cats.count(); ++i) + { + if (cats[i] == DATE_ONLY_CATEGORY) + dateOnly = true; + else if (cats[i] == CONFIRM_ACK_CATEGORY) + mConfirmAck = true; + else if (cats[i] == EMAIL_BCC_CATEGORY) + mEmailBcc = true; + else if (cats[i] == ARCHIVE_CATEGORY) + mArchive = true; + else if (cats[i] == KORGANIZER_CATEGORY) + mCopyToKOrganizer = true; + else if (cats[i].startsWith(KMAIL_SERNUM_CATEGORY)) + mKMailSerialNumber = cats[i].mid(KMAIL_SERNUM_CATEGORY.length()).toULong(); + else if (cats[i].startsWith(LOG_CATEGORY)) + { + QString logUrl = cats[i].mid(LOG_CATEGORY.length()); + if (logUrl == xtermURL) + mCommandXterm = true; + else + mLogFile = logUrl; + } + else if (cats[i].startsWith(ARCHIVE_CATEGORIES)) + { + // It's the archive flag plus a reminder time and/or repeat-at-login flag + mArchive = true; + QStringList list = QStringList::split(';', cats[i].mid(ARCHIVE_CATEGORIES.length())); + for (unsigned int j = 0; j < list.count(); ++j) + { + if (list[j] == AT_LOGIN_TYPE) + mArchiveRepeatAtLogin = true; + else if (list[j] == ARCHIVE_REMINDER_ONCE_TYPE) + mReminderOnceOnly = true; + else + { + char ch; + const char* cat = list[j].latin1(); + while ((ch = *cat) != 0 && (ch < '0' || ch > '9')) + ++cat; + if (ch) + { + mArchiveReminderMinutes = ch - '0'; + while ((ch = *++cat) >= '0' && ch <= '9') + mArchiveReminderMinutes = mArchiveReminderMinutes * 10 + ch - '0'; + switch (ch) + { + case 'M': break; + case 'H': mArchiveReminderMinutes *= 60; break; + case 'D': mArchiveReminderMinutes *= 1440; break; + } + } + } + } + } + else if (cats[i].startsWith(DEFER_CATEGORY)) + { + mDeferDefaultMinutes = static_cast<int>(cats[i].mid(DEFER_CATEGORY.length()).toUInt(&ok)); + if (!ok) + mDeferDefaultMinutes = 0; // invalid parameter + } + else if (cats[i].startsWith(TEMPL_AFTER_TIME_CATEGORY)) + { + mTemplateAfterTime = static_cast<int>(cats[i].mid(TEMPL_AFTER_TIME_CATEGORY.length()).toUInt(&ok)); + if (!ok) + mTemplateAfterTime = -1; // invalid parameter + } + else if (cats[i].startsWith(LATE_CANCEL_CATEGORY)) + { + mLateCancel = static_cast<int>(cats[i].mid(LATE_CANCEL_CATEGORY.length()).toUInt(&ok)); + if (!ok || !mLateCancel) + mLateCancel = 1; // invalid parameter defaults to 1 minute + } + else if (cats[i].startsWith(AUTO_CLOSE_CATEGORY)) + { + mLateCancel = static_cast<int>(cats[i].mid(AUTO_CLOSE_CATEGORY.length()).toUInt(&ok)); + if (!ok || !mLateCancel) + mLateCancel = 1; // invalid parameter defaults to 1 minute + mAutoClose = true; + } + } + QString prop = event.customProperty(APPNAME, REPEAT_PROPERTY); + if (!prop.isEmpty()) + { + // This property is used when the main alarm has expired + QStringList list = QStringList::split(':', prop); + if (list.count() >= 2) + { + int interval = static_cast<int>(list[0].toUInt()); + int count = static_cast<int>(list[1].toUInt()); + if (interval && count) + { + mRepeatInterval = interval; + mRepeatCount = count; + } + } + } + mNextMainDateTime = readDateTime(event, dateOnly, mStartDateTime); + mSaveDateTime = event.created(); + if (uidStatus() == TEMPLATE) + mTemplateName = event.summary(); + if (event.statusStr() == DISABLED_STATUS) + mEnabled = false; + + // Extract status from the event's alarms. + // First set up defaults. + mActionType = T_MESSAGE; + mMainExpired = true; + mRepeatAtLogin = false; + mDisplaying = false; + mRepeatSound = false; + mCommandScript = false; + mDeferral = NO_DEFERRAL; + mSoundVolume = -1; + mFadeVolume = -1; + mFadeSeconds = 0; + mReminderMinutes = 0; + mEmailFromIdentity = 0; + mText = ""; + mAudioFile = ""; + mPreAction = ""; + mPostAction = ""; + mEmailSubject = ""; + mEmailAddresses.clear(); + mEmailAttachments.clear(); + + // Extract data from all the event's alarms and index the alarms by sequence number + AlarmMap alarmMap; + readAlarms(event, &alarmMap); + + // Incorporate the alarms' details into the overall event + mAlarmCount = 0; // initialise as invalid + DateTime alTime; + bool set = false; + bool isEmailText = false; + bool setDeferralTime = false; + Duration deferralOffset; + for (AlarmMap::ConstIterator it = alarmMap.begin(); it != alarmMap.end(); ++it) + { + const AlarmData& data = it.data(); + DateTime dateTime = data.alarm->hasStartOffset() ? mNextMainDateTime.addSecs(data.alarm->startOffset().asSeconds()) : data.alarm->time(); + switch (data.type) + { + case KAAlarm::MAIN__ALARM: + mMainExpired = false; + alTime = dateTime; + alTime.setDateOnly(mStartDateTime.isDateOnly()); + if (data.repeatCount && data.repeatInterval) + { + mRepeatInterval = data.repeatInterval; // values may be adjusted in setRecurrence() + mRepeatCount = data.repeatCount; + mNextRepeat = data.nextRepeat; + } + break; + case KAAlarm::AT_LOGIN__ALARM: + mRepeatAtLogin = true; + mAtLoginDateTime = dateTime.rawDateTime(); + alTime = mAtLoginDateTime; + break; + case KAAlarm::REMINDER__ALARM: + mReminderMinutes = -(data.startOffsetSecs / 60); + if (mReminderMinutes) + mArchiveReminderMinutes = 0; + break; + case KAAlarm::DEFERRED_REMINDER_DATE__ALARM: + case KAAlarm::DEFERRED_DATE__ALARM: + mDeferral = (data.type == KAAlarm::DEFERRED_REMINDER_DATE__ALARM) ? REMINDER_DEFERRAL : NORMAL_DEFERRAL; + mDeferralTime = dateTime; + mDeferralTime.setDateOnly(true); + if (data.alarm->hasStartOffset()) + deferralOffset = data.alarm->startOffset(); + break; + case KAAlarm::DEFERRED_REMINDER_TIME__ALARM: + case KAAlarm::DEFERRED_TIME__ALARM: + mDeferral = (data.type == KAAlarm::DEFERRED_REMINDER_TIME__ALARM) ? REMINDER_DEFERRAL : NORMAL_DEFERRAL; + mDeferralTime = dateTime; + if (data.alarm->hasStartOffset()) + deferralOffset = data.alarm->startOffset(); + break; + case KAAlarm::DISPLAYING__ALARM: + { + mDisplaying = true; + mDisplayingFlags = data.displayingFlags; + bool dateOnly = (mDisplayingFlags & DEFERRAL) ? !(mDisplayingFlags & TIMED_FLAG) + : mStartDateTime.isDateOnly(); + mDisplayingTime = dateTime; + mDisplayingTime.setDateOnly(dateOnly); + alTime = mDisplayingTime; + break; + } + case KAAlarm::AUDIO__ALARM: + mAudioFile = data.cleanText; + mSpeak = data.speak && mAudioFile.isEmpty(); + mBeep = !mSpeak && mAudioFile.isEmpty(); + mSoundVolume = (!mBeep && !mSpeak) ? data.soundVolume : -1; + mFadeVolume = (mSoundVolume >= 0 && data.fadeSeconds > 0) ? data.fadeVolume : -1; + mFadeSeconds = (mFadeVolume >= 0) ? data.fadeSeconds : 0; + mRepeatSound = (!mBeep && !mSpeak) && (data.repeatCount < 0); + break; + case KAAlarm::PRE_ACTION__ALARM: + mPreAction = data.cleanText; + break; + case KAAlarm::POST_ACTION__ALARM: + mPostAction = data.cleanText; + break; + case KAAlarm::INVALID__ALARM: + default: + break; + } + + if (data.reminderOnceOnly) + mReminderOnceOnly = true; + bool noSetNextTime = false; + switch (data.type) + { + case KAAlarm::DEFERRED_REMINDER_DATE__ALARM: + case KAAlarm::DEFERRED_DATE__ALARM: + case KAAlarm::DEFERRED_REMINDER_TIME__ALARM: + case KAAlarm::DEFERRED_TIME__ALARM: + if (!set) + { + // The recurrence has to be evaluated before we can + // calculate the time of a deferral alarm. + setDeferralTime = true; + noSetNextTime = true; + } + // fall through to AT_LOGIN__ALARM etc. + case KAAlarm::AT_LOGIN__ALARM: + case KAAlarm::REMINDER__ALARM: + case KAAlarm::DISPLAYING__ALARM: + if (!set && !noSetNextTime) + mNextMainDateTime = alTime; + // fall through to MAIN__ALARM + case KAAlarm::MAIN__ALARM: + // Ensure that the basic fields are set up even if there is no main + // alarm in the event (if it has expired and then been deferred) + if (!set) + { + mActionType = data.action; + mText = (mActionType == T_COMMAND) ? data.cleanText.stripWhiteSpace() : data.cleanText; + switch (data.action) + { + case T_MESSAGE: + mFont = data.font; + mDefaultFont = data.defaultFont; + if (data.isEmailText) + isEmailText = true; + // fall through to T_FILE + case T_FILE: + mBgColour = data.bgColour; + mFgColour = data.fgColour; + break; + case T_COMMAND: + mCommandScript = data.commandScript; + break; + case T_EMAIL: + mEmailFromIdentity = data.emailFromId; + mEmailAddresses = data.emailAddresses; + mEmailSubject = data.emailSubject; + mEmailAttachments = data.emailAttachments; + break; + default: + break; + } + set = true; + } + if (data.action == T_FILE && mActionType == T_MESSAGE) + mActionType = T_FILE; + ++mAlarmCount; + break; + case KAAlarm::AUDIO__ALARM: + case KAAlarm::PRE_ACTION__ALARM: + case KAAlarm::POST_ACTION__ALARM: + case KAAlarm::INVALID__ALARM: + default: + break; + } + } + if (!isEmailText) + mKMailSerialNumber = 0; + if (mRepeatAtLogin) + mArchiveRepeatAtLogin = false; + + Recurrence* recur = event.recurrence(); + if (recur && recur->doesRecur()) + { + int nextRepeat = mNextRepeat; // setRecurrence() clears mNextRepeat + setRecurrence(*recur); + if (nextRepeat <= mRepeatCount) + mNextRepeat = nextRepeat; + } + else + checkRepetition(); + + if (mMainExpired && deferralOffset.asSeconds() && checkRecur() != KARecurrence::NO_RECUR) + { + // Adjust the deferral time for an expired recurrence, since the + // offset is relative to the first actual occurrence. + DateTime dt = mRecurrence->getNextDateTime(mStartDateTime.dateTime().addDays(-1)); + dt.setDateOnly(mStartDateTime.isDateOnly()); + if (mDeferralTime.isDateOnly()) + { + mDeferralTime = dt.addSecs(deferralOffset.asSeconds()); + mDeferralTime.setDateOnly(true); + } + else + mDeferralTime = deferralOffset.end(dt.dateTime()); + } + if (mDeferral) + { + if (mNextMainDateTime == mDeferralTime) + mDeferral = CANCEL_DEFERRAL; // it's a cancelled deferral + if (setDeferralTime) + mNextMainDateTime = mDeferralTime; + } + + mUpdated = false; +} + +/****************************************************************************** +* Fetch the start and next date/time for a KCal::Event. +* Reply = next main date/time. +*/ +DateTime KAEvent::readDateTime(const Event& event, bool dateOnly, DateTime& start) +{ + start.set(event.dtStart(), dateOnly); + DateTime next = start; + QString prop = event.customProperty(APPNAME, NEXT_RECUR_PROPERTY); + if (prop.length() >= 8) + { + // The next due recurrence time is specified + QDate d(prop.left(4).toInt(), prop.mid(4,2).toInt(), prop.mid(6,2).toInt()); + if (d.isValid()) + { + if (dateOnly && prop.length() == 8) + next = d; + else if (!dateOnly && prop.length() == 15 && prop[8] == QChar('T')) + { + QTime t(prop.mid(9,2).toInt(), prop.mid(11,2).toInt(), prop.mid(13,2).toInt()); + if (t.isValid()) + next = QDateTime(d, t); + } + } + } + return next; +} + +/****************************************************************************** + * Parse the alarms for a KCal::Event. + * Reply = map of alarm data, indexed by KAAlarm::Type + */ +void KAEvent::readAlarms(const Event& event, void* almap) +{ + AlarmMap* alarmMap = (AlarmMap*)almap; + Alarm::List alarms = event.alarms(); + for (Alarm::List::ConstIterator it = alarms.begin(); it != alarms.end(); ++it) + { + // Parse the next alarm's text + AlarmData data; + readAlarm(**it, data); + if (data.type != KAAlarm::INVALID__ALARM) + alarmMap->insert(data.type, data); + } +} + +/****************************************************************************** + * Parse a KCal::Alarm. + * Reply = alarm ID (sequence number) + */ +void KAEvent::readAlarm(const Alarm& alarm, AlarmData& data) +{ + // Parse the next alarm's text + data.alarm = &alarm; + data.startOffsetSecs = alarm.startOffset().asSeconds(); // can have start offset but no valid date/time (e.g. reminder in template) + data.displayingFlags = 0; + data.isEmailText = false; + data.nextRepeat = 0; + data.repeatInterval = alarm.snoozeTime(); + data.repeatCount = alarm.repeatCount(); + if (data.repeatCount) + { + bool ok; + QString property = alarm.customProperty(APPNAME, NEXT_REPEAT_PROPERTY); + int n = static_cast<int>(property.toUInt(&ok)); + if (ok) + data.nextRepeat = n; + } + switch (alarm.type()) + { + case Alarm::Procedure: + data.action = T_COMMAND; + data.cleanText = alarm.programFile(); + data.commandScript = data.cleanText.isEmpty(); // blank command indicates a script + if (!alarm.programArguments().isEmpty()) + { + if (!data.commandScript) + data.cleanText += ' '; + data.cleanText += alarm.programArguments(); + } + break; + case Alarm::Email: + data.action = T_EMAIL; + data.emailFromId = alarm.customProperty(APPNAME, EMAIL_ID_PROPERTY).toUInt(); + data.emailAddresses = alarm.mailAddresses(); + data.emailSubject = alarm.mailSubject(); + data.emailAttachments = alarm.mailAttachments(); + data.cleanText = alarm.mailText(); + break; + case Alarm::Display: + { + data.action = T_MESSAGE; + data.cleanText = AlarmText::fromCalendarText(alarm.text(), data.isEmailText); + QString property = alarm.customProperty(APPNAME, FONT_COLOUR_PROPERTY); + QStringList list = QStringList::split(QChar(';'), property, true); + data.bgColour = QColor(255, 255, 255); // white + data.fgColour = QColor(0, 0, 0); // black + int n = list.count(); + if (n > 0) + { + if (!list[0].isEmpty()) + { + QColor c(list[0]); + if (c.isValid()) + data.bgColour = c; + } + if (n > 1 && !list[1].isEmpty()) + { + QColor c(list[1]); + if (c.isValid()) + data.fgColour = c; + } + } + data.defaultFont = (n <= 2 || list[2].isEmpty()); + if (!data.defaultFont) + data.font.fromString(list[2]); + break; + } + case Alarm::Audio: + { + data.action = T_AUDIO; + data.cleanText = alarm.audioFile(); + data.type = KAAlarm::AUDIO__ALARM; + data.soundVolume = -1; + data.fadeVolume = -1; + data.fadeSeconds = 0; + data.speak = !alarm.customProperty(APPNAME, SPEAK_PROPERTY).isNull(); + QString property = alarm.customProperty(APPNAME, VOLUME_PROPERTY); + if (!property.isEmpty()) + { + bool ok; + float fadeVolume; + int fadeSecs = 0; + QStringList list = QStringList::split(QChar(';'), property, true); + data.soundVolume = list[0].toFloat(&ok); + if (!ok) + data.soundVolume = -1; + if (data.soundVolume >= 0 && list.count() >= 3) + { + fadeVolume = list[1].toFloat(&ok); + if (ok) + fadeSecs = static_cast<int>(list[2].toUInt(&ok)); + if (ok && fadeVolume >= 0 && fadeSecs > 0) + { + data.fadeVolume = fadeVolume; + data.fadeSeconds = fadeSecs; + } + } + } + return; + } + case Alarm::Invalid: + data.type = KAAlarm::INVALID__ALARM; + return; + } + + bool atLogin = false; + bool reminder = false; + bool deferral = false; + bool dateDeferral = false; + data.reminderOnceOnly = false; + data.type = KAAlarm::MAIN__ALARM; + QString property = alarm.customProperty(APPNAME, TYPE_PROPERTY); + QStringList types = QStringList::split(QChar(','), property); + for (unsigned int i = 0; i < types.count(); ++i) + { + QString type = types[i]; + if (type == AT_LOGIN_TYPE) + atLogin = true; + else if (type == FILE_TYPE && data.action == T_MESSAGE) + data.action = T_FILE; + else if (type == REMINDER_TYPE) + reminder = true; + else if (type == REMINDER_ONCE_TYPE) + reminder = data.reminderOnceOnly = true; + else if (type == TIME_DEFERRAL_TYPE) + deferral = true; + else if (type == DATE_DEFERRAL_TYPE) + dateDeferral = deferral = true; + else if (type == DISPLAYING_TYPE) + data.type = KAAlarm::DISPLAYING__ALARM; + else if (type == PRE_ACTION_TYPE && data.action == T_COMMAND) + data.type = KAAlarm::PRE_ACTION__ALARM; + else if (type == POST_ACTION_TYPE && data.action == T_COMMAND) + data.type = KAAlarm::POST_ACTION__ALARM; + } + + if (reminder) + { + if (data.type == KAAlarm::MAIN__ALARM) + data.type = dateDeferral ? KAAlarm::DEFERRED_REMINDER_DATE__ALARM + : deferral ? KAAlarm::DEFERRED_REMINDER_TIME__ALARM : KAAlarm::REMINDER__ALARM; + else if (data.type == KAAlarm::DISPLAYING__ALARM) + data.displayingFlags = dateDeferral ? REMINDER | DATE_DEFERRAL + : deferral ? REMINDER | TIME_DEFERRAL : REMINDER; + } + else if (deferral) + { + if (data.type == KAAlarm::MAIN__ALARM) + data.type = dateDeferral ? KAAlarm::DEFERRED_DATE__ALARM : KAAlarm::DEFERRED_TIME__ALARM; + else if (data.type == KAAlarm::DISPLAYING__ALARM) + data.displayingFlags = dateDeferral ? DATE_DEFERRAL : TIME_DEFERRAL; + } + if (atLogin) + { + if (data.type == KAAlarm::MAIN__ALARM) + data.type = KAAlarm::AT_LOGIN__ALARM; + else if (data.type == KAAlarm::DISPLAYING__ALARM) + data.displayingFlags = REPEAT_AT_LOGIN; + } +//kdDebug(5950)<<"ReadAlarm(): text="<<alarm.text()<<", time="<<alarm.time().toString()<<", valid time="<<alarm.time().isValid()<<endl; +} + +/****************************************************************************** + * Initialise the KAEvent with the specified parameters. + */ +void KAEvent::set(const QDateTime& dateTime, const QString& text, const QColor& bg, const QColor& fg, + const QFont& font, Action action, int lateCancel, int flags) +{ + clearRecur(); + mStartDateTime.set(dateTime, flags & ANY_TIME); + mNextMainDateTime = mStartDateTime; + switch (action) + { + case MESSAGE: + case FILE: + case COMMAND: + case EMAIL: + mActionType = (KAAlarmEventBase::Type)action; + break; + default: + mActionType = T_MESSAGE; + break; + } + mText = (mActionType == T_COMMAND) ? text.stripWhiteSpace() : text; + mEventID = QString::null; + mTemplateName = QString::null; + mPreAction = QString::null; + mPostAction = QString::null; + mAudioFile = ""; + mSoundVolume = -1; + mFadeVolume = -1; + mTemplateAfterTime = -1; + mFadeSeconds = 0; + mBgColour = bg; + mFgColour = fg; + mFont = font; + mAlarmCount = 1; + mLateCancel = lateCancel; // do this before setting flags + mDeferral = NO_DEFERRAL; // do this before setting flags + + KAAlarmEventBase::set(flags & ~READ_ONLY_FLAGS); + mStartDateTime.setDateOnly(flags & ANY_TIME); + set_deferral((flags & DEFERRAL) ? NORMAL_DEFERRAL : NO_DEFERRAL); + mCommandXterm = flags & EXEC_IN_XTERM; + mCopyToKOrganizer = flags & COPY_KORGANIZER; + mEnabled = !(flags & DISABLED); + + mKMailSerialNumber = 0; + mReminderMinutes = 0; + mArchiveReminderMinutes = 0; + mDeferDefaultMinutes = 0; + mArchiveRepeatAtLogin = false; + mReminderOnceOnly = false; + mDisplaying = false; + mMainExpired = false; + mArchive = false; + mUpdated = false; +} + +void KAEvent::setLogFile(const QString& logfile) +{ + mLogFile = logfile; + if (!logfile.isEmpty()) + mCommandXterm = false; +} + +void KAEvent::setEmail(uint from, const EmailAddressList& addresses, const QString& subject, const QStringList& attachments) +{ + mEmailFromIdentity = from; + mEmailAddresses = addresses; + mEmailSubject = subject; + mEmailAttachments = attachments; +} + +void KAEvent::setAudioFile(const QString& filename, float volume, float fadeVolume, int fadeSeconds) +{ + mAudioFile = filename; + mSoundVolume = filename.isEmpty() ? -1 : volume; + if (mSoundVolume >= 0) + { + mFadeVolume = (fadeSeconds > 0) ? fadeVolume : -1; + mFadeSeconds = (mFadeVolume >= 0) ? fadeSeconds : 0; + } + else + { + mFadeVolume = -1; + mFadeSeconds = 0; + } + mUpdated = true; +} + +void KAEvent::setReminder(int minutes, bool onceOnly) +{ + if (minutes != mReminderMinutes) + { + set_reminder(minutes); + mReminderOnceOnly = onceOnly; + mUpdated = true; + } +} + +/****************************************************************************** + * Return the time of the next scheduled occurrence of the event. + * Reminders and deferred reminders can optionally be ignored. + */ +DateTime KAEvent::displayDateTime() const +{ + DateTime dt = mainDateTime(true); + if (mDeferral > 0 && mDeferral != REMINDER_DEFERRAL) + { + if (mMainExpired) + return mDeferralTime; + return QMIN(mDeferralTime, dt); + } + return dt; +} + +/****************************************************************************** + * Convert a unique ID to indicate that the event is in a specified calendar file. + */ +QString KAEvent::uid(const QString& id, Status status) +{ + QString result = id; + Status oldStatus; + int i, len; + if ((i = result.find(EXPIRED_UID)) > 0) + { + oldStatus = EXPIRED; + len = EXPIRED_UID.length(); + } + else if ((i = result.find(DISPLAYING_UID)) > 0) + { + oldStatus = DISPLAYING; + len = DISPLAYING_UID.length(); + } + else if ((i = result.find(TEMPLATE_UID)) > 0) + { + oldStatus = TEMPLATE; + len = TEMPLATE_UID.length(); + } + else if ((i = result.find(KORGANIZER_UID)) > 0) + { + oldStatus = KORGANIZER; + len = KORGANIZER_UID.length(); + } + else + { + oldStatus = ACTIVE; + i = result.findRev('-'); + len = 1; + } + if (status != oldStatus && i > 0) + { + QString part; + switch (status) + { + case ACTIVE: part = "-"; break; + case EXPIRED: part = EXPIRED_UID; break; + case DISPLAYING: part = DISPLAYING_UID; break; + case TEMPLATE: part = TEMPLATE_UID; break; + case KORGANIZER: part = KORGANIZER_UID; break; + } + result.replace(i, len, part); + } + return result; +} + +/****************************************************************************** + * Get the calendar type for a unique ID. + */ +KAEvent::Status KAEvent::uidStatus(const QString& uid) +{ + if (uid.find(EXPIRED_UID) > 0) + return EXPIRED; + if (uid.find(DISPLAYING_UID) > 0) + return DISPLAYING; + if (uid.find(TEMPLATE_UID) > 0) + return TEMPLATE; + if (uid.find(KORGANIZER_UID) > 0) + return KORGANIZER; + return ACTIVE; +} + +int KAEvent::flags() const +{ + return KAAlarmEventBase::flags() + | (mStartDateTime.isDateOnly() ? ANY_TIME : 0) + | (mDeferral > 0 ? DEFERRAL : 0) + | (mCommandXterm ? EXEC_IN_XTERM : 0) + | (mCopyToKOrganizer ? COPY_KORGANIZER : 0) + | (mEnabled ? 0 : DISABLED); +} + +/****************************************************************************** + * Create a new Event from the KAEvent data. + */ +Event* KAEvent::event() const +{ + KCal::Event* ev = new KCal::Event; + ev->setUid(mEventID); + updateKCalEvent(*ev, false); + return ev; +} + +/****************************************************************************** + * Update an existing KCal::Event with the KAEvent data. + * If 'original' is true, the event start date/time is adjusted to its original + * value instead of its next occurrence, and the expired main alarm is + * reinstated. + */ +bool KAEvent::updateKCalEvent(Event& ev, bool checkUid, bool original, bool cancelCancelledDefer) const +{ + if (checkUid && !mEventID.isEmpty() && mEventID != ev.uid() + || !mAlarmCount && (!original || !mMainExpired)) + return false; + + checkRecur(); // ensure recurrence/repetition data is consistent + bool readOnly = ev.isReadOnly(); + ev.setReadOnly(false); + ev.setTransparency(Event::Transparent); + + // Set up event-specific data + + // Set up custom properties. + ev.removeCustomProperty(APPNAME, NEXT_RECUR_PROPERTY); + ev.removeCustomProperty(APPNAME, REPEAT_PROPERTY); + + QStringList cats; + if (mStartDateTime.isDateOnly()) + cats.append(DATE_ONLY_CATEGORY); + if (mConfirmAck) + cats.append(CONFIRM_ACK_CATEGORY); + if (mEmailBcc) + cats.append(EMAIL_BCC_CATEGORY); + if (mKMailSerialNumber) + cats.append(QString("%1%2").arg(KMAIL_SERNUM_CATEGORY).arg(mKMailSerialNumber)); + if (mCopyToKOrganizer) + cats.append(KORGANIZER_CATEGORY); + if (mCommandXterm) + cats.append(LOG_CATEGORY + xtermURL); + else if (!mLogFile.isEmpty()) + cats.append(LOG_CATEGORY + mLogFile); + if (mLateCancel) + cats.append(QString("%1%2").arg(mAutoClose ? AUTO_CLOSE_CATEGORY : LATE_CANCEL_CATEGORY).arg(mLateCancel)); + if (mDeferDefaultMinutes) + cats.append(QString("%1%2").arg(DEFER_CATEGORY).arg(mDeferDefaultMinutes)); + if (!mTemplateName.isEmpty() && mTemplateAfterTime >= 0) + cats.append(QString("%1%2").arg(TEMPL_AFTER_TIME_CATEGORY).arg(mTemplateAfterTime)); + if (mArchive && !original) + { + QStringList params; + if (mArchiveReminderMinutes) + { + if (mReminderOnceOnly) + params += ARCHIVE_REMINDER_ONCE_TYPE; + char unit = 'M'; + int count = mArchiveReminderMinutes; + if (count % 1440 == 0) + { + unit = 'D'; + count /= 1440; + } + else if (count % 60 == 0) + { + unit = 'H'; + count /= 60; + } + params += QString("%1%2").arg(count).arg(unit); + } + if (mArchiveRepeatAtLogin) + params += AT_LOGIN_TYPE; + if (params.count() > 0) + { + QString cat = ARCHIVE_CATEGORIES; + cat += params.join(QString::fromLatin1(";")); + cats.append(cat); + } + else + cats.append(ARCHIVE_CATEGORY); + } + ev.setCategories(cats); + ev.setCustomStatus(mEnabled ? QString::null : DISABLED_STATUS); + ev.setRevision(mRevision); + ev.clearAlarms(); + + // Always set DTSTART as date/time, since alarm times can only be specified + // in local time (instead of UTC) if they are relative to a DTSTART or DTEND + // which is also specified in local time. Instead of calling setFloats() to + // indicate a date-only event, the category "DATE" is included. + ev.setDtStart(mStartDateTime.dateTime()); + ev.setFloats(false); + ev.setHasEndDate(false); + + DateTime dtMain = original ? mStartDateTime : mNextMainDateTime; + int ancillaryType = 0; // 0 = invalid, 1 = time, 2 = offset + DateTime ancillaryTime; // time for ancillary alarms (audio, pre-action, etc) + int ancillaryOffset = 0; // start offset for ancillary alarms + if (!mMainExpired || original) + { + /* The alarm offset must always be zero for the main alarm. To determine + * which recurrence is due, the property X-KDE-KALARM_NEXTRECUR is used. + * If the alarm offset was non-zero, exception dates and rules would not + * work since they apply to the event time, not the alarm time. + */ + if (!original && checkRecur() != KARecurrence::NO_RECUR) + { + QDateTime dt = mNextMainDateTime.dateTime(); + ev.setCustomProperty(APPNAME, NEXT_RECUR_PROPERTY, + dt.toString(mNextMainDateTime.isDateOnly() ? "yyyyMMdd" : "yyyyMMddThhmmss")); + } + // Add the main alarm + initKCalAlarm(ev, 0, QStringList(), KAAlarm::MAIN_ALARM); + ancillaryOffset = 0; + ancillaryType = dtMain.isValid() ? 2 : 0; + } + else if (mRepeatCount && mRepeatInterval) + { + // Alarm repetition is normally held in the main alarm, but since + // the main alarm has expired, store in a custom property. + QString param = QString("%1:%2").arg(mRepeatInterval).arg(mRepeatCount); + ev.setCustomProperty(APPNAME, REPEAT_PROPERTY, param); + } + + // Add subsidiary alarms + if (mRepeatAtLogin || mArchiveRepeatAtLogin && original) + { + DateTime dtl; + if (mArchiveRepeatAtLogin) + dtl = mStartDateTime.dateTime().addDays(-1); + else if (mAtLoginDateTime.isValid()) + dtl = mAtLoginDateTime; + else if (mStartDateTime.isDateOnly()) + dtl = QDate::currentDate().addDays(-1); + else + dtl = QDateTime::currentDateTime(); + initKCalAlarm(ev, dtl, AT_LOGIN_TYPE); + if (!ancillaryType && dtl.isValid()) + { + ancillaryTime = dtl; + ancillaryType = 1; + } + } + if (mReminderMinutes || mArchiveReminderMinutes && original) + { + int minutes = mReminderMinutes ? mReminderMinutes : mArchiveReminderMinutes; + initKCalAlarm(ev, -minutes * 60, QStringList(mReminderOnceOnly ? REMINDER_ONCE_TYPE : REMINDER_TYPE)); + if (!ancillaryType) + { + ancillaryOffset = -minutes * 60; + ancillaryType = 2; + } + } + if (mDeferral > 0 || mDeferral == CANCEL_DEFERRAL && !cancelCancelledDefer) + { + DateTime nextDateTime = mNextMainDateTime; + if (mMainExpired) + { + if (checkRecur() == KARecurrence::NO_RECUR) + nextDateTime = mStartDateTime; + else if (!original) + { + // It's a deferral of an expired recurrence. + // Need to ensure that the alarm offset is to an occurrence + // which isn't excluded by an exception - otherwise, it will + // never be triggered. So choose the first recurrence which + // isn't an exception. + nextDateTime = mRecurrence->getNextDateTime(mStartDateTime.dateTime().addDays(-1)); + nextDateTime.setDateOnly(mStartDateTime.isDateOnly()); + } + } + int startOffset; + QStringList list; + if (mDeferralTime.isDateOnly()) + { + startOffset = nextDateTime.secsTo(mDeferralTime.dateTime()); + list += DATE_DEFERRAL_TYPE; + } + else + { + startOffset = nextDateTime.dateTime().secsTo(mDeferralTime.dateTime()); + list += TIME_DEFERRAL_TYPE; + } + if (mDeferral == REMINDER_DEFERRAL) + list += mReminderOnceOnly ? REMINDER_ONCE_TYPE : REMINDER_TYPE; + initKCalAlarm(ev, startOffset, list); + if (!ancillaryType && mDeferralTime.isValid()) + { + ancillaryOffset = startOffset; + ancillaryType = 2; + } + } + if (!mTemplateName.isEmpty()) + ev.setSummary(mTemplateName); + else if (mDisplaying) + { + QStringList list(DISPLAYING_TYPE); + if (mDisplayingFlags & REPEAT_AT_LOGIN) + list += AT_LOGIN_TYPE; + else if (mDisplayingFlags & DEFERRAL) + { + if (mDisplayingFlags & TIMED_FLAG) + list += TIME_DEFERRAL_TYPE; + else + list += DATE_DEFERRAL_TYPE; + } + if (mDisplayingFlags & REMINDER) + list += mReminderOnceOnly ? REMINDER_ONCE_TYPE : REMINDER_TYPE; + initKCalAlarm(ev, mDisplayingTime, list); + if (!ancillaryType && mDisplayingTime.isValid()) + { + ancillaryTime = mDisplayingTime; + ancillaryType = 1; + } + } + if (mBeep || mSpeak || !mAudioFile.isEmpty()) + { + // A sound is specified + if (ancillaryType == 2) + initKCalAlarm(ev, ancillaryOffset, QStringList(), KAAlarm::AUDIO_ALARM); + else + initKCalAlarm(ev, ancillaryTime, QStringList(), KAAlarm::AUDIO_ALARM); + } + if (!mPreAction.isEmpty()) + { + // A pre-display action is specified + if (ancillaryType == 2) + initKCalAlarm(ev, ancillaryOffset, QStringList(PRE_ACTION_TYPE), KAAlarm::PRE_ACTION_ALARM); + else + initKCalAlarm(ev, ancillaryTime, QStringList(PRE_ACTION_TYPE), KAAlarm::PRE_ACTION_ALARM); + } + if (!mPostAction.isEmpty()) + { + // A post-display action is specified + if (ancillaryType == 2) + initKCalAlarm(ev, ancillaryOffset, QStringList(POST_ACTION_TYPE), KAAlarm::POST_ACTION_ALARM); + else + initKCalAlarm(ev, ancillaryTime, QStringList(POST_ACTION_TYPE), KAAlarm::POST_ACTION_ALARM); + } + + if (mRecurrence) + mRecurrence->writeRecurrence(*ev.recurrence()); + else + ev.clearRecurrence(); + if (mSaveDateTime.isValid()) + ev.setCreated(mSaveDateTime); + ev.setReadOnly(readOnly); + return true; +} + +/****************************************************************************** + * Create a new alarm for a libkcal event, and initialise it according to the + * alarm action. If 'types' is non-null, it is appended to the X-KDE-KALARM-TYPE + * property value list. + */ +Alarm* KAEvent::initKCalAlarm(Event& event, const DateTime& dt, const QStringList& types, KAAlarm::Type type) const +{ + int startOffset = dt.isDateOnly() ? mStartDateTime.secsTo(dt) + : mStartDateTime.dateTime().secsTo(dt.dateTime()); + return initKCalAlarm(event, startOffset, types, type); +} + +Alarm* KAEvent::initKCalAlarm(Event& event, int startOffsetSecs, const QStringList& types, KAAlarm::Type type) const +{ + QStringList alltypes; + Alarm* alarm = event.newAlarm(); + alarm->setEnabled(true); + if (type != KAAlarm::MAIN_ALARM) + { + // RFC2445 specifies that absolute alarm times must be stored as UTC. + // So, in order to store local times, set the alarm time as an offset to DTSTART. + alarm->setStartOffset(startOffsetSecs); + } + + switch (type) + { + case KAAlarm::AUDIO_ALARM: + alarm->setAudioAlarm(mAudioFile); // empty for a beep or for speaking + if (mSpeak) + alarm->setCustomProperty(APPNAME, SPEAK_PROPERTY, QString::fromLatin1("Y")); + if (mRepeatSound) + { + alarm->setRepeatCount(-1); + alarm->setSnoozeTime(0); + } + if (!mAudioFile.isEmpty() && mSoundVolume >= 0) + alarm->setCustomProperty(APPNAME, VOLUME_PROPERTY, + QString::fromLatin1("%1;%2;%3").arg(QString::number(mSoundVolume, 'f', 2)) + .arg(QString::number(mFadeVolume, 'f', 2)) + .arg(mFadeSeconds)); + break; + case KAAlarm::PRE_ACTION_ALARM: + setProcedureAlarm(alarm, mPreAction); + break; + case KAAlarm::POST_ACTION_ALARM: + setProcedureAlarm(alarm, mPostAction); + break; + case KAAlarm::MAIN_ALARM: + alarm->setSnoozeTime(mRepeatInterval); + alarm->setRepeatCount(mRepeatCount); + if (mRepeatCount) + alarm->setCustomProperty(APPNAME, NEXT_REPEAT_PROPERTY, + QString::number(mNextRepeat)); + // fall through to INVALID_ALARM + case KAAlarm::INVALID_ALARM: + switch (mActionType) + { + case T_FILE: + alltypes += FILE_TYPE; + // fall through to T_MESSAGE + case T_MESSAGE: + alarm->setDisplayAlarm(AlarmText::toCalendarText(mText)); + alarm->setCustomProperty(APPNAME, FONT_COLOUR_PROPERTY, + QString::fromLatin1("%1;%2;%3").arg(mBgColour.name()) + .arg(mFgColour.name()) + .arg(mDefaultFont ? QString::null : mFont.toString())); + break; + case T_COMMAND: + if (mCommandScript) + alarm->setProcedureAlarm("", mText); + else + setProcedureAlarm(alarm, mText); + break; + case T_EMAIL: + alarm->setEmailAlarm(mEmailSubject, mText, mEmailAddresses, mEmailAttachments); + if (mEmailFromIdentity) + alarm->setCustomProperty(APPNAME, EMAIL_ID_PROPERTY, QString::number(mEmailFromIdentity)); + break; + case T_AUDIO: + break; + } + break; + case KAAlarm::REMINDER_ALARM: + case KAAlarm::DEFERRED_ALARM: + case KAAlarm::DEFERRED_REMINDER_ALARM: + case KAAlarm::AT_LOGIN_ALARM: + case KAAlarm::DISPLAYING_ALARM: + break; + } + alltypes += types; + if (alltypes.count() > 0) + alarm->setCustomProperty(APPNAME, TYPE_PROPERTY, alltypes.join(",")); + return alarm; +} + +/****************************************************************************** + * Return the alarm of the specified type. + */ +KAAlarm KAEvent::alarm(KAAlarm::Type type) const +{ + checkRecur(); // ensure recurrence/repetition data is consistent + KAAlarm al; // this sets type to INVALID_ALARM + if (mAlarmCount) + { + al.mEventID = mEventID; + al.mActionType = mActionType; + al.mText = mText; + al.mBgColour = mBgColour; + al.mFgColour = mFgColour; + al.mFont = mFont; + al.mDefaultFont = mDefaultFont; + al.mBeep = mBeep; + al.mSpeak = mSpeak; + al.mSoundVolume = mSoundVolume; + al.mFadeVolume = mFadeVolume; + al.mFadeSeconds = mFadeSeconds; + al.mRepeatSound = mRepeatSound; + al.mConfirmAck = mConfirmAck; + al.mRepeatCount = 0; + al.mRepeatInterval = 0; + al.mRepeatAtLogin = false; + al.mDeferred = false; + al.mLateCancel = mLateCancel; + al.mAutoClose = mAutoClose; + al.mEmailBcc = mEmailBcc; + al.mCommandScript = mCommandScript; + if (mActionType == T_EMAIL) + { + al.mEmailFromIdentity = mEmailFromIdentity; + al.mEmailAddresses = mEmailAddresses; + al.mEmailSubject = mEmailSubject; + al.mEmailAttachments = mEmailAttachments; + } + switch (type) + { + case KAAlarm::MAIN_ALARM: + if (!mMainExpired) + { + al.mType = KAAlarm::MAIN__ALARM; + al.mNextMainDateTime = mNextMainDateTime; + al.mRepeatCount = mRepeatCount; + al.mRepeatInterval = mRepeatInterval; + al.mNextRepeat = mNextRepeat; + } + break; + case KAAlarm::REMINDER_ALARM: + if (mReminderMinutes) + { + al.mType = KAAlarm::REMINDER__ALARM; + if (mReminderOnceOnly) + al.mNextMainDateTime = mStartDateTime.addMins(-mReminderMinutes); + else + al.mNextMainDateTime = mNextMainDateTime.addMins(-mReminderMinutes); + } + break; + case KAAlarm::DEFERRED_REMINDER_ALARM: + if (mDeferral != REMINDER_DEFERRAL) + break; + // fall through to DEFERRED_ALARM + case KAAlarm::DEFERRED_ALARM: + if (mDeferral > 0) + { + al.mType = static_cast<KAAlarm::SubType>((mDeferral == REMINDER_DEFERRAL ? KAAlarm::DEFERRED_REMINDER_ALARM : KAAlarm::DEFERRED_ALARM) + | (mDeferralTime.isDateOnly() ? 0 : KAAlarm::TIMED_DEFERRAL_FLAG)); + al.mNextMainDateTime = mDeferralTime; + al.mDeferred = true; + } + break; + case KAAlarm::AT_LOGIN_ALARM: + if (mRepeatAtLogin) + { + al.mType = KAAlarm::AT_LOGIN__ALARM; + al.mNextMainDateTime = mAtLoginDateTime; + al.mRepeatAtLogin = true; + al.mLateCancel = 0; + al.mAutoClose = false; + } + break; + case KAAlarm::DISPLAYING_ALARM: + if (mDisplaying) + { + al.mType = KAAlarm::DISPLAYING__ALARM; + al.mNextMainDateTime = mDisplayingTime; + al.mDisplaying = true; + } + break; + case KAAlarm::AUDIO_ALARM: + case KAAlarm::PRE_ACTION_ALARM: + case KAAlarm::POST_ACTION_ALARM: + case KAAlarm::INVALID_ALARM: + default: + break; + } + } + return al; +} + +/****************************************************************************** + * Return the main alarm for the event. + * If the main alarm does not exist, one of the subsidiary ones is returned if + * possible. + * N.B. a repeat-at-login alarm can only be returned if it has been read from/ + * written to the calendar file. + */ +KAAlarm KAEvent::firstAlarm() const +{ + if (mAlarmCount) + { + if (!mMainExpired) + return alarm(KAAlarm::MAIN_ALARM); + return nextAlarm(KAAlarm::MAIN_ALARM); + } + return KAAlarm(); +} + +/****************************************************************************** + * Return the next alarm for the event, after the specified alarm. + * N.B. a repeat-at-login alarm can only be returned if it has been read from/ + * written to the calendar file. + */ +KAAlarm KAEvent::nextAlarm(KAAlarm::Type prevType) const +{ + switch (prevType) + { + case KAAlarm::MAIN_ALARM: + if (mReminderMinutes) + return alarm(KAAlarm::REMINDER_ALARM); + // fall through to REMINDER_ALARM + case KAAlarm::REMINDER_ALARM: + // There can only be one deferral alarm + if (mDeferral == REMINDER_DEFERRAL) + return alarm(KAAlarm::DEFERRED_REMINDER_ALARM); + if (mDeferral == NORMAL_DEFERRAL) + return alarm(KAAlarm::DEFERRED_ALARM); + // fall through to DEFERRED_ALARM + case KAAlarm::DEFERRED_REMINDER_ALARM: + case KAAlarm::DEFERRED_ALARM: + if (mRepeatAtLogin) + return alarm(KAAlarm::AT_LOGIN_ALARM); + // fall through to AT_LOGIN_ALARM + case KAAlarm::AT_LOGIN_ALARM: + if (mDisplaying) + return alarm(KAAlarm::DISPLAYING_ALARM); + // fall through to DISPLAYING_ALARM + case KAAlarm::DISPLAYING_ALARM: + // fall through to default + case KAAlarm::AUDIO_ALARM: + case KAAlarm::PRE_ACTION_ALARM: + case KAAlarm::POST_ACTION_ALARM: + case KAAlarm::INVALID_ALARM: + default: + break; + } + return KAAlarm(); +} + +/****************************************************************************** + * Remove the alarm of the specified type from the event. + * This must only be called to remove an alarm which has expired, not to + * reconfigure the event. + */ +void KAEvent::removeExpiredAlarm(KAAlarm::Type type) +{ + int count = mAlarmCount; + switch (type) + { + case KAAlarm::MAIN_ALARM: + mAlarmCount = 0; // removing main alarm - also remove subsidiary alarms + break; + case KAAlarm::AT_LOGIN_ALARM: + if (mRepeatAtLogin) + { + // Remove the at-login alarm, but keep a note of it for archiving purposes + mArchiveRepeatAtLogin = true; + mRepeatAtLogin = false; + --mAlarmCount; + } + break; + case KAAlarm::REMINDER_ALARM: + // Remove any reminder alarm, but keep a note of it for archiving purposes + set_archiveReminder(); + break; + case KAAlarm::DEFERRED_REMINDER_ALARM: + case KAAlarm::DEFERRED_ALARM: + set_deferral(NO_DEFERRAL); + break; + case KAAlarm::DISPLAYING_ALARM: + if (mDisplaying) + { + mDisplaying = false; + --mAlarmCount; + } + break; + case KAAlarm::AUDIO_ALARM: + case KAAlarm::PRE_ACTION_ALARM: + case KAAlarm::POST_ACTION_ALARM: + case KAAlarm::INVALID_ALARM: + default: + break; + } + if (mAlarmCount != count) + mUpdated = true; +} + +/****************************************************************************** + * Defer the event to the specified time. + * If the main alarm time has passed, the main alarm is marked as expired. + * If 'adjustRecurrence' is true, ensure that the next scheduled recurrence is + * after the current time. + * Reply = true if a repetition has been deferred. + */ +bool KAEvent::defer(const DateTime& dateTime, bool reminder, bool adjustRecurrence) +{ + bool result = false; + bool setNextRepetition = false; + bool checkRepetition = false; + cancelCancelledDeferral(); + if (checkRecur() == KARecurrence::NO_RECUR) + { + if (mReminderMinutes || mDeferral == REMINDER_DEFERRAL || mArchiveReminderMinutes) + { + if (dateTime < mNextMainDateTime.dateTime()) + { + set_deferral(REMINDER_DEFERRAL); // defer reminder alarm + mDeferralTime = dateTime; + } + else + { + // Deferring past the main alarm time, so adjust any existing deferral + if (mReminderMinutes || mDeferral == REMINDER_DEFERRAL) + set_deferral(NO_DEFERRAL); + } + // Remove any reminder alarm, but keep a note of it for archiving purposes + if (mReminderMinutes) + set_archiveReminder(); + } + if (mDeferral != REMINDER_DEFERRAL) + { + // We're deferring the main alarm, not a reminder + if (mRepeatCount && mRepeatInterval && dateTime < mainEndRepeatTime()) + { + // The alarm is repeated, and we're deferring to a time before the last repetition + set_deferral(NORMAL_DEFERRAL); + mDeferralTime = dateTime; + result = true; + setNextRepetition = true; + } + else + { + // Main alarm has now expired + mNextMainDateTime = mDeferralTime = dateTime; + set_deferral(NORMAL_DEFERRAL); + if (!mMainExpired) + { + // Mark the alarm as expired now + mMainExpired = true; + --mAlarmCount; + if (mRepeatAtLogin) + { + // Remove the repeat-at-login alarm, but keep a note of it for archiving purposes + mArchiveRepeatAtLogin = true; + mRepeatAtLogin = false; + --mAlarmCount; + } + } + } + } + } + else if (reminder) + { + // Deferring a reminder for a recurring alarm + if (dateTime >= mNextMainDateTime.dateTime()) + set_deferral(NO_DEFERRAL); // (error) + else + { + set_deferral(REMINDER_DEFERRAL); + mDeferralTime = dateTime; + checkRepetition = true; + } + } + else + { + mDeferralTime = dateTime; + if (mDeferral <= 0) + set_deferral(NORMAL_DEFERRAL); + if (adjustRecurrence) + { + QDateTime now = QDateTime::currentDateTime(); + if (mainEndRepeatTime() < now) + { + // The last repetition (if any) of the current recurrence has already passed. + // Adjust to the next scheduled recurrence after now. + if (!mMainExpired && setNextOccurrence(now) == NO_OCCURRENCE) + { + mMainExpired = true; + --mAlarmCount; + } + } + else + setNextRepetition = (mRepeatCount && mRepeatInterval); + } + else + checkRepetition = true; + } + if (checkRepetition) + setNextRepetition = (mRepeatCount && mRepeatInterval && mDeferralTime < mainEndRepeatTime()); + if (setNextRepetition) + { + // The alarm is repeated, and we're deferring to a time before the last repetition. + // Set the next scheduled repetition to the one after the deferral. + mNextRepeat = (mNextMainDateTime < mDeferralTime) + ? mNextMainDateTime.secsTo(mDeferralTime) / (mRepeatInterval * 60) + 1 : 0; + } + mUpdated = true; + return result; +} + +/****************************************************************************** + * Cancel any deferral alarm. + */ +void KAEvent::cancelDefer() +{ + if (mDeferral > 0) + { + // Set the deferral time to be the same as the next recurrence/repetition. + // This prevents an immediate retriggering of the alarm. + if (mMainExpired + || nextOccurrence(QDateTime::currentDateTime(), mDeferralTime, RETURN_REPETITION) == NO_OCCURRENCE) + { + // The main alarm has expired, so simply delete the deferral + mDeferralTime = DateTime(); + set_deferral(NO_DEFERRAL); + } + else + set_deferral(CANCEL_DEFERRAL); + mUpdated = true; + } +} + +/****************************************************************************** + * Cancel any cancelled deferral alarm. + */ +void KAEvent::cancelCancelledDeferral() +{ + if (mDeferral == CANCEL_DEFERRAL) + { + mDeferralTime = DateTime(); + set_deferral(NO_DEFERRAL); + } +} + +/****************************************************************************** +* Find the latest time which the alarm can currently be deferred to. +*/ +DateTime KAEvent::deferralLimit(KAEvent::DeferLimitType* limitType) const +{ + DeferLimitType ltype; + DateTime endTime; + bool recurs = (checkRecur() != KARecurrence::NO_RECUR); + if (recurs || mRepeatCount) + { + // It's a repeated alarm. Don't allow it to be deferred past its + // next occurrence or repetition. + DateTime reminderTime; + QDateTime now = QDateTime::currentDateTime(); + OccurType type = nextOccurrence(now, endTime, RETURN_REPETITION); + if (type & OCCURRENCE_REPEAT) + ltype = LIMIT_REPETITION; + else if (type == NO_OCCURRENCE) + ltype = LIMIT_NONE; + else if (mReminderMinutes && (now < (reminderTime = endTime.addMins(-mReminderMinutes)))) + { + endTime = reminderTime; + ltype = LIMIT_REMINDER; + } + else if (type == FIRST_OR_ONLY_OCCURRENCE && !recurs) + ltype = LIMIT_REPETITION; + else + ltype = LIMIT_RECURRENCE; + } + else if ((mReminderMinutes || mDeferral == REMINDER_DEFERRAL || mArchiveReminderMinutes) + && QDateTime::currentDateTime() < mNextMainDateTime.dateTime()) + { + // It's an reminder alarm. Don't allow it to be deferred past its main alarm time. + endTime = mNextMainDateTime; + ltype = LIMIT_REMINDER; + } + else + ltype = LIMIT_NONE; + if (ltype != LIMIT_NONE) + endTime = endTime.addMins(-1); + if (limitType) + *limitType = ltype; + return endTime; +} + +/****************************************************************************** + * Set the event to be a copy of the specified event, making the specified + * alarm the 'displaying' alarm. + * The purpose of setting up a 'displaying' alarm is to be able to reinstate + * the alarm message in case of a crash, or to reinstate it should the user + * choose to defer the alarm. Note that even repeat-at-login alarms need to be + * saved in case their end time expires before the next login. + * Reply = true if successful, false if alarm was not copied. + */ +bool KAEvent::setDisplaying(const KAEvent& event, KAAlarm::Type alarmType, const QDateTime& repeatAtLoginTime) +{ + if (!mDisplaying + && (alarmType == KAAlarm::MAIN_ALARM + || alarmType == KAAlarm::REMINDER_ALARM + || alarmType == KAAlarm::DEFERRED_REMINDER_ALARM + || alarmType == KAAlarm::DEFERRED_ALARM + || alarmType == KAAlarm::AT_LOGIN_ALARM)) + { +//kdDebug(5950)<<"KAEvent::setDisplaying("<<event.id()<<", "<<(alarmType==KAAlarm::MAIN_ALARM?"MAIN":alarmType==KAAlarm::REMINDER_ALARM?"REMINDER":alarmType==KAAlarm::DEFERRED_REMINDER_ALARM?"REMINDER_DEFERRAL":alarmType==KAAlarm::DEFERRED_ALARM?"DEFERRAL":"LOGIN")<<"): time="<<repeatAtLoginTime.toString()<<endl; + KAAlarm al = event.alarm(alarmType); + if (al.valid()) + { + *this = event; + setUid(DISPLAYING); + mDisplaying = true; + mDisplayingTime = (alarmType == KAAlarm::AT_LOGIN_ALARM) ? repeatAtLoginTime : al.dateTime(); + switch (al.type()) + { + case KAAlarm::AT_LOGIN__ALARM: mDisplayingFlags = REPEAT_AT_LOGIN; break; + case KAAlarm::REMINDER__ALARM: mDisplayingFlags = REMINDER; break; + case KAAlarm::DEFERRED_REMINDER_TIME__ALARM: mDisplayingFlags = REMINDER | TIME_DEFERRAL; break; + case KAAlarm::DEFERRED_REMINDER_DATE__ALARM: mDisplayingFlags = REMINDER | DATE_DEFERRAL; break; + case KAAlarm::DEFERRED_TIME__ALARM: mDisplayingFlags = TIME_DEFERRAL; break; + case KAAlarm::DEFERRED_DATE__ALARM: mDisplayingFlags = DATE_DEFERRAL; break; + default: mDisplayingFlags = 0; break; + } + ++mAlarmCount; + mUpdated = true; + return true; + } + } + return false; +} + +/****************************************************************************** + * Return the original alarm which the displaying alarm refers to. + */ +KAAlarm KAEvent::convertDisplayingAlarm() const +{ + KAAlarm al; + if (mDisplaying) + { + al = alarm(KAAlarm::DISPLAYING_ALARM); + if (mDisplayingFlags & REPEAT_AT_LOGIN) + { + al.mRepeatAtLogin = true; + al.mType = KAAlarm::AT_LOGIN__ALARM; + } + else if (mDisplayingFlags & DEFERRAL) + { + al.mDeferred = true; + al.mType = (mDisplayingFlags == (REMINDER | DATE_DEFERRAL)) ? KAAlarm::DEFERRED_REMINDER_DATE__ALARM + : (mDisplayingFlags == (REMINDER | TIME_DEFERRAL)) ? KAAlarm::DEFERRED_REMINDER_TIME__ALARM + : (mDisplayingFlags == DATE_DEFERRAL) ? KAAlarm::DEFERRED_DATE__ALARM + : KAAlarm::DEFERRED_TIME__ALARM; + } + else if (mDisplayingFlags & REMINDER) + al.mType = KAAlarm::REMINDER__ALARM; + else + al.mType = KAAlarm::MAIN__ALARM; + } + return al; +} + +/****************************************************************************** + * Reinstate the original event from the 'displaying' event. + */ +void KAEvent::reinstateFromDisplaying(const KAEvent& dispEvent) +{ + if (dispEvent.mDisplaying) + { + *this = dispEvent; + setUid(ACTIVE); + mDisplaying = false; + --mAlarmCount; + mUpdated = true; + } +} + +/****************************************************************************** + * Determine whether the event will occur after the specified date/time. + * If 'includeRepetitions' is true and the alarm has a sub-repetition, it + * returns true if any repetitions occur after the specified date/time. + */ +bool KAEvent::occursAfter(const QDateTime& preDateTime, bool includeRepetitions) const +{ + QDateTime dt; + if (checkRecur() != KARecurrence::NO_RECUR) + { + if (mRecurrence->duration() < 0) + return true; // infinite recurrence + dt = mRecurrence->endDateTime(); + } + else + dt = mNextMainDateTime.dateTime(); + if (mStartDateTime.isDateOnly()) + { + QDate pre = preDateTime.date(); + if (preDateTime.time() < Preferences::startOfDay()) + pre = pre.addDays(-1); // today's recurrence (if today recurs) is still to come + if (pre < dt.date()) + return true; + } + else if (preDateTime < dt) + return true; + + if (includeRepetitions && mRepeatCount) + { + if (preDateTime < dt.addSecs(mRepeatCount * mRepeatInterval * 60)) + return true; + } + return false; +} + +/****************************************************************************** + * Get the date/time of the next occurrence of the event, after the specified + * date/time. + * 'result' = date/time of next occurrence, or invalid date/time if none. + */ +KAEvent::OccurType KAEvent::nextOccurrence(const QDateTime& preDateTime, DateTime& result, + KAEvent::OccurOption includeRepetitions) const +{ + int repeatSecs = 0; + QDateTime pre = preDateTime; + if (includeRepetitions != IGNORE_REPETITION) + { + if (!mRepeatCount || !mRepeatInterval) + includeRepetitions = IGNORE_REPETITION; + else + { + repeatSecs = mRepeatInterval * 60; + pre = preDateTime.addSecs(-mRepeatCount * repeatSecs); + } + } + + OccurType type; + bool recurs = (checkRecur() != KARecurrence::NO_RECUR); + if (recurs) + type = nextRecurrence(pre, result); + else if (pre < mNextMainDateTime.dateTime()) + { + result = mNextMainDateTime; + type = FIRST_OR_ONLY_OCCURRENCE; + } + else + { + result = DateTime(); + type = NO_OCCURRENCE; + } + + if (type != NO_OCCURRENCE && result <= preDateTime && includeRepetitions != IGNORE_REPETITION) + { + // The next occurrence is a sub-repetition + int repetition = result.secsTo(preDateTime) / repeatSecs + 1; + DateTime repeatDT = result.addSecs(repetition * repeatSecs); + if (recurs) + { + // We've found a recurrence before the specified date/time, which has + // a sub-repetition after the date/time. + // However, if the intervals between recurrences vary, we could possibly + // have missed a later recurrence, which fits the criterion, so check again. + DateTime dt; + OccurType newType = previousOccurrence(repeatDT.dateTime(), dt, false); + if (dt > result) + { + type = newType; + result = dt; + if (includeRepetitions == RETURN_REPETITION && result <= preDateTime) + { + // The next occurrence is a sub-repetition + int repetition = result.secsTo(preDateTime) / repeatSecs + 1; + result = result.addSecs(repetition * repeatSecs); + type = static_cast<OccurType>(type | OCCURRENCE_REPEAT); + } + return type; + } + } + if (includeRepetitions == RETURN_REPETITION) + { + // The next occurrence is a sub-repetition + result = repeatDT; + type = static_cast<OccurType>(type | OCCURRENCE_REPEAT); + } + } + return type; +} + +/****************************************************************************** + * Get the date/time of the last previous occurrence of the event, before the + * specified date/time. + * If 'includeRepetitions' is true and the alarm has a sub-repetition, the + * last previous repetition is returned if appropriate. + * 'result' = date/time of previous occurrence, or invalid date/time if none. + */ +KAEvent::OccurType KAEvent::previousOccurrence(const QDateTime& afterDateTime, DateTime& result, bool includeRepetitions) const +{ + if (mStartDateTime >= afterDateTime) + { + result = QDateTime(); + return NO_OCCURRENCE; // the event starts after the specified date/time + } + + // Find the latest recurrence of the event + OccurType type; + if (checkRecur() == KARecurrence::NO_RECUR) + { + result = mStartDateTime; + type = FIRST_OR_ONLY_OCCURRENCE; + } + else + { + QDateTime recurStart = mRecurrence->startDateTime(); + QDateTime after = afterDateTime; + if (mStartDateTime.isDateOnly() && afterDateTime.time() > Preferences::startOfDay()) + after = after.addDays(1); // today's recurrence (if today recurs) has passed + QDateTime dt = mRecurrence->getPreviousDateTime(after); + result.set(dt, mStartDateTime.isDateOnly()); + if (!dt.isValid()) + return NO_OCCURRENCE; + if (dt == recurStart) + type = FIRST_OR_ONLY_OCCURRENCE; + else if (mRecurrence->getNextDateTime(dt).isValid()) + type = result.isDateOnly() ? RECURRENCE_DATE : RECURRENCE_DATE_TIME; + else + type = LAST_RECURRENCE; + } + + if (includeRepetitions && mRepeatCount) + { + // Find the latest repetition which is before the specified time. + // N.B. This is coded to avoid 32-bit integer overflow which occurs + // in QDateTime::secsTo() for large enough time differences. + int repeatSecs = mRepeatInterval * 60; + DateTime lastRepetition = result.addSecs(mRepeatCount * repeatSecs); + if (lastRepetition < afterDateTime) + { + result = lastRepetition; + return static_cast<OccurType>(type | OCCURRENCE_REPEAT); + } + int repetition = (result.dateTime().secsTo(afterDateTime) - 1) / repeatSecs; + if (repetition > 0) + { + result = result.addSecs(repetition * repeatSecs); + return static_cast<OccurType>(type | OCCURRENCE_REPEAT); + } + } + return type; +} + +/****************************************************************************** + * Set the date/time of the event to the next scheduled occurrence after the + * specified date/time, provided that this is later than its current date/time. + * Any reminder alarm is adjusted accordingly. + * If the alarm has a sub-repetition, and a repetition of a previous + * recurrence occurs after the specified date/time, that repetition is set as + * the next occurrence. + */ +KAEvent::OccurType KAEvent::setNextOccurrence(const QDateTime& preDateTime) +{ + if (preDateTime < mNextMainDateTime.dateTime()) + return FIRST_OR_ONLY_OCCURRENCE; // it might not be the first recurrence - tant pis + QDateTime pre = preDateTime; + // If there are repetitions, adjust the comparison date/time so that + // we find the earliest recurrence which has a repetition falling after + // the specified preDateTime. + if (mRepeatCount && mRepeatInterval) + pre = preDateTime.addSecs(-mRepeatCount * mRepeatInterval * 60); + + DateTime dt; + OccurType type; + if (pre < mNextMainDateTime.dateTime()) + { + dt = mNextMainDateTime; + type = FIRST_OR_ONLY_OCCURRENCE; // may not actually be the first occurrence + } + else if (checkRecur() != KARecurrence::NO_RECUR) + { + type = nextRecurrence(pre, dt); + if (type == NO_OCCURRENCE) + return NO_OCCURRENCE; + if (type != FIRST_OR_ONLY_OCCURRENCE && dt != mNextMainDateTime) + { + // Need to reschedule the next trigger date/time + mNextMainDateTime = dt; + // Reinstate the reminder (if any) for the rescheduled recurrence + if (mDeferral == REMINDER_DEFERRAL || mArchiveReminderMinutes) + { + if (mReminderOnceOnly) + { + if (mReminderMinutes) + set_archiveReminder(); + } + else + set_reminder(mArchiveReminderMinutes); + } + if (mDeferral == REMINDER_DEFERRAL) + set_deferral(NO_DEFERRAL); + mUpdated = true; + } + } + else + return NO_OCCURRENCE; + + if (mRepeatCount && mRepeatInterval) + { + int secs = dt.dateTime().secsTo(preDateTime); + if (secs >= 0) + { + // The next occurrence is a sub-repetition. + type = static_cast<OccurType>(type | OCCURRENCE_REPEAT); + mNextRepeat = (secs / (60 * mRepeatInterval)) + 1; + // Repetitions can't have a reminder, so remove any. + if (mReminderMinutes) + set_archiveReminder(); + if (mDeferral == REMINDER_DEFERRAL) + set_deferral(NO_DEFERRAL); + mUpdated = true; + } + else if (mNextRepeat) + { + // The next occurrence is the main occurrence, not a repetition + mNextRepeat = 0; + mUpdated = true; + } + } + return type; +} + +/****************************************************************************** + * Get the date/time of the next recurrence of the event, after the specified + * date/time. + * 'result' = date/time of next occurrence, or invalid date/time if none. + */ +KAEvent::OccurType KAEvent::nextRecurrence(const QDateTime& preDateTime, DateTime& result) const +{ + QDateTime recurStart = mRecurrence->startDateTime(); + QDateTime pre = preDateTime; + if (mStartDateTime.isDateOnly() && preDateTime.time() < Preferences::startOfDay()) + { + pre = pre.addDays(-1); // today's recurrence (if today recurs) is still to come + pre.setTime(Preferences::startOfDay()); + } + QDateTime dt = mRecurrence->getNextDateTime(pre); + result.set(dt, mStartDateTime.isDateOnly()); + if (!dt.isValid()) + return NO_OCCURRENCE; + if (dt == recurStart) + return FIRST_OR_ONLY_OCCURRENCE; + if (mRecurrence->duration() >= 0 && dt == mRecurrence->endDateTime()) + return LAST_RECURRENCE; + return result.isDateOnly() ? RECURRENCE_DATE : RECURRENCE_DATE_TIME; +} + +/****************************************************************************** + * Return the recurrence interval as text suitable for display. + */ +QString KAEvent::recurrenceText(bool brief) const +{ + if (mRepeatAtLogin) + return brief ? i18n("Brief form of 'At Login'", "Login") : i18n("At login"); + if (mRecurrence) + { + int frequency = mRecurrence->frequency(); + switch (mRecurrence->defaultRRuleConst()->recurrenceType()) + { + case RecurrenceRule::rMinutely: + if (frequency < 60) + return i18n("1 Minute", "%n Minutes", frequency); + else if (frequency % 60 == 0) + return i18n("1 Hour", "%n Hours", frequency/60); + else + { + QString mins; + return i18n("Hours and Minutes", "%1H %2M").arg(QString::number(frequency/60)).arg(mins.sprintf("%02d", frequency%60)); + } + case RecurrenceRule::rDaily: + return i18n("1 Day", "%n Days", frequency); + case RecurrenceRule::rWeekly: + return i18n("1 Week", "%n Weeks", frequency); + case RecurrenceRule::rMonthly: + return i18n("1 Month", "%n Months", frequency); + case RecurrenceRule::rYearly: + return i18n("1 Year", "%n Years", frequency); + case RecurrenceRule::rNone: + default: + break; + } + } + return brief ? QString::null : i18n("None"); +} + +/****************************************************************************** + * Return the repetition interval as text suitable for display. + */ +QString KAEvent::repetitionText(bool brief) const +{ + if (mRepeatCount) + { + if (mRepeatInterval % 1440) + { + if (mRepeatInterval < 60) + return i18n("1 Minute", "%n Minutes", mRepeatInterval); + if (mRepeatInterval % 60 == 0) + return i18n("1 Hour", "%n Hours", mRepeatInterval/60); + QString mins; + return i18n("Hours and Minutes", "%1H %2M").arg(QString::number(mRepeatInterval/60)).arg(mins.sprintf("%02d", mRepeatInterval%60)); + } + if (mRepeatInterval % (7*1440)) + return i18n("1 Day", "%n Days", mRepeatInterval/1440); + return i18n("1 Week", "%n Weeks", mRepeatInterval/(7*1440)); + } + return brief ? QString::null : i18n("None"); +} + +/****************************************************************************** + * Adjust the event date/time to the first recurrence of the event, on or after + * start date/time. The event start date may not be a recurrence date, in which + * case a later date will be set. + */ +void KAEvent::setFirstRecurrence() +{ + switch (checkRecur()) + { + case KARecurrence::NO_RECUR: + case KARecurrence::MINUTELY: + return; + case KARecurrence::ANNUAL_DATE: + case KARecurrence::ANNUAL_POS: + if (mRecurrence->yearMonths().isEmpty()) + return; // (presumably it's a template) + break; + case KARecurrence::DAILY: + case KARecurrence::WEEKLY: + case KARecurrence::MONTHLY_POS: + case KARecurrence::MONTHLY_DAY: + break; + } + QDateTime recurStart = mRecurrence->startDateTime(); + if (mRecurrence->recursOn(recurStart.date())) + return; // it already recurs on the start date + + // Set the frequency to 1 to find the first possible occurrence + int frequency = mRecurrence->frequency(); + mRecurrence->setFrequency(1); + DateTime next; + nextRecurrence(mNextMainDateTime.dateTime(), next); + if (!next.isValid()) + mRecurrence->setStartDateTime(recurStart); // reinstate the old value + else + { + mRecurrence->setStartDateTime(next.dateTime()); + mStartDateTime = mNextMainDateTime = next; + mUpdated = true; + } + mRecurrence->setFrequency(frequency); // restore the frequency +} + +/****************************************************************************** +* Initialise the event's recurrence from a KCal::Recurrence. +* The event's start date/time is not changed. +*/ +void KAEvent::setRecurrence(const KARecurrence& recurrence) +{ + mUpdated = true; + delete mRecurrence; + if (recurrence.doesRecur()) + { + mRecurrence = new KARecurrence(recurrence); + mRecurrence->setStartDateTime(mStartDateTime.dateTime()); + mRecurrence->setFloats(mStartDateTime.isDateOnly()); + } + else + mRecurrence = 0; + + // Adjust sub-repetition values to fit the recurrence + setRepetition(mRepeatInterval, mRepeatCount); +} + +/****************************************************************************** +* Initialise the event's sub-repetition. +* The repetition length is adjusted if necessary to fit any recurrence interval. +* Reply = false if a non-daily interval was specified for a date-only recurrence. +*/ +bool KAEvent::setRepetition(int interval, int count) +{ + mUpdated = true; + mRepeatInterval = 0; + mRepeatCount = 0; + mNextRepeat = 0; + if (interval > 0 && count > 0 && !mRepeatAtLogin) + { + Q_ASSERT(checkRecur() != KARecurrence::NO_RECUR); + if (interval % 1440 && mStartDateTime.isDateOnly()) + return false; // interval must be in units of days for date-only alarms + if (checkRecur() != KARecurrence::NO_RECUR) + { + int longestInterval = mRecurrence->longestInterval() - 1; + if (interval * count > longestInterval) + count = longestInterval / interval; + } + mRepeatInterval = interval; + mRepeatCount = count; + } + return true; +} + +/****************************************************************************** + * Set the recurrence to recur at a minutes interval. + * Parameters: + * freq = how many minutes between recurrences. + * count = number of occurrences, including first and last. + * = -1 to recur indefinitely. + * = 0 to use 'end' instead. + * end = end date/time (invalid to use 'count' instead). + * Reply = false if no recurrence was set up. + */ +bool KAEvent::setRecurMinutely(int freq, int count, const QDateTime& end) +{ + return setRecur(RecurrenceRule::rMinutely, freq, count, end); +} + +/****************************************************************************** + * Set the recurrence to recur daily. + * Parameters: + * freq = how many days between recurrences. + * days = which days of the week alarms are allowed to occur on. + * count = number of occurrences, including first and last. + * = -1 to recur indefinitely. + * = 0 to use 'end' instead. + * end = end date (invalid to use 'count' instead). + * Reply = false if no recurrence was set up. + */ +bool KAEvent::setRecurDaily(int freq, const QBitArray& days, int count, const QDate& end) +{ + if (!setRecur(RecurrenceRule::rDaily, freq, count, end)) + return false; + int n = 0; + for (int i = 0; i < 7; ++i) + { + if (days.testBit(i)) + ++n; + } + if (n < 7) + mRecurrence->addWeeklyDays(days); + return true; +} + +/****************************************************************************** + * Set the recurrence to recur weekly, on the specified weekdays. + * Parameters: + * freq = how many weeks between recurrences. + * days = which days of the week alarms should occur on. + * count = number of occurrences, including first and last. + * = -1 to recur indefinitely. + * = 0 to use 'end' instead. + * end = end date (invalid to use 'count' instead). + * Reply = false if no recurrence was set up. + */ +bool KAEvent::setRecurWeekly(int freq, const QBitArray& days, int count, const QDate& end) +{ + if (!setRecur(RecurrenceRule::rWeekly, freq, count, end)) + return false; + mRecurrence->addWeeklyDays(days); + return true; +} + +/****************************************************************************** + * Set the recurrence to recur monthly, on the specified days within the month. + * Parameters: + * freq = how many months between recurrences. + * days = which days of the month alarms should occur on. + * count = number of occurrences, including first and last. + * = -1 to recur indefinitely. + * = 0 to use 'end' instead. + * end = end date (invalid to use 'count' instead). + * Reply = false if no recurrence was set up. + */ +bool KAEvent::setRecurMonthlyByDate(int freq, const QValueList<int>& days, int count, const QDate& end) +{ + if (!setRecur(RecurrenceRule::rMonthly, freq, count, end)) + return false; + for (QValueListConstIterator<int> it = days.begin(); it != days.end(); ++it) + mRecurrence->addMonthlyDate(*it); + return true; +} + +/****************************************************************************** + * Set the recurrence to recur monthly, on the specified weekdays in the + * specified weeks of the month. + * Parameters: + * freq = how many months between recurrences. + * posns = which days of the week/weeks of the month alarms should occur on. + * count = number of occurrences, including first and last. + * = -1 to recur indefinitely. + * = 0 to use 'end' instead. + * end = end date (invalid to use 'count' instead). + * Reply = false if no recurrence was set up. + */ +bool KAEvent::setRecurMonthlyByPos(int freq, const QValueList<MonthPos>& posns, int count, const QDate& end) +{ + if (!setRecur(RecurrenceRule::rMonthly, freq, count, end)) + return false; + for (QValueListConstIterator<MonthPos> it = posns.begin(); it != posns.end(); ++it) + mRecurrence->addMonthlyPos((*it).weeknum, (*it).days); + return true; +} + +/****************************************************************************** + * Set the recurrence to recur annually, on the specified start date in each + * of the specified months. + * Parameters: + * freq = how many years between recurrences. + * months = which months of the year alarms should occur on. + * day = day of month, or 0 to use start date + * feb29 = when February 29th should recur in non-leap years. + * count = number of occurrences, including first and last. + * = -1 to recur indefinitely. + * = 0 to use 'end' instead. + * end = end date (invalid to use 'count' instead). + * Reply = false if no recurrence was set up. + */ +bool KAEvent::setRecurAnnualByDate(int freq, const QValueList<int>& months, int day, KARecurrence::Feb29Type feb29, int count, const QDate& end) +{ + if (!setRecur(RecurrenceRule::rYearly, freq, count, end, feb29)) + return false; + for (QValueListConstIterator<int> it = months.begin(); it != months.end(); ++it) + mRecurrence->addYearlyMonth(*it); + if (day) + mRecurrence->addMonthlyDate(day); + return true; +} + +/****************************************************************************** + * Set the recurrence to recur annually, on the specified weekdays in the + * specified weeks of the specified months. + * Parameters: + * freq = how many years between recurrences. + * posns = which days of the week/weeks of the month alarms should occur on. + * months = which months of the year alarms should occur on. + * count = number of occurrences, including first and last. + * = -1 to recur indefinitely. + * = 0 to use 'end' instead. + * end = end date (invalid to use 'count' instead). + * Reply = false if no recurrence was set up. + */ +bool KAEvent::setRecurAnnualByPos(int freq, const QValueList<MonthPos>& posns, const QValueList<int>& months, int count, const QDate& end) +{ + if (!setRecur(RecurrenceRule::rYearly, freq, count, end)) + return false; + for (QValueListConstIterator<int> it = months.begin(); it != months.end(); ++it) + mRecurrence->addYearlyMonth(*it); + for (QValueListConstIterator<MonthPos> it = posns.begin(); it != posns.end(); ++it) + mRecurrence->addYearlyPos((*it).weeknum, (*it).days); + return true; +} + +/****************************************************************************** + * Initialise the event's recurrence data. + * Parameters: + * freq = how many intervals between recurrences. + * count = number of occurrences, including first and last. + * = -1 to recur indefinitely. + * = 0 to use 'end' instead. + * end = end date/time (invalid to use 'count' instead). + * Reply = false if no recurrence was set up. + */ +bool KAEvent::setRecur(RecurrenceRule::PeriodType recurType, int freq, int count, const QDateTime& end, KARecurrence::Feb29Type feb29) +{ + if (count >= -1 && (count || end.date().isValid())) + { + if (!mRecurrence) + mRecurrence = new KARecurrence; + if (mRecurrence->init(recurType, freq, count, mNextMainDateTime, end, feb29)) + { + mUpdated = true; + return true; + } + } + clearRecur(); + return false; +} + +/****************************************************************************** + * Clear the event's recurrence and alarm repetition data. + */ +void KAEvent::clearRecur() +{ + delete mRecurrence; + mRecurrence = 0; + mRepeatInterval = 0; + mRepeatCount = 0; + mNextRepeat = 0; + mUpdated = true; +} + +/****************************************************************************** +* Validate the event's recurrence data, correcting any inconsistencies (which +* should never occur!). +* Reply = true if a recurrence (as opposed to a login repetition) exists. +*/ +KARecurrence::Type KAEvent::checkRecur() const +{ + if (mRecurrence) + { + KARecurrence::Type type = mRecurrence->type(); + switch (type) + { + case KARecurrence::MINUTELY: // hourly + case KARecurrence::DAILY: // daily + case KARecurrence::WEEKLY: // weekly on multiple days of week + case KARecurrence::MONTHLY_DAY: // monthly on multiple dates in month + case KARecurrence::MONTHLY_POS: // monthly on multiple nth day of week + case KARecurrence::ANNUAL_DATE: // annually on multiple months (day of month = start date) + case KARecurrence::ANNUAL_POS: // annually on multiple nth day of week in multiple months + return type; + default: + if (mRecurrence) + const_cast<KAEvent*>(this)->clearRecur(); // recurrence shouldn't exist!! + break; + } + } + return KARecurrence::NO_RECUR; +} + + +/****************************************************************************** + * Return the recurrence interval in units of the recurrence period type. + */ +int KAEvent::recurInterval() const +{ + if (mRecurrence) + { + switch (mRecurrence->type()) + { + case KARecurrence::MINUTELY: + case KARecurrence::DAILY: + case KARecurrence::WEEKLY: + case KARecurrence::MONTHLY_DAY: + case KARecurrence::MONTHLY_POS: + case KARecurrence::ANNUAL_DATE: + case KARecurrence::ANNUAL_POS: + return mRecurrence->frequency(); + default: + break; + } + } + return 0; +} + +/****************************************************************************** +* Validate the event's alarm sub-repetition data, correcting any +* inconsistencies (which should never occur!). +*/ +void KAEvent::checkRepetition() const +{ + if (mRepeatCount && !mRepeatInterval) + const_cast<KAEvent*>(this)->mRepeatCount = 0; + if (!mRepeatCount && mRepeatInterval) + const_cast<KAEvent*>(this)->mRepeatInterval = 0; +} + +#if 0 +/****************************************************************************** + * Convert a QValueList<WDayPos> to QValueList<MonthPos>. + */ +QValueList<KAEvent::MonthPos> KAEvent::convRecurPos(const QValueList<KCal::RecurrenceRule::WDayPos>& wdaypos) +{ + QValueList<MonthPos> mposns; + for (QValueList<KCal::RecurrenceRule::WDayPos>::ConstIterator it = wdaypos.begin(); it != wdaypos.end(); ++it) + { + int daybit = (*it).day() - 1; + int weeknum = (*it).pos(); + bool found = false; + for (QValueList<MonthPos>::Iterator mit = mposns.begin(); mit != mposns.end(); ++mit) + { + if ((*mit).weeknum == weeknum) + { + (*mit).days.setBit(daybit); + found = true; + break; + } + } + if (!found) + { + MonthPos mpos; + mpos.days.fill(false); + mpos.days.setBit(daybit); + mpos.weeknum = weeknum; + mposns.append(mpos); + } + } + return mposns; +} +#endif + +/****************************************************************************** + * Find the alarm template with the specified name. + * Reply = invalid event if not found. + */ +KAEvent KAEvent::findTemplateName(AlarmCalendar& calendar, const QString& name) +{ + KAEvent event; + Event::List events = calendar.events(); + for (Event::List::ConstIterator evit = events.begin(); evit != events.end(); ++evit) + { + Event* ev = *evit; + if (ev->summary() == name) + { + event.set(*ev); + if (!event.isTemplate()) + return KAEvent(); // this shouldn't ever happen + break; + } + } + return event; +} + +/****************************************************************************** + * Adjust the time at which date-only events will occur for each of the events + * in a list. Events for which both date and time are specified are left + * unchanged. + * Reply = true if any events have been updated. + */ +bool KAEvent::adjustStartOfDay(const Event::List& events) +{ + bool changed = false; + QTime startOfDay = Preferences::startOfDay(); + for (Event::List::ConstIterator evit = events.begin(); evit != events.end(); ++evit) + { + Event* event = *evit; + const QStringList cats = event->categories(); + if (cats.find(DATE_ONLY_CATEGORY) != cats.end()) + { + // It's an untimed event, so fix it + QTime oldTime = event->dtStart().time(); + int adjustment = oldTime.secsTo(startOfDay); + if (adjustment) + { + event->setDtStart(QDateTime(event->dtStart().date(), startOfDay)); + Alarm::List alarms = event->alarms(); + int deferralOffset = 0; + for (Alarm::List::ConstIterator alit = alarms.begin(); alit != alarms.end(); ++alit) + { + // Parse the next alarm's text + Alarm& alarm = **alit; + AlarmData data; + readAlarm(alarm, data); + if (data.type & KAAlarm::TIMED_DEFERRAL_FLAG) + { + // Timed deferral alarm, so adjust the offset + deferralOffset = alarm.startOffset().asSeconds(); + alarm.setStartOffset(deferralOffset - adjustment); + } + else if (data.type == KAAlarm::AUDIO__ALARM + && alarm.startOffset().asSeconds() == deferralOffset) + { + // Audio alarm is set for the same time as the deferral alarm + alarm.setStartOffset(deferralOffset - adjustment); + } + } + changed = true; + } + } + else + { + // It's a timed event. Fix any untimed alarms. + int deferralOffset = 0; + int newDeferralOffset = 0; + DateTime start; + QDateTime nextMainDateTime = readDateTime(*event, false, start).rawDateTime(); + AlarmMap alarmMap; + readAlarms(*event, &alarmMap); + for (AlarmMap::Iterator it = alarmMap.begin(); it != alarmMap.end(); ++it) + { + const AlarmData& data = it.data(); + if (!data.alarm->hasStartOffset()) + continue; + if ((data.type & KAAlarm::DEFERRED_ALARM) + && !(data.type & KAAlarm::TIMED_DEFERRAL_FLAG)) + { + // Date-only deferral alarm, so adjust its time + QDateTime altime = nextMainDateTime.addSecs(data.alarm->startOffset().asSeconds()); + altime.setTime(startOfDay); + deferralOffset = data.alarm->startOffset().asSeconds(); + newDeferralOffset = event->dtStart().secsTo(altime); + const_cast<Alarm*>(data.alarm)->setStartOffset(newDeferralOffset); + changed = true; + } + else if (data.type == KAAlarm::AUDIO__ALARM + && data.alarm->startOffset().asSeconds() == deferralOffset) + { + // Audio alarm is set for the same time as the deferral alarm + const_cast<Alarm*>(data.alarm)->setStartOffset(newDeferralOffset); + changed = true; + } + } + } + } + return changed; +} + +/****************************************************************************** + * If the calendar was written by a previous version of KAlarm, do any + * necessary format conversions on the events to ensure that when the calendar + * is saved, no information is lost or corrupted. + */ +void KAEvent::convertKCalEvents(KCal::Calendar& calendar, int version, bool adjustSummerTime) +{ + // KAlarm pre-0.9 codes held in the alarm's DESCRIPTION property + static const QChar SEPARATOR = ';'; + static const QChar LATE_CANCEL_CODE = 'C'; + static const QChar AT_LOGIN_CODE = 'L'; // subsidiary alarm at every login + static const QChar DEFERRAL_CODE = 'D'; // extra deferred alarm + static const QString TEXT_PREFIX = QString::fromLatin1("TEXT:"); + static const QString FILE_PREFIX = QString::fromLatin1("FILE:"); + static const QString COMMAND_PREFIX = QString::fromLatin1("CMD:"); + + // KAlarm pre-0.9.2 codes held in the event's CATEGORY property + static const QString BEEP_CATEGORY = QString::fromLatin1("BEEP"); + + // KAlarm pre-1.1.1 LATECANCEL category with no parameter + static const QString LATE_CANCEL_CAT = QString::fromLatin1("LATECANCEL"); + + // KAlarm pre-1.3.0 TMPLDEFTIME category with no parameter + static const QString TEMPL_DEF_TIME_CAT = QString::fromLatin1("TMPLDEFTIME"); + + // KAlarm pre-1.3.1 XTERM category + static const QString EXEC_IN_XTERM_CAT = QString::fromLatin1("XTERM"); + + // KAlarm pre-1.4.22 properties + static const QCString KMAIL_ID_PROPERTY("KMAILID"); // X-KDE-KALARM-KMAILID property + + if (version >= calVersion()) + return; + + kdDebug(5950) << "KAEvent::convertKCalEvents(): adjusting version " << version << endl; + bool pre_0_7 = (version < KAlarm::Version(0,7,0)); + bool pre_0_9 = (version < KAlarm::Version(0,9,0)); + bool pre_0_9_2 = (version < KAlarm::Version(0,9,2)); + bool pre_1_1_1 = (version < KAlarm::Version(1,1,1)); + bool pre_1_2_1 = (version < KAlarm::Version(1,2,1)); + bool pre_1_3_0 = (version < KAlarm::Version(1,3,0)); + bool pre_1_3_1 = (version < KAlarm::Version(1,3,1)); + bool pre_1_4_14 = (version < KAlarm::Version(1,4,14)); + bool pre_1_5_0 = (version < KAlarm::Version(1,5,0)); + Q_ASSERT(calVersion() == KAlarm::Version(1,5,0)); + + QDateTime dt0(QDate(1970,1,1), QTime(0,0,0)); + QTime startOfDay = Preferences::startOfDay(); + + Event::List events = calendar.rawEvents(); + for (Event::List::ConstIterator evit = events.begin(); evit != events.end(); ++evit) + { + Event* event = *evit; + Alarm::List alarms = event->alarms(); + if (alarms.isEmpty()) + continue; // KAlarm isn't interested in events without alarms + QStringList cats = event->categories(); + bool addLateCancel = false; + + if (pre_0_7 && event->doesFloat()) + { + // It's a KAlarm pre-0.7 calendar file. + // Ensure that when the calendar is saved, the alarm time isn't lost. + event->setFloats(false); + } + + if (pre_0_9) + { + /* + * It's a KAlarm pre-0.9 calendar file. + * All alarms were of type DISPLAY. Instead of the X-KDE-KALARM-TYPE + * alarm property, characteristics were stored as a prefix to the + * alarm DESCRIPTION property, as follows: + * SEQNO;[FLAGS];TYPE:TEXT + * where + * SEQNO = sequence number of alarm within the event + * FLAGS = C for late-cancel, L for repeat-at-login, D for deferral + * TYPE = TEXT or FILE or CMD + * TEXT = message text, file name/URL or command + */ + for (Alarm::List::ConstIterator alit = alarms.begin(); alit != alarms.end(); ++alit) + { + Alarm* alarm = *alit; + bool atLogin = false; + bool deferral = false; + bool lateCancel = false; + KAAlarmEventBase::Type action = T_MESSAGE; + QString txt = alarm->text(); + int length = txt.length(); + int i = 0; + if (txt[0].isDigit()) + { + while (++i < length && txt[i].isDigit()) ; + if (i < length && txt[i++] == SEPARATOR) + { + while (i < length) + { + QChar ch = txt[i++]; + if (ch == SEPARATOR) + break; + if (ch == LATE_CANCEL_CODE) + lateCancel = true; + else if (ch == AT_LOGIN_CODE) + atLogin = true; + else if (ch == DEFERRAL_CODE) + deferral = true; + } + } + else + i = 0; // invalid prefix + } + if (txt.find(TEXT_PREFIX, i) == i) + i += TEXT_PREFIX.length(); + else if (txt.find(FILE_PREFIX, i) == i) + { + action = T_FILE; + i += FILE_PREFIX.length(); + } + else if (txt.find(COMMAND_PREFIX, i) == i) + { + action = T_COMMAND; + i += COMMAND_PREFIX.length(); + } + else + i = 0; + txt = txt.mid(i); + + QStringList types; + switch (action) + { + case T_FILE: + types += FILE_TYPE; + // fall through to T_MESSAGE + case T_MESSAGE: + alarm->setDisplayAlarm(txt); + break; + case T_COMMAND: + setProcedureAlarm(alarm, txt); + break; + case T_EMAIL: // email alarms were introduced in KAlarm 0.9 + case T_AUDIO: // never occurs in this context + break; + } + if (atLogin) + { + types += AT_LOGIN_TYPE; + lateCancel = false; + } + else if (deferral) + types += TIME_DEFERRAL_TYPE; + if (lateCancel) + addLateCancel = true; + if (types.count() > 0) + alarm->setCustomProperty(APPNAME, TYPE_PROPERTY, types.join(",")); + + if (pre_0_7 && alarm->repeatCount() > 0 && alarm->snoozeTime() > 0) + { + // It's a KAlarm pre-0.7 calendar file. + // Minutely recurrences were stored differently. + Recurrence* recur = event->recurrence(); + if (recur && recur->doesRecur()) + { + recur->setMinutely(alarm->snoozeTime()); + recur->setDuration(alarm->repeatCount() + 1); + alarm->setRepeatCount(0); + alarm->setSnoozeTime(0); + } + } + + if (adjustSummerTime) + { + // The calendar file was written by the KDE 3.0.0 version of KAlarm 0.5.7. + // Summer time was ignored when converting to UTC. + QDateTime dt = alarm->time(); + time_t t = dt0.secsTo(dt); + struct tm* dtm = localtime(&t); + if (dtm->tm_isdst) + { + dt = dt.addSecs(-3600); + alarm->setTime(dt); + } + } + } + } + + if (pre_0_9_2) + { + /* + * It's a KAlarm pre-0.9.2 calendar file. + * For the expired calendar, set the CREATED time to the DTEND value. + * Convert date-only DTSTART to date/time, and add category "DATE". + * Set the DTEND time to the DTSTART time. + * Convert all alarm times to DTSTART offsets. + * For display alarms, convert the first unlabelled category to an + * X-KDE-KALARM-FONTCOLOUR property. + * Convert BEEP category into an audio alarm with no audio file. + */ + if (uidStatus(event->uid()) == EXPIRED) + event->setCreated(event->dtEnd()); + QDateTime start = event->dtStart(); + if (event->doesFloat()) + { + event->setFloats(false); + start.setTime(startOfDay); + cats.append(DATE_ONLY_CATEGORY); + } + event->setHasEndDate(false); + + Alarm::List::ConstIterator alit; + for (alit = alarms.begin(); alit != alarms.end(); ++alit) + { + Alarm* alarm = *alit; + QDateTime dt = alarm->time(); + alarm->setStartOffset(start.secsTo(dt)); + } + + if (cats.count() > 0) + { + for (alit = alarms.begin(); alit != alarms.end(); ++alit) + { + Alarm* alarm = *alit; + if (alarm->type() == Alarm::Display) + alarm->setCustomProperty(APPNAME, FONT_COLOUR_PROPERTY, + QString::fromLatin1("%1;;").arg(cats[0])); + } + cats.remove(cats.begin()); + } + + for (QStringList::Iterator it = cats.begin(); it != cats.end(); ++it) + { + if (*it == BEEP_CATEGORY) + { + cats.remove(it); + + Alarm* alarm = event->newAlarm(); + alarm->setEnabled(true); + alarm->setAudioAlarm(); + QDateTime dt = event->dtStart(); // default + + // Parse and order the alarms to know which one's date/time to use + AlarmMap alarmMap; + readAlarms(*event, &alarmMap); + AlarmMap::ConstIterator it = alarmMap.begin(); + if (it != alarmMap.end()) + { + dt = it.data().alarm->time(); + break; + } + alarm->setStartOffset(start.secsTo(dt)); + break; + } + } + } + + if (pre_1_1_1) + { + /* + * It's a KAlarm pre-1.1.1 calendar file. + * Convert simple LATECANCEL category to LATECANCEL:n where n = minutes late. + */ + QStringList::Iterator it; + while ((it = cats.find(LATE_CANCEL_CAT)) != cats.end()) + { + cats.remove(it); + addLateCancel = true; + } + } + + if (pre_1_2_1) + { + /* + * It's a KAlarm pre-1.2.1 calendar file. + * Convert email display alarms from translated to untranslated header prefixes. + */ + for (Alarm::List::ConstIterator alit = alarms.begin(); alit != alarms.end(); ++alit) + { + Alarm* alarm = *alit; + if (alarm->type() == Alarm::Display) + { + QString oldtext = alarm->text(); + QString newtext = AlarmText::toCalendarText(oldtext); + if (oldtext != newtext) + alarm->setDisplayAlarm(newtext); + } + } + } + + if (pre_1_3_0) + { + /* + * It's a KAlarm pre-1.3.0 calendar file. + * Convert simple TMPLDEFTIME category to TMPLAFTTIME:n where n = minutes after. + */ + QStringList::Iterator it; + while ((it = cats.find(TEMPL_DEF_TIME_CAT)) != cats.end()) + { + cats.remove(it); + cats.append(QString("%1%2").arg(TEMPL_AFTER_TIME_CATEGORY).arg(0)); + } + } + + if (pre_1_3_1) + { + /* + * It's a KAlarm pre-1.3.1 calendar file. + * Convert simple XTERM category to LOG:xterm: + */ + QStringList::Iterator it; + while ((it = cats.find(EXEC_IN_XTERM_CAT)) != cats.end()) + { + cats.remove(it); + cats.append(LOG_CATEGORY + xtermURL); + } + } + + if (addLateCancel) + cats.append(QString("%1%2").arg(LATE_CANCEL_CATEGORY).arg(1)); + + event->setCategories(cats); + + + if (pre_1_4_14 + && event->recurrence() && event->recurrence()->doesRecur()) + { + /* + * It's a KAlarm pre-1.4.14 calendar file. + * For recurring events, convert the main alarm offset to an absolute + * time in the X-KDE-KALARM-NEXTRECUR property, and convert main + * alarm offsets to zero and deferral alarm offsets to be relative to + * the next recurrence. + */ + bool dateOnly = (cats.find(DATE_ONLY_CATEGORY) != cats.end()); + DateTime startDateTime(event->dtStart(), dateOnly); + // Convert the main alarm and get the next main trigger time from it + DateTime nextMainDateTime; + bool mainExpired = true; + Alarm::List::ConstIterator alit; + for (alit = alarms.begin(); alit != alarms.end(); ++alit) + { + Alarm* alarm = *alit; + if (!alarm->hasStartOffset()) + continue; + bool mainAlarm = true; + QString property = alarm->customProperty(APPNAME, TYPE_PROPERTY); + QStringList types = QStringList::split(QChar(','), property); + for (unsigned int i = 0; i < types.count(); ++i) + { + QString type = types[i]; + if (type == AT_LOGIN_TYPE + || type == TIME_DEFERRAL_TYPE + || type == DATE_DEFERRAL_TYPE + || type == REMINDER_TYPE + || type == REMINDER_ONCE_TYPE + || type == DISPLAYING_TYPE + || type == PRE_ACTION_TYPE + || type == POST_ACTION_TYPE) + mainAlarm = false; + } + if (mainAlarm) + { + mainExpired = false; + nextMainDateTime = alarm->time(); + nextMainDateTime.setDateOnly(dateOnly); + if (nextMainDateTime != startDateTime) + { + QDateTime dt = nextMainDateTime.dateTime(); + event->setCustomProperty(APPNAME, NEXT_RECUR_PROPERTY, + dt.toString(dateOnly ? "yyyyMMdd" : "yyyyMMddThhmmss")); + } + alarm->setStartOffset(0); + } + } + int adjustment; + if (mainExpired) + { + // It's an expired recurrence. + // Set the alarm offset relative to the first actual occurrence + // (taking account of possible exceptions). + DateTime dt = event->recurrence()->getNextDateTime(startDateTime.dateTime().addDays(-1)); + dt.setDateOnly(dateOnly); + adjustment = startDateTime.secsTo(dt); + } + else + adjustment = startDateTime.secsTo(nextMainDateTime); + if (adjustment) + { + // Convert deferred alarms + for (alit = alarms.begin(); alit != alarms.end(); ++alit) + { + Alarm* alarm = *alit; + if (!alarm->hasStartOffset()) + continue; + QString property = alarm->customProperty(APPNAME, TYPE_PROPERTY); + QStringList types = QStringList::split(QChar(','), property); + for (unsigned int i = 0; i < types.count(); ++i) + { + QString type = types[i]; + if (type == TIME_DEFERRAL_TYPE + || type == DATE_DEFERRAL_TYPE) + { + alarm->setStartOffset(alarm->startOffset().asSeconds() - adjustment); + break; + } + } + } + } + } + + if (pre_1_5_0) + { + /* + * It's a KAlarm pre-1.5.0 calendar file. + * Convert email identity names to uoids. + * Convert simple repetitions without a recurrence, to a recurrence. + */ + for (Alarm::List::ConstIterator alit = alarms.begin(); alit != alarms.end(); ++alit) + { + Alarm* alarm = *alit; + QString name = alarm->customProperty(APPNAME, KMAIL_ID_PROPERTY); + if (name.isEmpty()) + continue; + uint id = KAMail::identityUoid(name); + if (id) + alarm->setCustomProperty(APPNAME, EMAIL_ID_PROPERTY, QString::number(id)); + alarm->removeCustomProperty(APPNAME, KMAIL_ID_PROPERTY); + } + convertRepetition(event); + } + } +} + +/****************************************************************************** +* If the calendar was written by a pre-1.4.22 version of KAlarm, or another +* program, convert simple repetitions in events without a recurrence, to a +* recurrence. +* Reply = true if any conversions were done. +*/ +void KAEvent::convertRepetitions(KCal::CalendarLocal& calendar) +{ + + Event::List events = calendar.rawEvents(); + for (Event::List::ConstIterator ev = events.begin(); ev != events.end(); ++ev) + convertRepetition(*ev); +} + +/****************************************************************************** +* Convert simple repetitions in an event without a recurrence, to a +* recurrence. Repetitions which are an exact multiple of 24 hours are converted +* to daily recurrences; else they are converted to minutely recurrences. Note +* that daily and minutely recurrences produce different results when they span +* a daylight saving time change. +* Reply = true if any conversions were done. +*/ +bool KAEvent::convertRepetition(KCal::Event* event) +{ + Alarm::List alarms = event->alarms(); + if (alarms.isEmpty()) + return false; + Recurrence* recur = event->recurrence(); // guaranteed to return non-null + if (!recur->doesRecur()) + return false; + bool converted = false; + bool readOnly = event->isReadOnly(); + for (Alarm::List::ConstIterator alit = alarms.begin(); alit != alarms.end(); ++alit) + { + Alarm* alarm = *alit; + if (alarm->repeatCount() > 0 && alarm->snoozeTime() > 0) + { + if (!converted) + { + if (readOnly) + event->setReadOnly(false); + if (alarm->snoozeTime() % (24*3600)) + recur->setMinutely(alarm->snoozeTime()); + else + recur->setDaily(alarm->snoozeTime() / (24*3600)); + recur->setDuration(alarm->repeatCount() + 1); + converted = true; + } + alarm->setRepeatCount(0); + alarm->setSnoozeTime(0); + } + } + if (converted) + { + if (readOnly) + event->setReadOnly(true); + } + return converted; +} + +#ifndef NDEBUG +void KAEvent::dumpDebug() const +{ + kdDebug(5950) << "KAEvent dump:\n"; + KAAlarmEventBase::dumpDebug(); + if (!mTemplateName.isEmpty()) + { + kdDebug(5950) << "-- mTemplateName:" << mTemplateName << ":\n"; + kdDebug(5950) << "-- mTemplateAfterTime:" << mTemplateAfterTime << ":\n"; + } + if (mActionType == T_MESSAGE || mActionType == T_FILE) + { + kdDebug(5950) << "-- mAudioFile:" << mAudioFile << ":\n"; + kdDebug(5950) << "-- mPreAction:" << mPreAction << ":\n"; + kdDebug(5950) << "-- mPostAction:" << mPostAction << ":\n"; + } + else if (mActionType == T_COMMAND) + { + kdDebug(5950) << "-- mCommandXterm:" << (mCommandXterm ? "true" : "false") << ":\n"; + kdDebug(5950) << "-- mLogFile:" << mLogFile << ":\n"; + } + kdDebug(5950) << "-- mKMailSerialNumber:" << mKMailSerialNumber << ":\n"; + kdDebug(5950) << "-- mCopyToKOrganizer:" << (mCopyToKOrganizer ? "true" : "false") << ":\n"; + kdDebug(5950) << "-- mStartDateTime:" << mStartDateTime.toString() << ":\n"; + kdDebug(5950) << "-- mSaveDateTime:" << mSaveDateTime.toString() << ":\n"; + if (mRepeatAtLogin) + kdDebug(5950) << "-- mAtLoginDateTime:" << mAtLoginDateTime.toString() << ":\n"; + kdDebug(5950) << "-- mArchiveRepeatAtLogin:" << (mArchiveRepeatAtLogin ? "true" : "false") << ":\n"; + kdDebug(5950) << "-- mEnabled:" << (mEnabled ? "true" : "false") << ":\n"; + if (mReminderMinutes) + kdDebug(5950) << "-- mReminderMinutes:" << mReminderMinutes << ":\n"; + if (mArchiveReminderMinutes) + kdDebug(5950) << "-- mArchiveReminderMinutes:" << mArchiveReminderMinutes << ":\n"; + if (mReminderMinutes || mArchiveReminderMinutes) + kdDebug(5950) << "-- mReminderOnceOnly:" << mReminderOnceOnly << ":\n"; + else if (mDeferral > 0) + { + kdDebug(5950) << "-- mDeferral:" << (mDeferral == NORMAL_DEFERRAL ? "normal" : "reminder") << ":\n"; + kdDebug(5950) << "-- mDeferralTime:" << mDeferralTime.toString() << ":\n"; + } + else if (mDeferral == CANCEL_DEFERRAL) + kdDebug(5950) << "-- mDeferral:cancel:\n"; + kdDebug(5950) << "-- mDeferDefaultMinutes:" << mDeferDefaultMinutes << ":\n"; + if (mDisplaying) + { + kdDebug(5950) << "-- mDisplayingTime:" << mDisplayingTime.toString() << ":\n"; + kdDebug(5950) << "-- mDisplayingFlags:" << mDisplayingFlags << ":\n"; + } + kdDebug(5950) << "-- mRevision:" << mRevision << ":\n"; + kdDebug(5950) << "-- mRecurrence:" << (mRecurrence ? "true" : "false") << ":\n"; + kdDebug(5950) << "-- mAlarmCount:" << mAlarmCount << ":\n"; + kdDebug(5950) << "-- mMainExpired:" << (mMainExpired ? "true" : "false") << ":\n"; + kdDebug(5950) << "KAEvent dump end\n"; +} +#endif + + +/*============================================================================= += Class KAAlarm += Corresponds to a single KCal::Alarm instance. +=============================================================================*/ + +KAAlarm::KAAlarm(const KAAlarm& alarm) + : KAAlarmEventBase(alarm), + mType(alarm.mType), + mRecurs(alarm.mRecurs), + mDeferred(alarm.mDeferred) +{ } + + +int KAAlarm::flags() const +{ + return KAAlarmEventBase::flags() + | (mDeferred ? KAEvent::DEFERRAL : 0); + +} + +#ifndef NDEBUG +void KAAlarm::dumpDebug() const +{ + kdDebug(5950) << "KAAlarm dump:\n"; + KAAlarmEventBase::dumpDebug(); + const char* altype = 0; + switch (mType) + { + case MAIN__ALARM: altype = "MAIN"; break; + case REMINDER__ALARM: altype = "REMINDER"; break; + case DEFERRED_DATE__ALARM: altype = "DEFERRED(DATE)"; break; + case DEFERRED_TIME__ALARM: altype = "DEFERRED(TIME)"; break; + case DEFERRED_REMINDER_DATE__ALARM: altype = "DEFERRED_REMINDER(DATE)"; break; + case DEFERRED_REMINDER_TIME__ALARM: altype = "DEFERRED_REMINDER(TIME)"; break; + case AT_LOGIN__ALARM: altype = "LOGIN"; break; + case DISPLAYING__ALARM: altype = "DISPLAYING"; break; + case AUDIO__ALARM: altype = "AUDIO"; break; + case PRE_ACTION__ALARM: altype = "PRE_ACTION"; break; + case POST_ACTION__ALARM: altype = "POST_ACTION"; break; + default: altype = "INVALID"; break; + } + kdDebug(5950) << "-- mType:" << altype << ":\n"; + kdDebug(5950) << "-- mRecurs:" << (mRecurs ? "true" : "false") << ":\n"; + kdDebug(5950) << "-- mDeferred:" << (mDeferred ? "true" : "false") << ":\n"; + kdDebug(5950) << "KAAlarm dump end\n"; +} + +const char* KAAlarm::debugType(Type type) +{ + switch (type) + { + case MAIN_ALARM: return "MAIN"; + case REMINDER_ALARM: return "REMINDER"; + case DEFERRED_ALARM: return "DEFERRED"; + case DEFERRED_REMINDER_ALARM: return "DEFERRED_REMINDER"; + case AT_LOGIN_ALARM: return "LOGIN"; + case DISPLAYING_ALARM: return "DISPLAYING"; + case AUDIO_ALARM: return "AUDIO"; + case PRE_ACTION_ALARM: return "PRE_ACTION"; + case POST_ACTION_ALARM: return "POST_ACTION"; + default: return "INVALID"; + } +} +#endif + + +/*============================================================================= += Class KAAlarmEventBase +=============================================================================*/ + +void KAAlarmEventBase::copy(const KAAlarmEventBase& rhs) +{ + mEventID = rhs.mEventID; + mText = rhs.mText; + mNextMainDateTime = rhs.mNextMainDateTime; + mBgColour = rhs.mBgColour; + mFgColour = rhs.mFgColour; + mFont = rhs.mFont; + mEmailFromIdentity = rhs.mEmailFromIdentity; + mEmailAddresses = rhs.mEmailAddresses; + mEmailSubject = rhs.mEmailSubject; + mEmailAttachments = rhs.mEmailAttachments; + mSoundVolume = rhs.mSoundVolume; + mFadeVolume = rhs.mFadeVolume; + mFadeSeconds = rhs.mFadeSeconds; + mActionType = rhs.mActionType; + mCommandScript = rhs.mCommandScript; + mRepeatCount = rhs.mRepeatCount; + mRepeatInterval = rhs.mRepeatInterval; + mNextRepeat = rhs.mNextRepeat; + mBeep = rhs.mBeep; + mSpeak = rhs.mSpeak; + mRepeatSound = rhs.mRepeatSound; + mRepeatAtLogin = rhs.mRepeatAtLogin; + mDisplaying = rhs.mDisplaying; + mLateCancel = rhs.mLateCancel; + mAutoClose = rhs.mAutoClose; + mEmailBcc = rhs.mEmailBcc; + mConfirmAck = rhs.mConfirmAck; + mDefaultFont = rhs.mDefaultFont; +} + +void KAAlarmEventBase::set(int flags) +{ + mSpeak = flags & KAEvent::SPEAK; + mBeep = (flags & KAEvent::BEEP) && !mSpeak; + mRepeatSound = flags & KAEvent::REPEAT_SOUND; + mRepeatAtLogin = flags & KAEvent::REPEAT_AT_LOGIN; + mAutoClose = (flags & KAEvent::AUTO_CLOSE) && mLateCancel; + mEmailBcc = flags & KAEvent::EMAIL_BCC; + mConfirmAck = flags & KAEvent::CONFIRM_ACK; + mDisplaying = flags & KAEvent::DISPLAYING_; + mDefaultFont = flags & KAEvent::DEFAULT_FONT; + mCommandScript = flags & KAEvent::SCRIPT; +} + +int KAAlarmEventBase::flags() const +{ + return (mBeep && !mSpeak ? KAEvent::BEEP : 0) + | (mSpeak ? KAEvent::SPEAK : 0) + | (mRepeatSound ? KAEvent::REPEAT_SOUND : 0) + | (mRepeatAtLogin ? KAEvent::REPEAT_AT_LOGIN : 0) + | (mAutoClose ? KAEvent::AUTO_CLOSE : 0) + | (mEmailBcc ? KAEvent::EMAIL_BCC : 0) + | (mConfirmAck ? KAEvent::CONFIRM_ACK : 0) + | (mDisplaying ? KAEvent::DISPLAYING_ : 0) + | (mDefaultFont ? KAEvent::DEFAULT_FONT : 0) + | (mCommandScript ? KAEvent::SCRIPT : 0); +} + +const QFont& KAAlarmEventBase::font() const +{ + return mDefaultFont ? Preferences::messageFont() : mFont; +} + +#ifndef NDEBUG +void KAAlarmEventBase::dumpDebug() const +{ + kdDebug(5950) << "-- mEventID:" << mEventID << ":\n"; + kdDebug(5950) << "-- mActionType:" << (mActionType == T_MESSAGE ? "MESSAGE" : mActionType == T_FILE ? "FILE" : mActionType == T_COMMAND ? "COMMAND" : mActionType == T_EMAIL ? "EMAIL" : mActionType == T_AUDIO ? "AUDIO" : "??") << ":\n"; + kdDebug(5950) << "-- mText:" << mText << ":\n"; + if (mActionType == T_COMMAND) + kdDebug(5950) << "-- mCommandScript:" << (mCommandScript ? "true" : "false") << ":\n"; + kdDebug(5950) << "-- mNextMainDateTime:" << mNextMainDateTime.toString() << ":\n"; + if (mActionType == T_EMAIL) + { + kdDebug(5950) << "-- mEmail: FromKMail:" << mEmailFromIdentity << ":\n"; + kdDebug(5950) << "-- Addresses:" << mEmailAddresses.join(", ") << ":\n"; + kdDebug(5950) << "-- Subject:" << mEmailSubject << ":\n"; + kdDebug(5950) << "-- Attachments:" << mEmailAttachments.join(", ") << ":\n"; + kdDebug(5950) << "-- Bcc:" << (mEmailBcc ? "true" : "false") << ":\n"; + } + kdDebug(5950) << "-- mBgColour:" << mBgColour.name() << ":\n"; + kdDebug(5950) << "-- mFgColour:" << mFgColour.name() << ":\n"; + kdDebug(5950) << "-- mDefaultFont:" << (mDefaultFont ? "true" : "false") << ":\n"; + if (!mDefaultFont) + kdDebug(5950) << "-- mFont:" << mFont.toString() << ":\n"; + kdDebug(5950) << "-- mBeep:" << (mBeep ? "true" : "false") << ":\n"; + kdDebug(5950) << "-- mSpeak:" << (mSpeak ? "true" : "false") << ":\n"; + if (mActionType == T_AUDIO) + { + if (mSoundVolume >= 0) + { + kdDebug(5950) << "-- mSoundVolume:" << mSoundVolume << ":\n"; + if (mFadeVolume >= 0) + { + kdDebug(5950) << "-- mFadeVolume:" << mFadeVolume << ":\n"; + kdDebug(5950) << "-- mFadeSeconds:" << mFadeSeconds << ":\n"; + } + else + kdDebug(5950) << "-- mFadeVolume:-:\n"; + } + else + kdDebug(5950) << "-- mSoundVolume:-:\n"; + kdDebug(5950) << "-- mRepeatSound:" << (mRepeatSound ? "true" : "false") << ":\n"; + } + kdDebug(5950) << "-- mConfirmAck:" << (mConfirmAck ? "true" : "false") << ":\n"; + kdDebug(5950) << "-- mRepeatAtLogin:" << (mRepeatAtLogin ? "true" : "false") << ":\n"; + kdDebug(5950) << "-- mRepeatCount:" << mRepeatCount << ":\n"; + kdDebug(5950) << "-- mRepeatInterval:" << mRepeatInterval << ":\n"; + kdDebug(5950) << "-- mNextRepeat:" << mNextRepeat << ":\n"; + kdDebug(5950) << "-- mDisplaying:" << (mDisplaying ? "true" : "false") << ":\n"; + kdDebug(5950) << "-- mLateCancel:" << mLateCancel << ":\n"; + kdDebug(5950) << "-- mAutoClose:" << (mAutoClose ? "true" : "false") << ":\n"; +} +#endif + + +/*============================================================================= += Class EmailAddressList +=============================================================================*/ + +/****************************************************************************** + * Sets the list of email addresses, removing any empty addresses. + * Reply = false if empty addresses were found. + */ +EmailAddressList& EmailAddressList::operator=(const QValueList<Person>& addresses) +{ + clear(); + for (QValueList<Person>::ConstIterator it = addresses.begin(); it != addresses.end(); ++it) + { + if (!(*it).email().isEmpty()) + append(*it); + } + return *this; +} + +/****************************************************************************** + * Return the email address list as a string, each address being delimited by + * the specified separator string. + */ +QString EmailAddressList::join(const QString& separator) const +{ + QString result; + bool first = true; + for (QValueList<Person>::ConstIterator it = begin(); it != end(); ++it) + { + if (first) + first = false; + else + result += separator; + + bool quote = false; + QString name = (*it).name(); + if (!name.isEmpty()) + { + // Need to enclose the name in quotes if it has any special characters + int len = name.length(); + for (int i = 0; i < len; ++i) + { + QChar ch = name[i]; + if (!ch.isLetterOrNumber()) + { + quote = true; + result += '\"'; + break; + } + } + result += (*it).name(); + result += (quote ? "\" <" : " <"); + quote = true; // need angle brackets round email address + } + + result += (*it).email(); + if (quote) + result += '>'; + } + return result; +} + + +/*============================================================================= += Static functions +=============================================================================*/ + +/****************************************************************************** + * Set the specified alarm to be a procedure alarm with the given command line. + * The command line is first split into its program file and arguments before + * initialising the alarm. + */ +static void setProcedureAlarm(Alarm* alarm, const QString& commandLine) +{ + QString command = QString::null; + QString arguments = QString::null; + QChar quoteChar; + bool quoted = false; + uint posMax = commandLine.length(); + uint pos; + for (pos = 0; pos < posMax; ++pos) + { + QChar ch = commandLine[pos]; + if (quoted) + { + if (ch == quoteChar) + { + ++pos; // omit the quote character + break; + } + command += ch; + } + else + { + bool done = false; + switch (ch) + { + case ' ': + case ';': + case '|': + case '<': + case '>': + done = !command.isEmpty(); + break; + case '\'': + case '"': + if (command.isEmpty()) + { + // Start of a quoted string. Omit the quote character. + quoted = true; + quoteChar = ch; + break; + } + // fall through to default + default: + command += ch; + break; + } + if (done) + break; + } + } + + // Skip any spaces after the command + for ( ; pos < posMax && commandLine[pos] == ' '; ++pos) ; + arguments = commandLine.mid(pos); + + alarm->setProcedureAlarm(command, arguments); +} diff --git a/kalarm/alarmevent.h b/kalarm/alarmevent.h new file mode 100644 index 000000000..ff8ab3056 --- /dev/null +++ b/kalarm/alarmevent.h @@ -0,0 +1,500 @@ +/* + * alarmevent.h - represents calendar alarms and events + * Program: kalarm + * Copyright © 2001-2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KALARMEVENT_H +#define KALARMEVENT_H + +/** @file alarmevent.h - represents calendar alarms and events */ + +#include <qcolor.h> +#include <qfont.h> + +#include <libkcal/person.h> +#include <libkcal/event.h> +namespace KCal { + class Calendar; + class CalendarLocal; +} + +#include "datetime.h" +#include "karecurrence.h" + +class AlarmCalendar; +class KARecurrence; +struct AlarmData; + + +typedef KCal::Person EmailAddress; +class EmailAddressList : public QValueList<KCal::Person> +{ + public: + EmailAddressList() : QValueList<KCal::Person>() { } + EmailAddressList(const QValueList<KCal::Person>& list) { operator=(list); } + EmailAddressList& operator=(const QValueList<KCal::Person>&); + QString join(const QString& separator) const; +}; + + +// Base class containing data common to KAAlarm and KAEvent +class KAAlarmEventBase +{ + public: + ~KAAlarmEventBase() { } + const QString& cleanText() const { return mText; } + QString message() const { return (mActionType == T_MESSAGE || mActionType == T_EMAIL) ? mText : QString::null; } + QString fileName() const { return (mActionType == T_FILE) ? mText : QString::null; } + QString command() const { return (mActionType == T_COMMAND) ? mText : QString::null; } + uint emailFromId() const { return mEmailFromIdentity; } + const EmailAddressList& emailAddresses() const { return mEmailAddresses; } + QString emailAddresses(const QString& sep) const { return mEmailAddresses.join(sep); } + const QString& emailSubject() const { return mEmailSubject; } + const QStringList& emailAttachments() const { return mEmailAttachments; } + QString emailAttachments(const QString& sep) const { return mEmailAttachments.join(sep); } + bool emailBcc() const { return mEmailBcc; } + const QColor& bgColour() const { return mBgColour; } + const QColor& fgColour() const { return mFgColour; } + bool defaultFont() const { return mDefaultFont; } + const QFont& font() const; + int lateCancel() const { return mLateCancel; } + bool autoClose() const { return mAutoClose; } + bool commandScript() const { return mCommandScript; } + bool confirmAck() const { return mConfirmAck; } + bool repeatAtLogin() const { return mRepeatAtLogin; } + int repeatCount() const { return mRepeatCount; } + int repeatInterval() const { return mRepeatInterval; } + bool displaying() const { return mDisplaying; } + bool beep() const { return mBeep; } + bool speak() const { return (mActionType == T_MESSAGE) && mSpeak; } + int flags() const; +#ifdef NDEBUG + void dumpDebug() const { } +#else + void dumpDebug() const; +#endif + + protected: + enum Type { T_MESSAGE, T_FILE, T_COMMAND, T_AUDIO, T_EMAIL }; + + KAAlarmEventBase() : mRepeatCount(0), mLateCancel(0), mAutoClose(false), mBeep(false), mRepeatAtLogin(false), + mDisplaying(false), mEmailBcc(false), mConfirmAck(false) { } + KAAlarmEventBase(const KAAlarmEventBase& rhs) { copy(rhs); } + KAAlarmEventBase& operator=(const KAAlarmEventBase& rhs) { copy(rhs); return *this; } + void copy(const KAAlarmEventBase&); + void set(int flags); + + QString mEventID; // UID: KCal::Event unique ID + QString mText; // message text, file URL, command, email body [or audio file for KAAlarm] + DateTime mNextMainDateTime; // next time to display the alarm, excluding repetitions + QColor mBgColour; // background colour of alarm message + QColor mFgColour; // foreground colour of alarm message, or invalid for default + QFont mFont; // font of alarm message (ignored if mDefaultFont true) + uint mEmailFromIdentity;// KMail identity for email 'From' field, or empty + EmailAddressList mEmailAddresses; // ATTENDEE: addresses to send email to + QString mEmailSubject; // SUMMARY: subject line of email + QStringList mEmailAttachments; // ATTACH: email attachment file names + float mSoundVolume; // volume for sound file, or < 0 for unspecified + float mFadeVolume; // initial volume for sound file, or < 0 for no fade + int mFadeSeconds; // fade time for sound file, or 0 if none + Type mActionType; // alarm action type + int mRepeatCount; // sub-repetition count (excluding the first time) + int mRepeatInterval; // sub-repetition interval (minutes) + int mNextRepeat; // repetition count of next due sub-repetition + int mLateCancel; // how many minutes late will cancel the alarm, or 0 for no cancellation + bool mAutoClose; // whether to close the alarm window after the late-cancel period + bool mCommandScript; // the command text is a script, not a shell command line + bool mBeep; // whether to beep when the alarm is displayed + bool mSpeak; // whether to speak the message when the alarm is displayed + bool mRepeatSound; // whether to repeat the sound file while the alarm is displayed + bool mRepeatAtLogin; // whether to repeat the alarm at every login + bool mDisplaying; // whether the alarm is currently being displayed + bool mEmailBcc; // blind copy the email to the user + bool mConfirmAck; // alarm acknowledgement requires confirmation by user + bool mDefaultFont; // use default message font, not mFont + + friend class AlarmData; +}; + + +// KAAlarm corresponds to a single KCal::Alarm instance. +// A KAEvent may contain multiple KAAlarm's. +class KAAlarm : public KAAlarmEventBase +{ + public: + // Define the basic KAAlaem action types + enum Action + { + MESSAGE = T_MESSAGE, // KCal::Alarm::Display type: display a text message + FILE = T_FILE, // KCal::Alarm::Display type: display a file (URL given by the alarm text) + COMMAND = T_COMMAND, // KCal::Alarm::Procedure type: execute a shell command + EMAIL = T_EMAIL, // KCal::Alarm::Email type: send an email + AUDIO = T_AUDIO // KCal::Alarm::Audio type: play a sound file + }; + // Define the KAAlarm types. + // KAAlarm's of different types may be contained in a KAEvent, + // each defining a different component of the overall alarm. + enum Type + { + INVALID_ALARM = 0, // not an alarm + MAIN_ALARM = 1, // THE real alarm. Must be the first in the enumeration. + // The following values may be used in combination as a bitmask 0x0E + REMINDER_ALARM = 0x02, // reminder in advance of main alarm + DEFERRED_ALARM = 0x04, // deferred alarm + DEFERRED_REMINDER_ALARM = REMINDER_ALARM | DEFERRED_ALARM, // deferred early warning + // The following values must be greater than the preceding ones, to + // ensure that in ordered processing they are processed afterwards. + AT_LOGIN_ALARM = 0x10, // additional repeat-at-login trigger + DISPLAYING_ALARM = 0x20, // copy of the alarm currently being displayed + // The following values are for internal KAEvent use only + AUDIO_ALARM = 0x30, // sound to play when displaying the alarm + PRE_ACTION_ALARM = 0x40, // command to execute before displaying the alarm + POST_ACTION_ALARM = 0x50 // command to execute after the alarm window is closed + }; + enum SubType + { + INVALID__ALARM = INVALID_ALARM, + MAIN__ALARM = MAIN_ALARM, + // The following values may be used in combination as a bitmask 0x0E + REMINDER__ALARM = REMINDER_ALARM, + TIMED_DEFERRAL_FLAG = 0x08, // deferral has a time; if clear, it is date-only + DEFERRED_DATE__ALARM = DEFERRED_ALARM, // deferred alarm - date-only + DEFERRED_TIME__ALARM = DEFERRED_ALARM | TIMED_DEFERRAL_FLAG, + DEFERRED_REMINDER_DATE__ALARM = REMINDER_ALARM | DEFERRED_ALARM, // deferred early warning (date-only) + DEFERRED_REMINDER_TIME__ALARM = REMINDER_ALARM | DEFERRED_ALARM | TIMED_DEFERRAL_FLAG, // deferred early warning (date/time) + // The following values must be greater than the preceding ones, to + // ensure that in ordered processing they are processed afterwards. + AT_LOGIN__ALARM = AT_LOGIN_ALARM, + DISPLAYING__ALARM = DISPLAYING_ALARM, + // The following values are for internal KAEvent use only + AUDIO__ALARM = AUDIO_ALARM, + PRE_ACTION__ALARM = PRE_ACTION_ALARM, + POST_ACTION__ALARM = POST_ACTION_ALARM + }; + + KAAlarm() : mType(INVALID__ALARM), mDeferred(false) { } + KAAlarm(const KAAlarm&); + ~KAAlarm() { } + Action action() const { return (Action)mActionType; } + bool valid() const { return mType != INVALID__ALARM; } + Type type() const { return static_cast<Type>(mType & ~TIMED_DEFERRAL_FLAG); } + SubType subType() const { return mType; } + const QString& eventID() const { return mEventID; } + DateTime dateTime(bool withRepeats = false) const + { return (withRepeats && mNextRepeat && mRepeatInterval) + ? mNextMainDateTime.addSecs(mNextRepeat * mRepeatInterval * 60) : mNextMainDateTime; } + QDate date() const { return mNextMainDateTime.date(); } + QTime time() const { return mNextMainDateTime.time(); } + QString audioFile() const { return (mActionType == T_AUDIO) && !mBeep ? mText : QString::null; } + float soundVolume() const { return (mActionType == T_AUDIO) && !mBeep && !mText.isEmpty() ? mSoundVolume : -1; } + float fadeVolume() const { return (mActionType == T_AUDIO) && mSoundVolume >= 0 && mFadeSeconds && !mBeep && !mText.isEmpty() ? mFadeVolume : -1; } + int fadeSeconds() const { return (mActionType == T_AUDIO) && mSoundVolume >= 0 && mFadeVolume >= 0 && !mBeep && !mText.isEmpty() ? mFadeSeconds : 0; } + bool repeatSound() const { return (mActionType == T_AUDIO) && mRepeatSound && !mBeep && !mText.isEmpty(); } + bool reminder() const { return mType == REMINDER__ALARM; } + bool deferred() const { return mDeferred; } + void setTime(const DateTime& dt) { mNextMainDateTime = dt; } + void setTime(const QDateTime& dt) { mNextMainDateTime = dt; } + int flags() const; +#ifdef NDEBUG + void dumpDebug() const { } + static const char* debugType(Type) { return ""; } +#else + void dumpDebug() const; + static const char* debugType(Type); +#endif + + private: + SubType mType; // alarm type + bool mRecurs; // there is a recurrence rule for the alarm + bool mDeferred; // whether the alarm is an extra deferred/deferred-reminder alarm + + friend class KAEvent; +}; + + +/** KAEvent corresponds to a KCal::Event instance */ +class KAEvent : public KAAlarmEventBase +{ + public: + enum // flags for use in DCOP calls, etc. + { +#ifdef OLD_DCOP + // *** DON'T CHANGE THESE VALUES *** + // because they are part of KAlarm's external DCOP interface. + // (But it's alright to add new values.) + LATE_CANCEL = 0x01, // cancel alarm if not triggered within a minute of its scheduled time +#endif + BEEP = 0x02, // sound audible beep when alarm is displayed + REPEAT_AT_LOGIN = 0x04, // repeat alarm at every login + ANY_TIME = 0x08, // only a date is specified for the alarm, not a time + CONFIRM_ACK = 0x10, // closing the alarm message window requires confirmation prompt + EMAIL_BCC = 0x20, // blind copy the email to the user + DEFAULT_FONT = 0x40, // use default alarm message font + REPEAT_SOUND = 0x80, // repeat sound file while alarm is displayed + DISABLED = 0x100, // alarm is currently disabled + AUTO_CLOSE = 0x200, // auto-close alarm window after late-cancel period + SCRIPT = 0x400, // command is a script, not a shell command line + EXEC_IN_XTERM = 0x800, // execute command in terminal window + SPEAK = 0x1000, // speak the message when the alarm is displayed + COPY_KORGANIZER = 0x2000, // KOrganizer should hold a copy of the event +#ifdef OLD_DCOP + // The following are read-only internal values, and may be changed +#else + // The following are read-only internal values +#endif + REMINDER = 0x10000, + DEFERRAL = 0x20000, + TIMED_FLAG = 0x40000, + DATE_DEFERRAL = DEFERRAL, + TIME_DEFERRAL = DEFERRAL | TIMED_FLAG, + DISPLAYING_ = 0x80000, + READ_ONLY_FLAGS = 0xF0000 // mask for all read-only internal values + }; + /** The category of an event, indicated by the middle part of its UID. */ + enum Status + { + ACTIVE, // the event is currently active + EXPIRED, // the event has expired + DISPLAYING, // the event is currently being displayed + TEMPLATE, // the event is an alarm template + KORGANIZER // the event is a copy of a KAlarm event, held by KOrganizer + }; + enum Action + { + MESSAGE = T_MESSAGE, + FILE = T_FILE, + COMMAND = T_COMMAND, + EMAIL = T_EMAIL + }; + enum OccurType // what type of occurrence is due + { + NO_OCCURRENCE = 0, // no occurrence is due + FIRST_OR_ONLY_OCCURRENCE = 0x01, // the first occurrence (takes precedence over LAST_RECURRENCE) + RECURRENCE_DATE = 0x02, // a recurrence with only a date, not a time + RECURRENCE_DATE_TIME = 0x03, // a recurrence with a date and time + LAST_RECURRENCE = 0x04, // the last recurrence + OCCURRENCE_REPEAT = 0x10, // (bitmask for a repetition of an occurrence) + FIRST_OR_ONLY_OCCURRENCE_REPEAT = OCCURRENCE_REPEAT | FIRST_OR_ONLY_OCCURRENCE, // a repetition of the first occurrence + RECURRENCE_DATE_REPEAT = OCCURRENCE_REPEAT | RECURRENCE_DATE, // a repetition of a date-only recurrence + RECURRENCE_DATE_TIME_REPEAT = OCCURRENCE_REPEAT | RECURRENCE_DATE_TIME, // a repetition of a date/time recurrence + LAST_RECURRENCE_REPEAT = OCCURRENCE_REPEAT | LAST_RECURRENCE // a repetition of the last recurrence + }; + enum OccurOption // options for nextOccurrence() + { + IGNORE_REPETITION, // check for recurrences only, ignore repetitions + RETURN_REPETITION, // return repetition if it's the next occurrence + ALLOW_FOR_REPETITION // check for repetition being the next occurrence, but return recurrence + }; + enum DeferLimitType // what type of occurrence currently limits a deferral + { + LIMIT_NONE, + LIMIT_MAIN, + LIMIT_RECURRENCE, + LIMIT_REPETITION, + LIMIT_REMINDER + }; + + KAEvent() : mRevision(0), mRecurrence(0), mAlarmCount(0) { } + KAEvent(const QDateTime& dt, const QString& message, const QColor& bg, const QColor& fg, const QFont& f, Action action, int lateCancel, int flags) + : mRecurrence(0) { set(dt, message, bg, fg, f, action, lateCancel, flags); } + explicit KAEvent(const KCal::Event& e) : mRecurrence(0) { set(e); } + KAEvent(const KAEvent& e) : KAAlarmEventBase(e), mRecurrence(0) { copy(e); } + ~KAEvent() { delete mRecurrence; } + KAEvent& operator=(const KAEvent& e) { if (&e != this) copy(e); return *this; } + void set(const KCal::Event&); + void set(const QDateTime&, const QString& message, const QColor& bg, const QColor& fg, const QFont&, Action, int lateCancel, int flags); + void setEmail(uint from, const EmailAddressList&, const QString& subject, const QStringList& attachments); + void setAudioFile(const QString& filename, float volume, float fadeVolume, int fadeSeconds); + void setTemplate(const QString& name, int afterTime = -1) { mTemplateName = name; mTemplateAfterTime = afterTime; mUpdated = true; } + void setActions(const QString& pre, const QString& post) { mPreAction = pre; mPostAction = post; mUpdated = true; } + OccurType setNextOccurrence(const QDateTime& preDateTime); + void setFirstRecurrence(); + void setEventID(const QString& id) { mEventID = id; mUpdated = true; } + void setDate(const QDate& d) { mNextMainDateTime.set(d); mUpdated = true; } + void setTime(const QDateTime& dt) { mNextMainDateTime.set(dt); mUpdated = true; } + void setSaveDateTime(const QDateTime& dt) { mSaveDateTime = dt; mUpdated = true; } + void setLateCancel(int lc) { mLateCancel = lc; mUpdated = true; } + void setAutoClose(bool ac) { mAutoClose = ac; mUpdated = true; } + void setRepeatAtLogin(bool rl) { mRepeatAtLogin = rl; mUpdated = true; } + void setUid(Status s) { mEventID = uid(mEventID, s); mUpdated = true; } + void setKMailSerialNumber(unsigned long n) { mKMailSerialNumber = n; } + void setLogFile(const QString& logfile); + void setReminder(int minutes, bool onceOnly); + bool defer(const DateTime&, bool reminder, bool adjustRecurrence = false); + void cancelDefer(); + void cancelCancelledDeferral(); + void setDeferDefaultMinutes(int minutes) { mDeferDefaultMinutes = minutes; mUpdated = true; } + bool setDisplaying(const KAEvent&, KAAlarm::Type, const QDateTime&); + void reinstateFromDisplaying(const KAEvent& dispEvent); + void setArchive() { mArchive = true; mUpdated = true; } + void setEnabled(bool enable) { mEnabled = enable; mUpdated = true; } + void setUpdated() { mUpdated = true; } + void clearUpdated() const { mUpdated = false; } + void removeExpiredAlarm(KAAlarm::Type); + void incrementRevision() { ++mRevision; mUpdated = true; } + + KCal::Event* event() const; // convert to new Event + bool isTemplate() const { return !mTemplateName.isEmpty(); } + const QString& templateName() const { return mTemplateName; } + bool usingDefaultTime() const { return mTemplateAfterTime == 0; } + int templateAfterTime() const { return mTemplateAfterTime; } + KAAlarm alarm(KAAlarm::Type) const; + KAAlarm firstAlarm() const; + KAAlarm nextAlarm(const KAAlarm& al) const { return nextAlarm(al.type()); } + KAAlarm nextAlarm(KAAlarm::Type) const; + KAAlarm convertDisplayingAlarm() const; + bool updateKCalEvent(KCal::Event&, bool checkUid = true, bool original = false, bool cancelCancelledDefer = false) const; + Action action() const { return (Action)mActionType; } + bool displayAction() const { return mActionType == T_MESSAGE || mActionType == T_FILE; } + const QString& id() const { return mEventID; } + bool valid() const { return mAlarmCount && (mAlarmCount != 1 || !mRepeatAtLogin); } + int alarmCount() const { return mAlarmCount; } + const DateTime& startDateTime() const { return mStartDateTime; } + DateTime mainDateTime(bool withRepeats = false) const + { return (withRepeats && mNextRepeat && mRepeatInterval) + ? mNextMainDateTime.addSecs(mNextRepeat * mRepeatInterval * 60) : mNextMainDateTime; } + QDate mainDate() const { return mNextMainDateTime.date(); } + QTime mainTime() const { return mNextMainDateTime.time(); } + DateTime mainEndRepeatTime() const { return (mRepeatCount > 0 && mRepeatInterval) + ? mNextMainDateTime.addSecs(mRepeatCount * mRepeatInterval * 60) : mNextMainDateTime; } + int reminder() const { return mReminderMinutes; } + bool reminderOnceOnly() const { return mReminderOnceOnly; } + bool reminderDeferral() const { return mDeferral == REMINDER_DEFERRAL; } + int reminderArchived() const { return mArchiveReminderMinutes; } + DateTime deferDateTime() const { return mDeferralTime; } + DateTime deferralLimit(DeferLimitType* = 0) const; + int deferDefaultMinutes() const { return mDeferDefaultMinutes; } + DateTime displayDateTime() const; + const QString& messageFileOrCommand() const { return mText; } + QString logFile() const { return mLogFile; } + bool commandXterm() const { return mCommandXterm; } + unsigned long kmailSerialNumber() const { return mKMailSerialNumber; } + bool copyToKOrganizer() const { return mCopyToKOrganizer; } + const QString& audioFile() const { return mAudioFile; } + float soundVolume() const { return !mAudioFile.isEmpty() ? mSoundVolume : -1; } + float fadeVolume() const { return !mAudioFile.isEmpty() && mSoundVolume >= 0 && mFadeSeconds ? mFadeVolume : -1; } + int fadeSeconds() const { return !mAudioFile.isEmpty() && mSoundVolume >= 0 && mFadeVolume >= 0 ? mFadeSeconds : 0; } + bool repeatSound() const { return mRepeatSound && !mAudioFile.isEmpty(); } + const QString& preAction() const { return mPreAction; } + const QString& postAction() const { return mPostAction; } + bool recurs() const { return checkRecur() != KARecurrence::NO_RECUR; } + KARecurrence::Type recurType() const { return checkRecur(); } + KARecurrence* recurrence() const { return mRecurrence; } + int recurInterval() const; // recurrence period in units of the recurrence period type (minutes, days, etc) + int longestRecurrenceInterval() const { return mRecurrence ? mRecurrence->longestInterval() : 0; } + QString recurrenceText(bool brief = false) const; + QString repetitionText(bool brief = false) const; + bool occursAfter(const QDateTime& preDateTime, bool includeRepetitions) const; + OccurType nextOccurrence(const QDateTime& preDateTime, DateTime& result, OccurOption = IGNORE_REPETITION) const; + OccurType previousOccurrence(const QDateTime& afterDateTime, DateTime& result, bool includeRepetitions = false) const; + int flags() const; + bool deferred() const { return mDeferral > 0; } + bool toBeArchived() const { return mArchive; } + bool enabled() const { return mEnabled; } + bool updated() const { return mUpdated; } + bool mainExpired() const { return mMainExpired; } + bool expired() const { return mDisplaying && mMainExpired || uidStatus(mEventID) == EXPIRED; } + Status uidStatus() const { return uidStatus(mEventID); } + static Status uidStatus(const QString& uid); + static QString uid(const QString& id, Status); + static KAEvent findTemplateName(AlarmCalendar&, const QString& name); + + struct MonthPos + { + MonthPos() : days(7) { } + int weeknum; // week in month, or < 0 to count from end of month + QBitArray days; // days in week + }; + bool setRepetition(int interval, int count); + void setNoRecur() { clearRecur(); } + void setRecurrence(const KARecurrence&); + bool setRecurMinutely(int freq, int count, const QDateTime& end); + bool setRecurDaily(int freq, const QBitArray& days, int count, const QDate& end); + bool setRecurWeekly(int freq, const QBitArray& days, int count, const QDate& end); + bool setRecurMonthlyByDate(int freq, const QValueList<int>& days, int count, const QDate& end); + bool setRecurMonthlyByPos(int freq, const QValueList<MonthPos>& pos, int count, const QDate& end); + bool setRecurAnnualByDate(int freq, const QValueList<int>& months, int day, KARecurrence::Feb29Type, int count, const QDate& end); + bool setRecurAnnualByPos(int freq, const QValueList<MonthPos>& pos, const QValueList<int>& months, int count, const QDate& end); +// static QValueList<MonthPos> convRecurPos(const QValueList<KCal::RecurrenceRule::WDayPos>&); +#ifdef NDEBUG + void dumpDebug() const { } +#else + void dumpDebug() const; +#endif + static int calVersion(); + static QString calVersionString(); + static bool adjustStartOfDay(const KCal::Event::List&); + static void convertKCalEvents(KCal::Calendar&, int version, bool adjustSummerTime); + static void convertRepetitions(KCal::CalendarLocal&); + + private: + enum DeferType { + CANCEL_DEFERRAL = -1, // there was a deferred alarm, but it has been cancelled + NO_DEFERRAL = 0, // there is no deferred alarm + NORMAL_DEFERRAL, // the main alarm, a recurrence or a repeat is deferred + REMINDER_DEFERRAL // a reminder alarm is deferred + }; + + void copy(const KAEvent&); + bool setRecur(KCal::RecurrenceRule::PeriodType, int freq, int count, const QDateTime& end, KARecurrence::Feb29Type = KARecurrence::FEB29_FEB29); + void clearRecur(); + KARecurrence::Type checkRecur() const; + void checkRepetition() const; + OccurType nextRecurrence(const QDateTime& preDateTime, DateTime& result) const; + OccurType previousRecurrence(const QDateTime& afterDateTime, DateTime& result) const; + static bool convertRepetition(KCal::Event*); + KCal::Alarm* initKCalAlarm(KCal::Event&, const DateTime&, const QStringList& types, KAAlarm::Type = KAAlarm::INVALID_ALARM) const; + KCal::Alarm* initKCalAlarm(KCal::Event&, int startOffsetSecs, const QStringList& types, KAAlarm::Type = KAAlarm::INVALID_ALARM) const; + static DateTime readDateTime(const KCal::Event&, bool dateOnly, DateTime& start); + static void readAlarms(const KCal::Event&, void* alarmMap); + static void readAlarm(const KCal::Alarm&, AlarmData&); + inline void set_deferral(DeferType); + inline void set_reminder(int minutes); + inline void set_archiveReminder(); + + QString mTemplateName; // alarm template's name, or null if normal event + QString mAudioFile; // ATTACH: audio file to play + QString mPreAction; // command to execute before alarm is displayed + QString mPostAction; // command to execute after alarm window is closed + DateTime mStartDateTime; // DTSTART and DTEND: start and end time for event + QDateTime mSaveDateTime; // CREATED: date event was created, or saved in expired calendar + QDateTime mAtLoginDateTime; // repeat-at-login time + DateTime mDeferralTime; // extra time to trigger alarm (if alarm or reminder deferred) + DateTime mDisplayingTime; // date/time shown in the alarm currently being displayed + int mDisplayingFlags; // type of alarm which is currently being displayed + int mReminderMinutes; // how long in advance reminder is to be, or 0 if none + int mArchiveReminderMinutes; // original reminder period if now expired, or 0 if none + int mDeferDefaultMinutes; // default number of minutes for deferral dialogue, or 0 to select time control + int mRevision; // SEQUENCE: revision number of the original alarm, or 0 + KARecurrence* mRecurrence; // RECUR: recurrence specification, or 0 if none + int mAlarmCount; // number of alarms: count of !mMainExpired, mRepeatAtLogin, mDeferral, mReminderMinutes, mDisplaying + DeferType mDeferral; // whether the alarm is an extra deferred/deferred-reminder alarm + unsigned long mKMailSerialNumber;// if email text, message's KMail serial number + int mTemplateAfterTime;// time not specified: use n minutes after default time, or -1 (applies to templates only) + QString mLogFile; // alarm output is to be logged to this URL + bool mCommandXterm; // command alarm is to be executed in a terminal window + bool mCopyToKOrganizer; // KOrganizer should hold a copy of the event + bool mReminderOnceOnly; // the reminder is output only for the first recurrence + bool mMainExpired; // main alarm has expired (in which case a deferral alarm will exist) + bool mArchiveRepeatAtLogin; // if now expired, original event was repeat-at-login + bool mArchive; // event has triggered in the past, so archive it when closed + bool mEnabled; // false if event is disabled + mutable bool mUpdated; // event has been updated but not written to calendar file +}; + +#endif // KALARMEVENT_H diff --git a/kalarm/alarmlistview.cpp b/kalarm/alarmlistview.cpp new file mode 100644 index 000000000..19fcda4b5 --- /dev/null +++ b/kalarm/alarmlistview.cpp @@ -0,0 +1,711 @@ +/* + * alarmlistview.cpp - widget showing list of outstanding alarms + * Program: kalarm + * Copyright © 2001-2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qtooltip.h> +#include <qpainter.h> +#include <qstyle.h> +#include <qheader.h> +#include <qregexp.h> + +#include <kglobal.h> +#include <klocale.h> +#include <kdebug.h> + +#include <libkcal/icaldrag.h> +#include <libkcal/calendarlocal.h> + +#include "alarmcalendar.h" +#include "alarmtext.h" +#include "functions.h" +#include "kalarmapp.h" +#include "preferences.h" +#include "alarmlistview.moc" + + +class AlarmListTooltip : public QToolTip +{ + public: + AlarmListTooltip(QWidget* parent) : QToolTip(parent) { } + virtual ~AlarmListTooltip() {} + protected: + virtual void maybeTip(const QPoint&); +}; + + +/*============================================================================= += Class: AlarmListView += Displays the list of outstanding alarms. +=============================================================================*/ +QValueList<EventListViewBase*> AlarmListView::mInstanceList; +bool AlarmListView::mDragging = false; + + +AlarmListView::AlarmListView(const QValueList<int>& order, QWidget* parent, const char* name) + : EventListViewBase(parent, name), + mMousePressed(false), + mDrawMessageInColour(false), + mShowExpired(false) +{ + static QString titles[COLUMN_COUNT] = { + i18n("Time"), + i18n("Time To"), + i18n("Repeat"), + QString::null, + QString::null, + i18n("Message, File or Command") + }; + + setSelectionMode(QListView::Extended); + + // Set the column order + int i; + bool ok = false; + if (order.count() >= COLUMN_COUNT) + { + // The column order is specified + bool posns[COLUMN_COUNT]; + for (i = 0; i < COLUMN_COUNT; ++i) + posns[i] = false; + for (i = 0; i < COLUMN_COUNT; ++i) + { + int ord = order[i]; + if (ord < COLUMN_COUNT && ord >= 0) + { + mColumn[i] = ord; + posns[ord] = true; + } + } + ok = true; + for (i = 0; i < COLUMN_COUNT; ++i) + if (!posns[i]) + ok = false; + if (ok && mColumn[MESSAGE_COLUMN] != MESSAGE_COLUMN) + { + // Shift the message column to be last, since otherwise + // column widths get screwed up. + int messageCol = mColumn[MESSAGE_COLUMN]; + for (i = 0; i < COLUMN_COUNT; ++i) + if (mColumn[i] > messageCol) + --mColumn[i]; + mColumn[MESSAGE_COLUMN] = MESSAGE_COLUMN; + } + } + if (!ok) + { + // Either no column order was specified, or it was invalid, + // so use the default order + for (i = 0; i < COLUMN_COUNT; ++i) + mColumn[i] = i; + } + + // Initialise the columns + for (i = 0; i < COLUMN_COUNT; ++i) + { + for (int j = 0; j < COLUMN_COUNT; ++j) + if (mColumn[j] == i) + { + if (j != MESSAGE_COLUMN) + addColumn(titles[j]); + break; + } + } + addLastColumn(titles[MESSAGE_COLUMN]); + + setSorting(mColumn[TIME_COLUMN]); // sort initially by date/time + mTimeColumnHeaderWidth = columnWidth(mColumn[TIME_COLUMN]); + mTimeToColumnHeaderWidth = columnWidth(mColumn[TIME_TO_COLUMN]); + setColumnAlignment(mColumn[REPEAT_COLUMN], Qt::AlignHCenter); + setColumnWidthMode(mColumn[REPEAT_COLUMN], QListView::Maximum); + + // Set the width of the colour column in proportion to height + setColumnWidth(mColumn[COLOUR_COLUMN], itemHeight() * 3/4); + setColumnWidthMode(mColumn[COLOUR_COLUMN], QListView::Manual); + + // Set the width of the alarm type column to exactly accommodate the icons. + // Don't allow the user to resize it (to avoid refresh problems, and bearing + // in mind that resizing doesn't seem very useful anyway). + setColumnWidth(mColumn[TYPE_COLUMN], AlarmListViewItem::typeIconWidth(this)); + setColumnWidthMode(mColumn[TYPE_COLUMN], QListView::Manual); + header()->setResizeEnabled(false, mColumn[TYPE_COLUMN]); + + mInstanceList.append(this); + + mTooltip = new AlarmListTooltip(viewport()); +} + +AlarmListView::~AlarmListView() +{ + delete mTooltip; + mTooltip = 0; + mInstanceList.remove(this); +} + +/****************************************************************************** +* Add all the current alarms to the list. +*/ +void AlarmListView::populate() +{ + KAEvent event; + KCal::Event::List events; + KCal::Event::List::ConstIterator it; + QDateTime now = QDateTime::currentDateTime(); + if (mShowExpired) + { + AlarmCalendar* cal = AlarmCalendar::expiredCalendarOpen(); + if (cal) + { + events = cal->events(); + for (it = events.begin(); it != events.end(); ++it) + { + KCal::Event* kcalEvent = *it; + if (kcalEvent->alarms().count() > 0) + { + event.set(*kcalEvent); + addEntry(event, now); + } + } + } + } + events = AlarmCalendar::activeCalendar()->events(); + for (it = events.begin(); it != events.end(); ++it) + { + KCal::Event* kcalEvent = *it; + event.set(*kcalEvent); + if (mShowExpired || !event.expired()) + addEntry(event, now); + } +} + +/****************************************************************************** +* Set which time columns are to be displayed. +*/ +void AlarmListView::selectTimeColumns(bool time, bool timeTo) +{ + if (!time && !timeTo) + return; // always show at least one time column + bool changed = false; + int w = columnWidth(mColumn[TIME_COLUMN]); + if (time && !w) + { + // Unhide the time column + int colWidth = mTimeColumnHeaderWidth; + QFontMetrics fm = fontMetrics(); + for (AlarmListViewItem* item = firstChild(); item; item = item->nextSibling()) + { + int w = item->width(fm, this, mColumn[TIME_COLUMN]); + if (w > colWidth) + colWidth = w; + } + setColumnWidth(mColumn[TIME_COLUMN], colWidth); + setColumnWidthMode(mColumn[TIME_COLUMN], QListView::Maximum); + changed = true; + } + else if (!time && w) + { + // Hide the time column + setColumnWidthMode(mColumn[TIME_COLUMN], QListView::Manual); + setColumnWidth(mColumn[TIME_COLUMN], 0); + changed = true; + } + w = columnWidth(mColumn[TIME_TO_COLUMN]); + if (timeTo && !w) + { + // Unhide the time-to-alarm column + setColumnWidthMode(mColumn[TIME_TO_COLUMN], QListView::Maximum); + updateTimeToAlarms(true); + if (columnWidth(mColumn[TIME_TO_COLUMN]) < mTimeToColumnHeaderWidth) + setColumnWidth(mColumn[TIME_TO_COLUMN], mTimeToColumnHeaderWidth); + changed = true; + } + else if (!timeTo && w) + { + // Hide the time-to-alarm column + setColumnWidthMode(mColumn[TIME_TO_COLUMN], QListView::Manual); + setColumnWidth(mColumn[TIME_TO_COLUMN], 0); + changed = true; + } + if (changed) + { + resizeLastColumn(); + triggerUpdate(); // ensure scroll bar appears if needed + } +} + +/****************************************************************************** +* Update all the values in the time-to-alarm column. +*/ +void AlarmListView::updateTimeToAlarms(bool forceDisplay) +{ + if (forceDisplay || columnWidth(mColumn[TIME_TO_COLUMN])) + { + QDateTime now = QDateTime::currentDateTime(); + for (AlarmListViewItem* item = firstChild(); item; item = item->nextSibling()) + item->updateTimeToAlarm(now, forceDisplay); + } +} + +/****************************************************************************** +* Add an event to every list instance. +* The selection highlight is moved to the new event in the specified instance only. +*/ +void AlarmListView::addEvent(const KAEvent& event, EventListViewBase* view) +{ + QDateTime now = QDateTime::currentDateTime(); + for (InstanceListConstIterator it = mInstanceList.begin(); it != mInstanceList.end(); ++it) + static_cast<AlarmListView*>(*it)->addEntry(event, now, true, (*it == view)); +} + +/****************************************************************************** +* Add a new item to the list. +*/ +AlarmListViewItem* AlarmListView::addEntry(const KAEvent& event, const QDateTime& now, bool setSize, bool reselect) +{ + if (!mShowExpired && event.expired()) + return 0; + AlarmListViewItem* item = new AlarmListViewItem(this, event, now); + return static_cast<AlarmListViewItem*>(EventListViewBase::addEntry(item, setSize, reselect)); +} + +/****************************************************************************** +* Create a new list item for addEntry(). +*/ +EventListViewItemBase* AlarmListView::createItem(const KAEvent& event) +{ + return new AlarmListViewItem(this, event, QDateTime::currentDateTime()); +} + +/****************************************************************************** +* Check whether an item's alarm has expired. +*/ +bool AlarmListView::expired(AlarmListViewItem* item) const +{ + return item->event().expired(); +} + +/****************************************************************************** +* Return the column order. +*/ +QValueList<int> AlarmListView::columnOrder() const +{ + QHeader* hdr = header(); + int order[COLUMN_COUNT]; + order[TIME_COLUMN] = hdr->mapToIndex(mColumn[TIME_COLUMN]); + order[TIME_TO_COLUMN] = hdr->mapToIndex(mColumn[TIME_TO_COLUMN]); + order[REPEAT_COLUMN] = hdr->mapToIndex(mColumn[REPEAT_COLUMN]); + order[COLOUR_COLUMN] = hdr->mapToIndex(mColumn[COLOUR_COLUMN]); + order[TYPE_COLUMN] = hdr->mapToIndex(mColumn[TYPE_COLUMN]); + order[MESSAGE_COLUMN] = hdr->mapToIndex(mColumn[MESSAGE_COLUMN]); + QValueList<int> orderList; + for (int i = 0; i < COLUMN_COUNT; ++i) + orderList += order[i]; + return orderList; +} + +/****************************************************************************** +* Returns the QWhatsThis text for a specified column. +*/ +QString AlarmListView::whatsThisText(int column) const +{ + if (column == mColumn[TIME_COLUMN]) + return i18n("Next scheduled date and time of the alarm"); + if (column == mColumn[TIME_TO_COLUMN]) + return i18n("How long until the next scheduled trigger of the alarm"); + if (column == mColumn[REPEAT_COLUMN]) + return i18n("How often the alarm recurs"); + if (column == mColumn[COLOUR_COLUMN]) + return i18n("Background color of alarm message"); + if (column == mColumn[TYPE_COLUMN]) + return i18n("Alarm type (message, file, command or email)"); + if (column == mColumn[MESSAGE_COLUMN]) + return i18n("Alarm message text, URL of text file to display, command to execute, or email subject line"); + return i18n("List of scheduled alarms"); +} + +/****************************************************************************** +* Called when the mouse button is pressed. +* Records the position of the mouse when the left button is pressed, for use +* in drag operations. +*/ +void AlarmListView::contentsMousePressEvent(QMouseEvent* e) +{ + QListView::contentsMousePressEvent(e); + if (e->button() == Qt::LeftButton) + { + QPoint p(contentsToViewport(e->pos())); + if (itemAt(p)) + { + mMousePressPos = e->pos(); + mMousePressed = true; + } + mDragging = false; + } +} + +/****************************************************************************** +* Called when the mouse is moved. +* Creates a drag object when the mouse drags one or more selected items. +*/ +void AlarmListView::contentsMouseMoveEvent(QMouseEvent* e) +{ + QListView::contentsMouseMoveEvent(e); + if (mMousePressed + && (mMousePressPos - e->pos()).manhattanLength() > QApplication::startDragDistance()) + { + // Create a calendar object containing all the currently selected alarms + kdDebug(5950) << "AlarmListView::contentsMouseMoveEvent(): drag started" << endl; + mMousePressed = false; + KCal::CalendarLocal cal(QString::fromLatin1("UTC")); + cal.setLocalTime(); // write out using local time (i.e. no time zone) + QValueList<EventListViewItemBase*> items = selectedItems(); + if (!items.count()) + return; + for (QValueList<EventListViewItemBase*>::Iterator it = items.begin(); it != items.end(); ++it) + { + const KAEvent& event = (*it)->event(); + KCal::Event* kcalEvent = new KCal::Event; + event.updateKCalEvent(*kcalEvent, false, true); + kcalEvent->setUid(event.id()); + cal.addEvent(kcalEvent); + } + + // Create the drag object for the destination program to receive + mDragging = true; + KCal::ICalDrag* dobj = new KCal::ICalDrag(&cal, this); + dobj->dragCopy(); // the drag operation will copy the alarms + } +} + +/****************************************************************************** +* Called when the mouse button is released. +* Notes that the mouse left button is no longer pressed, for use in drag +* operations. +*/ +void AlarmListView::contentsMouseReleaseEvent(QMouseEvent *e) +{ + QListView::contentsMouseReleaseEvent(e); + mMousePressed = false; + mDragging = false; +} + + +/*============================================================================= += Class: AlarmListViewItem += Contains the details of one alarm for display in the AlarmListView. +=============================================================================*/ +int AlarmListViewItem::mTimeHourPos = -2; +int AlarmListViewItem::mDigitWidth = -1; + +AlarmListViewItem::AlarmListViewItem(AlarmListView* parent, const KAEvent& event, const QDateTime& now) + : EventListViewItemBase(parent, event), + mMessageTruncated(false), + mTimeToAlarmShown(false) +{ + setLastColumnText(); // set the message column text + + DateTime dateTime = event.expired() ? event.startDateTime() : event.displayDateTime(); + if (parent->column(AlarmListView::TIME_COLUMN) >= 0) + setText(parent->column(AlarmListView::TIME_COLUMN), alarmTimeText(dateTime)); + if (parent->column(AlarmListView::TIME_TO_COLUMN) >= 0) + { + QString tta = timeToAlarmText(now); + setText(parent->column(AlarmListView::TIME_TO_COLUMN), tta); + mTimeToAlarmShown = !tta.isNull(); + } + QTime t = dateTime.time(); + mDateTimeOrder.sprintf("%04d%03d%02d%02d", dateTime.date().year(), dateTime.date().dayOfYear(), + t.hour(), t.minute()); + + int repeatOrder = 0; + int repeatInterval = 0; + QString repeatText = event.recurrenceText(true); // text displayed in Repeat column + if (repeatText.isEmpty()) + repeatText = event.repetitionText(true); + if (event.repeatAtLogin()) + repeatOrder = 1; + else + { + repeatInterval = event.recurInterval(); + switch (event.recurType()) + { + case KARecurrence::MINUTELY: + repeatOrder = 2; + break; + case KARecurrence::DAILY: + repeatOrder = 3; + break; + case KARecurrence::WEEKLY: + repeatOrder = 4; + break; + case KARecurrence::MONTHLY_DAY: + case KARecurrence::MONTHLY_POS: + repeatOrder = 5; + break; + case KARecurrence::ANNUAL_DATE: + case KARecurrence::ANNUAL_POS: + repeatOrder = 6; + break; + case KARecurrence::NO_RECUR: + default: + break; + } + } + setText(parent->column(AlarmListView::REPEAT_COLUMN), repeatText); + mRepeatOrder.sprintf("%c%08d", '0' + repeatOrder, repeatInterval); + + bool showColour = (event.action() == KAEvent::MESSAGE || event.action() == KAEvent::FILE); + mColourOrder.sprintf("%06u", (showColour ? event.bgColour().rgb() : 0)); + + mTypeOrder.sprintf("%02d", event.action()); +} + +/****************************************************************************** +* Return the single line alarm summary text. +*/ +QString AlarmListViewItem::alarmText(const KAEvent& event) const +{ + bool truncated; + QString text = AlarmText::summary(event, 1, &truncated); + mMessageTruncated = truncated; + return text; +} + +/****************************************************************************** +* Return the alarm time text in the form "date time". +*/ +QString AlarmListViewItem::alarmTimeText(const DateTime& dateTime) const +{ + KLocale* locale = KGlobal::locale(); + QString dateTimeText = locale->formatDate(dateTime.date(), true); + if (!dateTime.isDateOnly()) + { + dateTimeText += ' '; + QString time = locale->formatTime(dateTime.time()); + if (mTimeHourPos == -2) + { + // Initialise the position of the hour within the time string, if leading + // zeroes are omitted, so that displayed times can be aligned with each other. + mTimeHourPos = -1; // default = alignment isn't possible/sensible + if (!QApplication::reverseLayout()) // don't try to align right-to-left languages + { + QString fmt = locale->timeFormat(); + int i = fmt.find(QRegExp("%[kl]")); // check if leading zeroes are omitted + if (i >= 0 && i == fmt.find('%')) // and whether the hour is first + mTimeHourPos = i; // yes, so need to align + } + } + if (mTimeHourPos >= 0 && (int)time.length() > mTimeHourPos + 1 + && time[mTimeHourPos].isDigit() && !time[mTimeHourPos + 1].isDigit()) + dateTimeText += '~'; // improve alignment of times with no leading zeroes + dateTimeText += time; + } + return dateTimeText + ' '; +} + +/****************************************************************************** +* Return the time-to-alarm text. +*/ +QString AlarmListViewItem::timeToAlarmText(const QDateTime& now) const +{ + if (event().expired()) + return QString::null; + DateTime dateTime = event().displayDateTime(); + if (dateTime.isDateOnly()) + { + int days = now.date().daysTo(dateTime.date()); + return i18n("n days", " %1d ").arg(days); + } + int mins = (now.secsTo(dateTime.dateTime()) + 59) / 60; + if (mins < 0) + return QString::null; + char minutes[3] = "00"; + minutes[0] = (mins%60) / 10 + '0'; + minutes[1] = (mins%60) % 10 + '0'; + if (mins < 24*60) + return i18n("hours:minutes", " %1:%2 ").arg(mins/60).arg(minutes); + int days = mins / (24*60); + mins = mins % (24*60); + return i18n("days hours:minutes", " %1d %2:%3 ").arg(days).arg(mins/60).arg(minutes); +} + +/****************************************************************************** +* Update the displayed time-to-alarm value. +* The updated value is only displayed if it is different from the existing value, +* or if 'forceDisplay' is true. +*/ +void AlarmListViewItem::updateTimeToAlarm(const QDateTime& now, bool forceDisplay) +{ + if (event().expired()) + { + if (forceDisplay || mTimeToAlarmShown) + { + setText(alarmListView()->column(AlarmListView::TIME_TO_COLUMN), QString::null); + mTimeToAlarmShown = false; + } + } + else + { + QString tta = timeToAlarmText(now); + if (forceDisplay || tta != text(alarmListView()->column(AlarmListView::TIME_TO_COLUMN))) + setText(alarmListView()->column(AlarmListView::TIME_TO_COLUMN), tta); + mTimeToAlarmShown = !tta.isNull(); + } +} + +/****************************************************************************** +* Paint one value in one of the columns in the list view. +*/ +void AlarmListViewItem::paintCell(QPainter* painter, const QColorGroup& cg, int column, int width, int /*align*/) +{ + const AlarmListView* listView = alarmListView(); + int margin = listView->itemMargin(); + QRect box(margin, margin, width - margin*2, height() - margin*2); // area within which to draw + bool selected = isSelected(); + QColor bgColour = selected ? cg.highlight() : cg.base(); + QColor fgColour = selected ? cg.highlightedText() + : !event().enabled() ? Preferences::disabledColour() + : event().expired() ? Preferences::expiredColour() : cg.text(); + painter->setPen(fgColour); + painter->fillRect(0, 0, width, height(), bgColour); + + if (column == listView->column(AlarmListView::TIME_COLUMN)) + { + int i = -1; + QString str = text(column); + if (mTimeHourPos >= 0) + { + // Need to pad out spacing to align times without leading zeroes + i = str.find(" ~"); + if (i >= 0) + { + if (mDigitWidth < 0) + mDigitWidth = painter->fontMetrics().width("0"); + QString date = str.left(i + 1); + int w = painter->fontMetrics().width(date) + mDigitWidth; + painter->drawText(box, AlignVCenter, date); + box.setLeft(box.left() + w); + painter->drawText(box, AlignVCenter, str.mid(i + 2)); + } + } + if (i < 0) + painter->drawText(box, AlignVCenter, str); + } + else if (column == listView->column(AlarmListView::TIME_TO_COLUMN)) + painter->drawText(box, AlignVCenter | AlignRight, text(column)); + else if (column == listView->column(AlarmListView::REPEAT_COLUMN)) + painter->drawText(box, AlignVCenter | AlignHCenter, text(column)); + else if (column == listView->column(AlarmListView::COLOUR_COLUMN)) + { + // Paint the cell the colour of the alarm message + if (event().action() == KAEvent::MESSAGE || event().action() == KAEvent::FILE) + painter->fillRect(box, event().bgColour()); + } + else if (column == listView->column(AlarmListView::TYPE_COLUMN)) + { + // Display the alarm type icon, horizontally and vertically centred in the cell + QPixmap* pixmap = eventIcon(); + QRect pixmapRect = pixmap->rect(); + int diff = box.height() - pixmap->height(); + if (diff < 0) + { + pixmapRect.setTop(-diff / 2); + pixmapRect.setHeight(box.height()); + } + QPoint iconTopLeft(box.left() + (box.width() - pixmapRect.width()) / 2, + box.top() + (diff > 0 ? diff / 2 : 0)); + painter->drawPixmap(iconTopLeft, *pixmap, pixmapRect); + } + else if (column == listView->column(AlarmListView::MESSAGE_COLUMN)) + { + if (!selected && listView->drawMessageInColour()) + { + painter->fillRect(box, event().bgColour()); + painter->setBackgroundColor(event().bgColour()); +// painter->setPen(event().fgColour()); + } + QString txt = text(column); + painter->drawText(box, AlignVCenter, txt); + mMessageColWidth = listView->fontMetrics().boundingRect(txt).width(); + } +} + +/****************************************************************************** +* Return the width needed for the icons in the alarm type column. +*/ +int AlarmListViewItem::typeIconWidth(AlarmListView* v) +{ + return iconWidth() + 2 * v->style().pixelMetric(QStyle::PM_DefaultFrameWidth); +} + +/****************************************************************************** +* Return the column sort order for one item in the list. +*/ +QString AlarmListViewItem::key(int column, bool) const +{ + AlarmListView* listView = alarmListView(); + if (column == listView->column(AlarmListView::TIME_COLUMN) + || column == listView->column(AlarmListView::TIME_TO_COLUMN)) + return mDateTimeOrder; + if (column == listView->column(AlarmListView::REPEAT_COLUMN)) + return mRepeatOrder; + if (column == listView->column(AlarmListView::COLOUR_COLUMN)) + return mColourOrder; + if (column == listView->column(AlarmListView::TYPE_COLUMN)) + return mTypeOrder; + return text(column).lower(); +} + + +/*============================================================================= += Class: AlarmListTooltip += Displays the full alarm text in a tooltip when necessary. +=============================================================================*/ + +/****************************************************************************** +* Displays the full alarm text in a tooltip, if not all the text is displayed. +*/ +void AlarmListTooltip::maybeTip(const QPoint& pt) +{ + AlarmListView* listView = (AlarmListView*)parentWidget()->parentWidget(); + int column = listView->column(AlarmListView::MESSAGE_COLUMN); + int xOffset = listView->contentsX(); + if (listView->header()->sectionAt(pt.x() + xOffset) == column) + { + AlarmListViewItem* item = (AlarmListViewItem*)listView->itemAt(pt); + if (item) + { + int columnX = listView->header()->sectionPos(column) - xOffset; + int columnWidth = listView->columnWidth(column); + int widthNeeded = item->messageColWidthNeeded(); + if (!item->messageTruncated() && columnWidth >= widthNeeded) + { + if (columnX + widthNeeded <= listView->viewport()->width()) + return; + } + QRect rect = listView->itemRect(item); + rect.setLeft(columnX); + rect.setWidth(columnWidth); + kdDebug(5950) << "AlarmListTooltip::maybeTip(): display\n"; + tip(rect, AlarmText::summary(item->event(), 10)); // display up to 10 lines of text + } + } +} + diff --git a/kalarm/alarmlistview.h b/kalarm/alarmlistview.h new file mode 100644 index 000000000..63d5bf0b0 --- /dev/null +++ b/kalarm/alarmlistview.h @@ -0,0 +1,133 @@ +/* + * alarmlistview.h - widget showing list of outstanding alarms + * Program: kalarm + * Copyright © 2001-2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef ALARMLISTVIEW_H +#define ALARMLISTVIEW_H + +#include "kalarm.h" +#include "eventlistviewbase.h" + +class AlarmListView; +class AlarmListTooltip; + + +class AlarmListViewItem : public EventListViewItemBase +{ + public: + AlarmListViewItem(AlarmListView* parent, const KAEvent&, const QDateTime& now); + QTime showTimeToAlarm(bool show); + void updateTimeToAlarm(const QDateTime& now, bool forceDisplay = false); + virtual void paintCell(QPainter*, const QColorGroup&, int column, int width, int align); + AlarmListView* alarmListView() const { return (AlarmListView*)listView(); } + bool messageTruncated() const { return mMessageTruncated; } + int messageColWidthNeeded() const { return mMessageColWidth; } + static int typeIconWidth(AlarmListView*); + // Overridden base class methods + AlarmListViewItem* nextSibling() const { return (AlarmListViewItem*)QListViewItem::nextSibling(); } + virtual QString key(int column, bool ascending) const; + protected: + virtual QString lastColumnText() const { return alarmText(event()); } + private: + QString alarmText(const KAEvent&) const; + QString alarmTimeText(const DateTime&) const; + QString timeToAlarmText(const QDateTime& now) const; + + static int mTimeHourPos; // position of hour within time string, or -1 if leading zeroes included + static int mDigitWidth; // display width of a digit + QString mDateTimeOrder; // controls ordering of date/time column + QString mRepeatOrder; // controls ordering of repeat column + QString mColourOrder; // controls ordering of colour column + QString mTypeOrder; // controls ordering of alarm type column + mutable int mMessageColWidth; // width needed to display complete message text + mutable bool mMessageTruncated; // multi-line message text has been truncated for the display + bool mTimeToAlarmShown; // relative alarm time is displayed +}; + + +class AlarmListView : public EventListViewBase +{ + Q_OBJECT // needed by QObject::isA() calls + public: + enum ColumnIndex { // default column order + TIME_COLUMN, TIME_TO_COLUMN, REPEAT_COLUMN, COLOUR_COLUMN, TYPE_COLUMN, MESSAGE_COLUMN, + COLUMN_COUNT + }; + + AlarmListView(const QValueList<int>& order, QWidget* parent = 0, const char* name = 0); + ~AlarmListView(); + void showExpired(bool show) { mShowExpired = show; } + bool showingExpired() const { return mShowExpired; } + bool showingTimeTo() const { return columnWidth(mColumn[TIME_TO_COLUMN]); } + void selectTimeColumns(bool time, bool timeTo); + void updateTimeToAlarms(bool forceDisplay = false); + bool expired(AlarmListViewItem*) const; + bool drawMessageInColour() const { return mDrawMessageInColour; } + void setDrawMessageInColour(bool inColour) { mDrawMessageInColour = inColour; } + QValueList<int> columnOrder() const; + int column(ColumnIndex i) const { return mColumn[i]; } + static bool dragging() { return mDragging; } + // Overridden base class methods + static void addEvent(const KAEvent&, EventListViewBase*); + static void modifyEvent(const KAEvent& e, EventListViewBase* selectionView) + { EventListViewBase::modifyEvent(e.id(), e, mInstanceList, selectionView); } + static void modifyEvent(const QString& oldEventID, const KAEvent& newEvent, EventListViewBase* selectionView) + { EventListViewBase::modifyEvent(oldEventID, newEvent, mInstanceList, selectionView); } + static void deleteEvent(const QString& eventID) + { EventListViewBase::deleteEvent(eventID, mInstanceList); } + static void undeleteEvent(const QString& oldEventID, const KAEvent& event, EventListViewBase* selectionView) + { EventListViewBase::modifyEvent(oldEventID, event, mInstanceList, selectionView); } + AlarmListViewItem* getEntry(const QString& eventID) { return (AlarmListViewItem*)EventListViewBase::getEntry(eventID); } + AlarmListViewItem* currentItem() const { return (AlarmListViewItem*)EventListViewBase::currentItem(); } + AlarmListViewItem* firstChild() const { return (AlarmListViewItem*)EventListViewBase::firstChild(); } + AlarmListViewItem* selectedItem() const { return (AlarmListViewItem*)EventListViewBase::selectedItem(); } + virtual void setSelected(QListViewItem* item, bool selected) { EventListViewBase::setSelected(item, selected); } + virtual void setSelected(AlarmListViewItem* item, bool selected) { EventListViewBase::setSelected(item, selected); } + virtual InstanceList instances() { return mInstanceList; } + + protected: + virtual void populate(); + EventListViewItemBase* createItem(const KAEvent&); + virtual QString whatsThisText(int column) const; + virtual bool shouldShowEvent(const KAEvent& e) const { return mShowExpired || !e.expired(); } + AlarmListViewItem* addEntry(const KAEvent& e, bool setSize = false) + { return addEntry(e, QDateTime::currentDateTime(), setSize); } + AlarmListViewItem* updateEntry(AlarmListViewItem* item, const KAEvent& newEvent, bool setSize = false) + { return (AlarmListViewItem*)EventListViewBase::updateEntry(item, newEvent, setSize); } + virtual void contentsMousePressEvent(QMouseEvent*); + virtual void contentsMouseMoveEvent(QMouseEvent*); + virtual void contentsMouseReleaseEvent(QMouseEvent*); + + private: + AlarmListViewItem* addEntry(const KAEvent&, const QDateTime& now, bool setSize = false, bool reselect = false); + + static InstanceList mInstanceList; // all current instances of this class + static bool mDragging; + int mColumn[COLUMN_COUNT]; // initial position of each column + int mTimeColumnHeaderWidth; + int mTimeToColumnHeaderWidth; + AlarmListTooltip* mTooltip; // tooltip for showing full text of alarm messages + QPoint mMousePressPos; // where the mouse left button was last pressed + bool mMousePressed; // true while the mouse left button is pressed + bool mDrawMessageInColour; + bool mShowExpired; // true to show expired alarms +}; + +#endif // ALARMLISTVIEW_H + diff --git a/kalarm/alarmtext.cpp b/kalarm/alarmtext.cpp new file mode 100644 index 000000000..2162f62a4 --- /dev/null +++ b/kalarm/alarmtext.cpp @@ -0,0 +1,288 @@ +/* + * alarmtext.cpp - text/email alarm text conversion + * Program: kalarm + * Copyright (C) 2004, 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" +#include <qstringlist.h> +#include <klocale.h> + +#include "alarmevent.h" +#include "editdlg.h" +#include "alarmtext.h" + + +QString AlarmText::mFromPrefix; +QString AlarmText::mToPrefix; +QString AlarmText::mCcPrefix; +QString AlarmText::mDatePrefix; +QString AlarmText::mSubjectPrefix; +QString AlarmText::mFromPrefixEn = QString::fromLatin1("From:"); +QString AlarmText::mToPrefixEn = QString::fromLatin1("To:"); +QString AlarmText::mCcPrefixEn = QString::fromLatin1("Cc:"); +QString AlarmText::mDatePrefixEn = QString::fromLatin1("Date:"); +QString AlarmText::mSubjectPrefixEn = QString::fromLatin1("Subject:"); + + +void AlarmText::setText(const QString& text) +{ + mBody = text; + mIsScript = text.startsWith(QString::fromLatin1("#!")); + mIsEmail = false; + mTo = mFrom = mCc = mTime = mSubject = QString::null; + mKMailSerialNum = 0; +} + +void AlarmText::setEmail(const QString& to, const QString& from, const QString& cc, const QString& time, + const QString& subject, const QString& body, unsigned long kmailSerialNumber) +{ + mIsScript = false; + mIsEmail = true; + mTo = to; + mFrom = from; + mCc = cc; + mTime = time; + mSubject = subject; + mBody = body; + mKMailSerialNum = kmailSerialNumber; +} + +/****************************************************************************** +* Return the text for a text message alarm, in display format. +*/ +QString AlarmText::displayText() const +{ + if (mIsEmail) + { + // Format the email into a text alarm + setUpTranslations(); + QString text; + text = mFromPrefix + '\t' + mFrom + '\n'; + text += mToPrefix + '\t' + mTo + '\n'; + if (!mCc.isEmpty()) + text += mCcPrefix + '\t' + mCc + '\n'; + if (!mTime.isEmpty()) + text += mDatePrefix + '\t' + mTime + '\n'; + text += mSubjectPrefix + '\t' + mSubject; + if (!mBody.isEmpty()) + { + text += "\n\n"; + text += mBody; + } + return text; + } + return mBody; +} + +/****************************************************************************** +* Return whether there is any text. +*/ +bool AlarmText::isEmpty() const +{ + if (!mBody.isEmpty()) + return false; + if (!mIsEmail) + return true; + return mFrom.isEmpty() && mTo.isEmpty() && mCc.isEmpty() && mTime.isEmpty() && mSubject.isEmpty(); +} + +/****************************************************************************** +* Check whether a text is an email. +*/ +bool AlarmText::checkIfEmail(const QString& text) +{ + QStringList lines = QStringList::split('\n', text); + return emailHeaderCount(lines); +} + +/****************************************************************************** +* Check whether a text is an email. +* Reply = number of email header lines, or 0 if not an email. +*/ +int AlarmText::emailHeaderCount(const QStringList& lines) +{ + setUpTranslations(); + int maxn = lines.count(); + if (maxn >= 4 + && lines[0].startsWith(mFromPrefix) + && lines[1].startsWith(mToPrefix)) + { + int n = 2; + if (lines[2].startsWith(mCcPrefix)) + ++n; + if (maxn > n + 1 + && lines[n].startsWith(mDatePrefix) + && lines[n+1].startsWith(mSubjectPrefix)) + return n+2; + } + return 0; +} + +/****************************************************************************** +* Check whether a text is an email, and if so return its headers or optionally +* only its subject line. +* Reply = headers/subject line, or QString::null if not the text of an email. +*/ +QString AlarmText::emailHeaders(const QString& text, bool subjectOnly) +{ + QStringList lines = QStringList::split('\n', text); + int n = emailHeaderCount(lines); + if (!n) + return QString::null; + if (subjectOnly) + return lines[n-1].mid(mSubjectPrefix.length()).stripWhiteSpace(); + QString h = lines[0]; + for (int i = 1; i < n; ++i) + { + h += '\n'; + h += lines[i]; + } + return h; +} + +/****************************************************************************** +* Translate an alarm calendar text to a display text. +* Translation is needed for email texts, since the alarm calendar stores +* untranslated email prefixes. +* 'email' is set to indicate whether it is an email text. +*/ +QString AlarmText::fromCalendarText(const QString& text, bool& email) +{ + QStringList lines = QStringList::split('\n', text); + int maxn = lines.count(); + if (maxn >= 4 + && lines[0].startsWith(mFromPrefixEn) + && lines[1].startsWith(mToPrefixEn)) + { + int n = 2; + if (lines[2].startsWith(mCcPrefixEn)) + ++n; + if (maxn > n + 1 + && lines[n].startsWith(mDatePrefixEn) + && lines[n+1].startsWith(mSubjectPrefixEn)) + { + setUpTranslations(); + QString dispText; + dispText = mFromPrefix + lines[0].mid(mFromPrefixEn.length()) + '\n'; + dispText += mToPrefix + lines[1].mid(mToPrefixEn.length()) + '\n'; + if (n == 3) + dispText += mCcPrefix + lines[2].mid(mCcPrefixEn.length()) + '\n'; + dispText += mDatePrefix + lines[n].mid(mDatePrefixEn.length()) + '\n'; + dispText += mSubjectPrefix + lines[n+1].mid(mSubjectPrefixEn.length()); + int i = text.find(mSubjectPrefixEn); + i = text.find('\n', i); + if (i > 0) + dispText += text.mid(i); + email = true; + return dispText; + } + } + email = false; + return text; +} + +/****************************************************************************** +* Return the text for a text message alarm, in alarm calendar format. +* (The prefix strings are untranslated in the calendar.) +*/ +QString AlarmText::toCalendarText(const QString& text) +{ + setUpTranslations(); + QStringList lines = QStringList::split('\n', text); + int maxn = lines.count(); + if (maxn >= 4 + && lines[0].startsWith(mFromPrefix) + && lines[1].startsWith(mToPrefix)) + { + int n = 2; + if (lines[2].startsWith(mCcPrefix)) + ++n; + if (maxn > n + 1 + && lines[n].startsWith(mDatePrefix) + && lines[n+1].startsWith(mSubjectPrefix)) + { + // Format the email into a text alarm + QString calText; + calText = mFromPrefixEn + lines[0].mid(mFromPrefix.length()) + '\n'; + calText += mToPrefixEn + lines[1].mid(mToPrefix.length()) + '\n'; + if (n == 3) + calText += mCcPrefixEn + lines[2].mid(mCcPrefix.length()) + '\n'; + calText += mDatePrefixEn + lines[n].mid(mDatePrefix.length()) + '\n'; + calText += mSubjectPrefixEn + lines[n+1].mid(mSubjectPrefix.length()); + int i = text.find(mSubjectPrefix); + i = text.find('\n', i); + if (i > 0) + calText += text.mid(i); + return calText; + } + } + return text; +} + +/****************************************************************************** +* Set up messages used by executeDropEvent() and emailHeaders(). +*/ +void AlarmText::setUpTranslations() +{ + if (mFromPrefix.isNull()) + { + mFromPrefix = EditAlarmDlg::i18n_EmailFrom(); + mToPrefix = EditAlarmDlg::i18n_EmailTo(); + mCcPrefix = i18n("Copy-to in email headers", "Cc:"); + mDatePrefix = i18n("Date:"); + mSubjectPrefix = EditAlarmDlg::i18n_EmailSubject(); + } +} + +/****************************************************************************** +* Return the alarm summary text for either single line or tooltip display. +* The maximum number of line returned is determined by 'maxLines'. +* If 'truncated' is non-null, it will be set true if the text returned has been +* truncated, other than to strip a trailing newline. +*/ +QString AlarmText::summary(const KAEvent& event, int maxLines, bool* truncated) +{ + QString text = (event.action() == KAEvent::EMAIL) ? event.emailSubject() : event.cleanText(); + if (event.action() == KAEvent::MESSAGE) + { + // If the message is the text of an email, return its headers or just subject line + QString subject = emailHeaders(text, (maxLines <= 1)); + if (!subject.isNull()) + { + if (truncated) + *truncated = true; + return subject; + } + } + if (truncated) + *truncated = false; + if (text.contains('\n') < maxLines) + return text; + int newline = -1; + for (int i = 0; i < maxLines; ++i) + { + newline = text.find('\n', newline + 1); + if (newline < 0) + return text; // not truncated after all !?! + } + if (newline == static_cast<int>(text.length()) - 1) + return text.left(newline); // text ends in newline + if (truncated) + *truncated = true; + return text.left(newline + (maxLines <= 1 ? 0 : 1)) + QString::fromLatin1("..."); +} diff --git a/kalarm/alarmtext.h b/kalarm/alarmtext.h new file mode 100644 index 000000000..8d643b3cb --- /dev/null +++ b/kalarm/alarmtext.h @@ -0,0 +1,75 @@ +/* + * alarmtext.h - text/email alarm text conversion + * Program: kalarm + * Copyright (C) 2004, 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef ALARMTEXT_H +#define ALARMTEXT_H + +#include <qstring.h> +class QStringList; +class KAEvent; + + +class AlarmText +{ + public: + AlarmText(const QString& text = QString::null) { setText(text); } + void setText(const QString&); + void setScript(const QString& text) { setText(text); mIsScript = true; } + void setEmail(const QString& to, const QString& from, const QString& cc, const QString& time, + const QString& subject, const QString& body, unsigned long kmailSerialNumber = 0); + QString displayText() const; + QString calendarText() const; + QString to() const { return mTo; } + QString from() const { return mFrom; } + QString cc() const { return mCc; } + QString time() const { return mTime; } + QString subject() const { return mSubject; } + QString body() const { return mIsEmail ? mBody : QString::null; } + bool isEmpty() const; + bool isEmail() const { return mIsEmail; } + bool isScript() const { return mIsScript; } + unsigned long kmailSerialNumber() const { return mKMailSerialNum; } + static QString summary(const KAEvent&, int maxLines = 1, bool* truncated = 0); + static bool checkIfEmail(const QString&); + static QString emailHeaders(const QString&, bool subjectOnly); + static QString fromCalendarText(const QString&, bool& email); + static QString toCalendarText(const QString&); + + private: + static void setUpTranslations(); + static int emailHeaderCount(const QStringList&); + + static QString mFromPrefix; // translated header prefixes + static QString mToPrefix; + static QString mCcPrefix; + static QString mDatePrefix; + static QString mSubjectPrefix; + static QString mFromPrefixEn; // untranslated header prefixes + static QString mToPrefixEn; + static QString mCcPrefixEn; + static QString mDatePrefixEn; + static QString mSubjectPrefixEn; + QString mBody, mFrom, mTo, mCc, mTime, mSubject; + unsigned long mKMailSerialNum; // if email, message's KMail serial number, else 0 + bool mIsEmail; + bool mIsScript; +}; + +#endif // ALARMTEXT_H diff --git a/kalarm/alarmtimewidget.cpp b/kalarm/alarmtimewidget.cpp new file mode 100644 index 000000000..1e7e777b0 --- /dev/null +++ b/kalarm/alarmtimewidget.cpp @@ -0,0 +1,556 @@ +/* + * alarmtimewidget.cpp - alarm date/time entry widget + * Program: kalarm + * Copyright © 2001-2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qlayout.h> +#include <qgroupbox.h> +#include <qhbox.h> +#include <qpushbutton.h> +#include <qwhatsthis.h> + +#include <kdialog.h> +#include <kmessagebox.h> +#include <klocale.h> + +#include "checkbox.h" +#include "dateedit.h" +#include "datetime.h" +#include "radiobutton.h" +#include "synchtimer.h" +#include "timeedit.h" +#include "timespinbox.h" +#include "alarmtimewidget.moc" + +static const QTime time_23_59(23, 59); + + +const int AlarmTimeWidget::maxDelayTime = 999*60 + 59; // < 1000 hours + +QString AlarmTimeWidget::i18n_w_TimeFromNow() { return i18n("Time from no&w:"); } +QString AlarmTimeWidget::i18n_TimeAfterPeriod() +{ + return i18n("Enter the length of time (in hours and minutes) after " + "the current time to schedule the alarm."); +} + + +/****************************************************************************** +* Construct a widget with a group box and title. +*/ +AlarmTimeWidget::AlarmTimeWidget(const QString& groupBoxTitle, int mode, QWidget* parent, const char* name) + : ButtonGroup(groupBoxTitle, parent, name), + mMinDateTimeIsNow(false), + mPastMax(false), + mMinMaxTimeSet(false) +{ + init(mode); +} + +/****************************************************************************** +* Construct a widget without a group box or title. +*/ +AlarmTimeWidget::AlarmTimeWidget(int mode, QWidget* parent, const char* name) + : ButtonGroup(parent, name), + mMinDateTimeIsNow(false), + mPastMax(false), + mMinMaxTimeSet(false) +{ + setFrameStyle(QFrame::NoFrame); + init(mode); +} + +void AlarmTimeWidget::init(int mode) +{ + static const QString recurText = i18n("For a simple repetition, enter the date/time of the first occurrence.\n" + "If a recurrence is configured, the start date/time will be adjusted " + "to the first recurrence on or after the entered date/time."); + + connect(this, SIGNAL(buttonSet(int)), SLOT(slotButtonSet(int))); + QBoxLayout* topLayout = new QVBoxLayout(this, 0, KDialog::spacingHint()); + if (!title().isEmpty()) + { + topLayout->setMargin(KDialog::marginHint()); + topLayout->addSpacing(fontMetrics().lineSpacing()/2); + } + + // At time radio button/label + mAtTimeRadio = new RadioButton(((mode & DEFER_TIME) ? i18n("&Defer to date/time:") : i18n("At &date/time:")), this, "atTimeRadio"); + mAtTimeRadio->setFixedSize(mAtTimeRadio->sizeHint()); + QWhatsThis::add(mAtTimeRadio, + ((mode & DEFER_TIME) ? i18n("Reschedule the alarm to the specified date and time.") + : i18n("Schedule the alarm at the specified date and time."))); + + // Date edit box + mDateEdit = new DateEdit(this); + mDateEdit->setFixedSize(mDateEdit->sizeHint()); + connect(mDateEdit, SIGNAL(dateEntered(const QDate&)), SLOT(dateTimeChanged())); + static const QString enterDateText = i18n("Enter the date to schedule the alarm."); + QWhatsThis::add(mDateEdit, ((mode & DEFER_TIME) ? enterDateText + : QString("%1\n%2").arg(enterDateText).arg(recurText))); + mAtTimeRadio->setFocusWidget(mDateEdit); + + // Time edit box and Any time checkbox + QHBox* timeBox = new QHBox(this); + timeBox->setSpacing(2*KDialog::spacingHint()); + mTimeEdit = new TimeEdit(timeBox); + mTimeEdit->setFixedSize(mTimeEdit->sizeHint()); + connect(mTimeEdit, SIGNAL(valueChanged(int)), SLOT(dateTimeChanged())); + static const QString enterTimeText = i18n("Enter the time to schedule the alarm."); + QWhatsThis::add(mTimeEdit, + ((mode & DEFER_TIME) ? QString("%1\n\n%2").arg(enterTimeText).arg(TimeSpinBox::shiftWhatsThis()) + : QString("%1\n%2\n\n%3").arg(enterTimeText).arg(recurText).arg(TimeSpinBox::shiftWhatsThis()))); + + mAnyTime = -1; // current status is uninitialised + if (mode & DEFER_TIME) + { + mAnyTimeAllowed = false; + mAnyTimeCheckBox = 0; + } + else + { + mAnyTimeAllowed = true; + mAnyTimeCheckBox = new CheckBox(i18n("An&y time"), timeBox); + mAnyTimeCheckBox->setFixedSize(mAnyTimeCheckBox->sizeHint()); + connect(mAnyTimeCheckBox, SIGNAL(toggled(bool)), SLOT(slotAnyTimeToggled(bool))); + QWhatsThis::add(mAnyTimeCheckBox, i18n("Schedule the alarm for any time during the day")); + } + + // 'Time from now' radio button/label + mAfterTimeRadio = new RadioButton(((mode & DEFER_TIME) ? i18n("Defer for time &interval:") : i18n_w_TimeFromNow()), + this, "afterTimeRadio"); + mAfterTimeRadio->setFixedSize(mAfterTimeRadio->sizeHint()); + QWhatsThis::add(mAfterTimeRadio, + ((mode & DEFER_TIME) ? i18n("Reschedule the alarm for the specified time interval after now.") + : i18n("Schedule the alarm after the specified time interval from now."))); + + // Delay time spin box + mDelayTimeEdit = new TimeSpinBox(1, maxDelayTime, this); + mDelayTimeEdit->setValue(maxDelayTime); + mDelayTimeEdit->setFixedSize(mDelayTimeEdit->sizeHint()); + connect(mDelayTimeEdit, SIGNAL(valueChanged(int)), SLOT(delayTimeChanged(int))); + QWhatsThis::add(mDelayTimeEdit, + ((mode & DEFER_TIME) ? QString("%1\n\n%2").arg(i18n_TimeAfterPeriod()).arg(TimeSpinBox::shiftWhatsThis()) + : QString("%1\n%2\n\n%3").arg(i18n_TimeAfterPeriod()).arg(recurText).arg(TimeSpinBox::shiftWhatsThis()))); + mAfterTimeRadio->setFocusWidget(mDelayTimeEdit); + + // Set up the layout, either narrow or wide + if (mode & NARROW) + { + QGridLayout* grid = new QGridLayout(topLayout, 2, 2, KDialog::spacingHint()); + grid->addWidget(mAtTimeRadio, 0, 0); + grid->addWidget(mDateEdit, 0, 1, Qt::AlignAuto); + grid->addWidget(timeBox, 1, 1, Qt::AlignAuto); + grid->setColStretch(2, 1); + topLayout->addStretch(); + QBoxLayout* layout = new QHBoxLayout(topLayout, KDialog::spacingHint()); + layout->addWidget(mAfterTimeRadio); + layout->addWidget(mDelayTimeEdit); + layout->addStretch(); + } + else + { + QGridLayout* grid = new QGridLayout(topLayout, 2, 3, KDialog::spacingHint()); + grid->addWidget(mAtTimeRadio, 0, 0, Qt::AlignAuto); + grid->addWidget(mDateEdit, 0, 1, Qt::AlignAuto); + grid->addWidget(timeBox, 0, 2, Qt::AlignAuto); + grid->setRowStretch(0, 1); + grid->addWidget(mAfterTimeRadio, 1, 0, Qt::AlignAuto); + grid->addWidget(mDelayTimeEdit, 1, 1, Qt::AlignAuto); + grid->setColStretch(3, 1); + topLayout->addStretch(); + } + + // Initialise the radio button statuses + setButton(id(mAtTimeRadio)); + + // Timeout every minute to update alarm time fields. + MinuteTimer::connect(this, SLOT(slotTimer())); +} + +/****************************************************************************** +* Set or clear read-only status for the controls +*/ +void AlarmTimeWidget::setReadOnly(bool ro) +{ + mAtTimeRadio->setReadOnly(ro); + mDateEdit->setReadOnly(ro); + mTimeEdit->setReadOnly(ro); + if (mAnyTimeCheckBox) + mAnyTimeCheckBox->setReadOnly(ro); + mAfterTimeRadio->setReadOnly(ro); + mDelayTimeEdit->setReadOnly(ro); +} + +/****************************************************************************** +* Select the "Time from now" radio button. +*/ +void AlarmTimeWidget::selectTimeFromNow(int minutes) +{ + mAfterTimeRadio->setChecked(true); + slotButtonSet(1); + if (minutes > 0) + mDelayTimeEdit->setValue(minutes); +} + +/****************************************************************************** +* Fetch the entered date/time. +* If 'checkExpired' is true and the entered value <= current time, an error occurs. +* If 'minsFromNow' is non-null, it is set to the number of minutes' delay selected, +* or to zero if a date/time was entered. +* In this case, if 'showErrorMessage' is true, output an error message. +* 'errorWidget' if non-null, is set to point to the widget containing the error. +* Reply = invalid date/time if error. +*/ +DateTime AlarmTimeWidget::getDateTime(int* minsFromNow, bool checkExpired, bool showErrorMessage, QWidget** errorWidget) const +{ + if (minsFromNow) + *minsFromNow = 0; + if (errorWidget) + *errorWidget = 0; + QTime nowt = QTime::currentTime(); + QDateTime now(QDate::currentDate(), QTime(nowt.hour(), nowt.minute())); + if (mAtTimeRadio->isOn()) + { + bool anyTime = mAnyTimeAllowed && mAnyTimeCheckBox && mAnyTimeCheckBox->isChecked(); + if (!mDateEdit->isValid() || !mTimeEdit->isValid()) + { + // The date and/or time is invalid + if (!mDateEdit->isValid()) + { + if (showErrorMessage) + KMessageBox::sorry(const_cast<AlarmTimeWidget*>(this), i18n("Invalid date")); + if (errorWidget) + *errorWidget = mDateEdit; + } + else + { + if (showErrorMessage) + KMessageBox::sorry(const_cast<AlarmTimeWidget*>(this), i18n("Invalid time")); + if (errorWidget) + *errorWidget = mTimeEdit; + } + return DateTime(); + } + + DateTime result; + if (anyTime) + { + result = mDateEdit->date(); + if (checkExpired && result.date() < now.date()) + { + if (showErrorMessage) + KMessageBox::sorry(const_cast<AlarmTimeWidget*>(this), i18n("Alarm date has already expired")); + if (errorWidget) + *errorWidget = mDateEdit; + return DateTime(); + } + } + else + { + result.set(mDateEdit->date(), mTimeEdit->time()); + if (checkExpired && result <= now.addSecs(1)) + { + if (showErrorMessage) + KMessageBox::sorry(const_cast<AlarmTimeWidget*>(this), i18n("Alarm time has already expired")); + if (errorWidget) + *errorWidget = mTimeEdit; + return DateTime(); + } + } + return result; + } + else + { + if (!mDelayTimeEdit->isValid()) + { + if (showErrorMessage) + KMessageBox::sorry(const_cast<AlarmTimeWidget*>(this), i18n("Invalid time")); + if (errorWidget) + *errorWidget = mDelayTimeEdit; + return DateTime(); + } + int delayMins = mDelayTimeEdit->value(); + if (minsFromNow) + *minsFromNow = delayMins; + return now.addSecs(delayMins * 60); + } +} + +/****************************************************************************** +* Set the date/time. +*/ +void AlarmTimeWidget::setDateTime(const DateTime& dt) +{ + if (dt.date().isValid()) + { + mTimeEdit->setValue(dt.time()); + mDateEdit->setDate(dt.date()); + dateTimeChanged(); // update the delay time edit box + } + else + { + mTimeEdit->setValid(false); + mDateEdit->setInvalid(); + mDelayTimeEdit->setValid(false); + } + if (mAnyTimeCheckBox) + { + bool anyTime = dt.isDateOnly(); + if (anyTime) + mAnyTimeAllowed = true; + mAnyTimeCheckBox->setChecked(anyTime); + setAnyTime(); + } +} + +/****************************************************************************** +* Set the minimum date/time to track the current time. +*/ +void AlarmTimeWidget::setMinDateTimeIsCurrent() +{ + mMinDateTimeIsNow = true; + mMinDateTime = QDateTime(); + QDateTime now = QDateTime::currentDateTime(); + mDateEdit->setMinDate(now.date()); + setMaxMinTimeIf(now); +} + +/****************************************************************************** +* Set the minimum date/time, adjusting the entered date/time if necessary. +* If 'dt' is invalid, any current minimum date/time is cleared. +*/ +void AlarmTimeWidget::setMinDateTime(const QDateTime& dt) +{ + mMinDateTimeIsNow = false; + mMinDateTime = dt; + mDateEdit->setMinDate(dt.date()); + setMaxMinTimeIf(QDateTime::currentDateTime()); +} + +/****************************************************************************** +* Set the maximum date/time, adjusting the entered date/time if necessary. +* If 'dt' is invalid, any current maximum date/time is cleared. +*/ +void AlarmTimeWidget::setMaxDateTime(const DateTime& dt) +{ + mPastMax = false; + if (dt.isValid() && dt.isDateOnly()) + mMaxDateTime = dt.dateTime().addSecs(24*3600 - 60); + else + mMaxDateTime = dt.dateTime(); + mDateEdit->setMaxDate(mMaxDateTime.date()); + QDateTime now = QDateTime::currentDateTime(); + setMaxMinTimeIf(now); + setMaxDelayTime(now); +} + +/****************************************************************************** +* If the minimum and maximum date/times fall on the same date, set the minimum +* and maximum times in the time edit box. +*/ +void AlarmTimeWidget::setMaxMinTimeIf(const QDateTime& now) +{ + int mint = 0; + QTime maxt = time_23_59; + mMinMaxTimeSet = false; + if (mMaxDateTime.isValid()) + { + bool set = true; + QDateTime minDT; + if (mMinDateTimeIsNow) + minDT = now.addSecs(60); + else if (mMinDateTime.isValid()) + minDT = mMinDateTime; + else + set = false; + if (set && mMaxDateTime.date() == minDT.date()) + { + // The minimum and maximum times are on the same date, so + // constrain the time value. + mint = minDT.time().hour()*60 + minDT.time().minute(); + maxt = mMaxDateTime.time(); + mMinMaxTimeSet = true; + } + } + mTimeEdit->setMinValue(mint); + mTimeEdit->setMaxValue(maxt); + mTimeEdit->setWrapping(!mint && maxt == time_23_59); +} + +/****************************************************************************** +* Set the maximum value for the delay time edit box, depending on the maximum +* value for the date/time. +*/ +void AlarmTimeWidget::setMaxDelayTime(const QDateTime& now) +{ + int maxVal = maxDelayTime; + if (mMaxDateTime.isValid()) + { + if (now.date().daysTo(mMaxDateTime.date()) < 100) // avoid possible 32-bit overflow on secsTo() + { + QDateTime dt(now.date(), QTime(now.time().hour(), now.time().minute(), 0)); // round down to nearest minute + maxVal = dt.secsTo(mMaxDateTime) / 60; + if (maxVal > maxDelayTime) + maxVal = maxDelayTime; + } + } + mDelayTimeEdit->setMaxValue(maxVal); +} + +/****************************************************************************** +* Set the status for whether a time is specified, or just a date. +*/ +void AlarmTimeWidget::setAnyTime() +{ + int old = mAnyTime; + mAnyTime = (mAtTimeRadio->isOn() && mAnyTimeAllowed && mAnyTimeCheckBox && mAnyTimeCheckBox->isChecked()) ? 1 : 0; + if (mAnyTime != old) + emit anyTimeToggled(mAnyTime); +} + +/****************************************************************************** +* Enable/disable the "any time" checkbox. +*/ +void AlarmTimeWidget::enableAnyTime(bool enable) +{ + if (mAnyTimeCheckBox) + { + mAnyTimeAllowed = enable; + bool at = mAtTimeRadio->isOn(); + mAnyTimeCheckBox->setEnabled(enable && at); + if (at) + mTimeEdit->setEnabled(!enable || !mAnyTimeCheckBox->isChecked()); + setAnyTime(); + } +} + +/****************************************************************************** +* Called every minute to update the alarm time data entry fields. +* If the maximum date/time has been reached, a 'pastMax()' signal is emitted. +*/ +void AlarmTimeWidget::slotTimer() +{ + QDateTime now; + if (mMinDateTimeIsNow) + { + // Make sure that the minimum date is updated when the day changes + now = QDateTime::currentDateTime(); + mDateEdit->setMinDate(now.date()); + } + if (mMaxDateTime.isValid()) + { + if (!now.isValid()) + now = QDateTime::currentDateTime(); + if (!mPastMax) + { + // Check whether the maximum date/time has now been reached + if (now.date() >= mMaxDateTime.date()) + { + // The current date has reached or has passed the maximum date + if (now.date() > mMaxDateTime.date() + || !mAnyTime && now.time() > mTimeEdit->maxTime()) + { + mPastMax = true; + emit pastMax(); + } + else if (mMinDateTimeIsNow && !mMinMaxTimeSet) + { + // The minimum date/time tracks the clock, so set the minimum + // and maximum times + setMaxMinTimeIf(now); + } + } + } + setMaxDelayTime(now); + } + + if (mAtTimeRadio->isOn()) + dateTimeChanged(); + else + delayTimeChanged(mDelayTimeEdit->value()); +} + + +/****************************************************************************** +* Called when the At or After time radio button states have been set. +* Updates the appropriate edit box. +*/ +void AlarmTimeWidget::slotButtonSet(int) +{ + bool at = mAtTimeRadio->isOn(); + mDateEdit->setEnabled(at); + mTimeEdit->setEnabled(at && (!mAnyTimeAllowed || !mAnyTimeCheckBox || !mAnyTimeCheckBox->isChecked())); + if (mAnyTimeCheckBox) + mAnyTimeCheckBox->setEnabled(at && mAnyTimeAllowed); + // Ensure that the value of the delay edit box is > 0. + QDateTime dt(mDateEdit->date(), mTimeEdit->time()); + int minutes = (QDateTime::currentDateTime().secsTo(dt) + 59) / 60; + if (minutes <= 0) + mDelayTimeEdit->setValid(true); + mDelayTimeEdit->setEnabled(!at); + setAnyTime(); +} + +/****************************************************************************** +* Called after the mAnyTimeCheckBox checkbox has been toggled. +*/ +void AlarmTimeWidget::slotAnyTimeToggled(bool on) +{ + mTimeEdit->setEnabled((!mAnyTimeAllowed || !on) && mAtTimeRadio->isOn()); + setAnyTime(); +} + +/****************************************************************************** +* Called when the date or time edit box values have changed. +* Updates the time delay edit box accordingly. +*/ +void AlarmTimeWidget::dateTimeChanged() +{ + QDateTime dt(mDateEdit->date(), mTimeEdit->time()); + int minutes = (QDateTime::currentDateTime().secsTo(dt) + 59) / 60; + bool blocked = mDelayTimeEdit->signalsBlocked(); + mDelayTimeEdit->blockSignals(true); // prevent infinite recursion between here and delayTimeChanged() + if (minutes <= 0 || minutes > mDelayTimeEdit->maxValue()) + mDelayTimeEdit->setValid(false); + else + mDelayTimeEdit->setValue(minutes); + mDelayTimeEdit->blockSignals(blocked); +} + +/****************************************************************************** +* Called when the delay time edit box value has changed. +* Updates the Date and Time edit boxes accordingly. +*/ +void AlarmTimeWidget::delayTimeChanged(int minutes) +{ + if (mDelayTimeEdit->isValid()) + { + QDateTime dt = QDateTime::currentDateTime().addSecs(minutes * 60); + bool blockedT = mTimeEdit->signalsBlocked(); + bool blockedD = mDateEdit->signalsBlocked(); + mTimeEdit->blockSignals(true); // prevent infinite recursion between here and dateTimeChanged() + mDateEdit->blockSignals(true); + mTimeEdit->setValue(dt.time()); + mDateEdit->setDate(dt.date()); + mTimeEdit->blockSignals(blockedT); + mDateEdit->blockSignals(blockedD); + } +} diff --git a/kalarm/alarmtimewidget.h b/kalarm/alarmtimewidget.h new file mode 100644 index 000000000..0a93e9cf4 --- /dev/null +++ b/kalarm/alarmtimewidget.h @@ -0,0 +1,94 @@ +/* + * alarmtimewidget.h - alarm date/time entry widget + * Program: kalarm + * Copyright © 2001-2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef ALARMTIMEWIDGET_H +#define ALARMTIMEWIDGET_H + +#include "buttongroup.h" +#include "datetime.h" + +class RadioButton; +class CheckBox; +class DateEdit; +class TimeEdit; +class TimeSpinBox; + + +class AlarmTimeWidget : public ButtonGroup +{ + Q_OBJECT + public: + enum { // 'mode' values for constructor. May be OR'ed together. + AT_TIME = 0x00, // "At ..." + DEFER_TIME = 0x01, // "Defer to ..." + NARROW = 0x02 // make a narrow widget + }; + AlarmTimeWidget(const QString& groupBoxTitle, int mode, QWidget* parent = 0, const char* name = 0); + AlarmTimeWidget(int mode, QWidget* parent = 0, const char* name = 0); + DateTime getDateTime(int* minsFromNow = 0, bool checkExpired = true, bool showErrorMessage = true, QWidget** errorWidget = 0) const; + void setDateTime(const DateTime&); + void setMinDateTimeIsCurrent(); + void setMinDateTime(const QDateTime& = QDateTime()); + void setMaxDateTime(const DateTime& = DateTime()); + const QDateTime& maxDateTime() const { return mMaxDateTime; } + void setReadOnly(bool); + bool anyTime() const { return mAnyTime; } + void enableAnyTime(bool enable); + void selectTimeFromNow(int minutes = 0); + QSize sizeHint() const { return minimumSizeHint(); } + + static QString i18n_w_TimeFromNow(); // text of 'Time from now:' radio button, with 'w' shortcut + static QString i18n_TimeAfterPeriod(); + static const int maxDelayTime; // maximum time from now + + signals: + void anyTimeToggled(bool anyTime); + void pastMax(); + + protected slots: + void slotTimer(); + void slotButtonSet(int id); + void dateTimeChanged(); + void delayTimeChanged(int); + void slotAnyTimeToggled(bool); + + private: + void init(int mode); + void setAnyTime(); + void setMaxDelayTime(const QDateTime& now); + void setMaxMinTimeIf(const QDateTime& now); + + RadioButton* mAtTimeRadio; + RadioButton* mAfterTimeRadio; + DateEdit* mDateEdit; + TimeEdit* mTimeEdit; + TimeSpinBox* mDelayTimeEdit; + CheckBox* mAnyTimeCheckBox; + QDateTime mMinDateTime; // earliest allowed date/time + QDateTime mMaxDateTime; // latest allowed date/time + int mAnyTime; // 0 = date/time is specified, 1 = only a date, -1 = uninitialised + bool mAnyTimeAllowed; // 'mAnyTimeCheckBox' is enabled + bool mMinDateTimeIsNow; // earliest allowed date/time is the current time + bool mPastMax; // current time is past the maximum date/time + bool mMinMaxTimeSet; // limits have been set for the time edit control + bool mTimerSyncing; // mTimer is not yet synchronised to the minute boundary +}; + +#endif // ALARMTIMEWIDGET_H diff --git a/kalarm/birthdaydlg.cpp b/kalarm/birthdaydlg.cpp new file mode 100644 index 000000000..3c4f28ab0 --- /dev/null +++ b/kalarm/birthdaydlg.cpp @@ -0,0 +1,427 @@ +/* + * birthdaydlg.cpp - dialog to pick birthdays from address book + * Program: kalarm + * Copyright © 2002-2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qlayout.h> +#include <qgroupbox.h> +#include <qhbox.h> +#include <qlabel.h> +#include <qlineedit.h> +#include <qwhatsthis.h> + +#include <klocale.h> +#include <kglobal.h> +#include <kconfig.h> +#include <kmessagebox.h> +#include <kaccel.h> +#include <kabc/addressbook.h> +#include <kabc/stdaddressbook.h> +#include <kdebug.h> + +#include "alarmcalendar.h" +#include "checkbox.h" +#include "colourcombo.h" +#include "editdlg.h" +#include "fontcolourbutton.h" +#include "kalarmapp.h" +#include "latecancel.h" +#include "preferences.h" +#include "reminder.h" +#include "repetition.h" +#include "shellprocess.h" +#include "soundpicker.h" +#include "specialactions.h" +#include "birthdaydlg.moc" + +using namespace KCal; + + +class AddresseeItem : public QListViewItem +{ + public: + enum columns { NAME = 0, BIRTHDAY = 1 }; + AddresseeItem(QListView* parent, const QString& name, const QDate& birthday); + QDate birthday() const { return mBirthday; } + virtual QString key(int column, bool ascending) const; + private: + QDate mBirthday; + QString mBirthdayOrder; +}; + + +const KABC::AddressBook* BirthdayDlg::mAddressBook = 0; + + +BirthdayDlg::BirthdayDlg(QWidget* parent) + : KDialogBase(KDialogBase::Plain, i18n("Import Birthdays From KAddressBook"), Ok|Cancel, Ok, parent, "BirthdayDlg"), + mSpecialActionsButton(0) +{ + QWidget* topWidget = plainPage(); + QBoxLayout* topLayout = new QVBoxLayout(topWidget); + topLayout->setSpacing(spacingHint()); + + // Prefix and suffix to the name in the alarm text + // Get default prefix and suffix texts from config file + KConfig* config = kapp->config(); + config->setGroup(QString::fromLatin1("General")); + mPrefixText = config->readEntry(QString::fromLatin1("BirthdayPrefix"), i18n("Birthday: ")); + mSuffixText = config->readEntry(QString::fromLatin1("BirthdaySuffix")); + + QGroupBox* textGroup = new QGroupBox(2, Qt::Horizontal, i18n("Alarm Text"), topWidget); + topLayout->addWidget(textGroup); + QLabel* label = new QLabel(i18n("Pre&fix:"), textGroup); + mPrefix = new BLineEdit(mPrefixText, textGroup); + mPrefix->setMinimumSize(mPrefix->sizeHint()); + label->setBuddy(mPrefix); + connect(mPrefix, SIGNAL(focusLost()), SLOT(slotTextLostFocus())); + QWhatsThis::add(mPrefix, + i18n("Enter text to appear before the person's name in the alarm message, " + "including any necessary trailing spaces.")); + + label = new QLabel(i18n("S&uffix:"), textGroup); + mSuffix = new BLineEdit(mSuffixText, textGroup); + mSuffix->setMinimumSize(mSuffix->sizeHint()); + label->setBuddy(mSuffix); + connect(mSuffix, SIGNAL(focusLost()), SLOT(slotTextLostFocus())); + QWhatsThis::add(mSuffix, + i18n("Enter text to appear after the person's name in the alarm message, " + "including any necessary leading spaces.")); + + QGroupBox* group = new QGroupBox(1, Qt::Horizontal, i18n("Select Birthdays"), topWidget); + topLayout->addWidget(group); + mAddresseeList = new BListView(group); + mAddresseeList->setMultiSelection(true); + mAddresseeList->setSelectionMode(QListView::Extended); + mAddresseeList->setAllColumnsShowFocus(true); + mAddresseeList->setFullWidth(true); + mAddresseeList->addColumn(i18n("Name")); + mAddresseeList->addColumn(i18n("Birthday")); + connect(mAddresseeList, SIGNAL(selectionChanged()), SLOT(slotSelectionChanged())); + QWhatsThis::add(mAddresseeList, + i18n("Select birthdays to set alarms for.\n" + "This list shows all birthdays in KAddressBook except those for which alarms already exist.\n\n" + "You can select multiple birthdays at one time by dragging the mouse over the list, " + "or by clicking the mouse while pressing Ctrl or Shift.")); + + group = new QGroupBox(i18n("Alarm Configuration"), topWidget); + topLayout->addWidget(group); + QBoxLayout* groupLayout = new QVBoxLayout(group, marginHint(), spacingHint()); + groupLayout->addSpacing(fontMetrics().lineSpacing()/2); + + // Font and colour choice button and sample text + mFontColourButton = new FontColourButton(group); + mFontColourButton->setMaximumHeight(mFontColourButton->sizeHint().height() * 3/2); + groupLayout->addWidget(mFontColourButton); + + // Sound checkbox and file selector + mSoundPicker = new SoundPicker(group); + mSoundPicker->setFixedSize(mSoundPicker->sizeHint()); + groupLayout->addWidget(mSoundPicker, 0, Qt::AlignAuto); + + // How much to advance warning to give + mReminder = new Reminder(i18n("&Reminder"), + i18n("Check to display a reminder in advance of the birthday."), + i18n("Enter the number of days before each birthday to display a reminder. " + "This is in addition to the alarm which is displayed on the birthday."), + false, false, group); + mReminder->setFixedSize(mReminder->sizeHint()); + mReminder->setMaximum(0, 364); + mReminder->setMinutes(0, true); + groupLayout->addWidget(mReminder, 0, Qt::AlignAuto); + + // Acknowledgement confirmation required - default = no confirmation + QHBoxLayout* layout = new QHBoxLayout(groupLayout, 2*spacingHint()); + mConfirmAck = EditAlarmDlg::createConfirmAckCheckbox(group); + layout->addWidget(mConfirmAck); + layout->addSpacing(2*spacingHint()); + layout->addStretch(); + + if (ShellProcess::authorised()) // don't display if shell commands not allowed (e.g. kiosk mode) + { + // Special actions button + mSpecialActionsButton = new SpecialActionsButton(i18n("Special Actions..."), group); + layout->addWidget(mSpecialActionsButton); + } + + // Late display checkbox - default = allow late display + layout = new QHBoxLayout(groupLayout, 2*spacingHint()); + mLateCancel = new LateCancelSelector(false, group); + layout->addWidget(mLateCancel); + layout->addStretch(); + + // Sub-repetition button + mSubRepetition = new RepetitionButton(i18n("Sub-Repetition"), false, group); + mSubRepetition->set(0, 0, true, 364*24*60); + QWhatsThis::add(mSubRepetition, i18n("Set up an additional alarm repetition")); + layout->addWidget(mSubRepetition); + + // Set the values to their defaults + mFontColourButton->setDefaultFont(); + mFontColourButton->setBgColour(Preferences::defaultBgColour()); + mFontColourButton->setFgColour(Preferences::defaultFgColour()); // set colour before setting alarm type buttons + mLateCancel->setMinutes(Preferences::defaultLateCancel(), true, TimePeriod::DAYS); + mConfirmAck->setChecked(Preferences::defaultConfirmAck()); + mSoundPicker->set(Preferences::defaultSoundType(), Preferences::defaultSoundFile(), + Preferences::defaultSoundVolume(), -1, 0, Preferences::defaultSoundRepeat()); + if (mSpecialActionsButton) + mSpecialActionsButton->setActions(Preferences::defaultPreAction(), Preferences::defaultPostAction()); + + // Initialise the birthday selection list and disable the OK button + loadAddressBook(); +} + +/****************************************************************************** +* Load the address book in preparation for displaying the birthday selection list. +*/ +void BirthdayDlg::loadAddressBook() +{ + if (!mAddressBook) + { +#if KDE_IS_VERSION(3,1,90) + mAddressBook = KABC::StdAddressBook::self(true); + if (mAddressBook) + connect(mAddressBook, SIGNAL(addressBookChanged(AddressBook*)), SLOT(updateSelectionList())); +#else + mAddressBook = KABC::StdAddressBook::self(); + if (mAddressBook) + updateSelectionList(); +#endif + } + else + updateSelectionList(); + if (!mAddressBook) + KMessageBox::error(this, i18n("Error reading address book")); +} + +/****************************************************************************** +* Close the address book.This is called at program termination. +*/ +void BirthdayDlg::close() +{ + if (mAddressBook) + { + KABC::StdAddressBook::close(); + mAddressBook = 0; + } +} + +/****************************************************************************** +* Initialise or update the birthday selection list by fetching all birthdays +* from the address book and displaying those which do not already have alarms. +*/ +void BirthdayDlg::updateSelectionList() +{ + // Compile a list of all pending alarm messages which look like birthdays + QStringList messageList; + KAEvent event; + Event::List events = AlarmCalendar::activeCalendar()->events(); + for (Event::List::ConstIterator it = events.begin(); it != events.end(); ++it) + { + Event* kcalEvent = *it; + event.set(*kcalEvent); + if (event.action() == KAEvent::MESSAGE + && event.recurType() == KARecurrence::ANNUAL_DATE + && (mPrefixText.isEmpty() || event.message().startsWith(mPrefixText))) + messageList.append(event.message()); + } + + // Fetch all birthdays from the address book + for (KABC::AddressBook::ConstIterator abit = mAddressBook->begin(); abit != mAddressBook->end(); ++abit) + { + const KABC::Addressee& addressee = *abit; + if (addressee.birthday().isValid()) + { + // Create a list entry for this birthday + QDate birthday = addressee.birthday().date(); + QString name = addressee.nickName(); + if (name.isEmpty()) + name = addressee.realName(); + // Check if the birthday already has an alarm + QString text = mPrefixText + name + mSuffixText; + bool alarmExists = (messageList.find(text) != messageList.end()); + // Check if the birthday is already in the selection list + bool inSelectionList = false; + AddresseeItem* item = 0; + for (QListViewItem* qitem = mAddresseeList->firstChild(); qitem; qitem = qitem->nextSibling()) + { + item = dynamic_cast<AddresseeItem*>(qitem); + if (item && item->text(AddresseeItem::NAME) == name && item->birthday() == birthday) + { + inSelectionList = true; + break; + } + } + + if (alarmExists && inSelectionList) + delete item; // alarm exists, so remove from selection list + else if (!alarmExists && !inSelectionList) + new AddresseeItem(mAddresseeList, name, birthday); // add to list + } + } +// mAddresseeList->setUpdatesEnabled(true); + + // Enable/disable OK button according to whether anything is currently selected + bool selection = false; + for (QListViewItem* item = mAddresseeList->firstChild(); item; item = item->nextSibling()) + if (mAddresseeList->isSelected(item)) + { + selection = true; + break; + } + enableButtonOK(selection); +} + +/****************************************************************************** +* Return a list of events for birthdays chosen. +*/ +QValueList<KAEvent> BirthdayDlg::events() const +{ + QValueList<KAEvent> list; + QDate today = QDate::currentDate(); + QDateTime todayNoon(today, QTime(12, 0, 0)); + int thisYear = today.year(); + int reminder = mReminder->minutes(); + + for (QListViewItem* item = mAddresseeList->firstChild(); item; item = item->nextSibling()) + { + if (mAddresseeList->isSelected(item)) + { + AddresseeItem* aItem = dynamic_cast<AddresseeItem*>(item); + if (aItem) + { + QDate date = aItem->birthday(); + date.setYMD(thisYear, date.month(), date.day()); + if (date <= today) + date.setYMD(thisYear + 1, date.month(), date.day()); + KAEvent event(date, + mPrefix->text() + aItem->text(AddresseeItem::NAME) + mSuffix->text(), + mFontColourButton->bgColour(), mFontColourButton->fgColour(), + mFontColourButton->font(), KAEvent::MESSAGE, mLateCancel->minutes(), + mFlags); + float fadeVolume; + int fadeSecs; + float volume = mSoundPicker->volume(fadeVolume, fadeSecs); + event.setAudioFile(mSoundPicker->file(), volume, fadeVolume, fadeSecs); + QValueList<int> months; + months.append(date.month()); + event.setRecurAnnualByDate(1, months, 0, Preferences::defaultFeb29Type(), -1, QDate()); + event.setRepetition(mSubRepetition->interval(), mSubRepetition->count()); + event.setNextOccurrence(todayNoon); + if (reminder) + event.setReminder(reminder, false); + if (mSpecialActionsButton) + event.setActions(mSpecialActionsButton->preAction(), + mSpecialActionsButton->postAction()); + list.append(event); + } + } + } + return list; +} + +/****************************************************************************** +* Called when the OK button is selected to import the selected birthdays. +*/ +void BirthdayDlg::slotOk() +{ + // Save prefix and suffix texts to use as future defaults + KConfig* config = kapp->config(); + config->setGroup(QString::fromLatin1("General")); + config->writeEntry(QString::fromLatin1("BirthdayPrefix"), mPrefix->text()); + config->writeEntry(QString::fromLatin1("BirthdaySuffix"), mSuffix->text()); + config->sync(); + + mFlags = (mSoundPicker->sound() == SoundPicker::BEEP ? KAEvent::BEEP : 0) + | (mSoundPicker->repeat() ? KAEvent::REPEAT_SOUND : 0) + | (mConfirmAck->isChecked() ? KAEvent::CONFIRM_ACK : 0) + | (mFontColourButton->defaultFont() ? KAEvent::DEFAULT_FONT : 0) + | KAEvent::ANY_TIME; + KDialogBase::slotOk(); +} + +/****************************************************************************** +* Called when the group of items selected changes. +* Enable/disable the OK button depending on whether anything is selected. +*/ +void BirthdayDlg::slotSelectionChanged() +{ + for (QListViewItem* item = mAddresseeList->firstChild(); item; item = item->nextSibling()) + if (mAddresseeList->isSelected(item)) + { + enableButtonOK(true); + return; + } + enableButtonOK(false); + +} + +/****************************************************************************** +* Called when the prefix or suffix text has lost keyboard focus. +* If the text has changed, re-evaluates the selection list according to the new +* birthday alarm text format. +*/ +void BirthdayDlg::slotTextLostFocus() +{ + QString prefix = mPrefix->text(); + QString suffix = mSuffix->text(); + if (prefix != mPrefixText || suffix != mSuffixText) + { + // Text has changed - re-evaluate the selection list + mPrefixText = prefix; + mSuffixText = suffix; + loadAddressBook(); + } +} + + +/*============================================================================= += Class: AddresseeItem +=============================================================================*/ + +AddresseeItem::AddresseeItem(QListView* parent, const QString& name, const QDate& birthday) + : QListViewItem(parent), + mBirthday(birthday) +{ + setText(NAME, name); + setText(BIRTHDAY, KGlobal::locale()->formatDate(mBirthday, true)); + mBirthdayOrder.sprintf("%04d%03d", mBirthday.year(), mBirthday.dayOfYear()); +} + +QString AddresseeItem::key(int column, bool) const +{ + if (column == BIRTHDAY) + return mBirthdayOrder; + return text(column).lower(); +} + + +/*============================================================================= += Class: BListView +=============================================================================*/ + +BListView::BListView(QWidget* parent, const char* name) + : KListView(parent, name) +{ + KAccel* accel = new KAccel(this); + accel->insert(KStdAccel::SelectAll, this, SLOT(slotSelectAll())); + accel->insert(KStdAccel::Deselect, this, SLOT(slotDeselect())); + accel->readSettings(); +} diff --git a/kalarm/birthdaydlg.h b/kalarm/birthdaydlg.h new file mode 100644 index 000000000..b8dd202c1 --- /dev/null +++ b/kalarm/birthdaydlg.h @@ -0,0 +1,103 @@ +/* + * birthdaydlg.h - dialog to pick birthdays from address book + * Program: kalarm + * Copyright © 2002-2004,2006,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#ifndef BIRTHDAYDLG_H +#define BIRTHDAYDLG_H + +#include <qlineedit.h> +#include <klistview.h> +#include <kdialogbase.h> + +#include "alarmevent.h" + +class QCheckBox; +class KListView; +class CheckBox; +class FontColourButton; +class SoundPicker; +class SpecialActionsButton; +class RepetitionButton; +class LateCancelSelector; +class Reminder; +namespace KABC { class AddressBook; } +class BLineEdit; +class BListView; + + +class BirthdayDlg : public KDialogBase +{ + Q_OBJECT + public: + BirthdayDlg(QWidget* parent = 0); + QValueList<KAEvent> events() const; + static void close(); + + protected slots: + virtual void slotOk(); + + private slots: + void slotSelectionChanged(); + void slotTextLostFocus(); + void updateSelectionList(); + + private: + void loadAddressBook(); + + static const KABC::AddressBook* mAddressBook; + BListView* mAddresseeList; + BLineEdit* mPrefix; + BLineEdit* mSuffix; + Reminder* mReminder; + SoundPicker* mSoundPicker; + FontColourButton* mFontColourButton; + CheckBox* mConfirmAck; + LateCancelSelector* mLateCancel; + SpecialActionsButton* mSpecialActionsButton; + RepetitionButton* mSubRepetition; + QString mPrefixText; // last entered value of prefix text + QString mSuffixText; // last entered value of suffix text + int mFlags; // event flag bits +}; + + +class BLineEdit : public QLineEdit +{ + Q_OBJECT + public: + BLineEdit(QWidget* parent = 0, const char* name = 0) + : QLineEdit(parent, name) {} + BLineEdit(const QString& text, QWidget* parent = 0, const char* name = 0) + : QLineEdit(text, parent, name) {} + signals: + void focusLost(); + protected: + virtual void focusOutEvent(QFocusEvent*) { emit focusLost(); } +}; + +class BListView : public KListView +{ + Q_OBJECT + public: + BListView(QWidget* parent = 0, const char* name = 0); + public slots: + virtual void slotSelectAll() { selectAll(true); } + virtual void slotDeselect() { selectAll(false); } +}; + +#endif // BIRTHDAYDLG_H diff --git a/kalarm/calendarcompat.cpp b/kalarm/calendarcompat.cpp new file mode 100644 index 000000000..ada75e768 --- /dev/null +++ b/kalarm/calendarcompat.cpp @@ -0,0 +1,150 @@ +/* + * calendarcompat.cpp - compatibility for old calendar file formats + * Program: kalarm + * Copyright © 2001-2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qfile.h> +#include <qtextstream.h> +#include <qstringlist.h> + +#include <kapplication.h> +#include <kaboutdata.h> +#include <kdebug.h> + +#include <libkcal/calendar.h> + +#include "alarmevent.h" +#include "functions.h" +#include "preferences.h" +#include "calendarcompat.h" + +using namespace KCal; + + +/****************************************************************************** +* Find the version of KAlarm which wrote the calendar file, and do any +* necessary conversions to the current format. The calendar is not saved - any +* conversions will only be saved if changes are made later. +*/ +void CalendarCompat::fix(KCal::Calendar& calendar, const QString& localFile) +{ + bool version057_UTC = false; + QString subVersion; + int version = readKAlarmVersion(calendar, subVersion); + if (!version) + { + // The calendar was created either by the current version of KAlarm, + // or another program, so don't do any conversions + return; + } + if (version == KAlarm::Version(0,5,7) && !localFile.isEmpty()) + { + // KAlarm version 0.5.7 - check whether times are stored in UTC, in which + // case it is the KDE 3.0.0 version, which needs adjustment of summer times. + version057_UTC = isUTC(localFile); + kdDebug(5950) << "CalendarCompat::fix(): KAlarm version 0.5.7 (" << (version057_UTC ? "" : "non-") << "UTC)\n"; + } + else + kdDebug(5950) << "CalendarCompat::fix(): KAlarm version " << version << endl; + + // Convert events to current KAlarm format for when calendar is saved + KAEvent::convertKCalEvents(calendar, version, version057_UTC); +} + +/****************************************************************************** +* Return the KAlarm version which wrote the calendar which has been loaded. +* The format is, for example, 000507 for 0.5.7. +* Reply = 0 if the calendar was created by the current version of KAlarm, +* KAlarm pre-0.3.5, or another program. +*/ +int CalendarCompat::readKAlarmVersion(KCal::Calendar& calendar, QString& subVersion) +{ + subVersion = QString::null; + const QString& prodid = calendar.productId(); + + // Find the KAlarm identifier + QString progname = QString::fromLatin1(" KAlarm "); + int i = prodid.find(progname, 0, false); + if (i < 0) + { + // Older versions used KAlarm's translated name in the product ID, which + // could have created problems using a calendar in different locales. + progname = QString(" ") + kapp->aboutData()->programName() + ' '; + i = prodid.find(progname, 0, false); + if (i < 0) + return 0; // calendar wasn't created by KAlarm + } + + // Extract the KAlarm version string + QString ver = prodid.mid(i + progname.length()).stripWhiteSpace(); + i = ver.find('/'); + int j = ver.find(' '); + if (j >= 0 && j < i) + i = j; + if (i <= 0) + return 0; // missing version string + ver = ver.left(i); // ver now contains the KAlarm version string + if (ver == KAlarm::currentCalendarVersionString()) + return 0; // the calendar is in the current KAlarm format + return KAlarm::getVersionNumber(ver, &subVersion); +} + +/****************************************************************************** + * Check whether the calendar file has its times stored as UTC times, + * indicating that it was written by the KDE 3.0.0 version of KAlarm 0.5.7. + * Reply = true if times are stored in UTC + * = false if the calendar is a vCalendar, times are not UTC, or any error occurred. + */ +bool CalendarCompat::isUTC(const QString& localFile) +{ + // Read the calendar file into a QString + QFile file(localFile); + if (!file.open(IO_ReadOnly)) + return false; + QTextStream ts(&file); + ts.setEncoding(QTextStream::Latin1); + QString text = ts.read(); + file.close(); + + // Extract the CREATED property for the first VEVENT from the calendar + QString VCALENDAR = QString::fromLatin1("BEGIN:VCALENDAR"); + QString VEVENT = QString::fromLatin1("BEGIN:VEVENT"); + QString CREATED = QString::fromLatin1("CREATED:"); + QStringList lines = QStringList::split(QChar('\n'), text); + for (QStringList::ConstIterator it = lines.begin(); it != lines.end(); ++it) + { + if ((*it).startsWith(VCALENDAR)) + { + while (++it != lines.end()) + { + if ((*it).startsWith(VEVENT)) + { + while (++it != lines.end()) + { + if ((*it).startsWith(CREATED)) + return (*it).endsWith("Z"); + } + } + } + break; + } + } + return false; +} diff --git a/kalarm/calendarcompat.h b/kalarm/calendarcompat.h new file mode 100644 index 000000000..a8cc3e812 --- /dev/null +++ b/kalarm/calendarcompat.h @@ -0,0 +1,40 @@ +/* + * calendarcompat.h - compatibility for old calendar file formats + * Program: kalarm + * Copyright © 2005,2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef CALENDARCOMPAT_H +#define CALENDARCOMPAT_H + +/* @file calendarcompat.h - compatibility for old calendar file formats */ + +#include <qstring.h> +namespace KCal { class Calendar; } + + +class CalendarCompat +{ + public: + static void fix(KCal::Calendar&, const QString& localFile = QString::null); + + private: + static int readKAlarmVersion(KCal::Calendar&, QString& subVersion); + static bool isUTC(const QString& localFile); +}; + +#endif // CALENDARCOMPAT_H diff --git a/kalarm/configure.in.bot b/kalarm/configure.in.bot new file mode 100644 index 000000000..9af89064a --- /dev/null +++ b/kalarm/configure.in.bot @@ -0,0 +1,16 @@ +if test -z "$KMIX"; then + echo "" + echo "=================================================================" + echo "kmix (from the kdemultimedia package) is missing." + echo "KAlarm will not be able to set the absolute sound volume when" + echo "playing audio files until you install kmix." + echo "=================================================================" +fi +if test -z "$KTTSD"; then + echo "" + echo "=================================================================" + echo "kttsd (from the kdeaccessibility package) is missing." + echo "KAlarm will not be able to speak alarm messages until you install" + echo "kttsd together with appropriate speech synthesis application(s)." + echo "=================================================================" +fi diff --git a/kalarm/configure.in.in b/kalarm/configure.in.in new file mode 100644 index 000000000..fdf300906 --- /dev/null +++ b/kalarm/configure.in.in @@ -0,0 +1,2 @@ +KDE_FIND_PATH(kmix, KMIX, [$kde_default_bindirs]) +KDE_FIND_PATH(kttsd, KTTSD, [$kde_default_bindirs]) diff --git a/kalarm/daemon.cpp b/kalarm/daemon.cpp new file mode 100644 index 000000000..22f1fb36d --- /dev/null +++ b/kalarm/daemon.cpp @@ -0,0 +1,774 @@ +/* + * daemon.cpp - interface with alarm daemon + * Program: kalarm + * Copyright © 2001-2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qtimer.h> +#include <qiconset.h> + +#include <kstandarddirs.h> +#include <kconfig.h> +#include <kaboutdata.h> +#include <kmessagebox.h> +#include <dcopclient.h> +#include <kdebug.h> + +#include "kalarmd/kalarmd.h" +#include "kalarmd/alarmdaemoniface.h" +#include "kalarmd/alarmdaemoniface_stub.h" +#include "kalarmd/alarmguiiface.h" + +#include "alarmcalendar.h" +#include "kalarmapp.h" +#include "preferences.h" +#include "daemon.moc" + + +static const int REGISTER_TIMEOUT = 20; // seconds to wait before assuming registration with daemon has failed +static const char* NOTIFY_DCOP_OBJECT = "notify"; // DCOP name of KAlarm's interface for notification by alarm daemon + +static QString expandURL(const QString& urlString); + + +/*============================================================================= += Class: NotificationHandler += Handles the the alarm daemon's client notification DCOP interface. +=============================================================================*/ + +class NotificationHandler : public QObject, virtual public AlarmGuiIface +{ + public: + NotificationHandler(); + private: + // DCOP interface + void alarmDaemonUpdate(int calendarStatus, const QString& calendarURL); + void handleEvent(const QString& calendarURL, const QString& eventID); + void registered(bool reregister, int result, int version); +}; + + +Daemon* Daemon::mInstance = 0; +NotificationHandler* Daemon::mDcopHandler = 0; +QValueList<QString> Daemon::mQueuedEvents; +QValueList<QString> Daemon::mSavingEvents; +QTimer* Daemon::mStartTimer = 0; +QTimer* Daemon::mRegisterTimer = 0; +QTimer* Daemon::mStatusTimer = 0; +int Daemon::mStatusTimerCount = 0; +int Daemon::mStatusTimerInterval; +int Daemon::mStartTimeout = 0; +Daemon::Status Daemon::mStatus = Daemon::STOPPED; +bool Daemon::mRunning = false; +bool Daemon::mCalendarDisabled = false; +bool Daemon::mEnableCalPending = false; +bool Daemon::mRegisterFailMsg = false; + +// How frequently to check the daemon's status after starting it. +// This is equal to the length of time we wait after the daemon is registered with DCOP +// before we assume that it is ready to accept DCOP calls. +static const int startCheckInterval = 500; // 500 milliseconds + + +/****************************************************************************** +* Initialise. +* A Daemon instance needs to be constructed only in order for slots to work. +* All external access is via static methods. +*/ +void Daemon::initialise() +{ + if (!mInstance) + mInstance = new Daemon(); + connect(AlarmCalendar::activeCalendar(), SIGNAL(calendarSaved(AlarmCalendar*)), mInstance, SLOT(slotCalendarSaved(AlarmCalendar*))); +} + +/****************************************************************************** +* Initialise the daemon status timer. +*/ +void Daemon::createDcopHandler() +{ + if (mDcopHandler) + return; + mDcopHandler = new NotificationHandler(); + // Check if the alarm daemon is running, but don't start it yet, since + // the program is still initialising. + mRunning = isRunning(false); + + mStatusTimerInterval = Preferences::daemonTrayCheckInterval(); + Preferences::connect(SIGNAL(preferencesChanged()), mInstance, SLOT(slotPreferencesChanged())); + + mStatusTimer = new QTimer(mInstance); + connect(mStatusTimer, SIGNAL(timeout()), mInstance, SLOT(timerCheckIfRunning())); + mStatusTimer->start(mStatusTimerInterval * 1000); // check regularly if daemon is running +} + +/****************************************************************************** +* Start the alarm daemon if necessary, and register this application with it. +* Reply = false if the daemon definitely couldn't be started or registered with. +*/ +bool Daemon::start() +{ + kdDebug(5950) << "Daemon::start()\n"; + updateRegisteredStatus(); + switch (mStatus) + { + case STOPPED: + { + if (mStartTimer) + return true; // we're currently waiting for the daemon to start + // Start the alarm daemon. It is a KUniqueApplication, which means that + // there is automatically only one instance of the alarm daemon running. + QString execStr = locate("exe", QString::fromLatin1(DAEMON_APP_NAME)); + if (execStr.isEmpty()) + { + KMessageBox::error(0, i18n("Alarm daemon not found.")); + kdError() << "Daemon::startApp(): " DAEMON_APP_NAME " not found" << endl; + return false; + } + KApplication::kdeinitExec(execStr); + kdDebug(5950) << "Daemon::start(): Alarm daemon started" << endl; + mStartTimeout = 5000/startCheckInterval + 1; // check daemon status for 5 seconds before giving up + mStartTimer = new QTimer(mInstance); + connect(mStartTimer, SIGNAL(timeout()), mInstance, SLOT(checkIfStarted())); + mStartTimer->start(startCheckInterval); + mInstance->checkIfStarted(); + return true; + } + case RUNNING: + return true; // we're waiting for the daemon to be completely ready + case READY: + // Daemon is ready. Register this application with it. + if (!registerWith(false)) + return false; + break; + case REGISTERED: + break; + } + return true; +} + +/****************************************************************************** +* Register this application with the alarm daemon, and tell it to load the +* calendar. +* Set 'reregister' true in order to notify the daemon of a change in the +* 'disable alarms if stopped' setting. +*/ +bool Daemon::registerWith(bool reregister) +{ + if (mRegisterTimer) + return true; + switch (mStatus) + { + case STOPPED: + case RUNNING: + return false; + case REGISTERED: + if (!reregister) + return true; + break; + case READY: + break; + } + + bool disabledIfStopped = theApp()->alarmsDisabledIfStopped(); + kdDebug(5950) << (reregister ? "Daemon::reregisterWith(): " : "Daemon::registerWith(): ") << (disabledIfStopped ? "NO_START" : "COMMAND_LINE") << endl; + QCString appname = kapp->aboutData()->appName(); + AlarmDaemonIface_stub s(DAEMON_APP_NAME, DAEMON_DCOP_OBJECT); + if (reregister) + s.registerChange(appname, !disabledIfStopped); + else + s.registerApp(appname, kapp->aboutData()->programName(), QCString(NOTIFY_DCOP_OBJECT), AlarmCalendar::activeCalendar()->urlString(), !disabledIfStopped); + if (!s.ok()) + { + kdError(5950) << "Daemon::registerWith(" << reregister << "): DCOP error" << endl; + registrationResult(reregister, KAlarmd::FAILURE); + return false; + } + mRegisterTimer = new QTimer(mInstance); + connect(mRegisterTimer, SIGNAL(timeout()), mInstance, SLOT(registerTimerExpired())); + mRegisterTimer->start(REGISTER_TIMEOUT * 1000); // wait for the reply + return true; +} + +/****************************************************************************** +* Called when the daemon has notified us of the result of the register() DCOP call. +*/ +void Daemon::registrationResult(bool reregister, int result, int version) +{ + kdDebug(5950) << "Daemon::registrationResult(" << reregister << ") version: " << version << endl; + delete mRegisterTimer; + mRegisterTimer = 0; + bool failed = false; + QString errmsg; + if (version && version != DAEMON_VERSION_NUM) + { + failed = true; + kdError(5950) << "Daemon::registrationResult(" << reregister << "): kalarmd reports incompatible version " << version << endl; + errmsg = i18n("Cannot enable alarms.\nInstallation or configuration error: Alarm Daemon (%1) version is incompatible.") + .arg(QString::fromLatin1(DAEMON_APP_NAME)); + } + else + { + switch (result) + { + case KAlarmd::SUCCESS: + break; + case KAlarmd::NOT_FOUND: + // We've successfully registered with the daemon, but the daemon can't + // find the KAlarm executable so won't be able to restart KAlarm if + // KAlarm exits. + kdError(5950) << "Daemon::registrationResult(" << reregister << "): registerApp dcop call: " << kapp->aboutData()->appName() << " not found\n"; + KMessageBox::error(0, i18n("Alarms will be disabled if you stop KAlarm.\n" + "(Installation or configuration error: %1 cannot locate %2 executable.)") + .arg(QString::fromLatin1(DAEMON_APP_NAME)) + .arg(kapp->aboutData()->appName())); + break; + case KAlarmd::FAILURE: + default: + // Either the daemon reported an error in the registration DCOP call, + // there was a DCOP error calling the daemon, or a timeout on the reply. + kdError(5950) << "Daemon::registrationResult(" << reregister << "): registerApp dcop call failed -> " << result << endl; + failed = true; + if (!reregister) + { + errmsg = i18n("Cannot enable alarms:\nFailed to register with Alarm Daemon (%1)") + .arg(QString::fromLatin1(DAEMON_APP_NAME)); + } + break; + } + } + + if (failed) + { + if (!errmsg.isEmpty()) + { + if (mStatus == REGISTERED) + mStatus = READY; + if (!mRegisterFailMsg) + { + mRegisterFailMsg = true; + KMessageBox::error(0, errmsg); + } + } + return; + } + + if (!reregister) + { + // The alarm daemon has loaded the calendar + mStatus = REGISTERED; + mRegisterFailMsg = false; + kdDebug(5950) << "Daemon::start(): daemon startup complete" << endl; + } +} + +/****************************************************************************** +* Check whether the alarm daemon has started yet, and if so, register with it. +*/ +void Daemon::checkIfStarted() +{ + updateRegisteredStatus(); + bool err = false; + switch (mStatus) + { + case STOPPED: + if (--mStartTimeout > 0) + return; // wait a bit more to check again + // Output error message, but delete timer first to prevent + // multiple messages. + err = true; + break; + case RUNNING: + case READY: + case REGISTERED: + break; + } + delete mStartTimer; + mStartTimer = 0; + if (err) + { + kdError(5950) << "Daemon::checkIfStarted(): failed to start daemon" << endl; + KMessageBox::error(0, i18n("Cannot enable alarms:\nFailed to start Alarm Daemon (%1)").arg(QString::fromLatin1(DAEMON_APP_NAME))); + } +} + +/****************************************************************************** +* Check whether the alarm daemon has started yet, and if so, whether it is +* ready to accept DCOP calls. +*/ +void Daemon::updateRegisteredStatus(bool timeout) +{ + if (!kapp->dcopClient()->isApplicationRegistered(DAEMON_APP_NAME)) + { + mStatus = STOPPED; + mRegisterFailMsg = false; + } + else + { + switch (mStatus) + { + case STOPPED: + // The daemon has newly been detected as registered with DCOP. + // Wait for a short time to ensure that it is ready for DCOP calls. + mStatus = RUNNING; + QTimer::singleShot(startCheckInterval, mInstance, SLOT(slotStarted())); + break; + case RUNNING: + if (timeout) + { + mStatus = READY; + start(); + } + break; + case READY: + case REGISTERED: + break; + } + } + kdDebug(5950) << "Daemon::updateRegisteredStatus() -> " << mStatus << endl; +} + +/****************************************************************************** +* Stop the alarm daemon if it is running. +*/ +bool Daemon::stop() +{ + kdDebug(5950) << "Daemon::stop()" << endl; + if (kapp->dcopClient()->isApplicationRegistered(DAEMON_APP_NAME)) + { + AlarmDaemonIface_stub s(DAEMON_APP_NAME, DAEMON_DCOP_OBJECT); + s.quit(); + if (!s.ok()) + { + kdError(5950) << "Daemon::stop(): dcop call failed" << endl; + return false; + } + } + return true; +} + +/****************************************************************************** +* Reset the alarm daemon. +* Reply = true if daemon was told to reset +* = false if daemon is not running. +*/ +bool Daemon::reset() +{ + kdDebug(5950) << "Daemon::reset()" << endl; + if (!kapp->dcopClient()->isApplicationRegistered(DAEMON_APP_NAME)) + return false; + AlarmDaemonIface_stub s(DAEMON_APP_NAME, DAEMON_DCOP_OBJECT); + s.resetCalendar(QCString(kapp->aboutData()->appName()), AlarmCalendar::activeCalendar()->urlString()); + if (!s.ok()) + kdError(5950) << "Daemon::reset(): resetCalendar dcop send failed" << endl; + return true; +} + +/****************************************************************************** +* Tell the alarm daemon to reread the calendar file. +*/ +void Daemon::reload() +{ + kdDebug(5950) << "Daemon::reload()\n"; + AlarmDaemonIface_stub s(DAEMON_APP_NAME, DAEMON_DCOP_OBJECT); + s.reloadCalendar(QCString(kapp->aboutData()->appName()), AlarmCalendar::activeCalendar()->urlString()); + if (!s.ok()) + kdError(5950) << "Daemon::reload(): reloadCalendar dcop send failed" << endl; +} + +/****************************************************************************** +* Tell the alarm daemon to enable/disable monitoring of the calendar file. +*/ +void Daemon::enableCalendar(bool enable) +{ + AlarmDaemonIface_stub s(DAEMON_APP_NAME, DAEMON_DCOP_OBJECT); + s.enableCalendar(AlarmCalendar::activeCalendar()->urlString(), enable); + mEnableCalPending = false; +} + +/****************************************************************************** +* Tell the alarm daemon to enable/disable autostart at login. +*/ +void Daemon::enableAutoStart(bool enable) +{ + // Tell the alarm daemon in case it is running. + AlarmDaemonIface_stub s(DAEMON_APP_NAME, DAEMON_DCOP_OBJECT); + s.enableAutoStart(enable); + + // The return status doesn't report failure even if the daemon isn't running, + // so in case of failure, rewrite the config file in any case. + KConfig adconfig(locate("config", DAEMON_APP_NAME"rc")); + adconfig.setGroup(QString::fromLatin1(DAEMON_AUTOSTART_SECTION)); + adconfig.writeEntry(QString::fromLatin1(DAEMON_AUTOSTART_KEY), enable); + adconfig.sync(); +} + +/****************************************************************************** +* Notify the alarm daemon that the start-of-day time for date-only alarms has +* changed. +*/ +void Daemon::notifyTimeChanged() +{ + AlarmDaemonIface_stub s(DAEMON_APP_NAME, DAEMON_DCOP_OBJECT); + s.timeConfigChanged(); + if (!s.ok()) + kdError(5950) << "Daemon::timeConfigChanged(): dcop send failed" << endl; +} + +/****************************************************************************** +* Read the alarm daemon's autostart-at-login setting. +*/ +bool Daemon::autoStart() +{ + KConfig adconfig(locate("config", DAEMON_APP_NAME"rc")); + adconfig.setGroup(QString::fromLatin1(DAEMON_AUTOSTART_SECTION)); + return adconfig.readBoolEntry(QString::fromLatin1(DAEMON_AUTOSTART_KEY), true); +} + +/****************************************************************************** +* Notification that the alarm daemon has enabled/disabled monitoring of the +* calendar file. +*/ +void Daemon::calendarIsEnabled(bool enabled) +{ + mCalendarDisabled = !enabled; + emit mInstance->daemonRunning(enabled); +} + +/****************************************************************************** +* Tell the alarm daemon to stop or start monitoring the calendar file as +* appropriate. +*/ +void Daemon::setAlarmsEnabled(bool enable) +{ + kdDebug(5950) << "Daemon::setAlarmsEnabled(" << enable << ")\n"; + if (enable && !checkIfRunning()) + { + // The daemon is not running, so start it + if (!start()) + { + emit daemonRunning(false); + return; + } + mEnableCalPending = true; + setFastCheck(); + } + + // If the daemon is now running, tell it to enable/disable the calendar + if (checkIfRunning()) + enableCalendar(enable); +} + +/****************************************************************************** +* Return whether the alarm daemon is monitoring alarms. +*/ +bool Daemon::monitoringAlarms() +{ + bool ok = !mCalendarDisabled && isRunning(); + emit mInstance->daemonRunning(ok); + return ok; +} + +/****************************************************************************** +* Check whether the alarm daemon is currently running and available. +*/ +bool Daemon::isRunning(bool startdaemon) +{ + static bool runState = false; + updateRegisteredStatus(); + bool newRunState = (mStatus == READY || mStatus == REGISTERED); + if (newRunState != runState) + { + // Daemon's status has changed + runState = newRunState; + if (runState && startdaemon) + start(); // re-register with the daemon + } + return runState && (mStatus == REGISTERED); +} + +/****************************************************************************** +* Called by the timer to check whether the daemon is running. +*/ +void Daemon::timerCheckIfRunning() +{ + checkIfRunning(); + // Limit how long we check at the fast rate + if (mStatusTimerCount > 0 && --mStatusTimerCount <= 0) + mStatusTimer->changeInterval(mStatusTimerInterval * 1000); +} + +/****************************************************************************** +* Check whether the alarm daemon is currently running. +* If its status has changed, trigger GUI updates. +*/ +bool Daemon::checkIfRunning() +{ + bool newstatus = isRunning(); + if (newstatus != mRunning) + { + mRunning = newstatus; + int status = mRunning && !mCalendarDisabled; + emit mInstance->daemonRunning(status); + mStatusTimer->changeInterval(mStatusTimerInterval * 1000); // exit from fast checking + mStatusTimerCount = 0; + if (mRunning) + { + // The alarm daemon has started up + if (mEnableCalPending) + enableCalendar(true); // tell it to monitor the calendar, if appropriate + } + } + return mRunning; +} + +/****************************************************************************** +* Starts checking at a faster rate whether the daemon is running. +*/ +void Daemon::setFastCheck() +{ + mStatusTimer->start(500); // check new status every half second + mStatusTimerCount = 20; // don't check at this rate for more than 10 seconds +} + +/****************************************************************************** +* Called when a program setting has changed. +* If the system tray icon update interval has changed, reset the timer. +*/ +void Daemon::slotPreferencesChanged() +{ + int newInterval = Preferences::daemonTrayCheckInterval(); + if (newInterval != mStatusTimerInterval) + { + // Daemon check interval has changed + mStatusTimerInterval = newInterval; + if (mStatusTimerCount <= 0) // don't change if on fast rate + mStatusTimer->changeInterval(mStatusTimerInterval * 1000); + } +} + +/****************************************************************************** +* Create an "Alarms Enabled/Enable Alarms" action. +*/ +AlarmEnableAction* Daemon::createAlarmEnableAction(KActionCollection* actions, const char* name) +{ + AlarmEnableAction* a = new AlarmEnableAction(0, actions, name); + connect(a, SIGNAL(userClicked(bool)), mInstance, SLOT(setAlarmsEnabled(bool))); + connect(mInstance, SIGNAL(daemonRunning(bool)), a, SLOT(setCheckedActual(bool))); + return a; +} + +/****************************************************************************** +* Called when a calendar has been saved. +* If it's the active alarm calendar, notify the alarm daemon. +*/ +void Daemon::slotCalendarSaved(AlarmCalendar* cal) +{ + if (cal == AlarmCalendar::activeCalendar()) + { + int n = mSavingEvents.count(); + if (n) + { + // We have just saved a modified event originally triggered by the daemon. + // Notify the daemon of the event, and tell it to reload the calendar. + for (int i = 0; i < n - 1; ++i) + notifyEventHandled(mSavingEvents[i], false); + notifyEventHandled(mSavingEvents[n - 1], true); + mSavingEvents.clear(); + } + else + reload(); + } +} + +/****************************************************************************** +* Note an event ID which has been triggered by the alarm daemon. +*/ +void Daemon::queueEvent(const QString& eventId) +{ + mQueuedEvents += eventId; +} + +/****************************************************************************** +* Note an event ID which is currently being saved in the calendar file, if the +* event was originally triggered by the alarm daemon. +*/ +void Daemon::savingEvent(const QString& eventId) +{ + if (mQueuedEvents.remove(eventId) > 0) + mSavingEvents += eventId; +} + +/****************************************************************************** +* If the event ID has been triggered by the alarm daemon, tell the daemon that +* it has been processed, and whether to reload its calendar. +*/ +void Daemon::eventHandled(const QString& eventId, bool reloadCal) +{ + if (mQueuedEvents.remove(eventId) > 0) + notifyEventHandled(eventId, reloadCal); // it's a daemon event, so tell daemon that it's been handled + else if (reloadCal) + reload(); // not a daemon event, so simply tell the daemon to reload the calendar +} + +/****************************************************************************** +* Tell the daemon that an event has been processed, and whether to reload its +* calendar. +*/ +void Daemon::notifyEventHandled(const QString& eventId, bool reloadCal) +{ + kdDebug(5950) << "Daemon::notifyEventHandled(" << eventId << (reloadCal ? "): reload" : ")") << endl; + AlarmDaemonIface_stub s(DAEMON_APP_NAME, DAEMON_DCOP_OBJECT); + s.eventHandled(QCString(kapp->aboutData()->appName()), AlarmCalendar::activeCalendar()->urlString(), eventId, reloadCal); + if (!s.ok()) + kdError(5950) << "Daemon::notifyEventHandled(): eventHandled dcop send failed" << endl; +} + +/****************************************************************************** +* Return the maximum time (in seconds) elapsed since the last time the alarm +* daemon must have checked alarms. +*/ +int Daemon::maxTimeSinceCheck() +{ + return DAEMON_CHECK_INTERVAL; +} + + +/*============================================================================= += Class: NotificationHandler +=============================================================================*/ + +NotificationHandler::NotificationHandler() + : DCOPObject(NOTIFY_DCOP_OBJECT), + QObject() +{ + kdDebug(5950) << "NotificationHandler::NotificationHandler()\n"; +} + +/****************************************************************************** + * DCOP call from the alarm daemon to notify a change. + * The daemon notifies calendar statuses when we first register as a GUI, and whenever + * a calendar status changes. So we don't need to read its config files. + */ +void NotificationHandler::alarmDaemonUpdate(int calendarStatus, const QString& calendarURL) +{ + kdDebug(5950) << "NotificationHandler::alarmDaemonUpdate(" << calendarStatus << ")\n"; + KAlarmd::CalendarStatus status = KAlarmd::CalendarStatus(calendarStatus); + if (expandURL(calendarURL) != AlarmCalendar::activeCalendar()->urlString()) + return; // it's not a notification about KAlarm's calendar + bool enabled = false; + switch (status) + { + case KAlarmd::CALENDAR_UNAVAILABLE: + // Calendar is not available for monitoring + kdDebug(5950) << "NotificationHandler::alarmDaemonUpdate(CALENDAR_UNAVAILABLE)\n"; + break; + case KAlarmd::CALENDAR_DISABLED: + // Calendar is available for monitoring but is not currently being monitored + kdDebug(5950) << "NotificationHandler::alarmDaemonUpdate(DISABLE_CALENDAR)\n"; + break; + case KAlarmd::CALENDAR_ENABLED: + // Calendar is currently being monitored + kdDebug(5950) << "NotificationHandler::alarmDaemonUpdate(ENABLE_CALENDAR)\n"; + enabled = true; + break; + default: + return; + } + Daemon::calendarIsEnabled(enabled); +} + +/****************************************************************************** + * DCOP call from the alarm daemon to notify that an alarm is due. + */ +void NotificationHandler::handleEvent(const QString& url, const QString& eventId) +{ + QString id = eventId; + if (id.startsWith(QString::fromLatin1("ad:"))) + { + // It's a notification from the alarm deamon + id = id.mid(3); + Daemon::queueEvent(id); + } + theApp()->handleEvent(url, id); +} + +/****************************************************************************** + * DCOP call from the alarm daemon to notify the success or failure of a + * registration request from KAlarm. + */ +void NotificationHandler::registered(bool reregister, int result, int version) +{ + Daemon::registrationResult(reregister, result, version); +} + + +/*============================================================================= += Class: AlarmEnableAction +=============================================================================*/ + +AlarmEnableAction::AlarmEnableAction(int accel, QObject* parent, const char* name) + : KToggleAction(i18n("Enable &Alarms"), accel, parent, name), + mInitialised(false) +{ + setCheckedState(i18n("Disable &Alarms")); + setCheckedActual(false); // set the correct text + mInitialised = true; +} + +/****************************************************************************** +* Set the checked status and the correct text for the Alarms Enabled action. +*/ +void AlarmEnableAction::setCheckedActual(bool running) +{ + kdDebug(5950) << "AlarmEnableAction::setCheckedActual(" << running << ")\n"; + if (running != isChecked() || !mInitialised) + { + KToggleAction::setChecked(running); + emit switched(running); + } +} + +/****************************************************************************** +* Request a change in the checked status. +* The status is only actually changed when the alarm daemon run state changes. +*/ +void AlarmEnableAction::setChecked(bool check) +{ + kdDebug(5950) << "AlarmEnableAction::setChecked(" << check << ")\n"; + if (check != isChecked()) + { + if (check) + Daemon::allowRegisterFailMsg(); + emit userClicked(check); + } +} + + +/****************************************************************************** + * Expand a DCOP call parameter URL to a full URL. + * (We must store full URLs in the calendar data since otherwise later calls to + * reload or remove calendars won't necessarily find a match.) + */ +QString expandURL(const QString& urlString) +{ + if (urlString.isEmpty()) + return QString(); + return KURL(urlString).url(); +} diff --git a/kalarm/daemon.h b/kalarm/daemon.h new file mode 100644 index 000000000..04198904c --- /dev/null +++ b/kalarm/daemon.h @@ -0,0 +1,134 @@ +/* + * daemon.h - interface with alarm daemon + * Program: kalarm + * Copyright © 2001-2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef DAEMON_H +#define DAEMON_H + +#include <qobject.h> +#include <qdatetime.h> +#include <kaction.h> + +#include <kalarmd/kalarmd.h> +#include <kalarmd/alarmguiiface.h> + +class KActionCollection; +class AlarmCalendar; +class AlarmEnableAction; +class NotificationHandler; + + +class Daemon : public QObject +{ + Q_OBJECT + public: + static void initialise(); + static void createDcopHandler(); + static bool isDcopHandlerReady() { return mDcopHandler; } + static AlarmEnableAction* createAlarmEnableAction(KActionCollection*, const char* name); + static bool start(); + static bool reregister() { return registerWith(true); } + static bool reset(); + static bool stop(); + static bool autoStart(); + static void enableAutoStart(bool enable); + static void notifyTimeChanged(); + static void setAlarmsEnabled() { mInstance->setAlarmsEnabled(true); } + static void checkStatus() { checkIfRunning(); } + static bool monitoringAlarms(); + static bool isRunning(bool startDaemon = true); + static int maxTimeSinceCheck(); + static bool isRegistered() { return mStatus == REGISTERED; } + static void allowRegisterFailMsg() { mRegisterFailMsg = false; } + + static void queueEvent(const QString& eventID); + static void savingEvent(const QString& eventID); + static void eventHandled(const QString& eventID, bool reloadCal); + + signals: + void daemonRunning(bool running); + + private slots: + void slotCalendarSaved(AlarmCalendar*); + void checkIfStarted(); + void slotStarted() { updateRegisteredStatus(true); } + void registerTimerExpired() { registrationResult((mStatus == REGISTERED), KAlarmd::FAILURE); } + + void setAlarmsEnabled(bool enable); + void timerCheckIfRunning(); + void slotPreferencesChanged(); + + private: + enum Status // daemon status. KEEP IN THIS ORDER!! + { + STOPPED, // daemon is not registered with DCOP + RUNNING, // daemon is newly registered with DCOP + READY, // daemon is ready to accept DCOP calls + REGISTERED // we have registered with the daemon + }; + Daemon() { } + static bool registerWith(bool reregister); + static void registrationResult(bool reregister, int result, int version = 0); + static void reload(); + static void notifyEventHandled(const QString& eventID, bool reloadCal); + static void updateRegisteredStatus(bool timeout = false); + static void enableCalendar(bool enable); + static void calendarIsEnabled(bool enabled); + static bool checkIfRunning(); + static void setFastCheck(); + + static Daemon* mInstance; // only one instance allowed + static NotificationHandler* mDcopHandler; // handles DCOP requests from daemon + static QValueList<QString> mQueuedEvents; // IDs of pending events that daemon has triggered + static QValueList<QString> mSavingEvents; // IDs of updated events that are currently being saved + static QTimer* mStartTimer; // timer to check daemon status after starting daemon + static QTimer* mRegisterTimer; // timer to check whether daemon has sent registration status + static QTimer* mStatusTimer; // timer for checking daemon status + static int mStatusTimerCount; // countdown for fast status checking + static int mStatusTimerInterval; // timer interval (seconds) for checking daemon status + static int mStartTimeout; // remaining number of times to check if alarm daemon has started + static Status mStatus; // daemon status + static bool mRunning; // whether the alarm daemon is currently running + static bool mCalendarDisabled; // monitoring of calendar is currently disabled by daemon + static bool mEnableCalPending; // waiting to tell daemon to enable calendar + static bool mRegisterFailMsg; // true if registration failure message has been displayed + + friend class NotificationHandler; +}; + +/*============================================================================= += Class: AlarmEnableAction +=============================================================================*/ + +class AlarmEnableAction : public KToggleAction +{ + Q_OBJECT + public: + AlarmEnableAction(int accel, QObject* parent, const char* name = 0); + public slots: + void setCheckedActual(bool); // set state and emit switched() signal + virtual void setChecked(bool); // request state change and emit userClicked() signal + signals: + void switched(bool); // state has changed (KToggleAction::toggled() is only emitted when clicked by user) + void userClicked(bool); // user has clicked the control (param = desired state) + private: + bool mInitialised; +}; + +#endif // DAEMON_H diff --git a/kalarm/dcophandler.cpp b/kalarm/dcophandler.cpp new file mode 100644 index 000000000..ab4105fe6 --- /dev/null +++ b/kalarm/dcophandler.cpp @@ -0,0 +1,768 @@ +/* + * dcophandler.cpp - handler for DCOP calls by other applications + * Program: kalarm + * Copyright © 2002-2006,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <stdlib.h> + +#include <kdebug.h> + +#include "alarmcalendar.h" +#include "daemon.h" +#include "functions.h" +#include "kalarmapp.h" +#include "kamail.h" +#include "karecurrence.h" +#include "mainwindow.h" +#include "preferences.h" +#include "dcophandler.moc" + +static const char* DCOP_OBJECT_NAME = "request"; // DCOP name of KAlarm's request interface +#ifdef OLD_DCOP +static const char* DCOP_OLD_OBJECT_NAME = "display"; +#endif + + +/*============================================================================= += DcopHandler += This class's function is to handle DCOP requests by other applications. +=============================================================================*/ +DcopHandler::DcopHandler() + : DCOPObject(DCOP_OBJECT_NAME), + QWidget() +{ + kdDebug(5950) << "DcopHandler::DcopHandler()\n"; +} + + +bool DcopHandler::cancelEvent(const QString& url,const QString& eventId) +{ + return theApp()->deleteEvent(url, eventId); +} + +bool DcopHandler::triggerEvent(const QString& url,const QString& eventId) +{ + return theApp()->triggerEvent(url, eventId); +} + +bool DcopHandler::scheduleMessage(const QString& message, const QString& startDateTime, int lateCancel, unsigned flags, + const QString& bgColor, const QString& fgColor, const QString& font, + const KURL& audioFile, int reminderMins, const QString& recurrence, + int subRepeatInterval, int subRepeatCount) +{ + DateTime start; + KARecurrence recur; + if (!convertRecurrence(start, recur, startDateTime, recurrence, subRepeatInterval)) + return false; + return scheduleMessage(message, start, lateCancel, flags, bgColor, fgColor, font, audioFile, reminderMins, recur, subRepeatInterval, subRepeatCount); +} + +bool DcopHandler::scheduleMessage(const QString& message, const QString& startDateTime, int lateCancel, unsigned flags, + const QString& bgColor, const QString& fgColor, const QString& font, + const KURL& audioFile, int reminderMins, + int recurType, int recurInterval, int recurCount) +{ + DateTime start; + KARecurrence recur; + if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, recurCount)) + return false; + return scheduleMessage(message, start, lateCancel, flags, bgColor, fgColor, font, audioFile, reminderMins, recur); +} + +bool DcopHandler::scheduleMessage(const QString& message, const QString& startDateTime, int lateCancel, unsigned flags, + const QString& bgColor, const QString& fgColor, const QString& font, + const KURL& audioFile, int reminderMins, + int recurType, int recurInterval, const QString& endDateTime) +{ + DateTime start; + KARecurrence recur; + if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, endDateTime)) + return false; + return scheduleMessage(message, start, lateCancel, flags, bgColor, fgColor, font, audioFile, reminderMins, recur); +} + +bool DcopHandler::scheduleFile(const KURL& file, const QString& startDateTime, int lateCancel, unsigned flags, const QString& bgColor, + const KURL& audioFile, int reminderMins, const QString& recurrence, + int subRepeatInterval, int subRepeatCount) +{ + DateTime start; + KARecurrence recur; + if (!convertRecurrence(start, recur, startDateTime, recurrence, subRepeatInterval)) + return false; + return scheduleFile(file, start, lateCancel, flags, bgColor, audioFile, reminderMins, recur, subRepeatInterval, subRepeatCount); +} + +bool DcopHandler::scheduleFile(const KURL& file, const QString& startDateTime, int lateCancel, unsigned flags, const QString& bgColor, + const KURL& audioFile, int reminderMins, int recurType, int recurInterval, int recurCount) +{ + DateTime start; + KARecurrence recur; + if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, recurCount)) + return false; + return scheduleFile(file, start, lateCancel, flags, bgColor, audioFile, reminderMins, recur); +} + +bool DcopHandler::scheduleFile(const KURL& file, const QString& startDateTime, int lateCancel, unsigned flags, const QString& bgColor, + const KURL& audioFile, int reminderMins, int recurType, int recurInterval, const QString& endDateTime) +{ + DateTime start; + KARecurrence recur; + if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, endDateTime)) + return false; + return scheduleFile(file, start, lateCancel, flags, bgColor, audioFile, reminderMins, recur); +} + +bool DcopHandler::scheduleCommand(const QString& commandLine, const QString& startDateTime, int lateCancel, unsigned flags, + const QString& recurrence, int subRepeatInterval, int subRepeatCount) +{ + DateTime start; + KARecurrence recur; + if (!convertRecurrence(start, recur, startDateTime, recurrence, subRepeatInterval)) + return false; + return scheduleCommand(commandLine, start, lateCancel, flags, recur, subRepeatInterval, subRepeatCount); +} + +bool DcopHandler::scheduleCommand(const QString& commandLine, const QString& startDateTime, int lateCancel, unsigned flags, + int recurType, int recurInterval, int recurCount) +{ + DateTime start = convertStartDateTime(startDateTime); + if (!start.isValid()) + return false; + KARecurrence recur; + if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, recurCount)) + return false; + return scheduleCommand(commandLine, start, lateCancel, flags, recur); +} + +bool DcopHandler::scheduleCommand(const QString& commandLine, const QString& startDateTime, int lateCancel, unsigned flags, + int recurType, int recurInterval, const QString& endDateTime) +{ + DateTime start; + KARecurrence recur; + if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, endDateTime)) + return false; + return scheduleCommand(commandLine, start, lateCancel, flags, recur); +} + +bool DcopHandler::scheduleEmail(const QString& fromID, const QString& addresses, const QString& subject, const QString& message, + const QString& attachments, const QString& startDateTime, int lateCancel, unsigned flags, + const QString& recurrence, int subRepeatInterval, int subRepeatCount) +{ + DateTime start; + KARecurrence recur; + if (!convertRecurrence(start, recur, startDateTime, recurrence, subRepeatInterval)) + return false; + return scheduleEmail(fromID, addresses, subject, message, attachments, start, lateCancel, flags, recur, subRepeatInterval, subRepeatCount); +} + +bool DcopHandler::scheduleEmail(const QString& fromID, const QString& addresses, const QString& subject, const QString& message, + const QString& attachments, const QString& startDateTime, int lateCancel, unsigned flags, + int recurType, int recurInterval, int recurCount) +{ + DateTime start; + KARecurrence recur; + if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, recurCount)) + return false; + return scheduleEmail(fromID, addresses, subject, message, attachments, start, lateCancel, flags, recur); +} + +bool DcopHandler::scheduleEmail(const QString& fromID, const QString& addresses, const QString& subject, const QString& message, + const QString& attachments, const QString& startDateTime, int lateCancel, unsigned flags, + int recurType, int recurInterval, const QString& endDateTime) +{ + DateTime start; + KARecurrence recur; + if (!convertRecurrence(start, recur, startDateTime, recurType, recurInterval, endDateTime)) + return false; + return scheduleEmail(fromID, addresses, subject, message, attachments, start, lateCancel, flags, recur); +} + +bool DcopHandler::edit(const QString& eventID) +{ + return KAlarm::edit(eventID); +} + +bool DcopHandler::editNew(const QString& templateName) +{ + return KAlarm::editNew(templateName); +} + + +/****************************************************************************** +* Schedule a message alarm, after converting the parameters from strings. +*/ +bool DcopHandler::scheduleMessage(const QString& message, const DateTime& start, int lateCancel, unsigned flags, + const QString& bgColor, const QString& fgColor, const QString& fontStr, + const KURL& audioFile, int reminderMins, const KARecurrence& recurrence, + int subRepeatInterval, int subRepeatCount) +{ + unsigned kaEventFlags = convertStartFlags(start, flags); + QColor bg = convertBgColour(bgColor); + if (!bg.isValid()) + return false; + QColor fg; + if (fgColor.isEmpty()) + fg = Preferences::defaultFgColour(); + else + { + fg.setNamedColor(fgColor); + if (!fg.isValid()) + { + kdError(5950) << "DCOP call: invalid foreground color: " << fgColor << endl; + return false; + } + } + QFont font; + if (fontStr.isEmpty()) + kaEventFlags |= KAEvent::DEFAULT_FONT; + else + { + if (!font.fromString(fontStr)) // N.B. this doesn't do good validation + { + kdError(5950) << "DCOP call: invalid font: " << fontStr << endl; + return false; + } + } + return theApp()->scheduleEvent(KAEvent::MESSAGE, message, start.dateTime(), lateCancel, kaEventFlags, bg, fg, font, + audioFile.url(), -1, reminderMins, recurrence, subRepeatInterval, subRepeatCount); +} + +/****************************************************************************** +* Schedule a file alarm, after converting the parameters from strings. +*/ +bool DcopHandler::scheduleFile(const KURL& file, + const DateTime& start, int lateCancel, unsigned flags, const QString& bgColor, + const KURL& audioFile, int reminderMins, const KARecurrence& recurrence, + int subRepeatInterval, int subRepeatCount) +{ + unsigned kaEventFlags = convertStartFlags(start, flags); + QColor bg = convertBgColour(bgColor); + if (!bg.isValid()) + return false; + return theApp()->scheduleEvent(KAEvent::FILE, file.url(), start.dateTime(), lateCancel, kaEventFlags, bg, Qt::black, QFont(), + audioFile.url(), -1, reminderMins, recurrence, subRepeatInterval, subRepeatCount); +} + +/****************************************************************************** +* Schedule a command alarm, after converting the parameters from strings. +*/ +bool DcopHandler::scheduleCommand(const QString& commandLine, + const DateTime& start, int lateCancel, unsigned flags, + const KARecurrence& recurrence, int subRepeatInterval, int subRepeatCount) +{ + unsigned kaEventFlags = convertStartFlags(start, flags); + return theApp()->scheduleEvent(KAEvent::COMMAND, commandLine, start.dateTime(), lateCancel, kaEventFlags, Qt::black, Qt::black, QFont(), + QString::null, -1, 0, recurrence, subRepeatInterval, subRepeatCount); +} + +/****************************************************************************** +* Schedule an email alarm, after validating the addresses and attachments. +*/ +bool DcopHandler::scheduleEmail(const QString& fromID, const QString& addresses, const QString& subject, + const QString& message, const QString& attachments, + const DateTime& start, int lateCancel, unsigned flags, + const KARecurrence& recurrence, int subRepeatInterval, int subRepeatCount) +{ + unsigned kaEventFlags = convertStartFlags(start, flags); + uint senderId = 0; + if (!fromID.isEmpty()) + { + senderId = KAMail::identityUoid(fromID); + if (!senderId) + { + kdError(5950) << "DCOP call scheduleEmail(): unknown sender ID: " << fromID << endl; + return false; + } + } + EmailAddressList addrs; + QString bad = KAMail::convertAddresses(addresses, addrs); + if (!bad.isEmpty()) + { + kdError(5950) << "DCOP call scheduleEmail(): invalid email addresses: " << bad << endl; + return false; + } + if (addrs.isEmpty()) + { + kdError(5950) << "DCOP call scheduleEmail(): no email address\n"; + return false; + } + QStringList atts; + bad = KAMail::convertAttachments(attachments, atts); + if (!bad.isEmpty()) + { + kdError(5950) << "DCOP call scheduleEmail(): invalid email attachment: " << bad << endl; + return false; + } + return theApp()->scheduleEvent(KAEvent::EMAIL, message, start.dateTime(), lateCancel, kaEventFlags, Qt::black, Qt::black, QFont(), + QString::null, -1, 0, recurrence, subRepeatInterval, subRepeatCount, senderId, addrs, subject, atts); +} + + +/****************************************************************************** +* Convert the start date/time string to a DateTime. The date/time string is in +* the format YYYY-MM-DD[THH:MM[:SS]] or [T]HH:MM[:SS] +*/ +DateTime DcopHandler::convertStartDateTime(const QString& startDateTime) +{ + DateTime start; + if (startDateTime.length() > 10) + { + // Both a date and a time are specified + start = QDateTime::fromString(startDateTime, Qt::ISODate); + } + else + { + // Check whether a time is specified + QString t; + if (startDateTime[0] == 'T') + t = startDateTime.mid(1); // it's a time: remove the leading 'T' + else if (!startDateTime[2].isDigit()) + t = startDateTime; // it's a time with no leading 'T' + + if (t.isEmpty()) + { + // It's a date + start = QDate::fromString(startDateTime, Qt::ISODate); + } + else + { + // It's a time, so use today as the date + start.set(QDate::currentDate(), QTime::fromString(t, Qt::ISODate)); + } + } + if (!start.isValid()) + kdError(5950) << "DCOP call: invalid start date/time: " << startDateTime << endl; + return start; +} + +/****************************************************************************** +* Convert the flag bits to KAEvent flag bits. +*/ +unsigned DcopHandler::convertStartFlags(const DateTime& start, unsigned flags) +{ + unsigned kaEventFlags = 0; + if (flags & REPEAT_AT_LOGIN) kaEventFlags |= KAEvent::REPEAT_AT_LOGIN; + if (flags & BEEP) kaEventFlags |= KAEvent::BEEP; + if (flags & SPEAK) kaEventFlags |= KAEvent::SPEAK; + if (flags & CONFIRM_ACK) kaEventFlags |= KAEvent::CONFIRM_ACK; + if (flags & REPEAT_SOUND) kaEventFlags |= KAEvent::REPEAT_SOUND; + if (flags & AUTO_CLOSE) kaEventFlags |= KAEvent::AUTO_CLOSE; + if (flags & EMAIL_BCC) kaEventFlags |= KAEvent::EMAIL_BCC; + if (flags & SCRIPT) kaEventFlags |= KAEvent::SCRIPT; + if (flags & EXEC_IN_XTERM) kaEventFlags |= KAEvent::EXEC_IN_XTERM; + if (flags & SHOW_IN_KORG) kaEventFlags |= KAEvent::COPY_KORGANIZER; + if (flags & DISABLED) kaEventFlags |= KAEvent::DISABLED; + if (start.isDateOnly()) kaEventFlags |= KAEvent::ANY_TIME; + return kaEventFlags; +} + +/****************************************************************************** +* Convert the background colour string to a QColor. +*/ +QColor DcopHandler::convertBgColour(const QString& bgColor) +{ + if (bgColor.isEmpty()) + return Preferences::defaultBgColour(); + QColor bg(bgColor); + if (!bg.isValid()) + kdError(5950) << "DCOP call: invalid background color: " << bgColor << endl; + return bg; +} + +bool DcopHandler::convertRecurrence(DateTime& start, KARecurrence& recurrence, + const QString& startDateTime, const QString& icalRecurrence, + int& subRepeatInterval) +{ + start = convertStartDateTime(startDateTime); + if (!start.isValid()) + return false; + if (!recurrence.set(icalRecurrence)) + return false; + if (subRepeatInterval && recurrence.type() == KARecurrence::NO_RECUR) + { + subRepeatInterval = 0; + kdWarning(5950) << "DCOP call: no recurrence specified, so sub-repetition ignored" << endl; + } + return true; +} + +bool DcopHandler::convertRecurrence(DateTime& start, KARecurrence& recurrence, const QString& startDateTime, + int recurType, int recurInterval, int recurCount) +{ + start = convertStartDateTime(startDateTime); + if (!start.isValid()) + return false; + return convertRecurrence(recurrence, start, recurType, recurInterval, recurCount, QDateTime()); +} + +bool DcopHandler::convertRecurrence(DateTime& start, KARecurrence& recurrence, const QString& startDateTime, + int recurType, int recurInterval, const QString& endDateTime) +{ + start = convertStartDateTime(startDateTime); + if (!start.isValid()) + return false; + QDateTime end; + if (endDateTime.find('T') < 0) + { + if (!start.isDateOnly()) + { + kdError(5950) << "DCOP call: alarm is date-only, but recurrence end is date/time" << endl; + return false; + } + end.setDate(QDate::fromString(endDateTime, Qt::ISODate)); + } + else + { + if (start.isDateOnly()) + { + kdError(5950) << "DCOP call: alarm is timed, but recurrence end is date-only" << endl; + return false; + } + end = QDateTime::fromString(endDateTime, Qt::ISODate); + } + if (!end.isValid()) + { + kdError(5950) << "DCOP call: invalid recurrence end date/time: " << endDateTime << endl; + return false; + } + return convertRecurrence(recurrence, start, recurType, recurInterval, 0, end); +} + +bool DcopHandler::convertRecurrence(KARecurrence& recurrence, const DateTime& start, int recurType, + int recurInterval, int recurCount, const QDateTime& end) +{ + KARecurrence::Type type; + switch (recurType) + { + case MINUTELY: type = KARecurrence::MINUTELY; break; + case DAILY: type = KARecurrence::DAILY; break; + case WEEKLY: type = KARecurrence::WEEKLY; break; + case MONTHLY: type = KARecurrence::MONTHLY_DAY; break; + case YEARLY: type = KARecurrence::ANNUAL_DATE; break; + break; + default: + kdError(5950) << "DCOP call: invalid recurrence type: " << recurType << endl; + return false; + } + recurrence.set(type, recurInterval, recurCount, start, end); + return true; +} + + +#ifdef OLD_DCOP +/*============================================================================= += DcopHandlerOld += This class's function is simply to act as a receiver for DCOP requests. +=============================================================================*/ +DcopHandlerOld::DcopHandlerOld() + : QWidget(), + DCOPObject(DCOP_OLD_OBJECT_NAME) +{ + kdDebug(5950) << "DcopHandlerOld::DcopHandlerOld()\n"; +} + +/****************************************************************************** +* Process a DCOP request. +*/ +bool DcopHandlerOld::process(const QCString& func, const QByteArray& data, QCString& replyType, QByteArray&) +{ + kdDebug(5950) << "DcopHandlerOld::process(): " << func << endl; + enum + { + OPERATION = 0x0007, // mask for main operation + HANDLE = 0x0001, + CANCEL = 0x0002, + TRIGGER = 0x0003, + SCHEDULE = 0x0004, + ALARM_TYPE = 0x00F0, // mask for SCHEDULE alarm type + MESSAGE = 0x0010, + FILE = 0x0020, + COMMAND = 0x0030, + EMAIL = 0x0040, + SCH_FLAGS = 0x0F00, // mask for SCHEDULE flags + REP_COUNT = 0x0100, + REP_END = 0x0200, + FONT = 0x0400, + PRE_096 = 0x1000, // old-style pre-0.9.6 deprecated method + PRE_091 = 0x2000 | PRE_096 // old-style pre-0.9.1 deprecated method + }; + replyType = "void"; + int function; + if (func == "handleEvent(const QString&,const QString&)" + || func == "handleEvent(QString,QString)") + function = HANDLE; + else if (func == "cancelEvent(const QString&,const QString&)" + || func == "cancelEvent(QString,QString)") + function = CANCEL; + else if (func == "triggerEvent(const QString&,const QString&)" + || func == "triggerEvent(QString,QString)") + function = TRIGGER; + + // scheduleMessage(message, dateTime, colour, colourfg, flags, audioURL, reminder, recurrence) + else if (func == "scheduleMessage(const QString&,const QDateTime&,const QColor&,const QColor&,Q_UINT32,const QString&,Q_INT32,const QString&)" + || func == "scheduleMessage(QString,QDateTime,QColor,QColor,Q_UINT32,QString,Q_UINT32,QString)") + function = SCHEDULE | MESSAGE; + // scheduleMessage(message, dateTime, colour, colourfg, font, flags, audioURL, reminder, recurrence) + else if (func == "scheduleMessage(const QString&,const QDateTime&,const QColor&,const QColor&,const QFont&,Q_UINT32,const QString&,Q_INT32,const QString&)" + || func == "scheduleMessage(QString,QDateTime,QColor,QColor,QFont,Q_UINT32,QString,Q_UINT32,QString)") + function = SCHEDULE | MESSAGE | FONT; + // scheduleFile(URL, dateTime, colour, flags, audioURL, reminder, recurrence) + else if (func == "scheduleFile(const QString&,const QDateTime&,const QColor&,Q_UINT32,const QString&,Q_INT32,Q_INT32,const QString&)" + || func == "scheduleFile(QString,QDateTime,QColor,Q_UINT32,QString,Q_UINT32,QString)") + function = SCHEDULE | FILE; + // scheduleCommand(commandLine, dateTime, flags, recurrence) + else if (func == "scheduleCommand(const QString&,const QDateTime&,Q_UINT32,const QString&)" + || func == "scheduleCommand(QString,QDateTime,Q_UINT32,QString)") + function = SCHEDULE | COMMAND; + // scheduleEmail(addresses, subject, message, attachments, dateTime, flags, recurrence) + else if (func == "scheduleEmail(const QString&,const QString&,const QString&,const QString&,const QDateTime&,Q_UINT32,const QString&)" + || func == "scheduleEmail(QString,QString,QString,QString,QDateTime,Q_UINT32,QString)") + function = SCHEDULE | EMAIL; + + // scheduleMessage(message, dateTime, colour, colourfg, flags, audioURL, reminder, recurType, interval, recurCount) + else if (func == "scheduleMessage(const QString&,const QDateTime&,const QColor&,const QColor&,Q_UINT32,const QString&,Q_INT32,Q_INT32,Q_INT32,Q_INT32)" + || func == "scheduleMessage(QString,QDateTime,QColor,QColor,Q_UINT32,QString,Q_INT32,Q_INT32,Q_INT32,Q_INT32)") + function = SCHEDULE | MESSAGE | REP_COUNT; + // scheduleFile(URL, dateTime, colour, flags, audioURL, reminder, recurType, interval, recurCount) + else if (func == "scheduleFile(const QString&,const QDateTime&,const QColor&,Q_UINT32,const QString&,Q_INT32,Q_INT32,Q_INT32,Q_INT32)" + || func == "scheduleFile(QString,QDateTime,QColor,Q_UINT32,QString,Q_INT32,Q_INT32,Q_INT32,Q_INT32)") + function = SCHEDULE | FILE | REP_COUNT; + // scheduleCommand(commandLine, dateTime, flags, recurType, interval, recurCount) + else if (func == "scheduleCommand(const QString&,const QDateTime&,Q_UINT32,Q_INT32,Q_INT32,Q_INT32)" + || func == "scheduleCommand(QString,QDateTime,Q_UINT32,Q_INT32,Q_INT32,Q_INT32)") + function = SCHEDULE | COMMAND | REP_COUNT; + // scheduleEmail(addresses, subject, message, attachments, dateTime, flags, recurType, interval, recurCount) + else if (func == "scheduleEmail(const QString&,const QString&,const QString&,const QString&,const QDateTime&,Q_UINT32,Q_INT32,Q_INT32,Q_INT32)" + || func == "scheduleEmail(QString,QString,QString,QString,QDateTime,Q_UINT32,Q_INT32,Q_INT32,Q_INT32)") + function = SCHEDULE | EMAIL | REP_COUNT; + + // scheduleMessage(message, dateTime, colour, colourfg, flags, audioURL, reminder, recurType, interval, endTime) + else if (func == "scheduleMessage(const QString&,const QDateTime&,const QColor&,const QColor&,Q_UINT32,const QString&,Q_INT32,Q_INT32,Q_INT32,const QDateTime&)" + || func == "scheduleMessage(QString,QDateTime,QColor,QColor,Q_UINT32,QString,Q_INT32,Q_INT32,Q_INT32,QDateTime)") + function = SCHEDULE | MESSAGE | REP_END; + // scheduleFile(URL, dateTime, colour, flags, audioURL, reminder, recurType, interval, endTime) + else if (func == "scheduleFile(const QString&,const QDateTime&,const QColor&,Q_UINT32,const QString&,Q_INT32,Q_INT32,Q_INT32,const QDateTime&)" + || func == "scheduleFile(QString,QDateTime,QColor,Q_UINT32,QString,Q_INT32,Q_INT32,Q_INT32,QDateTime)") + function = SCHEDULE | FILE | REP_END; + // scheduleCommand(commandLine, dateTime, flags, recurType, interval, endTime) + else if (func == "scheduleCommand(const QString&,const QDateTime&,Q_UINT32,Q_INT32,Q_INT32,const QDateTime&)" + || func == "scheduleCommand(QString,QDateTime,Q_UINT32,Q_INT32,Q_INT32,QDateTime)") + function = SCHEDULE | COMMAND | REP_END; + // scheduleEmail(addresses, subject, message, attachments, dateTime, flags, recurType, interval, endTime) + else if (func == "scheduleEmail(const QString&,const QString&,const QString&,const QString&,const QDateTime&,Q_UINT32,Q_INT32,Q_INT32,const QDateTime&)" + || func == "scheduleEmail(QString,QString,QString,QString,QDateTime,Q_UINT32,Q_INT32,Q_INT32,QDateTime)") + function = SCHEDULE | EMAIL | REP_END; + + // Deprecated methods: backwards compatibility with KAlarm pre-0.9.6 + // scheduleMessage(message, dateTime, colour, flags, audioURL, reminder, recurrence) + else if (func == "scheduleMessage(const QString&,const QDateTime&,const QColor&,Q_UINT32,const QString&,Q_INT32,const QString&)" + || func == "scheduleMessage(QString,QDateTime,QColor,Q_UINT32,QString,Q_UINT32,QString)") + function = SCHEDULE | MESSAGE | PRE_096; + // scheduleMessage(message, dateTime, colour, font, flags, audioURL, reminder, recurrence) + else if (func == "scheduleMessage(const QString&,const QDateTime&,const QColor&,const QFont&,Q_UINT32,const QString&,Q_INT32,const QString&)" + || func == "scheduleMessage(QString,QDateTime,QColor,QFont,Q_UINT32,QString,Q_UINT32,QString)") + function = SCHEDULE | MESSAGE | FONT | PRE_096; + // scheduleMessage(message, dateTime, colour, flags, audioURL, reminder, recurType, interval, recurCount) + else if (func == "scheduleMessage(const QString&,const QDateTime&,const QColor&,Q_UINT32,const QString&,Q_INT32,Q_INT32,Q_INT32,Q_INT32)" + || func == "scheduleMessage(QString,QDateTime,QColor,Q_UINT32,QString,Q_INT32,Q_INT32,Q_INT32,Q_INT32)") + function = SCHEDULE | MESSAGE | REP_COUNT | PRE_096; + // scheduleMessage(message, dateTime, colour, flags, audioURL, reminder, recurType, interval, endTime) + else if (func == "scheduleMessage(const QString&,const QDateTime&,const QColor&,Q_UINT32,const QString&,Q_INT32,Q_INT32,Q_INT32,const QDateTime&)" + || func == "scheduleMessage(QString,QDateTime,QColor,Q_UINT32,QString,Q_INT32,Q_INT32,Q_INT32,QDateTime)") + function = SCHEDULE | MESSAGE | REP_END | PRE_096; + + // Deprecated methods: backwards compatibility with KAlarm pre-0.9.1 + // scheduleMessage(message, dateTime, colour, flags, audioURL) + else if (func == "scheduleMessage(const QString&,const QDateTime&,const QColor&,Q_UINT32,const QString&)" + || func == "scheduleMessage(QString,QDateTime,QColor,Q_UINT32,QString)") + function = SCHEDULE | MESSAGE | PRE_091; + // scheduleFile(URL, dateTime, colour, flags, audioURL) + else if (func == "scheduleFile(const QString&,const QDateTime&,const QColor&,Q_UINT32,const QString&)" + || func == "scheduleFile(QString,QDateTime,QColor,Q_UINT32,QString)") + function = SCHEDULE | FILE | PRE_091; + // scheduleMessage(message, dateTime, colour, flags, audioURL, recurType, interval, recurCount) + else if (func == "scheduleMessage(const QString&,const QDateTime&,const QColor&,Q_UINT32,const QString&,Q_INT32,Q_INT32,Q_INT32)" + || func == "scheduleMessage(QString,QDateTime,QColor,Q_UINT32,QString,Q_INT32,Q_INT32,Q_INT32)") + function = SCHEDULE | MESSAGE | REP_COUNT | PRE_091; + // scheduleFile(URL, dateTime, colour, flags, audioURL, recurType, interval, recurCount) + else if (func == "scheduleFile(const QString&,const QDateTime&,const QColor&,Q_UINT32,const QString&,Q_INT32,Q_INT32,Q_INT32)" + || func == "scheduleFile(QString,QDateTime,QColor,Q_UINT32,QString,Q_INT32,Q_INT32,Q_INT32)") + function = SCHEDULE | FILE | REP_COUNT | PRE_091; + // scheduleMessage(message, dateTime, colour, flags, audioURL, recurType, interval, endTime) + else if (func == "scheduleMessage(const QString&,const QDateTime&,const QColor&,Q_UINT32,const QString&,Q_INT32,Q_INT32,const QDateTime&)" + || func == "scheduleMessage(QString,QDateTime,QColor,Q_UINT32,QString,Q_INT32,Q_INT32,QDateTime)") + function = SCHEDULE | MESSAGE | REP_END | PRE_091; + // scheduleFile(URL, dateTime, colour, flags, audioURL, recurType, interval, endTime) + else if (func == "scheduleFile(const QString&,const QDateTime&,const QColor&,Q_UINT32,const QString&,Q_INT32,Q_INT32,const QDateTime&)" + || func == "scheduleFile(QString,QDateTime,QColor,Q_UINT32,QString,Q_INT32,Q_INT32,QDateTime)") + function = SCHEDULE | FILE | REP_END | PRE_091; + + // Obsolete methods: backwards compatibility with KAlarm pre-0.7 + else if (func == "scheduleMessage(const QString&,const QDateTime&,const QColor&,Q_UINT32,Q_INT32,Q_INT32)" + || func == "scheduleMessage(QString,QDateTime,QColor,Q_UINT32,Q_INT32,Q_INT32)" + || func == "scheduleFile(const QString&,const QDateTime&,const QColor&,Q_UINT32,Q_INT32,Q_INT32)" + || func == "scheduleFile(QString,QDateTime,QColor,Q_UINT32,Q_INT32,Q_INT32)" + || func == "scheduleCommand(const QString&,const QDateTime&,Q_UINT32,Q_INT32,Q_INT32)" + || func == "scheduleCommand(QString,QDateTime,Q_UINT32,Q_INT32,Q_INT32)" + // Obsolete methods: backwards compatibility with KAlarm pre-0.6 + || func == "cancelMessage(const QString&,const QString&)" + || func == "cancelMessage(QString,QString)" + || func == "displayMessage(const QString&,const QString&)" + || func == "displayMessage(QString,QString)") + { + kdError(5950) << "DcopHandlerOld::process(): obsolete DCOP function call: '" << func << "'" << endl; + return false; + } + else + { + kdError(5950) << "DcopHandlerOld::process(): unknown DCOP function" << endl; + return false; + } + + switch (function & OPERATION) + { + case HANDLE: // trigger or cancel event with specified ID from calendar file + case CANCEL: // cancel event with specified ID from calendar file + case TRIGGER: // trigger event with specified ID in calendar file + { + + QDataStream arg(data, IO_ReadOnly); + QString urlString, vuid; + arg >> urlString >> vuid; + switch (function) + { + case HANDLE: + return theApp()->handleEvent(urlString, vuid); + case CANCEL: + return theApp()->deleteEvent(urlString, vuid); + case TRIGGER: + return theApp()->triggerEvent(urlString, vuid); + } + break; + } + case SCHEDULE: // schedule a new event + { + KAEvent::Action action; + switch (function & ALARM_TYPE) + { + case MESSAGE: action = KAEvent::MESSAGE; break; + case FILE: action = KAEvent::FILE; break; + case COMMAND: action = KAEvent::COMMAND; break; + case EMAIL: action = KAEvent::EMAIL; break; + default: return false; + } + QDataStream arg(data, IO_ReadOnly); + QString text, audioFile, mailSubject; + float audioVolume = -1; + EmailAddressList mailAddresses; + QStringList mailAttachments; + QDateTime dateTime, endTime; + QColor bgColour; + QColor fgColour(Qt::black); + QFont font; + Q_UINT32 flags; + int lateCancel = 0; + KARecurrence recurrence; + Q_INT32 reminderMinutes = 0; + if (action == KAEvent::EMAIL) + { + QString addresses, attachments; + arg >> addresses >> mailSubject >> text >> attachments; + QString bad = KAMail::convertAddresses(addresses, mailAddresses); + if (!bad.isEmpty()) + { + kdError(5950) << "DcopHandlerOld::process(): invalid email addresses: " << bad << endl; + return false; + } + if (mailAddresses.isEmpty()) + { + kdError(5950) << "DcopHandlerOld::process(): no email address\n"; + return false; + } + bad = KAMail::convertAttachments(attachments, mailAttachments); + if (!bad.isEmpty()) + { + kdError(5950) << "DcopHandlerOld::process(): invalid email attachment: " << bad << endl; + return false; + } + } + else + arg >> text; + arg.readRawBytes((char*)&dateTime, sizeof(dateTime)); + if (action != KAEvent::COMMAND) + arg.readRawBytes((char*)&bgColour, sizeof(bgColour)); + if (action == KAEvent::MESSAGE && !(function & PRE_096)) + arg.readRawBytes((char*)&fgColour, sizeof(fgColour)); + if (function & FONT) + { + arg.readRawBytes((char*)&font, sizeof(font)); + arg >> flags; + } + else + { + arg >> flags; + flags |= KAEvent::DEFAULT_FONT; + } + if (flags & KAEvent::LATE_CANCEL) + lateCancel = 1; + if (action == KAEvent::MESSAGE || action == KAEvent::FILE) + { + arg >> audioFile; + if (!(function & PRE_091)) + arg >> reminderMinutes; + } + if (function & (REP_COUNT | REP_END)) + { + KARecurrence::Type recurType; + Q_INT32 recurCount = 0; + Q_INT32 recurInterval; + Q_INT32 type; + arg >> type >> recurInterval; + switch (type) + { + case 1: recurType = KARecurrence::MINUTELY; break; + case 3: recurType = KARecurrence::DAILY; break; + case 4: recurType = KARecurrence::WEEKLY; break; + case 6: recurType = KARecurrence::MONTHLY_DAY; break; + case 7: recurType = KARecurrence::ANNUAL_DATE; break; + default: + kdError(5950) << "DcopHandlerOld::process(): invalid simple repetition type: " << type << endl; + return false; + } + if (function & REP_COUNT) + arg >> recurCount; + else + arg.readRawBytes((char*)&endTime, sizeof(endTime)); + recurrence.set(recurType, recurInterval, recurCount, + DateTime(dateTime, flags & KAEvent::ANY_TIME), endTime); + } + else if (!(function & PRE_091)) + { + QString rule; + arg >> rule; + recurrence.set(rule); + } + return theApp()->scheduleEvent(action, text, dateTime, lateCancel, flags, bgColour, fgColour, font, audioFile, + audioVolume, reminderMinutes, recurrence, 0, 0, 0, mailAddresses, mailSubject, mailAttachments); + } + } + return false; +} +#endif // OLD_DCOP diff --git a/kalarm/dcophandler.h b/kalarm/dcophandler.h new file mode 100644 index 000000000..1ea6f05e5 --- /dev/null +++ b/kalarm/dcophandler.h @@ -0,0 +1,107 @@ +/* + * dcophandler.h - handler for DCOP calls by other applications + * Program: kalarm + * Copyright © 2001,2002,2004-2006,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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 + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef DCOPHANDLER_H +#define DCOPHANDLER_H + +#include <qwidget.h> +#include <dcopobject.h> + +#include "datetime.h" +#include "kalarmiface.h" + + +class DcopHandler : public QWidget, virtual public KAlarmIface +{ + public: + DcopHandler(); + virtual bool cancelEvent(const QString& url,const QString& eventId); + virtual bool triggerEvent(const QString& url,const QString& eventId); + + virtual bool scheduleMessage(const QString& message, const QString& startDateTime, int lateCancel, unsigned flags, + const QString& bgColor, const QString& fgColor, const QString& font, + const KURL& audioFile, int reminderMins, const QString& recurrence, + int subRepeatInterval, int subRepeatCount); + virtual bool scheduleMessage(const QString& message, const QString& startDateTime, int lateCancel, unsigned flags, + const QString& bgColor, const QString& fgColor, const QString& font, + const KURL& audioFile, int reminderMins, int recurType, int recurInterval, int recurCount); + virtual bool scheduleMessage(const QString& message, const QString& startDateTime, int lateCancel, unsigned flags, + const QString& bgColor, const QString& fgColor, const QString& font, + const KURL& audioFile, int reminderMins, int recurType, int recurInterval, const QString& endDateTime); + virtual bool scheduleFile(const KURL& file, const QString& startDateTime, int lateCancel, unsigned flags, const QString& bgColor, + const KURL& audioFile, int reminderMins, const QString& recurrence, + int subRepeatInterval, int subRepeatCount); + virtual bool scheduleFile(const KURL& file, const QString& startDateTime, int lateCancel, unsigned flags, const QString& bgColor, + const KURL& audioFile, int reminderMins, int recurType, int recurInterval, int recurCount); + virtual bool scheduleFile(const KURL& file, const QString& startDateTime, int lateCancel, unsigned flags, const QString& bgColor, + const KURL& audioFile, int reminderMins, int recurType, int recurInterval, const QString& endDateTime); + virtual bool scheduleCommand(const QString& commandLine, const QString& startDateTime, int lateCancel, unsigned flags, + const QString& recurrence, int subRepeatInterval, int subRepeatCount); + virtual bool scheduleCommand(const QString& commandLine, const QString& startDateTime, int lateCancel, unsigned flags, + int recurType, int recurInterval, int recurCount); + virtual bool scheduleCommand(const QString& commandLine, const QString& startDateTime, int lateCancel, unsigned flags, + int recurType, int recurInterval, const QString& endDateTime); + virtual bool scheduleEmail(const QString& fromID, const QString& addresses, const QString& subject, const QString& message, + const QString& attachments, const QString& startDateTime, int lateCancel, unsigned flags, + const QString& recurrence, int recurInterval, int recurCount); + virtual bool scheduleEmail(const QString& fromID, const QString& addresses, const QString& subject, const QString& message, + const QString& attachments, const QString& startDateTime, int lateCancel, unsigned flags, + int recurType, int recurInterval, int recurCount); + virtual bool scheduleEmail(const QString& fromID, const QString& addresses, const QString& subject, const QString& message, + const QString& attachments, const QString& startDateTime, int lateCancel, unsigned flags, + int recurType, int recurInterval, const QString& endDateTime); + virtual bool edit(const QString& eventID); + virtual bool editNew(const QString& templateName); + + private: + static bool scheduleMessage(const QString& message, const DateTime& start, int lateCancel, unsigned flags, + const QString& bgColor, const QString& fgColor, const QString& fontStr, + const KURL& audioFile, int reminderMins, const KARecurrence&, + int subRepeatInterval = 0, int subRepeatCount = 0); + static bool scheduleFile(const KURL& file, const DateTime& start, int lateCancel, unsigned flags, const QString& bgColor, + const KURL& audioFile, int reminderMins, const KARecurrence&, + int subRepeatInterval = 0, int subRepeatCount = 0); + static bool scheduleCommand(const QString& commandLine, const DateTime& start, int lateCancel, unsigned flags, + const KARecurrence&, int subRepeatInterval = 0, int subRepeatCount = 0); + static bool scheduleEmail(const QString& fromID, const QString& addresses, const QString& subject, const QString& message, + const QString& attachments, const DateTime& start, int lateCancel, unsigned flags, + const KARecurrence&, int subRepeatInterval = 0, int subRepeatCount = 0); + static DateTime convertStartDateTime(const QString& startDateTime); + static unsigned convertStartFlags(const DateTime& start, unsigned flags); + static QColor convertBgColour(const QString& bgColor); + static bool convertRecurrence(DateTime& start, KARecurrence&, const QString& startDateTime, const QString& icalRecurrence, int& subRepeatInterval); + static bool convertRecurrence(DateTime& start, KARecurrence&, const QString& startDateTime, int recurType, int recurInterval, int recurCount); + static bool convertRecurrence(DateTime& start, KARecurrence&, const QString& startDateTime, int recurType, int recurInterval, const QString& endDateTime); + static bool convertRecurrence(KARecurrence&, const DateTime& start, int recurType, int recurInterval, int recurCount, const QDateTime& end); +}; + + +#ifdef OLD_DCOP +class DcopHandlerOld : public QWidget, public DCOPObject +{ + Q_OBJECT + public: + DcopHandlerOld(); + ~DcopHandlerOld() { } + virtual bool process(const QCString& func, const QByteArray& data, QCString& replyType, QByteArray& replyData); +}; +#endif + +#endif // DCOPHANDLER_H diff --git a/kalarm/deferdlg.cpp b/kalarm/deferdlg.cpp new file mode 100644 index 000000000..5e0e039c4 --- /dev/null +++ b/kalarm/deferdlg.cpp @@ -0,0 +1,177 @@ +/* + * deferdlg.cpp - dialogue to defer an alarm + * Program: kalarm + * Copyright © 2002-2006,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qlayout.h> + +#include <kglobal.h> +#include <klocale.h> +#include <kmessagebox.h> +#include <kdebug.h> + +#include <libkcal/event.h> +#include <libkcal/recurrence.h> + +#include "alarmcalendar.h" +#include "alarmevent.h" +#include "alarmtimewidget.h" +#include "datetime.h" +#include "functions.h" +#include "kalarmapp.h" +#include "deferdlg.moc" + + +DeferAlarmDlg::DeferAlarmDlg(const QString& caption, const DateTime& initialDT, + bool cancelButton, QWidget* parent, const char* name) + : KDialogBase(parent, name, true, caption, Ok|Cancel|User1, Ok, false, i18n("Cancel &Deferral")) +{ + if (!cancelButton) + showButton(User1, false); + + QWidget* page = new QWidget(this); + setMainWidget(page); + QVBoxLayout* layout = new QVBoxLayout(page, 0, spacingHint()); + + mTimeWidget = new AlarmTimeWidget(AlarmTimeWidget::DEFER_TIME, page, "timeGroup"); + mTimeWidget->setDateTime(initialDT); + mTimeWidget->setMinDateTimeIsCurrent(); + connect(mTimeWidget, SIGNAL(pastMax()), SLOT(slotPastLimit())); + layout->addWidget(mTimeWidget); + layout->addSpacing(spacingHint()); + + setButtonWhatsThis(Ok, i18n("Defer the alarm until the specified time.")); + setButtonWhatsThis(User1, i18n("Cancel the deferred alarm. This does not affect future recurrences.")); +} + + +/****************************************************************************** +* Called when the OK button is clicked. +*/ +void DeferAlarmDlg::slotOk() +{ + mAlarmDateTime = mTimeWidget->getDateTime(&mDeferMinutes); + if (!mAlarmDateTime.isValid()) + return; + KAEvent::DeferLimitType limitType; + DateTime endTime; + if (!mLimitEventID.isEmpty()) + { + // Get the event being deferred + const KCal::Event* kcalEvent = AlarmCalendar::getEvent(mLimitEventID); + if (kcalEvent) + { + KAEvent event(*kcalEvent); + endTime = event.deferralLimit(&limitType); + } + } + else + { + endTime = mLimitDateTime; + limitType = mLimitDateTime.isValid() ? KAEvent::LIMIT_MAIN : KAEvent::LIMIT_NONE; + } + if (endTime.isValid() && mAlarmDateTime > endTime) + { + QString text; + switch (limitType) + { + case KAEvent::LIMIT_REPETITION: + text = i18n("Cannot defer past the alarm's next sub-repetition (currently %1)"); + break; + case KAEvent::LIMIT_RECURRENCE: + text = i18n("Cannot defer past the alarm's next recurrence (currently %1)"); + break; + case KAEvent::LIMIT_REMINDER: + text = i18n("Cannot defer past the alarm's next reminder (currently %1)"); + break; + case KAEvent::LIMIT_MAIN: + text = i18n("Cannot defer reminder past the main alarm time (%1)"); + break; + case KAEvent::LIMIT_NONE: + break; // can't happen with a valid endTime + } + KMessageBox::sorry(this, text.arg(endTime.formatLocale())); + } + else + accept(); +} + +/****************************************************************************** +* Select the 'Time from now' radio button and preset its value. +*/ +void DeferAlarmDlg::setDeferMinutes(int minutes) +{ + mTimeWidget->selectTimeFromNow(minutes); +} + +/****************************************************************************** +* Called the maximum date/time for the date/time edit widget has been passed. +*/ +void DeferAlarmDlg::slotPastLimit() +{ + enableButtonOK(false); +} + +/****************************************************************************** +* Set the time limit for deferral based on the next occurrence of the alarm +* with the specified ID. +*/ +void DeferAlarmDlg::setLimit(const DateTime& limit) +{ + mLimitEventID = QString::null; + mLimitDateTime = limit; + mTimeWidget->setMaxDateTime(mLimitDateTime); +} + +/****************************************************************************** +* Set the time limit for deferral based on the next occurrence of the alarm +* with the specified ID. +*/ +DateTime DeferAlarmDlg::setLimit(const QString& eventID) +{ + mLimitEventID = eventID; + const KCal::Event* kcalEvent = AlarmCalendar::getEvent(mLimitEventID); + if (kcalEvent) + { + KAEvent event(*kcalEvent); + mLimitDateTime = event.deferralLimit(); + } + else + mLimitDateTime = DateTime(); + mTimeWidget->setMaxDateTime(mLimitDateTime); + return mLimitDateTime; +} + +/****************************************************************************** +* Called when the Cancel Deferral button is clicked. +*/ +void DeferAlarmDlg::slotUser1() +{ + mAlarmDateTime = DateTime(); + accept(); +} + +/****************************************************************************** +* Called when the Cancel button is clicked. +*/ +void DeferAlarmDlg::slotCancel() +{ + reject(); +} diff --git a/kalarm/deferdlg.h b/kalarm/deferdlg.h new file mode 100644 index 000000000..69275a054 --- /dev/null +++ b/kalarm/deferdlg.h @@ -0,0 +1,58 @@ +/* + * deferdlg.h - dialogue to defer an alarm + * Program: kalarm + * Copyright © 2002-2004,2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef DEFERDLG_H +#define DEFERDLG_H + +#include <kdialogbase.h> +#include "datetime.h" + +class AlarmTimeWidget; + + +class DeferAlarmDlg : public KDialogBase +{ + Q_OBJECT + public: + DeferAlarmDlg(const QString& caption, const DateTime& initialDT, + bool cancelButton, QWidget* parent = 0, const char* name = 0); + void setLimit(const DateTime&); + DateTime setLimit(const QString& eventID); + const DateTime& getDateTime() const { return mAlarmDateTime; } + void setDeferMinutes(int mins); + int deferMinutes() const { return mDeferMinutes; } + + protected slots: + virtual void slotOk(); + virtual void slotCancel(); + virtual void slotUser1(); + + private slots: + void slotPastLimit(); + + private: + AlarmTimeWidget* mTimeWidget; + DateTime mAlarmDateTime; + DateTime mLimitDateTime; // latest date/time allowed for deferral + QString mLimitEventID; // event from whose recurrences to derive the limit date/time for deferral + int mDeferMinutes; // number of minutes deferral selected, or 0 if date/time entered +}; + +#endif // DEFERDLG_H diff --git a/kalarm/editdlg.cpp b/kalarm/editdlg.cpp new file mode 100644 index 000000000..232111000 --- /dev/null +++ b/kalarm/editdlg.cpp @@ -0,0 +1,2043 @@ +/* + * editdlg.cpp - dialogue to create or modify an alarm or alarm template + * Program: kalarm + * Copyright © 2001-2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <limits.h> + +#include <qlayout.h> +#include <qpopupmenu.h> +#include <qvbox.h> +#include <qgroupbox.h> +#include <qwidgetstack.h> +#include <qdragobject.h> +#include <qlabel.h> +#include <qmessagebox.h> +#include <qtabwidget.h> +#include <qvalidator.h> +#include <qwhatsthis.h> +#include <qtooltip.h> +#include <qdir.h> +#include <qstyle.h> + +#include <kglobal.h> +#include <klocale.h> +#include <kconfig.h> +#include <kfiledialog.h> +#include <kiconloader.h> +#include <kio/netaccess.h> +#include <kfileitem.h> +#include <kmessagebox.h> +#include <kurldrag.h> +#include <kurlcompletion.h> +#include <kwin.h> +#include <kwinmodule.h> +#include <kstandarddirs.h> +#include <kstdguiitem.h> +#include <kabc/addresseedialog.h> +#include <kdebug.h> + +#include <libkdepim/maillistdrag.h> +#include <libkdepim/kvcarddrag.h> +#include <libkcal/icaldrag.h> + +#include "alarmcalendar.h" +#include "alarmtimewidget.h" +#include "checkbox.h" +#include "colourcombo.h" +#include "deferdlg.h" +#include "emailidcombo.h" +#include "fontcolourbutton.h" +#include "functions.h" +#include "kalarmapp.h" +#include "kamail.h" +#include "latecancel.h" +#include "lineedit.h" +#include "mainwindow.h" +#include "pickfileradio.h" +#include "preferences.h" +#include "radiobutton.h" +#include "recurrenceedit.h" +#include "reminder.h" +#include "shellprocess.h" +#include "soundpicker.h" +#include "specialactions.h" +#include "spinbox.h" +#include "templatepickdlg.h" +#include "timeedit.h" +#include "timespinbox.h" +#include "editdlg.moc" +#include "editdlgprivate.moc" + +using namespace KCal; + +static const char EDIT_DIALOG_NAME[] = "EditDialog"; +static const int maxDelayTime = 99*60 + 59; // < 100 hours + +/*============================================================================= += Class PickAlarmFileRadio +=============================================================================*/ +class PickAlarmFileRadio : public PickFileRadio +{ + public: + PickAlarmFileRadio(const QString& text, QButtonGroup* parent, const char* name = 0) + : PickFileRadio(text, parent, name) { } + virtual QString pickFile() // called when browse button is pressed to select a file to display + { + return KAlarm::browseFile(i18n("Choose Text or Image File to Display"), mDefaultDir, fileEdit()->text(), + QString::null, KFile::ExistingOnly, parentWidget(), "pickAlarmFile"); + } + private: + QString mDefaultDir; // default directory for file browse button +}; + +/*============================================================================= += Class PickLogFileRadio +=============================================================================*/ +class PickLogFileRadio : public PickFileRadio +{ + public: + PickLogFileRadio(QPushButton* b, LineEdit* e, const QString& text, QButtonGroup* parent, const char* name = 0) + : PickFileRadio(b, e, text, parent, name) { } + virtual QString pickFile() // called when browse button is pressed to select a log file + { + return KAlarm::browseFile(i18n("Choose Log File"), mDefaultDir, fileEdit()->text(), QString::null, + KFile::LocalOnly, parentWidget(), "pickLogFile"); + } + private: + QString mDefaultDir; // default directory for log file browse button +}; + +inline QString recurText(const KAEvent& event) +{ + QString r; + if (event.repeatCount()) + r = QString::fromLatin1("%1 / %2").arg(event.recurrenceText()).arg(event.repetitionText()); + else + r = event.recurrenceText(); + return i18n("&Recurrence - [%1]").arg(r); +} + +// Collect these widget labels together to ensure consistent wording and +// translations across different modules. +QString EditAlarmDlg::i18n_ConfirmAck() { return i18n("Confirm acknowledgment"); } +QString EditAlarmDlg::i18n_k_ConfirmAck() { return i18n("Confirm ac&knowledgment"); } +QString EditAlarmDlg::i18n_SpecialActions() { return i18n("Special Actions..."); } +QString EditAlarmDlg::i18n_ShowInKOrganizer() { return i18n("Show in KOrganizer"); } +QString EditAlarmDlg::i18n_g_ShowInKOrganizer() { return i18n("Show in KOr&ganizer"); } +QString EditAlarmDlg::i18n_EnterScript() { return i18n("Enter a script"); } +QString EditAlarmDlg::i18n_p_EnterScript() { return i18n("Enter a scri&pt"); } +QString EditAlarmDlg::i18n_ExecInTermWindow() { return i18n("Execute in terminal window"); } +QString EditAlarmDlg::i18n_w_ExecInTermWindow() { return i18n("Execute in terminal &window"); } +QString EditAlarmDlg::i18n_u_ExecInTermWindow() { return i18n("Exec&ute in terminal window"); } +QString EditAlarmDlg::i18n_g_LogToFile() { return i18n("Lo&g to file"); } +QString EditAlarmDlg::i18n_CopyEmailToSelf() { return i18n("Copy email to self"); } +QString EditAlarmDlg::i18n_e_CopyEmailToSelf() { return i18n("Copy &email to self"); } +QString EditAlarmDlg::i18n_s_CopyEmailToSelf() { return i18n("Copy email to &self"); } +QString EditAlarmDlg::i18n_EmailFrom() { return i18n("'From' email address", "From:"); } +QString EditAlarmDlg::i18n_f_EmailFrom() { return i18n("'From' email address", "&From:"); } +QString EditAlarmDlg::i18n_EmailTo() { return i18n("Email addressee", "To:"); } +QString EditAlarmDlg::i18n_EmailSubject() { return i18n("Email subject", "Subject:"); } +QString EditAlarmDlg::i18n_j_EmailSubject() { return i18n("Email subject", "Sub&ject:"); } + + +/****************************************************************************** + * Constructor. + * Parameters: + * Template = true to edit/create an alarm template + * = false to edit/create an alarm. + * event != to initialise the dialogue to show the specified event's data. + */ +EditAlarmDlg::EditAlarmDlg(bool Template, const QString& caption, QWidget* parent, const char* name, + const KAEvent* event, bool readOnly) + : KDialogBase(parent, (name ? name : Template ? "TemplEditDlg" : "EditDlg"), true, caption, + (readOnly ? Cancel|Try : Template ? Ok|Cancel|Try : Ok|Cancel|Try|Default), + (readOnly ? Cancel : Ok)), + mMainPageShown(false), + mRecurPageShown(false), + mRecurSetDefaultEndDate(true), + mTemplateName(0), + mSpecialActionsButton(0), + mReminderDeferral(false), + mReminderArchived(false), + mEmailRemoveButton(0), + mDeferGroup(0), + mTimeWidget(0), + mShowInKorganizer(0), + mDeferGroupHeight(0), + mTemplate(Template), + mDesiredReadOnly(readOnly), + mReadOnly(readOnly), + mSavedEvent(0) +{ + setButtonText(Default, i18n("Load Template...")); + QVBox* mainWidget = new QVBox(this); + mainWidget->setSpacing(spacingHint()); + setMainWidget(mainWidget); + if (mTemplate) + { + QHBox* box = new QHBox(mainWidget); + box->setSpacing(spacingHint()); + QLabel* label = new QLabel(i18n("Template name:"), box); + label->setFixedSize(label->sizeHint()); + mTemplateName = new QLineEdit(box); + mTemplateName->setReadOnly(mReadOnly); + label->setBuddy(mTemplateName); + QWhatsThis::add(box, i18n("Enter the name of the alarm template")); + box->setFixedHeight(box->sizeHint().height()); + } + mTabs = new QTabWidget(mainWidget); + mTabs->setMargin(marginHint()); + + QVBox* mainPageBox = new QVBox(mTabs); + mainPageBox->setSpacing(spacingHint()); + mTabs->addTab(mainPageBox, i18n("&Alarm")); + mMainPageIndex = 0; + PageFrame* mainPage = new PageFrame(mainPageBox); + connect(mainPage, SIGNAL(shown()), SLOT(slotShowMainPage())); + QVBoxLayout* topLayout = new QVBoxLayout(mainPage, 0, spacingHint()); + + // Recurrence tab + QVBox* recurTab = new QVBox(mTabs); + mainPageBox->setSpacing(spacingHint()); + mTabs->addTab(recurTab, QString::null); + mRecurPageIndex = 1; + mRecurrenceEdit = new RecurrenceEdit(readOnly, recurTab, "recurPage"); + connect(mRecurrenceEdit, SIGNAL(shown()), SLOT(slotShowRecurrenceEdit())); + connect(mRecurrenceEdit, SIGNAL(typeChanged(int)), SLOT(slotRecurTypeChange(int))); + connect(mRecurrenceEdit, SIGNAL(frequencyChanged()), SLOT(slotRecurFrequencyChange())); + connect(mRecurrenceEdit, SIGNAL(repeatNeedsInitialisation()), SLOT(slotSetSubRepetition())); + + // Alarm action + + mActionGroup = new ButtonGroup(i18n("Action"), mainPage, "actionGroup"); + connect(mActionGroup, SIGNAL(buttonSet(int)), SLOT(slotAlarmTypeChanged(int))); + topLayout->addWidget(mActionGroup, 1); + QBoxLayout* layout = new QVBoxLayout(mActionGroup, marginHint(), spacingHint()); + layout->addSpacing(fontMetrics().lineSpacing()/2); + QGridLayout* grid = new QGridLayout(layout, 1, 5); + + // Message radio button + mMessageRadio = new RadioButton(i18n("Te&xt"), mActionGroup, "messageButton"); + mMessageRadio->setFixedSize(mMessageRadio->sizeHint()); + QWhatsThis::add(mMessageRadio, + i18n("If checked, the alarm will display a text message.")); + grid->addWidget(mMessageRadio, 1, 0); + grid->setColStretch(1, 1); + + // File radio button + mFileRadio = new PickAlarmFileRadio(i18n("&File"), mActionGroup, "fileButton"); + mFileRadio->setFixedSize(mFileRadio->sizeHint()); + QWhatsThis::add(mFileRadio, + i18n("If checked, the alarm will display the contents of a text or image file.")); + grid->addWidget(mFileRadio, 1, 2); + grid->setColStretch(3, 1); + + // Command radio button + mCommandRadio = new RadioButton(i18n("Co&mmand"), mActionGroup, "cmdButton"); + mCommandRadio->setFixedSize(mCommandRadio->sizeHint()); + QWhatsThis::add(mCommandRadio, + i18n("If checked, the alarm will execute a shell command.")); + grid->addWidget(mCommandRadio, 1, 4); + grid->setColStretch(5, 1); + + // Email radio button + mEmailRadio = new RadioButton(i18n("&Email"), mActionGroup, "emailButton"); + mEmailRadio->setFixedSize(mEmailRadio->sizeHint()); + QWhatsThis::add(mEmailRadio, + i18n("If checked, the alarm will send an email.")); + grid->addWidget(mEmailRadio, 1, 6); + + initDisplayAlarms(mActionGroup); + layout->addWidget(mDisplayAlarmsFrame); + initCommand(mActionGroup); + layout->addWidget(mCommandFrame); + initEmail(mActionGroup); + layout->addWidget(mEmailFrame); + + // Deferred date/time: visible only for a deferred recurring event. + mDeferGroup = new QGroupBox(1, Qt::Vertical, i18n("Deferred Alarm"), mainPage, "deferGroup"); + topLayout->addWidget(mDeferGroup); + QLabel* label = new QLabel(i18n("Deferred to:"), mDeferGroup); + label->setFixedSize(label->sizeHint()); + mDeferTimeLabel = new QLabel(mDeferGroup); + + mDeferChangeButton = new QPushButton(i18n("C&hange..."), mDeferGroup); + mDeferChangeButton->setFixedSize(mDeferChangeButton->sizeHint()); + connect(mDeferChangeButton, SIGNAL(clicked()), SLOT(slotEditDeferral())); + QWhatsThis::add(mDeferChangeButton, i18n("Change the alarm's deferred time, or cancel the deferral")); + mDeferGroup->addSpace(0); + + layout = new QHBoxLayout(topLayout); + + // Date and time entry + if (mTemplate) + { + mTemplateTimeGroup = new ButtonGroup(i18n("Time"), mainPage, "templateGroup"); + connect(mTemplateTimeGroup, SIGNAL(buttonSet(int)), SLOT(slotTemplateTimeType(int))); + layout->addWidget(mTemplateTimeGroup); + grid = new QGridLayout(mTemplateTimeGroup, 2, 2, marginHint(), spacingHint()); + grid->addRowSpacing(0, fontMetrics().lineSpacing()/2); + // Get alignment to use in QGridLayout (AlignAuto doesn't work correctly there) + int alignment = QApplication::reverseLayout() ? Qt::AlignRight : Qt::AlignLeft; + + mTemplateDefaultTime = new RadioButton(i18n("&Default time"), mTemplateTimeGroup, "templateDefTimeButton"); + mTemplateDefaultTime->setFixedSize(mTemplateDefaultTime->sizeHint()); + mTemplateDefaultTime->setReadOnly(mReadOnly); + QWhatsThis::add(mTemplateDefaultTime, + i18n("Do not specify a start time for alarms based on this template. " + "The normal default start time will be used.")); + grid->addWidget(mTemplateDefaultTime, 0, 0, alignment); + + QHBox* box = new QHBox(mTemplateTimeGroup); + box->setSpacing(spacingHint()); + mTemplateUseTime = new RadioButton(i18n("Time:"), box, "templateTimeButton"); + mTemplateUseTime->setFixedSize(mTemplateUseTime->sizeHint()); + mTemplateUseTime->setReadOnly(mReadOnly); + QWhatsThis::add(mTemplateUseTime, + i18n("Specify a start time for alarms based on this template.")); + mTemplateTimeGroup->insert(mTemplateUseTime); + mTemplateTime = new TimeEdit(box, "templateTimeEdit"); + mTemplateTime->setFixedSize(mTemplateTime->sizeHint()); + mTemplateTime->setReadOnly(mReadOnly); + QWhatsThis::add(mTemplateTime, + QString("%1\n\n%2").arg(i18n("Enter the start time for alarms based on this template.")) + .arg(TimeSpinBox::shiftWhatsThis())); + box->setStretchFactor(new QWidget(box), 1); // left adjust the controls + box->setFixedHeight(box->sizeHint().height()); + grid->addWidget(box, 0, 1, alignment); + + mTemplateAnyTime = new RadioButton(i18n("An&y time"), mTemplateTimeGroup, "templateAnyTimeButton"); + mTemplateAnyTime->setFixedSize(mTemplateAnyTime->sizeHint()); + mTemplateAnyTime->setReadOnly(mReadOnly); + QWhatsThis::add(mTemplateAnyTime, + i18n("Set the '%1' option for alarms based on this template.").arg(i18n("Any time"))); + grid->addWidget(mTemplateAnyTime, 1, 0, alignment); + + box = new QHBox(mTemplateTimeGroup); + box->setSpacing(spacingHint()); + mTemplateUseTimeAfter = new RadioButton(AlarmTimeWidget::i18n_w_TimeFromNow(), box, "templateFromNowButton"); + mTemplateUseTimeAfter->setFixedSize(mTemplateUseTimeAfter->sizeHint()); + mTemplateUseTimeAfter->setReadOnly(mReadOnly); + QWhatsThis::add(mTemplateUseTimeAfter, + i18n("Set alarms based on this template to start after the specified time " + "interval from when the alarm is created.")); + mTemplateTimeGroup->insert(mTemplateUseTimeAfter); + mTemplateTimeAfter = new TimeSpinBox(1, maxDelayTime, box); + mTemplateTimeAfter->setValue(1439); + mTemplateTimeAfter->setFixedSize(mTemplateTimeAfter->sizeHint()); + mTemplateTimeAfter->setReadOnly(mReadOnly); + QWhatsThis::add(mTemplateTimeAfter, + QString("%1\n\n%2").arg(AlarmTimeWidget::i18n_TimeAfterPeriod()) + .arg(TimeSpinBox::shiftWhatsThis())); + box->setFixedHeight(box->sizeHint().height()); + grid->addWidget(box, 1, 1, alignment); + + layout->addStretch(); + } + else + { + mTimeWidget = new AlarmTimeWidget(i18n("Time"), AlarmTimeWidget::AT_TIME, mainPage, "timeGroup"); + connect(mTimeWidget, SIGNAL(anyTimeToggled(bool)), SLOT(slotAnyTimeToggled(bool))); + topLayout->addWidget(mTimeWidget); + } + + // Reminder + static const QString reminderText = i18n("Enter how long in advance of the main alarm to display a reminder alarm."); + mReminder = new Reminder(i18n("Rem&inder:"), + i18n("Check to additionally display a reminder in advance of the main alarm time(s)."), + QString("%1\n\n%2").arg(reminderText).arg(TimeSpinBox::shiftWhatsThis()), + true, true, mainPage); + mReminder->setFixedSize(mReminder->sizeHint()); + topLayout->addWidget(mReminder, 0, Qt::AlignAuto); + + // Late cancel selector - default = allow late display + mLateCancel = new LateCancelSelector(true, mainPage); + topLayout->addWidget(mLateCancel, 0, Qt::AlignAuto); + + // Acknowledgement confirmation required - default = no confirmation + layout = new QHBoxLayout(topLayout, 0); + mConfirmAck = createConfirmAckCheckbox(mainPage); + mConfirmAck->setFixedSize(mConfirmAck->sizeHint()); + layout->addWidget(mConfirmAck); + layout->addSpacing(2*spacingHint()); + layout->addStretch(); + + if (theApp()->korganizerEnabled()) + { + // Show in KOrganizer checkbox + mShowInKorganizer = new CheckBox(i18n_ShowInKOrganizer(), mainPage); + mShowInKorganizer->setFixedSize(mShowInKorganizer->sizeHint()); + QWhatsThis::add(mShowInKorganizer, i18n("Check to copy the alarm into KOrganizer's calendar")); + layout->addWidget(mShowInKorganizer); + } + + setButtonWhatsThis(Ok, i18n("Schedule the alarm at the specified time.")); + + // Initialise the state of all controls according to the specified event, if any + initialise(event); + if (mTemplateName) + mTemplateName->setFocus(); + + // Save the initial state of all controls so that we can later tell if they have changed + saveState((event && (mTemplate || !event->isTemplate())) ? event : 0); + + // Note the current desktop so that the dialog can be shown on it. + // If a main window is visible, the dialog will by KDE default always appear on its + // desktop. If the user invokes the dialog via the system tray on a different desktop, + // that can cause confusion. + mDesktop = KWin::currentDesktop(); +} + +EditAlarmDlg::~EditAlarmDlg() +{ + delete mSavedEvent; +} + +/****************************************************************************** + * Set up the dialog controls common to display alarms. + */ +void EditAlarmDlg::initDisplayAlarms(QWidget* parent) +{ + mDisplayAlarmsFrame = new QFrame(parent); + mDisplayAlarmsFrame->setFrameStyle(QFrame::NoFrame); + QBoxLayout* frameLayout = new QVBoxLayout(mDisplayAlarmsFrame, 0, spacingHint()); + + // Text message edit box + mTextMessageEdit = new TextEdit(mDisplayAlarmsFrame); + mTextMessageEdit->setWordWrap(KTextEdit::NoWrap); + QWhatsThis::add(mTextMessageEdit, i18n("Enter the text of the alarm message. It may be multi-line.")); + frameLayout->addWidget(mTextMessageEdit); + + // File name edit box + mFileBox = new QHBox(mDisplayAlarmsFrame); + frameLayout->addWidget(mFileBox); + mFileMessageEdit = new LineEdit(LineEdit::Url, mFileBox); + mFileMessageEdit->setAcceptDrops(true); + QWhatsThis::add(mFileMessageEdit, i18n("Enter the name or URL of a text or image file to display.")); + + // File browse button + mFileBrowseButton = new QPushButton(mFileBox); + mFileBrowseButton->setPixmap(SmallIcon("fileopen")); + mFileBrowseButton->setFixedSize(mFileBrowseButton->sizeHint()); + QToolTip::add(mFileBrowseButton, i18n("Choose a file")); + QWhatsThis::add(mFileBrowseButton, i18n("Select a text or image file to display.")); + mFileRadio->init(mFileBrowseButton, mFileMessageEdit); + + // Font and colour choice button and sample text + mFontColourButton = new FontColourButton(mDisplayAlarmsFrame); + mFontColourButton->setMaximumHeight(mFontColourButton->sizeHint().height()); + frameLayout->addWidget(mFontColourButton); + + QHBoxLayout* layout = new QHBoxLayout(frameLayout, 0, 0); + mBgColourBox = new QHBox(mDisplayAlarmsFrame); + mBgColourBox->setSpacing(spacingHint()); + layout->addWidget(mBgColourBox); + layout->addStretch(); + QLabel* label = new QLabel(i18n("&Background color:"), mBgColourBox); + mBgColourButton = new ColourCombo(mBgColourBox); + label->setBuddy(mBgColourButton); + QWhatsThis::add(mBgColourBox, i18n("Select the alarm message background color")); + + // Sound checkbox and file selector + layout = new QHBoxLayout(frameLayout); + mSoundPicker = new SoundPicker(mDisplayAlarmsFrame); + mSoundPicker->setFixedSize(mSoundPicker->sizeHint()); + layout->addWidget(mSoundPicker); + layout->addSpacing(2*spacingHint()); + layout->addStretch(); + + if (ShellProcess::authorised()) // don't display if shell commands not allowed (e.g. kiosk mode) + { + // Special actions button + mSpecialActionsButton = new SpecialActionsButton(i18n_SpecialActions(), mDisplayAlarmsFrame); + mSpecialActionsButton->setFixedSize(mSpecialActionsButton->sizeHint()); + layout->addWidget(mSpecialActionsButton); + } + + // Top-adjust the controls + mFilePadding = new QHBox(mDisplayAlarmsFrame); + frameLayout->addWidget(mFilePadding); + frameLayout->setStretchFactor(mFilePadding, 1); +} + +/****************************************************************************** + * Set up the command alarm dialog controls. + */ +void EditAlarmDlg::initCommand(QWidget* parent) +{ + mCommandFrame = new QFrame(parent); + mCommandFrame->setFrameStyle(QFrame::NoFrame); + QBoxLayout* frameLayout = new QVBoxLayout(mCommandFrame, 0, spacingHint()); + + mCmdTypeScript = new CheckBox(i18n_p_EnterScript(), mCommandFrame); + mCmdTypeScript->setFixedSize(mCmdTypeScript->sizeHint()); + connect(mCmdTypeScript, SIGNAL(toggled(bool)), SLOT(slotCmdScriptToggled(bool))); + QWhatsThis::add(mCmdTypeScript, i18n("Check to enter the contents of a script instead of a shell command line")); + frameLayout->addWidget(mCmdTypeScript, 0, Qt::AlignAuto); + + mCmdCommandEdit = new LineEdit(LineEdit::Url, mCommandFrame); + QWhatsThis::add(mCmdCommandEdit, i18n("Enter a shell command to execute.")); + frameLayout->addWidget(mCmdCommandEdit); + + mCmdScriptEdit = new TextEdit(mCommandFrame); + QWhatsThis::add(mCmdScriptEdit, i18n("Enter the contents of a script to execute")); + frameLayout->addWidget(mCmdScriptEdit); + + // What to do with command output + + mCmdOutputGroup = new ButtonGroup(i18n("Command Output"), mCommandFrame); + frameLayout->addWidget(mCmdOutputGroup); + QBoxLayout* layout = new QVBoxLayout(mCmdOutputGroup, marginHint(), spacingHint()); + layout->addSpacing(fontMetrics().lineSpacing()/2); + + // Execute in terminal window + RadioButton* button = new RadioButton(i18n_u_ExecInTermWindow(), mCmdOutputGroup, "execInTerm"); + button->setFixedSize(button->sizeHint()); + QWhatsThis::add(button, i18n("Check to execute the command in a terminal window")); + mCmdOutputGroup->insert(button, EXEC_IN_TERMINAL); + layout->addWidget(button, 0, Qt::AlignAuto); + + // Log file name edit box + QHBox* box = new QHBox(mCmdOutputGroup); + (new QWidget(box))->setFixedWidth(button->style().subRect(QStyle::SR_RadioButtonIndicator, button).width()); // indent the edit box +// (new QWidget(box))->setFixedWidth(button->style().pixelMetric(QStyle::PM_ExclusiveIndicatorWidth)); // indent the edit box + mCmdLogFileEdit = new LineEdit(LineEdit::Url, box); + mCmdLogFileEdit->setAcceptDrops(true); + QWhatsThis::add(mCmdLogFileEdit, i18n("Enter the name or path of the log file.")); + + // Log file browse button. + // The file browser dialogue is activated by the PickLogFileRadio class. + QPushButton* browseButton = new QPushButton(box); + browseButton->setPixmap(SmallIcon("fileopen")); + browseButton->setFixedSize(browseButton->sizeHint()); + QToolTip::add(browseButton, i18n("Choose a file")); + QWhatsThis::add(browseButton, i18n("Select a log file.")); + + // Log output to file + button = new PickLogFileRadio(browseButton, mCmdLogFileEdit, i18n_g_LogToFile(), mCmdOutputGroup, "cmdLog"); + button->setFixedSize(button->sizeHint()); + QWhatsThis::add(button, + i18n("Check to log the command output to a local file. The output will be appended to any existing contents of the file.")); + mCmdOutputGroup->insert(button, LOG_TO_FILE); + layout->addWidget(button, 0, Qt::AlignAuto); + layout->addWidget(box); + + // Discard output + button = new RadioButton(i18n("Discard"), mCmdOutputGroup, "cmdDiscard"); + button->setFixedSize(button->sizeHint()); + QWhatsThis::add(button, i18n("Check to discard command output.")); + mCmdOutputGroup->insert(button, DISCARD_OUTPUT); + layout->addWidget(button, 0, Qt::AlignAuto); + + // Top-adjust the controls + mCmdPadding = new QHBox(mCommandFrame); + frameLayout->addWidget(mCmdPadding); + frameLayout->setStretchFactor(mCmdPadding, 1); +} + +/****************************************************************************** + * Set up the email alarm dialog controls. + */ +void EditAlarmDlg::initEmail(QWidget* parent) +{ + mEmailFrame = new QFrame(parent); + mEmailFrame->setFrameStyle(QFrame::NoFrame); + QBoxLayout* layout = new QVBoxLayout(mEmailFrame, 0, spacingHint()); + QGridLayout* grid = new QGridLayout(layout, 3, 3, spacingHint()); + grid->setColStretch(1, 1); + + mEmailFromList = 0; + if (Preferences::emailFrom() == Preferences::MAIL_FROM_KMAIL) + { + // Email sender identity + QLabel* label = new QLabel(i18n_EmailFrom(), mEmailFrame); + label->setFixedSize(label->sizeHint()); + grid->addWidget(label, 0, 0); + + mEmailFromList = new EmailIdCombo(KAMail::identityManager(), mEmailFrame); + mEmailFromList->setMinimumSize(mEmailFromList->sizeHint()); + label->setBuddy(mEmailFromList); + QWhatsThis::add(mEmailFromList, + i18n("Your email identity, used to identify you as the sender when sending email alarms.")); + grid->addMultiCellWidget(mEmailFromList, 0, 0, 1, 2); + } + + // Email recipients + QLabel* label = new QLabel(i18n_EmailTo(), mEmailFrame); + label->setFixedSize(label->sizeHint()); + grid->addWidget(label, 1, 0); + + mEmailToEdit = new LineEdit(LineEdit::Emails, mEmailFrame); + mEmailToEdit->setMinimumSize(mEmailToEdit->sizeHint()); + QWhatsThis::add(mEmailToEdit, + i18n("Enter the addresses of the email recipients. Separate multiple addresses by " + "commas or semicolons.")); + grid->addWidget(mEmailToEdit, 1, 1); + + mEmailAddressButton = new QPushButton(mEmailFrame); + mEmailAddressButton->setPixmap(SmallIcon("contents")); + mEmailAddressButton->setFixedSize(mEmailAddressButton->sizeHint()); + connect(mEmailAddressButton, SIGNAL(clicked()), SLOT(openAddressBook())); + QToolTip::add(mEmailAddressButton, i18n("Open address book")); + QWhatsThis::add(mEmailAddressButton, i18n("Select email addresses from your address book.")); + grid->addWidget(mEmailAddressButton, 1, 2); + + // Email subject + label = new QLabel(i18n_j_EmailSubject(), mEmailFrame); + label->setFixedSize(label->sizeHint()); + grid->addWidget(label, 2, 0); + + mEmailSubjectEdit = new LineEdit(mEmailFrame); + mEmailSubjectEdit->setMinimumSize(mEmailSubjectEdit->sizeHint()); + label->setBuddy(mEmailSubjectEdit); + QWhatsThis::add(mEmailSubjectEdit, i18n("Enter the email subject.")); + grid->addMultiCellWidget(mEmailSubjectEdit, 2, 2, 1, 2); + + // Email body + mEmailMessageEdit = new TextEdit(mEmailFrame); + QWhatsThis::add(mEmailMessageEdit, i18n("Enter the email message.")); + layout->addWidget(mEmailMessageEdit); + + // Email attachments + grid = new QGridLayout(layout, 2, 3, spacingHint()); + label = new QLabel(i18n("Attachment&s:"), mEmailFrame); + label->setFixedSize(label->sizeHint()); + grid->addWidget(label, 0, 0); + + mEmailAttachList = new QComboBox(true, mEmailFrame); + mEmailAttachList->setMinimumSize(mEmailAttachList->sizeHint()); + mEmailAttachList->lineEdit()->setReadOnly(true); +QListBox* list = mEmailAttachList->listBox(); +QRect rect = list->geometry(); +list->setGeometry(rect.left() - 50, rect.top(), rect.width(), rect.height()); + label->setBuddy(mEmailAttachList); + QWhatsThis::add(mEmailAttachList, + i18n("Files to send as attachments to the email.")); + grid->addWidget(mEmailAttachList, 0, 1); + grid->setColStretch(1, 1); + + mEmailAddAttachButton = new QPushButton(i18n("Add..."), mEmailFrame); + connect(mEmailAddAttachButton, SIGNAL(clicked()), SLOT(slotAddAttachment())); + QWhatsThis::add(mEmailAddAttachButton, i18n("Add an attachment to the email.")); + grid->addWidget(mEmailAddAttachButton, 0, 2); + + mEmailRemoveButton = new QPushButton(i18n("Remo&ve"), mEmailFrame); + connect(mEmailRemoveButton, SIGNAL(clicked()), SLOT(slotRemoveAttachment())); + QWhatsThis::add(mEmailRemoveButton, i18n("Remove the highlighted attachment from the email.")); + grid->addWidget(mEmailRemoveButton, 1, 2); + + // BCC email to sender + mEmailBcc = new CheckBox(i18n_s_CopyEmailToSelf(), mEmailFrame); + mEmailBcc->setFixedSize(mEmailBcc->sizeHint()); + QWhatsThis::add(mEmailBcc, + i18n("If checked, the email will be blind copied to you.")); + grid->addMultiCellWidget(mEmailBcc, 1, 1, 0, 1, Qt::AlignAuto); +} + +/****************************************************************************** + * Initialise the dialogue controls from the specified event. + */ +void EditAlarmDlg::initialise(const KAEvent* event) +{ + mReadOnly = mDesiredReadOnly; + if (!mTemplate && event && event->action() == KAEvent::COMMAND && !ShellProcess::authorised()) + mReadOnly = true; // don't allow editing of existing command alarms in kiosk mode + setReadOnly(); + + mChanged = false; + mOnlyDeferred = false; + mExpiredRecurrence = false; + mKMailSerialNumber = 0; + bool deferGroupVisible = false; + if (event) + { + // Set the values to those for the specified event + if (mTemplate) + mTemplateName->setText(event->templateName()); + bool recurs = event->recurs(); + if ((recurs || event->repeatCount()) && !mTemplate && event->deferred()) + { + deferGroupVisible = true; + mDeferDateTime = event->deferDateTime(); + mDeferTimeLabel->setText(mDeferDateTime.formatLocale()); + mDeferGroup->show(); + } + if (event->defaultFont()) + mFontColourButton->setDefaultFont(); + else + mFontColourButton->setFont(event->font()); + mFontColourButton->setBgColour(event->bgColour()); + mFontColourButton->setFgColour(event->fgColour()); + mBgColourButton->setColour(event->bgColour()); + if (mTemplate) + { + // Editing a template + int afterTime = event->isTemplate() ? event->templateAfterTime() : -1; + bool noTime = !afterTime; + bool useTime = !event->mainDateTime().isDateOnly(); + int button = mTemplateTimeGroup->id(noTime ? mTemplateDefaultTime : + (afterTime > 0) ? mTemplateUseTimeAfter : + useTime ? mTemplateUseTime : mTemplateAnyTime); + mTemplateTimeGroup->setButton(button); + mTemplateTimeAfter->setValue(afterTime > 0 ? afterTime : 1); + if (!noTime && useTime) + mTemplateTime->setValue(event->mainDateTime().time()); + else + mTemplateTime->setValue(0); + } + else + { + if (event->isTemplate()) + { + // Initialising from an alarm template: use current date + QDateTime now = QDateTime::currentDateTime(); + int afterTime = event->templateAfterTime(); + if (afterTime >= 0) + { + mTimeWidget->setDateTime(now.addSecs(afterTime * 60)); + mTimeWidget->selectTimeFromNow(); + } + else + { + QDate d = now.date(); + QTime t = event->startDateTime().time(); + bool dateOnly = event->startDateTime().isDateOnly(); + if (!dateOnly && now.time() >= t) + d = d.addDays(1); // alarm time has already passed, so use tomorrow + mTimeWidget->setDateTime(DateTime(QDateTime(d, t), dateOnly)); + } + } + else + { + mExpiredRecurrence = recurs && event->mainExpired(); + mTimeWidget->setDateTime(recurs || event->uidStatus() == KAEvent::EXPIRED ? event->startDateTime() + : event->mainExpired() ? event->deferDateTime() : event->mainDateTime()); + } + } + + KAEvent::Action action = event->action(); + AlarmText altext; + if (event->commandScript()) + altext.setScript(event->cleanText()); + else + altext.setText(event->cleanText()); + setAction(action, altext); + if (action == KAEvent::MESSAGE && event->kmailSerialNumber() + && AlarmText::checkIfEmail(event->cleanText())) + mKMailSerialNumber = event->kmailSerialNumber(); + if (action == KAEvent::EMAIL) + mEmailAttachList->insertStringList(event->emailAttachments()); + + mLateCancel->setMinutes(event->lateCancel(), event->startDateTime().isDateOnly(), + TimePeriod::HOURS_MINUTES); + mLateCancel->showAutoClose(action == KAEvent::MESSAGE || action == KAEvent::FILE); + mLateCancel->setAutoClose(event->autoClose()); + mLateCancel->setFixedSize(mLateCancel->sizeHint()); + if (mShowInKorganizer) + mShowInKorganizer->setChecked(event->copyToKOrganizer()); + mConfirmAck->setChecked(event->confirmAck()); + int reminder = event->reminder(); + if (!reminder && event->reminderDeferral() && !recurs) + { + reminder = event->reminderDeferral(); + mReminderDeferral = true; + } + if (!reminder && event->reminderArchived() && recurs) + { + reminder = event->reminderArchived(); + mReminderArchived = true; + } + mReminder->setMinutes(reminder, (mTimeWidget ? mTimeWidget->anyTime() : mTemplateAnyTime->isOn())); + mReminder->setOnceOnly(event->reminderOnceOnly()); + mReminder->enableOnceOnly(event->recurs()); + if (mSpecialActionsButton) + mSpecialActionsButton->setActions(event->preAction(), event->postAction()); + mRecurrenceEdit->set(*event, (mTemplate || event->isTemplate())); // must be called after mTimeWidget is set up, to ensure correct date-only enabling + mTabs->setTabLabel(mTabs->page(mRecurPageIndex), recurText(*event)); + SoundPicker::Type soundType = event->speak() ? SoundPicker::SPEAK + : event->beep() ? SoundPicker::BEEP + : !event->audioFile().isEmpty() ? SoundPicker::PLAY_FILE + : SoundPicker::NONE; + mSoundPicker->set(soundType, event->audioFile(), event->soundVolume(), + event->fadeVolume(), event->fadeSeconds(), event->repeatSound()); + CmdLogType logType = event->commandXterm() ? EXEC_IN_TERMINAL + : !event->logFile().isEmpty() ? LOG_TO_FILE + : DISCARD_OUTPUT; + if (logType == LOG_TO_FILE) + mCmdLogFileEdit->setText(event->logFile()); // set file name before setting radio button + mCmdOutputGroup->setButton(logType); + mEmailToEdit->setText(event->emailAddresses(", ")); + mEmailSubjectEdit->setText(event->emailSubject()); + mEmailBcc->setChecked(event->emailBcc()); + if (mEmailFromList) + mEmailFromList->setCurrentIdentity(event->emailFromId()); + } + else + { + // Set the values to their defaults + if (!ShellProcess::authorised()) + { + // Don't allow shell commands in kiosk mode + mCommandRadio->setEnabled(false); + if (mSpecialActionsButton) + mSpecialActionsButton->setEnabled(false); + } + mFontColourButton->setDefaultFont(); + mFontColourButton->setBgColour(Preferences::defaultBgColour()); + mFontColourButton->setFgColour(Preferences::defaultFgColour()); + mBgColourButton->setColour(Preferences::defaultBgColour()); + QDateTime defaultTime = QDateTime::currentDateTime().addSecs(60); + if (mTemplate) + { + mTemplateTimeGroup->setButton(mTemplateTimeGroup->id(mTemplateDefaultTime)); + mTemplateTime->setValue(0); + mTemplateTimeAfter->setValue(1); + } + else + mTimeWidget->setDateTime(defaultTime); + mActionGroup->setButton(mActionGroup->id(mMessageRadio)); + mLateCancel->setMinutes((Preferences::defaultLateCancel() ? 1 : 0), false, TimePeriod::HOURS_MINUTES); + mLateCancel->showAutoClose(true); + mLateCancel->setAutoClose(Preferences::defaultAutoClose()); + mLateCancel->setFixedSize(mLateCancel->sizeHint()); + if (mShowInKorganizer) + mShowInKorganizer->setChecked(Preferences::defaultCopyToKOrganizer()); + mConfirmAck->setChecked(Preferences::defaultConfirmAck()); + if (mSpecialActionsButton) + mSpecialActionsButton->setActions(Preferences::defaultPreAction(), Preferences::defaultPostAction()); + mRecurrenceEdit->setDefaults(defaultTime); // must be called after mTimeWidget is set up, to ensure correct date-only enabling + slotRecurFrequencyChange(); // update the Recurrence text + mReminder->setMinutes(0, false); + mReminder->enableOnceOnly(mRecurrenceEdit->isTimedRepeatType()); // must be called after mRecurrenceEdit is set up + mSoundPicker->set(Preferences::defaultSoundType(), Preferences::defaultSoundFile(), + Preferences::defaultSoundVolume(), -1, 0, Preferences::defaultSoundRepeat()); + mCmdTypeScript->setChecked(Preferences::defaultCmdScript()); + mCmdLogFileEdit->setText(Preferences::defaultCmdLogFile()); // set file name before setting radio button + mCmdOutputGroup->setButton(Preferences::defaultCmdLogType()); + mEmailBcc->setChecked(Preferences::defaultEmailBcc()); + } + slotCmdScriptToggled(mCmdTypeScript->isChecked()); + + if (!deferGroupVisible) + mDeferGroup->hide(); + + bool enable = !!mEmailAttachList->count(); + mEmailAttachList->setEnabled(enable); + if (mEmailRemoveButton) + mEmailRemoveButton->setEnabled(enable); + AlarmCalendar* cal = AlarmCalendar::templateCalendar(); + bool empty = cal->isOpen() && !cal->events().count(); + enableButton(Default, !empty); +} + +/****************************************************************************** + * Set the read-only status of all non-template controls. + */ +void EditAlarmDlg::setReadOnly() +{ + // Common controls + mMessageRadio->setReadOnly(mReadOnly); + mFileRadio->setReadOnly(mReadOnly); + mCommandRadio->setReadOnly(mReadOnly); + mEmailRadio->setReadOnly(mReadOnly); + if (mTimeWidget) + mTimeWidget->setReadOnly(mReadOnly); + mLateCancel->setReadOnly(mReadOnly); + if (mReadOnly) + mDeferChangeButton->hide(); + else + mDeferChangeButton->show(); + if (mShowInKorganizer) + mShowInKorganizer->setReadOnly(mReadOnly); + + // Message alarm controls + mTextMessageEdit->setReadOnly(mReadOnly); + mFileMessageEdit->setReadOnly(mReadOnly); + mFontColourButton->setReadOnly(mReadOnly); + mBgColourButton->setReadOnly(mReadOnly); + mSoundPicker->setReadOnly(mReadOnly); + mConfirmAck->setReadOnly(mReadOnly); + mReminder->setReadOnly(mReadOnly); + if (mSpecialActionsButton) + mSpecialActionsButton->setReadOnly(mReadOnly); + if (mReadOnly) + { + mFileBrowseButton->hide(); + mFontColourButton->hide(); + } + else + { + mFileBrowseButton->show(); + mFontColourButton->show(); + } + + // Command alarm controls + mCmdTypeScript->setReadOnly(mReadOnly); + mCmdCommandEdit->setReadOnly(mReadOnly); + mCmdScriptEdit->setReadOnly(mReadOnly); + for (int id = DISCARD_OUTPUT; id < EXEC_IN_TERMINAL; ++id) + ((RadioButton*)mCmdOutputGroup->find(id))->setReadOnly(mReadOnly); + + // Email alarm controls + mEmailToEdit->setReadOnly(mReadOnly); + mEmailSubjectEdit->setReadOnly(mReadOnly); + mEmailMessageEdit->setReadOnly(mReadOnly); + mEmailBcc->setReadOnly(mReadOnly); + if (mEmailFromList) + mEmailFromList->setReadOnly(mReadOnly); + if (mReadOnly) + { + mEmailAddressButton->hide(); + mEmailAddAttachButton->hide(); + mEmailRemoveButton->hide(); + } + else + { + mEmailAddressButton->show(); + mEmailAddAttachButton->show(); + mEmailRemoveButton->show(); + } +} + +/****************************************************************************** + * Set the dialog's action and the action's text. + */ +void EditAlarmDlg::setAction(KAEvent::Action action, const AlarmText& alarmText) +{ + QString text = alarmText.displayText(); + bool script; + QRadioButton* radio; + switch (action) + { + case KAEvent::FILE: + radio = mFileRadio; + mFileMessageEdit->setText(text); + break; + case KAEvent::COMMAND: + radio = mCommandRadio; + script = alarmText.isScript(); + mCmdTypeScript->setChecked(script); + if (script) + mCmdScriptEdit->setText(text); + else + mCmdCommandEdit->setText(text); + break; + case KAEvent::EMAIL: + radio = mEmailRadio; + mEmailMessageEdit->setText(text); + break; + case KAEvent::MESSAGE: + default: + radio = mMessageRadio; + mTextMessageEdit->setText(text); + mKMailSerialNumber = 0; + if (alarmText.isEmail()) + { + mKMailSerialNumber = alarmText.kmailSerialNumber(); + + // Set up email fields also, in case the user wants an email alarm + mEmailToEdit->setText(alarmText.to()); + mEmailSubjectEdit->setText(alarmText.subject()); + mEmailMessageEdit->setText(alarmText.body()); + } + else if (alarmText.isScript()) + { + // Set up command script field also, in case the user wants a command alarm + mCmdScriptEdit->setText(text); + mCmdTypeScript->setChecked(true); + } + break; + } + mActionGroup->setButton(mActionGroup->id(radio)); +} + +/****************************************************************************** + * Create an "acknowledgement confirmation required" checkbox. + */ +CheckBox* EditAlarmDlg::createConfirmAckCheckbox(QWidget* parent, const char* name) +{ + CheckBox* widget = new CheckBox(i18n_k_ConfirmAck(), parent, name); + QWhatsThis::add(widget, + i18n("Check to be prompted for confirmation when you acknowledge the alarm.")); + return widget; +} + +/****************************************************************************** + * Save the state of all controls. + */ +void EditAlarmDlg::saveState(const KAEvent* event) +{ + delete mSavedEvent; + mSavedEvent = 0; + if (event) + mSavedEvent = new KAEvent(*event); + if (mTemplate) + { + mSavedTemplateName = mTemplateName->text(); + mSavedTemplateTimeType = mTemplateTimeGroup->selected(); + mSavedTemplateTime = mTemplateTime->time(); + mSavedTemplateAfterTime = mTemplateTimeAfter->value(); + } + mSavedTypeRadio = mActionGroup->selected(); + mSavedSoundType = mSoundPicker->sound(); + mSavedSoundFile = mSoundPicker->file(); + mSavedSoundVolume = mSoundPicker->volume(mSavedSoundFadeVolume, mSavedSoundFadeSeconds); + mSavedRepeatSound = mSoundPicker->repeat(); + mSavedConfirmAck = mConfirmAck->isChecked(); + mSavedFont = mFontColourButton->font(); + mSavedFgColour = mFontColourButton->fgColour(); + mSavedBgColour = mFileRadio->isOn() ? mBgColourButton->colour() : mFontColourButton->bgColour(); + mSavedReminder = mReminder->minutes(); + mSavedOnceOnly = mReminder->isOnceOnly(); + if (mSpecialActionsButton) + { + mSavedPreAction = mSpecialActionsButton->preAction(); + mSavedPostAction = mSpecialActionsButton->postAction(); + } + checkText(mSavedTextFileCommandMessage, false); + mSavedCmdScript = mCmdTypeScript->isChecked(); + mSavedCmdOutputRadio = mCmdOutputGroup->selected(); + mSavedCmdLogFile = mCmdLogFileEdit->text(); + if (mEmailFromList) + mSavedEmailFrom = mEmailFromList->currentIdentityName(); + mSavedEmailTo = mEmailToEdit->text(); + mSavedEmailSubject = mEmailSubjectEdit->text(); + mSavedEmailAttach.clear(); + for (int i = 0; i < mEmailAttachList->count(); ++i) + mSavedEmailAttach += mEmailAttachList->text(i); + mSavedEmailBcc = mEmailBcc->isChecked(); + if (mTimeWidget) + mSavedDateTime = mTimeWidget->getDateTime(0, false, false); + mSavedLateCancel = mLateCancel->minutes(); + mSavedAutoClose = mLateCancel->isAutoClose(); + if (mShowInKorganizer) + mSavedShowInKorganizer = mShowInKorganizer->isChecked(); + mSavedRecurrenceType = mRecurrenceEdit->repeatType(); +} + +/****************************************************************************** + * Check whether any of the controls has changed state since the dialog was + * first displayed. + * Reply = true if any non-deferral controls have changed, or if it's a new event. + * = false if no non-deferral controls have changed. In this case, + * mOnlyDeferred indicates whether deferral controls may have changed. + */ +bool EditAlarmDlg::stateChanged() const +{ + mChanged = true; + mOnlyDeferred = false; + if (!mSavedEvent) + return true; + QString textFileCommandMessage; + checkText(textFileCommandMessage, false); + if (mTemplate) + { + if (mSavedTemplateName != mTemplateName->text() + || mSavedTemplateTimeType != mTemplateTimeGroup->selected() + || mTemplateUseTime->isOn() && mSavedTemplateTime != mTemplateTime->time() + || mTemplateUseTimeAfter->isOn() && mSavedTemplateAfterTime != mTemplateTimeAfter->value()) + return true; + } + else + if (mSavedDateTime != mTimeWidget->getDateTime(0, false, false)) + return true; + if (mSavedTypeRadio != mActionGroup->selected() + || mSavedLateCancel != mLateCancel->minutes() + || mShowInKorganizer && mSavedShowInKorganizer != mShowInKorganizer->isChecked() + || textFileCommandMessage != mSavedTextFileCommandMessage + || mSavedRecurrenceType != mRecurrenceEdit->repeatType()) + return true; + if (mMessageRadio->isOn() || mFileRadio->isOn()) + { + if (mSavedSoundType != mSoundPicker->sound() + || mSavedConfirmAck != mConfirmAck->isChecked() + || mSavedFont != mFontColourButton->font() + || mSavedFgColour != mFontColourButton->fgColour() + || mSavedBgColour != (mFileRadio->isOn() ? mBgColourButton->colour() : mFontColourButton->bgColour()) + || mSavedReminder != mReminder->minutes() + || mSavedOnceOnly != mReminder->isOnceOnly() + || mSavedAutoClose != mLateCancel->isAutoClose()) + return true; + if (mSpecialActionsButton) + { + if (mSavedPreAction != mSpecialActionsButton->preAction() + || mSavedPostAction != mSpecialActionsButton->postAction()) + return true; + } + if (mSavedSoundType == SoundPicker::PLAY_FILE) + { + if (mSavedSoundFile != mSoundPicker->file()) + return true; + if (!mSavedSoundFile.isEmpty()) + { + float fadeVolume; + int fadeSecs; + if (mSavedRepeatSound != mSoundPicker->repeat() + || mSavedSoundVolume != mSoundPicker->volume(fadeVolume, fadeSecs) + || mSavedSoundFadeVolume != fadeVolume + || mSavedSoundFadeSeconds != fadeSecs) + return true; + } + } + } + else if (mCommandRadio->isOn()) + { + if (mSavedCmdScript != mCmdTypeScript->isChecked() + || mSavedCmdOutputRadio != mCmdOutputGroup->selected()) + return true; + if (mCmdOutputGroup->selectedId() == LOG_TO_FILE) + { + if (mSavedCmdLogFile != mCmdLogFileEdit->text()) + return true; + } + } + else if (mEmailRadio->isOn()) + { + QStringList emailAttach; + for (int i = 0; i < mEmailAttachList->count(); ++i) + emailAttach += mEmailAttachList->text(i); + if (mEmailFromList && mSavedEmailFrom != mEmailFromList->currentIdentityName() + || mSavedEmailTo != mEmailToEdit->text() + || mSavedEmailSubject != mEmailSubjectEdit->text() + || mSavedEmailAttach != emailAttach + || mSavedEmailBcc != mEmailBcc->isChecked()) + return true; + } + if (mRecurrenceEdit->stateChanged()) + return true; + if (mSavedEvent && mSavedEvent->deferred()) + mOnlyDeferred = true; + mChanged = false; + return false; +} + +/****************************************************************************** + * Get the currently entered dialogue data. + * The data is returned in the supplied KAEvent instance. + * Reply = false if the only change has been to an existing deferral. + */ +bool EditAlarmDlg::getEvent(KAEvent& event) +{ + if (mChanged) + { + // It's a new event, or the edit controls have changed + setEvent(event, mAlarmMessage, false); + return true; + } + + // Only the deferral time may have changed + event = *mSavedEvent; + if (mOnlyDeferred) + { + // Just modify the original event, to avoid expired recurring events + // being returned as rubbish. + if (mDeferDateTime.isValid()) + event.defer(mDeferDateTime, event.reminderDeferral(), false); + else + event.cancelDefer(); + } + return false; +} + +/****************************************************************************** +* Extract the data in the dialogue and set up a KAEvent from it. +* If 'trial' is true, the event is set up for a simple one-off test, ignoring +* recurrence, reminder, template etc. data. +*/ +void EditAlarmDlg::setEvent(KAEvent& event, const QString& text, bool trial) +{ + QDateTime dt; + if (!trial) + { + if (!mTemplate) + dt = mAlarmDateTime.dateTime(); + else if (mTemplateUseTime->isOn()) + dt = QDateTime(QDate(2000,1,1), mTemplateTime->time()); + } + KAEvent::Action type = getAlarmType(); + event.set(dt, text, (mFileRadio->isOn() ? mBgColourButton->colour() : mFontColourButton->bgColour()), + mFontColourButton->fgColour(), mFontColourButton->font(), + type, (trial ? 0 : mLateCancel->minutes()), getAlarmFlags()); + switch (type) + { + case KAEvent::MESSAGE: + if (AlarmText::checkIfEmail(text)) + event.setKMailSerialNumber(mKMailSerialNumber); + // fall through to FILE + case KAEvent::FILE: + { + float fadeVolume; + int fadeSecs; + float volume = mSoundPicker->volume(fadeVolume, fadeSecs); + event.setAudioFile(mSoundPicker->file(), volume, fadeVolume, fadeSecs); + if (!trial) + event.setReminder(mReminder->minutes(), mReminder->isOnceOnly()); + if (mSpecialActionsButton) + event.setActions(mSpecialActionsButton->preAction(), mSpecialActionsButton->postAction()); + break; + } + case KAEvent::EMAIL: + { + uint from = mEmailFromList ? mEmailFromList->currentIdentity() : 0; + event.setEmail(from, mEmailAddresses, mEmailSubjectEdit->text(), mEmailAttachments); + break; + } + case KAEvent::COMMAND: + if (mCmdOutputGroup->selectedId() == LOG_TO_FILE) + event.setLogFile(mCmdLogFileEdit->text()); + break; + default: + break; + } + if (!trial) + { + if (mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR) + { + mRecurrenceEdit->updateEvent(event, !mTemplate); + QDateTime now = QDateTime::currentDateTime(); + bool dateOnly = mAlarmDateTime.isDateOnly(); + if (dateOnly && mAlarmDateTime.date() < now.date() + || !dateOnly && mAlarmDateTime.rawDateTime() < now) + { + // A timed recurrence has an entered start date which has + // already expired, so we must adjust the next repetition. + event.setNextOccurrence(now); + } + mAlarmDateTime = event.startDateTime(); + if (mDeferDateTime.isValid() && mDeferDateTime < mAlarmDateTime) + { + bool deferral = true; + bool deferReminder = false; + int reminder = mReminder->minutes(); + if (reminder) + { + DateTime remindTime = mAlarmDateTime.addMins(-reminder); + if (mDeferDateTime >= remindTime) + { + if (remindTime > QDateTime::currentDateTime()) + deferral = false; // ignore deferral if it's after next reminder + else if (mDeferDateTime > remindTime) + deferReminder = true; // it's the reminder which is being deferred + } + } + if (deferral) + event.defer(mDeferDateTime, deferReminder, false); + } + } + if (mTemplate) + { + int afterTime = mTemplateDefaultTime->isOn() ? 0 + : mTemplateUseTimeAfter->isOn() ? mTemplateTimeAfter->value() : -1; + event.setTemplate(mTemplateName->text(), afterTime); + } + } +} + +/****************************************************************************** + * Get the currently specified alarm flag bits. + */ +int EditAlarmDlg::getAlarmFlags() const +{ + bool displayAlarm = mMessageRadio->isOn() || mFileRadio->isOn(); + bool cmdAlarm = mCommandRadio->isOn(); + bool emailAlarm = mEmailRadio->isOn(); + return (displayAlarm && mSoundPicker->sound() == SoundPicker::BEEP ? KAEvent::BEEP : 0) + | (displayAlarm && mSoundPicker->sound() == SoundPicker::SPEAK ? KAEvent::SPEAK : 0) + | (displayAlarm && mSoundPicker->repeat() ? KAEvent::REPEAT_SOUND : 0) + | (displayAlarm && mConfirmAck->isChecked() ? KAEvent::CONFIRM_ACK : 0) + | (displayAlarm && mLateCancel->isAutoClose() ? KAEvent::AUTO_CLOSE : 0) + | (cmdAlarm && mCmdTypeScript->isChecked() ? KAEvent::SCRIPT : 0) + | (cmdAlarm && mCmdOutputGroup->selectedId() == EXEC_IN_TERMINAL ? KAEvent::EXEC_IN_XTERM : 0) + | (emailAlarm && mEmailBcc->isChecked() ? KAEvent::EMAIL_BCC : 0) + | (mShowInKorganizer && mShowInKorganizer->isChecked() ? KAEvent::COPY_KORGANIZER : 0) + | (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN ? KAEvent::REPEAT_AT_LOGIN : 0) + | ((mTemplate ? mTemplateAnyTime->isOn() : mAlarmDateTime.isDateOnly()) ? KAEvent::ANY_TIME : 0) + | (mFontColourButton->defaultFont() ? KAEvent::DEFAULT_FONT : 0); +} + +/****************************************************************************** + * Get the currently selected alarm type. + */ +KAEvent::Action EditAlarmDlg::getAlarmType() const +{ + return mFileRadio->isOn() ? KAEvent::FILE + : mCommandRadio->isOn() ? KAEvent::COMMAND + : mEmailRadio->isOn() ? KAEvent::EMAIL + : KAEvent::MESSAGE; +} + +/****************************************************************************** +* Called when the dialog is displayed. +* The first time through, sets the size to the same as the last time it was +* displayed. +*/ +void EditAlarmDlg::showEvent(QShowEvent* se) +{ + if (!mDeferGroupHeight) + { + mDeferGroupHeight = mDeferGroup->height() + spacingHint(); + QSize s; + if (KAlarm::readConfigWindowSize(EDIT_DIALOG_NAME, s)) + s.setHeight(s.height() + (mDeferGroup->isHidden() ? 0 : mDeferGroupHeight)); + else + s = minimumSize(); + resize(s); + } + KWin::setOnDesktop(winId(), mDesktop); // ensure it displays on the desktop expected by the user + KDialog::showEvent(se); +} + +/****************************************************************************** +* Called when the dialog's size has changed. +* Records the new size (adjusted to ignore the optional height of the deferred +* time edit widget) in the config file. +*/ +void EditAlarmDlg::resizeEvent(QResizeEvent* re) +{ + if (isVisible()) + { + QSize s = re->size(); + s.setHeight(s.height() - (mDeferGroup->isHidden() ? 0 : mDeferGroupHeight)); + KAlarm::writeConfigWindowSize(EDIT_DIALOG_NAME, s); + } + KDialog::resizeEvent(re); +} + +/****************************************************************************** +* Called when the OK button is clicked. +* Validate the input data. +*/ +void EditAlarmDlg::slotOk() +{ + if (!stateChanged()) + { + // No changes have been made except possibly to an existing deferral + if (!mOnlyDeferred) + reject(); + else + accept(); + return; + } + RecurrenceEdit::RepeatType recurType = mRecurrenceEdit->repeatType(); + if (mTimeWidget + && mTabs->currentPageIndex() == mRecurPageIndex && recurType == RecurrenceEdit::AT_LOGIN) + mTimeWidget->setDateTime(mRecurrenceEdit->endDateTime()); + bool timedRecurrence = mRecurrenceEdit->isTimedRepeatType(); // does it recur other than at login? + if (mTemplate) + { + // Check that the template name is not blank and is unique + QString errmsg; + QString name = mTemplateName->text(); + if (name.isEmpty()) + errmsg = i18n("You must enter a name for the alarm template"); + else if (name != mSavedTemplateName) + { + AlarmCalendar* cal = AlarmCalendar::templateCalendarOpen(); + if (cal && KAEvent::findTemplateName(*cal, name).valid()) + errmsg = i18n("Template name is already in use"); + } + if (!errmsg.isEmpty()) + { + mTemplateName->setFocus(); + KMessageBox::sorry(this, errmsg); + return; + } + } + else + { + QWidget* errWidget; + mAlarmDateTime = mTimeWidget->getDateTime(0, !timedRecurrence, false, &errWidget); + if (errWidget) + { + // It's more than just an existing deferral being changed, so the time matters + mTabs->setCurrentPage(mMainPageIndex); + errWidget->setFocus(); + mTimeWidget->getDateTime(); // display the error message now + return; + } + } + if (!checkCommandData() + || !checkEmailData()) + return; + if (!mTemplate) + { + if (timedRecurrence) + { + QDateTime now = QDateTime::currentDateTime(); + if (mAlarmDateTime.date() < now.date() + || mAlarmDateTime.date() == now.date() + && !mAlarmDateTime.isDateOnly() && mAlarmDateTime.time() < now.time()) + { + // A timed recurrence has an entered start date which + // has already expired, so we must adjust it. + KAEvent event; + getEvent(event); // this may adjust mAlarmDateTime + if (( mAlarmDateTime.date() < now.date() + || mAlarmDateTime.date() == now.date() + && !mAlarmDateTime.isDateOnly() && mAlarmDateTime.time() < now.time()) + && event.nextOccurrence(now, mAlarmDateTime, KAEvent::ALLOW_FOR_REPETITION) == KAEvent::NO_OCCURRENCE) + { + KMessageBox::sorry(this, i18n("Recurrence has already expired")); + return; + } + } + } + QString errmsg; + QWidget* errWidget = mRecurrenceEdit->checkData(mAlarmDateTime.dateTime(), errmsg); + if (errWidget) + { + mTabs->setCurrentPage(mRecurPageIndex); + errWidget->setFocus(); + KMessageBox::sorry(this, errmsg); + return; + } + } + if (recurType != RecurrenceEdit::NO_RECUR) + { + KAEvent recurEvent; + int longestRecurInterval = -1; + int reminder = mReminder->minutes(); + if (reminder && !mReminder->isOnceOnly()) + { + mRecurrenceEdit->updateEvent(recurEvent, false); + longestRecurInterval = recurEvent.longestRecurrenceInterval(); + if (longestRecurInterval && reminder >= longestRecurInterval) + { + mTabs->setCurrentPage(mMainPageIndex); + mReminder->setFocusOnCount(); + KMessageBox::sorry(this, i18n("Reminder period must be less than the recurrence interval, unless '%1' is checked." + ).arg(Reminder::i18n_first_recurrence_only())); + return; + } + } + if (mRecurrenceEdit->subRepeatCount()) + { + if (longestRecurInterval < 0) + { + mRecurrenceEdit->updateEvent(recurEvent, false); + longestRecurInterval = recurEvent.longestRecurrenceInterval(); + } + if (longestRecurInterval > 0 + && recurEvent.repeatInterval() * recurEvent.repeatCount() >= longestRecurInterval - reminder) + { + KMessageBox::sorry(this, i18n("The duration of a repetition within the recurrence must be less than the recurrence interval minus any reminder period")); + mRecurrenceEdit->activateSubRepetition(); // display the alarm repetition dialog again + return; + } + if (recurEvent.repeatInterval() % 1440 + && (mTemplate && mTemplateAnyTime->isOn() || !mTemplate && mAlarmDateTime.isDateOnly())) + { + KMessageBox::sorry(this, i18n("For a repetition within the recurrence, its period must be in units of days or weeks for a date-only alarm")); + mRecurrenceEdit->activateSubRepetition(); // display the alarm repetition dialog again + return; + } + } + } + if (checkText(mAlarmMessage)) + accept(); +} + +/****************************************************************************** +* Called when the Try button is clicked. +* Display/execute the alarm immediately for the user to check its configuration. +*/ +void EditAlarmDlg::slotTry() +{ + QString text; + if (checkText(text)) + { + if (mEmailRadio->isOn()) + { + if (!checkEmailData() + || KMessageBox::warningContinueCancel(this, i18n("Do you really want to send the email now to the specified recipient(s)?"), + i18n("Confirm Email"), i18n("&Send")) != KMessageBox::Continue) + return; + } + KAEvent event; + setEvent(event, text, true); + void* proc = theApp()->execAlarm(event, event.firstAlarm(), false, false); + if (proc) + { + if (mCommandRadio->isOn() && mCmdOutputGroup->selectedId() != EXEC_IN_TERMINAL) + { + theApp()->commandMessage((ShellProcess*)proc, this); + KMessageBox::information(this, i18n("Command executed:\n%1").arg(text)); + theApp()->commandMessage((ShellProcess*)proc, 0); + } + else if (mEmailRadio->isOn()) + { + QString bcc; + if (mEmailBcc->isChecked()) + bcc = i18n("\nBcc: %1").arg(Preferences::emailBccAddress()); + KMessageBox::information(this, i18n("Email sent to:\n%1%2").arg(mEmailAddresses.join("\n")).arg(bcc)); + } + } + } +} + +/****************************************************************************** +* Called when the Cancel button is clicked. +*/ +void EditAlarmDlg::slotCancel() +{ + reject(); +} + +/****************************************************************************** +* Called when the Load Template button is clicked. +* Prompt to select a template and initialise the dialogue with its contents. +*/ +void EditAlarmDlg::slotDefault() +{ + TemplatePickDlg dlg(this, "templPickDlg"); + if (dlg.exec() == QDialog::Accepted) + initialise(dlg.selectedTemplate()); +} + +/****************************************************************************** + * Called when the Change deferral button is clicked. + */ +void EditAlarmDlg::slotEditDeferral() +{ + if (!mTimeWidget) + return; + bool limit = true; + int repeatInterval; + int repeatCount = mRecurrenceEdit->subRepeatCount(&repeatInterval); + DateTime start = mSavedEvent->recurs() ? (mExpiredRecurrence ? DateTime() : mSavedEvent->mainDateTime()) + : mTimeWidget->getDateTime(0, !repeatCount, !mExpiredRecurrence); + if (!start.isValid()) + { + if (!mExpiredRecurrence) + return; + limit = false; + } + QDateTime now = QDateTime::currentDateTime(); + if (limit) + { + if (repeatCount && start < now) + { + // Sub-repetition - find the time of the next one + repeatInterval *= 60; + int repetition = (start.secsTo(now) + repeatInterval - 1) / repeatInterval; + if (repetition > repeatCount) + { + mTimeWidget->getDateTime(); // output the appropriate error message + return; + } + start = start.addSecs(repetition * repeatInterval); + } + } + + bool deferred = mDeferDateTime.isValid(); + DeferAlarmDlg deferDlg(i18n("Defer Alarm"), (deferred ? mDeferDateTime : DateTime(now.addSecs(60))), + deferred, this, "EditDeferDlg"); + if (limit) + { + // Don't allow deferral past the next recurrence + int reminder = mReminder->minutes(); + if (reminder) + { + DateTime remindTime = start.addMins(-reminder); + if (QDateTime::currentDateTime() < remindTime) + start = remindTime; + } + deferDlg.setLimit(start.addSecs(-60)); + } + if (deferDlg.exec() == QDialog::Accepted) + { + mDeferDateTime = deferDlg.getDateTime(); + mDeferTimeLabel->setText(mDeferDateTime.isValid() ? mDeferDateTime.formatLocale() : QString::null); + } +} + +/****************************************************************************** +* Called when the main page is shown. +* Sets the focus widget to the first edit field. +*/ +void EditAlarmDlg::slotShowMainPage() +{ + slotAlarmTypeChanged(-1); + if (!mMainPageShown) + { + if (mTemplateName) + mTemplateName->setFocus(); + mMainPageShown = true; + } + if (mTimeWidget) + { + if (!mReadOnly && mRecurPageShown && mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) + mTimeWidget->setDateTime(mRecurrenceEdit->endDateTime()); + if (mReadOnly || mRecurrenceEdit->isTimedRepeatType()) + mTimeWidget->setMinDateTime(); // don't set a minimum date/time + else + mTimeWidget->setMinDateTimeIsCurrent(); // set the minimum date/time to track the clock + } +} + +/****************************************************************************** +* Called when the recurrence edit page is shown. +* The recurrence defaults are set to correspond to the start date. +* The first time, for a new alarm, the recurrence end date is set according to +* the alarm start time. +*/ +void EditAlarmDlg::slotShowRecurrenceEdit() +{ + mRecurPageIndex = mTabs->currentPageIndex(); + if (!mReadOnly && !mTemplate) + { + QDateTime now = QDateTime::currentDateTime(); + mAlarmDateTime = mTimeWidget->getDateTime(0, false, false); + bool expired = (mAlarmDateTime.dateTime() < now); + if (mRecurSetDefaultEndDate) + { + mRecurrenceEdit->setDefaultEndDate(expired ? now.date() : mAlarmDateTime.date()); + mRecurSetDefaultEndDate = false; + } + mRecurrenceEdit->setStartDate(mAlarmDateTime.date(), now.date()); + if (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) + mRecurrenceEdit->setEndDateTime(expired ? now : mAlarmDateTime); + } + mRecurPageShown = true; +} + +/****************************************************************************** +* Called when the recurrence type selection changes. +* Enables/disables date-only alarms as appropriate. +* Enables/disables controls depending on at-login setting. +*/ +void EditAlarmDlg::slotRecurTypeChange(int repeatType) +{ + bool atLogin = (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN); + if (!mTemplate) + { + bool recurs = (mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR); + if (mDeferGroup) + mDeferGroup->setEnabled(recurs); + mTimeWidget->enableAnyTime(!recurs || repeatType != RecurrenceEdit::SUBDAILY); + if (atLogin) + { + mAlarmDateTime = mTimeWidget->getDateTime(0, false, false); + mRecurrenceEdit->setEndDateTime(mAlarmDateTime.dateTime()); + } + mReminder->enableOnceOnly(recurs && !atLogin); + } + mReminder->setEnabled(!atLogin); + mLateCancel->setEnabled(!atLogin); + if (mShowInKorganizer) + mShowInKorganizer->setEnabled(!atLogin); + slotRecurFrequencyChange(); +} + +/****************************************************************************** +* Called when the recurrence frequency selection changes, or the sub- +* repetition interval changes. +* Updates the recurrence frequency text. +*/ +void EditAlarmDlg::slotRecurFrequencyChange() +{ + slotSetSubRepetition(); + KAEvent event; + mRecurrenceEdit->updateEvent(event, false); + mTabs->setTabLabel(mTabs->page(mRecurPageIndex), recurText(event)); +} + +/****************************************************************************** +* Called when the Repetition within Recurrence button has been pressed to +* display the sub-repetition dialog. +* Alarm repetition has the following restrictions: +* 1) Not allowed for a repeat-at-login alarm +* 2) For a date-only alarm, the repeat interval must be a whole number of days. +* 3) The overall repeat duration must be less than the recurrence interval. +*/ +void EditAlarmDlg::slotSetSubRepetition() +{ + bool dateOnly = mTemplate ? mTemplateAnyTime->isOn() : mTimeWidget->anyTime(); + mRecurrenceEdit->setSubRepetition(mReminder->minutes(), dateOnly); +} + +/****************************************************************************** +* Validate and convert command alarm data. +*/ +bool EditAlarmDlg::checkCommandData() +{ + if (mCommandRadio->isOn() && mCmdOutputGroup->selectedId() == LOG_TO_FILE) + { + // Validate the log file name + QString file = mCmdLogFileEdit->text(); + QFileInfo info(file); + QDir::setCurrent(QDir::homeDirPath()); + bool err = file.isEmpty() || info.isDir(); + if (!err) + { + if (info.exists()) + { + err = !info.isWritable(); + } + else + { + QFileInfo dirinfo(info.dirPath(true)); // get absolute directory path + err = (!dirinfo.isDir() || !dirinfo.isWritable()); + } + } + if (err) + { + mTabs->setCurrentPage(mMainPageIndex); + mCmdLogFileEdit->setFocus(); + KMessageBox::sorry(this, i18n("Log file must be the name or path of a local file, with write permission.")); + return false; + } + // Convert the log file to an absolute path + mCmdLogFileEdit->setText(info.absFilePath()); + } + return true; +} + +/****************************************************************************** +* Convert the email addresses to a list, and validate them. Convert the email +* attachments to a list. +*/ +bool EditAlarmDlg::checkEmailData() +{ + if (mEmailRadio->isOn()) + { + QString addrs = mEmailToEdit->text(); + if (addrs.isEmpty()) + mEmailAddresses.clear(); + else + { + QString bad = KAMail::convertAddresses(addrs, mEmailAddresses); + if (!bad.isEmpty()) + { + mEmailToEdit->setFocus(); + KMessageBox::error(this, i18n("Invalid email address:\n%1").arg(bad)); + return false; + } + } + if (mEmailAddresses.isEmpty()) + { + mEmailToEdit->setFocus(); + KMessageBox::error(this, i18n("No email address specified")); + return false; + } + + mEmailAttachments.clear(); + for (int i = 0; i < mEmailAttachList->count(); ++i) + { + QString att = mEmailAttachList->text(i); + switch (KAMail::checkAttachment(att)) + { + case 1: + mEmailAttachments.append(att); + break; + case 0: + break; // empty + case -1: + mEmailAttachList->setFocus(); + KMessageBox::error(this, i18n("Invalid email attachment:\n%1").arg(att)); + return false; + } + } + } + return true; +} + +/****************************************************************************** +* Called when one of the alarm action type radio buttons is clicked, +* to display the appropriate set of controls for that action type. +*/ +void EditAlarmDlg::slotAlarmTypeChanged(int) +{ + bool displayAlarm = false; + QWidget* focus = 0; + if (mMessageRadio->isOn()) + { + mFileBox->hide(); + mFilePadding->hide(); + mTextMessageEdit->show(); + mFontColourButton->show(); + mBgColourBox->hide(); + mSoundPicker->showSpeak(true); + mDisplayAlarmsFrame->show(); + mCommandFrame->hide(); + mEmailFrame->hide(); + mReminder->show(); + mConfirmAck->show(); + setButtonWhatsThis(Try, i18n("Display the alarm message now")); + focus = mTextMessageEdit; + displayAlarm = true; + } + else if (mFileRadio->isOn()) + { + mTextMessageEdit->hide(); + mFileBox->show(); + mFilePadding->show(); + mFontColourButton->hide(); + mBgColourBox->show(); + mSoundPicker->showSpeak(false); + mDisplayAlarmsFrame->show(); + mCommandFrame->hide(); + mEmailFrame->hide(); + mReminder->show(); + mConfirmAck->show(); + setButtonWhatsThis(Try, i18n("Display the file now")); + mFileMessageEdit->setNoSelect(); + focus = mFileMessageEdit; + displayAlarm = true; + } + else if (mCommandRadio->isOn()) + { + mDisplayAlarmsFrame->hide(); + mCommandFrame->show(); + mEmailFrame->hide(); + mReminder->hide(); + mConfirmAck->hide(); + setButtonWhatsThis(Try, i18n("Execute the specified command now")); + mCmdCommandEdit->setNoSelect(); + focus = mCmdCommandEdit; + } + else if (mEmailRadio->isOn()) + { + mDisplayAlarmsFrame->hide(); + mCommandFrame->hide(); + mEmailFrame->show(); + mReminder->hide(); + mConfirmAck->hide(); + setButtonWhatsThis(Try, i18n("Send the email to the specified addressees now")); + mEmailToEdit->setNoSelect(); + focus = mEmailToEdit; + } + mLateCancel->showAutoClose(displayAlarm); + mLateCancel->setFixedSize(mLateCancel->sizeHint()); + if (focus) + focus->setFocus(); +} + +/****************************************************************************** +* Called when one of the command type radio buttons is clicked, +* to display the appropriate edit field. +*/ +void EditAlarmDlg::slotCmdScriptToggled(bool on) +{ + if (on) + { + mCmdCommandEdit->hide(); + mCmdPadding->hide(); + mCmdScriptEdit->show(); + mCmdScriptEdit->setFocus(); + } + else + { + mCmdScriptEdit->hide(); + mCmdCommandEdit->show(); + mCmdPadding->show(); + mCmdCommandEdit->setFocus(); + } +} + +/****************************************************************************** +* Called when one of the template time radio buttons is clicked, +* to enable or disable the template time entry spin boxes. +*/ +void EditAlarmDlg::slotTemplateTimeType(int) +{ + mTemplateTime->setEnabled(mTemplateUseTime->isOn()); + mTemplateTimeAfter->setEnabled(mTemplateUseTimeAfter->isOn()); +} + +/****************************************************************************** +* Called when the "Any time" checkbox is toggled in the date/time widget. +* Sets the advance reminder and late cancel units to days if any time is checked. +*/ +void EditAlarmDlg::slotAnyTimeToggled(bool anyTime) +{ + if (mReminder->isReminder()) + mReminder->setDateOnly(anyTime); + mLateCancel->setDateOnly(anyTime); +} + +/****************************************************************************** + * Get a selection from the Address Book. + */ +void EditAlarmDlg::openAddressBook() +{ + KABC::Addressee a = KABC::AddresseeDialog::getAddressee(this); + if (a.isEmpty()) + return; + Person person(a.realName(), a.preferredEmail()); + QString addrs = mEmailToEdit->text().stripWhiteSpace(); + if (!addrs.isEmpty()) + addrs += ", "; + addrs += person.fullName(); + mEmailToEdit->setText(addrs); +} + +/****************************************************************************** + * Select a file to attach to the email. + */ +void EditAlarmDlg::slotAddAttachment() +{ + QString url = KAlarm::browseFile(i18n("Choose File to Attach"), mAttachDefaultDir, QString::null, + QString::null, KFile::ExistingOnly, this, "pickAttachFile"); + if (!url.isEmpty()) + { + mEmailAttachList->insertItem(url); + mEmailAttachList->setCurrentItem(mEmailAttachList->count() - 1); // select the new item + mEmailRemoveButton->setEnabled(true); + mEmailAttachList->setEnabled(true); + } +} + +/****************************************************************************** + * Remove the currently selected attachment from the email. + */ +void EditAlarmDlg::slotRemoveAttachment() +{ + int item = mEmailAttachList->currentItem(); + mEmailAttachList->removeItem(item); + int count = mEmailAttachList->count(); + if (item >= count) + mEmailAttachList->setCurrentItem(count - 1); + if (!count) + { + mEmailRemoveButton->setEnabled(false); + mEmailAttachList->setEnabled(false); + } +} + +/****************************************************************************** +* Clean up the alarm text, and if it's a file, check whether it's valid. +*/ +bool EditAlarmDlg::checkText(QString& result, bool showErrorMessage) const +{ + if (mMessageRadio->isOn()) + result = mTextMessageEdit->text(); + else if (mEmailRadio->isOn()) + result = mEmailMessageEdit->text(); + else if (mCommandRadio->isOn()) + { + if (mCmdTypeScript->isChecked()) + result = mCmdScriptEdit->text(); + else + result = mCmdCommandEdit->text(); + result = result.stripWhiteSpace(); + } + else if (mFileRadio->isOn()) + { + QString alarmtext = mFileMessageEdit->text().stripWhiteSpace(); + // Convert any relative file path to absolute + // (using home directory as the default) + enum Err { NONE = 0, BLANK, NONEXISTENT, DIRECTORY, UNREADABLE, NOT_TEXT_IMAGE }; + Err err = NONE; + KURL url; + int i = alarmtext.find(QString::fromLatin1("/")); + if (i > 0 && alarmtext[i - 1] == ':') + { + url = alarmtext; + url.cleanPath(); + alarmtext = url.prettyURL(); + KIO::UDSEntry uds; + if (!KIO::NetAccess::stat(url, uds, MainWindow::mainMainWindow())) + err = NONEXISTENT; + else + { + KFileItem fi(uds, url); + if (fi.isDir()) err = DIRECTORY; + else if (!fi.isReadable()) err = UNREADABLE; + } + } + else if (alarmtext.isEmpty()) + err = BLANK; // blank file name + else + { + // It's a local file - convert to absolute path & check validity + QFileInfo info(alarmtext); + QDir::setCurrent(QDir::homeDirPath()); + alarmtext = info.absFilePath(); + url.setPath(alarmtext); + alarmtext = QString::fromLatin1("file:") + alarmtext; + if (!err) + { + if (info.isDir()) err = DIRECTORY; + else if (!info.exists()) err = NONEXISTENT; + else if (!info.isReadable()) err = UNREADABLE; + } + } + if (!err) + { + switch (KAlarm::fileType(KFileItem(KFileItem::Unknown, KFileItem::Unknown, url).mimetype())) + { + case KAlarm::TextFormatted: + case KAlarm::TextPlain: + case KAlarm::TextApplication: + case KAlarm::Image: + break; + default: + err = NOT_TEXT_IMAGE; + break; + } + } + if (err && showErrorMessage) + { + mFileMessageEdit->setFocus(); + QString errmsg; + switch (err) + { + case BLANK: + KMessageBox::sorry(const_cast<EditAlarmDlg*>(this), i18n("Please select a file to display")); + return false; + case NONEXISTENT: errmsg = i18n("%1\nnot found"); break; + case DIRECTORY: errmsg = i18n("%1\nis a folder"); break; + case UNREADABLE: errmsg = i18n("%1\nis not readable"); break; + case NOT_TEXT_IMAGE: errmsg = i18n("%1\nappears not to be a text or image file"); break; + case NONE: + default: + break; + } + if (KMessageBox::warningContinueCancel(const_cast<EditAlarmDlg*>(this), errmsg.arg(alarmtext)) + == KMessageBox::Cancel) + return false; + } + result = alarmtext; + } + return true; +} + + +/*============================================================================= += Class TextEdit += A text edit field with a minimum height of 3 text lines. += Provides KDE 2 compatibility. +=============================================================================*/ +TextEdit::TextEdit(QWidget* parent, const char* name) + : KTextEdit(parent, name) +{ + QSize tsize = sizeHint(); + tsize.setHeight(fontMetrics().lineSpacing()*13/4 + 2*frameWidth()); + setMinimumSize(tsize); +} + +void TextEdit::dragEnterEvent(QDragEnterEvent* e) +{ + if (KCal::ICalDrag::canDecode(e)) + e->accept(false); // don't accept "text/calendar" objects + KTextEdit::dragEnterEvent(e); +} diff --git a/kalarm/editdlg.h b/kalarm/editdlg.h new file mode 100644 index 000000000..8a8f40ce9 --- /dev/null +++ b/kalarm/editdlg.h @@ -0,0 +1,260 @@ +/* + * editdlg.h - dialogue to create or modify an alarm or alarm template + * Program: kalarm + * Copyright © 2001-2006,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef EDITDLG_H +#define EDITDLG_H + +#include <qdatetime.h> +#include <qlineedit.h> + +#include <kdialogbase.h> + +#include "alarmevent.h" +#include "alarmtext.h" +#include "datetime.h" +#include "soundpicker.h" + +class QButton; +class QGroupBox; +class QComboBox; +class QTabWidget; +class QVBox; +class QHBox; +class EmailIdCombo; +class FontColourButton; +class ColourCombo; +class ButtonGroup; +class TimeEdit; +class RadioButton; +class CheckBox; +class LateCancelSelector; +class AlarmTimeWidget; +class RecurrenceEdit; +class Reminder; +class SpecialActionsButton; +class TimeSpinBox; +class LineEdit; +class TextEdit; +class PickAlarmFileRadio; + + +class EditAlarmDlg : public KDialogBase +{ + Q_OBJECT + public: + enum MessageType { MESSAGE, FILE }; + enum CmdLogType { DISCARD_OUTPUT, LOG_TO_FILE, EXEC_IN_TERMINAL }; + + EditAlarmDlg(bool Template, const QString& caption, QWidget* parent = 0, const char* name = 0, + const KAEvent* = 0, bool readOnly = false); + virtual ~EditAlarmDlg(); + bool getEvent(KAEvent&); + void setAction(KAEvent::Action, const AlarmText& = AlarmText()); + + static CheckBox* createConfirmAckCheckbox(QWidget* parent, const char* name = 0); + + static QString i18n_ConfirmAck(); // plain text of 'Confirm acknowledgement' checkbox + static QString i18n_k_ConfirmAck(); // text of 'Confirm acknowledgement' checkbox, with 'k' shortcut + static QString i18n_SpecialActions(); // plain text of 'Special Actions...' button + static QString i18n_ShowInKOrganizer(); // plain text of 'Show in KOrganizer' checkbox + static QString i18n_g_ShowInKOrganizer(); // text of 'Show in KOrganizer' checkbox, with 'G' shortcut + static QString i18n_EnterScript(); // plain text of 'Enter a script' checkbox + static QString i18n_p_EnterScript(); // text of 'Enter a script' checkbox, with 'P' shortcut + static QString i18n_ExecInTermWindow(); // plain text of 'Execute in terminal window' checkbox + static QString i18n_w_ExecInTermWindow(); // text of 'Execute in terminal window' radio button, with 'W' shortcut + static QString i18n_u_ExecInTermWindow(); // text of 'Execute in terminal window' radio button, with 'U' shortcut + static QString i18n_g_LogToFile(); // text of 'Log to file' radio button, with 'G' shortcut + static QString i18n_CopyEmailToSelf(); // plain text of 'Copy email to self' checkbox + static QString i18n_e_CopyEmailToSelf(); // text of 'Copy email to self' checkbox, with 'E' shortcut + static QString i18n_s_CopyEmailToSelf(); // text of 'Copy email to self' checkbox, with 'S' shortcut + static QString i18n_EmailFrom(); // plain text of 'From:' (email address) + static QString i18n_f_EmailFrom(); // text of 'From:' (email address), with 'F' shortcut + static QString i18n_EmailTo(); // plain text of 'To:' (email addressee) + static QString i18n_EmailSubject(); // plain text of 'Subject:' (email) + static QString i18n_j_EmailSubject(); // text of 'Subject:' (email), with 'J' shortcut + + protected: + virtual void resizeEvent(QResizeEvent*); + virtual void showEvent(QShowEvent*); + protected slots: + virtual void slotOk(); + virtual void slotCancel(); + virtual void slotTry(); + virtual void slotDefault(); // Load Template + private slots: + void slotRecurTypeChange(int repeatType); + void slotRecurFrequencyChange(); + void slotAlarmTypeChanged(int id); + void slotEditDeferral(); + void openAddressBook(); + void slotAddAttachment(); + void slotRemoveAttachment(); + void slotShowMainPage(); + void slotShowRecurrenceEdit(); + void slotAnyTimeToggled(bool anyTime); + void slotTemplateTimeType(int id); + void slotSetSubRepetition(); + void slotCmdScriptToggled(bool); + + private: + void initialise(const KAEvent*); + void setReadOnly(); + void setEvent(KAEvent&, const QString& text, bool trial); + KAEvent::Action getAlarmType() const; + int getAlarmFlags() const; + bool checkText(QString& result, bool showErrorMessage = true) const; + void setSoundPicker(); + void setRecurTabTitle(const KAEvent* = 0); + bool checkCommandData(); + bool checkEmailData(); + + void initDisplayAlarms(QWidget* parent); + void initCommand(QWidget* parent); + void initEmail(QWidget* parent); + void saveState(const KAEvent*); + bool stateChanged() const; + + QTabWidget* mTabs; // the tabs in the dialog + int mMainPageIndex; + int mRecurPageIndex; + bool mMainPageShown; // true once the main tab has been displayed + bool mRecurPageShown; // true once the recurrence tab has been displayed + bool mRecurSetDefaultEndDate; // adjust default end date/time when recurrence tab is displayed + + ButtonGroup* mActionGroup; + RadioButton* mMessageRadio; + RadioButton* mCommandRadio; + PickAlarmFileRadio* mFileRadio; + RadioButton* mEmailRadio; + QWidgetStack* mAlarmTypeStack; + + // Templates + QLineEdit* mTemplateName; + ButtonGroup* mTemplateTimeGroup; + RadioButton* mTemplateDefaultTime; // no alarm time is specified + RadioButton* mTemplateUseTimeAfter;// alarm time is specified as an offset from current + RadioButton* mTemplateAnyTime; // alarms have date only, no time + RadioButton* mTemplateUseTime; // an alarm time is specified + TimeSpinBox* mTemplateTimeAfter; // the specified offset from the current time + TimeEdit* mTemplateTime; // the alarm time which is specified + + // Display alarm options widgets + QFrame* mDisplayAlarmsFrame; + QHBox* mFileBox; + QHBox* mFilePadding; + SoundPicker* mSoundPicker; + CheckBox* mConfirmAck; + FontColourButton* mFontColourButton; // for text display option + ColourCombo* mBgColourButton; // for file display option + QHBox* mBgColourBox; + SpecialActionsButton* mSpecialActionsButton; + Reminder* mReminder; + bool mReminderDeferral; + bool mReminderArchived; + // Text message alarm widgets + TextEdit* mTextMessageEdit; // text message edit box + // Text file alarm widgets + LineEdit* mFileMessageEdit; // text file URL edit box + QPushButton* mFileBrowseButton; // text file browse button + QString mFileDefaultDir; // default directory for browse button + // Command alarm widgets + QFrame* mCommandFrame; + CheckBox* mCmdTypeScript; // entering a script + LineEdit* mCmdCommandEdit; // command line edit box + TextEdit* mCmdScriptEdit; // script edit box + ButtonGroup* mCmdOutputGroup; // what to do with command output + LineEdit* mCmdLogFileEdit; // log file URL edit box + QWidget* mCmdPadding; + // Email alarm widgets + QFrame* mEmailFrame; + EmailIdCombo* mEmailFromList; + LineEdit* mEmailToEdit; + QPushButton* mEmailAddressButton; // email open address book button + QLineEdit* mEmailSubjectEdit; + TextEdit* mEmailMessageEdit; // email body edit box + QComboBox* mEmailAttachList; + QPushButton* mEmailAddAttachButton; + QPushButton* mEmailRemoveButton; + CheckBox* mEmailBcc; + QString mAttachDefaultDir; + + QGroupBox* mDeferGroup; + QLabel* mDeferTimeLabel; + QPushButton* mDeferChangeButton; + + AlarmTimeWidget* mTimeWidget; + LateCancelSelector* mLateCancel; + CheckBox* mShowInKorganizer; + + RecurrenceEdit* mRecurrenceEdit; + + QString mAlarmMessage; // message text/file name/command/email message + DateTime mAlarmDateTime; + DateTime mDeferDateTime; + EmailAddressList mEmailAddresses; // list of addresses to send email to + QStringList mEmailAttachments; // list of email attachment file names + unsigned long mKMailSerialNumber; // if email text, message's KMail serial number, else 0 + int mDeferGroupHeight; // height added by deferred time widget + int mDesktop; // desktop to display the dialog in + bool mTemplate; // editing an alarm template + bool mExpiredRecurrence; // initially a recurrence which has expired + mutable bool mChanged; // controls other than deferral have changed since dialog was displayed + mutable bool mOnlyDeferred; // the only change made in the dialog was to the existing deferral + bool mDesiredReadOnly; // the specified read-only status of the dialogue + bool mReadOnly; // the actual read-only status of the dialogue + + // Initial state of all controls + KAEvent* mSavedEvent; + QString mSavedTemplateName; // mTemplateName value + QButton* mSavedTemplateTimeType; // selected button in mTemplateTimeGroup + QTime mSavedTemplateTime; // mTemplateTime value + int mSavedTemplateAfterTime; // mTemplateAfterTime value + QButton* mSavedTypeRadio; // mMessageRadio, etc + SoundPicker::Type mSavedSoundType; // mSoundPicker sound type + bool mSavedRepeatSound; // mSoundPicker repeat status + QString mSavedSoundFile; // mSoundPicker sound file + float mSavedSoundVolume; // mSoundPicker volume + float mSavedSoundFadeVolume;// mSoundPicker fade volume + int mSavedSoundFadeSeconds;// mSoundPicker fade time + bool mSavedConfirmAck; // mConfirmAck status + QFont mSavedFont; // mFontColourButton font + QColor mSavedBgColour; // mFontColourButton background colour + QColor mSavedFgColour; // mFontColourButton foreground colour + QString mSavedPreAction; // mSpecialActionsButton pre-alarm action + QString mSavedPostAction; // mSpecialActionsButton post-alarm action + int mSavedReminder; // mReminder value + bool mSavedOnceOnly; // mReminder once-only status + QString mSavedTextFileCommandMessage; // mTextMessageEdit/mFileMessageEdit/mCmdCommandEdit/mEmailMessageEdit value + QString mSavedEmailFrom; // mEmailFromList current value + QString mSavedEmailTo; // mEmailToEdit value + QString mSavedEmailSubject; // mEmailSubjectEdit value + QStringList mSavedEmailAttach; // mEmailAttachList values + bool mSavedEmailBcc; // mEmailBcc status + bool mSavedCmdScript; // mCmdTypeScript status + QButton* mSavedCmdOutputRadio; // selected button in mCmdOutputGroup + QString mSavedCmdLogFile; // mCmdLogFileEdit value + DateTime mSavedDateTime; // mTimeWidget value + int mSavedRecurrenceType; // RecurrenceEdit::RepeatType value + int mSavedLateCancel; // mLateCancel value + bool mSavedAutoClose; // mLateCancel->isAutoClose() value + bool mSavedShowInKorganizer; // mShowInKorganizer status +}; + +#endif // EDITDLG_H diff --git a/kalarm/editdlgprivate.h b/kalarm/editdlgprivate.h new file mode 100644 index 000000000..cbcddb62f --- /dev/null +++ b/kalarm/editdlgprivate.h @@ -0,0 +1,47 @@ +/* + * editdlgprivate.h - private classes for editdlg.cpp + * Program: kalarm + * Copyright © 2003-2005,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef EDITDLGPRIVATE_H +#define EDITDLGPRIVATE_H + +#include <ktextedit.h> + + +class PageFrame : public QFrame +{ + Q_OBJECT + public: + PageFrame(QWidget* parent = 0, const char* name = 0) : QFrame(parent, name) { } + protected: + virtual void showEvent(QShowEvent*) { emit shown(); } + signals: + void shown(); +}; + +class TextEdit : public KTextEdit +{ + Q_OBJECT + public: + TextEdit(QWidget* parent, const char* name = 0); + protected: + virtual void dragEnterEvent(QDragEnterEvent*); +}; + +#endif // EDITDLGPRIVATE_H diff --git a/kalarm/emailidcombo.cpp b/kalarm/emailidcombo.cpp new file mode 100644 index 000000000..fc4fc6e74 --- /dev/null +++ b/kalarm/emailidcombo.cpp @@ -0,0 +1,62 @@ +/* + * emailidcombo.cpp - email identity combo box with read-only option + * Program: kalarm + * Copyright (C) 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "emailidcombo.moc" + + +EmailIdCombo::EmailIdCombo(KPIM::IdentityManager* manager, QWidget* parent, const char* name) + : KPIM::IdentityCombo(manager, parent, name), + mReadOnly(false) +{ } + +void EmailIdCombo::mousePressEvent(QMouseEvent* e) +{ + if (mReadOnly) + { + // Swallow up the event if it's the left button + if (e->button() == LeftButton) + return; + } + KPIM::IdentityCombo::mousePressEvent(e); +} + +void EmailIdCombo::mouseReleaseEvent(QMouseEvent* e) +{ + if (!mReadOnly) + KPIM::IdentityCombo::mouseReleaseEvent(e); +} + +void EmailIdCombo::mouseMoveEvent(QMouseEvent* e) +{ + if (!mReadOnly) + KPIM::IdentityCombo::mouseMoveEvent(e); +} + +void EmailIdCombo::keyPressEvent(QKeyEvent* e) +{ + if (!mReadOnly || e->key() == Qt::Key_Escape) + KPIM::IdentityCombo::keyPressEvent(e); +} + +void EmailIdCombo::keyReleaseEvent(QKeyEvent* e) +{ + if (!mReadOnly) + KPIM::IdentityCombo::keyReleaseEvent(e); +} diff --git a/kalarm/emailidcombo.h b/kalarm/emailidcombo.h new file mode 100644 index 000000000..d0399ec08 --- /dev/null +++ b/kalarm/emailidcombo.h @@ -0,0 +1,44 @@ +/* + * emailidcombo.h - email identity combo box with read-only option + * Program: kalarm + * Copyright © 2004,2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef EMAILIDCOMBO_H +#define EMAILIDCOMBO_H + +#include <libkpimidentities/identitycombo.h> +#include "combobox.h" + + +class EmailIdCombo : public KPIM::IdentityCombo +{ + Q_OBJECT + public: + explicit EmailIdCombo(KPIM::IdentityManager*, QWidget* parent = 0, const char* name = 0); + void setReadOnly(bool ro) { mReadOnly = ro; } + protected: + virtual void mousePressEvent(QMouseEvent*); + virtual void mouseReleaseEvent(QMouseEvent*); + virtual void mouseMoveEvent(QMouseEvent*); + virtual void keyPressEvent(QKeyEvent*); + virtual void keyReleaseEvent(QKeyEvent*); + private: + bool mReadOnly; // value cannot be changed +}; + +#endif // EMAILIDCOMBO_H diff --git a/kalarm/eventlistviewbase.cpp b/kalarm/eventlistviewbase.cpp new file mode 100644 index 000000000..1a25e2ac0 --- /dev/null +++ b/kalarm/eventlistviewbase.cpp @@ -0,0 +1,466 @@ +/* + * eventlistviewbase.cpp - base classes for widget showing list of events + * Program: kalarm + * Copyright (c) 2004-2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qwhatsthis.h> +#include <qheader.h> + +#include <kiconloader.h> +#include <kdebug.h> + +#include "find.h" +#include "eventlistviewbase.moc" + + +class EventListWhatsThisBase : public QWhatsThis +{ + public: + EventListWhatsThisBase(EventListViewBase* lv) : QWhatsThis(lv), mListView(lv) { } + virtual QString text(const QPoint&); + private: + EventListViewBase* mListView; +}; + + +/*============================================================================= += Class: EventListViewBase += Base class for displaying a list of events. +=============================================================================*/ + +EventListViewBase::EventListViewBase(QWidget* parent, const char* name) + : KListView(parent, name), + mFind(0), + mLastColumn(-1), + mLastColumnHeaderWidth(0) +{ + setAllColumnsShowFocus(true); + setShowSortIndicator(true); + + new EventListWhatsThisBase(this); +} + +void EventListViewBase::addLastColumn(const QString& title) +{ + addColumn(title); + mLastColumn = columns() - 1; + mLastColumnHeaderWidth = columnWidth(mLastColumn); + setColumnWidthMode(mLastColumn, QListView::Maximum); +} + +/****************************************************************************** +* Refresh the list by clearing it and redisplaying all the current alarms. +*/ +void EventListViewBase::refresh() +{ + QString currentID; + if (currentItem()) + currentID = currentItem()->event().id(); // save current item for restoration afterwards + clear(); + populate(); + resizeLastColumn(); + EventListViewItemBase* current = getEntry(currentID); + if (current) + { + setCurrentItem(current); + ensureItemVisible(current); + } +} + +/****************************************************************************** +* Get the item for a given event ID. +*/ +EventListViewItemBase* EventListViewBase::getEntry(const QString& eventID) const +{ + if (!eventID.isEmpty()) + { + for (EventListViewItemBase* item = firstChild(); item; item = item->nextSibling()) + if (item->event().id() == eventID) + return item; + } + return 0; +} + +/****************************************************************************** +* Add an event to every list instance. +* If 'selectionView' is non-null, the selection highlight is moved to the new +* event in that listView instance. +*/ +void EventListViewBase::addEvent(const KAEvent& event, const InstanceList& instanceList, EventListViewBase* selectionView) +{ + for (InstanceListConstIterator it = instanceList.begin(); it != instanceList.end(); ++it) + (*it)->addEntry(event, true, (*it == selectionView)); +} + +/****************************************************************************** +* Modify an event in every list instance. +* If 'selectionView' is non-null, the selection highlight is moved to the +* modified event in that listView instance. +*/ +void EventListViewBase::modifyEvent(const QString& oldEventID, const KAEvent& newEvent, + const InstanceList& instanceList, EventListViewBase* selectionView) +{ + for (InstanceListConstIterator it = instanceList.begin(); it != instanceList.end(); ++it) + { + EventListViewBase* v = *it; + EventListViewItemBase* item = v->getEntry(oldEventID); + if (item) + v->deleteEntry(item, false); + v->addEntry(newEvent, true, (v == selectionView)); + } +} + +/****************************************************************************** +* Delete an event from every displayed list. +*/ +void EventListViewBase::deleteEvent(const QString& eventID, const InstanceList& instanceList) +{ + for (InstanceListConstIterator it = instanceList.begin(); it != instanceList.end(); ++it) + { + EventListViewBase* v = *it; + EventListViewItemBase* item = v->getEntry(eventID); + if (item) + v->deleteEntry(item, true); + else + v->refresh(); + } +} + +/****************************************************************************** +* Add a new item to the list. +* If 'reselect' is true, select/highlight the new item. +*/ +EventListViewItemBase* EventListViewBase::addEntry(const KAEvent& event, bool setSize, bool reselect) +{ + if (!shouldShowEvent(event)) + return 0; + return addEntry(createItem(event), setSize, reselect); +} + +EventListViewItemBase* EventListViewBase::addEntry(EventListViewItemBase* item, bool setSize, bool reselect) +{ + if (setSize) + resizeLastColumn(); + if (reselect) + { + clearSelection(); + setSelected(item, true); + } + return item; +} + +/****************************************************************************** +* Update a specified item in the list. +* If 'reselect' is true, select the updated item. +*/ +EventListViewItemBase* EventListViewBase::updateEntry(EventListViewItemBase* item, const KAEvent& newEvent, bool setSize, bool reselect) +{ + deleteEntry(item); + return addEntry(newEvent, setSize, reselect); +} + +/****************************************************************************** +* Delete a specified item from the list. +*/ +void EventListViewBase::deleteEntry(EventListViewItemBase* item, bool setSize) +{ + if (item) + { + delete item; + if (setSize) + resizeLastColumn(); + emit itemDeleted(); + } +} + +/****************************************************************************** +* Called when the Find action is selected. +* Display the non-modal Find dialog. +*/ +void EventListViewBase::slotFind() +{ + if (!mFind) + { + mFind = new Find(this); + connect(mFind, SIGNAL(active(bool)), SIGNAL(findActive(bool))); + } + mFind->display(); +} + +/****************************************************************************** +* Called when the Find Next or Find Prev action is selected. +*/ +void EventListViewBase::findNext(bool forward) +{ + if (mFind) + mFind->findNext(forward); +} + +/****************************************************************************** +* Called when the Select All action is selected. +* Select all items in the list. +*/ +void EventListViewBase::slotSelectAll() +{ + if (selectionMode() == QListView::Multi || selectionMode() == QListView::Extended) + selectAll(true); +} + +/****************************************************************************** +* Called when the Deselect action is selected. +* Deselect all items in the list. +*/ +void EventListViewBase::slotDeselect() +{ + selectAll(false); +} + +/****************************************************************************** +* Check whether there are any selected items. +*/ +bool EventListViewBase::anySelected() const +{ + for (QListViewItem* item = KListView::firstChild(); item; item = item->nextSibling()) + if (isSelected(item)) + return true; + return false; +} + +/****************************************************************************** +* Get the single selected event. +* Reply = the event +* = 0 if no event is selected or multiple events are selected. +*/ +const KAEvent* EventListViewBase::selectedEvent() const +{ + EventListViewItemBase* sel = selectedItem(); + return sel ? &sel->event() : 0; +} + +/****************************************************************************** +* Fetch the single selected item. +* This method works in both Single and Multi selection mode, unlike +* QListView::selectedItem(). +* Reply = null if no items are selected, or if multiple items are selected. +*/ +EventListViewItemBase* EventListViewBase::selectedItem() const +{ + if (selectionMode() == QListView::Single) + return (EventListViewItemBase*)KListView::selectedItem(); + + QListViewItem* item = 0; + for (QListViewItem* it = firstChild(); it; it = it->nextSibling()) + { + if (isSelected(it)) + { + if (item) + return 0; + item = it; + } + } + return (EventListViewItemBase*)item; +} + +/****************************************************************************** +* Fetch all selected items. +*/ +QValueList<EventListViewItemBase*> EventListViewBase::selectedItems() const +{ + QValueList<EventListViewItemBase*> items; + for (QListViewItem* item = firstChild(); item; item = item->nextSibling()) + { + if (isSelected(item)) + items.append((EventListViewItemBase*)item); + } + return items; +} + +/****************************************************************************** +* Return how many items are selected. +*/ +int EventListViewBase::selectedCount() const +{ + int count = 0; + for (QListViewItem* item = firstChild(); item; item = item->nextSibling()) + { + if (isSelected(item)) + ++count; + } + return count; +} + +/****************************************************************************** +* Sets the last column in the list view to extend at least to the right hand +* edge of the list view. +*/ +void EventListViewBase::resizeLastColumn() +{ + int lastColumnWidth = mLastColumnHeaderWidth; + for (EventListViewItemBase* item = firstChild(); item; item = item->nextSibling()) + { + int mw = item->lastColumnWidth(); + if (mw > lastColumnWidth) + lastColumnWidth = mw; + } + QHeader* head = header(); + int x = head->sectionPos(mLastColumn); + int availableWidth = visibleWidth() - x; + int rightColWidth = 0; + int index = head->mapToIndex(mLastColumn); + if (index < mLastColumn) + { + // The last column has been dragged by the user to a different position. + // Ensure that the columns now to the right of it are still shown. + for (int i = index + 1; i <= mLastColumn; ++i) + rightColWidth += columnWidth(head->mapToSection(i)); + availableWidth -= rightColWidth; + } + if (availableWidth < lastColumnWidth) + availableWidth = lastColumnWidth; + setColumnWidth(mLastColumn, availableWidth); + if (contentsWidth() > x + availableWidth + rightColWidth) + resizeContents(x + availableWidth + rightColWidth, contentsHeight()); +} + +/****************************************************************************** +* Called when the widget's size has changed (before it is painted). +* Sets the last column in the list view to extend at least to the right hand +* edge of the list view. +*/ +void EventListViewBase::resizeEvent(QResizeEvent* re) +{ + KListView::resizeEvent(re); + resizeLastColumn(); +} + +/****************************************************************************** +* Called when the widget is first displayed. +* Sets the last column in the list view to extend at least to the right hand +* edge of the list view. +*/ +void EventListViewBase::showEvent(QShowEvent* se) +{ + KListView::showEvent(se); + resizeLastColumn(); +} + +/****************************************************************************** +* Find the height of one list item. +*/ +int EventListViewBase::itemHeight() +{ + EventListViewItemBase* item = firstChild(); + if (!item) + { + // The list is empty, so create a temporary item to find its height + QListViewItem* item = new QListViewItem(this, QString::null); + int height = item->height(); + delete item; + return height; + } + else + return item->height(); +} + + +/*============================================================================= += Class: EventListViewItemBase += Base class containing the details of one event for display in an +* EventListViewBase. +=============================================================================*/ +QPixmap* EventListViewItemBase::mTextIcon; +QPixmap* EventListViewItemBase::mFileIcon; +QPixmap* EventListViewItemBase::mCommandIcon; +QPixmap* EventListViewItemBase::mEmailIcon; +int EventListViewItemBase::mIconWidth = 0; + + +EventListViewItemBase::EventListViewItemBase(EventListViewBase* parent, const KAEvent& event) + : QListViewItem(parent), + mEvent(event) +{ + iconWidth(); // load the icons +} + +/****************************************************************************** +* Set the text for the last column, and find its width. +*/ +void EventListViewItemBase::setLastColumnText() +{ + EventListViewBase* parent = (EventListViewBase*)listView(); + setText(parent->lastColumn(), lastColumnText()); + mLastColumnWidth = width(parent->fontMetrics(), parent, parent->lastColumn()); +} + +/****************************************************************************** +* Return the width of the widest alarm type icon. +*/ +int EventListViewItemBase::iconWidth() +{ + if (!mIconWidth) + { + mTextIcon = new QPixmap(SmallIcon("message")); + mFileIcon = new QPixmap(SmallIcon("file")); + mCommandIcon = new QPixmap(SmallIcon("exec")); + mEmailIcon = new QPixmap(SmallIcon("mail_generic")); + if (mTextIcon) + mIconWidth = mTextIcon->width(); + if (mFileIcon && mFileIcon->width() > mIconWidth) + mIconWidth = mFileIcon->width(); + if (mCommandIcon && mCommandIcon->width() > mIconWidth) + mIconWidth = mCommandIcon->width(); + if (mEmailIcon && mEmailIcon->width() > mIconWidth) + mIconWidth = mEmailIcon->width(); + } + return mIconWidth; +} + +/****************************************************************************** +* Return the icon associated with the event's action. +*/ +QPixmap* EventListViewItemBase::eventIcon() const +{ + switch (mEvent.action()) + { + case KAAlarm::FILE: return mFileIcon; + case KAAlarm::COMMAND: return mCommandIcon; + case KAAlarm::EMAIL: return mEmailIcon; + case KAAlarm::MESSAGE: + default: return mTextIcon; + } +} + + +/*============================================================================= += Class: EventListWhatsThisBase += Sets What's This? text depending on where in the list view is clicked. +=============================================================================*/ + +QString EventListWhatsThisBase::text(const QPoint& pt) +{ + int column = -1; + QPoint viewportPt = mListView->viewport()->mapFrom(mListView, pt); + QRect frame = mListView->header()->frameGeometry(); + if (frame.contains(pt) + || mListView->itemAt(QPoint(mListView->itemMargin(), viewportPt.y())) && frame.contains(QPoint(pt.x(), frame.y()))) + column = mListView->header()->sectionAt(pt.x()); + return mListView->whatsThisText(column); +} + diff --git a/kalarm/eventlistviewbase.h b/kalarm/eventlistviewbase.h new file mode 100644 index 000000000..23831c71d --- /dev/null +++ b/kalarm/eventlistviewbase.h @@ -0,0 +1,132 @@ +/* + * eventlistviewbase.h - base classes for widget showing list of events + * Program: kalarm + * Copyright (c) 2004-2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef EVENTLISTVIEWBASE_H +#define EVENTLISTVIEWBASE_H + +#include "kalarm.h" + +#include <qvaluelist.h> +#include <klistview.h> + +#include "alarmevent.h" + +class QPixmap; +class EventListViewItemBase; +class Find; + + +class EventListViewBase : public KListView +{ + Q_OBJECT + public: + typedef QValueList<EventListViewBase*> InstanceList; + typedef QValueListIterator<EventListViewBase*> InstanceListIterator; + typedef QValueListConstIterator<EventListViewBase*> InstanceListConstIterator; + + EventListViewBase(QWidget* parent = 0, const char* name = 0); + virtual ~EventListViewBase() { } + EventListViewItemBase* getEntry(const QString& eventID) const; + void addEvent(const KAEvent& e) { addEvent(e, instances(), this); } + void modifyEvent(const KAEvent& e) + { modifyEvent(e.id(), e, instances(), this); } + void modifyEvent(const QString& oldEventID, const KAEvent& newEvent) + { modifyEvent(oldEventID, newEvent, instances(), this); } + void deleteEvent(const QString& eventID) { deleteEvent(eventID, instances()); } + static void addEvent(const KAEvent&, const InstanceList&, EventListViewBase* selectionView); + static void modifyEvent(const KAEvent& e, const InstanceList& list, EventListViewBase* selectionView) + { modifyEvent(e.id(), e, list, selectionView); } + static void modifyEvent(const QString& oldEventID, const KAEvent& newEvent, const InstanceList&, EventListViewBase* selectionView); + static void deleteEvent(const QString& eventID, const InstanceList&); + static void undeleteEvent(const QString& oldEventID, const KAEvent& event, const InstanceList& list, EventListViewBase* selectionView) + { modifyEvent(oldEventID, event, list, selectionView); } + void resizeLastColumn(); + int itemHeight(); + EventListViewItemBase* currentItem() const { return (EventListViewItemBase*)KListView::currentItem(); } + EventListViewItemBase* firstChild() const { return (EventListViewItemBase*)KListView::firstChild(); } + bool anySelected() const; // are any items selected? + const KAEvent* selectedEvent() const; + EventListViewItemBase* selectedItem() const; + QValueList<EventListViewItemBase*> selectedItems() const; + int selectedCount() const; + int lastColumn() const { return mLastColumn; } + virtual QString whatsThisText(int column) const = 0; + virtual InstanceList instances() = 0; // return all instances + + public slots: + void refresh(); + virtual void slotFind(); + virtual void slotFindNext() { findNext(true); } + virtual void slotFindPrev() { findNext(false); } + virtual void slotSelectAll(); + virtual void slotDeselect(); + + signals: + void itemDeleted(); + void findActive(bool); + + protected: + virtual void populate() = 0; // populate the list with all desired events + virtual EventListViewItemBase* createItem(const KAEvent&) = 0; // only used by default addEntry() method + virtual bool shouldShowEvent(const KAEvent&) const { return true; } + EventListViewItemBase* addEntry(const KAEvent&, bool setSize = false, bool reselect = false); + EventListViewItemBase* addEntry(EventListViewItemBase*, bool setSize, bool reselect); + EventListViewItemBase* updateEntry(EventListViewItemBase*, const KAEvent& newEvent, bool setSize = false, bool reselect = false); + void addLastColumn(const QString& title); + virtual void showEvent(QShowEvent*); + virtual void resizeEvent(QResizeEvent*); + + private: + void deleteEntry(EventListViewItemBase*, bool setSize = false); + void findNext(bool forward); + + Find* mFind; // alarm search object + int mLastColumn; // index to last column + int mLastColumnHeaderWidth; +}; + + +class EventListViewItemBase : public QListViewItem +{ + public: + EventListViewItemBase(EventListViewBase* parent, const KAEvent&); + const KAEvent& event() const { return mEvent; } + QPixmap* eventIcon() const; + int lastColumnWidth() const { return mLastColumnWidth; } + EventListViewItemBase* nextSibling() const { return (EventListViewItemBase*)QListViewItem::nextSibling(); } + static int iconWidth(); + + protected: + void setLastColumnText(); + virtual QString lastColumnText() const = 0; // get the text to display in the last column + + private: + static QPixmap* mTextIcon; + static QPixmap* mFileIcon; + static QPixmap* mCommandIcon; + static QPixmap* mEmailIcon; + static int mIconWidth; // maximum width of any icon + + KAEvent mEvent; // the event for this item + int mLastColumnWidth; // width required to display message column +}; + +#endif // EVENTLISTVIEWBASE_H + diff --git a/kalarm/find.cpp b/kalarm/find.cpp new file mode 100644 index 000000000..7caf4bde0 --- /dev/null +++ b/kalarm/find.cpp @@ -0,0 +1,394 @@ +/* + * find.cpp - search facility + * Program: kalarm + * Copyright © 2005,2006,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qlayout.h> +#include <qwhatsthis.h> +#include <qgroupbox.h> +#include <qcheckbox.h> + +#include <kfinddialog.h> +#include <kfind.h> +#include <kseparator.h> +#include <kwin.h> +#include <klocale.h> +#include <kmessagebox.h> +#include <kdebug.h> + +#include "alarmlistview.h" +#include "preferences.h" +#include "find.moc" + +// KAlarm-specific options for Find dialog +enum { + FIND_LIVE = KFindDialog::MinimumUserOption, + FIND_EXPIRED = KFindDialog::MinimumUserOption << 1, + FIND_MESSAGE = KFindDialog::MinimumUserOption << 2, + FIND_FILE = KFindDialog::MinimumUserOption << 3, + FIND_COMMAND = KFindDialog::MinimumUserOption << 4, + FIND_EMAIL = KFindDialog::MinimumUserOption << 5 +}; +static long FIND_KALARM_OPTIONS = FIND_LIVE | FIND_EXPIRED | FIND_MESSAGE | FIND_FILE | FIND_COMMAND | FIND_EMAIL; + + +Find::Find(EventListViewBase* parent) + : QObject(parent), + mListView(parent), + mDialog(0), + mFind(0), + mOptions(0) +{ +} + +Find::~Find() +{ + delete mDialog; // automatically set to 0 + delete mFind; + mFind = 0; +} + +/****************************************************************************** +* Display the Find dialog. +*/ +void Find::display() +{ + if (!mOptions) + // Set defaults the first time the Find dialog is activated + mOptions = FIND_LIVE | FIND_EXPIRED | FIND_MESSAGE | FIND_FILE | FIND_COMMAND | FIND_EMAIL; + bool noExpired = !Preferences::expiredKeepDays(); + bool showExpired = mListView->isA("AlarmListView") && ((AlarmListView*)mListView)->showingExpired(); + if (noExpired || !showExpired) // these settings could change between activations + mOptions &= ~FIND_EXPIRED; + + if (mDialog) + { + KWin::activateWindow(mDialog->winId()); + } + else + { +#ifdef MODAL_FIND + mDialog = new KFindDialog(mListView, "FindDlg", mOptions, mHistory, (mListView->selectedCount() > 1)); +#else + mDialog = new KFindDialog(false, mListView, "FindDlg", mOptions, mHistory, (mListView->selectedCount() > 1)); +#endif + mDialog->setHasSelection(false); + QWidget* kalarmWidgets = mDialog->findExtension(); + + // Alarm types + QBoxLayout* layout = new QVBoxLayout(kalarmWidgets, 0, KDialog::spacingHint()); + QGroupBox* group = new QGroupBox(i18n("Alarm Type"), kalarmWidgets); + layout->addWidget(group); + QGridLayout* grid = new QGridLayout(group, 2, 2, KDialog::marginHint(), KDialog::spacingHint()); + grid->addRowSpacing(0, mDialog->fontMetrics().lineSpacing()/2); + grid->setColStretch(1, 1); + + // Live & expired alarm selection + mLive = new QCheckBox(i18n("Acti&ve"), group); + mLive->setFixedSize(mLive->sizeHint()); + QWhatsThis::add(mLive, i18n("Check to include active alarms in the search.")); + grid->addWidget(mLive, 1, 0, Qt::AlignAuto); + + mExpired = new QCheckBox(i18n("Ex&pired"), group); + mExpired->setFixedSize(mExpired->sizeHint()); + QWhatsThis::add(mExpired, + i18n("Check to include expired alarms in the search. " + "This option is only available if expired alarms are currently being displayed.")); + grid->addWidget(mExpired, 1, 2, Qt::AlignAuto); + + mActiveExpiredSep = new KSeparator(Qt::Horizontal, kalarmWidgets); + grid->addMultiCellWidget(mActiveExpiredSep, 2, 2, 0, 2); + + // Alarm actions + mMessageType = new QCheckBox(i18n("Text"), group, "message"); + mMessageType->setFixedSize(mMessageType->sizeHint()); + QWhatsThis::add(mMessageType, i18n("Check to include text message alarms in the search.")); + grid->addWidget(mMessageType, 3, 0); + + mFileType = new QCheckBox(i18n("Fi&le"), group, "file"); + mFileType->setFixedSize(mFileType->sizeHint()); + QWhatsThis::add(mFileType, i18n("Check to include file alarms in the search.")); + grid->addWidget(mFileType, 3, 2); + + mCommandType = new QCheckBox(i18n("Co&mmand"), group, "command"); + mCommandType->setFixedSize(mCommandType->sizeHint()); + QWhatsThis::add(mCommandType, i18n("Check to include command alarms in the search.")); + grid->addWidget(mCommandType, 4, 0); + + mEmailType = new QCheckBox(i18n("&Email"), group, "email"); + mEmailType->setFixedSize(mEmailType->sizeHint()); + QWhatsThis::add(mEmailType, i18n("Check to include email alarms in the search.")); + grid->addWidget(mEmailType, 4, 2); + + // Set defaults + mLive->setChecked(mOptions & FIND_LIVE); + mExpired->setChecked(mOptions & FIND_EXPIRED); + mMessageType->setChecked(mOptions & FIND_MESSAGE); + mFileType->setChecked(mOptions & FIND_FILE); + mCommandType->setChecked(mOptions & FIND_COMMAND); + mEmailType->setChecked(mOptions & FIND_EMAIL); + +#ifndef MODAL_FIND + connect(mDialog, SIGNAL(okClicked()), this, SLOT(slotFind())); +#endif + } + + // Only display active/expired options if expired alarms are being kept + if (noExpired) + { + mLive->hide(); + mExpired->hide(); + mActiveExpiredSep->hide(); + } + else + { + mLive->show(); + mExpired->show(); + mActiveExpiredSep->show(); + } + + // Disable options where no displayed alarms match them + bool live = false; + bool expired = false; + bool text = false; + bool file = false; + bool command = false; + bool email = false; + for (EventListViewItemBase* item = mListView->firstChild(); item; item = item->nextSibling()) + { + const KAEvent& event = item->event(); + if (event.expired()) + expired = true; + else + live = true; + switch (event.action()) + { + case KAEvent::MESSAGE: text = true; break; + case KAEvent::FILE: file = true; break; + case KAEvent::COMMAND: command = true; break; + case KAEvent::EMAIL: email = true; break; + } + } + mLive->setEnabled(live); + mExpired->setEnabled(expired); + mMessageType->setEnabled(text); + mFileType->setEnabled(file); + mCommandType->setEnabled(command); + mEmailType->setEnabled(email); + + mDialog->setHasCursor(mListView->currentItem()); +#ifdef MODAL_FIND + if (mDialog->exec() == QDialog::Accepted) + slotFind(); + else + delete mDialog; +#else + mDialog->show(); +#endif +} + +/****************************************************************************** +* Called when the user requests a search by clicking the dialog OK button. +*/ +void Find::slotFind() +{ + if (!mDialog) + return; + mHistory = mDialog->findHistory(); // save search history so that it can be displayed again + mOptions = mDialog->options() & ~FIND_KALARM_OPTIONS; + mOptions |= (mLive->isEnabled() && mLive->isChecked() ? FIND_LIVE : 0) + | (mExpired->isEnabled() && mExpired->isChecked() ? FIND_EXPIRED : 0) + | (mMessageType->isEnabled() && mMessageType->isChecked() ? FIND_MESSAGE : 0) + | (mFileType->isEnabled() && mFileType->isChecked() ? FIND_FILE : 0) + | (mCommandType->isEnabled() && mCommandType->isChecked() ? FIND_COMMAND : 0) + | (mEmailType->isEnabled() && mEmailType->isChecked() ? FIND_EMAIL : 0); + if (!(mOptions & (FIND_LIVE | FIND_EXPIRED)) + || !(mOptions & (FIND_MESSAGE | FIND_FILE | FIND_COMMAND | FIND_EMAIL))) + { + KMessageBox::sorry(mDialog, i18n("No alarm types are selected to search")); + return; + } + + // Supply KFind with only those options which relate to the text within alarms + long options = mOptions & (KFindDialog::WholeWordsOnly | KFindDialog::CaseSensitive | KFindDialog::RegularExpression); + bool newFind = !mFind; + bool newPattern = (mDialog->pattern() != mLastPattern); + mLastPattern = mDialog->pattern(); + if (mFind) + { + mFind->resetCounts(); + mFind->setPattern(mLastPattern); + mFind->setOptions(options); + } + else + { +#ifdef MODAL_FIND + mFind = new KFind(mLastPattern, options, mListView); + mDialog->deleteLater(); // automatically set to 0 +#else + mFind = new KFind(mLastPattern, options, mListView, mDialog); +#endif + connect(mFind, SIGNAL(destroyed()), SLOT(slotKFindDestroyed())); + mFind->closeFindNextDialog(); // prevent 'Find Next' dialog appearing + } + + // Set the starting point for the search + mStartID = QString::null; + mNoCurrentItem = newPattern; + bool checkEnd = false; + if (newPattern) + { + mFound = false; + if (mOptions & KFindDialog::FromCursor) + { + EventListViewItemBase* item = mListView->currentItem(); + if (item) + { + mStartID = item->event().id(); + mNoCurrentItem = false; + checkEnd = true; + } + } + } + + // Execute the search + findNext(true, true, checkEnd, false); + if (mFind && newFind) + emit active(true); +} + +/****************************************************************************** +* Perform the search. +* If 'fromCurrent' is true, the search starts with the current search item; +* otherwise, it starts from the next item. +*/ +void Find::findNext(bool forward, bool sort, bool checkEnd, bool fromCurrent) +{ + if (sort) + mListView->sort(); // ensure the whole list is sorted, not just the visible items + + EventListViewItemBase* item = mNoCurrentItem ? 0 : mListView->currentItem(); + if (!fromCurrent) + item = nextItem(item, forward); + + // Search successive alarms until a match is found or the end is reached + bool found = false; + bool last = false; + for ( ; item && !last; item = nextItem(item, forward)) + { + const KAEvent& event = item->event(); + if (!fromCurrent && !mStartID.isNull() && mStartID == event.id()) + last = true; // we've wrapped round and reached the starting alarm again + fromCurrent = false; + bool live = !event.expired(); + if (live && !(mOptions & FIND_LIVE) + || !live && !(mOptions & FIND_EXPIRED)) + continue; // we're not searching this type of alarm + switch (event.action()) + { + case KAEvent::MESSAGE: + if (!(mOptions & FIND_MESSAGE)) + break; + mFind->setData(event.cleanText()); + found = (mFind->find() == KFind::Match); + break; + + case KAEvent::FILE: + if (!(mOptions & FIND_FILE)) + break; + mFind->setData(event.cleanText()); + found = (mFind->find() == KFind::Match); + break; + + case KAEvent::COMMAND: + if (!(mOptions & FIND_COMMAND)) + break; + mFind->setData(event.cleanText()); + found = (mFind->find() == KFind::Match); + break; + + case KAEvent::EMAIL: + if (!(mOptions & FIND_EMAIL)) + break; + mFind->setData(event.emailAddresses(", ")); + found = (mFind->find() == KFind::Match); + if (found) + break; + mFind->setData(event.emailSubject()); + found = (mFind->find() == KFind::Match); + if (found) + break; + mFind->setData(event.emailAttachments().join(", ")); + found = (mFind->find() == KFind::Match); + if (found) + break; + mFind->setData(event.cleanText()); + found = (mFind->find() == KFind::Match); + break; + } + if (found) + break; + } + + // Process the search result + mNoCurrentItem = !item; + if (found) + { + // A matching alarm was found - highlight it and make it current + mFound = true; + mListView->clearSelection(); + mListView->setSelected(item, true); + mListView->setCurrentItem(item); + mListView->ensureItemVisible(item); + } + else + { + // No match was found + if (mFound || checkEnd) + { + QString msg = forward ? i18n("End of alarm list reached.\nContinue from the beginning?") + : i18n("Beginning of alarm list reached.\nContinue from the end?"); + if (KMessageBox::questionYesNo(mListView, msg, QString::null, KStdGuiItem::cont(), KStdGuiItem::cancel()) == KMessageBox::Yes) + { + mNoCurrentItem = true; + findNext(forward, false); + return; + } + } + else + mFind->displayFinalDialog(); // display "no match was found" + mNoCurrentItem = false; // restart from the currently highlighted alarm if Find Next etc selected + } +} + +/****************************************************************************** +* Get the next alarm item to search. +*/ +EventListViewItemBase* Find::nextItem(EventListViewItemBase* item, bool forward) const +{ + QListViewItem* it; + if (mOptions & KFindDialog::FindBackwards) + forward = !forward; + if (forward) + it = item ? item->itemBelow() : mListView->firstChild(); + else + it = item ? item->itemAbove() : mListView->lastItem(); + return (EventListViewItemBase*)it; +} diff --git a/kalarm/find.h b/kalarm/find.h new file mode 100644 index 000000000..1bb4913b1 --- /dev/null +++ b/kalarm/find.h @@ -0,0 +1,75 @@ +/* + * find.h - search facility + * Program: kalarm + * Copyright © 2005,2006,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef FIND_H +#define FIND_H + +#include <qobject.h> +#include <qguardedptr.h> +#include <qstringlist.h> + +class QCheckBox; +class KFindDialog; +class KFind; +class KSeparator; +class EventListViewBase; +class EventListViewItemBase; + + +class Find : public QObject +{ + Q_OBJECT + public: + explicit Find(EventListViewBase* parent); + ~Find(); + void display(); + void findNext(bool forward) { findNext(forward, true); } + + signals: + void active(bool); + + private slots: + void slotFind(); + void slotKFindDestroyed() { emit active(false); } + + private: + void findNext(bool forward, bool sort, bool checkEnd = false, bool fromCurrent = false); + EventListViewItemBase* nextItem(EventListViewItemBase*, bool forward) const; + + EventListViewBase* mListView; // parent list view + QGuardedPtr<KFindDialog> mDialog; + QCheckBox* mExpired; + QCheckBox* mLive; + KSeparator* mActiveExpiredSep; + QCheckBox* mMessageType; + QCheckBox* mFileType; + QCheckBox* mCommandType; + QCheckBox* mEmailType; + KFind* mFind; + QStringList mHistory; // list of history items for Find dialog + QString mLastPattern; // pattern used in last search + QString mStartID; // ID of first alarm searched if 'from cursor' was selected + long mOptions; // OR of find dialog options + bool mNoCurrentItem; // there is no current item for the purposes of searching + bool mFound; // true if any matches have been found +}; + +#endif // FIND_H + diff --git a/kalarm/fontcolour.cpp b/kalarm/fontcolour.cpp new file mode 100644 index 000000000..dae2e9856 --- /dev/null +++ b/kalarm/fontcolour.cpp @@ -0,0 +1,265 @@ +/* + * fontcolour.cpp - font and colour chooser widget + * Program: kalarm + * Copyright © 2001-2003,2005,2008 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include <qobjectlist.h> +#include <qwidget.h> +#include <qgroupbox.h> +#include <qpushbutton.h> +#include <qhbox.h> +#include <qlabel.h> +#include <qlayout.h> +#include <qwhatsthis.h> + +#include <kglobal.h> +#include <klocale.h> +#include <kcolordialog.h> + +#include "kalarmapp.h" +#include "preferences.h" +#include "colourcombo.h" +#include "checkbox.h" +#include "fontcolour.moc" + + +FontColourChooser::FontColourChooser(QWidget *parent, const char *name, + bool onlyFixed, const QStringList &fontList, + const QString& frameLabel, bool editColours, bool fg, bool defaultFont, + int visibleListSize) + : QWidget(parent, name), + mFgColourButton(0), + mRemoveColourButton(0), + mColourList(Preferences::messageColours()), + mReadOnly(false) +{ + QVBoxLayout* topLayout = new QVBoxLayout(this, 0, KDialog::spacingHint()); + QWidget* page = this; + if (!frameLabel.isNull()) + { + page = new QGroupBox(frameLabel, this); + topLayout->addWidget(page); + topLayout = new QVBoxLayout(page, KDialog::marginHint(), KDialog::spacingHint()); + topLayout->addSpacing(fontMetrics().height() - KDialog::marginHint() + KDialog::spacingHint()); + } + QHBoxLayout* hlayout = new QHBoxLayout(topLayout); + QVBoxLayout* colourLayout = new QVBoxLayout(hlayout); + if (fg) + { + QHBox* box = new QHBox(page); // to group widgets for QWhatsThis text + box->setSpacing(KDialog::spacingHint()/2); + colourLayout->addWidget(box); + + QLabel* label = new QLabel(i18n("&Foreground color:"), box); + box->setStretchFactor(new QWidget(box), 0); + mFgColourButton = new ColourCombo(box); + connect(mFgColourButton, SIGNAL(activated(const QString&)), SLOT(setSampleColour())); + label->setBuddy(mFgColourButton); + QWhatsThis::add(box, i18n("Select the alarm message foreground color")); + } + + QHBox* box = new QHBox(page); // to group widgets for QWhatsThis text + box->setSpacing(KDialog::spacingHint()/2); + colourLayout->addWidget(box); + + QLabel* label = new QLabel(i18n("&Background color:"), box); + box->setStretchFactor(new QWidget(box), 0); + mBgColourButton = new ColourCombo(box); + connect(mBgColourButton, SIGNAL(activated(const QString&)), SLOT(setSampleColour())); + label->setBuddy(mBgColourButton); + QWhatsThis::add(box, i18n("Select the alarm message background color")); + hlayout->addStretch(); + + if (editColours) + { + QHBoxLayout* layout = new QHBoxLayout(topLayout); + QPushButton* button = new QPushButton(i18n("Add Co&lor..."), page); + button->setFixedSize(button->sizeHint()); + connect(button, SIGNAL(clicked()), SLOT(slotAddColour())); + QWhatsThis::add(button, i18n("Choose a new color to add to the color selection list.")); + layout->addWidget(button); + + mRemoveColourButton = new QPushButton(i18n("&Remove Color"), page); + mRemoveColourButton->setFixedSize(mRemoveColourButton->sizeHint()); + connect(mRemoveColourButton, SIGNAL(clicked()), SLOT(slotRemoveColour())); + QWhatsThis::add(mRemoveColourButton, + i18n("Remove the color currently shown in the background color chooser, from the color selection list.")); + layout->addWidget(mRemoveColourButton); + } + + if (defaultFont) + { + QHBoxLayout* layout = new QHBoxLayout(topLayout); + mDefaultFont = new CheckBox(i18n("Use &default font"), page); + mDefaultFont->setMinimumSize(mDefaultFont->sizeHint()); + connect(mDefaultFont, SIGNAL(toggled(bool)), SLOT(slotDefaultFontToggled(bool))); + QWhatsThis::add(mDefaultFont, + i18n("Check to use the default font current at the time the alarm is displayed.")); + layout->addWidget(mDefaultFont); + layout->addWidget(new QWidget(page)); // left adjust the widget + } + else + mDefaultFont = 0; + + mFontChooser = new KFontChooser(page, name, onlyFixed, fontList, false, visibleListSize); + mFontChooser->installEventFilter(this); // for read-only mode + const QObjectList* kids = mFontChooser->queryList(); + for (QObjectList::ConstIterator it = kids->constBegin(); it != kids->constEnd(); ++it) + (*it)->installEventFilter(this); + topLayout->addWidget(mFontChooser); + + slotDefaultFontToggled(false); +} + +void FontColourChooser::setDefaultFont() +{ + if (mDefaultFont) + mDefaultFont->setChecked(true); +} + +void FontColourChooser::setFont(const QFont& font, bool onlyFixed) +{ + if (mDefaultFont) + mDefaultFont->setChecked(false); + mFontChooser->setFont(font, onlyFixed); +} + +bool FontColourChooser::defaultFont() const +{ + return mDefaultFont ? mDefaultFont->isChecked() : false; +} + +QFont FontColourChooser::font() const +{ + return (mDefaultFont && mDefaultFont->isChecked()) ? QFont() : mFontChooser->font(); +} + +void FontColourChooser::setBgColour(const QColor& colour) +{ + mBgColourButton->setColor(colour); + mFontChooser->setBackgroundColor(colour); +} + +void FontColourChooser::setSampleColour() +{ + QColor bg = mBgColourButton->color(); + mFontChooser->setBackgroundColor(bg); + QColor fg = fgColour(); + mFontChooser->setColor(fg); + if (mRemoveColourButton) + mRemoveColourButton->setEnabled(!mBgColourButton->isCustomColour()); // no deletion of custom colour +} + +QColor FontColourChooser::bgColour() const +{ + return mBgColourButton->color(); +} + +QColor FontColourChooser::fgColour() const +{ + if (mFgColourButton) + return mFgColourButton->color(); + else + { + QColor bg = mBgColourButton->color(); + QPalette pal(bg, bg); + return pal.color(QPalette::Active, QColorGroup::Text); + } +} + +QString FontColourChooser::sampleText() const +{ + return mFontChooser->sampleText(); +} + +void FontColourChooser::setSampleText(const QString& text) +{ + mFontChooser->setSampleText(text); +} + +void FontColourChooser::setFgColour(const QColor& colour) +{ + if (mFgColourButton) + { + mFgColourButton->setColor(colour); + mFontChooser->setColor(colour); + } +} + +void FontColourChooser::setReadOnly(bool ro) +{ + if (ro != mReadOnly) + { + mReadOnly = ro; + if (mFgColourButton) + mFgColourButton->setReadOnly(ro); + mBgColourButton->setReadOnly(ro); + mDefaultFont->setReadOnly(ro); + } +} + +bool FontColourChooser::eventFilter(QObject*, QEvent* e) +{ + if (mReadOnly) + { + switch (e->type()) + { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + case QEvent::MouseButtonDblClick: + case QEvent::KeyPress: + case QEvent::KeyRelease: + return true; // prevent the event being handled + default: + break; + } + } + return false; +} + +void FontColourChooser::slotDefaultFontToggled(bool on) +{ + mFontChooser->setEnabled(!on); +} + +void FontColourChooser::setColours(const ColourList& colours) +{ + mColourList = colours; + mBgColourButton->setColours(mColourList); + mFontChooser->setBackgroundColor(mBgColourButton->color()); +} + +void FontColourChooser::slotAddColour() +{ + QColor colour; + if (KColorDialog::getColor(colour, this) == QDialog::Accepted) + { + mColourList.insert(colour); + mBgColourButton->setColours(mColourList); + } +} + +void FontColourChooser::slotRemoveColour() +{ + if (!mBgColourButton->isCustomColour()) + { + mColourList.remove(mBgColourButton->color()); + mBgColourButton->setColours(mColourList); + } +} + diff --git a/kalarm/fontcolour.h b/kalarm/fontcolour.h new file mode 100644 index 000000000..1e37b3836 --- /dev/null +++ b/kalarm/fontcolour.h @@ -0,0 +1,78 @@ +/* + * fontcolour.h - font and colour chooser widget + * Program: kalarm + * Copyright © 2001,2003,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef FONTCOLOUR_H +#define FONTCOLOUR_H + +#include <kdeversion.h> +#include <qwidget.h> +#include <qstringlist.h> +#include <kfontdialog.h> +#include "colourlist.h" + +class ColourCombo; +class QPushButton; +class CheckBox; + + +class FontColourChooser : public QWidget +{ + Q_OBJECT +public: + explicit FontColourChooser(QWidget* parent = 0, const char* name = 0, + bool onlyFixed = false, + const QStringList& fontList = QStringList(), + const QString& frameLabel = i18n("Requested font"), + bool editColours = false, bool fg = true, bool defaultFont = false, + int visibleListSize = 8); + + void setDefaultFont(); + void setFont(const QFont&, bool onlyFixed = false); + bool defaultFont() const; + QFont font() const; + QColor fgColour() const; + QColor bgColour() const; + const ColourList& colours() const { return mColourList; } + void setFgColour(const QColor&); + void setBgColour(const QColor&); + void setColours(const ColourList&); + QString sampleText() const; + void setSampleText(const QString& text); + bool isReadOnly() const { return mReadOnly; } + void setReadOnly(bool); + virtual bool eventFilter(QObject*, QEvent*); + +private slots: + void setSampleColour(); + void slotDefaultFontToggled(bool); + void slotAddColour(); + void slotRemoveColour(); + +private: + ColourCombo* mFgColourButton; // or null + ColourCombo* mBgColourButton; + QPushButton* mRemoveColourButton; + KFontChooser* mFontChooser; + CheckBox* mDefaultFont; // or null + ColourList mColourList; + bool mReadOnly; +}; + +#endif diff --git a/kalarm/fontcolourbutton.cpp b/kalarm/fontcolourbutton.cpp new file mode 100644 index 000000000..b3a55359a --- /dev/null +++ b/kalarm/fontcolourbutton.cpp @@ -0,0 +1,161 @@ +/* + * fontcolourbutton.cpp - pushbutton widget to select a font and colour + * Program: kalarm + * Copyright © 2003-2005,2007,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qcheckbox.h> +#include <qlayout.h> +#include <qwhatsthis.h> + +#include <klocale.h> +#include <kdebug.h> + +#include "fontcolour.h" +#include "preferences.h" +#include "pushbutton.h" +#include "fontcolourbutton.moc" + + +/*============================================================================= += Class FontColourButton += Font/colour selection button. +=============================================================================*/ + +FontColourButton::FontColourButton(QWidget* parent, const char* name) + : QFrame(parent, name), + mReadOnly(false) +{ + setFrameStyle(NoFrame); + QHBoxLayout* layout = new QHBoxLayout(this, 0, KDialog::spacingHint()); + + mButton = new PushButton(i18n("Font && Co&lor..."), this); + mButton->setFixedSize(mButton->sizeHint()); + connect(mButton, SIGNAL(clicked()), SLOT(slotButtonPressed())); + QWhatsThis::add(mButton, + i18n("Choose the font, and foreground and background color, for the alarm message.")); + layout->addWidget(mButton); + + // Font and colour sample display + mSample = new QLineEdit(this); + mSample->setMinimumHeight(QMAX(mSample->fontMetrics().lineSpacing(), mButton->height()*3/2)); + mSample->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::MinimumExpanding); + mSample->setText(i18n("The Quick Brown Fox Jumps Over The Lazy Dog")); + mSample->setCursorPosition(0); + mSample->setAlignment(Qt::AlignCenter); + QWhatsThis::add(mSample, + i18n("This sample text illustrates the current font and color settings. " + "You may edit it to test special characters.")); + layout->addWidget(mSample); +} + +void FontColourButton::setDefaultFont() +{ + mDefaultFont = true; + mSample->setFont(Preferences::messageFont()); +} + +void FontColourButton::setFont(const QFont& font) +{ + mDefaultFont = false; + mFont = font; + mSample->setFont(mFont); +} + +void FontColourButton::setBgColour(const QColor& colour) +{ + mBgColour = colour; + mSample->setPaletteBackgroundColor(mBgColour); +} + +void FontColourButton::setFgColour(const QColor& colour) +{ + mFgColour = colour; + mSample->setPaletteForegroundColor(mFgColour); +} + +/****************************************************************************** +* Called when the OK button is clicked. +* Display a font and colour selection dialog and get the selections. +*/ +void FontColourButton::slotButtonPressed() +{ + FontColourDlg dlg(mBgColour, mFgColour, mFont, mDefaultFont, + i18n("Choose Alarm Font & Color"), this, "fontColourDlg"); + dlg.setReadOnly(mReadOnly); + if (dlg.exec() == QDialog::Accepted) + { + mDefaultFont = dlg.defaultFont(); + mFont = dlg.font(); + mSample->setFont(mFont); + mBgColour = dlg.bgColour(); + mSample->setPaletteBackgroundColor(mBgColour); + mFgColour = dlg.fgColour(); + mSample->setPaletteForegroundColor(mFgColour); + emit selected(); + } +} + + +/*============================================================================= += Class FontColourDlg += Font/colour selection dialog. +=============================================================================*/ + +FontColourDlg::FontColourDlg(const QColor& bgColour, const QColor& fgColour, const QFont& font, + bool defaultFont, const QString& caption, QWidget* parent, const char* name) + : KDialogBase(parent, name, true, caption, Ok|Cancel, Ok, false), + mReadOnly(false) +{ + QWidget* page = new QWidget(this); + setMainWidget(page); + QVBoxLayout* layout = new QVBoxLayout(page, 0, spacingHint()); + mChooser = new FontColourChooser(page, 0, false, QStringList(), QString::null, false, true, true); + mChooser->setBgColour(bgColour); + mChooser->setFgColour(fgColour); + if (defaultFont) + mChooser->setDefaultFont(); + else + mChooser->setFont(font); + layout->addWidget(mChooser); + layout->addSpacing(KDialog::spacingHint()); +} + +/****************************************************************************** +* Called when the OK button is clicked. +*/ +void FontColourDlg::slotOk() +{ + if (mReadOnly) + { + reject(); + return; + } + mDefaultFont = mChooser->defaultFont(); + mFont = mChooser->font(); + mBgColour = mChooser->bgColour(); + mFgColour = mChooser->fgColour(); + accept(); +} + +void FontColourDlg::setReadOnly(bool ro) +{ + mReadOnly = ro; + mChooser->setReadOnly(mReadOnly); +} diff --git a/kalarm/fontcolourbutton.h b/kalarm/fontcolourbutton.h new file mode 100644 index 000000000..9bbe3d8db --- /dev/null +++ b/kalarm/fontcolourbutton.h @@ -0,0 +1,91 @@ +/* + * fontcolourbutton.h - pushbutton widget to select a font and colour + * Program: kalarm + * Copyright © 2003,2007 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef FONTCOLOURBUTTON_H +#define FONTCOLOURBUTTON_H + +#include <qfont.h> +#include <qcolor.h> +#include <qframe.h> +#include <kdialogbase.h> + +class QLineEdit; +class FontColourChooser; +class PushButton; + + +class FontColourButton : public QFrame +{ + Q_OBJECT + public: + FontColourButton(QWidget* parent = 0, const char* name = 0); + void setDefaultFont(); + void setFont(const QFont&); + void setBgColour(const QColor&); + void setFgColour(const QColor&); + bool defaultFont() const { return mDefaultFont; } + QFont font() const { return mFont; } + QColor bgColour() const { return mBgColour; } + QColor fgColour() const { return mFgColour; } + virtual void setReadOnly(bool ro) { mReadOnly = ro; } + virtual bool isReadOnly() const { return mReadOnly; } + + signals: + void selected(); + + protected slots: + void slotButtonPressed(); + + private: + PushButton* mButton; + QColor mBgColour, mFgColour; + QFont mFont; + QLineEdit* mSample; + bool mDefaultFont; + bool mReadOnly; +}; + + +// Font and colour selection dialog displayed by the push button +class FontColourDlg : public KDialogBase +{ + Q_OBJECT + public: + FontColourDlg(const QColor& bg, const QColor& fg, const QFont&, bool defaultFont, + const QString& caption, QWidget* parent = 0, const char* name = 0); + bool defaultFont() const { return mDefaultFont; } + QFont font() const { return mFont; } + QColor bgColour() const { return mBgColour; } + QColor fgColour() const { return mFgColour; } + void setReadOnly(bool); + bool isReadOnly() const { return mReadOnly; } + + protected slots: + virtual void slotOk(); + + private: + FontColourChooser* mChooser; + QColor mBgColour, mFgColour; + QFont mFont; + bool mDefaultFont; + bool mReadOnly; +}; + +#endif // FONTCOLOURBUTTON_H diff --git a/kalarm/functions.cpp b/kalarm/functions.cpp new file mode 100644 index 000000000..dd83fea43 --- /dev/null +++ b/kalarm/functions.cpp @@ -0,0 +1,1099 @@ +/* + * functions.cpp - miscellaneous functions + * Program: kalarm + * Copyright © 2001-2009 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" +#include "functions.h" + +#include "alarmcalendar.h" +#include "alarmevent.h" +#include "alarmlistview.h" +#include "daemon.h" +#include "kalarmapp.h" +#include "kamail.h" +#include "mainwindow.h" +#include "messagewin.h" +#include "preferences.h" +#include "shellprocess.h" +#include "templatelistview.h" +#include "templatemenuaction.h" + +#include <qdeepcopy.h> +#include <qdir.h> +#include <qregexp.h> + +#include <kconfig.h> +#include <kaction.h> +#include <kglobal.h> +#include <klocale.h> +#include <kstdguiitem.h> +#include <kstdaccel.h> +#include <kmessagebox.h> +#include <kfiledialog.h> +#include <dcopclient.h> +#include <dcopref.h> +#include <kdcopservicestarter.h> +#include <kdebug.h> + +#include <libkcal/event.h> +#include <libkcal/icalformat.h> +#include <libkpimidentities/identitymanager.h> +#include <libkpimidentities/identity.h> +#include <libkcal/person.h> + + +namespace +{ +bool resetDaemonQueued = false; +QCString korganizerName = "korganizer"; +QString korgStartError; +#define KORG_DCOP_OBJECT "KOrganizerIface" +const char* KORG_DCOP_WINDOW = "KOrganizer MainWindow"; +const char* KMAIL_DCOP_WINDOW = "kmail-mainwindow#1"; + +bool sendToKOrganizer(const KAEvent&); +bool deleteFromKOrganizer(const QString& eventID); +bool runKOrganizer(); +} +#ifdef HAVE_XTEST +void x11_cancelScreenSaver(); +#endif + + +namespace KAlarm +{ + +/****************************************************************************** +* Display a main window with the specified event selected. +*/ +MainWindow* displayMainWindowSelected(const QString& eventID) +{ + MainWindow* win = MainWindow::firstWindow(); + if (!win) + { + if (theApp()->checkCalendarDaemon()) // ensure calendar is open and daemon started + { + win = MainWindow::create(); + win->show(); + } + } + else + { + // There is already a main window, so make it the active window + bool visible = win->isVisible(); + if (visible) + win->hide(); // in case it's on a different desktop + if (!visible || win->isMinimized()) + win->showNormal(); + win->raise(); + win->setActiveWindow(); + } + if (win && !eventID.isEmpty()) + win->selectEvent(eventID); + return win; +} + +/****************************************************************************** +* Create a New Alarm KAction. +*/ +KAction* createNewAlarmAction(const QString& label, QObject* receiver, const char* slot, KActionCollection* actions, const char* name) +{ + return new KAction(label, "filenew", KStdAccel::openNew(), receiver, slot, actions, name); +} + +/****************************************************************************** +* Create a New From Template KAction. +*/ +TemplateMenuAction* createNewFromTemplateAction(const QString& label, QObject* receiver, const char* slot, KActionCollection* actions, const char* name) +{ + return new TemplateMenuAction(label, "new_from_template", receiver, slot, actions, name); +} + +/****************************************************************************** +* Add a new active (non-expired) alarm. +* Save it in the calendar file and add it to every main window instance. +* If 'selectionView' is non-null, the selection highlight is moved to the new +* event in that listView instance. +* 'event' is updated with the actual event ID. +*/ +UpdateStatus addEvent(KAEvent& event, AlarmListView* selectionView, QWidget* errmsgParent, bool useEventID, bool allowKOrgUpdate) +{ + kdDebug(5950) << "KAlarm::addEvent(): " << event.id() << endl; + UpdateStatus status = UPDATE_OK; + if (!theApp()->checkCalendarDaemon()) // ensure calendar is open and daemon started + return UPDATE_FAILED; + else + { + // Save the event details in the calendar file, and get the new event ID + AlarmCalendar* cal = AlarmCalendar::activeCalendar(); + if (!cal->addEvent(event, useEventID)) + status = UPDATE_FAILED; + else if (!cal->save()) + status = SAVE_FAILED; + } + if (status == UPDATE_OK) + { + if (allowKOrgUpdate && event.copyToKOrganizer()) + { + if (!sendToKOrganizer(event)) // tell KOrganizer to show the event + status = UPDATE_KORG_ERR; + } + + // Update the window lists + AlarmListView::addEvent(event, selectionView); + return status; + } + + if (errmsgParent) + displayUpdateError(errmsgParent, status, ERR_ADD, 1); + return status; +} + +/****************************************************************************** +* Save the event in the expired calendar file and adjust every main window instance. +* The event's ID is changed to an expired ID if necessary. +*/ +bool addExpiredEvent(KAEvent& event) +{ + kdDebug(5950) << "KAlarm::addExpiredEvent(" << event.id() << ")\n"; + AlarmCalendar* cal = AlarmCalendar::expiredCalendarOpen(); + if (!cal) + return false; + bool archiving = (KAEvent::uidStatus(event.id()) == KAEvent::ACTIVE); + if (archiving) + event.setSaveDateTime(QDateTime::currentDateTime()); // time stamp to control purging + KCal::Event* kcalEvent = cal->addEvent(event); + cal->save(); + + // Update window lists + if (!archiving) + AlarmListView::addEvent(event, 0); + else if (kcalEvent) + AlarmListView::modifyEvent(KAEvent(*kcalEvent), 0); + return true; +} + +/****************************************************************************** +* Add a new template. +* Save it in the calendar file and add it to every template list view. +* If 'selectionView' is non-null, the selection highlight is moved to the new +* event in that listView instance. +* 'event' is updated with the actual event ID. +*/ +UpdateStatus addTemplate(KAEvent& event, TemplateListView* selectionView, QWidget* errmsgParent) +{ + kdDebug(5950) << "KAlarm::addTemplate(): " << event.id() << endl; + UpdateStatus status = UPDATE_OK; + + // Add the template to the calendar file + AlarmCalendar* cal = AlarmCalendar::templateCalendarOpen(); + if (!cal || !cal->addEvent(event)) + status = UPDATE_FAILED; + else if (!cal->save()) + status = SAVE_FAILED; + else + { + cal->emitEmptyStatus(); + + // Update the window lists + TemplateListView::addEvent(event, selectionView); + return UPDATE_OK; + } + + if (errmsgParent) + displayUpdateError(errmsgParent, status, ERR_TEMPLATE, 1); + return status; +} + +/****************************************************************************** +* Modify an active (non-expired) alarm in the calendar file and in every main +* window instance. +* The new event will have a different event ID from the old one. +* If 'selectionView' is non-null, the selection highlight is moved to the +* modified event in that listView instance. +*/ +UpdateStatus modifyEvent(KAEvent& oldEvent, const KAEvent& newEvent, AlarmListView* selectionView, QWidget* errmsgParent) +{ + kdDebug(5950) << "KAlarm::modifyEvent(): '" << oldEvent.id() << endl; + + UpdateStatus status = UPDATE_OK; + if (!newEvent.valid()) + { + deleteEvent(oldEvent, true); + status = UPDATE_FAILED; + } + else + { + if (oldEvent.copyToKOrganizer()) + { + // Tell KOrganizer to delete its old event. + // But ignore errors, because the user could have manually + // deleted it since KAlarm asked KOrganizer to set it up. + deleteFromKOrganizer(oldEvent.id()); + } + + // Update the event in the calendar file, and get the new event ID + AlarmCalendar* cal = AlarmCalendar::activeCalendar(); + if (!cal->deleteEvent(oldEvent.id()) + || !cal->addEvent(const_cast<KAEvent&>(newEvent), true)) + status = UPDATE_FAILED; + else if (!cal->save()) + status = SAVE_FAILED; + if (status == UPDATE_OK) + { + if (newEvent.copyToKOrganizer()) + { + if (!sendToKOrganizer(newEvent)) // tell KOrganizer to show the new event + status = UPDATE_KORG_ERR; + } + + // Update the window lists + AlarmListView::modifyEvent(oldEvent.id(), newEvent, selectionView); + return status; + } + } + + if (errmsgParent) + displayUpdateError(errmsgParent, status, ERR_ADD, 1); + return status; +} + +/****************************************************************************** +* Update an active (non-expired) alarm from the calendar file and from every +* main window instance. +* The new event will have the same event ID as the old one. +* If 'selectionView' is non-null, the selection highlight is moved to the +* updated event in that listView instance. +* The event is not updated in KOrganizer, since this function is called when an +* existing alarm is rescheduled (due to recurrence or deferral). +*/ +UpdateStatus updateEvent(KAEvent& event, AlarmListView* selectionView, QWidget* errmsgParent, bool archiveOnDelete, bool incRevision) +{ + kdDebug(5950) << "KAlarm::updateEvent(): " << event.id() << endl; + + if (!event.valid()) + deleteEvent(event, archiveOnDelete); + else + { + // Update the event in the calendar file. + if (incRevision) + event.incrementRevision(); // ensure alarm daemon sees the event has changed + AlarmCalendar* cal = AlarmCalendar::activeCalendar(); + cal->updateEvent(event); + if (!cal->save()) + { + if (errmsgParent) + displayUpdateError(errmsgParent, SAVE_FAILED, ERR_ADD, 1); + return SAVE_FAILED; + } + + // Update the window lists + AlarmListView::modifyEvent(event, selectionView); + } + return UPDATE_OK; +} + +/****************************************************************************** +* Update a template in the calendar file and in every template list view. +* If 'selectionView' is non-null, the selection highlight is moved to the +* updated event in that listView instance. +*/ +UpdateStatus updateTemplate(const KAEvent& event, TemplateListView* selectionView, QWidget* errmsgParent) +{ + UpdateStatus status = UPDATE_OK; + AlarmCalendar* cal = AlarmCalendar::templateCalendarOpen(); + if (!cal) + status = UPDATE_FAILED; + else + { + cal->updateEvent(event); + if (!cal->save()) + status = SAVE_FAILED; + else + { + TemplateListView::modifyEvent(event.id(), event, selectionView); + return UPDATE_OK; + } + } + + if (errmsgParent) + displayUpdateError(errmsgParent, SAVE_FAILED, ERR_TEMPLATE, 1); + return status; +} + +/****************************************************************************** +* Delete an alarm from the calendar file and from every main window instance. +* If the event is archived, the event's ID is changed to an expired ID if necessary. +*/ +UpdateStatus deleteEvent(KAEvent& event, bool archive, QWidget* errmsgParent) +{ + QString id = event.id(); + kdDebug(5950) << "KAlarm::deleteEvent(): " << id << endl; + + // Update the window lists + AlarmListView::deleteEvent(id); + + UpdateStatus status = UPDATE_OK; + AlarmCalendar* cal; + + // Delete the event from the calendar file + if (KAEvent::uidStatus(id) == KAEvent::EXPIRED) + { + cal = AlarmCalendar::expiredCalendarOpen(); + if (!cal) + status = UPDATE_FAILED; + } + else + { + if (event.copyToKOrganizer()) + { + // The event was shown in KOrganizer, so tell KOrganizer to + // delete it. Note that an error could occur if the user + // manually deleted it from KOrganizer since it was set up. + if (!deleteFromKOrganizer(event.id())) + status = UPDATE_KORG_ERR; + } + if (archive && event.toBeArchived()) + addExpiredEvent(event); // this changes the event ID to an expired ID + cal = AlarmCalendar::activeCalendar(); + } + if (status != UPDATE_FAILED) + { + if (!cal->deleteEvent(id, true)) // save calendar after deleting + status = SAVE_FAILED; + } + if (status > UPDATE_KORG_ERR && errmsgParent) + displayUpdateError(errmsgParent, SAVE_FAILED, ERR_DELETE, 1); + return status; +} + +/****************************************************************************** +* Delete a template from the calendar file and from every template list view. +*/ +UpdateStatus deleteTemplate(const KAEvent& event) +{ + QString id = event.id(); + + // Delete the template from the calendar file + AlarmCalendar* cal = AlarmCalendar::templateCalendarOpen(); + if (!cal) + return UPDATE_FAILED; + if (!cal->deleteEvent(id, true)) // save calendar after deleting + return SAVE_FAILED; + cal->emitEmptyStatus(); + + // Update the window lists + TemplateListView::deleteEvent(id); + return UPDATE_OK; +} + +/****************************************************************************** +* Delete an alarm from the display calendar. +*/ +void deleteDisplayEvent(const QString& eventID) +{ + kdDebug(5950) << "KAlarm::deleteDisplayEvent(" << eventID << ")\n"; + + if (KAEvent::uidStatus(eventID) == KAEvent::DISPLAYING) + { + AlarmCalendar* cal = AlarmCalendar::displayCalendarOpen(); + if (cal) + cal->deleteEvent(eventID, true); // save calendar after deleting + } +} + +/****************************************************************************** +* Undelete an expired alarm, and update every main window instance. +* The archive bit is set to ensure that it gets re-archived if it is deleted again. +* If 'selectionView' is non-null, the selection highlight is moved to the +* restored event in that listView instance. +*/ +UpdateStatus reactivateEvent(KAEvent& event, AlarmListView* selectionView, bool useEventID) +{ + QString id = event.id(); + kdDebug(5950) << "KAlarm::reactivateEvent(): " << id << endl; + + // Delete the event from the expired calendar file + if (KAEvent::uidStatus(id) == KAEvent::EXPIRED) + { + QDateTime now = QDateTime::currentDateTime(); + if (event.occursAfter(now, true)) + { + if (event.recurs() || event.repeatCount()) + event.setNextOccurrence(now); // skip any recurrences in the past + event.setArchive(); // ensure that it gets re-archived if it is deleted + + // Save the event details in the calendar file, and get the new event ID + AlarmCalendar* cal = AlarmCalendar::activeCalendar(); + if (!cal->addEvent(event, useEventID)) + return UPDATE_FAILED; + if (!cal->save()) + return SAVE_FAILED; + + UpdateStatus status = UPDATE_OK; + if (event.copyToKOrganizer()) + { + if (!sendToKOrganizer(event)) // tell KOrganizer to show the event + status = UPDATE_KORG_ERR; + } + + // Update the window lists + AlarmListView::undeleteEvent(id, event, selectionView); + + cal = AlarmCalendar::expiredCalendarOpen(); + if (cal) + cal->deleteEvent(id, true); // save calendar after deleting + return status; + } + } + return UPDATE_FAILED; +} + +/****************************************************************************** +* Enable or disable an alarm in the calendar file and in every main window instance. +* The new event will have the same event ID as the old one. +* If 'selectionView' is non-null, the selection highlight is moved to the +* updated event in that listView instance. +*/ +UpdateStatus enableEvent(KAEvent& event, AlarmListView* selectionView, bool enable) +{ + kdDebug(5950) << "KAlarm::enableEvent(" << enable << "): " << event.id() << endl; + + if (enable != event.enabled()) + { + event.setEnabled(enable); + + // Update the event in the calendar file + AlarmCalendar* cal = AlarmCalendar::activeCalendar(); + cal->updateEvent(event); + if (!cal->save()) + return SAVE_FAILED; + + // If we're disabling a display alarm, close any message window + if (!enable && event.displayAction()) + { + MessageWin* win = MessageWin::findEvent(event.id()); + delete win; + } + + // Update the window lists + AlarmListView::modifyEvent(event, selectionView); + } + return UPDATE_OK; +} + +/****************************************************************************** +* Display an error message about an error saving an event. +*/ +void displayUpdateError(QWidget* parent, UpdateStatus, UpdateError code, int nAlarms) +{ + QString errmsg; + switch (code) + { + case ERR_ADD: + errmsg = (nAlarms > 1) ? i18n("Error saving alarms") + : i18n("Error saving alarm"); + break; + case ERR_DELETE: + errmsg = (nAlarms > 1) ? i18n("Error deleting alarms") + : i18n("Error deleting alarm"); + break; + case ERR_REACTIVATE: + errmsg = (nAlarms > 1) ? i18n("Error saving reactivated alarms") + : i18n("Error saving reactivated alarm"); + break; + case ERR_TEMPLATE: + errmsg = i18n("Error saving alarm template"); + break; + } + KMessageBox::error(parent, errmsg); +} + +/****************************************************************************** +* Display an error message corresponding to a specified alarm update error code. +*/ +void displayKOrgUpdateError(QWidget* parent, KOrgUpdateError code, int nAlarms) +{ + QString errmsg; + switch (code) + { + case KORG_ERR_ADD: + errmsg = (nAlarms > 1) ? i18n("Unable to show alarms in KOrganizer") + : i18n("Unable to show alarm in KOrganizer"); + break; + case KORG_ERR_MODIFY: + errmsg = i18n("Unable to update alarm in KOrganizer"); + break; + case KORG_ERR_DELETE: + errmsg = (nAlarms > 1) ? i18n("Unable to delete alarms from KOrganizer") + : i18n("Unable to delete alarm from KOrganizer"); + break; + } + KMessageBox::error(parent, errmsg); +} + +/****************************************************************************** +* Display the alarm edit dialogue to edit a specified alarm. +*/ +bool edit(const QString& eventID) +{ + AlarmCalendar* cal; + switch (KAEvent::uidStatus(eventID)) + { + case KAEvent::ACTIVE: + cal = AlarmCalendar::activeCalendar(); + break; + case KAEvent::TEMPLATE: + cal = AlarmCalendar::templateCalendarOpen(); + break; + default: + kdError(5950) << "KAlarm::edit(" << eventID << "): event not active or template" << endl; + return false; + } + KCal::Event* kcalEvent = cal->event(eventID); + if (!kcalEvent) + { + kdError(5950) << "KAlarm::edit(): event ID not found: " << eventID << endl; + return false; + } + KAEvent event(*kcalEvent); + MainWindow::executeEdit(event); + return true; +} + +/****************************************************************************** +* Display the alarm edit dialogue to edit a new alarm, optionally preset with +* a template. +*/ +bool editNew(const QString& templateName) +{ + bool result = true; + if (!templateName.isEmpty()) + { + AlarmCalendar* cal = AlarmCalendar::templateCalendarOpen(); + if (cal) + { + KAEvent templateEvent = KAEvent::findTemplateName(*cal, templateName); + if (templateEvent.valid()) + { + MainWindow::executeNew(templateEvent); + return true; + } + kdWarning(5950) << "KAlarm::editNew(" << templateName << "): template not found" << endl; + } + result = false; + } + MainWindow::executeNew(); + return result; +} + +/****************************************************************************** +* Returns a list of all alarm templates. +* If shell commands are disabled, command alarm templates are omitted. +*/ +QValueList<KAEvent> templateList() +{ + QValueList<KAEvent> templates; + AlarmCalendar* cal = AlarmCalendar::templateCalendarOpen(); + if (cal) + { + bool includeCmdAlarms = ShellProcess::authorised(); + KCal::Event::List events = cal->events(); + for (KCal::Event::List::ConstIterator it = events.begin(); it != events.end(); ++it) + { + KCal::Event* kcalEvent = *it; + KAEvent event(*kcalEvent); + if (includeCmdAlarms || event.action() != KAEvent::COMMAND) + templates.append(event); + } + } + return templates; +} + +/****************************************************************************** +* To be called after an alarm has been edited. +* Prompt the user to re-enable alarms if they are currently disabled, and if +* it's an email alarm, warn if no 'From' email address is configured. +*/ +void outputAlarmWarnings(QWidget* parent, const KAEvent* event) +{ + if (event && event->action() == KAEvent::EMAIL + && Preferences::emailAddress().isEmpty()) + KMessageBox::information(parent, i18n("Please set the 'From' email address...", + "%1\nPlease set it in the Preferences dialog.").arg(KAMail::i18n_NeedFromEmailAddress())); + + if (!Daemon::monitoringAlarms()) + { + if (KMessageBox::warningYesNo(parent, i18n("Alarms are currently disabled.\nDo you want to enable alarms now?"), + QString::null, i18n("Enable"), i18n("Keep Disabled"), + QString::fromLatin1("EditEnableAlarms")) + == KMessageBox::Yes) + Daemon::setAlarmsEnabled(); + } +} + +/****************************************************************************** +* Reset the alarm daemon and reload the calendar. +* If the daemon is not already running, start it. +*/ +void resetDaemon() +{ + kdDebug(5950) << "KAlarm::resetDaemon()" << endl; + if (!resetDaemonQueued) + { + resetDaemonQueued = true; + theApp()->processQueue(); + } +} + +/****************************************************************************** +* This method must only be called from the main KAlarm queue processing loop, +* to prevent asynchronous calendar operations interfering with one another. +* +* If resetDaemon() has been called, reset the alarm daemon and reload the calendars. +* If the daemon is not already running, start it. +*/ +void resetDaemonIfQueued() +{ + if (resetDaemonQueued) + { + kdDebug(5950) << "KAlarm::resetDaemonIfNeeded()" << endl; + AlarmCalendar::activeCalendar()->reload(); + AlarmCalendar::expiredCalendar()->reload(); + + // Close any message windows for alarms which are now disabled + KAEvent event; + KCal::Event::List events = AlarmCalendar::activeCalendar()->events(); + for (KCal::Event::List::ConstIterator it = events.begin(); it != events.end(); ++it) + { + KCal::Event* kcalEvent = *it; + event.set(*kcalEvent); + if (!event.enabled() && event.displayAction()) + { + MessageWin* win = MessageWin::findEvent(event.id()); + delete win; + } + } + + MainWindow::refresh(); + if (!Daemon::reset()) + Daemon::start(); + resetDaemonQueued = false; + } +} + +/****************************************************************************** +* Start KMail if it isn't already running, and optionally iconise it. +* Reply = reason for failure to run KMail (which may be the empty string) +* = null string if success. +*/ +QString runKMail(bool minimise) +{ + QCString dcopName; + QString errmsg; + if (!runProgram("kmail", (minimise ? KMAIL_DCOP_WINDOW : ""), dcopName, errmsg)) + return i18n("Unable to start KMail\n(%1)").arg(errmsg); + return QString::null; +} + +/****************************************************************************** +* Start another program for DCOP access if it isn't already running. +* If 'windowName' is not empty, the program's window of that name is iconised. +* On exit, 'dcopName' contains the DCOP name to access the application, and +* 'errorMessage' contains an error message if failure. +* Reply = true if the program is now running. +*/ +bool runProgram(const QCString& program, const QCString& windowName, QCString& dcopName, QString& errorMessage) +{ + if (!kapp->dcopClient()->isApplicationRegistered(program)) + { + // KOrganizer is not already running, so start it + if (KApplication::startServiceByDesktopName(QString::fromLatin1(program), QString::null, &errorMessage, &dcopName)) + { + kdError(5950) << "runProgram(): couldn't start " << program << " (" << errorMessage << ")\n"; + return false; + } + // Minimise its window - don't use hide() since this would remove all + // trace of it from the panel if it is not configured to be docked in + // the system tray. + kapp->dcopClient()->send(dcopName, windowName, "minimize()", QString::null); + } + else if (dcopName.isEmpty()) + dcopName = program; + errorMessage = QString::null; + return true; +} + +/****************************************************************************** +* Read the size for the specified window from the config file, for the +* current screen resolution. +* Reply = true if size set in the config file, in which case 'result' is set +* = false if no size is set, in which case 'result' is unchanged. +*/ +bool readConfigWindowSize(const char* window, QSize& result) +{ + KConfig* config = KGlobal::config(); + config->setGroup(QString::fromLatin1(window)); + QWidget* desktop = KApplication::desktop(); + QSize s = QSize(config->readNumEntry(QString::fromLatin1("Width %1").arg(desktop->width()), 0), + config->readNumEntry(QString::fromLatin1("Height %1").arg(desktop->height()), 0)); + if (s.isEmpty()) + return false; + result = s; + return true; +} + +/****************************************************************************** +* Write the size for the specified window to the config file, for the +* current screen resolution. +*/ +void writeConfigWindowSize(const char* window, const QSize& size) +{ + KConfig* config = KGlobal::config(); + config->setGroup(QString::fromLatin1(window)); + QWidget* desktop = KApplication::desktop(); + config->writeEntry(QString::fromLatin1("Width %1").arg(desktop->width()), size.width()); + config->writeEntry(QString::fromLatin1("Height %1").arg(desktop->height()), size.height()); + config->sync(); +} + +/****************************************************************************** +* Return the current KAlarm version number. +*/ +int Version() +{ + static int version = 0; + if (!version) + version = getVersionNumber(KALARM_VERSION); + return version; +} + +/****************************************************************************** +* Convert the supplied KAlarm version string to a version number. +* Reply = version number (double digit for each of major, minor & issue number, +* e.g. 010203 for 1.2.3 +* = 0 if invalid version string. +*/ +int getVersionNumber(const QString& version, QString* subVersion) +{ + // N.B. Remember to change Version(int major, int minor, int rev) + // if the representation returned by this method changes. + if (subVersion) + *subVersion = QString::null; + int count = version.contains('.') + 1; + if (count < 2) + return 0; + bool ok; + unsigned vernum = version.section('.', 0, 0).toUInt(&ok) * 10000; // major version + if (!ok) + return 0; + unsigned v = version.section('.', 1, 1).toUInt(&ok); // minor version + if (!ok) + return 0; + vernum += (v < 99 ? v : 99) * 100; + if (count >= 3) + { + // Issue number: allow other characters to follow the last digit + QString issue = version.section('.', 2); + if (!issue.at(0).isDigit()) + return 0; + int n = issue.length(); + int i; + for (i = 0; i < n && issue.at(i).isDigit(); ++i) ; + if (subVersion) + *subVersion = issue.mid(i); + v = issue.left(i).toUInt(); // issue number + vernum += (v < 99 ? v : 99); + } + return vernum; +} + +/****************************************************************************** +* Check from its mime type whether a file appears to be a text or image file. +* If a text file, its type is distinguished. +* Reply = file type. +*/ +FileType fileType(const QString& mimetype) +{ + static const char* applicationTypes[] = { + "x-shellscript", "x-nawk", "x-awk", "x-perl", "x-python", + "x-desktop", "x-troff", 0 }; + static const char* formattedTextTypes[] = { + "html", "xml", 0 }; + + if (mimetype.startsWith(QString::fromLatin1("image/"))) + return Image; + int slash = mimetype.find('/'); + if (slash < 0) + return Unknown; + QString type = mimetype.mid(slash + 1); + const char* typel = type.latin1(); + if (mimetype.startsWith(QString::fromLatin1("application"))) + { + for (int i = 0; applicationTypes[i]; ++i) + if (!strcmp(typel, applicationTypes[i])) + return TextApplication; + } + else if (mimetype.startsWith(QString::fromLatin1("text"))) + { + for (int i = 0; formattedTextTypes[i]; ++i) + if (!strcmp(typel, formattedTextTypes[i])) + return TextFormatted; + return TextPlain; + } + return Unknown; +} + +/****************************************************************************** +* Display a modal dialogue to choose an existing file, initially highlighting +* any specified file. +* @param initialFile The file to initially highlight - must be a full path name or URL. +* @param defaultDir The directory to start in if @p initialFile is empty. If empty, +* the user's home directory will be used. Updated to the +* directory containing the selected file, if a file is chosen. +* @param mode OR of KFile::Mode values, e.g. ExistingOnly, LocalOnly. +* Reply = URL selected. If none is selected, URL.isEmpty() is true. +*/ +QString browseFile(const QString& caption, QString& defaultDir, const QString& initialFile, + const QString& filter, int mode, QWidget* parent, const char* name) +{ + QString initialDir = !initialFile.isEmpty() ? QString(initialFile).remove(QRegExp("/[^/]*$")) + : !defaultDir.isEmpty() ? defaultDir + : QDir::homeDirPath(); + KFileDialog fileDlg(initialDir, filter, parent, name, true); + fileDlg.setOperationMode(mode & KFile::ExistingOnly ? KFileDialog::Opening : KFileDialog::Saving); + fileDlg.setMode(KFile::File | mode); + fileDlg.setCaption(caption); + if (!initialFile.isEmpty()) + fileDlg.setSelection(initialFile); + if (fileDlg.exec() != QDialog::Accepted) + return QString::null; + KURL url = fileDlg.selectedURL(); + defaultDir = url.path(); + return (mode & KFile::LocalOnly) ? url.path() : url.prettyURL(); +} + +/****************************************************************************** +* Return the first day of the week for the user's locale. +* Reply = 1 (Mon) .. 7 (Sun). +*/ +int localeFirstDayOfWeek() +{ + static int firstDay = 0; + if (!firstDay) + firstDay = KGlobal::locale()->weekStartDay(); + return firstDay; +} + +/****************************************************************************** +* Return the supplied string with any accelerator code stripped out. +*/ +QString stripAccel(const QString& text) +{ + unsigned len = text.length(); + QString out = QDeepCopy<QString>(text); + QChar *corig = (QChar*)out.unicode(); + QChar *cout = corig; + QChar *cin = cout; + while (len) + { + if ( *cin == '&' ) + { + ++cin; + --len; + if ( !len ) + break; + } + *cout = *cin; + ++cout; + ++cin; + --len; + } + unsigned newlen = cout - corig; + if (newlen != out.length()) + out.truncate(newlen); + return out; +} + +/****************************************************************************** +* Cancel the screen saver, in case it is active. +* Only implemented if the X11 XTest extension is installed. +*/ +void cancelScreenSaver() +{ +#ifdef HAVE_XTEST + x11_cancelScreenSaver(); +#endif // HAVE_XTEST +} + +} // namespace KAlarm + + +namespace { + +/****************************************************************************** +* Tell KOrganizer to put an alarm in its calendar. +* It will be held by KOrganizer as a simple event, without alarms - KAlarm +* is still responsible for alarming. +*/ +bool sendToKOrganizer(const KAEvent& event) +{ + KCal::Event* kcalEvent = event.event(); + QString uid = KAEvent::uid(event.id(), KAEvent::KORGANIZER); + kcalEvent->setUid(uid); + kcalEvent->clearAlarms(); + QString userEmail; + switch (event.action()) + { + case KAEvent::MESSAGE: + case KAEvent::FILE: + case KAEvent::COMMAND: + kcalEvent->setSummary(event.cleanText()); + userEmail = Preferences::emailAddress(); + break; + case KAEvent::EMAIL: + { + QString from = event.emailFromId() + ? KAMail::identityManager()->identityForUoid(event.emailFromId()).fullEmailAddr() + : Preferences::emailAddress(); + AlarmText atext; + atext.setEmail(event.emailAddresses(", "), from, QString::null, QString::null, event.emailSubject(), QString::null); + kcalEvent->setSummary(atext.displayText()); + userEmail = from; + break; + } + } + kcalEvent->setOrganizer(KCal::Person(QString::null, userEmail)); + + // Translate the event into string format + KCal::ICalFormat format; + format.setTimeZone(QString::null, false); + QString iCal = format.toICalString(kcalEvent); +kdDebug(5950)<<"Korg->"<<iCal<<endl; + delete kcalEvent; + + // Send the event to KOrganizer + if (!runKOrganizer()) // start KOrganizer if it isn't already running + return false; + QByteArray data, replyData; + QCString replyType; + QDataStream arg(data, IO_WriteOnly); + arg << iCal; + if (kapp->dcopClient()->call(korganizerName, KORG_DCOP_OBJECT, "addIncidence(QString)", data, replyType, replyData) + && replyType == "bool") + { + bool result; + QDataStream reply(replyData, IO_ReadOnly); + reply >> result; + if (result) + { + kdDebug(5950) << "sendToKOrganizer(" << uid << "): success\n"; + return true; + } + } + kdError(5950) << "sendToKOrganizer(): KOrganizer addEvent(" << uid << ") dcop call failed\n"; + return false; +} + +/****************************************************************************** +* Tell KOrganizer to delete an event from its calendar. +*/ +bool deleteFromKOrganizer(const QString& eventID) +{ + if (!runKOrganizer()) // start KOrganizer if it isn't already running + return false; + QString newID = KAEvent::uid(eventID, KAEvent::KORGANIZER); + QByteArray data, replyData; + QCString replyType; + QDataStream arg(data, IO_WriteOnly); + arg << newID << true; + if (kapp->dcopClient()->call(korganizerName, KORG_DCOP_OBJECT, "deleteIncidence(QString,bool)", data, replyType, replyData) + && replyType == "bool") + { + bool result; + QDataStream reply(replyData, IO_ReadOnly); + reply >> result; + if (result) + { + kdDebug(5950) << "deleteFromKOrganizer(" << newID << "): success\n"; + return true; + } + } + kdError(5950) << "sendToKOrganizer(): KOrganizer deleteEvent(" << newID << ") dcop call failed\n"; + return false; +} + +/****************************************************************************** +* Start KOrganizer if not already running, and create its DCOP interface. +*/ +bool runKOrganizer() +{ + QString error; + QCString dcopService; + int result = KDCOPServiceStarter::self()->findServiceFor("DCOP/Organizer", QString::null, QString::null, &error, &dcopService); + if (result) + { + kdDebug(5950) << "Unable to start DCOP/Organizer: " << dcopService << " " << error << endl; + return false; + } + // If Kontact is running, there is be a load() method which needs to be called + // to load KOrganizer into Kontact. But if KOrganizer is running independently, + // the load() method doesn't exist. + QCString dummy; + if (!kapp->dcopClient()->findObject(dcopService, KORG_DCOP_OBJECT, "", QByteArray(), dummy, dummy)) + { + DCOPRef ref(dcopService, dcopService); // talk to the KUniqueApplication or its Kontact wrapper + DCOPReply reply = ref.call("load()"); + if (!reply.isValid() || !(bool)reply) + { + kdWarning(5950) << "Error loading " << dcopService << endl; + return false; + } + if (!kapp->dcopClient()->findObject(dcopService, KORG_DCOP_OBJECT, "", QByteArray(), dummy, dummy)) + { + kdWarning(5950) << "Unable to access KOrganizer's "KORG_DCOP_OBJECT" DCOP object" << endl; + return false; + } + } + return true; +} + +} // namespace + +#ifdef HAVE_XTEST +#include <X11/keysym.h> +#include <X11/extensions/XTest.h> +#include <qwindowdefs.h> + +/****************************************************************************** +* Cancel the screen saver, in case it is active. +* Only implemented if the X11 XTest extension is installed. +*/ +void x11_cancelScreenSaver() +{ + kdDebug(5950) << "KAlarm::cancelScreenSaver()" << endl; + Display* display = qt_xdisplay(); + static int XTestKeyCode = 0; + if (!XTestKeyCode) + XTestKeyCode = XKeysymToKeycode(display, XK_Shift_L); + XTestFakeKeyEvent(display, XTestKeyCode, true, CurrentTime); + XTestFakeKeyEvent(display, XTestKeyCode, false, CurrentTime); + XSync(display, false); +} +#endif // HAVE_XTEST diff --git a/kalarm/functions.h b/kalarm/functions.h new file mode 100644 index 000000000..c5337cc48 --- /dev/null +++ b/kalarm/functions.h @@ -0,0 +1,130 @@ +/* + * functions.h - miscellaneous functions + * Program: kalarm + * Copyright © 2004-2006,2009 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef FUNCTIONS_H +#define FUNCTIONS_H + +/** @file functions.h - miscellaneous functions */ + +#include <qsize.h> +#include <qstring.h> + +#include "alarmevent.h" + +class QObject; +class QWidget; +class QString; +class KAction; +class KActionCollection; +namespace KCal { class Event; } +class KAEvent; +class MainWindow; +class AlarmListView; +class TemplateListView; +class TemplateMenuAction; + +namespace KAlarm +{ + +/** Return codes from fileType() */ +enum FileType { Unknown, TextPlain, TextFormatted, TextApplication, Image }; +/** Return codes from calendar update functions. + * The codes are ordered by severity. + */ +enum UpdateStatus { + UPDATE_OK, // update succeeded + UPDATE_KORG_ERR, // update succeeded, but KOrganizer update failed + UPDATE_ERROR, // update failed partially + UPDATE_FAILED, // update failed completely + SAVE_FAILED // calendar was updated in memory, but save failed +}; +/** Error codes supplied as parameter to displayUpdateError() */ +enum UpdateError { ERR_ADD, ERR_DELETE, ERR_REACTIVATE, ERR_TEMPLATE }; +/** Error codes supplied as parameter to displayKOrgUpdateError() */ +enum KOrgUpdateError { KORG_ERR_ADD, KORG_ERR_MODIFY, KORG_ERR_DELETE }; + + +/** Display a main window with the specified event selected */ +MainWindow* displayMainWindowSelected(const QString& eventID = QString::null); +bool readConfigWindowSize(const char* window, QSize&); +void writeConfigWindowSize(const char* window, const QSize&); +/** Check from its mime type whether a file appears to be a text or image file. + * If a text file, its type is distinguished. + */ +FileType fileType(const QString& mimetype); +/** Return current KAlarm version number */ +int Version(); +inline int Version(int major, int minor, int rev) { return major*10000 + minor*100 + rev; } +int getVersionNumber(const QString& version, QString* subVersion = 0); +/** Return which version of KAlarm was the first to use the current calendar/event format */ +inline int currentCalendarVersion() { return KAEvent::calVersion(); } +inline QString currentCalendarVersionString() { return KAEvent::calVersionString(); } +QString browseFile(const QString& caption, QString& defaultDir, const QString& initialFile = QString::null, + const QString& filter = QString::null, int mode = 0, QWidget* parent = 0, const char* name = 0); +bool edit(const QString& eventID); +bool editNew(const QString& templateName = QString::null); +/** Create a "New Alarm" KAction */ +KAction* createNewAlarmAction(const QString& label, QObject* receiver, const char* slot, KActionCollection*, const char* name); +/** Create a "New From Template" KAction */ +TemplateMenuAction* createNewFromTemplateAction(const QString& label, QObject* receiver, const char* slot, KActionCollection*, const char* name); +/** Returns a list of all alarm templates. + * If shell commands are disabled, command alarm templates are omitted. + */ +QValueList<KAEvent> templateList(); +void outputAlarmWarnings(QWidget* parent, const KAEvent* = 0); +void resetDaemon(); +void resetDaemonIfQueued(); // must only be called from KAlarmApp::processQueue() +QString runKMail(bool minimise); +bool runProgram(const QCString& program, const QCString& windowName, QCString& dcopName, QString& errorMessage); + +UpdateStatus addEvent(KAEvent&, AlarmListView* selectionView, QWidget* errmsgParent = 0, bool useEventID = false, bool allowKOrgUpdate = true); +bool addExpiredEvent(KAEvent&); +UpdateStatus addTemplate(KAEvent&, TemplateListView* selectionView, QWidget* errmsgParent = 0); +UpdateStatus modifyEvent(KAEvent& oldEvent, const KAEvent& newEvent, AlarmListView* selectionView, QWidget* errmsgParent = 0); +UpdateStatus updateEvent(KAEvent&, AlarmListView* selectionView, QWidget* errmsgParent = 0, bool archiveOnDelete = true, bool incRevision = true); +UpdateStatus updateTemplate(const KAEvent&, TemplateListView* selectionView, QWidget* errmsgParent = 0); +UpdateStatus deleteEvent(KAEvent&, bool archive = true, QWidget* errmsgParent = 0); +UpdateStatus deleteTemplate(const KAEvent&); +void deleteDisplayEvent(const QString& eventID); +UpdateStatus reactivateEvent(KAEvent&, AlarmListView* selectionView, bool useEventID = false); +UpdateStatus enableEvent(KAEvent&, AlarmListView* selectionView, bool enable); +void displayUpdateError(QWidget* parent, UpdateStatus, UpdateError, int nAlarms); +void displayKOrgUpdateError(QWidget* parent, KOrgUpdateError, int nAlarms); + +void cancelScreenSaver(); +QString stripAccel(const QString&); + +int localeFirstDayOfWeek(); + +/* Given a standard KDE day number, return the day number in the week for the user's locale. + * Standard day number = 1 (Mon) .. 7 (Sun) + * Locale day number in week = 0 .. 6 + */ +inline int weekDay_to_localeDayInWeek(int weekDay) { return (weekDay + 7 - localeFirstDayOfWeek()) % 7; } + +/* Given a day number in the week for the user's locale, return the standard KDE day number. + * 'index' = 0 .. 6 + * Standard day number = 1 (Mon) .. 7 (Sun) + */ +inline int localeDayInWeek_to_weekDay(int index) { return (index + localeFirstDayOfWeek() - 1) % 7 + 1; } + +} // namespace KAlarm + +#endif // FUNCTIONS_H diff --git a/kalarm/hi16-app-kalarm.png b/kalarm/hi16-app-kalarm.png Binary files differnew file mode 100644 index 000000000..72b7ece31 --- /dev/null +++ b/kalarm/hi16-app-kalarm.png diff --git a/kalarm/hi32-app-kalarm.png b/kalarm/hi32-app-kalarm.png Binary files differnew file mode 100644 index 000000000..3c5b9f065 --- /dev/null +++ b/kalarm/hi32-app-kalarm.png diff --git a/kalarm/hi48-app-kalarm.png b/kalarm/hi48-app-kalarm.png Binary files differnew file mode 100644 index 000000000..8351a1ebb --- /dev/null +++ b/kalarm/hi48-app-kalarm.png diff --git a/kalarm/kalarm.desktop b/kalarm/kalarm.desktop new file mode 100644 index 000000000..970847004 --- /dev/null +++ b/kalarm/kalarm.desktop @@ -0,0 +1,75 @@ +# KDE Config File +[Desktop Entry] +Name=KAlarm +Name[af]=K-alarm +Name[cy]=KLarwm +Name[eo]=Alarmilo +Name[hi]=के-अलार्म +Name[ko]=KDE 알람 +Name[mk]=КАларм +Name[ne]=केडीई संसूचक +Name[nso]=KAlamo +Name[pl]=Alarm +Name[sv]=Kalarm +Name[ta]=Kஅலாரம் +Name[th]=เตือนการนัดหมาย - K +Name[ven]=Alamu ya K +Name[zh_TW]=KAlarm 鬧鐘 +Type=Application +Exec=kalarm -caption "%c" %i +Icon=kalarm +DocPath=kalarm/index.html +GenericName=Personal Alarm Scheduler +GenericName[af]=Persoonlike alarm skeduleerder +GenericName[bg]=Аларма +GenericName[bs]=Lični alarm +GenericName[ca]=Planificador d'alarmes personals +GenericName[cs]=Osobní plánovač alarmů +GenericName[cy]=Trefnlennydd Larwm Personol +GenericName[da]=Personlig skemalægning af alarm +GenericName[de]=Persönliche Termin-Erinnerung +GenericName[el]=Προσωπικός προγραμματιστής ειδοποιήσεων +GenericName[eo]=Persona alarmplanilo +GenericName[es]=Planificador de alarmas personales +GenericName[et]=Meeldetuletuste ajastaja +GenericName[eu]=Alarma pertsonalen programatzailea +GenericName[fa]=زمانبند هشدار شخصی +GenericName[fi]=Henkilökohtainen hälytysajastin +GenericName[fr]=Planificateur d'alarme personnel +GenericName[fy]=Persoanlike alarmplanner +GenericName[gl]=Progamador Persoal de Alarmas +GenericName[he]=מנהל זמן אישי +GenericName[hi]=निजी अलार्म शेड्यूलर +GenericName[hu]=Emlékeztetőkezelő +GenericName[is]=Áminningakerfi +GenericName[it]=Programmatore degli avvisi personali +GenericName[ja]=個人アラームスケジューラ +GenericName[ka]=პერსონალური მაღვიძარას მგეგმავი +GenericName[kk]=Дербес ескертулер жоспарлағышы +GenericName[km]=កម្មវិធីកំណត់ម៉ោងរោទ៍ផ្ទាល់ខ្លួន +GenericName[lt]=Asmeninių žinučių-priminimų planuoklis +GenericName[mk]=Закажување лични аларми +GenericName[ms]=Penjadual Penggera Peribadi +GenericName[nb]=Personlig varslingsplanlegger +GenericName[nds]=Persöönlich Anstöötgever +GenericName[ne]=व्यक्तिगत संसूचक अनुसूचक +GenericName[nl]=Persoonlijke alarmplanner +GenericName[nn]=Planleggar for alarmar/påminningsmeldingar +GenericName[pl]=Program przypominający o zdarzeniach +GenericName[pt]=Gestor de Alarmes Pessoal +GenericName[pt_BR]=Agendador de Alarme Pessoal +GenericName[ru]=Напоминания +GenericName[sk]=Osobný plánovač alarmov +GenericName[sl]=Razporejevalnik osebnih alarmov +GenericName[sr]=Лични планер аларма +GenericName[sr@Latn]=Lični planer alarma +GenericName[sv]=Personlig alarmschemaläggning +GenericName[tg]=Идоракунии хотиррасониҳои шахсӣ +GenericName[tr]=Kişisel Alarm +GenericName[uk]=Персональний планувальник нагадувань +GenericName[zh_CN]=个人日程提醒 +GenericName[zh_TW]=個人鬧鐘排程程式 +Terminal=false +X-DCOP-ServiceType=Unique +X-KDE-StartupNotify=true +Categories=Qt;KDE;Utility;X-KDE-Utilities-PIM;Office;Calendar; diff --git a/kalarm/kalarm.h b/kalarm/kalarm.h new file mode 100644 index 000000000..e0af27f57 --- /dev/null +++ b/kalarm/kalarm.h @@ -0,0 +1,39 @@ +/* + * kalarm.h - global header file + * Program: kalarm + * Copyright © 2001-2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KALARM_H +#define KALARM_H + + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#define KALARM_VERSION "1.5.5" +#define KALARM_NAME "KAlarm" + +#include <kdeversion.h> + +#define AUTOSTART_BY_KALARMD // temporary fix for autostart before session restoration + +#define OLD_DCOP // retain DCOP pre-1.2 compatibility + +#endif // KALARM_H + diff --git a/kalarm/kalarm.tray.desktop b/kalarm/kalarm.tray.desktop new file mode 100644 index 000000000..d161dc67e --- /dev/null +++ b/kalarm/kalarm.tray.desktop @@ -0,0 +1,74 @@ +# KDE Config File +[Desktop Entry] +Name=KAlarm +Name[af]=K-alarm +Name[cy]=KLarwm +Name[eo]=Alarmilo +Name[hi]=के-अलार्म +Name[ko]=KDE 알람 +Name[mk]=КАларм +Name[ne]=केडीई संसूचक +Name[nso]=KAlamo +Name[pl]=Alarm +Name[sv]=Kalarm +Name[ta]=Kஅலாரம் +Name[th]=เตือนการนัดหมาย - K +Name[ven]=Alamu ya K +Name[zh_TW]=KAlarm 鬧鐘 +Exec=kalarm --tray +Icon=kalarm +Type=Application +DocPath=kalarm/index.html +Comment=Personal Alarm Scheduler: start as system tray icon +Comment[af]=Persoonlike alarm skeduleerder: begin as stelsel laai ikoon +Comment[bg]=Аларма: стартиране като икона в системния панел +Comment[bs]=Lični alarm: pokreni kao ikonu u panelu +Comment[ca]=Planificador d'alarma personal; s'inicia com a una icona a la safata del sistema +Comment[cs]=Plánovač alarmů a připomenutí: spustit v systémové liště +Comment[cy]=Trefnlennydd Larwm Personol: cychwynnwch fel eicon yn y cafn cysawd +Comment[da]=Skemalægger personlig alarm: start som statusikon. +Comment[de]=Erinnerungsfunktion im Systemabschnitt der Kontrollleiste +Comment[el]=Προσωπικός προγραμματιστής ειδοποιήσεων: εκκίνηση σαν εικονίδιο στο πλαίσιο συστήματος +Comment[es]=Programador de alarma personal: comenzar como icono de la bandeja del sistema +Comment[et]=Häirete ja meeldetuletuste ajakava: käivitamine süsteemse doki ikoonina +Comment[eu]=Alarma pertsonalen programatzailea: abiatu sistemaren bandejako ikono bezala +Comment[fa]=زمانبند هشدار شخصی: آغاز به عنوان شمایل سینی سیستم +Comment[fi]=Henkilökohtainen hälytysajastin: käynnistä paneelikuvake +Comment[fr]=Planificateur d'alarme personnel : démarre dans la boîte à miniatures +Comment[fy]=Persoanlike alarmplanner: begjinne yn it systeemfak +Comment[gl]=Programador persoal de alarmas: iniciar como icona na bandexa do sistema +Comment[he]=מתזמן הודעות תזכורת: הפעלה בתור סמל במגש המערכת +Comment[hi]=निजी अलार्म शेड्यूलरः तंत्र तश्तरी प्रतीक की तरह प्रारंभ हों +Comment[hu]=Az emlékeztető üzenetek megjelenítőprogramja: indítás a paneltálcában +Comment[is]=Áminningakerfi: ræsir sem táknmynd á spjaldinu +Comment[it]=Programmatore personale degli avvisi: all'avvio si mette nel vassoio di sistema +Comment[ja]=個人アラームスケジューラ: システムトレイアイコンで起動 +Comment[ka]=პესონალური მაღვიძარას მგეგმავი: სისტემურ პანელის ხატულად იწყება +Comment[kk]=Дербес ескертулер жоспарлағышы: жүйелік сөреде орналасады +Comment[km]=កម្មវិធីកំណត់ម៉ោងរោទ៍ផ្ទាល់ខ្លួន ៖ ចាប់ផ្ដើមជារូបតំណាងក្នុងថាសប្រព័ន្ធ +Comment[lt]=Žinučių-priminimų planuoklis: paleisti kaip sisteminio dėklo ženkliuką +Comment[mk]=Закажување лични аларми: стартувај во сис. лента +Comment[ms]=Penjadual Penggera Peribadi: mulakan sebagai ikon dulang sistem +Comment[nb]=Tidsplanlegger for alarm- og påminnelsesbeskjeder: start som ikon i systemkurven +Comment[nds]=Anstöötgever binnen den Systeemafsnitt starten +Comment[ne]=व्यक्तिगत संसूचक अनुसूचक: प्रणाली ट्रे प्रतिमा अनुरुप सुरु गर्नुहोस् +Comment[nl]=Persoonlijke alarmplanner: opstarten in systeemvak +Comment[nn]=Personleg alarmplanleggjar. Start som ikon i systemtrauet +Comment[pl]=Program przypominający o zdarzeniach: startuje w tacce systemowej +Comment[pt]=Escalonador de Alarmes Pessoal: iniciar como ícone da bandeja +Comment[pt_BR]=Agendador de Alarme Pessoal: iniciar como um ícone na bandeja do sistema +Comment[ru]=Персональный будильник (запускается в виде значка в панели задач) +Comment[sk]=Osobný plánovač alarmov: spustiť v systémovej lište +Comment[sl]=Razporejevalnik osebnih alarmov: zaženi kot ikono sistemske vrstice +Comment[sr]=Лични планер аларма: покреће се као икона у системској касети +Comment[sr@Latn]=Lični planer alarma: pokreće se kao ikona u sistemskoj kaseti +Comment[sv]=Personlig alarmschemaläggare: starta som en ikon i systembrickan +Comment[ta]=Personal Alarm Scheduler: கணினி தட்டு சின்னமாக துவங்கு +Comment[tg]=Соати рӯимизии зангдори шахсӣ (ба намуди ишорот дар панели вазифа ба кор дароварда мешавад) +Comment[tr]=Kişisel Alarm Zamanlayıcı +Comment[uk]=Персональний планувальник нагадувань: стартує як піктограма в лотку +Comment[zh_CN]=个人日程提醒程序:以系统托盘图标启动 +Comment[zh_TW]=警示/提醒訊息排程器:以系統列圖示的方式啟動 +Terminal=false +X-KDE-autostart-phase=2 +X-KDE-autostart-condition=kalarmrc:General:AutostartTrayDummy:false diff --git a/kalarm/kalarmapp.cpp b/kalarm/kalarmapp.cpp new file mode 100644 index 000000000..b222eeece --- /dev/null +++ b/kalarm/kalarmapp.cpp @@ -0,0 +1,2187 @@ +/* + * kalarmapp.cpp - the KAlarm application object + * Program: kalarm + * Copyright © 2001-2009 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <stdlib.h> +#include <ctype.h> +#include <iostream> + +#include <qobjectlist.h> +#include <qtimer.h> +#include <qregexp.h> +#include <qfile.h> + +#include <kcmdlineargs.h> +#include <klocale.h> +#include <kstandarddirs.h> +#include <kconfig.h> +#include <kaboutdata.h> +#include <dcopclient.h> +#include <kprocess.h> +#include <ktempfile.h> +#include <kfileitem.h> +#include <kstdguiitem.h> +#include <ktrader.h> +#include <kstaticdeleter.h> +#include <kdebug.h> + +#include <libkcal/calformat.h> + +#include <kalarmd/clientinfo.h> + +#include "alarmcalendar.h" +#include "alarmlistview.h" +#include "birthdaydlg.h" +#include "editdlg.h" +#include "daemon.h" +#include "dcophandler.h" +#include "functions.h" +#include "kamail.h" +#include "karecurrence.h" +#include "mainwindow.h" +#include "messagebox.h" +#include "messagewin.h" +#include "preferences.h" +#include "prefdlg.h" +#include "shellprocess.h" +#include "traywindow.h" +#include "kalarmapp.moc" + +#include <netwm.h> + + +static bool convWakeTime(const QCString& timeParam, QDateTime&, bool& noTime); +static bool convInterval(const QCString& timeParam, KARecurrence::Type&, int& timeInterval, bool allowMonthYear = false); + +/****************************************************************************** +* Find the maximum number of seconds late which a late-cancel alarm is allowed +* to be. This is calculated as the alarm daemon's check interval, plus a few +* seconds leeway to cater for any timing irregularities. +*/ +static inline int maxLateness(int lateCancel) +{ + static const int LATENESS_LEEWAY = 5; + int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0; + return Daemon::maxTimeSinceCheck() + LATENESS_LEEWAY + lc; +} + + +KAlarmApp* KAlarmApp::theInstance = 0; +int KAlarmApp::mActiveCount = 0; +int KAlarmApp::mFatalError = 0; +QString KAlarmApp::mFatalMessage; + + +/****************************************************************************** +* Construct the application. +*/ +KAlarmApp::KAlarmApp() + : KUniqueApplication(), + mInitialised(false), + mDcopHandler(new DcopHandler()), +#ifdef OLD_DCOP + mDcopHandlerOld(new DcopHandlerOld()), +#endif + mTrayWindow(0), + mPendingQuit(false), + mProcessingQueue(false), + mCheckingSystemTray(false), + mSessionClosingDown(false), + mRefreshExpiredAlarms(false), + mSpeechEnabled(false) +{ + Preferences::initialise(); + Preferences::connect(SIGNAL(preferencesChanged()), this, SLOT(slotPreferencesChanged())); + KCal::CalFormat::setApplication(aboutData()->programName(), AlarmCalendar::icalProductId()); + KARecurrence::setDefaultFeb29Type(Preferences::defaultFeb29Type()); + + // Check if the system tray is supported by this window manager + mHaveSystemTray = true; // assume yes in lieu of a test which works + + if (AlarmCalendar::initialiseCalendars()) + { + connect(AlarmCalendar::expiredCalendar(), SIGNAL(purged()), SLOT(slotExpiredPurged())); + + KConfig* config = kapp->config(); + config->setGroup(QString::fromLatin1("General")); + mNoSystemTray = config->readBoolEntry(QString::fromLatin1("NoSystemTray"), false); + mSavedNoSystemTray = mNoSystemTray; + mOldRunInSystemTray = wantRunInSystemTray(); + mDisableAlarmsIfStopped = mOldRunInSystemTray && !mNoSystemTray && Preferences::disableAlarmsIfStopped(); + mStartOfDay = Preferences::startOfDay(); + if (Preferences::hasStartOfDayChanged()) + mStartOfDay.setHMS(100,0,0); // start of day time has changed: flag it as invalid + DateTime::setStartOfDay(mStartOfDay); + mPrefsExpiredColour = Preferences::expiredColour(); + mPrefsExpiredKeepDays = Preferences::expiredKeepDays(); + } + + // Check if the speech synthesis daemon is installed + mSpeechEnabled = (KTrader::self()->query("DCOP/Text-to-Speech", "Name == 'KTTSD'").count() > 0); + if (!mSpeechEnabled) + kdDebug(5950) << "KAlarmApp::KAlarmApp(): speech synthesis disabled (KTTSD not found)" << endl; + // Check if KOrganizer is installed + QString korg = QString::fromLatin1("korganizer"); + mKOrganizerEnabled = !locate("exe", korg).isNull() || !KStandardDirs::findExe(korg).isNull(); + if (!mKOrganizerEnabled) + kdDebug(5950) << "KAlarmApp::KAlarmApp(): KOrganizer options disabled (KOrganizer not found)" << endl; +} + +/****************************************************************************** +*/ +KAlarmApp::~KAlarmApp() +{ + while (!mCommandProcesses.isEmpty()) + { + ProcData* pd = mCommandProcesses.first(); + mCommandProcesses.pop_front(); + delete pd; + } + AlarmCalendar::terminateCalendars(); +} + +/****************************************************************************** +* Return the one and only KAlarmApp instance. +* If it doesn't already exist, it is created first. +*/ +KAlarmApp* KAlarmApp::getInstance() +{ + if (!theInstance) + { + theInstance = new KAlarmApp; + + if (mFatalError) + theInstance->quitFatal(); + else + { + // This is here instead of in the constructor to avoid recursion + Daemon::initialise(); // calendars must be initialised before calling this + } + } + return theInstance; +} + +/****************************************************************************** +* Restore the saved session if required. +*/ +bool KAlarmApp::restoreSession() +{ + if (!isRestored()) + return false; + if (mFatalError) + { + quitFatal(); + return false; + } + + // Process is being restored by session management. + kdDebug(5950) << "KAlarmApp::restoreSession(): Restoring\n"; + ++mActiveCount; + if (!initCheck(true)) // open the calendar file (needed for main windows) + { + --mActiveCount; + quitIf(1, true); // error opening the main calendar - quit + return true; + } + MainWindow* trayParent = 0; + for (int i = 1; KMainWindow::canBeRestored(i); ++i) + { + QString type = KMainWindow::classNameOfToplevel(i); + if (type == QString::fromLatin1("MainWindow")) + { + MainWindow* win = MainWindow::create(true); + win->restore(i, false); + if (win->isHiddenTrayParent()) + trayParent = win; + else + win->show(); + } + else if (type == QString::fromLatin1("MessageWin")) + { + MessageWin* win = new MessageWin; + win->restore(i, false); + if (win->isValid()) + win->show(); + else + delete win; + } + } + initCheck(); // register with the alarm daemon + + // Try to display the system tray icon if it is configured to be autostarted, + // or if we're in run-in-system-tray mode. + if (Preferences::autostartTrayIcon() + || MainWindow::count() && wantRunInSystemTray()) + { + displayTrayIcon(true, trayParent); + // Occasionally for no obvious reason, the main main window is + // shown when it should be hidden, so hide it just to be sure. + if (trayParent) + trayParent->hide(); + } + + --mActiveCount; + quitIf(0); // quit if no windows are open + return true; +} + +/****************************************************************************** +* Called for a KUniqueApplication when a new instance of the application is +* started. +*/ +int KAlarmApp::newInstance() +{ + kdDebug(5950)<<"KAlarmApp::newInstance()\n"; + if (mFatalError) + { + quitFatal(); + return 1; + } + ++mActiveCount; + int exitCode = 0; // default = success + static bool firstInstance = true; + bool dontRedisplay = false; + if (!firstInstance || !isRestored()) + { + QString usage; + KCmdLineArgs* args = KCmdLineArgs::parsedArgs(); + + // Use a 'do' loop which is executed only once to allow easy error exits. + // Errors use 'break' to skip to the end of the function. + + // Note that DCOP handling is only set up once the command line parameters + // have been checked, since we mustn't register with the alarm daemon only + // to quit immediately afterwards. + do + { + #define USAGE(message) { usage = message; break; } + if (args->isSet("stop")) + { + // Stop the alarm daemon + kdDebug(5950)<<"KAlarmApp::newInstance(): stop\n"; + args->clear(); // free up memory + if (!Daemon::stop()) + { + exitCode = 1; + break; + } + dontRedisplay = true; // exit program if no other instances running + } + else + if (args->isSet("reset")) + { + // Reset the alarm daemon, if it's running. + // (If it's not running, it will reset automatically when it eventually starts.) + kdDebug(5950)<<"KAlarmApp::newInstance(): reset\n"; + args->clear(); // free up memory + Daemon::reset(); + dontRedisplay = true; // exit program if no other instances running + } + else + if (args->isSet("tray")) + { + // Display only the system tray icon + kdDebug(5950)<<"KAlarmApp::newInstance(): tray\n"; + args->clear(); // free up memory + if (!mHaveSystemTray) + { + exitCode = 1; + break; + } + if (!initCheck()) // open the calendar, register with daemon + { + exitCode = 1; + break; + } + if (!displayTrayIcon(true)) + { + exitCode = 1; + break; + } + } + else + if (args->isSet("handleEvent") || args->isSet("triggerEvent") || args->isSet("cancelEvent") || args->isSet("calendarURL")) + { + // Display or delete the event with the specified event ID + kdDebug(5950)<<"KAlarmApp::newInstance(): handle event\n"; + EventFunc function = EVENT_HANDLE; + int count = 0; + const char* option = 0; + if (args->isSet("handleEvent")) { function = EVENT_HANDLE; option = "handleEvent"; ++count; } + if (args->isSet("triggerEvent")) { function = EVENT_TRIGGER; option = "triggerEvent"; ++count; } + if (args->isSet("cancelEvent")) { function = EVENT_CANCEL; option = "cancelEvent"; ++count; } + if (!count) + USAGE(i18n("%1 requires %2, %3 or %4").arg(QString::fromLatin1("--calendarURL")).arg(QString::fromLatin1("--handleEvent")).arg(QString::fromLatin1("--triggerEvent")).arg(QString::fromLatin1("--cancelEvent"))) + if (count > 1) + USAGE(i18n("%1, %2, %3 mutually exclusive").arg(QString::fromLatin1("--handleEvent")).arg(QString::fromLatin1("--triggerEvent")).arg(QString::fromLatin1("--cancelEvent"))); + if (!initCheck(true)) // open the calendar, don't register with daemon yet + { + exitCode = 1; + break; + } + if (args->isSet("calendarURL")) + { + QString calendarUrl = args->getOption("calendarURL"); + if (KURL(calendarUrl).url() != AlarmCalendar::activeCalendar()->urlString()) + USAGE(i18n("%1: wrong calendar file").arg(QString::fromLatin1("--calendarURL"))) + } + QString eventID = args->getOption(option); + args->clear(); // free up memory + if (eventID.startsWith(QString::fromLatin1("ad:"))) + { + // It's a notification from the alarm deamon + eventID = eventID.mid(3); + Daemon::queueEvent(eventID); + } + setUpDcop(); // start processing DCOP calls + if (!handleEvent(eventID, function)) + { + exitCode = 1; + break; + } + } + else + if (args->isSet("edit")) + { + QString eventID = args->getOption("edit"); + if (!initCheck()) + { + exitCode = 1; + break; + } + if (!KAlarm::edit(eventID)) + { + USAGE(i18n("%1: Event %2 not found, or not editable").arg(QString::fromLatin1("--edit")).arg(eventID)) + exitCode = 1; + break; + } + } + else + if (args->isSet("edit-new") || args->isSet("edit-new-preset")) + { + QString templ; + if (args->isSet("edit-new-preset")) + templ = args->getOption("edit-new-preset"); + if (!initCheck()) + { + exitCode = 1; + break; + } + KAlarm::editNew(templ); + } + else + if (args->isSet("file") || args->isSet("exec") || args->isSet("mail") || args->count()) + { + // Display a message or file, execute a command, or send an email + KAEvent::Action action = KAEvent::MESSAGE; + QCString alMessage; + uint alFromID = 0; + EmailAddressList alAddresses; + QStringList alAttachments; + QCString alSubject; + if (args->isSet("file")) + { + kdDebug(5950)<<"KAlarmApp::newInstance(): file\n"; + if (args->isSet("exec")) + USAGE(i18n("%1 incompatible with %2").arg(QString::fromLatin1("--exec")).arg(QString::fromLatin1("--file"))) + if (args->isSet("mail")) + USAGE(i18n("%1 incompatible with %2").arg(QString::fromLatin1("--mail")).arg(QString::fromLatin1("--file"))) + if (args->count()) + USAGE(i18n("message incompatible with %1").arg(QString::fromLatin1("--file"))) + alMessage = args->getOption("file"); + action = KAEvent::FILE; + } + else if (args->isSet("exec")) + { + kdDebug(5950)<<"KAlarmApp::newInstance(): exec\n"; + if (args->isSet("mail")) + USAGE(i18n("%1 incompatible with %2").arg(QString::fromLatin1("--mail")).arg(QString::fromLatin1("--exec"))) + alMessage = args->getOption("exec"); + int n = args->count(); + for (int i = 0; i < n; ++i) + { + alMessage += ' '; + alMessage += args->arg(i); + } + action = KAEvent::COMMAND; + } + else if (args->isSet("mail")) + { + kdDebug(5950)<<"KAlarmApp::newInstance(): mail\n"; + if (args->isSet("subject")) + alSubject = args->getOption("subject"); + if (args->isSet("from-id")) + alFromID = KAMail::identityUoid(args->getOption("from-id")); + QCStringList params = args->getOptionList("mail"); + for (QCStringList::Iterator i = params.begin(); i != params.end(); ++i) + { + QString addr = QString::fromLocal8Bit(*i); + if (!KAMail::checkAddress(addr)) + USAGE(i18n("%1: invalid email address").arg(QString::fromLatin1("--mail"))) + alAddresses += KCal::Person(QString::null, addr); + } + params = args->getOptionList("attach"); + for (QCStringList::Iterator i = params.begin(); i != params.end(); ++i) + alAttachments += QString::fromLocal8Bit(*i); + alMessage = args->arg(0); + action = KAEvent::EMAIL; + } + else + { + kdDebug(5950)<<"KAlarmApp::newInstance(): message\n"; + alMessage = args->arg(0); + } + + if (action != KAEvent::EMAIL) + { + if (args->isSet("subject")) + USAGE(i18n("%1 requires %2").arg(QString::fromLatin1("--subject")).arg(QString::fromLatin1("--mail"))) + if (args->isSet("from-id")) + USAGE(i18n("%1 requires %2").arg(QString::fromLatin1("--from-id")).arg(QString::fromLatin1("--mail"))) + if (args->isSet("attach")) + USAGE(i18n("%1 requires %2").arg(QString::fromLatin1("--attach")).arg(QString::fromLatin1("--mail"))) + if (args->isSet("bcc")) + USAGE(i18n("%1 requires %2").arg(QString::fromLatin1("--bcc")).arg(QString::fromLatin1("--mail"))) + } + + bool alarmNoTime = false; + QDateTime alarmTime, endTime; + QColor bgColour = Preferences::defaultBgColour(); + QColor fgColour = Preferences::defaultFgColour(); + KARecurrence recurrence; + int repeatCount = 0; + int repeatInterval = 0; + if (args->isSet("color")) + { + // Background colour is specified + QCString colourText = args->getOption("color"); + if (static_cast<const char*>(colourText)[0] == '0' + && tolower(static_cast<const char*>(colourText)[1]) == 'x') + colourText.replace(0, 2, "#"); + bgColour.setNamedColor(colourText); + if (!bgColour.isValid()) + USAGE(i18n("Invalid %1 parameter").arg(QString::fromLatin1("--color"))) + } + if (args->isSet("colorfg")) + { + // Foreground colour is specified + QCString colourText = args->getOption("colorfg"); + if (static_cast<const char*>(colourText)[0] == '0' + && tolower(static_cast<const char*>(colourText)[1]) == 'x') + colourText.replace(0, 2, "#"); + fgColour.setNamedColor(colourText); + if (!fgColour.isValid()) + USAGE(i18n("Invalid %1 parameter").arg(QString::fromLatin1("--colorfg"))) + } + + if (args->isSet("time")) + { + QCString dateTime = args->getOption("time"); + if (!convWakeTime(dateTime, alarmTime, alarmNoTime)) + USAGE(i18n("Invalid %1 parameter").arg(QString::fromLatin1("--time"))) + } + else + alarmTime = QDateTime::currentDateTime(); + + bool haveRecurrence = args->isSet("recurrence"); + if (haveRecurrence) + { + if (args->isSet("login")) + USAGE(i18n("%1 incompatible with %2").arg(QString::fromLatin1("--login")).arg(QString::fromLatin1("--recurrence"))) + if (args->isSet("until")) + USAGE(i18n("%1 incompatible with %2").arg(QString::fromLatin1("--until")).arg(QString::fromLatin1("--recurrence"))) + QCString rule = args->getOption("recurrence"); + recurrence.set(QString::fromLocal8Bit(static_cast<const char*>(rule))); + } + if (args->isSet("interval")) + { + // Repeat count is specified + int count; + if (args->isSet("login")) + USAGE(i18n("%1 incompatible with %2").arg(QString::fromLatin1("--login")).arg(QString::fromLatin1("--interval"))) + bool ok; + if (args->isSet("repeat")) + { + count = args->getOption("repeat").toInt(&ok); + if (!ok || !count || count < -1 || (count < 0 && haveRecurrence)) + USAGE(i18n("Invalid %1 parameter").arg(QString::fromLatin1("--repeat"))) + } + else if (haveRecurrence) + USAGE(i18n("%1 requires %2").arg(QString::fromLatin1("--interval")).arg(QString::fromLatin1("--repeat"))) + else if (args->isSet("until")) + { + count = 0; + QCString dateTime = args->getOption("until"); + if (!convWakeTime(dateTime, endTime, alarmNoTime)) + USAGE(i18n("Invalid %1 parameter").arg(QString::fromLatin1("--until"))) + if (endTime < alarmTime) + USAGE(i18n("%1 earlier than %2").arg(QString::fromLatin1("--until")).arg(QString::fromLatin1("--time"))) + } + else + count = -1; + + // Get the recurrence interval + int interval; + KARecurrence::Type recurType; + if (!convInterval(args->getOption("interval"), recurType, interval, !haveRecurrence) + || interval < 0) + USAGE(i18n("Invalid %1 parameter").arg(QString::fromLatin1("--interval"))) + if (alarmNoTime && recurType == KARecurrence::MINUTELY) + USAGE(i18n("Invalid %1 parameter for date-only alarm").arg(QString::fromLatin1("--interval"))) + + if (haveRecurrence) + { + // There is a also a recurrence specified, so set up a sub-repetition + int longestInterval = recurrence.longestInterval(); + if (count * interval > longestInterval) + USAGE(i18n("Invalid %1 and %2 parameters: repetition is longer than %3 interval").arg(QString::fromLatin1("--interval")).arg(QString::fromLatin1("--repeat")).arg(QString::fromLatin1("--recurrence"))); + repeatCount = count; + repeatInterval = interval; + } + else + { + // There is no other recurrence specified, so convert the repetition + // parameters into a KCal::Recurrence + recurrence.set(recurType, interval, count, DateTime(alarmTime, alarmNoTime), endTime); + } + } + else + { + if (args->isSet("repeat")) + USAGE(i18n("%1 requires %2").arg(QString::fromLatin1("--repeat")).arg(QString::fromLatin1("--interval"))) + if (args->isSet("until")) + USAGE(i18n("%1 requires %2").arg(QString::fromLatin1("--until")).arg(QString::fromLatin1("--interval"))) + } + + QCString audioFile; + float audioVolume = -1; +#ifdef WITHOUT_ARTS + bool audioRepeat = false; +#else + bool audioRepeat = args->isSet("play-repeat"); +#endif + if (audioRepeat || args->isSet("play")) + { + // Play a sound with the alarm + if (audioRepeat && args->isSet("play")) + USAGE(i18n("%1 incompatible with %2").arg(QString::fromLatin1("--play")).arg(QString::fromLatin1("--play-repeat"))) + if (args->isSet("beep")) + USAGE(i18n("%1 incompatible with %2").arg(QString::fromLatin1("--beep")).arg(QString::fromLatin1(audioRepeat ? "--play-repeat" : "--play"))) + if (args->isSet("speak")) + USAGE(i18n("%1 incompatible with %2").arg(QString::fromLatin1("--speak")).arg(QString::fromLatin1(audioRepeat ? "--play-repeat" : "--play"))) + audioFile = args->getOption(audioRepeat ? "play-repeat" : "play"); +#ifndef WITHOUT_ARTS + if (args->isSet("volume")) + { + bool ok; + int volumepc = args->getOption("volume").toInt(&ok); + if (!ok || volumepc < 0 || volumepc > 100) + USAGE(i18n("Invalid %1 parameter").arg(QString::fromLatin1("--volume"))) + audioVolume = static_cast<float>(volumepc) / 100; + } +#endif + } +#ifndef WITHOUT_ARTS + else if (args->isSet("volume")) + USAGE(i18n("%1 requires %2 or %3").arg(QString::fromLatin1("--volume")).arg(QString::fromLatin1("--play")).arg(QString::fromLatin1("--play-repeat"))) +#endif + if (args->isSet("speak")) + { + if (args->isSet("beep")) + USAGE(i18n("%1 incompatible with %2").arg(QString::fromLatin1("--beep")).arg(QString::fromLatin1("--speak"))) + if (!mSpeechEnabled) + USAGE(i18n("%1 requires speech synthesis to be configured using KTTSD").arg(QString::fromLatin1("--speak"))) + } + int reminderMinutes = 0; + bool onceOnly = args->isSet("reminder-once"); + if (args->isSet("reminder") || onceOnly) + { + // Issue a reminder alarm in advance of the main alarm + if (onceOnly && args->isSet("reminder")) + USAGE(i18n("%1 incompatible with %2").arg(QString::fromLatin1("--reminder")).arg(QString::fromLatin1("--reminder-once"))) + QString opt = onceOnly ? QString::fromLatin1("--reminder-once") : QString::fromLatin1("--reminder"); + if (args->isSet("exec")) + USAGE(i18n("%1 incompatible with %2").arg(opt).arg(QString::fromLatin1("--exec"))) + if (args->isSet("mail")) + USAGE(i18n("%1 incompatible with %2").arg(opt).arg(QString::fromLatin1("--mail"))) + KARecurrence::Type recurType; + QString optval = args->getOption(onceOnly ? "reminder-once" : "reminder"); + if (!convInterval(args->getOption(onceOnly ? "reminder-once" : "reminder"), recurType, reminderMinutes)) + USAGE(i18n("Invalid %1 parameter").arg(opt)) + if (recurType == KARecurrence::MINUTELY && alarmNoTime) + USAGE(i18n("Invalid %1 parameter for date-only alarm").arg(opt)) + } + + int lateCancel = 0; + if (args->isSet("late-cancel")) + { + KARecurrence::Type recurType; + bool ok = convInterval(args->getOption("late-cancel"), recurType, lateCancel); + if (!ok || lateCancel <= 0) + USAGE(i18n("Invalid %1 parameter").arg(QString::fromLatin1("late-cancel"))) + } + else if (args->isSet("auto-close")) + USAGE(i18n("%1 requires %2").arg(QString::fromLatin1("--auto-close")).arg(QString::fromLatin1("--late-cancel"))) + + int flags = KAEvent::DEFAULT_FONT; + if (args->isSet("ack-confirm")) + flags |= KAEvent::CONFIRM_ACK; + if (args->isSet("auto-close")) + flags |= KAEvent::AUTO_CLOSE; + if (args->isSet("beep")) + flags |= KAEvent::BEEP; + if (args->isSet("speak")) + flags |= KAEvent::SPEAK; + if (args->isSet("korganizer")) + flags |= KAEvent::COPY_KORGANIZER; + if (args->isSet("disable")) + flags |= KAEvent::DISABLED; + if (audioRepeat) + flags |= KAEvent::REPEAT_SOUND; + if (args->isSet("login")) + flags |= KAEvent::REPEAT_AT_LOGIN; + if (args->isSet("bcc")) + flags |= KAEvent::EMAIL_BCC; + if (alarmNoTime) + flags |= KAEvent::ANY_TIME; + args->clear(); // free up memory + + // Display or schedule the event + if (!initCheck()) + { + exitCode = 1; + break; + } + if (!scheduleEvent(action, alMessage, alarmTime, lateCancel, flags, bgColour, fgColour, QFont(), audioFile, + audioVolume, reminderMinutes, recurrence, repeatInterval, repeatCount, + alFromID, alAddresses, alSubject, alAttachments)) + { + exitCode = 1; + break; + } + } + else + { + // No arguments - run interactively & display the main window + kdDebug(5950)<<"KAlarmApp::newInstance(): interactive\n"; + if (args->isSet("ack-confirm")) + usage += QString::fromLatin1("--ack-confirm "); + if (args->isSet("attach")) + usage += QString::fromLatin1("--attach "); + if (args->isSet("auto-close")) + usage += QString::fromLatin1("--auto-close "); + if (args->isSet("bcc")) + usage += QString::fromLatin1("--bcc "); + if (args->isSet("beep")) + usage += QString::fromLatin1("--beep "); + if (args->isSet("color")) + usage += QString::fromLatin1("--color "); + if (args->isSet("colorfg")) + usage += QString::fromLatin1("--colorfg "); + if (args->isSet("disable")) + usage += QString::fromLatin1("--disable "); + if (args->isSet("from-id")) + usage += QString::fromLatin1("--from-id "); + if (args->isSet("korganizer")) + usage += QString::fromLatin1("--korganizer "); + if (args->isSet("late-cancel")) + usage += QString::fromLatin1("--late-cancel "); + if (args->isSet("login")) + usage += QString::fromLatin1("--login "); + if (args->isSet("play")) + usage += QString::fromLatin1("--play "); +#ifndef WITHOUT_ARTS + if (args->isSet("play-repeat")) + usage += QString::fromLatin1("--play-repeat "); +#endif + if (args->isSet("reminder")) + usage += QString::fromLatin1("--reminder "); + if (args->isSet("reminder-once")) + usage += QString::fromLatin1("--reminder-once "); + if (args->isSet("speak")) + usage += QString::fromLatin1("--speak "); + if (args->isSet("subject")) + usage += QString::fromLatin1("--subject "); + if (args->isSet("time")) + usage += QString::fromLatin1("--time "); +#ifndef WITHOUT_ARTS + if (args->isSet("volume")) + usage += QString::fromLatin1("--volume "); +#endif + if (!usage.isEmpty()) + { + usage += i18n(": option(s) only valid with a message/%1/%2").arg(QString::fromLatin1("--file")).arg(QString::fromLatin1("--exec")); + break; + } + + args->clear(); // free up memory + if (!initCheck()) + { + exitCode = 1; + break; + } + + (MainWindow::create())->show(); + } + } while (0); // only execute once + + if (!usage.isEmpty()) + { + // Note: we can't use args->usage() since that also quits any other + // running 'instances' of the program. + std::cerr << usage.local8Bit().data() + << i18n("\nUse --help to get a list of available command line options.\n").local8Bit().data(); + exitCode = 1; + } + } + if (firstInstance && !dontRedisplay && !exitCode) + redisplayAlarms(); + + --mActiveCount; + firstInstance = false; + + // Quit the application if this was the last/only running "instance" of the program. + // Executing 'return' doesn't work very well since the program continues to + // run if no windows were created. + quitIf(exitCode); + return exitCode; +} + +/****************************************************************************** +* Quit the program, optionally only if there are no more "instances" running. +*/ +void KAlarmApp::quitIf(int exitCode, bool force) +{ + if (force) + { + // Quit regardless, except for message windows + MainWindow::closeAll(); + displayTrayIcon(false); + if (MessageWin::instanceCount()) + return; + } + else + { + // Quit only if there are no more "instances" running + mPendingQuit = false; + if (mActiveCount > 0 || MessageWin::instanceCount()) + return; + int mwcount = MainWindow::count(); + MainWindow* mw = mwcount ? MainWindow::firstWindow() : 0; + if (mwcount > 1 || mwcount && (!mw->isHidden() || !mw->isTrayParent())) + return; + // There are no windows left except perhaps a main window which is a hidden tray icon parent + if (mTrayWindow) + { + // There is a system tray icon. + // Don't exit unless the system tray doesn't seem to exist. + if (checkSystemTray()) + return; + } + if (!mDcopQueue.isEmpty() || !mCommandProcesses.isEmpty()) + { + // Don't quit yet if there are outstanding actions on the DCOP queue + mPendingQuit = true; + mPendingQuitCode = exitCode; + return; + } + } + + // This was the last/only running "instance" of the program, so exit completely. + kdDebug(5950) << "KAlarmApp::quitIf(" << exitCode << "): quitting" << endl; + BirthdayDlg::close(); + exit(exitCode); +} + +/****************************************************************************** +* Called when the Quit menu item is selected. +* Closes the system tray window and all main windows, but does not exit the +* program if other windows are still open. +*/ +void KAlarmApp::doQuit(QWidget* parent) +{ + kdDebug(5950) << "KAlarmApp::doQuit()\n"; + if (mDisableAlarmsIfStopped + && MessageBox::warningContinueCancel(parent, KMessageBox::Cancel, + i18n("Quitting will disable alarms\n(once any alarm message windows are closed)."), + QString::null, KStdGuiItem::quit(), Preferences::QUIT_WARN + ) != KMessageBox::Yes) + return; + quitIf(0, true); +} + +/****************************************************************************** +* Called when the session manager is about to close down the application. +*/ +void KAlarmApp::commitData(QSessionManager& sm) +{ + mSessionClosingDown = true; + KUniqueApplication::commitData(sm); + mSessionClosingDown = false; // reset in case shutdown is cancelled +} + +/****************************************************************************** +* Display an error message for a fatal error. Prevent further actions since +* the program state is unsafe. +*/ +void KAlarmApp::displayFatalError(const QString& message) +{ + if (!mFatalError) + { + mFatalError = 1; + mFatalMessage = message; + if (theInstance) + QTimer::singleShot(0, theInstance, SLOT(quitFatal())); + } +} + +/****************************************************************************** +* Quit the program, once the fatal error message has been acknowledged. +*/ +void KAlarmApp::quitFatal() +{ + switch (mFatalError) + { + case 0: + case 2: + return; + case 1: + mFatalError = 2; + KMessageBox::error(0, mFatalMessage); + mFatalError = 3; + // fall through to '3' + case 3: + if (theInstance) + theInstance->quitIf(1, true); + break; + } + QTimer::singleShot(1000, this, SLOT(quitFatal())); +} + +/****************************************************************************** +* The main processing loop for KAlarm. +* All KAlarm operations involving opening or updating calendar files are called +* from this loop to ensure that only one operation is active at any one time. +* This precaution is necessary because KAlarm's activities are mostly +* asynchronous, being in response to DCOP calls from the alarm daemon (or other +* programs) or timer events, any of which can be received in the middle of +* performing another operation. If a calendar file is opened or updated while +* another calendar operation is in progress, the program has been observed to +* hang, or the first calendar call has failed with data loss - clearly +* unacceptable!! +*/ +void KAlarmApp::processQueue() +{ + if (mInitialised && !mProcessingQueue) + { + kdDebug(5950) << "KAlarmApp::processQueue()\n"; + mProcessingQueue = true; + + // Reset the alarm daemon if it's been queued + KAlarm::resetDaemonIfQueued(); + + // Process DCOP calls + while (!mDcopQueue.isEmpty()) + { + DcopQEntry& entry = mDcopQueue.first(); + if (entry.eventId.isEmpty()) + { + // It's a new alarm + switch (entry.function) + { + case EVENT_TRIGGER: + execAlarm(entry.event, entry.event.firstAlarm(), false); + break; + case EVENT_HANDLE: + KAlarm::addEvent(entry.event, 0); + break; + case EVENT_CANCEL: + break; + } + } + else + handleEvent(entry.eventId, entry.function); + mDcopQueue.pop_front(); + } + + // Purge the expired alarms calendar if it's time to do so + AlarmCalendar::expiredCalendar()->purgeIfQueued(); + + // Now that the queue has been processed, quit if a quit was queued + if (mPendingQuit) + quitIf(mPendingQuitCode); + + mProcessingQueue = false; + } +} + +/****************************************************************************** +* Redisplay alarms which were being shown when the program last exited. +* Normally, these alarms will have been displayed by session restoration, but +* if the program crashed or was killed, we can redisplay them here so that +* they won't be lost. +*/ +void KAlarmApp::redisplayAlarms() +{ + AlarmCalendar* cal = AlarmCalendar::displayCalendar(); + if (cal->isOpen()) + { + KCal::Event::List events = cal->events(); + for (KCal::Event::List::ConstIterator it = events.begin(); it != events.end(); ++it) + { + KCal::Event* kcalEvent = *it; + KAEvent event(*kcalEvent); + event.setUid(KAEvent::ACTIVE); + if (!MessageWin::findEvent(event.id())) + { + // This event should be displayed, but currently isn't being + kdDebug(5950) << "KAlarmApp::redisplayAlarms(): " << event.id() << endl; + KAAlarm alarm = event.convertDisplayingAlarm(); + (new MessageWin(event, alarm, false, !alarm.repeatAtLogin()))->show(); + } + } + } +} + +/****************************************************************************** +* Called when the system tray main window is closed. +*/ +void KAlarmApp::removeWindow(TrayWindow*) +{ + mTrayWindow = 0; + quitIf(); +} + +/****************************************************************************** +* Display or close the system tray icon. +*/ +bool KAlarmApp::displayTrayIcon(bool show, MainWindow* parent) +{ + static bool creating = false; + if (show) + { + if (!mTrayWindow && !creating) + { + if (!mHaveSystemTray) + return false; + if (!MainWindow::count() && wantRunInSystemTray()) + { + creating = true; // prevent main window constructor from creating an additional tray icon + parent = MainWindow::create(); + creating = false; + } + mTrayWindow = new TrayWindow(parent ? parent : MainWindow::firstWindow()); + connect(mTrayWindow, SIGNAL(deleted()), SIGNAL(trayIconToggled())); + mTrayWindow->show(); + emit trayIconToggled(); + + // Set up a timer so that we can check after all events in the window system's + // event queue have been processed, whether the system tray actually exists + mCheckingSystemTray = true; + mSavedNoSystemTray = mNoSystemTray; + mNoSystemTray = false; + QTimer::singleShot(0, this, SLOT(slotSystemTrayTimer())); + } + } + else if (mTrayWindow) + { + delete mTrayWindow; + mTrayWindow = 0; + } + return true; +} + +/****************************************************************************** +* Called by a timer to check whether the system tray icon has been housed in +* the system tray. Because there is a delay between the system tray icon show +* event and the icon being reparented by the system tray, we have to use a +* timer to check whether the system tray has actually grabbed it, or whether +* the system tray probably doesn't exist. +*/ +void KAlarmApp::slotSystemTrayTimer() +{ + mCheckingSystemTray = false; + if (!checkSystemTray()) + quitIf(0); // exit the application if there are no open windows +} + +/****************************************************************************** +* Check whether the system tray icon has been housed in the system tray. +* If the system tray doesn't seem to exist, tell the alarm daemon to notify us +* of alarms regardless of whether we're running. +*/ +bool KAlarmApp::checkSystemTray() +{ + if (mCheckingSystemTray || !mTrayWindow) + return true; + if (mTrayWindow->inSystemTray() != !mSavedNoSystemTray) + { + kdDebug(5950) << "KAlarmApp::checkSystemTray(): changed -> " << mSavedNoSystemTray << endl; + mNoSystemTray = mSavedNoSystemTray = !mSavedNoSystemTray; + + // Store the new setting in the config file, so that if KAlarm exits and is then + // next activated by the daemon to display a message, it will register with the + // daemon with the correct NOTIFY type. If that happened when there was no system + // tray and alarms are disabled when KAlarm is not running, registering with + // NO_START_NOTIFY could result in alarms never being seen. + KConfig* config = kapp->config(); + config->setGroup(QString::fromLatin1("General")); + config->writeEntry(QString::fromLatin1("NoSystemTray"), mNoSystemTray); + config->sync(); + + // Update other settings and reregister with the alarm daemon + slotPreferencesChanged(); + } + else + { + kdDebug(5950) << "KAlarmApp::checkSystemTray(): no change = " << !mSavedNoSystemTray << endl; + mNoSystemTray = mSavedNoSystemTray; + } + return !mNoSystemTray; +} + +/****************************************************************************** +* Return the main window associated with the system tray icon. +*/ +MainWindow* KAlarmApp::trayMainWindow() const +{ + return mTrayWindow ? mTrayWindow->assocMainWindow() : 0; +} + +/****************************************************************************** +* Called when KAlarm preferences have changed. +*/ +void KAlarmApp::slotPreferencesChanged() +{ + bool newRunInSysTray = wantRunInSystemTray(); + if (newRunInSysTray != mOldRunInSystemTray) + { + // The system tray run mode has changed + ++mActiveCount; // prevent the application from quitting + MainWindow* win = mTrayWindow ? mTrayWindow->assocMainWindow() : 0; + delete mTrayWindow; // remove the system tray icon if it is currently shown + mTrayWindow = 0; + mOldRunInSystemTray = newRunInSysTray; + if (!newRunInSysTray) + { + if (win && win->isHidden()) + delete win; + } + displayTrayIcon(true); + --mActiveCount; + } + + bool newDisableIfStopped = wantRunInSystemTray() && !mNoSystemTray && Preferences::disableAlarmsIfStopped(); + if (newDisableIfStopped != mDisableAlarmsIfStopped) + { + mDisableAlarmsIfStopped = newDisableIfStopped; // N.B. this setting is used by Daemon::reregister() + Preferences::setQuitWarn(true); // since mode has changed, re-allow warning messages on Quit + Daemon::reregister(); // re-register with the alarm daemon + } + + // Change alarm times for date-only alarms if the start of day time has changed + if (Preferences::startOfDay() != mStartOfDay) + changeStartOfDay(); + + // In case the date for February 29th recurrences has changed + KARecurrence::setDefaultFeb29Type(Preferences::defaultFeb29Type()); + + if (Preferences::expiredColour() != mPrefsExpiredColour) + { + // The expired alarms text colour has changed + mRefreshExpiredAlarms = true; + mPrefsExpiredColour = Preferences::expiredColour(); + } + + if (Preferences::expiredKeepDays() != mPrefsExpiredKeepDays) + { + // How long expired alarms are being kept has changed. + // N.B. This also adjusts for any change in start-of-day time. + mPrefsExpiredKeepDays = Preferences::expiredKeepDays(); + AlarmCalendar::expiredCalendar()->setPurgeDays(mPrefsExpiredKeepDays); + } + + if (mRefreshExpiredAlarms) + { + mRefreshExpiredAlarms = false; + MainWindow::updateExpired(); + } +} + +/****************************************************************************** +* Change alarm times for date-only alarms after the start of day time has changed. +*/ +void KAlarmApp::changeStartOfDay() +{ + Daemon::notifyTimeChanged(); // tell the alarm daemon the new time + QTime sod = Preferences::startOfDay(); + DateTime::setStartOfDay(sod); + AlarmCalendar* cal = AlarmCalendar::activeCalendar(); + if (KAEvent::adjustStartOfDay(cal->events())) + cal->save(); + Preferences::updateStartOfDayCheck(); // now that calendar is updated, set OK flag in config file + mStartOfDay = sod; +} + +/****************************************************************************** +* Called when the expired alarms calendar has been purged. +* Updates the alarm list in all main windows. +*/ +void KAlarmApp::slotExpiredPurged() +{ + mRefreshExpiredAlarms = false; + MainWindow::updateExpired(); +} + +/****************************************************************************** +* Return whether the program is configured to be running in the system tray. +*/ +bool KAlarmApp::wantRunInSystemTray() const +{ + return Preferences::runInSystemTray() && mHaveSystemTray; +} + +/****************************************************************************** +* Called to schedule a new alarm, either in response to a DCOP notification or +* to command line options. +* Reply = true unless there was a parameter error or an error opening calendar file. +*/ +bool KAlarmApp::scheduleEvent(KAEvent::Action action, const QString& text, const QDateTime& dateTime, + int lateCancel, int flags, const QColor& bg, const QColor& fg, const QFont& font, + const QString& audioFile, float audioVolume, int reminderMinutes, + const KARecurrence& recurrence, int repeatInterval, int repeatCount, + uint mailFromID, const EmailAddressList& mailAddresses, + const QString& mailSubject, const QStringList& mailAttachments) +{ + kdDebug(5950) << "KAlarmApp::scheduleEvent(): " << text << endl; + if (!dateTime.isValid()) + return false; + QDateTime now = QDateTime::currentDateTime(); + if (lateCancel && dateTime < now.addSecs(-maxLateness(lateCancel))) + return true; // alarm time was already expired too long ago + QDateTime alarmTime = dateTime; + // Round down to the nearest minute to avoid scheduling being messed up + alarmTime.setTime(QTime(alarmTime.time().hour(), alarmTime.time().minute(), 0)); + + KAEvent event(alarmTime, text, bg, fg, font, action, lateCancel, flags); + if (reminderMinutes) + { + bool onceOnly = (reminderMinutes < 0); + event.setReminder((onceOnly ? -reminderMinutes : reminderMinutes), onceOnly); + } + if (!audioFile.isEmpty()) + event.setAudioFile(audioFile, audioVolume, -1, 0); + if (!mailAddresses.isEmpty()) + event.setEmail(mailFromID, mailAddresses, mailSubject, mailAttachments); + event.setRecurrence(recurrence); + event.setFirstRecurrence(); + event.setRepetition(repeatInterval, repeatCount - 1); + if (alarmTime <= now) + { + // Alarm is due for display already. + // First execute it once without adding it to the calendar file. + if (!mInitialised) + mDcopQueue.append(DcopQEntry(event, EVENT_TRIGGER)); + else + execAlarm(event, event.firstAlarm(), false); + // If it's a recurring alarm, reschedule it for its next occurrence + if (!event.recurs() + || event.setNextOccurrence(now) == KAEvent::NO_OCCURRENCE) + return true; + // It has recurrences in the future + } + + // Queue the alarm for insertion into the calendar file + mDcopQueue.append(DcopQEntry(event)); + if (mInitialised) + QTimer::singleShot(0, this, SLOT(processQueue())); + return true; +} + +/****************************************************************************** +* Called in response to a DCOP notification by the alarm daemon that an event +* should be handled, i.e. displayed or cancelled. +* Optionally display the event. Delete the event from the calendar file and +* from every main window instance. +*/ +bool KAlarmApp::handleEvent(const QString& urlString, const QString& eventID, EventFunc function) +{ + kdDebug(5950) << "KAlarmApp::handleEvent(DCOP): " << eventID << endl; + AlarmCalendar* cal = AlarmCalendar::activeCalendar(); // this can be called before calendars have been initialised + if (cal && KURL(urlString).url() != cal->urlString()) + { + kdError(5950) << "KAlarmApp::handleEvent(DCOP): wrong calendar file " << urlString << endl; + Daemon::eventHandled(eventID, false); + return false; + } + mDcopQueue.append(DcopQEntry(function, eventID)); + if (mInitialised) + QTimer::singleShot(0, this, SLOT(processQueue())); + return true; +} + +/****************************************************************************** +* Either: +* a) Display the event and then delete it if it has no outstanding repetitions. +* b) Delete the event. +* c) Reschedule the event for its next repetition. If none remain, delete it. +* If the event is deleted, it is removed from the calendar file and from every +* main window instance. +*/ +bool KAlarmApp::handleEvent(const QString& eventID, EventFunc function) +{ + kdDebug(5950) << "KAlarmApp::handleEvent(): " << eventID << ", " << (function==EVENT_TRIGGER?"TRIGGER":function==EVENT_CANCEL?"CANCEL":function==EVENT_HANDLE?"HANDLE":"?") << endl; + KCal::Event* kcalEvent = AlarmCalendar::activeCalendar()->event(eventID); + if (!kcalEvent) + { + kdError(5950) << "KAlarmApp::handleEvent(): event ID not found: " << eventID << endl; + Daemon::eventHandled(eventID, false); + return false; + } + KAEvent event(*kcalEvent); + switch (function) + { + case EVENT_CANCEL: + KAlarm::deleteEvent(event, true); + break; + + case EVENT_TRIGGER: // handle it if it's due, else execute it regardless + case EVENT_HANDLE: // handle it if it's due + { + QDateTime now = QDateTime::currentDateTime(); + bool updateCalAndDisplay = false; + bool alarmToExecuteValid = false; + KAAlarm alarmToExecute; + // Check all the alarms in turn. + // Note that the main alarm is fetched before any other alarms. + for (KAAlarm alarm = event.firstAlarm(); alarm.valid(); alarm = event.nextAlarm(alarm)) + { + // Check if the alarm is due yet. + int secs = alarm.dateTime(true).dateTime().secsTo(now); + if (secs < 0) + { + // This alarm is definitely not due yet + kdDebug(5950) << "KAlarmApp::handleEvent(): alarm " << alarm.type() << ": not due\n"; + continue; + } + if (alarm.repeatAtLogin()) + { + // Alarm is to be displayed at every login. + // Check if the alarm has only just been set up. + // (The alarm daemon will immediately notify that it is due + // since it is set up with a time in the past.) + kdDebug(5950) << "KAlarmApp::handleEvent(): REPEAT_AT_LOGIN\n"; + if (secs < maxLateness(1)) + continue; + + // Check if the main alarm is already being displayed. + // (We don't want to display both at the same time.) + if (alarmToExecute.valid()) + continue; + + // Set the time to display if it's a display alarm + alarm.setTime(now); + } + if (alarm.lateCancel()) + { + // Alarm is due, and it is to be cancelled if too late. + kdDebug(5950) << "KAlarmApp::handleEvent(): LATE_CANCEL\n"; + bool late = false; + bool cancel = false; + if (alarm.dateTime().isDateOnly()) + { + // The alarm has no time, so cancel it if its date is too far past + int maxlate = alarm.lateCancel() / 1440; // maximum lateness in days + QDateTime limit(alarm.date().addDays(maxlate + 1), Preferences::startOfDay()); + if (now >= limit) + { + // It's too late to display the scheduled occurrence. + // Find the last previous occurrence of the alarm. + DateTime next; + KAEvent::OccurType type = event.previousOccurrence(now, next, true); + switch (type & ~KAEvent::OCCURRENCE_REPEAT) + { + case KAEvent::FIRST_OR_ONLY_OCCURRENCE: + case KAEvent::RECURRENCE_DATE: + case KAEvent::RECURRENCE_DATE_TIME: + case KAEvent::LAST_RECURRENCE: + limit.setDate(next.date().addDays(maxlate + 1)); + limit.setTime(Preferences::startOfDay()); + if (now >= limit) + { + if (type == KAEvent::LAST_RECURRENCE + || type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event.recurs()) + cancel = true; // last occurrence (and there are no repetitions) + else + late = true; + } + break; + case KAEvent::NO_OCCURRENCE: + default: + late = true; + break; + } + } + } + else + { + // The alarm is timed. Allow it to be the permitted amount late before cancelling it. + int maxlate = maxLateness(alarm.lateCancel()); + if (secs > maxlate) + { + // It's over the maximum interval late. + // Find the most recent occurrence of the alarm. + DateTime next; + KAEvent::OccurType type = event.previousOccurrence(now, next, true); + switch (type & ~KAEvent::OCCURRENCE_REPEAT) + { + case KAEvent::FIRST_OR_ONLY_OCCURRENCE: + case KAEvent::RECURRENCE_DATE: + case KAEvent::RECURRENCE_DATE_TIME: + case KAEvent::LAST_RECURRENCE: + if (next.dateTime().secsTo(now) > maxlate) + { + if (type == KAEvent::LAST_RECURRENCE + || type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event.recurs()) + cancel = true; // last occurrence (and there are no repetitions) + else + late = true; + } + break; + case KAEvent::NO_OCCURRENCE: + default: + late = true; + break; + } + } + } + + if (cancel) + { + // All recurrences are finished, so cancel the event + event.setArchive(); + cancelAlarm(event, alarm.type(), false); + updateCalAndDisplay = true; + continue; + } + if (late) + { + // The latest repetition was too long ago, so schedule the next one + rescheduleAlarm(event, alarm, false); + updateCalAndDisplay = true; + continue; + } + } + if (!alarmToExecuteValid) + { + kdDebug(5950) << "KAlarmApp::handleEvent(): alarm " << alarm.type() << ": execute\n"; + alarmToExecute = alarm; // note the alarm to be executed + alarmToExecuteValid = true; // only trigger one alarm for the event + } + else + kdDebug(5950) << "KAlarmApp::handleEvent(): alarm " << alarm.type() << ": skip\n"; + } + + // If there is an alarm to execute, do this last after rescheduling/cancelling + // any others. This ensures that the updated event is only saved once to the calendar. + if (alarmToExecute.valid()) + execAlarm(event, alarmToExecute, true, !alarmToExecute.repeatAtLogin()); + else + { + if (function == EVENT_TRIGGER) + { + // The alarm is to be executed regardless of whether it's due. + // Only trigger one alarm from the event - we don't want multiple + // identical messages, for example. + KAAlarm alarm = event.firstAlarm(); + if (alarm.valid()) + execAlarm(event, alarm, false); + } + if (updateCalAndDisplay) + KAlarm::updateEvent(event, 0); // update the window lists and calendar file + else if (function != EVENT_TRIGGER) + { + kdDebug(5950) << "KAlarmApp::handleEvent(): no action\n"; + Daemon::eventHandled(eventID, false); + } + } + break; + } + } + return true; +} + +/****************************************************************************** +* Called when an alarm is currently being displayed, to store a copy of the +* alarm in the displaying calendar, and to reschedule it for its next repetition. +* If no repetitions remain, cancel it. +*/ +void KAlarmApp::alarmShowing(KAEvent& event, KAAlarm::Type alarmType, const DateTime& alarmTime) +{ + kdDebug(5950) << "KAlarmApp::alarmShowing(" << event.id() << ", " << KAAlarm::debugType(alarmType) << ")\n"; + KCal::Event* kcalEvent = AlarmCalendar::activeCalendar()->event(event.id()); + if (!kcalEvent) + kdError(5950) << "KAlarmApp::alarmShowing(): event ID not found: " << event.id() << endl; + else + { + KAAlarm alarm = event.alarm(alarmType); + if (!alarm.valid()) + kdError(5950) << "KAlarmApp::alarmShowing(): alarm type not found: " << event.id() << ":" << alarmType << endl; + else + { + // Copy the alarm to the displaying calendar in case of a crash, etc. + KAEvent dispEvent; + dispEvent.setDisplaying(event, alarmType, alarmTime.dateTime()); + AlarmCalendar* cal = AlarmCalendar::displayCalendarOpen(); + if (cal) + { + cal->deleteEvent(dispEvent.id()); // in case it already exists + cal->addEvent(dispEvent); + cal->save(); + } + + rescheduleAlarm(event, alarm, true); + return; + } + } + Daemon::eventHandled(event.id(), false); +} + +/****************************************************************************** +* Called when an alarm action has completed, to perform any post-alarm actions. +*/ +void KAlarmApp::alarmCompleted(const KAEvent& event) +{ + if (!event.postAction().isEmpty() && ShellProcess::authorised()) + { + QString command = event.postAction(); + kdDebug(5950) << "KAlarmApp::alarmCompleted(" << event.id() << "): " << command << endl; + doShellCommand(command, event, 0, ProcData::POST_ACTION); + } +} + +/****************************************************************************** +* Reschedule the alarm for its next recurrence. If none remain, delete it. +* If the alarm is deleted and it is the last alarm for its event, the event is +* removed from the calendar file and from every main window instance. +*/ +void KAlarmApp::rescheduleAlarm(KAEvent& event, const KAAlarm& alarm, bool updateCalAndDisplay) +{ + kdDebug(5950) << "KAlarmApp::rescheduleAlarm()" << endl; + bool update = false; + if (alarm.reminder() || alarm.deferred()) + { + // It's an advance warning alarm or an extra deferred alarm, so delete it + event.removeExpiredAlarm(alarm.type()); + update = true; + } + else if (alarm.repeatAtLogin()) + { + // Leave an alarm which repeats at every login until its main alarm is deleted + if (updateCalAndDisplay && event.updated()) + update = true; + } + else + { + // Reschedule the alarm for its next recurrence. + KAEvent::OccurType type = event.setNextOccurrence(QDateTime::currentDateTime()); + switch (type) + { + case KAEvent::NO_OCCURRENCE: + // All repetitions are finished, so cancel the event + cancelAlarm(event, alarm.type(), updateCalAndDisplay); + break; + default: + if (!(type & KAEvent::OCCURRENCE_REPEAT)) + break; + // Next occurrence is a repeat, so fall through to recurrence handling + case KAEvent::RECURRENCE_DATE: + case KAEvent::RECURRENCE_DATE_TIME: + case KAEvent::LAST_RECURRENCE: + // The event is due by now and repetitions still remain, so rewrite the event + if (updateCalAndDisplay) + update = true; + else + { + event.cancelCancelledDeferral(); + event.setUpdated(); // note that the calendar file needs to be updated + } + break; + case KAEvent::FIRST_OR_ONLY_OCCURRENCE: + // The first occurrence is still due?!?, so don't do anything + break; + } + if (event.deferred()) + { + // Just in case there's also a deferred alarm, ensure it's removed + event.removeExpiredAlarm(KAAlarm::DEFERRED_ALARM); + update = true; + } + } + if (update) + { + event.cancelCancelledDeferral(); + KAlarm::updateEvent(event, 0); // update the window lists and calendar file + } +} + +/****************************************************************************** +* Delete the alarm. If it is the last alarm for its event, the event is removed +* from the calendar file and from every main window instance. +*/ +void KAlarmApp::cancelAlarm(KAEvent& event, KAAlarm::Type alarmType, bool updateCalAndDisplay) +{ + kdDebug(5950) << "KAlarmApp::cancelAlarm()" << endl; + event.cancelCancelledDeferral(); + if (alarmType == KAAlarm::MAIN_ALARM && !event.displaying() && event.toBeArchived()) + { + // The event is being deleted. Save it in the expired calendar file first. + QString id = event.id(); // save event ID since KAlarm::addExpiredEvent() changes it + KAlarm::addExpiredEvent(event); + event.setEventID(id); // restore event ID + } + event.removeExpiredAlarm(alarmType); + if (!event.alarmCount()) + KAlarm::deleteEvent(event, false); + else if (updateCalAndDisplay) + KAlarm::updateEvent(event, 0); // update the window lists and calendar file +} + +/****************************************************************************** +* Execute an alarm by displaying its message or file, or executing its command. +* Reply = ShellProcess instance if a command alarm +* != 0 if successful +* = 0 if the alarm is disabled, or if an error message was output. +*/ +void* KAlarmApp::execAlarm(KAEvent& event, const KAAlarm& alarm, bool reschedule, bool allowDefer, bool noPreAction) +{ + if (!event.enabled()) + { + // The event is disabled. + if (reschedule) + rescheduleAlarm(event, alarm, true); + return 0; + } + + void* result = (void*)1; + event.setArchive(); + switch (alarm.action()) + { + case KAAlarm::MESSAGE: + case KAAlarm::FILE: + { + // Display a message or file, provided that the same event isn't already being displayed + MessageWin* win = MessageWin::findEvent(event.id()); + // Find if we're changing a reminder message to the real message + bool reminder = (alarm.type() & KAAlarm::REMINDER_ALARM); + bool replaceReminder = !reminder && win && (win->alarmType() & KAAlarm::REMINDER_ALARM); + if (!reminder && !event.deferred() + && (replaceReminder || !win) && !noPreAction + && !event.preAction().isEmpty() && ShellProcess::authorised()) + { + // It's not a reminder or a deferred alarm, and there is no message window + // (other than a reminder window) currently displayed for this alarm, + // and we need to execute a command before displaying the new window. + // Check whether the command is already being executed for this alarm. + for (QValueList<ProcData*>::Iterator it = mCommandProcesses.begin(); it != mCommandProcesses.end(); ++it) + { + ProcData* pd = *it; + if (pd->event->id() == event.id() && (pd->flags & ProcData::PRE_ACTION)) + { + kdDebug(5950) << "KAlarmApp::execAlarm(): already executing pre-DISPLAY command" << endl; + return pd->process; // already executing - don't duplicate the action + } + } + + QString command = event.preAction(); + kdDebug(5950) << "KAlarmApp::execAlarm(): pre-DISPLAY command: " << command << endl; + int flags = (reschedule ? ProcData::RESCHEDULE : 0) | (allowDefer ? ProcData::ALLOW_DEFER : 0); + if (doShellCommand(command, event, &alarm, (flags | ProcData::PRE_ACTION))) + return result; // display the message after the command completes + // Error executing command - display the message even though it failed + } + if (!event.enabled()) + delete win; // event is disabled - close its window + else if (!win + || !win->hasDefer() && !alarm.repeatAtLogin() + || replaceReminder) + { + // Either there isn't already a message for this event, + // or there is a repeat-at-login message with no Defer + // button, which needs to be replaced with a new message, + // or the caption needs to be changed from "Reminder" to "Message". + if (win) + win->setRecreating(); // prevent post-alarm actions + delete win; + KAlarm::cancelScreenSaver(); + (new MessageWin(event, alarm, reschedule, allowDefer))->show(); + } + else + { + // Raise the existing message window and replay any sound + KAlarm::cancelScreenSaver(); + win->repeat(alarm); // N.B. this reschedules the alarm + } + break; + } + case KAAlarm::COMMAND: + { + int flags = event.commandXterm() ? ProcData::EXEC_IN_XTERM : 0; + QString command = event.cleanText(); + if (event.commandScript()) + { + // Store the command script in a temporary file for execution + kdDebug(5950) << "KAlarmApp::execAlarm(): COMMAND: (script)" << endl; + QString tmpfile = createTempScriptFile(command, false, event, alarm); + if (tmpfile.isEmpty()) + result = 0; + else + result = doShellCommand(tmpfile, event, &alarm, (flags | ProcData::TEMP_FILE)); + } + else + { + kdDebug(5950) << "KAlarmApp::execAlarm(): COMMAND: " << command << endl; + result = doShellCommand(command, event, &alarm, flags); + } + if (reschedule) + rescheduleAlarm(event, alarm, true); + break; + } + case KAAlarm::EMAIL: + { + kdDebug(5950) << "KAlarmApp::execAlarm(): EMAIL to: " << event.emailAddresses(", ") << endl; + QStringList errmsgs; + if (!KAMail::send(event, errmsgs, (reschedule || allowDefer))) + result = 0; + if (!errmsgs.isEmpty()) + { + // Some error occurred, although the email may have been sent successfully + if (result) + kdDebug(5950) << "KAlarmApp::execAlarm(): copy error: " << errmsgs[1] << endl; + else + kdDebug(5950) << "KAlarmApp::execAlarm(): failed: " << errmsgs[1] << endl; + (new MessageWin(event, alarm.dateTime(), errmsgs))->show(); + } + if (reschedule) + rescheduleAlarm(event, alarm, true); + break; + } + default: + return 0; + } + return result; +} + +/****************************************************************************** +* Execute a shell command line specified by an alarm. +* If the PRE_ACTION bit of 'flags' is set, the alarm will be executed via +* execAlarm() once the command completes, the execAlarm() parameters being +* derived from the remaining bits in 'flags'. +*/ +ShellProcess* KAlarmApp::doShellCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags) +{ + kdDebug(5950) << "KAlarmApp::doShellCommand(" << command << ", " << event.id() << ")" << endl; + KProcess::Communication comms = KProcess::NoCommunication; + QString cmd; + QString tmpXtermFile; + if (flags & ProcData::EXEC_IN_XTERM) + { + // Execute the command in a terminal window. + cmd = Preferences::cmdXTermCommand(); + cmd.replace("%t", aboutData()->programName()); // set the terminal window title + if (cmd.find("%C") >= 0) + { + // Execute the command from a temporary script file + if (flags & ProcData::TEMP_FILE) + cmd.replace("%C", command); // the command is already calling a temporary file + else + { + tmpXtermFile = createTempScriptFile(command, true, event, *alarm); + if (tmpXtermFile.isEmpty()) + return 0; + cmd.replace("%C", tmpXtermFile); // %C indicates where to insert the command + } + } + else if (cmd.find("%W") >= 0) + { + // Execute the command from a temporary script file, + // with a sleep after the command is executed + tmpXtermFile = createTempScriptFile(command + QString::fromLatin1("\nsleep 86400\n"), true, event, *alarm); + if (tmpXtermFile.isEmpty()) + return 0; + cmd.replace("%W", tmpXtermFile); // %w indicates where to insert the command + } + else if (cmd.find("%w") >= 0) + { + // Append a sleep to the command. + // Quote the command in case it contains characters such as [>|;]. + QString exec = KShellProcess::quote(command + QString::fromLatin1("; sleep 86400")); + cmd.replace("%w", exec); // %w indicates where to insert the command string + } + else + { + // Set the command to execute. + // Put it in quotes in case it contains characters such as [>|;]. + QString exec = KShellProcess::quote(command); + if (cmd.find("%c") >= 0) + cmd.replace("%c", exec); // %c indicates where to insert the command string + else + cmd.append(exec); // otherwise, simply append the command string + } + } + else + { + cmd = command; + comms = KProcess::AllOutput; + } + ShellProcess* proc = new ShellProcess(cmd); + connect(proc, SIGNAL(shellExited(ShellProcess*)), SLOT(slotCommandExited(ShellProcess*))); + QGuardedPtr<ShellProcess> logproc = 0; + if (comms == KProcess::AllOutput && !event.logFile().isEmpty()) + { + // Output is to be appended to a log file. + // Set up a logging process to write the command's output to. + connect(proc, SIGNAL(receivedStdout(KProcess*,char*,int)), SLOT(slotCommandOutput(KProcess*,char*,int))); + connect(proc, SIGNAL(receivedStderr(KProcess*,char*,int)), SLOT(slotCommandOutput(KProcess*,char*,int))); + logproc = new ShellProcess(QString::fromLatin1("cat >>%1").arg(event.logFile())); + connect(logproc, SIGNAL(shellExited(ShellProcess*)), SLOT(slotLogProcExited(ShellProcess*))); + logproc->start(KProcess::Stdin); + QCString heading; + if (alarm && alarm->dateTime().isValid()) + { + QString dateTime = alarm->dateTime().isDateOnly() + ? KGlobal::locale()->formatDate(alarm->dateTime().date(), true) + : KGlobal::locale()->formatDateTime(alarm->dateTime().dateTime()); + heading.sprintf("\n******* KAlarm %s *******\n", dateTime.latin1()); + } + else + heading = "\n******* KAlarm *******\n"; + logproc->writeStdin(heading, heading.length()+1); + } + ProcData* pd = new ProcData(proc, logproc, new KAEvent(event), (alarm ? new KAAlarm(*alarm) : 0), flags); + if (flags & ProcData::TEMP_FILE) + pd->tempFiles += command; + if (!tmpXtermFile.isEmpty()) + pd->tempFiles += tmpXtermFile; + mCommandProcesses.append(pd); + if (proc->start(comms)) + return proc; + + // Error executing command - report it + kdError(5950) << "KAlarmApp::doShellCommand(): command failed to start\n"; + commandErrorMsg(proc, event, alarm, flags); + mCommandProcesses.remove(pd); + delete pd; + return 0; +} + +/****************************************************************************** +* Create a temporary script file containing the specified command string. +* Reply = path of temporary file, or null string if error. +*/ +QString KAlarmApp::createTempScriptFile(const QString& command, bool insertShell, const KAEvent& event, const KAAlarm& alarm) +{ + KTempFile tmpFile(QString::null, QString::null, 0700); + tmpFile.setAutoDelete(false); // don't delete file when it is destructed + QTextStream* stream = tmpFile.textStream(); + if (!stream) + kdError(5950) << "KAlarmApp::createTempScript(): Unable to create a temporary script file" << endl; + else + { + if (insertShell) + *stream << "#!" << ShellProcess::shellPath() << "\n"; + *stream << command; + tmpFile.close(); + if (tmpFile.status()) + kdError(5950) << "KAlarmApp::createTempScript(): Error " << tmpFile.status() << " writing to temporary script file" << endl; + else + return tmpFile.name(); + } + + QStringList errmsgs(i18n("Error creating temporary script file")); + (new MessageWin(event, alarm.dateTime(), errmsgs))->show(); + return QString::null; +} + +/****************************************************************************** +* Called when an executing command alarm sends output to stdout or stderr. +*/ +void KAlarmApp::slotCommandOutput(KProcess* proc, char* buffer, int bufflen) +{ +//kdDebug(5950) << "KAlarmApp::slotCommandOutput(): '" << QCString(buffer, bufflen+1) << "'\n"; + // Find this command in the command list + for (QValueList<ProcData*>::Iterator it = mCommandProcesses.begin(); it != mCommandProcesses.end(); ++it) + { + ProcData* pd = *it; + if (pd->process == proc && pd->logProcess) + { + pd->logProcess->writeStdin(buffer, bufflen); + break; + } + } +} + +/****************************************************************************** +* Called when a logging process completes. +*/ +void KAlarmApp::slotLogProcExited(ShellProcess* proc) +{ + // Because it's held as a guarded pointer in the ProcData structure, + // we don't need to set any pointers to zero. + delete proc; +} + +/****************************************************************************** +* Called when a command alarm's execution completes. +*/ +void KAlarmApp::slotCommandExited(ShellProcess* proc) +{ + kdDebug(5950) << "KAlarmApp::slotCommandExited()\n"; + // Find this command in the command list + for (QValueList<ProcData*>::Iterator it = mCommandProcesses.begin(); it != mCommandProcesses.end(); ++it) + { + ProcData* pd = *it; + if (pd->process == proc) + { + // Found the command + if (pd->logProcess) + pd->logProcess->stdinExit(); // terminate the logging process + + // Check its exit status + if (!proc->normalExit()) + { + QString errmsg = proc->errorMessage(); + kdWarning(5950) << "KAlarmApp::slotCommandExited(" << pd->event->cleanText() << "): " << errmsg << endl; + if (pd->messageBoxParent) + { + // Close the existing informational KMessageBox for this process + QObjectList* dialogs = pd->messageBoxParent->queryList("KDialogBase", 0, false, true); + KDialogBase* dialog = (KDialogBase*)dialogs->getFirst(); + delete dialog; + delete dialogs; + if (!pd->tempFile()) + { + errmsg += '\n'; + errmsg += proc->command(); + } + KMessageBox::error(pd->messageBoxParent, errmsg); + } + else + commandErrorMsg(proc, *pd->event, pd->alarm, pd->flags); + } + if (pd->preAction()) + execAlarm(*pd->event, *pd->alarm, pd->reschedule(), pd->allowDefer(), true); + mCommandProcesses.remove(it); + delete pd; + break; + } + } + + // If there are now no executing shell commands, quit if a quit was queued + if (mPendingQuit && mCommandProcesses.isEmpty()) + quitIf(mPendingQuitCode); +} + +/****************************************************************************** +* Output an error message for a shell command. +*/ +void KAlarmApp::commandErrorMsg(const ShellProcess* proc, const KAEvent& event, const KAAlarm* alarm, int flags) +{ + QStringList errmsgs; + if (flags & ProcData::PRE_ACTION) + errmsgs += i18n("Pre-alarm action:"); + else if (flags & ProcData::POST_ACTION) + errmsgs += i18n("Post-alarm action:"); + errmsgs += proc->errorMessage(); + if (!(flags & ProcData::TEMP_FILE)) + errmsgs += proc->command(); + (new MessageWin(event, (alarm ? alarm->dateTime() : DateTime()), errmsgs))->show(); +} + +/****************************************************************************** +* Notes that an informational KMessageBox is displayed for this process. +*/ +void KAlarmApp::commandMessage(ShellProcess* proc, QWidget* parent) +{ + // Find this command in the command list + for (QValueList<ProcData*>::Iterator it = mCommandProcesses.begin(); it != mCommandProcesses.end(); ++it) + { + ProcData* pd = *it; + if (pd->process == proc) + { + pd->messageBoxParent = parent; + break; + } + } +} + +/****************************************************************************** +* Set up remaining DCOP handlers and start processing DCOP calls. +*/ +void KAlarmApp::setUpDcop() +{ + if (!mInitialised) + { + mInitialised = true; // we're now ready to handle DCOP calls + Daemon::createDcopHandler(); + QTimer::singleShot(0, this, SLOT(processQueue())); // process anything already queued + } +} + +/****************************************************************************** +* If this is the first time through, open the calendar file, optionally start +* the alarm daemon and register with it, and set up the DCOP handler. +*/ +bool KAlarmApp::initCheck(bool calendarOnly) +{ + bool startdaemon; + AlarmCalendar* cal = AlarmCalendar::activeCalendar(); + if (!cal->isOpen()) + { + kdDebug(5950) << "KAlarmApp::initCheck(): opening active calendar\n"; + + // First time through. Open the calendar file. + if (!cal->open()) + return false; + + if (!mStartOfDay.isValid()) + changeStartOfDay(); // start of day time has changed, so adjust date-only alarms + + /* Need to open the display calendar now, since otherwise if the daemon + * immediately notifies display alarms, they will often be processed while + * redisplayAlarms() is executing open() (but before open() completes), + * which causes problems!! + */ + AlarmCalendar::displayCalendar()->open(); + + /* Need to open the expired alarm calendar now, since otherwise if the daemon + * immediately notifies multiple alarms, the second alarm is likely to be + * processed while the calendar is executing open() (but before open() completes), + * which causes a hang!! + */ + AlarmCalendar::expiredCalendar()->open(); + AlarmCalendar::expiredCalendar()->setPurgeDays(theInstance->mPrefsExpiredKeepDays); + + startdaemon = true; + } + else + startdaemon = !Daemon::isRegistered(); + + if (!calendarOnly) + { + setUpDcop(); // start processing DCOP calls + if (startdaemon) + Daemon::start(); // make sure the alarm daemon is running + } + return true; +} + +/****************************************************************************** +* Convert the --time parameter string into a date/time or date value. +* The parameter is in the form [[[yyyy-]mm-]dd-]hh:mm or yyyy-mm-dd. +* Reply = true if successful. +*/ +static bool convWakeTime(const QCString& timeParam, QDateTime& dateTime, bool& noTime) +{ + if (timeParam.length() > 19) + return false; + char timeStr[20]; + strcpy(timeStr, timeParam); + int dt[5] = { -1, -1, -1, -1, -1 }; + char* s; + char* end; + // Get the minute value + if ((s = strchr(timeStr, ':')) == 0) + noTime = true; + else + { + noTime = false; + *s++ = 0; + dt[4] = strtoul(s, &end, 10); + if (end == s || *end || dt[4] >= 60) + return false; + // Get the hour value + if ((s = strrchr(timeStr, '-')) == 0) + s = timeStr; + else + *s++ = 0; + dt[3] = strtoul(s, &end, 10); + if (end == s || *end || dt[3] >= 24) + return false; + } + bool dateSet = false; + if (s != timeStr) + { + dateSet = true; + // Get the day value + if ((s = strrchr(timeStr, '-')) == 0) + s = timeStr; + else + *s++ = 0; + dt[2] = strtoul(s, &end, 10); + if (end == s || *end || dt[2] == 0 || dt[2] > 31) + return false; + if (s != timeStr) + { + // Get the month value + if ((s = strrchr(timeStr, '-')) == 0) + s = timeStr; + else + *s++ = 0; + dt[1] = strtoul(s, &end, 10); + if (end == s || *end || dt[1] == 0 || dt[1] > 12) + return false; + if (s != timeStr) + { + // Get the year value + dt[0] = strtoul(timeStr, &end, 10); + if (end == timeStr || *end) + return false; + } + } + } + + QDate date(dt[0], dt[1], dt[2]); + QTime time(0, 0, 0); + if (noTime) + { + // No time was specified, so the full date must have been specified + if (dt[0] < 0) + return false; + } + else + { + // Compile the values into a date/time structure + QDateTime now = QDateTime::currentDateTime(); + if (dt[0] < 0) + date.setYMD(now.date().year(), + (dt[1] < 0 ? now.date().month() : dt[1]), + (dt[2] < 0 ? now.date().day() : dt[2])); + time.setHMS(dt[3], dt[4], 0); + if (!dateSet && time < now.time()) + date = date.addDays(1); + } + if (!date.isValid()) + return false; + dateTime.setDate(date); + dateTime.setTime(time); + return true; +} + +/****************************************************************************** +* Convert a time interval command line parameter. +* 'timeInterval' receives the count for the recurType. If 'allowMonthYear' is +* false, 'timeInterval' is converted to minutes. +* Reply = true if successful. +*/ +static bool convInterval(const QCString& timeParam, KARecurrence::Type& recurType, int& timeInterval, bool allowMonthYear) +{ + QCString timeString = timeParam; + // Get the recurrence interval + bool ok = true; + uint interval = 0; + bool negative = (timeString[0] == '-'); + if (negative) + timeString = timeString.right(1); + uint length = timeString.length(); + switch (timeString[length - 1]) + { + case 'Y': + if (!allowMonthYear) + ok = false; + recurType = KARecurrence::ANNUAL_DATE; + timeString = timeString.left(length - 1); + break; + case 'W': + recurType = KARecurrence::WEEKLY; + timeString = timeString.left(length - 1); + break; + case 'D': + recurType = KARecurrence::DAILY; + timeString = timeString.left(length - 1); + break; + case 'M': + { + int i = timeString.find('H'); + if (i < 0) + { + if (!allowMonthYear) + ok = false; + recurType = KARecurrence::MONTHLY_DAY; + timeString = timeString.left(length - 1); + } + else + { + recurType = KARecurrence::MINUTELY; + interval = timeString.left(i).toUInt(&ok) * 60; + timeString = timeString.mid(i + 1, length - i - 2); + } + break; + } + default: // should be a digit + recurType = KARecurrence::MINUTELY; + break; + } + if (ok) + interval += timeString.toUInt(&ok); + if (!allowMonthYear) + { + // Convert time interval to minutes + switch (recurType) + { + case KARecurrence::WEEKLY: + interval *= 7; + // fall through to DAILY + case KARecurrence::DAILY: + interval *= 24*60; + break; + default: + break; + } + } + timeInterval = static_cast<int>(interval); + if (negative) + timeInterval = -timeInterval; + return ok; +} + +KAlarmApp::ProcData::ProcData(ShellProcess* p, ShellProcess* logp, KAEvent* e, KAAlarm* a, int f) + : process(p), + logProcess(logp), + event(e), + alarm(a), + messageBoxParent(0), + flags(f) +{ } + +KAlarmApp::ProcData::~ProcData() +{ + while (!tempFiles.isEmpty()) + { + // Delete the temporary file called by the XTerm command + QFile f(tempFiles.first()); + f.remove(); + tempFiles.remove(tempFiles.begin()); + } + delete process; + delete event; + delete alarm; +} diff --git a/kalarm/kalarmapp.h b/kalarm/kalarmapp.h new file mode 100644 index 000000000..22c7649b3 --- /dev/null +++ b/kalarm/kalarmapp.h @@ -0,0 +1,189 @@ +/* + * kalarmapp.h - the KAlarm application object + * Program: kalarm + * Copyright © 2001-2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KALARMAPP_H +#define KALARMAPP_H + +/** @file kalarmapp.h - the KAlarm application object */ + +#include <qguardedptr.h> +class QTimer; +class QDateTime; + +#include <kuniqueapplication.h> +#include <kurl.h> +class KProcess; +namespace KCal { class Event; } + +#include "alarmevent.h" +class DcopHandler; +#ifdef OLD_DCOP +class DcopHandlerOld; +#endif +class AlarmCalendar; +class MainWindow; +class AlarmListView; +class MessageWin; +class TrayWindow; +class ShellProcess; + + +class KAlarmApp : public KUniqueApplication +{ + Q_OBJECT + public: + ~KAlarmApp(); + virtual int newInstance(); + static KAlarmApp* getInstance(); + bool checkCalendarDaemon() { return initCheck(); } + bool haveSystemTray() const { return mHaveSystemTray; } + bool wantRunInSystemTray() const; + bool alarmsDisabledIfStopped() const { return mDisableAlarmsIfStopped; } + bool speechEnabled() const { return mSpeechEnabled; } + bool korganizerEnabled() const { return mKOrganizerEnabled; } + bool restoreSession(); + bool sessionClosingDown() const { return mSessionClosingDown; } + void quitIf() { quitIf(0); } + void doQuit(QWidget* parent); + static void displayFatalError(const QString& message); + void addWindow(TrayWindow* w) { mTrayWindow = w; } + void removeWindow(TrayWindow*); + TrayWindow* trayWindow() const { return mTrayWindow; } + MainWindow* trayMainWindow() const; + bool displayTrayIcon(bool show, MainWindow* = 0); + bool trayIconDisplayed() const { return !!mTrayWindow; } + bool editNewAlarm(MainWindow* = 0); + virtual void commitData(QSessionManager&); + + void* execAlarm(KAEvent&, const KAAlarm&, bool reschedule, bool allowDefer = true, bool noPreAction = false); + void alarmShowing(KAEvent&, KAAlarm::Type, const DateTime&); + void alarmCompleted(const KAEvent&); + bool deleteEvent(const QString& eventID) { return handleEvent(eventID, EVENT_CANCEL); } + void commandMessage(ShellProcess*, QWidget* parent); + // Methods called indirectly by the DCOP interface + bool scheduleEvent(KAEvent::Action, const QString& text, const QDateTime&, + int lateCancel, int flags, const QColor& bg, const QColor& fg, + const QFont&, const QString& audioFile, float audioVolume, + int reminderMinutes, const KARecurrence& recurrence, + int repeatInterval, int repeatCount, + uint mailFromID = 0, const EmailAddressList& mailAddresses = EmailAddressList(), + const QString& mailSubject = QString::null, + const QStringList& mailAttachments = QStringList()); + bool handleEvent(const QString& calendarFile, const QString& eventID) { return handleEvent(calendarFile, eventID, EVENT_HANDLE); } + bool triggerEvent(const QString& calendarFile, const QString& eventID) { return handleEvent(calendarFile, eventID, EVENT_TRIGGER); } + bool deleteEvent(const QString& calendarFile, const QString& eventID) { return handleEvent(calendarFile, eventID, EVENT_CANCEL); } + public slots: + void processQueue(); + signals: + void trayIconToggled(); + protected: + KAlarmApp(); + private slots: + void quitFatal(); + void slotPreferencesChanged(); + void slotCommandOutput(KProcess*, char* buffer, int bufflen); + void slotLogProcExited(ShellProcess*); + void slotCommandExited(ShellProcess*); + void slotSystemTrayTimer(); + void slotExpiredPurged(); + private: + enum EventFunc + { + EVENT_HANDLE, // if the alarm is due, execute it and then reschedule it + EVENT_TRIGGER, // execute the alarm regardless, and then reschedule it if it already due + EVENT_CANCEL // delete the alarm + }; + struct ProcData + { + ProcData(ShellProcess* p, ShellProcess* logp, KAEvent* e, KAAlarm* a, int f = 0); + ~ProcData(); + enum { PRE_ACTION = 0x01, POST_ACTION = 0x02, RESCHEDULE = 0x04, ALLOW_DEFER = 0x08, + TEMP_FILE = 0x10, EXEC_IN_XTERM = 0x20 }; + bool preAction() const { return flags & PRE_ACTION; } + bool postAction() const { return flags & POST_ACTION; } + bool reschedule() const { return flags & RESCHEDULE; } + bool allowDefer() const { return flags & ALLOW_DEFER; } + bool tempFile() const { return flags & TEMP_FILE; } + bool execInXterm() const { return flags & EXEC_IN_XTERM; } + ShellProcess* process; + QGuardedPtr<ShellProcess> logProcess; + KAEvent* event; + KAAlarm* alarm; + QGuardedPtr<QWidget> messageBoxParent; + QStringList tempFiles; + int flags; + }; + struct DcopQEntry + { + DcopQEntry(EventFunc f, const QString& id) : function(f), eventId(id) { } + DcopQEntry(const KAEvent& e, EventFunc f = EVENT_HANDLE) : function(f), event(e) { } + DcopQEntry() { } + EventFunc function; + QString eventId; + KAEvent event; + }; + + bool initCheck(bool calendarOnly = false); + void quitIf(int exitCode, bool force = false); + void redisplayAlarms(); + bool checkSystemTray(); + void changeStartOfDay(); + void setUpDcop(); + bool handleEvent(const QString& calendarFile, const QString& eventID, EventFunc); + bool handleEvent(const QString& eventID, EventFunc); + void rescheduleAlarm(KAEvent&, const KAAlarm&, bool updateCalAndDisplay); + void cancelAlarm(KAEvent&, KAAlarm::Type, bool updateCalAndDisplay); + ShellProcess* doShellCommand(const QString& command, const KAEvent&, const KAAlarm*, int flags = 0); + QString createTempScriptFile(const QString& command, bool insertShell, const KAEvent&, const KAAlarm&); + void commandErrorMsg(const ShellProcess*, const KAEvent&, const KAAlarm*, int flags = 0); + + static KAlarmApp* theInstance; // the one and only KAlarmApp instance + static int mActiveCount; // number of active instances without main windows + static int mFatalError; // a fatal error has occurred - just wait to exit + static QString mFatalMessage; // fatal error message to output + bool mInitialised; // initialisation complete: ready to handle DCOP calls + DcopHandler* mDcopHandler; // the parent of the main DCOP receiver object +#ifdef OLD_DCOP + DcopHandlerOld* mDcopHandlerOld; // the parent of the old main DCOP receiver object +#endif + TrayWindow* mTrayWindow; // active system tray icon + QTime mStartOfDay; // start-of-day time currently in use + QColor mPrefsExpiredColour; // expired alarms text colour + int mPrefsExpiredKeepDays;// how long expired alarms are being kept + QValueList<ProcData*> mCommandProcesses; // currently active command alarm processes + QValueList<DcopQEntry> mDcopQueue; // DCOP command queue + int mPendingQuitCode; // exit code for a pending quit + bool mPendingQuit; // quit once the DCOP command and shell command queues have been processed + bool mProcessingQueue; // a mDcopQueue entry is currently being processed + bool mHaveSystemTray; // whether there is a system tray + bool mNoSystemTray; // no KDE system tray exists + bool mSavedNoSystemTray; // mNoSystemTray before mCheckingSystemTray was true + bool mCheckingSystemTray; // the existence of the system tray is being checked + bool mSessionClosingDown; // session manager is closing the application + bool mOldRunInSystemTray; // running continuously in system tray was selected + bool mDisableAlarmsIfStopped; // disable alarms whenever KAlarm is not running + bool mRefreshExpiredAlarms; // need to refresh the expired alarms display + bool mSpeechEnabled; // speech synthesis is enabled (kttsd exists) + bool mKOrganizerEnabled; // KOrganizer options are enabled (korganizer exists) +}; + +inline KAlarmApp* theApp() { return KAlarmApp::getInstance(); } + +#endif // KALARMAPP_H diff --git a/kalarm/kalarmd/Makefile.am b/kalarm/kalarmd/Makefile.am new file mode 100644 index 000000000..7770c6816 --- /dev/null +++ b/kalarm/kalarmd/Makefile.am @@ -0,0 +1,28 @@ +INCLUDES= -I$(top_srcdir) -I$(top_srcdir)/kalarm $(all_includes) + +noinst_LTLIBRARIES = libkalarmd.la + +libkalarmd_la_LDFLAGS = $(all_libraries) $(KDE_RPATH) +libkalarmd_la_LIBADD = $(LIB_KDECORE) $(top_builddir)/libkcal/libkcal.la +libkalarmd_la_SOURCES = alarmdaemoniface.stub alarmguiiface.stub + +bin_PROGRAMS = kalarmd + +kalarmd_LDFLAGS = $(all_libraries) $(KDE_RPATH) +kalarmd_LDADD = $(LIB_KDEUI) $(top_builddir)/libkcal/libkcal.la libkalarmd.la +kalarmd_SOURCES = alarmdaemon.cpp admain.cpp adapp.cpp \ + adcalendar.cpp adconfigdata.cpp clientinfo.cpp \ + alarmdaemoniface.skel +kalarmd_COMPILE_FIRST = alarmguiiface_stub.h + +noinst_HEADERS = adapp.h alarmdaemon.h alarmdaemoniface.h alarmguiiface.h \ + adcalendar.h adconfigdata.h clientinfo.h kalarmd.h + +METASOURCES = AUTO + +autostart_DATA = kalarmd.autostart.desktop +autostartdir = $(datadir)/autostart + +app_DATA = kalarmd.desktop +appdir = $(kde_appsdir)/.hidden + diff --git a/kalarm/kalarmd/adapp.cpp b/kalarm/kalarmd/adapp.cpp new file mode 100644 index 000000000..77f31e4f0 --- /dev/null +++ b/kalarm/kalarmd/adapp.cpp @@ -0,0 +1,71 @@ +/* + * adapp.cpp - kalarmd application + * Program: KAlarm's alarm daemon (kalarmd) + * Copyright (c) 2001, 2004, 2005 by David Jarvie <software@astrojar.org.uk> + * Copyright (c) 2000,2001 Cornelius Schumacher <schumacher@kde.org> + * Copyright (c) 1997-1999 Preston Brown <pbrown@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarmd.h" + +#include <kcmdlineargs.h> +#include <kdebug.h> + +#include "alarmdaemon.h" +#include "adapp.moc" + + +AlarmDaemonApp::AlarmDaemonApp() + : KUniqueApplication(false, false), + mAd(0) +{ + disableSessionManagement(); +} + +int AlarmDaemonApp::newInstance() +{ + kdDebug(5900) << "AlarmDaemonApp::newInstance()" << endl; + + /* Prevent the application being restored automatically by the session manager + * at session startup. Instead, the KDE autostart facility is used to start + * the application. This allows the user to configure whether or not it is to + * be started automatically, and also ensures that it is started in the correct + * phase of session startup, i.e. after clients have been restored by the + * session manager. + */ + disableSessionManagement(); + + // Check if we already have a running alarm daemon widget + if (mAd) + return 0; + + // Check if we are starting up at session startup + static bool restored = false; + bool autostart = false; + if (!restored && isRestored()) + restored = true; // make sure we restore only once + else + { + KCmdLineArgs* args = KCmdLineArgs::parsedArgs(); + autostart = args->isSet("autostart"); + args->clear(); // free up memory + } + + mAd = new AlarmDaemon(autostart, 0, DAEMON_DCOP_OBJECT); + + return 0; +} diff --git a/kalarm/kalarmd/adapp.h b/kalarm/kalarmd/adapp.h new file mode 100644 index 000000000..83d11d298 --- /dev/null +++ b/kalarm/kalarmd/adapp.h @@ -0,0 +1,42 @@ +/* + * adapp.h - kalarmd application + * Program: KAlarm's alarm daemon (kalarmd) + * Copyright (C) 2001, 2004 by David Jarvie <software@astrojar.org.uk> + * Copyright (c) 2000,2001 Cornelius Schumacher <schumacher@kde.org> + * Copyright (c) 1997-1999 Preston Brown <pbrown@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef ADAPP_H +#define ADAPP_H + +#include <kuniqueapplication.h> + +class AlarmDaemon; + + +class AlarmDaemonApp : public KUniqueApplication +{ + Q_OBJECT + public: + AlarmDaemonApp(); + virtual int newInstance(); + + private: + AlarmDaemon* mAd; +}; + +#endif // ADAPP_H diff --git a/kalarm/kalarmd/adcalendar.cpp b/kalarm/kalarmd/adcalendar.cpp new file mode 100644 index 000000000..8af97cc0b --- /dev/null +++ b/kalarm/kalarmd/adcalendar.cpp @@ -0,0 +1,253 @@ +/* + * adcalendar.cpp - calendar file access + * Program: KAlarm's alarm daemon (kalarmd) + * Copyright (c) 2001, 2004-2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include <unistd.h> +#include <assert.h> + +#include <qfile.h> + +#include <ktempfile.h> +#include <kio/job.h> +#include <kio/jobclasses.h> +#include <kdebug.h> + +#include "adcalendar.moc" + +QValueList<ADCalendar*> ADCalendar::mCalendars; +ADCalendar::EventsMap ADCalendar::mEventsHandled; +ADCalendar::EventsMap ADCalendar::mEventsPending; +QStringList ADCalendar::mCalendarUrls; // never delete or reorder anything in this list! + + +ADCalendar::ADCalendar(const QString& url, const QCString& appname) + : KCal::CalendarLocal(QString::fromLatin1("UTC")), + mUrlString(url), + mAppName(appname), + mLoaded(false), + mLoadedConnected(false), + mUnregistered(false), + mEnabled(true) +{ + ADCalendar* cal = getCalendar(url); + if (cal) + { + kdError(5900) << "ADCalendar::ADCalendar(" << url << "): calendar already exists" << endl; + assert(0); + } + mUrlIndex = mCalendarUrls.findIndex(url); // get unique index for this URL + if (mUrlIndex < 0) + { + mUrlIndex = static_cast<int>(mCalendarUrls.count()); + mCalendarUrls.append(url); + } + loadFile(false); + mCalendars.append(this); +} + +ADCalendar::~ADCalendar() +{ + clearEventsHandled(); + mCalendars.remove(this); +} + +/****************************************************************************** +* Load the calendar file. +*/ +bool ADCalendar::loadFile(bool reset) +{ + if (reset) + clearEventsHandled(); + if (!mTempFileName.isNull()) + { + // Don't try to load the file if already downloading it + kdError(5900) << "ADCalendar::loadFile(): already downloading another file\n"; + return false; + } + mLoaded = false; + KURL url(mUrlString); + if (url.isLocalFile()) + { + // It's a local file + loadLocalFile(url.path()); + emit loaded(this, mLoaded); + } + else + { + // It's a remote file. Download to a temporary file before loading it + KTempFile tempFile; + mTempFileName = tempFile.name(); + KURL dest; + dest.setPath(mTempFileName); + KIO::FileCopyJob* job = KIO::file_copy(url, dest, -1, true); + connect(job, SIGNAL(result(KIO::Job*)), SLOT(slotDownloadJobResult(KIO::Job*))); + } + return true; +} + +void ADCalendar::slotDownloadJobResult(KIO::Job *job) +{ + if (job->error()) + { + KURL url(mUrlString); + kdDebug(5900) << "Error downloading calendar from " << url.prettyURL() << endl; + job->showErrorDialog(0); + } + else + { + kdDebug(5900) << "--- Downloaded to " << mTempFileName << endl; + loadLocalFile(mTempFileName); + } + unlink(QFile::encodeName(mTempFileName)); + mTempFileName = QString::null; + emit loaded(this, mLoaded); +} + +void ADCalendar::loadLocalFile(const QString& filename) +{ + mLoaded = load(filename); + if (!mLoaded) + kdDebug(5900) << "ADCalendar::loadLocalFile(): Error loading calendar file '" << filename << "'\n"; + else + clearEventsHandled(true); // remove all events which no longer exist from handled list +} + +bool ADCalendar::setLoadedConnected() +{ + if (mLoadedConnected) + return true; + mLoadedConnected = true; + return false; +} + +/****************************************************************************** +* Check whether all the alarms for the event with the given ID have already +* been handled. +*/ +bool ADCalendar::eventHandled(const KCal::Event* event, const QValueList<QDateTime>& alarmtimes) +{ + EventsMap::ConstIterator it = mEventsHandled.find(EventKey(event->uid(), mUrlIndex)); + if (it == mEventsHandled.end()) + return false; + + int oldCount = it.data().alarmTimes.count(); + int count = alarmtimes.count(); + for (int i = 0; i < count; ++i) + { + if (alarmtimes[i].isValid() + && (i >= oldCount // is it an additional alarm? + || !it.data().alarmTimes[i].isValid() // or has it just become due? + || it.data().alarmTimes[i].isValid() // or has it changed? + && alarmtimes[i] != it.data().alarmTimes[i])) + return false; // this alarm has changed + } + return true; +} + +/****************************************************************************** +* Remember that the event with the given ID has been handled. +* It must already be in the pending list. +*/ +void ADCalendar::setEventHandled(const QString& eventID) +{ + kdDebug(5900) << "ADCalendar::setEventHandled(" << eventID << ")\n"; + EventKey key(eventID, mUrlIndex); + + // Remove it from the pending list, and add it to the handled list + EventsMap::Iterator it = mEventsPending.find(key); + if (it != mEventsPending.end()) + { + setEventInMap(mEventsHandled, key, it.data().alarmTimes, it.data().eventSequence); + mEventsPending.remove(it); + } +} + +/****************************************************************************** +* Remember that the specified alarms for the event with the given ID have been +* notified to KAlarm, but no reply has come back yet. +*/ +void ADCalendar::setEventPending(const KCal::Event* event, const QValueList<QDateTime>& alarmtimes) +{ + if (event) + { + kdDebug(5900) << "ADCalendar::setEventPending(" << event->uid() << ")\n"; + EventKey key(event->uid(), mUrlIndex); + setEventInMap(mEventsPending, key, alarmtimes, event->revision()); + } +} + +/****************************************************************************** +* Add a specified entry to the events pending or handled list. +*/ +void ADCalendar::setEventInMap(EventsMap& map, const EventKey& key, const QValueList<QDateTime>& alarmtimes, int sequence) +{ + EventsMap::Iterator it = map.find(key); + if (it != map.end()) + { + // Update the existing entry for the event + it.data().alarmTimes = alarmtimes; + it.data().eventSequence = sequence; + } + else + map.insert(key, EventItem(sequence, alarmtimes)); +} + +/****************************************************************************** +* Clear all memory of events handled for the calendar. +*/ +void ADCalendar::clearEventsHandled(bool nonexistentOnly) +{ + clearEventMap(mEventsPending, nonexistentOnly); + clearEventMap(mEventsHandled, nonexistentOnly); +} + +/****************************************************************************** +* Clear the events pending or handled list of all events handled for the calendar. +*/ +void ADCalendar::clearEventMap(EventsMap& map, bool nonexistentOnly) +{ + for (EventsMap::Iterator it = map.begin(); it != map.end(); ) + { + if (it.key().calendarIndex == mUrlIndex + && (!nonexistentOnly || !event(it.key().eventID))) + { + EventsMap::Iterator i = it; + ++it; // prevent iterator becoming invalid with remove() + map.remove(i); + } + else + ++it; + } +} + +/****************************************************************************** +* Look up the calendar with the specified full calendar URL. +*/ +ADCalendar* ADCalendar::getCalendar(const QString& calendarURL) +{ + if (!calendarURL.isEmpty()) + { + for (ConstIterator it = begin(); it != end(); ++it) + { + if ((*it)->urlString() == calendarURL) + return *it; + } + } + return 0; +} diff --git a/kalarm/kalarmd/adcalendar.h b/kalarm/kalarmd/adcalendar.h new file mode 100644 index 000000000..656070bef --- /dev/null +++ b/kalarm/kalarmd/adcalendar.h @@ -0,0 +1,120 @@ +/* + * adcalendar.h - calendar file access + * Program: KAlarm's alarm daemon (kalarmd) + * Copyright (c) 2001, 2004-2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef ADCALENDAR_H +#define ADCALENDAR_H + +#include <libkcal/calendarlocal.h> +namespace KIO { class Job; } +class ADCalendar; + + +// Alarm Daemon calendar access +class ADCalendar : public KCal::CalendarLocal +{ + Q_OBJECT + public: + typedef QValueList<ADCalendar*>::ConstIterator ConstIterator; + + ~ADCalendar(); + + const QString& urlString() const { return mUrlString; } + const QCString& appName() const { return mAppName; } + + void setEnabled(bool enabled) { mEnabled = enabled; } + bool enabled() const { return mEnabled && !unregistered(); } + bool available() const { return loaded() && !unregistered(); } + + // Client has registered since calendar was constructed, but + // has not since added the calendar. Monitoring is disabled. + void setUnregistered(bool u) { mUnregistered = u; } + bool unregistered() const { return mUnregistered; } + + void setEventPending(const KCal::Event*, const QValueList<QDateTime>&); + void setEventHandled(const QString& eventID); + void clearEventsHandled(bool nonexistentOnly = false); + bool eventHandled(const KCal::Event*, const QValueList<QDateTime>&); + + bool loadFile(bool reset); + bool setLoadedConnected(); // check status of mLoadedConnected and set it to true + bool downloading() const { return !mTempFileName.isNull(); } + bool loaded() const { return mLoaded; } + + + static ConstIterator begin() { return mCalendars.begin(); } + static ConstIterator end() { return mCalendars.end(); } + static ADCalendar* getCalendar(const QString& calendarURL); + + signals: + void loaded(ADCalendar*, bool success); + + protected: + // Only ClientInfo can construct ADCalendar objects + friend class ClientInfo; + ADCalendar(const QString& url, const QCString& appname); + + private slots: + void slotDownloadJobResult(KIO::Job*); + + private: + struct EventKey + { + EventKey() : calendarIndex(-1) { } + EventKey(const QString& id, int cal) : eventID(id), calendarIndex(cal) { } + bool operator<(const EventKey& k) const + { return (calendarIndex == k.calendarIndex) + ? (eventID < k.eventID) : (calendarIndex < k.calendarIndex); + } + QString eventID; + int calendarIndex; + }; + struct EventItem + { + EventItem() : eventSequence(0) { } + EventItem(int seqno, const QValueList<QDateTime>& alarmtimes) + : eventSequence(seqno), alarmTimes(alarmtimes) {} + int eventSequence; + QValueList<QDateTime> alarmTimes; + }; + + typedef QMap<EventKey, EventItem> EventsMap; // calendar/event ID, event sequence num + static EventsMap mEventsHandled; // IDs of already triggered events which have been processed by KAlarm + static EventsMap mEventsPending; // IDs of already triggered events not yet processed by KAlarm + static QStringList mCalendarUrls; // URLs of all calendars ever opened + static QValueList<ADCalendar*> mCalendars; // list of all constructed calendars + + ADCalendar(const ADCalendar&); // prohibit copying + ADCalendar& operator=(const ADCalendar&); // prohibit copying + + void loadLocalFile(const QString& filename); + void clearEventMap(EventsMap&, bool nonexistentOnly); + void setEventInMap(EventsMap&, const EventKey&, const QValueList<QDateTime>& alarmtimes, int sequence); + + QString mUrlString; // calendar file URL + QCString mAppName; // name of application owning this calendar + QString mTempFileName; // temporary file used if currently downloading, else null + int mUrlIndex; // unique index to URL in mCalendarUrls + bool mLoaded; // true if calendar file is currently loaded + bool mLoadedConnected; // true if the loaded() signal has been connected to AlarmDaemon + bool mUnregistered; // client has registered, but has not since added the calendar + bool mEnabled; // events are currently manually enabled +}; + +#endif // ADCALENDAR_H diff --git a/kalarm/kalarmd/adconfigdata.cpp b/kalarm/kalarmd/adconfigdata.cpp new file mode 100644 index 000000000..11dc6bf67 --- /dev/null +++ b/kalarm/kalarmd/adconfigdata.cpp @@ -0,0 +1,146 @@ +/* + * adcalendar.cpp - configuration file access + * Program: KAlarm's alarm daemon (kalarmd) + * Copyright (C) 2001, 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarmd.h" + +#include <qregexp.h> +#include <qstringlist.h> + +#include <kconfig.h> +#include <kstandarddirs.h> +#include <kdebug.h> + +#include "adcalendar.h" +#include "adconfigdata.h" + +// Config file key strings +const QString CLIENT_GROUP(QString::fromLatin1("Client ")); +const QRegExp CLIENT_GROUP_SEARCH("^Client "); +// Client data file key strings +const QString CALENDAR_KEY(QString::fromLatin1("Calendar")); +const QString TITLE_KEY(QString::fromLatin1("Title")); +const QString DCOP_OBJECT_KEY(QString::fromLatin1("DCOP object")); +const QString START_CLIENT_KEY(QString::fromLatin1("Start")); + + +/****************************************************************************** +* Read the configuration file. +* Create the client list and open all calendar files. +*/ +void ADConfigData::readConfig() +{ + kdDebug(5900) << "ADConfigData::readConfig()" << endl; + ClientInfo::clear(); + KConfig* config = KGlobal::config(); + QStringList clients = config->groupList().grep(CLIENT_GROUP_SEARCH); + for (QStringList::Iterator cl = clients.begin(); cl != clients.end(); ++cl) + { + // Read this client's configuration + config->setGroup(*cl); + QString client = *cl; + client.remove(CLIENT_GROUP_SEARCH); + QString title = config->readEntry(TITLE_KEY, client); // read app title (default = app name) + QCString dcopObject = config->readEntry(DCOP_OBJECT_KEY).local8Bit(); + bool startClient = config->readBoolEntry(START_CLIENT_KEY, false); + QString calendar = config->readPathEntry(CALENDAR_KEY); + + // Verify the configuration + bool ok = false; + if (client.isEmpty() || KStandardDirs::findExe(client).isNull()) + kdError(5900) << "ADConfigData::readConfig(): group '" << *cl << "' deleted (client app not found)\n"; + else if (calendar.isEmpty()) + kdError(5900) << "ADConfigData::readConfig(): no calendar specified for '" << client << "'\n"; + else if (dcopObject.isEmpty()) + kdError(5900) << "ADConfigData::readConfig(): no DCOP object specified for '" << client << "'\n"; + else + { + ADCalendar* cal = ADCalendar::getCalendar(calendar); + if (cal) + kdError(5900) << "ADConfigData::readConfig(): calendar registered by multiple clients: " << calendar << endl; + else + ok = true; + } + if (!ok) + { + config->deleteGroup(*cl, true); + continue; + } + + // Create the client and calendar objects + new ClientInfo(client.local8Bit(), title, dcopObject, calendar, startClient); + kdDebug(5900) << "ADConfigData::readConfig(): client " << client << " : calendar " << calendar << endl; + } + + // Remove obsolete CheckInterval entry (if it exists) + config->setGroup("General"); + config->deleteEntry("CheckInterval"); + + // Save any updates + config->sync(); +} + +/****************************************************************************** +* Write a client application's details to the config file. +*/ +void ADConfigData::writeClient(const QCString& appName, const ClientInfo* cinfo) +{ + KConfig* config = KGlobal::config(); + config->setGroup(CLIENT_GROUP + QString::fromLocal8Bit(appName)); + config->writeEntry(TITLE_KEY, cinfo->title()); + config->writeEntry(DCOP_OBJECT_KEY, QString::fromLocal8Bit(cinfo->dcopObject())); + config->writeEntry(START_CLIENT_KEY, cinfo->startClient()); + config->writePathEntry(CALENDAR_KEY, cinfo->calendar()->urlString()); + config->sync(); +} + +/****************************************************************************** +* Remove a client application's details from the config file. +*/ +void ADConfigData::removeClient(const QCString& appName) +{ + KConfig* config = KGlobal::config(); + config->deleteGroup(CLIENT_GROUP + QString::fromLocal8Bit(appName)); + config->sync(); +} + +/****************************************************************************** +* Set the calendar file URL for a specified application. +*/ +void ADConfigData::setCalendar(const QCString& appName, ADCalendar* cal) +{ + KConfig* config = KGlobal::config(); + config->setGroup(CLIENT_GROUP + QString::fromLocal8Bit(appName)); + config->writePathEntry(CALENDAR_KEY, cal->urlString()); + config->sync(); +} + +/****************************************************************************** +* DCOP call to set autostart at login on or off. +*/ +void ADConfigData::enableAutoStart(bool on) +{ + kdDebug(5900) << "ADConfigData::enableAutoStart(" << on << ")\n"; + KConfig* config = KGlobal::config(); + config->reparseConfiguration(); + config->setGroup(QString::fromLatin1(DAEMON_AUTOSTART_SECTION)); + config->writeEntry(QString::fromLatin1(DAEMON_AUTOSTART_KEY), on); + config->sync(); +} + diff --git a/kalarm/kalarmd/adconfigdata.h b/kalarm/kalarmd/adconfigdata.h new file mode 100644 index 000000000..2c841fdff --- /dev/null +++ b/kalarm/kalarmd/adconfigdata.h @@ -0,0 +1,41 @@ +/* + * adconfigdata.h - configuration file access + * Program: KAlarm's alarm daemon (kalarmd) + * Copyright (C) 2001, 2004 by David Jarvie <software@astrojar.org.uk> + * Based on the original, (c) 1998, 1999 Preston Brown + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef ADCONFIGDATA_H +#define ADCONFIGDATA_H + +#include "clientinfo.h" + +class ADCalendar; +class ClientInfo; + + +class ADConfigData +{ + public: + static void readConfig(); + static void writeClient(const QCString& appName, const ClientInfo*); + static void removeClient(const QCString& appName); + static void setCalendar(const QCString& appName, ADCalendar*); + static void enableAutoStart(bool); +}; + +#endif // ADCONFIGDATA_H diff --git a/kalarm/kalarmd/admain.cpp b/kalarm/kalarmd/admain.cpp new file mode 100644 index 000000000..c0a7792f1 --- /dev/null +++ b/kalarm/kalarmd/admain.cpp @@ -0,0 +1,59 @@ +/* + * admain.cpp - kalarmd main program + * Program: KAlarm's alarm daemon (kalarmd) + * Copyright (c) 2001,2004,2005 by David Jarvie <software@astrojar.org.uk> + * Copyright (c) 2000,2001 Cornelius Schumacher <schumacher@kde.org> + * Copyright (c) 1997-1999 Preston Brown <pbrown@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarmd.h" + +#include <stdlib.h> + +#include <klocale.h> +#include <kcmdlineargs.h> +#include <kaboutdata.h> +#include <kstartupinfo.h> + +#include "adapp.h" + +static const KCmdLineOptions options[] = +{ + { "autostart", "", 0 }, + KCmdLineLastOption +}; + +int main(int argc, char** argv) +{ + KAboutData aboutData(DAEMON_APP_NAME, I18N_NOOP("KAlarm Daemon"), + DAEMON_VERSION, I18N_NOOP("KAlarm Alarm Daemon"), KAboutData::License_GPL, + "Copyright 1997-1999 Preston Brown\nCopyright 2000-2001 Cornelius Schumacher\nCopyright 2001,2004-2007 David Jarvie", 0, + "http://www.astrojar.org.uk/kalarm"); + aboutData.addAuthor("David Jarvie", I18N_NOOP("Maintainer"), "software@astrojar.org.uk"); + aboutData.addAuthor("Cornelius Schumacher", I18N_NOOP("Author"), "schumacher@kde.org"); + aboutData.addAuthor("Preston Brown", I18N_NOOP("Original Author"), "pbrown@kde.org"); + + KCmdLineArgs::init(argc, argv, &aboutData); + KCmdLineArgs::addCmdLineOptions(options); + KUniqueApplication::addCmdLineOptions(); + KStartupInfo::disableAutoAppStartedSending(); + + if (!AlarmDaemonApp::start()) + exit(0); + AlarmDaemonApp app; + return app.exec(); +} diff --git a/kalarm/kalarmd/alarmdaemon.cpp b/kalarm/kalarmd/alarmdaemon.cpp new file mode 100644 index 000000000..576678574 --- /dev/null +++ b/kalarm/kalarmd/alarmdaemon.cpp @@ -0,0 +1,614 @@ +/* + * alarmdaemon.cpp - alarm daemon control routines + * Program: KAlarm's alarm daemon (kalarmd) + * Copyright © 2001,2004-2007 by David Jarvie <software@astrojar.org.uk> + * Based on the original, (c) 1998, 1999 Preston Brown + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarmd.h" + +#include <unistd.h> +#include <stdlib.h> + +#include <qtimer.h> +#include <qfile.h> +#include <qdatetime.h> + +#include <kapplication.h> +#include <kstandarddirs.h> +#include <kprocess.h> +#include <kio/netaccess.h> +#include <dcopclient.h> +#include <kconfig.h> +#include <kdebug.h> + +#include <libkcal/calendarlocal.h> +#include <libkcal/icalformat.h> + +#include "adcalendar.h" +#include "adconfigdata.h" +#include "alarmguiiface.h" +#include "alarmguiiface_stub.h" +#include "alarmdaemon.moc" + + +#ifdef AUTOSTART_KALARM +// Number of seconds to wait before autostarting KAlarm. +// Allow plenty of time for session restoration to happen first. +static const int KALARM_AUTOSTART_TIMEOUT = 30; +#endif +static const int SECS_PER_DAY = 3600 * 24; + +// KAlarm config file keys +static const QString START_OF_DAY(QString::fromLatin1("StartOfDay")); +static const QString AUTOSTART_TRAY(QString::fromLatin1("AutostartTray")); + + +AlarmDaemon::AlarmDaemon(bool autostart, QObject *parent, const char *name) + : DCOPObject(name), + QObject(parent, name), + mAlarmTimer(0) +{ + kdDebug(5900) << "AlarmDaemon::AlarmDaemon()" << endl; + ADConfigData::readConfig(); + + ADConfigData::enableAutoStart(true); // switch autostart on whenever the program is run + + readKAlarmConfig(); // read time-related KAlarm config items + +#ifdef AUTOSTART_KALARM + if (autostart) + { + /* The alarm daemon is being autostarted. + * Check if KAlarm needs to be autostarted in the system tray. + * This should ideally be handled internally by KAlarm, but is done by kalarmd + * for the following reason: + * KAlarm needs to be both session restored and autostarted, but KDE doesn't + * currently cater properly for this - there is no guarantee that the session + * restoration activation will come before the autostart activation. If they + * come in the wrong order, KAlarm won't know that it is supposed to restore + * itself and instead will simply open a new window. + */ + KConfig kaconfig(locate("config", "kalarmrc")); + kaconfig.setGroup(QString::fromLatin1("General")); + autostart = kaconfig.readBoolEntry(AUTOSTART_TRAY, false); + if (autostart) + { + kdDebug(5900) << "AlarmDaemon::AlarmDaemon(): wait to autostart KAlarm\n"; + QTimer::singleShot(KALARM_AUTOSTART_TIMEOUT * 1000, this, SLOT(autostartKAlarm())); + } + } + if (!autostart) +#endif + startMonitoring(); // otherwise, start monitoring alarms now +} + +/****************************************************************************** +* DCOP call to quit the program. +*/ +void AlarmDaemon::quit() +{ + kdDebug(5900) << "AlarmDaemon::quit()" << endl; + exit(0); +} + +/****************************************************************************** +* Called after a timer delay to autostart KAlarm in the system tray. +*/ +void AlarmDaemon::autostartKAlarm() +{ +#ifdef AUTOSTART_KALARM + if (mAlarmTimer) + { + kdDebug(5900) << "AlarmDaemon::autostartKAlarm(): KAlarm already registered\n"; + return; // KAlarm has already registered with us + } + kdDebug(5900) << "AlarmDaemon::autostartKAlarm(): starting KAlarm\n"; + QStringList args; + args << QString::fromLatin1("--tray"); + int ret = KApplication::kdeinitExec(QString::fromLatin1("kalarm"), args); + if (ret) + kdError(5900) << "AlarmDaemon::autostartKAlarm(): error=" << ret << endl; + else + kdDebug(5900) << "AlarmDaemon::autostartKAlarm(): success" << endl; + + startMonitoring(); +#endif +} + +/****************************************************************************** +* Start monitoring alarms. +*/ +void AlarmDaemon::startMonitoring() +{ + // Set up the alarm timer + mAlarmTimer = new QTimer(this); + connect(mAlarmTimer, SIGNAL(timeout()), SLOT(checkAlarmsSlot())); + setTimerStatus(); + + // Start monitoring calendar files. + // They are monitored until their client application registers, upon which + // monitoring ceases until KAlarm tells the daemon to monitor it. + checkAlarms(); +} + +/****************************************************************************** +* DCOP call to enable or disable monitoring of a calendar. +*/ +void AlarmDaemon::enableCal(const QString& urlString, bool enable) +{ + kdDebug(5900) << "AlarmDaemon::enableCal(" << urlString << ")" << endl; + ADCalendar* cal = ADCalendar::getCalendar(urlString); + if (cal) + { + cal->setEnabled(enable); + notifyCalStatus(cal); // notify KAlarm + } +} + +/****************************************************************************** +* DCOP call to reload, and optionally reset, the specified calendar. +*/ +void AlarmDaemon::reloadCal(const QCString& appname, const QString& urlString, bool reset) +{ + kdDebug(5900) << "AlarmDaemon::reloadCal(" << urlString << ")" << endl; + ADCalendar* cal = ADCalendar::getCalendar(urlString); + if (!cal || cal->appName() != appname) + return; + reloadCal(cal, reset); +} + +/****************************************************************************** +* Reload the specified calendar. +* If 'reset' is true, the data associated with the calendar is reset. +*/ +void AlarmDaemon::reloadCal(ADCalendar* cal, bool reset) +{ + kdDebug(5900) << "AlarmDaemon::reloadCal(): calendar" << endl; + if (!cal) + return; + if (!cal->downloading()) + { + cal->close(); + if (!cal->setLoadedConnected()) + connect(cal, SIGNAL(loaded(ADCalendar*, bool)), SLOT(calendarLoaded(ADCalendar*, bool))); + cal->loadFile(reset); + } + else if (reset) + cal->clearEventsHandled(); +} + +void AlarmDaemon::calendarLoaded(ADCalendar* cal, bool success) +{ + if (success) + kdDebug(5900) << "Calendar reloaded" << endl; + notifyCalStatus(cal); // notify KAlarm + setTimerStatus(); + checkAlarms(cal); +} + +/****************************************************************************** +* DCOP call to notify the daemon that an event has been handled, and optionally +* to tell it to reload the calendar. +*/ +void AlarmDaemon::eventHandled(const QCString& appname, const QString& calendarUrl, const QString& eventID, bool reload) +{ + QString urlString = expandURL(calendarUrl); + kdDebug(5900) << "AlarmDaemon::eventHandled(" << urlString << (reload ? "): reload" : ")") << endl; + ADCalendar* cal = ADCalendar::getCalendar(urlString); + if (!cal || cal->appName() != appname) + return; + cal->setEventHandled(eventID); + if (reload) + reloadCal(cal, false); +} + +/****************************************************************************** +* DCOP call to add an application to the list of client applications, +* and add it to the config file. +* N.B. This method must not return a bool because DCOPClient::call() can cause +* a hang if the daemon happens to send a notification to KAlarm at the +* same time as KAlarm calls this DCCOP method. +*/ +void AlarmDaemon::registerApp(const QCString& appName, const QString& appTitle, + const QCString& dcopObject, const QString& calendarUrl, + bool startClient) +{ + kdDebug(5900) << "AlarmDaemon::registerApp(" << appName << ", " << appTitle << ", " + << dcopObject << ", " << startClient << ")" << endl; + KAlarmd::RegisterResult result; + if (appName.isEmpty()) + result = KAlarmd::FAILURE; + else if (startClient && KStandardDirs::findExe(appName).isNull()) + { + kdError() << "AlarmDaemon::registerApp(): app not found" << endl; + result = KAlarmd::NOT_FOUND; + } + else + { + ADCalendar* keepCal = 0; + ClientInfo* client = ClientInfo::get(appName); + if (client) + { + // The application is already a client. + // If it's the same calendar file, don't delete its calendar object. + if (client->calendar() && client->calendar()->urlString() == calendarUrl) + { + keepCal = client->calendar(); + client->detachCalendar(); + } + ClientInfo::remove(appName); // this deletes the calendar if not detached + } + + if (keepCal) + client = new ClientInfo(appName, appTitle, dcopObject, keepCal, startClient); + else + client = new ClientInfo(appName, appTitle, dcopObject, calendarUrl, startClient); + client->calendar()->setUnregistered(false); + ADConfigData::writeClient(appName, client); + + ADConfigData::enableAutoStart(true); + setTimerStatus(); + notifyCalStatus(client->calendar()); + result = KAlarmd::SUCCESS; + } + + // Notify the client of whether the call succeeded. + AlarmGuiIface_stub stub(appName, dcopObject); + stub.registered(false, result, DAEMON_VERSION_NUM); + kdDebug(5900) << "AlarmDaemon::registerApp() -> " << result << endl; +} + +/****************************************************************************** +* DCOP call to change whether KAlarm should be started when an event needs to +* be notified to it. +* N.B. This method must not return a bool because DCOPClient::call() can cause +* a hang if the daemon happens to send a notification to KAlarm at the +* same time as KAlarm calls this DCCOP method. +*/ +void AlarmDaemon::registerChange(const QCString& appName, bool startClient) +{ + kdDebug(5900) << "AlarmDaemon::registerChange(" << appName << ", " << startClient << ")" << endl; + KAlarmd::RegisterResult result; + ClientInfo* client = ClientInfo::get(appName); + if (!client) + return; // can't access client to tell it the result + if (startClient && KStandardDirs::findExe(appName).isNull()) + { + kdError() << "AlarmDaemon::registerChange(): app not found" << endl; + result = KAlarmd::NOT_FOUND; + } + else + { + client->setStartClient(startClient); + ADConfigData::writeClient(appName, client); + result = KAlarmd::SUCCESS; + } + + // Notify the client of whether the call succeeded. + AlarmGuiIface_stub stub(appName, client->dcopObject()); + stub.registered(true, result, DAEMON_VERSION_NUM); + kdDebug(5900) << "AlarmDaemon::registerChange() -> " << result << endl; +} + +/****************************************************************************** +* DCOP call to set autostart at login on or off. +*/ +void AlarmDaemon::enableAutoStart(bool on) +{ + ADConfigData::enableAutoStart(on); +} + +/****************************************************************************** +* Check if any alarms are pending for any enabled calendar, and display the +* pending alarms. +* Called by the alarm timer. +*/ +void AlarmDaemon::checkAlarmsSlot() +{ + kdDebug(5901) << "AlarmDaemon::checkAlarmsSlot()" << endl; + if (mAlarmTimerSyncing) + { + // We've synched to the minute boundary. Now set timer to the check interval. + mAlarmTimer->changeInterval(DAEMON_CHECK_INTERVAL * 1000); + mAlarmTimerSyncing = false; + mAlarmTimerSyncCount = 10; // resynch every 10 minutes, in case of glitches + } + else if (--mAlarmTimerSyncCount <= 0) + { + int interval = DAEMON_CHECK_INTERVAL + 1 - QTime::currentTime().second(); + if (interval < DAEMON_CHECK_INTERVAL - 1) + { + // Need to re-synch to 1 second past the minute + mAlarmTimer->changeInterval(interval * 1000); + mAlarmTimerSyncing = true; + kdDebug(5900) << "Resynching alarm timer" << endl; + } + else + mAlarmTimerSyncCount = 10; + } + checkAlarms(); +} + +/****************************************************************************** +* Check if any alarms are pending for any enabled calendar, and display the +* pending alarms. +*/ +void AlarmDaemon::checkAlarms() +{ + kdDebug(5901) << "AlarmDaemon::checkAlarms()" << endl; + for (ADCalendar::ConstIterator it = ADCalendar::begin(); it != ADCalendar::end(); ++it) + checkAlarms(*it); +} + +/****************************************************************************** +* Check if any alarms are pending for a specified calendar, and display the +* pending alarms. +*/ +void AlarmDaemon::checkAlarms(ADCalendar* cal) +{ + kdDebug(5901) << "AlarmDaemons::checkAlarms(" << cal->urlString() << ")" << endl; + if (!cal->loaded() || !cal->enabled()) + return; + + QDateTime now = QDateTime::currentDateTime(); + kdDebug(5901) << " To: " << now.toString() << endl; + QValueList<KCal::Alarm*> alarms = cal->alarmsTo(now); + if (!alarms.count()) + return; + QValueList<KCal::Event*> eventsDone; + for (QValueList<KCal::Alarm*>::ConstIterator it = alarms.begin(); it != alarms.end(); ++it) + { + KCal::Event* event = dynamic_cast<KCal::Event*>((*it)->parent()); + if (!event || eventsDone.find(event) != eventsDone.end()) + continue; // either not an event, or the event has already been processed + eventsDone += event; + const QString& eventID = event->uid(); + kdDebug(5901) << "AlarmDaemon::checkAlarms(): event " << eventID << endl; + + // Check which of the alarms for this event are due. + // The times in 'alarmtimes' corresponding to due alarms are set. + // The times for non-due alarms are set invalid in 'alarmtimes'. + bool recurs = event->doesRecur(); + const QStringList cats = event->categories(); + bool floats = (cats.find(QString::fromLatin1("DATE")) != cats.end()); + QDateTime nextDateTime = event->dtStart(); + if (recurs) + { + QString prop = event->customProperty("KALARM", "NEXTRECUR"); + if (prop.length() >= 8) + { + // The next due recurrence time is specified + QDate d(prop.left(4).toInt(), prop.mid(4,2).toInt(), prop.mid(6,2).toInt()); + if (d.isValid()) + { + if (floats && prop.length() == 8) + nextDateTime = d; + else if (!floats && prop.length() == 15 && prop[8] == QChar('T')) + { + QTime t(prop.mid(9,2).toInt(), prop.mid(11,2).toInt(), prop.mid(13,2).toInt()); + if (t.isValid()) + nextDateTime = QDateTime(d, t); + } + } + } + } + if (floats) + nextDateTime.setTime(mStartOfDay); + QValueList<QDateTime> alarmtimes; + KCal::Alarm::List alarms = event->alarms(); + for (KCal::Alarm::List::ConstIterator al = alarms.begin(); al != alarms.end(); ++al) + { + KCal::Alarm* alarm = *al; + QDateTime dt; + if (alarm->enabled()) + { + QDateTime dt1; + if (!alarm->hasTime()) + { + // Find the latest recurrence for the alarm. + // Need to do this for alarms with offsets in order to detect + // reminders due for recurrences. + int offset = alarm->hasStartOffset() ? alarm->startOffset().asSeconds() + : alarm->endOffset().asSeconds() + event->dtStart().secsTo(event->dtEnd()); + if (offset) + { + dt1 = nextDateTime.addSecs(floats ? (offset/SECS_PER_DAY)*SECS_PER_DAY : offset); + if (dt1 > now) + dt1 = QDateTime(); + } + } + // Get latest due repetition, or the recurrence time if none + dt = nextDateTime; + if (nextDateTime <= now && alarm->repeatCount() > 0) + { + int snoozeSecs = alarm->snoozeTime() * 60; + int offset = alarm->repeatCount() * snoozeSecs; + QDateTime lastRepetition = nextDateTime.addSecs(floats ? (offset/SECS_PER_DAY)*SECS_PER_DAY : offset); + if (lastRepetition <= now) + dt = lastRepetition; + else + { + int repetition = nextDateTime.secsTo(now) / snoozeSecs; + int offset = repetition * snoozeSecs; + dt = nextDateTime.addSecs(floats ? (offset/SECS_PER_DAY)*SECS_PER_DAY : offset); + } + } + if (!dt.isValid() || dt > now + || dt1.isValid() && dt1 > dt) // already tested dt1 <= now + dt = dt1; + } + alarmtimes.append(dt); + } + if (!cal->eventHandled(event, alarmtimes)) + { + if (notifyEvent(cal, eventID)) + cal->setEventPending(event, alarmtimes); + } + } +} + +/****************************************************************************** +* Send a DCOP message to KAlarm telling it that an alarm should now be handled. +* Reply = false if the event should be held pending until KAlarm can be started. +*/ +bool AlarmDaemon::notifyEvent(ADCalendar* calendar, const QString& eventID) +{ + if (!calendar) + return true; + QCString appname = calendar->appName(); + const ClientInfo* client = ClientInfo::get(appname); + if (!client) + { + kdDebug(5900) << "AlarmDaemon::notifyEvent(" << appname << "): unknown client" << endl; + return false; + } + kdDebug(5900) << "AlarmDaemon::notifyEvent(" << appname << ", " << eventID << "): notification type=" << client->startClient() << endl; + QString id = QString::fromLatin1("ad:") + eventID; // prefix to indicate that the notification if from the daemon + + // Check if the client application is running and ready to receive notification + bool registered = kapp->dcopClient()->isApplicationRegistered(static_cast<const char*>(appname)); + bool ready = registered; + if (registered) + { + // It's running, but check if it has created our DCOP interface yet + QCStringList objects = kapp->dcopClient()->remoteObjects(appname); + if (objects.find(client->dcopObject()) == objects.end()) + ready = false; + } + if (!ready) + { + // KAlarm is not running, or is not yet ready to receive notifications. + if (!client->startClient()) + { + if (registered) + kdDebug(5900) << "AlarmDaemon::notifyEvent(): client not ready\n"; + else + kdDebug(5900) << "AlarmDaemon::notifyEvent(): don't start client\n"; + return false; + } + + // Start KAlarm, using the command line to specify the alarm + KProcess p; + QString cmd = locate("exe", appname); + if (cmd.isEmpty()) + { + kdDebug(5900) << "AlarmDaemon::notifyEvent(): '" << appname << "' not found" << endl; + return true; + } + p << cmd; + p << "--handleEvent" << id << "--calendarURL" << calendar->urlString(); + p.start(KProcess::DontCare); + kdDebug(5900) << "AlarmDaemon::notifyEvent(): used command line" << endl; + return true; + } + + // Notify the client by telling it the calendar URL and event ID + AlarmGuiIface_stub stub(appname, client->dcopObject()); + stub.handleEvent(calendar->urlString(), id); + if (!stub.ok()) + { + kdDebug(5900) << "AlarmDaemon::notifyEvent(): dcop send failed" << endl; + return false; + } + return true; +} + +/****************************************************************************** +* Starts or stops the alarm timer as necessary after a calendar is enabled/disabled. +*/ +void AlarmDaemon::setTimerStatus() +{ +#ifdef AUTOSTART_KALARM + if (!mAlarmTimer) + { + // KAlarm is now running, so start monitoring alarms + startMonitoring(); + return; // startMonitoring() calls this method + } + +#endif + // Count the number of currently loaded calendars whose names should be displayed + int nLoaded = 0; + for (ADCalendar::ConstIterator it = ADCalendar::begin(); it != ADCalendar::end(); ++it) + if ((*it)->loaded()) + ++nLoaded; + + // Start or stop the alarm timer if necessary + if (!mAlarmTimer->isActive() && nLoaded) + { + // Timeout every minute. + // But first synchronise to one second after the minute boundary. + int firstInterval = DAEMON_CHECK_INTERVAL + 1 - QTime::currentTime().second(); + mAlarmTimer->start(1000 * firstInterval); + mAlarmTimerSyncing = (firstInterval != DAEMON_CHECK_INTERVAL); + kdDebug(5900) << "Started alarm timer" << endl; + } + else if (mAlarmTimer->isActive() && !nLoaded) + { + mAlarmTimer->stop(); + kdDebug(5900) << "Stopped alarm timer" << endl; + } +} + +/****************************************************************************** +* Send a DCOP message to to the client which owns the specified calendar, +* notifying it of a change in calendar status. +*/ +void AlarmDaemon::notifyCalStatus(const ADCalendar* cal) +{ + ClientInfo* client = ClientInfo::get(cal); + if (!client) + return; + QCString appname = client->appName(); + if (kapp->dcopClient()->isApplicationRegistered(static_cast<const char*>(appname))) + { + KAlarmd::CalendarStatus change = cal->available() ? (cal->enabled() ? KAlarmd::CALENDAR_ENABLED : KAlarmd::CALENDAR_DISABLED) + : KAlarmd::CALENDAR_UNAVAILABLE; + kdDebug(5900) << "AlarmDaemon::notifyCalStatus() sending:" << appname << " -> " << change << endl; + AlarmGuiIface_stub stub(appname, client->dcopObject()); + stub.alarmDaemonUpdate(change, cal->urlString()); + if (!stub.ok()) + kdError(5900) << "AlarmDaemon::notifyCalStatus(): dcop send failed:" << appname << endl; + } +} + +/****************************************************************************** +* Read all relevant items from KAlarm config. +* Executed on DCOP call to notify a time related value change in the KAlarm +* config file. +*/ +void AlarmDaemon::readKAlarmConfig() +{ + KConfig config(locate("config", "kalarmrc")); + config.setGroup(QString::fromLatin1("General")); + QDateTime defTime(QDate(1900,1,1), QTime()); + mStartOfDay = config.readDateTimeEntry(START_OF_DAY, &defTime).time(); + kdDebug(5900) << "AlarmDaemon::readKAlarmConfig()" << endl; +} + +/****************************************************************************** +* Expand a DCOP call parameter URL to a full URL. +* (We must store full URLs in the calendar data since otherwise later calls to +* reload or remove calendars won't necessarily find a match.) +*/ +QString AlarmDaemon::expandURL(const QString& urlString) +{ + if (urlString.isEmpty()) + return QString(); + return KURL(urlString).url(); +} diff --git a/kalarm/kalarmd/alarmdaemon.h b/kalarm/kalarmd/alarmdaemon.h new file mode 100644 index 000000000..6ab5d0f9a --- /dev/null +++ b/kalarm/kalarmd/alarmdaemon.h @@ -0,0 +1,79 @@ +/* + * alarmdaemon.h - alarm daemon control routines + * Program: KAlarm's alarm daemon (kalarmd) + * Copyright © 2001,2004-2007 by David Jarvie <software@astrojar.org.uk> + * Based on the original, (c) 1998, 1999 Preston Brown + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef ALARMDAEMON_H +#define ALARMDAEMON_H + +#include <libkcal/calendarlocal.h> + +#include "alarmdaemoniface.h" + +class ADCalendar; + + +class AlarmDaemon : public QObject, virtual public AlarmDaemonIface +{ + Q_OBJECT + public: + AlarmDaemon(bool autostart, QObject* parent = 0, const char* name = 0); + + private slots: +//#ifdef AUTOSTART_KALARM + void autostartKAlarm(); +//#endif + void calendarLoaded(ADCalendar*, bool success); + void checkAlarmsSlot(); + void checkAlarms(); + + private: + // DCOP interface + void enableAutoStart(bool enable); + void enableCalendar(const QString& urlString, bool enable) + { enableCal(expandURL(urlString), enable); } + void reloadCalendar(const QCString& appname, const QString& urlString) + { reloadCal(appname, expandURL(urlString), false); } + void resetCalendar(const QCString& appname, const QString& urlString) + { reloadCal(appname, expandURL(urlString), true); } + void registerApp(const QCString& appName, const QString& appTitle, const QCString& dcopObject, + const QString& calendarUrl, bool startClient); + void registerChange(const QCString& appName, bool startClient); + void eventHandled(const QCString& appname, const QString& calendarURL, const QString& eventID, bool reload); + void timeConfigChanged() { readKAlarmConfig(); } + void quit(); + // Other methods + void readKAlarmConfig(); + void startMonitoring(); + void enableCal(const QString& urlString, bool enable); + void reloadCal(const QCString& appname, const QString& urlString, bool reset); + void reloadCal(ADCalendar*, bool reset); + void checkAlarms(ADCalendar*); + bool notifyEvent(ADCalendar*, const QString& eventID); + void notifyCalStatus(const ADCalendar*); + void setTimerStatus(); + static QString expandURL(const QString& urlString); + + QTimer* mAlarmTimer; + int mAlarmTimerSyncCount; // countdown to re-synching the alarm timer + bool mAlarmTimerSyncing; // true while alarm timer interval < 1 minute + QTime mStartOfDay; // start of day for date-only alarms +}; + +#endif // ALARMDAEMON_H diff --git a/kalarm/kalarmd/alarmdaemoniface.h b/kalarm/kalarmd/alarmdaemoniface.h new file mode 100644 index 000000000..708f5b281 --- /dev/null +++ b/kalarm/kalarmd/alarmdaemoniface.h @@ -0,0 +1,45 @@ +/* + * alarmdaemoniface.h - DCOP request interface + * Program: KAlarm's alarm daemon (kalarmd) + * Copyright © 2001,2004-2007 by David Jarvie <software@astrojar.org.uk> + * Copyright (c) 2000,2001 Cornelius Schumacher <schumacher@kde.org> + * Copyright (c) 1997-1999 Preston Brown <pbrown@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef ALARMDAEMONIFACE_H +#define ALARMDAEMONIFACE_H + +#include <dcopobject.h> + + +class AlarmDaemonIface : virtual public DCOPObject +{ + K_DCOP + k_dcop: + virtual ASYNC enableAutoStart(bool enable) = 0; + virtual ASYNC enableCalendar(const QString& urlString, bool enable) = 0; + virtual ASYNC reloadCalendar(const QCString& appname, const QString& urlString) = 0; + virtual ASYNC resetCalendar(const QCString& appname, const QString& urlString) = 0; + virtual ASYNC registerApp(const QCString& appName, const QString& appTitle, const QCString& dcopObject, + const QString& calendarUrl, bool startClient) = 0; + virtual ASYNC registerChange(const QCString& appName, bool startClient) = 0; + virtual ASYNC eventHandled(const QCString& appname, const QString& calendarURL, const QString& eventID, bool reload) = 0; + virtual ASYNC timeConfigChanged() = 0; + virtual ASYNC quit() = 0; +}; + +#endif diff --git a/kalarm/kalarmd/alarmguiiface.h b/kalarm/kalarmd/alarmguiiface.h new file mode 100644 index 000000000..7e98c831d --- /dev/null +++ b/kalarm/kalarmd/alarmguiiface.h @@ -0,0 +1,71 @@ +/* + * alarmguiiface.h - DCOP interface which alarm daemon clients must implement + * Program: KAlarm's alarm daemon (kalarmd) + * Copyright © 2001,2004,2007 by David Jarvie <software@astrojar.org.uk> + * Based on the original, (c) 1998, 1999 Preston Brown + * Copyright (c) 2000,2001 Cornelius Schumacher <schumacher@kde.org> + * Copyright (c) 1997-1999 Preston Brown <pbrown@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef DAEMONGUIIFACE_H +#define DAEMONGUIIFACE_H + +#include <dcopobject.h> + +namespace KAlarmd +{ + enum RegisterResult // result code of registerApp() DCOP call + { + FAILURE = 0, + SUCCESS = 1, + NOT_FOUND = 2 // notification type requires client start, but client executable not found + }; + + enum CalendarStatus // parameters to client notification + { + CALENDAR_ENABLED, // calendar is now being monitored + CALENDAR_DISABLED, // calendar is available but not being monitored + CALENDAR_UNAVAILABLE // calendar is unavailable for monitoring + }; +} + +/*============================================================================= += Class: AlarmGuiIface += Client applications should inherit from this class to receive notifications +* from the alarm daemon. +=============================================================================*/ +class AlarmGuiIface : virtual public DCOPObject +{ + K_DCOP + k_dcop: + /** Called to notify a change in status of the calendar. + @param calendarStatus new calendar status. Value is of type CalendarStatus. + */ + virtual ASYNC alarmDaemonUpdate(int calendarStatus, const QString& calendarURL) = 0; + + /** Called to notify that an alarm is due. + */ + virtual ASYNC handleEvent(const QString& calendarURL, const QString& eventID) = 0; + + /** Called to indicate success/failure of (re)register() call. + @param result success/failure code. Value is of type RegisterResult. + @param version kalarmd version, e.g. 50101 indicates 5.1.1. + */ + virtual ASYNC registered(bool reregister, int result, int version) = 0; +}; + +#endif diff --git a/kalarm/kalarmd/clientinfo.cpp b/kalarm/kalarmd/clientinfo.cpp new file mode 100644 index 000000000..21889082e --- /dev/null +++ b/kalarm/kalarmd/clientinfo.cpp @@ -0,0 +1,110 @@ +/* + * clientinfo.cpp - client application information + * Program: KAlarm's alarm daemon (kalarmd) + * Copyright (C) 2001, 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "adcalendar.h" +#include "clientinfo.h" + +QMap<QCString, ClientInfo*> ClientInfo::mClients; + + +ClientInfo::ClientInfo(const QCString& appName, const QString& title, + const QCString& dcopObj, const QString& calendar, bool startClient) + : mAppName(appName), + mTitle(title), + mDcopObject(dcopObj), + mCalendar(new ADCalendar(calendar, appName)), + mStartClient(startClient) +{ + mClients[mAppName] = this; +} + +ClientInfo::ClientInfo(const QCString& appName, const QString& title, + const QCString& dcopObj, ADCalendar* calendar, bool startClient) + : mAppName(appName), + mTitle(title), + mDcopObject(dcopObj), + mCalendar(calendar), + mStartClient(startClient) +{ + mClients[mAppName] = this; +} + +ClientInfo::~ClientInfo() +{ + delete mCalendar; + mClients.remove(mAppName); +} + +/****************************************************************************** +* Set a new calendar for the specified client application. +*/ +ADCalendar* ClientInfo::setCalendar(const QString& url) +{ + if (url != mCalendar->urlString()) + { + delete mCalendar; + mCalendar = new ADCalendar(url, mAppName); + } + return mCalendar; +} + +/****************************************************************************** +* Return the ClientInfo object for the specified client application. +*/ +ClientInfo* ClientInfo::get(const QCString& appName) +{ + if (appName.isEmpty()) + return 0; + QMap<QCString, ClientInfo*>::ConstIterator it = mClients.find(appName); + if (it == mClients.end()) + return 0; + return it.data(); +} + +/****************************************************************************** +* Return the ClientInfo object for client which owns the specified calendar. +*/ +ClientInfo* ClientInfo::get(const ADCalendar* cal) +{ + for (ClientInfo::ConstIterator it = ClientInfo::begin(); it != ClientInfo::end(); ++it) + if (it.data()->calendar() == cal) + return it.data(); + return 0; +} + +/****************************************************************************** +* Delete all clients. +*/ +void ClientInfo::clear() +{ + QMap<QCString, ClientInfo*>::Iterator it; + while ((it = mClients.begin()) != mClients.end()) + delete it.data(); +} + +/****************************************************************************** +* Delete the client with the specified name. +*/ +void ClientInfo::remove(const QCString& appName) +{ + QMap<QCString, ClientInfo*>::Iterator it = mClients.find(appName); + if (it != mClients.end()) + delete it.data(); +} diff --git a/kalarm/kalarmd/clientinfo.h b/kalarm/kalarmd/clientinfo.h new file mode 100644 index 000000000..fe6491d7e --- /dev/null +++ b/kalarm/kalarmd/clientinfo.h @@ -0,0 +1,72 @@ +/* + * clientinfo.h - client application information + * Program: KAlarm's alarm daemon (kalarmd) + * Copyright (C) 2001, 2004 by David Jarvie <software@astrojar.org.uk> + * Based on the original, (c) 1998, 1999 Preston Brown + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef _CALCLIENT_H +#define _CALCLIENT_H + +#include <qcstring.h> +#include <qstring.h> +#include <qmap.h> + +class ADCalendar; + + +/*============================================================================= += Class: ClientInfo += Details of a KAlarm client application. +=============================================================================*/ +class ClientInfo +{ + public: + typedef QMap<QCString, ClientInfo*>::ConstIterator ConstIterator; + + ClientInfo(const QCString &appName, const QString &title, const QCString &dcopObj, + const QString& calendar, bool startClient); + ClientInfo(const QCString &appName, const QString &title, const QCString &dcopObj, + ADCalendar* calendar, bool startClient); + ~ClientInfo(); + ADCalendar* setCalendar(const QString& url); + void detachCalendar() { mCalendar = 0; } + void setStartClient(bool start) { mStartClient = start; } + + QCString appName() const { return mAppName; } + QString title() const { return mTitle; } + QCString dcopObject() const { return mDcopObject; } + ADCalendar* calendar() const { return mCalendar; } + bool startClient() const { return mStartClient; } + + static ConstIterator begin() { return mClients.begin(); } + static ConstIterator end() { return mClients.end(); } + static ClientInfo* get(const QCString& appName); + static ClientInfo* get(const ADCalendar*); + static void remove(const QCString& appName); + static void clear(); + + private: + static QMap<QCString, ClientInfo*> mClients; // list of all constructed clients + QCString mAppName; // client's executable and DCOP name + QString mTitle; // application title for display purposes + QCString mDcopObject; // object to receive DCOP messages + ADCalendar* mCalendar; // this client's event calendar + bool mStartClient; // whether to notify events via command line if client app isn't running +}; + +#endif diff --git a/kalarm/kalarmd/kalarmd.autostart.desktop b/kalarm/kalarmd/kalarmd.autostart.desktop new file mode 100644 index 000000000..de5add486 --- /dev/null +++ b/kalarm/kalarmd/kalarmd.autostart.desktop @@ -0,0 +1,103 @@ +# KDE Config File +[Desktop Entry] +Name=KAlarm Daemon +Name[af]=KAlarm Bediener +Name[bg]=Демон на KAlarm +Name[br]=Diaoul KAlarm +Name[ca]=Dimoni KAlarm +Name[cs]=KAlarm démon +Name[da]=KAlarm Dæmon +Name[de]=KAlarm Erinnerungsprogramm +Name[el]=Δαίμονας KAlarm +Name[eo]=KAlarm-demono +Name[es]=Daemon de KAlarm +Name[et]=KAlarmi häiredeemon +Name[eu]=KAlarm deabrua +Name[fa]=شبح KAlarm +Name[fi]=Hälytyspalvelin +Name[fr]=Démon d'alarme +Name[ga]=Deamhan KAlarm +Name[gl]=Daemon de KAlarm +Name[he]=תהליך הרקע תזכורות +Name[hu]=KAlarm szolgáltatás +Name[is]=KAlarm þjónn +Name[it]=Demone degli avvisi +Name[ja]=KAlarm デーモン +Name[ka]=KAlarm დემონი +Name[kk]=KAlarm қызметі +Name[km]=ដេមិន KAlarm +Name[lt]=KAlarm tarnyba +Name[mk]=Даемон за КАларм +Name[ms]=Daemon KAlarm +Name[nb]=KAlarm-nisse +Name[nds]=KAlarm-Dämoon +Name[ne]=केडीई संसूचक डेइमन +Name[nn]=KAlarm-nisse +Name[pl]=Demon alarmowy +Name[pt]=Servidor do KAlarm +Name[pt_BR]=Servidor do KAlarm +Name[ru]=Служба уведомлений +Name[sk]=KAlarm démon +Name[sl]=Demon KAlarm +Name[sr]=Демон KAlarm-а +Name[sr@Latn]=Demon KAlarm-a +Name[sv]=Alarmdemon +Name[ta]=கேஅலாரம் டெமான் +Name[tr]=KAlarm Servis Programı +Name[uk]=Демон KAlarm +Name[zh_CN]=KAlarm 进程 +Name[zh_TW]=KAlarm 守護程式 +Exec=kalarmd --autostart +Icon=kalarmd +Type=Application +Comment=KAlarm alarm daemon autostart at login +Comment[af]=Begin KAlarm bediener outomaties tydens aanteken +Comment[bg]=Автоматично стартиране на процеса на алармата KAlarm при влизане в системата +Comment[ca]=Dimoni d'inici automàtic de l'alarma KAlarm en connectar +Comment[cs]=Automatické spouštění alarmovacího démona při startu +Comment[da]=KAlarm-alarmdæmon autostart ved login +Comment[de]=Autostart des KAlarm Erinnerungsprogramms von KOrganizer bei der Anmeldung +Comment[el]=Αυτόματη εκκίνηση του δαίμονα KAlarm κατά τη σύνδεση +Comment[es]=Inicio automático al ingresar del daemon de alarma de KAlarm +Comment[et]=KAlarmi häiredeemoni automaatne käivitamine +Comment[eu]=KAlarm alarma deabrua saioa hastean automatikoki abiatzen da +Comment[fa]=آغاز خودکار هشدار KAlarm در ورود +Comment[fi]=KOrganizer/KAlarm-hälytyspalvelimen automaattikäynnistys sisäänkirjautuessa +Comment[fr]=Le démon d'alarme de KOrganizer et de KAlarm démarre automatiquement lors de la connexion +Comment[fy]=KAlarm alarmdaemon automatysk begjinne by it oanmelden +Comment[gl]=Autoinicio á entrada do daemon de KAlarm +Comment[he]=הפעלה אוטומטית של תהליך הרקע תזכורות של KAlarm בעת ההפעלה +Comment[hu]=A KAlarm emlékeztető szolgáltatás automatikus elindítása +Comment[is]=Ræsa KAlarm áminningaþjónn sjálfkrafa við byrjun setu +Comment[it]=Avvio automatico del demone degli avvisi +Comment[ja]=KAlarm アラームデーモンのログイン時の自動起動 +Comment[ka]=KAlarm მაღვიძარას დემონის ავტოდაწყება შესვლისას +Comment[kk]=KAlarm қызметі жүйеге кіргенде жегіледі +Comment[km]=ចាប់ផ្ដើមដេមិនរោទ៍របស់ KAlarm ពេលចូល +Comment[lt]=KOrganizer/KAlarm priminimų tarnybos automatinis paleidimas prisiregistruojant +Comment[mk]=Даемон за аларми од КАларм - автом. старт при најава +Comment[ms]= Automula daemon penggera KAlarm semasa log masuk +Comment[nb]=start alarmnisse ved innlogging +Comment[nds]=KAlarm-Dämoon bi't Anmellen automaatsch starten +Comment[ne]=लगइनमा केडीई संसूचक संसूचक डेइमन स्वत: सुरुआत हुन्छ +Comment[nl]=KAlarm alarmdaemon automatisch starten bij login +Comment[nn]=Start alarmnisse ved innlogging +Comment[pl]=Demon alarmu KOrganizera uruchamiany przy zalogowaniu +Comment[pt]=Servidor de alarme do KAlarm auto-iniciado no arranque +Comment[pt_BR]=Servidor de alarmes do KAlarm inicia automaticamente no login +Comment[ru]=Служба уведомлений KDE +Comment[sk]=Automatické spustenie kAlarm démona pri štarte +Comment[sl]=Samodejni zagon alarmskega strežnika KAlarma ob zagonu +Comment[sr]=Аутоматско покретање алармног демона KAlarm-а по пријављивању +Comment[sr@Latn]=Automatsko pokretanje alarmnog demona KAlarm-a po prijavljivanju +Comment[sv]=Kalarm-alarmdemon, automatisk start vid inloggning +Comment[ta]=உள்நுழையும்போது கேஅலாரம் அலாரம் டெமான் தானாகவே துவங்கும் +Comment[tr]=KAlarm alarm servis programı (açılışta başlar) +Comment[uk]=Автозавантаження демона нагадувань KAlarm +Comment[zh_CN]=登录时自动启动 KAlarm 定时守护进程 +Comment[zh_TW]=登入時自動啟動 KAlarm 鬧鐘守護程式 +Terminal=false +NoDisplay=true +X-KDE-autostart-phase=2 +X-KDE-autostart-condition=kalarmdrc:General:Autostart:false +X-KDE-StartupNotify=true diff --git a/kalarm/kalarmd/kalarmd.desktop b/kalarm/kalarmd/kalarmd.desktop new file mode 100644 index 000000000..31d3a0fa7 --- /dev/null +++ b/kalarm/kalarmd/kalarmd.desktop @@ -0,0 +1,55 @@ +# KDE Config File +[Desktop Entry] +Name=KAlarm Daemon +Name[af]=KAlarm Bediener +Name[bg]=Демон на KAlarm +Name[br]=Diaoul KAlarm +Name[ca]=Dimoni KAlarm +Name[cs]=KAlarm démon +Name[da]=KAlarm Dæmon +Name[de]=KAlarm Erinnerungsprogramm +Name[el]=Δαίμονας KAlarm +Name[eo]=KAlarm-demono +Name[es]=Daemon de KAlarm +Name[et]=KAlarmi häiredeemon +Name[eu]=KAlarm deabrua +Name[fa]=شبح KAlarm +Name[fi]=Hälytyspalvelin +Name[fr]=Démon d'alarme +Name[ga]=Deamhan KAlarm +Name[gl]=Daemon de KAlarm +Name[he]=תהליך הרקע תזכורות +Name[hu]=KAlarm szolgáltatás +Name[is]=KAlarm þjónn +Name[it]=Demone degli avvisi +Name[ja]=KAlarm デーモン +Name[ka]=KAlarm დემონი +Name[kk]=KAlarm қызметі +Name[km]=ដេមិន KAlarm +Name[lt]=KAlarm tarnyba +Name[mk]=Даемон за КАларм +Name[ms]=Daemon KAlarm +Name[nb]=KAlarm-nisse +Name[nds]=KAlarm-Dämoon +Name[ne]=केडीई संसूचक डेइमन +Name[nn]=KAlarm-nisse +Name[pl]=Demon alarmowy +Name[pt]=Servidor do KAlarm +Name[pt_BR]=Servidor do KAlarm +Name[ru]=Служба уведомлений +Name[sk]=KAlarm démon +Name[sl]=Demon KAlarm +Name[sr]=Демон KAlarm-а +Name[sr@Latn]=Demon KAlarm-a +Name[sv]=Alarmdemon +Name[ta]=கேஅலாரம் டெமான் +Name[tr]=KAlarm Servis Programı +Name[uk]=Демон KAlarm +Name[zh_CN]=KAlarm 进程 +Name[zh_TW]=KAlarm 守護程式 +Exec=kalarmd +Icon=kalarmd +Type=Application +Terminal=false +X-DCOP-ServiceType=Unique +NoDisplay=true diff --git a/kalarm/kalarmd/kalarmd.h b/kalarm/kalarmd/kalarmd.h new file mode 100644 index 000000000..5e45243bf --- /dev/null +++ b/kalarm/kalarmd/kalarmd.h @@ -0,0 +1,40 @@ +/* + * kalarmd.h - global header file + * Program: KAlarm's alarm daemon (kalarmd) + * Copyright © 2004,2005,2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KALARMD_H +#define KALARMD_H + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#define DAEMON_VERSION "4.3" // kalarmd version number string +#define DAEMON_VERSION_NUM 40300 // kalarmd version number integer +#define DAEMON_APP_NAME "kalarmd" // DCOP name of alarm daemon application +#define DAEMON_DCOP_OBJECT "ad" // DCOP name of kalarmd's DCOP interface + +#define DAEMON_CHECK_INTERVAL 60 // the daemon checks calendar files every minute + +#define DAEMON_AUTOSTART_SECTION "General" // daemon's config file section for autostart-at-login +#define DAEMON_AUTOSTART_KEY "Autostart" // daemon's config file entry for autostart-at-login + +#define AUTOSTART_KALARM // fix for KAlarm autostart before session restoration + +#endif // KALARMD_H diff --git a/kalarm/kalarmiface.h b/kalarm/kalarmiface.h new file mode 100644 index 000000000..cd3aaf0cc --- /dev/null +++ b/kalarm/kalarmiface.h @@ -0,0 +1,355 @@ +/* + * kalarmiface.h - DCOP interface to KAlarm + * Program: kalarm + * Copyright © 2004-2006,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KALARMIFACE_H +#define KALARMIFACE_H + +/** @file kalarmiface.h - DCOP interface to KAlarm */ + +// No forward declarations - dcopidl2cpp won't work +#include <dcopobject.h> +#include <kurl.h> +#include <qstringlist.h> +class QString; + +/** KAlarmIface provides a DCOP interface for other applications to request + * KAlarm actions. + */ + +class KAlarmIface : virtual public DCOPObject +{ + K_DCOP + public: + /** Bit values for the @p flags parameter of "scheduleXxxx()" DCOP calls. + * The bit values may be OR'ed together. + * @li REPEAT_AT_LOGIN - repeat the alarm at every login. + * @li BEEP - sound an audible beep when the alarm is displayed. + * @li SPEAK - speak the alarm message when it is displayed. + * @li REPEAT_SOUND - repeat the sound file while the alarm is displayed. + * @li CONFIRM_ACK - closing the alarm message window requires a confirmation prompt. + * @li AUTO_CLOSE - auto-close the alarm window after the late-cancel period. + * @li SCRIPT - the command to execute is a script, not a shell command line. + * @li EXEC_IN_XTERM - execute the command alarm in a terminal window. + * @li EMAIL_BCC - send a blind copy the email to the user. + * @li SHOW_IN_KORG - show the alarm as an event in KOrganizer + * @li DISABLED - set the alarm status to disabled. + */ + enum Flags + { + REPEAT_AT_LOGIN = 0x01, // repeat alarm at every login + BEEP = 0x02, // sound audible beep when alarm is displayed + REPEAT_SOUND = 0x08, // repeat sound file while alarm is displayed + CONFIRM_ACK = 0x04, // closing the alarm message window requires confirmation prompt + AUTO_CLOSE = 0x10, // auto-close alarm window after late-cancel period + EMAIL_BCC = 0x20, // blind copy the email to the user + DISABLED = 0x40, // alarm is currently disabled + SCRIPT = 0x80, // command is a script, not a shell command line + EXEC_IN_XTERM = 0x100, // execute command alarm in terminal window + SPEAK = 0x200, // speak the alarm message when it is displayed + SHOW_IN_KORG = 0x400 // show the alarm as an event in KOrganizer + }; + /** Values for the @p repeatType parameter of "scheduleXxxx()" DCOP calls. + * @li MINUTELY - the repeat interval is measured in minutes. + * @li DAILY - the repeat interval is measured in days. + * @li WEEKLY - the repeat interval is measured in weeks. + * @li MONTHLY - the repeat interval is measured in months. + * @li YEARLY - the repeat interval is measured in years. + */ + enum RecurType + { + MINUTELY = 1, // the repeat interval is measured in minutes + DAILY = 2, // the repeat interval is measured in days + WEEKLY = 3, // the repeat interval is measured in weeks + MONTHLY = 4, // the repeat interval is measured in months + YEARLY = 5 // the repeat interval is measured in years + }; + + k_dcop: + /** Cancel (delete) an already scheduled alarm. + * @param url - The URL (not path) of the calendar file containing the event to be cancelled. + * Used only for integrity checking: the call will fail if it is not KAlarm's + * current calendar file. + * @param eventId - The unique ID of the event to be cancelled, as stored in the calendar file @p url. + */ + virtual bool cancelEvent(const QString& url, const QString& eventId) = 0; + + /** Trigger the immediate display or execution of an alarm, regardless of what time it is scheduled for. + * @param url - The URL (not path) of the calendar file containing the event to be triggered. + * Used only for integrity checking: the call will fail if it is not KAlarm's + * current calendar file. + * @param eventId - The unique ID of the event to be triggered, as stored in the calendar file @p url. + */ + virtual bool triggerEvent(const QString& url, const QString& eventId) = 0; + + /** Schedule a message display alarm. + * @param message The text of the message to display. + * @param startDateTime Start date/time, in the format YYYY-MM-DD[THH:MM[:SS]] or [T]HH:MM[:SS] + * @param lateCancel Late-cancellation period in minutes, or 0 for no cancellation. + * @param flags OR of flag bits defined in Flags enum. + * @param bgColor The background colour for the alarm message window, or QString::null for the + * current default background colour. The string may be in any of the formats + * accepted by QColor::QColor(const QString&). + * @param fgColor The foreground colour for the alarm message, or QString::null for the current + * default foreground colour. The format of the string is the same as for @p bgColor. + * @param font The font for the alarm message, or QString::null for the default message font + * current at the time the message is displayed. The string should be in format + * returned by QFont::toString(). + * @param audioFile The audio file to play when the alarm is displayed, or QString::null for none. + * @param reminderMins The number of minutes in advance of the main alarm and its recurrences to display + * a reminder alarm, or 0 for no reminder. + * @param recurrence Recurrence specification using iCalendar syntax (defined in RFC2445). + * @param repeatInterval Simple repetition repeat interval in minutes, or 0 for no sub-repetition. + * @param repeatCount Simple repetition repeat count (after the first occurrence), or 0 for no sub-repetition. + * @return true if alarm was scheduled successfully, false if configuration errors were found. + */ + virtual bool scheduleMessage(const QString& message, const QString& startDateTime, int lateCancel, unsigned flags, + const QString& bgColor, const QString& fgColor, const QString& font, + const KURL& audioFile, int reminderMins, const QString& recurrence, + int repeatInterval, int repeatCount) = 0; + /** Schedule a message display alarm. + * @param message The text of the message to display. + * @param startDateTime Start date/time, in the format YYYY-MM-DD[THH:MM[:SS]] or [T]HH:MM[:SS] + * @param lateCancel Late-cancellation period in minutes, or 0 for no cancellation. + * @param flags OR of flag bits defined in Flags enum. + * @param bgColor The background colour for the alarm message window, or QString::null for the + * current default background colour. The string may be in any of the formats + * accepted by QColor::QColor(const QString&). + * @param fgColor The foreground colour for the alarm message, or QString::null for the current + * default foreground colour. The format of the string is the same as for @p bgColor. + * @param font The font for the alarm message, or QString::null for the default message font + * current at the time the message is displayed. The string should be in format + * returned by QFont::toString(). + * @param audioFile The audio file to play when the alarm is displayed, or QString::null for none. + * @param reminderMins The number of minutes in advance of the main alarm and its recurrences to display + * a reminder alarm, or 0 for no reminder. + * @param repeatType The time units to use for recurrence. The actual recurrence interval is equal to + * @p repeatType multiplied by @p repeatInterval. + * The value of @p repeatType must a value defined in the RecurType enum. + * @param repeatInterval Recurrence interval in units defined by @p repeatType, or 0 for no recurrence. + * @param repeatCount Recurrence count (after the first occurrence), or 0 for no recurrence. + * @return true if alarm was scheduled successfully, false if configuration errors were found. + */ + virtual bool scheduleMessage(const QString& message, const QString& startDateTime, int lateCancel, unsigned flags, + const QString& bgColor, const QString& fgColor, const QString& font, + const KURL& audioFile, int reminderMins, + int repeatType, int repeatInterval, int repeatCount) = 0; + /** Schedule a message display alarm. + * @param message The text of the message to display. + * @param startDateTime Start date/time, in the format YYYY-MM-DD[THH:MM[:SS]] or [T]HH:MM[:SS] + * @param lateCancel Late-cancellation period in minutes, or 0 for no cancellation. + * @param flags OR of flag bits defined in Flags enum. + * @param bgColor The background colour for the alarm message window, or QString::null for the + * current default background colour. The string may be in any of the formats + * accepted by QColor::QColor(const QString&). + * @param fgColor The foreground colour for the alarm message, or QString::null for the current + * default foreground colour. The format of the string is the same as for @p bgColor. + * @param font The font for the alarm message, or QString::null for the default message font + * current at the time the message is displayed. The string should be in format + * returned by QFont::toString(). + * @param audioFile The audio file to play when the alarm is displayed, or QString::null for none. + * @param reminderMins The number of minutes in advance of the main alarm and its recurrences to display + * a reminder alarm, or 0 for no reminder. + * @param repeatType The time units to use for recurrence. The actual recurrence interval is equal to + * @p repeatType multiplied by @p repeatInterval. + * The value of @p repeatType must a value defined in the RecurType enum. + * @param repeatInterval Recurrence interval in units defined by @p repeatType, or 0 for no recurrence. + * @param endDateTime Date/time after which the recurrence will end. + * @return true if alarm was scheduled successfully, false if configuration errors were found. + */ + virtual bool scheduleMessage(const QString& message, const QString& startDateTime, int lateCancel, unsigned flags, + const QString& bgColor, const QString& fgColor, const QString& font, + const KURL& audioFile, int reminderMins, + int repeatType, int repeatInterval, const QString& endDateTime) = 0; + + /** Schedule a file display alarm. + * @param file The text or image file to display. + * @param startDateTime Start date/time, in the format YYYY-MM-DD[THH:MM[:SS]] or [T]HH:MM[:SS] + * @param lateCancel Late-cancellation period in minutes, or 0 for no cancellation. + * @param flags OR of flag bits defined in Flags enum. + * @param bgColor The background colour for the alarm message window, or QString::null for the + * current default background colour. The string may be in any of the formats + * accepted by QColor::QColor(const QString&). + * @param audioFile The audio file to play when the alarm is displayed, or QString::null for none. + * @param reminderMins The number of minutes in advance of the main alarm and its recurrences to display + * a reminder alarm, or 0 for no reminder. + * @param recurrence Recurrence specification using iCalendar syntax (defined in RFC2445). + * @param repeatInterval Simple repetition repeat interval in minutes, or 0 for no sub-repetition. + * @param repeatCount Simple repetition repeat count (after the first occurrence), or 0 for no sub-repetition. + * @return true if alarm was scheduled successfully, false if configuration errors were found. + */ + virtual bool scheduleFile(const KURL& file, const QString& startDateTime, int lateCancel, unsigned flags, const QString& bgColor, + const KURL& audioFile, int reminderMins, const QString& recurrence, + int repeatInterval, int repeatCount) = 0; + /** Schedule a file display alarm. + * @param file The text or image file to display. + * @param startDateTime Start date/time, in the format YYYY-MM-DD[THH:MM[:SS]] or [T]HH:MM[:SS] + * @param lateCancel Late-cancellation period in minutes, or 0 for no cancellation. + * @param flags OR of flag bits defined in Flags enum. + * @param bgColor The background colour for the alarm message window, or QString::null for the + * current default background colour. The string may be in any of the formats + * accepted by QColor::QColor(const QString&). + * @param audioFile The audio file to play when the alarm is displayed, or QString::null for none. + * @param reminderMins The number of minutes in advance of the main alarm and its recurrences to display + * a reminder alarm, or 0 for no reminder. + * @param repeatType The time units to use for recurrence. The actual recurrence interval is equal to + * @p repeatType multiplied by @p repeatInterval. + * The value of @p repeatType must a value defined in the RecurType enum. + * @param repeatInterval Recurrence interval in units defined by @p repeatType, or 0 for no recurrence. + * @param repeatCount Recurrence count (after the first occurrence), or 0 for no recurrence. + * @return true if alarm was scheduled successfully, false if configuration errors were found. + */ + virtual bool scheduleFile(const KURL& file, const QString& startDateTime, int lateCancel, unsigned flags, const QString& bgColor, + const KURL& audioFile, int reminderMins, int repeatType, int repeatInterval, int repeatCount) = 0; + /** Schedule a file display alarm. + * @param file The text or image file to display. + * @param startDateTime Start date/time, in the format YYYY-MM-DD[THH:MM[:SS]] or [T]HH:MM[:SS] + * @param lateCancel Late-cancellation period in minutes, or 0 for no cancellation. + * @param flags OR of flag bits defined in Flags enum. + * @param bgColor The background colour for the alarm message window, or QString::null for the + * current default background colour. The string may be in any of the formats + * accepted by QColor::QColor(const QString&). + * @param audioFile The audio file to play when the alarm is displayed, or QString::null for none. + * @param reminderMins The number of minutes in advance of the main alarm and its recurrences to display + * a reminder alarm, or 0 for no reminder. + * @param repeatType The time units to use for recurrence. The actual recurrence interval is equal to + * @p repeatType multiplied by @p repeatInterval. + * The value of @p repeatType must a value defined in the RecurType enum. + * @param repeatInterval Recurrence interval in units defined by @p repeatType, or 0 for no recurrence. + * @param endDateTime Date/time after which the recurrence will end. + * @return true if alarm was scheduled successfully, false if configuration errors were found. + */ + virtual bool scheduleFile(const KURL& file, const QString& startDateTime, int lateCancel, unsigned flags, const QString& bgColor, + const KURL& audioFile, int reminderMins, + int repeatType, int repeatInterval, const QString& endDateTime) = 0; + + /** Schedule a command execution alarm. + * @param commandLine The command line or command script to execute. + * @param startDateTime Start date/time, in the format YYYY-MM-DD[THH:MM[:SS]] or [T]HH:MM[:SS] + * @param lateCancel Late-cancellation period in minutes, or 0 for no cancellation. + * @param flags OR of flag bits defined in Flags enum. + * @param recurrence Recurrence specification using iCalendar syntax (defined in RFC2445). + * @param repeatInterval Simple repetition repeat interval in minutes, or 0 for no sub-repetition. + * @param repeatCount Simple repetition repeat count (after the first occurrence), or 0 for no sub-repetition. + * @return true if alarm was scheduled successfully, false if configuration errors were found. + */ + virtual bool scheduleCommand(const QString& commandLine, const QString& startDateTime, int lateCancel, unsigned flags, + const QString& recurrence, int repeatInterval, int repeatCount) = 0; + /** Schedule a command execution alarm. + * @param commandLine The command line or command script to execute. + * @param startDateTime Start date/time, in the format YYYY-MM-DD[THH:MM[:SS]] or [T]HH:MM[:SS] + * @param lateCancel Late-cancellation period in minutes, or 0 for no cancellation. + * @param flags OR of flag bits defined in Flags enum. + * @param repeatType The time units to use for recurrence. The actual recurrence interval is equal to + * @p repeatType multiplied by @p repeatInterval. + * The value of @p repeatType must a value defined in the RecurType enum. + * @param repeatInterval Recurrence interval in units defined by @p repeatType, or 0 for no recurrence. + * @param repeatCount Recurrence count (after the first occurrence), or 0 for no recurrence. + * @return true if alarm was scheduled successfully, false if configuration errors were found. + */ + virtual bool scheduleCommand(const QString& commandLine, const QString& startDateTime, int lateCancel, unsigned flags, + int repeatType, int repeatInterval, int repeatCount) = 0; + /** Schedule a command execution alarm. + * @param commandLine The command line or command script to execute. + * @param startDateTime Start date/time, in the format YYYY-MM-DD[THH:MM[:SS]] or [T]HH:MM[:SS] + * @param lateCancel Late-cancellation period in minutes, or 0 for no cancellation. + * @param flags OR of flag bits defined in Flags enum. + * @param repeatType The time units to use for recurrence. The actual recurrence interval is equal to + * @p repeatType multiplied by @p repeatInterval. + * The value of @p repeatType must a value defined in the RecurType enum. + * @param repeatInterval Recurrence interval in units defined by @p repeatType, or 0 for no recurrence. + * @param endDateTime Date/time after which the recurrence will end. + * @return true if alarm was scheduled successfully, false if configuration errors were found. + */ + virtual bool scheduleCommand(const QString& commandLine, const QString& startDateTime, int lateCancel, unsigned flags, + int repeatType, int repeatInterval, const QString& endDateTime) = 0; + + /** Schedule an email alarm. + * @param fromID The KMail identity to use as the sender of the email, or QString::null to use KAlarm's default sender ID. + * @param addresses Comma-separated list of addresses to send the email to. + * @param subject Subject line of the email. + * @param message Email message's body text. + * @param attachments Comma- or semicolon-separated list of paths or URLs of files to send as + * attachments to the email. + * @param startDateTime Start date/time, in the format YYYY-MM-DD[THH:MM[:SS]] or [T]HH:MM[:SS] + * @param lateCancel Late-cancellation period in minutes, or 0 for no cancellation. + * @param flags OR of flag bits defined in Flags enum. + * @param recurrence Recurrence specification using iCalendar syntax (defined in RFC2445). + * @param repeatInterval Simple repetition repeat interval in minutes, or 0 for no sub-repetition. + * @param repeatCount Simple repetition repeat count (after the first occurrence), or 0 for no sub-repetition. + * @return true if alarm was scheduled successfully, false if configuration errors were found. + */ + virtual bool scheduleEmail(const QString& fromID, const QString& addresses, const QString& subject, const QString& message, + const QString& attachments, const QString& startDateTime, int lateCancel, unsigned flags, + const QString& recurrence, int repeatInterval, int repeatCount) = 0; + /** Schedule an email alarm. + * @param fromID The KMail identity to use as the sender of the email, or QString::null to use KAlarm's default sender ID. + * @param addresses Comma-separated list of addresses to send the email to. + * @param subject Subject line of the email. + * @param message Email message's body text. + * @param attachments Comma- or semicolon-separated list of paths or URLs of files to send as + * attachments to the email. + * @param startDateTime Start date/time, in the format YYYY-MM-DD[THH:MM[:SS]] or [T]HH:MM[:SS] + * @param lateCancel Late-cancellation period in minutes, or 0 for no cancellation. + * @param flags OR of flag bits defined in the Flags enum. + * @param repeatType The time units to use for recurrence. The actual recurrence interval is equal to + * @p repeatType multiplied by @p repeatInterval. + * The value of @p repeatType must a value defined in the RecurType enum. + * @param repeatInterval Recurrence interval in units defined by @p repeatType, or 0 for no recurrence. + * @param repeatCount Recurrence count (after the first occurrence), or 0 for no recurrence. + * @return true if alarm was scheduled successfully, false if configuration errors were found. + */ + virtual bool scheduleEmail(const QString& fromID, const QString& addresses, const QString& subject, const QString& message, + const QString& attachments, const QString& startDateTime, int lateCancel, unsigned flags, + int repeatType, int repeatInterval, int repeatCount) = 0; + /** Schedule an email alarm. + * @param fromID The KMail identity to use as the sender of the email, or QString::null to use KAlarm's default sender ID. + * @param addresses Comma-separated list of addresses to send the email to. + * @param subject Subject line of the email. + * @param message Email message's body text. + * @param attachments Comma- or semicolon-separated list of paths or URLs of files to send as + * attachments to the email. + * @param startDateTime Start date/time, in the format YYYY-MM-DD[THH:MM[:SS]] or [T]HH:MM[:SS] + * @param lateCancel Late-cancellation period in minutes, or 0 for no cancellation. + * @param flags OR of flag bits defined in the Flags enum. + * @param repeatType The time units to use for recurrence. The actual recurrence interval is equal to + * @p repeatType multiplied by @p repeatInterval. + * The value of @p repeatType must a value defined in the RecurType enum. + * @param repeatInterval Recurrence interval in units defined by @p repeatType, or 0 for no recurrence. + * @param endDateTime Date/time after which the recurrence will end. + * @return true if alarm was scheduled successfully, false if configuration errors were found. + */ + virtual bool scheduleEmail(const QString& fromID, const QString& addresses, const QString& subject, const QString& message, + const QString& attachments, const QString& startDateTime, int lateCancel, unsigned flags, + int repeatType, int repeatInterval, const QString& endDateTime) = 0; + /** Open the alarm edit dialog to edit an existing alarm. + * @param eventId The unique ID of the event to be edited, or QString::null to create a new alarm. + * @return false if the alarm could not be found or is read-only, true otherwise. + */ + virtual bool edit(const QString& eventID) = 0; + /** Open the alarm edit dialog to edit a new alarm. + * @param templateName Name of the alarm template to base the new alarm on, or QString::null if none. + * If a template is specified but cannot be found, the alarm edit dialog is still + * opened but is (obviously) not preset with the template. + * @return false if an alarm template was specified but could not be found, true otherwise. + */ + virtual bool editNew(const QString& templateName) = 0; +}; + +#endif // KALARMIFACE_H diff --git a/kalarm/kalarmui.rc b/kalarm/kalarmui.rc new file mode 100644 index 000000000..686271707 --- /dev/null +++ b/kalarm/kalarmui.rc @@ -0,0 +1,67 @@ +<!DOCTYPE kpartgui> +<kpartgui name="kalarm" version="143" > + <ToolBar noMerge="1" name="mainToolBar" > + <Action name="new" /> + <Action name="newFromTempl" /> + <Action name="copy" /> + <Action name="modify" /> + <Action name="delete" /> + <Action name="view" /> + <Separator /> + <Action name="edit_undo" /> + <Action name="edit_redo" /> + <Separator /> + <Action name="edit_find"/> + </ToolBar> + <MenuBar> + <Menu name="file" > + <text>&File</text> + <Action name="templates" /> + <Action name="importAlarms" /> + <Action name="importBirthdays" /> + </Menu> + <Menu name="view" > + <text>&View</text> + <Action name="showAlarmTimes" /> + <Action name="showTimeToAlarms" /> + <Separator/> + <Action name="showExpiredAlarms" /> + <Action name="showInSystemTray" /> + </Menu> + <Menu name="actions" > + <text>&Actions</text> + <Action name="new" /> + <Action name="newFromTempl" /> + <Action name="copy" /> + <Action name="modify" /> + <Action name="delete" /> + <Action name="undelete" /> + <Action name="view" /> + <Action name="disable" /> + <Action name="createTemplate" /> + <Separator/> + <Action name="alarmEnable" /> + <Action name="refreshAlarms" /> + </Menu> + <Menu noMerge="1" name="settings" > + <text>&Settings</text> + <Merge name="StandardToolBarMenuHandler" /> + <Separator/> + <Action name="options_configure_keybinding" /> + <Action name="options_configure_toolbars" /> + <Action name="options_configure" /> + </Menu> + </MenuBar> + <Menu name="listContext" > + <Action name="new" /> + <Action name="newFromTempl" /> + <Action name="copy" /> + <Action name="modify" /> + <Action name="delete" /> + <Action name="undelete" /> + <Action name="view" /> + <Action name="disable" /> + <Action name="createTemplate" /> + </Menu> + <ActionProperties/> +</kpartgui> diff --git a/kalarm/kamail.cpp b/kalarm/kamail.cpp new file mode 100644 index 000000000..3042a270f --- /dev/null +++ b/kalarm/kamail.cpp @@ -0,0 +1,1096 @@ +/* + * kamail.cpp - email functions + * Program: kalarm + * Copyright © 2002-2005,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <stdlib.h> +#include <unistd.h> +#include <time.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <pwd.h> + +#include <qfile.h> +#include <qregexp.h> + +#include <kstandarddirs.h> +#include <dcopclient.h> +#include <dcopref.h> +#include <kmessagebox.h> +#include <kprocess.h> +#include <klocale.h> +#include <kaboutdata.h> +#include <kfileitem.h> +#include <kio/netaccess.h> +#include <ktempfile.h> +#include <kemailsettings.h> +#include <kdebug.h> + +#include <libkpimidentities/identitymanager.h> +#include <libkpimidentities/identity.h> +#include <libemailfunctions/email.h> +#include <libkcal/person.h> + +#include <kmime_header_parsing.h> + +#include "alarmevent.h" +#include "functions.h" +#include "kalarmapp.h" +#include "mainwindow.h" +#include "preferences.h" +#include "kamail.h" + + +namespace HeaderParsing +{ +bool parseAddress( const char* & scursor, const char * const send, + KMime::Types::Address & result, bool isCRLF=false ); +bool parseAddressList( const char* & scursor, const char * const send, + QValueList<KMime::Types::Address> & result, bool isCRLF=false ); +} + +namespace +{ +QString getHostName(); +} + +struct KAMailData +{ + KAMailData(const KAEvent& e, const QString& fr, const QString& bc, bool allownotify) + : event(e), from(fr), bcc(bc), allowNotify(allownotify) { } + const KAEvent& event; + QString from; + QString bcc; + bool allowNotify; +}; + + +QString KAMail::i18n_NeedFromEmailAddress() +{ return i18n("A 'From' email address must be configured in order to execute email alarms."); } + +QString KAMail::i18n_sent_mail() +{ return i18n("KMail folder name: this should be translated the same as in kmail", "sent-mail"); } + +KPIM::IdentityManager* KAMail::mIdentityManager = 0; +KPIM::IdentityManager* KAMail::identityManager() +{ + if (!mIdentityManager) + mIdentityManager = new KPIM::IdentityManager(true); // create a read-only kmail identity manager + return mIdentityManager; +} + + +/****************************************************************************** +* Send the email message specified in an event. +* Reply = true if the message was sent - 'errmsgs' may contain copy error messages. +* = false if the message was not sent - 'errmsgs' contains the error messages. +*/ +bool KAMail::send(const KAEvent& event, QStringList& errmsgs, bool allowNotify) +{ + QString err; + QString from; + KPIM::Identity identity; + if (!event.emailFromId()) + from = Preferences::emailAddress(); + else + { + identity = mIdentityManager->identityForUoid(event.emailFromId()); + if (identity.isNull()) + { + kdError(5950) << "KAMail::send(): identity" << event.emailFromId() << "not found" << endl; + errmsgs = errors(i18n("Invalid 'From' email address.\nKMail identity '%1' not found.").arg(event.emailFromId())); + return false; + } + from = identity.fullEmailAddr(); + if (from.isEmpty()) + { + kdError(5950) << "KAMail::send(): identity" << identity.identityName() << "uoid" << identity.uoid() << ": no email address" << endl; + errmsgs = errors(i18n("Invalid 'From' email address.\nEmail identity '%1' has no email address").arg(identity.identityName())); + return false; + } + } + if (from.isEmpty()) + { + switch (Preferences::emailFrom()) + { + case Preferences::MAIL_FROM_KMAIL: + errmsgs = errors(i18n("No 'From' email address is configured (no default KMail identity found)\nPlease set it in KMail or in the KAlarm Preferences dialog.")); + break; + case Preferences::MAIL_FROM_CONTROL_CENTRE: + errmsgs = errors(i18n("No 'From' email address is configured.\nPlease set it in the KDE Control Center or in the KAlarm Preferences dialog.")); + break; + case Preferences::MAIL_FROM_ADDR: + default: + errmsgs = errors(i18n("No 'From' email address is configured.\nPlease set it in the KAlarm Preferences dialog.")); + break; + } + return false; + } + KAMailData data(event, from, + (event.emailBcc() ? Preferences::emailBccAddress() : QString::null), + allowNotify); + kdDebug(5950) << "KAlarmApp::sendEmail(): To: " << event.emailAddresses(", ") + << "\nSubject: " << event.emailSubject() << endl; + + if (Preferences::emailClient() == Preferences::SENDMAIL) + { + // Use sendmail to send the message + QString textComplete; + QString command = KStandardDirs::findExe(QString::fromLatin1("sendmail"), + QString::fromLatin1("/sbin:/usr/sbin:/usr/lib")); + if (!command.isNull()) + { + command += QString::fromLatin1(" -f "); + command += KPIM::getEmailAddress(from); + command += QString::fromLatin1(" -oi -t "); + textComplete = initHeaders(data, false); + } + else + { + command = KStandardDirs::findExe(QString::fromLatin1("mail")); + if (command.isNull()) + { + errmsgs = errors(i18n("%1 not found").arg(QString::fromLatin1("sendmail"))); // give up + return false; + } + + command += QString::fromLatin1(" -s "); + command += KShellProcess::quote(event.emailSubject()); + + if (!data.bcc.isEmpty()) + { + command += QString::fromLatin1(" -b "); + command += KShellProcess::quote(data.bcc); + } + + command += ' '; + command += event.emailAddresses(" "); // locally provided, okay + } + + // Add the body and attachments to the message. + // (Sendmail requires attachments to have already been included in the message.) + err = appendBodyAttachments(textComplete, event); + if (!err.isNull()) + { + errmsgs = errors(err); + return false; + } + + // Execute the send command + FILE* fd = popen(command.local8Bit(), "w"); + if (!fd) + { + kdError(5950) << "KAMail::send(): Unable to open a pipe to " << command << endl; + errmsgs = errors(); + return false; + } + fwrite(textComplete.local8Bit(), textComplete.length(), 1, fd); + pclose(fd); + + if (Preferences::emailCopyToKMail()) + { + // Create a copy of the sent email in KMail's 'Sent-mail' folder + err = addToKMailFolder(data, "sent-mail", true); + if (!err.isNull()) + errmsgs = errors(err, false); // not a fatal error - continue + } + + if (allowNotify) + notifyQueued(event); + } + else + { + // Use KMail to send the message + err = sendKMail(data); + if (!err.isNull()) + { + errmsgs = errors(err); + return false; + } + } + return true; +} + +/****************************************************************************** +* Send the email message via KMail. +* Reply = reason for failure (which may be the empty string) +* = null string if success. +*/ +QString KAMail::sendKMail(const KAMailData& data) +{ + QString err = KAlarm::runKMail(true); + if (!err.isNull()) + return err; + + // KMail is now running. Determine which DCOP call to use. + bool useSend = false; + QCString sendFunction = "sendMessage(QString,QString,QString,QString,QString,QString,KURL::List)"; + QCStringList funcs = kapp->dcopClient()->remoteFunctions("kmail", "MailTransportServiceIface"); + for (QCStringList::Iterator it=funcs.begin(); it != funcs.end() && !useSend; ++it) + { + QCString func = DCOPClient::normalizeFunctionSignature(*it); + if (func.left(5) == "bool ") + { + func = func.mid(5); + func.replace(QRegExp(" [0-9A-Za-z_:]+"), ""); + useSend = (func == sendFunction); + } + } + + QByteArray callData; + QDataStream arg(callData, IO_WriteOnly); + kdDebug(5950) << "KAMail::sendKMail(): using " << (useSend ? "sendMessage()" : "dcopAddMessage()") << endl; + if (useSend) + { + // This version of KMail has the sendMessage() function, + // which transmits the message immediately. + arg << data.from; + arg << data.event.emailAddresses(", "); + arg << ""; // CC: + arg << data.bcc; + arg << data.event.emailSubject(); + arg << data.event.message(); + arg << KURL::List(data.event.emailAttachments()); + if (!callKMail(callData, "MailTransportServiceIface", sendFunction, "bool")) + return i18n("Error calling KMail"); + } + else + { + // KMail is an older version, so use dcopAddMessage() + // to add the message to the outbox for later transmission. + err = addToKMailFolder(data, "outbox", false); + if (!err.isNull()) + return err; + } + if (data.allowNotify) + notifyQueued(data.event); + return QString::null; +} + +/****************************************************************************** +* Add the message to a KMail folder. +* Reply = reason for failure (which may be the empty string) +* = null string if success. +*/ +QString KAMail::addToKMailFolder(const KAMailData& data, const char* folder, bool checkKmailRunning) +{ + QString err; + if (checkKmailRunning) + err = KAlarm::runKMail(true); + if (err.isNull()) + { + QString message = initHeaders(data, true); + err = appendBodyAttachments(message, data.event); + if (!err.isNull()) + return err; + + // Write to a temporary file for feeding to KMail + KTempFile tmpFile; + tmpFile.setAutoDelete(true); // delete file when it is destructed + QTextStream* stream = tmpFile.textStream(); + if (!stream) + { + kdError(5950) << "KAMail::addToKMailFolder(" << folder << "): Unable to open a temporary mail file" << endl; + return QString(""); + } + *stream << message; + tmpFile.close(); + if (tmpFile.status()) + { + kdError(5950) << "KAMail::addToKMailFolder(" << folder << "): Error " << tmpFile.status() << " writing to temporary mail file" << endl; + return QString(""); + } + + // Notify KMail of the message in the temporary file + QByteArray callData; + QDataStream arg(callData, IO_WriteOnly); + arg << QString::fromLatin1(folder) << tmpFile.name(); + if (callKMail(callData, "KMailIface", "dcopAddMessage(QString,QString)", "int")) + return QString::null; + err = i18n("Error calling KMail"); + } + kdError(5950) << "KAMail::addToKMailFolder(" << folder << "): " << err << endl; + return err; +} + +/****************************************************************************** +* Call KMail via DCOP. The DCOP function must return an 'int'. +*/ +bool KAMail::callKMail(const QByteArray& callData, const QCString& iface, const QCString& function, const QCString& funcType) +{ + QCString replyType; + QByteArray replyData; + if (!kapp->dcopClient()->call("kmail", iface, function, callData, replyType, replyData) + || replyType != funcType) + { + QCString funcname = function; + funcname.replace(QRegExp("(.+$"), "()"); + kdError(5950) << "KAMail::callKMail(): kmail " << funcname << " call failed\n";; + return false; + } + QDataStream replyStream(replyData, IO_ReadOnly); + QCString funcname = function; + funcname.replace(QRegExp("(.+$"), "()"); + if (replyType == "int") + { + int result; + replyStream >> result; + if (result <= 0) + { + kdError(5950) << "KAMail::callKMail(): kmail " << funcname << " call returned error code = " << result << endl; + return false; + } + } + else if (replyType == "bool") + { + bool result; + replyStream >> result; + if (!result) + { + kdError(5950) << "KAMail::callKMail(): kmail " << funcname << " call returned error\n"; + return false; + } + } + return true; +} + +/****************************************************************************** +* Create the headers part of the email. +*/ +QString KAMail::initHeaders(const KAMailData& data, bool dateId) +{ + QString message; + if (dateId) + { + struct timeval tod; + gettimeofday(&tod, 0); + time_t timenow = tod.tv_sec; + char buff[64]; + strftime(buff, sizeof(buff), "Date: %a, %d %b %Y %H:%M:%S %z", localtime(&timenow)); + QString from = data.from; + from.replace(QRegExp("^.*<"), QString::null).replace(QRegExp(">.*$"), QString::null); + message = QString::fromLatin1(buff); + message += QString::fromLatin1("\nMessage-Id: <%1.%2.%3>\n").arg(timenow).arg(tod.tv_usec).arg(from); + } + message += QString::fromLatin1("From: ") + data.from; + message += QString::fromLatin1("\nTo: ") + data.event.emailAddresses(", "); + if (!data.bcc.isEmpty()) + message += QString::fromLatin1("\nBcc: ") + data.bcc; + message += QString::fromLatin1("\nSubject: ") + data.event.emailSubject(); + message += QString::fromLatin1("\nX-Mailer: %1/" KALARM_VERSION).arg(kapp->aboutData()->programName()); + return message; +} + +/****************************************************************************** +* Append the body and attachments to the email text. +* Reply = reason for error +* = 0 if successful. +*/ +QString KAMail::appendBodyAttachments(QString& message, const KAEvent& event) +{ + static const char* textMimeTypes[] = { + "application/x-sh", "application/x-csh", "application/x-shellscript", + "application/x-nawk", "application/x-gawk", "application/x-awk", + "application/x-perl", "application/x-desktop", + 0 + }; + QStringList attachments = event.emailAttachments(); + if (!attachments.count()) + { + // There are no attachments, so simply append the message body + message += "\n\n"; + message += event.message(); + } + else + { + // There are attachments, so the message must be in MIME format + // Create a boundary string + time_t timenow; + time(&timenow); + QCString boundary; + boundary.sprintf("------------_%lu_-%lx=", 2*timenow, timenow); + message += QString::fromLatin1("\nMIME-Version: 1.0"); + message += QString::fromLatin1("\nContent-Type: multipart/mixed;\n boundary=\"%1\"\n").arg(boundary); + + if (!event.message().isEmpty()) + { + // There is a message body + message += QString::fromLatin1("\n--%1\nContent-Type: text/plain\nContent-Transfer-Encoding: 8bit\n\n").arg(boundary); + message += event.message(); + } + + // Append each attachment in turn + QString attachError = i18n("Error attaching file:\n%1"); + for (QStringList::Iterator at = attachments.begin(); at != attachments.end(); ++at) + { + QString attachment = (*at).local8Bit(); + KURL url(attachment); + url.cleanPath(); + KIO::UDSEntry uds; + if (!KIO::NetAccess::stat(url, uds, MainWindow::mainMainWindow())) { + kdError(5950) << "KAMail::appendBodyAttachments(): not found: " << attachment << endl; + return i18n("Attachment not found:\n%1").arg(attachment); + } + KFileItem fi(uds, url); + if (fi.isDir() || !fi.isReadable()) { + kdError(5950) << "KAMail::appendBodyAttachments(): not file/not readable: " << attachment << endl; + return attachError.arg(attachment); + } + + // Check if the attachment is a text file + QString mimeType = fi.mimetype(); + bool text = mimeType.startsWith("text/"); + if (!text) + { + for (int i = 0; !text && textMimeTypes[i]; ++i) + text = (mimeType == textMimeTypes[i]); + } + + message += QString::fromLatin1("\n--%1").arg(boundary); + message += QString::fromLatin1("\nContent-Type: %2; name=\"%3\"").arg(mimeType).arg(fi.text()); + message += QString::fromLatin1("\nContent-Transfer-Encoding: %1").arg(QString::fromLatin1(text ? "8bit" : "BASE64")); + message += QString::fromLatin1("\nContent-Disposition: attachment; filename=\"%4\"\n\n").arg(fi.text()); + + // Read the file contents + QString tmpFile; + if (!KIO::NetAccess::download(url, tmpFile, MainWindow::mainMainWindow())) { + kdError(5950) << "KAMail::appendBodyAttachments(): load failure: " << attachment << endl; + return attachError.arg(attachment); + } + QFile file(tmpFile); + if (!file.open(IO_ReadOnly) ) { + kdDebug(5950) << "KAMail::appendBodyAttachments() tmp load error: " << attachment << endl; + return attachError.arg(attachment); + } + QIODevice::Offset size = file.size(); + char* contents = new char [size + 1]; + Q_LONG bytes = file.readBlock(contents, size); + file.close(); + contents[size] = 0; + bool atterror = false; + if (bytes == -1 || (QIODevice::Offset)bytes < size) { + kdDebug(5950) << "KAMail::appendBodyAttachments() read error: " << attachment << endl; + atterror = true; + } + else if (text) + { + // Text attachment doesn't need conversion + message += contents; + } + else + { + // Convert the attachment to BASE64 encoding + QIODevice::Offset base64Size; + char* base64 = base64Encode(contents, size, base64Size); + if (base64Size == (QIODevice::Offset)-1) { + kdDebug(5950) << "KAMail::appendBodyAttachments() base64 buffer overflow: " << attachment << endl; + atterror = true; + } + else + message += QString::fromLatin1(base64, base64Size); + delete[] base64; + } + delete[] contents; + if (atterror) + return attachError.arg(attachment); + } + message += QString::fromLatin1("\n--%1--\n.\n").arg(boundary); + } + return QString::null; +} + +/****************************************************************************** +* If any of the destination email addresses are non-local, display a +* notification message saying that an email has been queued for sending. +*/ +void KAMail::notifyQueued(const KAEvent& event) +{ + KMime::Types::Address addr; + QString localhost = QString::fromLatin1("localhost"); + QString hostname = getHostName(); + const EmailAddressList& addresses = event.emailAddresses(); + for (QValueList<KCal::Person>::ConstIterator it = addresses.begin(); it != addresses.end(); ++it) + { + QCString email = (*it).email().local8Bit(); + const char* em = email; + if (!email.isEmpty() + && HeaderParsing::parseAddress(em, em + email.length(), addr)) + { + QString domain = addr.mailboxList.first().addrSpec.domain; + if (!domain.isEmpty() && domain != localhost && domain != hostname) + { + QString text = (Preferences::emailClient() == Preferences::KMAIL) + ? i18n("An email has been queued to be sent by KMail") + : i18n("An email has been queued to be sent"); + KMessageBox::information(0, text, QString::null, Preferences::EMAIL_QUEUED_NOTIFY); + return; + } + } + } +} + +/****************************************************************************** +* Return whether any KMail identities exist. +*/ +bool KAMail::identitiesExist() +{ + identityManager(); // create identity manager if not already done + return mIdentityManager->begin() != mIdentityManager->end(); +} + +/****************************************************************************** +* Fetch the uoid of an email identity name or uoid string. +*/ +uint KAMail::identityUoid(const QString& identityUoidOrName) +{ + bool ok; + uint id = identityUoidOrName.toUInt(&ok); + if (!ok || identityManager()->identityForUoid(id).isNull()) + { + identityManager(); // fetch it if not already done + for (KPIM::IdentityManager::ConstIterator it = mIdentityManager->begin(); + it != mIdentityManager->end(); ++it) + { + if ((*it).identityName() == identityUoidOrName) + { + id = (*it).uoid(); + break; + } + } + } + return id; +} + +/****************************************************************************** +* Fetch the user's email address configured in the KDE Control Centre. +*/ +QString KAMail::controlCentreAddress() +{ + KEMailSettings e; + return e.getSetting(KEMailSettings::EmailAddress); +} + +/****************************************************************************** +* Parse a list of email addresses, optionally containing display names, +* entered by the user. +* Reply = the invalid item if error, else empty string. +*/ +QString KAMail::convertAddresses(const QString& items, EmailAddressList& list) +{ + list.clear(); + QCString addrs = items.local8Bit(); + const char* ad = static_cast<const char*>(addrs); + + // parse an address-list + QValueList<KMime::Types::Address> maybeAddressList; + if (!HeaderParsing::parseAddressList(ad, ad + addrs.length(), maybeAddressList)) + return QString::fromLocal8Bit(ad); // return the address in error + + // extract the mailboxes and complain if there are groups + for (QValueList<KMime::Types::Address>::ConstIterator it = maybeAddressList.begin(); + it != maybeAddressList.end(); ++it) + { + QString bad = convertAddress(*it, list); + if (!bad.isEmpty()) + return bad; + } + return QString::null; +} + +#if 0 +/****************************************************************************** +* Parse an email address, optionally containing display name, entered by the +* user, and append it to the specified list. +* Reply = the invalid item if error, else empty string. +*/ +QString KAMail::convertAddress(const QString& item, EmailAddressList& list) +{ + QCString addr = item.local8Bit(); + const char* ad = static_cast<const char*>(addr); + KMime::Types::Address maybeAddress; + if (!HeaderParsing::parseAddress(ad, ad + addr.length(), maybeAddress)) + return item; // error + return convertAddress(maybeAddress, list); +} +#endif + +/****************************************************************************** +* Convert a single KMime::Types address to a KCal::Person instance and append +* it to the specified list. +*/ +QString KAMail::convertAddress(KMime::Types::Address addr, EmailAddressList& list) +{ + if (!addr.displayName.isEmpty()) + { + kdDebug(5950) << "mailbox groups not allowed! Name: \"" << addr.displayName << "\"" << endl; + return addr.displayName; + } + const QValueList<KMime::Types::Mailbox>& mblist = addr.mailboxList; + for (QValueList<KMime::Types::Mailbox>::ConstIterator mb = mblist.begin(); + mb != mblist.end(); ++mb) + { + QString addrPart = (*mb).addrSpec.localPart; + if (!(*mb).addrSpec.domain.isEmpty()) + { + addrPart += QChar('@'); + addrPart += (*mb).addrSpec.domain; + } + list += KCal::Person((*mb).displayName, addrPart); + } + return QString::null; +} + +/* +QString KAMail::convertAddresses(const QString& items, QStringList& list) +{ + EmailAddressList addrs; + QString item = convertAddresses(items, addrs); + if (!item.isEmpty()) + return item; + for (EmailAddressList::Iterator ad = addrs.begin(); ad != addrs.end(); ++ad) + { + item = (*ad).fullName().local8Bit(); + switch (checkAddress(item)) + { + case 1: // OK + list += item; + break; + case 0: // null address + break; + case -1: // invalid address + return item; + } + } + return QString::null; +}*/ + +/****************************************************************************** +* Check the validity of an email address. +* Because internal email addresses don't have to abide by the usual internet +* email address rules, only some basic checks are made. +* Reply = 1 if alright, 0 if empty, -1 if error. +*/ +int KAMail::checkAddress(QString& address) +{ + address = address.stripWhiteSpace(); + // Check that there are no list separator characters present + if (address.find(',') >= 0 || address.find(';') >= 0) + return -1; + int n = address.length(); + if (!n) + return 0; + int start = 0; + int end = n - 1; + if (address[end] == '>') + { + // The email address is in <...> + if ((start = address.find('<')) < 0) + return -1; + ++start; + --end; + } + int i = address.find('@', start); + if (i >= 0) + { + if (i == start || i == end) // check @ isn't the first or last character +// || address.find('@', i + 1) >= 0) // check for multiple @ characters + return -1; + } +/* else + { + // Allow the @ character to be missing if it's a local user + if (!getpwnam(address.mid(start, end - start + 1).local8Bit())) + return false; + } + for (int i = start; i <= end; ++i) + { + char ch = address[i].latin1(); + if (ch == '.' || ch == '@' || ch == '-' || ch == '_' + || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') + || (ch >= '0' && ch <= '9')) + continue; + return false; + }*/ + return 1; +} + +/****************************************************************************** +* Convert a comma or semicolon delimited list of attachments into a +* QStringList. The items are checked for validity. +* Reply = the invalid item if error, else empty string. +*/ +QString KAMail::convertAttachments(const QString& items, QStringList& list) +{ + KURL url; + list.clear(); + int length = items.length(); + for (int next = 0; next < length; ) + { + // Find the first delimiter character (, or ;) + int i = items.find(',', next); + if (i < 0) + i = items.length(); + int sc = items.find(';', next); + if (sc < 0) + sc = items.length(); + if (sc < i) + i = sc; + QString item = items.mid(next, i - next).stripWhiteSpace(); + switch (checkAttachment(item)) + { + case 1: list += item; break; + case 0: break; // empty attachment name + case -1: + default: return item; // error + } + next = i + 1; + } + return QString::null; +} + +#if 0 +/****************************************************************************** +* Convert a comma or semicolon delimited list of attachments into a +* KURL::List. The items are checked for validity. +* Reply = the invalid item if error, else empty string. +*/ +QString KAMail::convertAttachments(const QString& items, KURL::List& list) +{ + KURL url; + list.clear(); + QCString addrs = items.local8Bit(); + int length = items.length(); + for (int next = 0; next < length; ) + { + // Find the first delimiter character (, or ;) + int i = items.find(',', next); + if (i < 0) + i = items.length(); + int sc = items.find(';', next); + if (sc < 0) + sc = items.length(); + if (sc < i) + i = sc; + QString item = items.mid(next, i - next); + switch (checkAttachment(item, &url)) + { + case 1: list += url; break; + case 0: break; // empty attachment name + case -1: + default: return item; // error + } + next = i + 1; + } + return QString::null; +} +#endif + +/****************************************************************************** +* Check for the existence of the attachment file. +* If non-null, '*url' receives the KURL of the attachment. +* Reply = 1 if attachment exists +* = 0 if null name +* = -1 if doesn't exist. +*/ +int KAMail::checkAttachment(QString& attachment, KURL* url) +{ + attachment = attachment.stripWhiteSpace(); + if (attachment.isEmpty()) + { + if (url) + *url = KURL(); + return 0; + } + // Check that the file exists + KURL u = KURL::fromPathOrURL(attachment); + u.cleanPath(); + if (url) + *url = u; + return checkAttachment(u) ? 1 : -1; +} + +/****************************************************************************** +* Check for the existence of the attachment file. +*/ +bool KAMail::checkAttachment(const KURL& url) +{ + KIO::UDSEntry uds; + if (!KIO::NetAccess::stat(url, uds, MainWindow::mainMainWindow())) + return false; // doesn't exist + KFileItem fi(uds, url); + if (fi.isDir() || !fi.isReadable()) + return false; + return true; +} + + +/****************************************************************************** +* Convert a block of memory to Base64 encoding. +* 'outSize' is set to the number of bytes used in the returned block, or to +* -1 if overflow. +* Reply = BASE64 buffer, which the caller must delete[] afterwards. +*/ +char* KAMail::base64Encode(const char* in, QIODevice::Offset size, QIODevice::Offset& outSize) +{ + const int MAX_LINELEN = 72; + static unsigned char dtable[65] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + + char* out = new char [2*size + 5]; + outSize = (QIODevice::Offset)-1; + QIODevice::Offset outIndex = 0; + int lineLength = 0; + for (QIODevice::Offset inIndex = 0; inIndex < size; ) + { + unsigned char igroup[3]; + int n; + for (n = 0; n < 3; ++n) + { + if (inIndex < size) + igroup[n] = (unsigned char)in[inIndex++]; + else + { + igroup[n] = igroup[2] = 0; + break; + } + } + + if (n > 0) + { + unsigned char ogroup[4]; + ogroup[0] = dtable[igroup[0] >> 2]; + ogroup[1] = dtable[((igroup[0] & 3) << 4) | (igroup[1] >> 4)]; + ogroup[2] = dtable[((igroup[1] & 0xF) << 2) | (igroup[2] >> 6)]; + ogroup[3] = dtable[igroup[2] & 0x3F]; + + if (n < 3) + { + ogroup[3] = '='; + if (n < 2) + ogroup[2] = '='; + } + if (outIndex >= size*2) + { + delete[] out; + return 0; + } + for (int i = 0; i < 4; ++i) + { + if (lineLength >= MAX_LINELEN) + { + out[outIndex++] = '\r'; + out[outIndex++] = '\n'; + lineLength = 0; + } + out[outIndex++] = ogroup[i]; + ++lineLength; + } + } + } + + if (outIndex + 2 < size*2) + { + out[outIndex++] = '\r'; + out[outIndex++] = '\n'; + } + outSize = outIndex; + return out; +} + +/****************************************************************************** +* Set the appropriate error messages for a given error string. +*/ +QStringList KAMail::errors(const QString& err, bool sendfail) +{ + QString error1 = sendfail ? i18n("Failed to send email") + : i18n("Error copying sent email to KMail %1 folder").arg(i18n_sent_mail()); + if (err.isEmpty()) + return QStringList(error1); + QStringList errs(QString::fromLatin1("%1:").arg(error1)); + errs += err; + return errs; +} + +/****************************************************************************** +* Get the body of an email, given its serial number. +*/ +QString KAMail::getMailBody(Q_UINT32 serialNumber) +{ + // Get the body of the email from KMail + QCString replyType; + QByteArray replyData; + QByteArray data; + QDataStream arg(data, IO_WriteOnly); + arg << serialNumber; + arg << (int)0; + QString body; + if (kapp->dcopClient()->call("kmail", "KMailIface", "getDecodedBodyPart(Q_UINT32,int)", data, replyType, replyData) + && replyType == "QString") + { + QDataStream reply_stream(replyData, IO_ReadOnly); + reply_stream >> body; + } + else + kdDebug(5950) << "KAMail::getMailBody(): kmail getDecodedBodyPart() call failed\n"; + return body; +} + +namespace +{ +/****************************************************************************** +* Get the local system's host name. +*/ +QString getHostName() +{ + char hname[256]; + if (gethostname(hname, sizeof(hname))) + return QString::null; + return QString::fromLocal8Bit(hname); +} +} + + +/*============================================================================= += HeaderParsing : modified and additional functions. += The following functions are modified from, or additional to, those in += libkdenetwork kmime_header_parsing.cpp. +=============================================================================*/ + +namespace HeaderParsing +{ + +using namespace KMime; +using namespace KMime::Types; +using namespace KMime::HeaderParsing; + +/****************************************************************************** +* New function. +* Allow a local user name to be specified as an email address. +*/ +bool parseUserName( const char* & scursor, const char * const send, + QString & result, bool isCRLF ) { + + QString maybeLocalPart; + QString tmp; + + if ( scursor != send ) { + // first, eat any whitespace + eatCFWS( scursor, send, isCRLF ); + + char ch = *scursor++; + switch ( ch ) { + case '.': // dot + case '@': + case '"': // quoted-string + return false; + + default: // atom + scursor--; // re-set scursor to point to ch again + tmp = QString::null; + if ( parseAtom( scursor, send, result, false /* no 8bit */ ) ) { + if (getpwnam(result.local8Bit())) + return true; + } + return false; // parseAtom can only fail if the first char is non-atext. + } + } + return false; +} + +/****************************************************************************** +* Modified function. +* Allow a local user name to be specified as an email address, and reinstate +* the original scursor on error return. +*/ +bool parseAddress( const char* & scursor, const char * const send, + Address & result, bool isCRLF ) { + // address := mailbox / group + + eatCFWS( scursor, send, isCRLF ); + if ( scursor == send ) return false; + + // first try if it's a single mailbox: + Mailbox maybeMailbox; + const char * oldscursor = scursor; + if ( parseMailbox( scursor, send, maybeMailbox, isCRLF ) ) { + // yes, it is: + result.displayName = QString::null; + result.mailboxList.append( maybeMailbox ); + return true; + } + scursor = oldscursor; + + // KAlarm: Allow a local user name to be specified + // no, it's not a single mailbox. Try if it's a local user name: + QString maybeUserName; + if ( parseUserName( scursor, send, maybeUserName, isCRLF ) ) { + // yes, it is: + maybeMailbox.displayName = QString::null; + maybeMailbox.addrSpec.localPart = maybeUserName; + maybeMailbox.addrSpec.domain = QString::null; + result.displayName = QString::null; + result.mailboxList.append( maybeMailbox ); + return true; + } + scursor = oldscursor; + + Address maybeAddress; + + // no, it's not a single mailbox. Try if it's a group: + if ( !parseGroup( scursor, send, maybeAddress, isCRLF ) ) + { + scursor = oldscursor; // KAlarm: reinstate original scursor on error return + return false; + } + + result = maybeAddress; + return true; +} + +/****************************************************************************** +* Modified function. +* Allow either ',' or ';' to be used as an email address separator. +*/ +bool parseAddressList( const char* & scursor, const char * const send, + QValueList<Address> & result, bool isCRLF ) { + while ( scursor != send ) { + eatCFWS( scursor, send, isCRLF ); + // end of header: this is OK. + if ( scursor == send ) return true; + // empty entry: ignore: + if ( *scursor == ',' || *scursor == ';' ) { scursor++; continue; } // KAlarm: allow ';' as address separator + + // parse one entry + Address maybeAddress; + if ( !parseAddress( scursor, send, maybeAddress, isCRLF ) ) return false; + result.append( maybeAddress ); + + eatCFWS( scursor, send, isCRLF ); + // end of header: this is OK. + if ( scursor == send ) return true; + // comma separating entries: eat it. + if ( *scursor == ',' || *scursor == ';' ) scursor++; // KAlarm: allow ';' as address separator + } + return true; +} + +} // namespace HeaderParsing diff --git a/kalarm/kamail.h b/kalarm/kamail.h new file mode 100644 index 000000000..86c0482a5 --- /dev/null +++ b/kalarm/kamail.h @@ -0,0 +1,65 @@ +/* + * kamail.h - email functions + * Program: kalarm + * Copyright © 2002-2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KAMAIL_H +#define KAMAIL_H + +#include <qstring.h> +#include <qstringlist.h> +class KURL; +class KAEvent; +class EmailAddressList; +namespace KPIM { class IdentityManager; } +namespace KMime { namespace Types { struct Address; } } + +struct KAMailData; + + +class KAMail +{ + public: + static bool send(const KAEvent&, QStringList& errmsgs, bool allowNotify = true); + static int checkAddress(QString& address); + static int checkAttachment(QString& attachment, KURL* = 0); + static bool checkAttachment(const KURL&); + static QString convertAddresses(const QString& addresses, EmailAddressList&); + static QString convertAttachments(const QString& attachments, QStringList& list); + static KPIM::IdentityManager* identityManager(); + static bool identitiesExist(); + static uint identityUoid(const QString& identityUoidOrName); + static QString controlCentreAddress(); + static QString getMailBody(Q_UINT32 serialNumber); + static QString i18n_NeedFromEmailAddress(); + static QString i18n_sent_mail(); + + private: + static KPIM::IdentityManager* mIdentityManager; // KMail identity manager + static QString sendKMail(const KAMailData&); + static QString initHeaders(const KAMailData&, bool dateId); + static QString appendBodyAttachments(QString& message, const KAEvent&); + static QString addToKMailFolder(const KAMailData&, const char* folder, bool checkKmailRunning); + static bool callKMail(const QByteArray& callData, const QCString& iface, const QCString& function, const QCString& funcType); + static QString convertAddress(KMime::Types::Address, EmailAddressList&); + static void notifyQueued(const KAEvent&); + static char* base64Encode(const char* in, QIODevice::Offset size, QIODevice::Offset& outSize); + static QStringList errors(const QString& error = QString::null, bool sendfail = true); +}; + +#endif // KAMAIL_H diff --git a/kalarm/karecurrence.cpp b/kalarm/karecurrence.cpp new file mode 100644 index 000000000..9461d71ec --- /dev/null +++ b/kalarm/karecurrence.cpp @@ -0,0 +1,876 @@ +/* + * karecurrence.cpp - recurrence with special yearly February 29th handling + * Program: kalarm + * Copyright © 2005,2006,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qbitarray.h> +#include <kdebug.h> + +#include <libkcal/icalformat.h> + +#include "datetime.h" +#include "functions.h" +#include "karecurrence.h" + +using namespace KCal; + +/*============================================================================= += Class KARecurrence += The purpose of this class is to represent the restricted range of recurrence += types which are handled by KAlarm, and to translate between these and the += libkcal Recurrence class. In particular, it handles yearly recurrences on += 29th February specially: += += KARecurrence allows annual 29th February recurrences to fall on 28th += February or 1st March, or not at all, in non-leap years. It allows such += 29th February recurrences to be combined with the 29th of other months in += a simple way, represented simply as the 29th of multiple months including += February. For storage in the libkcal calendar, the 29th day of the month += recurrence for other months is combined with a last-day-of-February or a += 60th-day-of-the-year recurrence rule, thereby conforming to RFC2445. +=============================================================================*/ + + +KARecurrence::Feb29Type KARecurrence::mDefaultFeb29 = KARecurrence::FEB29_FEB29; + + +/****************************************************************************** +* Set up a KARecurrence from recurrence parameters, using the start date to +* determine the recurrence day/month as appropriate. +* Only a restricted subset of recurrence types is allowed. +* Reply = true if successful. +*/ +bool KARecurrence::set(Type recurType, int freq, int count, int f29, const DateTime& start, const QDateTime& end) +{ + mCachedType = -1; + RecurrenceRule::PeriodType rrtype; + switch (recurType) + { + case MINUTELY: rrtype = RecurrenceRule::rMinutely; break; + case DAILY: rrtype = RecurrenceRule::rDaily; break; + case WEEKLY: rrtype = RecurrenceRule::rWeekly; break; + case MONTHLY_DAY: rrtype = RecurrenceRule::rMonthly; break; + case ANNUAL_DATE: rrtype = RecurrenceRule::rYearly; break; + case NO_RECUR: rrtype = RecurrenceRule::rNone; break; + default: + return false; + } + if (!init(rrtype, freq, count, f29, start, end)) + return false; + switch (recurType) + { + case WEEKLY: + { + QBitArray days(7); + days.setBit(start.date().dayOfWeek() - 1); + addWeeklyDays(days); + break; + } + case MONTHLY_DAY: + addMonthlyDate(start.date().day()); + break; + case ANNUAL_DATE: + addYearlyDate(start.date().day()); + addYearlyMonth(start.date().month()); + break; + default: + break; + } + return true; +} + +/****************************************************************************** +* Initialise a KARecurrence from recurrence parameters. +* Reply = true if successful. +*/ +bool KARecurrence::init(RecurrenceRule::PeriodType recurType, int freq, int count, int f29, const DateTime& start, + const QDateTime& end) +{ + mCachedType = -1; + Feb29Type feb29Type = (f29 == -1) ? mDefaultFeb29 : static_cast<Feb29Type>(f29); + mFeb29Type = FEB29_FEB29; + clear(); + if (count < -1) + return false; + bool dateOnly = start.isDateOnly(); + if (!count && (!dateOnly && !end.isValid() + || dateOnly && !end.date().isValid())) + return false; + switch (recurType) + { + case RecurrenceRule::rMinutely: + case RecurrenceRule::rDaily: + case RecurrenceRule::rWeekly: + case RecurrenceRule::rMonthly: + case RecurrenceRule::rYearly: + break; + case rNone: + return true; + default: + return false; + } + setNewRecurrenceType(recurType, freq); + if (count) + setDuration(count); + else if (dateOnly) + setEndDate(end.date()); + else + setEndDateTime(end); + QDateTime startdt = start.dateTime(); + if (recurType == RecurrenceRule::rYearly + && feb29Type == FEB29_FEB28 || feb29Type == FEB29_MAR1) + { + int year = startdt.date().year(); + if (!QDate::leapYear(year) + && startdt.date().dayOfYear() == (feb29Type == FEB29_MAR1 ? 60 : 59)) + { + /* The event start date is February 28th or March 1st, but it + * is a recurrence on February 29th (recurring on February 28th + * or March 1st in non-leap years). Adjust the start date to + * be on February 29th in the last previous leap year. + * This is necessary because KARecurrence represents all types + * of 29th February recurrences by a simple 29th February. + */ + while (!QDate::leapYear(--year)) ; + startdt.setDate(QDate(year, 2, 29)); + } + mFeb29Type = feb29Type; + } + if (dateOnly) + setStartDate(startdt.date()); + else + setStartDateTime(startdt); + return true; +} + +/****************************************************************************** + * Initialise the recurrence from an iCalendar RRULE string. + */ +bool KARecurrence::set(const QString& icalRRULE) +{ + static QString RRULE = QString::fromLatin1("RRULE:"); + mCachedType = -1; + clear(); + if (icalRRULE.isEmpty()) + return true; + ICalFormat format; + if (!format.fromString(defaultRRule(true), + (icalRRULE.startsWith(RRULE) ? icalRRULE.mid(RRULE.length()) : icalRRULE))) + return false; + fix(); + return true; +} + +/****************************************************************************** +* Must be called after presetting with a KCal::Recurrence, to convert the +* recurrence to KARecurrence types: +* - Convert hourly recurrences to minutely. +* - Remove all but the first day in yearly date recurrences. +* - Check for yearly recurrences falling on February 29th and adjust them as +* necessary. A 29th of the month rule can be combined with either a 60th day +* of the year rule or a last day of February rule. +*/ +void KARecurrence::fix() +{ + mCachedType = -1; + mFeb29Type = FEB29_FEB29; + int convert = 0; + int days[2] = { 0, 0 }; + RecurrenceRule* rrules[2]; + RecurrenceRule::List rrulelist = rRules(); + RecurrenceRule::List::ConstIterator rr = rrulelist.begin(); + for (int i = 0; i < 2 && rr != rrulelist.end(); ++i, ++rr) + { + RecurrenceRule* rrule = *rr; + rrules[i] = rrule; + bool stop = true; + int rtype = recurrenceType(rrule); + switch (rtype) + { + case rHourly: + // Convert an hourly recurrence to a minutely one + rrule->setRecurrenceType(RecurrenceRule::rMinutely); + rrule->setFrequency(rrule->frequency() * 60); + // fall through to rMinutely + case rMinutely: + case rDaily: + case rWeekly: + case rMonthlyDay: + case rMonthlyPos: + case rYearlyPos: + if (!convert) + ++rr; // remove all rules except the first + break; + case rOther: + if (dailyType(rrule)) + { // it's a daily rule with BYDAYS + if (!convert) + ++rr; // remove all rules except the first + } + break; + case rYearlyDay: + { + // Ensure that the yearly day number is 60 (i.e. Feb 29th/Mar 1st) + if (convert) + { + // This is the second rule. + // Ensure that it can be combined with the first one. + if (days[0] != 29 + || rrule->frequency() != rrules[0]->frequency() + || rrule->startDt() != rrules[0]->startDt()) + break; + } + QValueList<int> ds = rrule->byYearDays(); + if (!ds.isEmpty() && ds.first() == 60) + { + ++convert; // this rule needs to be converted + days[i] = 60; + stop = false; + break; + } + break; // not day 60, so remove this rule + } + case rYearlyMonth: + { + QValueList<int> ds = rrule->byMonthDays(); + if (!ds.isEmpty()) + { + int day = ds.first(); + if (convert) + { + // This is the second rule. + // Ensure that it can be combined with the first one. + if (day == days[0] || day == -1 && days[0] == 60 + || rrule->frequency() != rrules[0]->frequency() + || rrule->startDt() != rrules[0]->startDt()) + break; + } + if (ds.count() > 1) + { + ds.clear(); // remove all but the first day + ds.append(day); + rrule->setByMonthDays(ds); + } + if (day == -1) + { + // Last day of the month - only combine if it's February + QValueList<int> months = rrule->byMonths(); + if (months.count() != 1 || months.first() != 2) + day = 0; + } + if (day == 29 || day == -1) + { + ++convert; // this rule may need to be converted + days[i] = day; + stop = false; + break; + } + } + if (!convert) + ++rr; + break; + } + default: + break; + } + if (stop) + break; + } + + // Remove surplus rules + for ( ; rr != rrulelist.end(); ++rr) + { + removeRRule(*rr); + delete *rr; + } + + QDate end; + int count; + QValueList<int> months; + if (convert == 2) + { + // There are two yearly recurrence rules to combine into a February 29th recurrence. + // Combine the two recurrence rules into a single rYearlyMonth rule falling on Feb 29th. + // Find the duration of the two RRULEs combined, using the shorter of the two if they differ. + if (days[0] != 29) + { + // Swap the two rules so that the 29th rule is the first + RecurrenceRule* rr = rrules[0]; + rrules[0] = rrules[1]; // the 29th rule + rrules[1] = rr; + int d = days[0]; + days[0] = days[1]; + days[1] = d; // the non-29th day + } + // If February is included in the 29th rule, remove it to avoid duplication + months = rrules[0]->byMonths(); + if (months.remove(2)) + rrules[0]->setByMonths(months); + + count = combineDurations(rrules[0], rrules[1], end); + mFeb29Type = (days[1] == 60) ? FEB29_MAR1 : FEB29_FEB28; + } + else if (convert == 1 && days[0] == 60) + { + // There is a single 60th day of the year rule. + // Convert it to a February 29th recurrence. + count = duration(); + if (!count) + end = endDate(); + mFeb29Type = FEB29_MAR1; + } + else + return; + + // Create the new February 29th recurrence + setNewRecurrenceType(RecurrenceRule::rYearly, frequency()); + RecurrenceRule* rrule = defaultRRule(); + months.append(2); + rrule->setByMonths(months); + QValueList<int> ds; + ds.append(29); + rrule->setByMonthDays(ds); + if (count) + setDuration(count); + else + setEndDate(end); +} + +/****************************************************************************** +* Get the next time the recurrence occurs, strictly after a specified time. +*/ +QDateTime KARecurrence::getNextDateTime(const QDateTime& preDateTime) const +{ + switch (type()) + { + case ANNUAL_DATE: + case ANNUAL_POS: + { + Recurrence recur; + writeRecurrence(recur); + return recur.getNextDateTime(preDateTime); + } + default: + return Recurrence::getNextDateTime(preDateTime); + } +} + +/****************************************************************************** +* Get the previous time the recurrence occurred, strictly before a specified time. +*/ +QDateTime KARecurrence::getPreviousDateTime(const QDateTime& afterDateTime) const +{ + switch (type()) + { + case ANNUAL_DATE: + case ANNUAL_POS: + { + Recurrence recur; + writeRecurrence(recur); + return recur.getPreviousDateTime(afterDateTime); + } + default: + return Recurrence::getPreviousDateTime(afterDateTime); + } +} + +/****************************************************************************** +* Initialise a KCal::Recurrence to be the same as this instance. +* Additional recurrence rules are created as necessary if it recurs on Feb 29th. +*/ +void KARecurrence::writeRecurrence(KCal::Recurrence& recur) const +{ + recur.clear(); + recur.setStartDateTime(startDateTime()); + recur.setExDates(exDates()); + recur.setExDateTimes(exDateTimes()); + const RecurrenceRule* rrule = defaultRRuleConst(); + if (!rrule) + return; + int freq = frequency(); + int count = duration(); + static_cast<KARecurrence*>(&recur)->setNewRecurrenceType(rrule->recurrenceType(), freq); + if (count) + recur.setDuration(count); + else + recur.setEndDateTime(endDateTime()); + switch (type()) + { + case DAILY: + if (rrule->byDays().isEmpty()) + break; + // fall through to rWeekly + case WEEKLY: + case MONTHLY_POS: + recur.defaultRRule(true)->setByDays(rrule->byDays()); + break; + case MONTHLY_DAY: + recur.defaultRRule(true)->setByMonthDays(rrule->byMonthDays()); + break; + case ANNUAL_POS: + recur.defaultRRule(true)->setByMonths(rrule->byMonths()); + recur.defaultRRule()->setByDays(rrule->byDays()); + break; + case ANNUAL_DATE: + { + QValueList<int> months = rrule->byMonths(); + QValueList<int> days = monthDays(); + bool special = (mFeb29Type != FEB29_FEB29 && !days.isEmpty() + && days.first() == 29 && months.remove(2)); + RecurrenceRule* rrule1 = recur.defaultRRule(); + rrule1->setByMonths(months); + rrule1->setByMonthDays(days); + if (!special) + break; + + // It recurs on the 29th February. + // Create an additional 60th day of the year, or last day of February, rule. + RecurrenceRule* rrule2 = new RecurrenceRule(); + rrule2->setRecurrenceType(RecurrenceRule::rYearly); + rrule2->setFrequency(freq); + rrule2->setStartDt(startDateTime()); + rrule2->setFloats(doesFloat()); + if (!count) + rrule2->setEndDt(endDateTime()); + if (mFeb29Type == FEB29_MAR1) + { + QValueList<int> ds; + ds.append(60); + rrule2->setByYearDays(ds); + } + else + { + QValueList<int> ds; + ds.append(-1); + rrule2->setByMonthDays(ds); + QValueList<int> ms; + ms.append(2); + rrule2->setByMonths(ms); + } + + if (months.isEmpty()) + { + // Only February recurs. + // Replace the RRULE and keep the recurrence count the same. + if (count) + rrule2->setDuration(count); + recur.unsetRecurs(); + } + else + { + // Months other than February also recur on the 29th. + // Remove February from the list and add a separate RRULE for February. + if (count) + { + rrule1->setDuration(-1); + rrule2->setDuration(-1); + if (count > 0) + { + /* Adjust counts in the two rules to keep the correct occurrence total. + * Note that durationTo() always includes the start date. Since for an + * individual RRULE the start date may not actually be included, we need + * to decrement the count if the start date doesn't actually recur in + * this RRULE. + * Note that if the count is small, one of the rules may not recur at + * all. In that case, retain it so that the February 29th characteristic + * is not lost should the user later change the recurrence count. + */ + QDateTime end = endDateTime(); +kdDebug()<<"29th recurrence: count="<<count<<", end date="<<end.toString()<<endl; + int count1 = rrule1->durationTo(end) + - (rrule1->recursOn(startDate()) ? 0 : 1); + if (count1 > 0) + rrule1->setDuration(count1); + else + rrule1->setEndDt(startDateTime()); + int count2 = rrule2->durationTo(end) + - (rrule2->recursOn(startDate()) ? 0 : 1); + if (count2 > 0) + rrule2->setDuration(count2); + else + rrule2->setEndDt(startDateTime()); + } + } + } + recur.addRRule(rrule2); + break; + } + default: + break; + } +} + +/****************************************************************************** +* Return the date/time of the last recurrence. +*/ +QDateTime KARecurrence::endDateTime() const +{ + if (mFeb29Type == FEB29_FEB29 || duration() <= 1) + { + /* Either it doesn't have any special February 29th treatment, + * it's infinite (count = -1), the end date is specified + * (count = 0), or it ends on the start date (count = 1). + * So just use the normal KCal end date calculation. + */ + return Recurrence::endDateTime(); + } + + /* Create a temporary recurrence rule to find the end date. + * In a standard KCal recurrence, the 29th February only occurs once every + * 4 years. So shift the temporary recurrence date to the 28th to ensure + * that it occurs every year, thus giving the correct occurrence count. + */ + RecurrenceRule* rrule = new RecurrenceRule(); + rrule->setRecurrenceType(RecurrenceRule::rYearly); + QDateTime dt = startDateTime(); + QDate d = dt.date(); + switch (d.day()) + { + case 29: + // The start date is definitely a recurrence date, so shift + // start date to the temporary recurrence date of the 28th + d.setYMD(d.year(), d.month(), 28); + break; + case 28: + if (d.month() != 2 || mFeb29Type != FEB29_FEB28 || QDate::leapYear(d.year())) + { + // Start date is not a recurrence date, so shift it to 27th + d.setYMD(d.year(), d.month(), 27); + } + break; + case 1: + if (d.month() == 3 && mFeb29Type == FEB29_MAR1 && !QDate::leapYear(d.year())) + { + // Start date is a March 1st recurrence date, so shift + // start date to the temporary recurrence date of the 28th + d.setYMD(d.year(), 2, 28); + } + break; + default: + break; + } + dt.setDate(d); + rrule->setStartDt(dt); + rrule->setFloats(doesFloat()); + rrule->setFrequency(frequency()); + rrule->setDuration(duration()); + QValueList<int> ds; + ds.append(28); + rrule->setByMonthDays(ds); + rrule->setByMonths(defaultRRuleConst()->byMonths()); + dt = rrule->endDt(); + delete rrule; + + // We've found the end date for a recurrence on the 28th. Unless that date + // is a real February 28th recurrence, adjust to the actual recurrence date. + if (mFeb29Type == FEB29_FEB28 && dt.date().month() == 2 && !QDate::leapYear(dt.date().year())) + return dt; + return dt.addDays(1); +} + +/****************************************************************************** +* Return the date/time of the last recurrence. +*/ +QDate KARecurrence::endDate() const +{ + QDateTime end = endDateTime(); + return end.isValid() ? end.date() : QDate(); +} + +/****************************************************************************** +* Return whether the event will recur on the specified date. +* The start date only returns true if it matches the recurrence rules. +*/ +bool KARecurrence::recursOn(const QDate& dt) const +{ + if (!Recurrence::recursOn(dt)) + return false; + if (dt != startDate()) + return true; + // We know now that it isn't in EXDATES or EXRULES, + // so we just need to check if it's in RDATES or RRULES + if (rDates().contains(dt)) + return true; + RecurrenceRule::List rulelist = rRules(); + for (RecurrenceRule::List::ConstIterator rr = rulelist.begin(); rr != rulelist.end(); ++rr) + if ((*rr)->recursOn(dt)) + return true; + DateTimeList dtlist = rDateTimes(); + for (DateTimeList::ConstIterator rdt = dtlist.begin(); rdt != dtlist.end(); ++rdt) + if ((*rdt).date() == dt) + return true; + return false; +} + +/****************************************************************************** +* Find the duration of two RRULEs combined. +* Use the shorter of the two if they differ. +*/ +int KARecurrence::combineDurations(const RecurrenceRule* rrule1, const RecurrenceRule* rrule2, QDate& end) const +{ + int count1 = rrule1->duration(); + int count2 = rrule2->duration(); + if (count1 == -1 && count2 == -1) + return -1; + + // One of the RRULEs may not recur at all if the recurrence count is small. + // In this case, its end date will have been set to the start date. + if (count1 && !count2 && rrule2->endDt().date() == startDateTime().date()) + return count1; + if (count2 && !count1 && rrule1->endDt().date() == startDateTime().date()) + return count2; + + /* The duration counts will be different even for RRULEs of the same length, + * because the first RRULE only actually occurs every 4 years. So we need to + * compare the end dates. + */ + if (!count1 || !count2) + count1 = count2 = 0; + // Get the two rules sorted by end date. + QDateTime end1 = rrule1->endDt(); + QDateTime end2 = rrule2->endDt(); + if (end1.date() == end2.date()) + { + end = end1.date(); + return count1 + count2; + } + const RecurrenceRule* rr1; // earlier end date + const RecurrenceRule* rr2; // later end date + if (end2.isValid() + && (!end1.isValid() || end1.date() > end2.date())) + { + // Swap the two rules to make rr1 have the earlier end date + rr1 = rrule2; + rr2 = rrule1; + QDateTime e = end1; + end1 = end2; + end2 = e; + } + else + { + rr1 = rrule1; + rr2 = rrule2; + } + + // Get the date of the next occurrence after the end of the earlier ending rule + RecurrenceRule rr(*rr1); + rr.setDuration(-1); + QDateTime next1(rr.getNextDate(end1).date()); + if (!next1.isValid()) + end = end1.date(); + else + { + if (end2.isValid() && next1 > end2) + { + // The next occurrence after the end of the earlier ending rule + // is later than the end of the later ending rule. So simply use + // the end date of the later rule. + end = end2.date(); + return count1 + count2; + } + QDate prev2 = rr2->getPreviousDate(next1).date(); + end = (prev2 > end1.date()) ? prev2 : end1.date(); + } + if (count2) + count2 = rr2->durationTo(end); + return count1 + count2; +} + +/****************************************************************************** + * Return the longest interval (in minutes) between recurrences. + * Reply = 0 if it never recurs. + */ +int KARecurrence::longestInterval() const +{ + int freq = frequency(); + switch (type()) + { + case MINUTELY: + return freq; + + case DAILY: + { + QValueList<RecurrenceRule::WDayPos> days = defaultRRuleConst()->byDays(); + if (days.isEmpty()) + return freq * 1440; + + // It recurs only on certain days of the week, so the maximum interval + // may be greater than the frequency. + bool ds[7] = { false, false, false, false, false, false, false }; + for (QValueList<RecurrenceRule::WDayPos>::ConstIterator it = days.begin(); it != days.end(); ++it) + if ((*it).pos() == 0) + ds[(*it).day() - 1] = true; + if (freq % 7) + { + // It will recur on every day of the week in some week or other + // (except for those days which are excluded). + int first = -1; + int last = -1; + int maxgap = 1; + for (int i = 0; i < freq*7; i += freq) + { + if (ds[i % 7]) + { + if (first < 0) + first = i; + else if (i - last > maxgap) + maxgap = i - last; + last = i; + } + } + int wrap = freq*7 - last + first; + if (wrap > maxgap) + maxgap = wrap; + return maxgap * 1440; + } + else + { + // It will recur on the same day of the week every time. + // Ensure that the day is a day which is not excluded. + return ds[startDate().dayOfWeek() - 1] ? freq * 1440 : 0; + } + } + case WEEKLY: + { + // Find which days of the week it recurs on, and if on more than + // one, reduce the maximum interval accordingly. + QBitArray ds = days(); + int first = -1; + int last = -1; + int maxgap = 1; + for (int i = 0; i < 7; ++i) + { + if (ds.testBit(KAlarm::localeDayInWeek_to_weekDay(i) - 1)) + { + if (first < 0) + first = i; + else if (i - last > maxgap) + maxgap = i - last; + last = i; + } + } + if (first < 0) + break; // no days recur + int span = last - first; + if (freq > 1) + return (freq*7 - span) * 1440; + if (7 - span > maxgap) + return (7 - span) * 1440; + return maxgap * 1440; + } + case MONTHLY_DAY: + case MONTHLY_POS: + return freq * 1440 * 31; + + case ANNUAL_DATE: + case ANNUAL_POS: + { + // Find which months of the year it recurs on, and if on more than + // one, reduce the maximum interval accordingly. + const QValueList<int> months = yearMonths(); // month list is sorted + if (months.isEmpty()) + break; // no months recur + if (months.count() == 1) + return freq * 1440 * 365; + int first = -1; + int last = -1; + int maxgap = 0; + for (QValueListConstIterator<int> it = months.begin(); it != months.end(); ++it) + { + if (first < 0) + first = *it; + else + { + int span = QDate(2001, last, 1).daysTo(QDate(2001, *it, 1)); + if (span > maxgap) + maxgap = span; + } + last = *it; + } + int span = QDate(2001, first, 1).daysTo(QDate(2001, last, 1)); + if (freq > 1) + return (freq*365 - span) * 1440; + if (365 - span > maxgap) + return (365 - span) * 1440; + return maxgap * 1440; + } + default: + break; + } + return 0; +} + +/****************************************************************************** + * Return the recurrence's period type. + */ +KARecurrence::Type KARecurrence::type() const +{ + if (mCachedType == -1) + mCachedType = type(defaultRRuleConst()); + return static_cast<Type>(mCachedType); +} + +KARecurrence::Type KARecurrence::type(const RecurrenceRule* rrule) +{ + switch (recurrenceType(rrule)) + { + case rMinutely: return MINUTELY; + case rDaily: return DAILY; + case rWeekly: return WEEKLY; + case rMonthlyDay: return MONTHLY_DAY; + case rMonthlyPos: return MONTHLY_POS; + case rYearlyMonth: return ANNUAL_DATE; + case rYearlyPos: return ANNUAL_POS; + default: + if (dailyType(rrule)) + return DAILY; + return NO_RECUR; + } +} + +/****************************************************************************** + * Check if the rule is a daily rule with or without BYDAYS specified. + */ +bool KARecurrence::dailyType(const RecurrenceRule* rrule) +{ + if (rrule->recurrenceType() != RecurrenceRule::rDaily + || !rrule->bySeconds().isEmpty() + || !rrule->byMinutes().isEmpty() + || !rrule->byHours().isEmpty() + || !rrule->byWeekNumbers().isEmpty() + || !rrule->byMonthDays().isEmpty() + || !rrule->byMonths().isEmpty() + || !rrule->bySetPos().isEmpty() + || !rrule->byYearDays().isEmpty()) + return false; + QValueList<RecurrenceRule::WDayPos> days = rrule->byDays(); + if (days.isEmpty()) + return true; + // Check that all the positions are zero (i.e. every time) + bool found = false; + for (QValueList<RecurrenceRule::WDayPos>::ConstIterator it = days.begin(); it != days.end(); ++it) + { + if ((*it).pos() != 0) + return false; + found = true; + } + return found; + +} diff --git a/kalarm/karecurrence.h b/kalarm/karecurrence.h new file mode 100644 index 000000000..ed0fc2fc9 --- /dev/null +++ b/kalarm/karecurrence.h @@ -0,0 +1,89 @@ +/* + * karecurrence.h - recurrence with special yearly February 29th handling + * Program: kalarm + * Copyright © 2005,2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KARECURRENCE_H +#define KARECURRENCE_H + +#include <libkcal/recurrence.h> +class DateTime; + + +class KARecurrence : public KCal::Recurrence +{ + public: + /** The recurrence's period type. + * This is a subset of the possible KCal recurrence types. + */ + enum Type { + NO_RECUR, // does not recur + MINUTELY, // at an hours/minutes interval + DAILY, // daily + WEEKLY, // weekly, on specified weekdays + MONTHLY_POS, // monthly, on specified weekdays in a specified week of the month + MONTHLY_DAY, // monthly, on a specified day of the month + ANNUAL_DATE, // yearly, on a specified date in each of the specified months + ANNUAL_POS // yearly, on specified weekdays in the specified weeks of the specified months + }; + /** The date on which a yearly February 29th recurrence falls in non-leap years */ + enum Feb29Type { + FEB29_FEB29, // February 29th recurrences are omitted in non-leap years + FEB29_MAR1, // February 29th recurrences are on March 1st in non-leap years + FEB29_FEB28 // February 29th recurrences are on February 28th in non-leap years + }; + + KARecurrence() : KCal::Recurrence(), mFeb29Type(FEB29_FEB29), mCachedType(-1) { } + KARecurrence(const KCal::Recurrence& r) : KCal::Recurrence(r) { fix(); } + KARecurrence(const KARecurrence& r) : KCal::Recurrence(r), mFeb29Type(r.mFeb29Type), mCachedType(r.mCachedType) { } + bool set(const QString& icalRRULE); + bool set(Type t, int freq, int count, const DateTime& start, const QDateTime& end) + { return set(t, freq, count, -1, start, end); } + bool set(Type t, int freq, int count, const DateTime& start, const QDateTime& end, Feb29Type f29) + { return set(t, freq, count, f29, start, end); } + bool init(KCal::RecurrenceRule::PeriodType t, int freq, int count, const DateTime& start, const QDateTime& end) + { return init(t, freq, count, -1, start, end); } + bool init(KCal::RecurrenceRule::PeriodType t, int freq, int count, const DateTime& start, const QDateTime& end, Feb29Type f29) + { return init(t, freq, count, f29, start, end); } + void fix(); + void writeRecurrence(KCal::Recurrence&) const; + QDateTime endDateTime() const; + QDate endDate() const; + bool recursOn(const QDate&) const; + QDateTime getNextDateTime(const QDateTime& preDateTime) const; + QDateTime getPreviousDateTime(const QDateTime& afterDateTime) const; + int longestInterval() const; + Type type() const; + static Type type(const KCal::RecurrenceRule*); + static bool dailyType(const KCal::RecurrenceRule*); + Feb29Type feb29Type() const { return mFeb29Type; } + static Feb29Type defaultFeb29Type() { return mDefaultFeb29; } + static void setDefaultFeb29Type(Feb29Type t) { mDefaultFeb29 = t; } + + private: + bool set(Type, int freq, int count, int feb29Type, const DateTime& start, const QDateTime& end); + bool init(KCal::RecurrenceRule::PeriodType, int freq, int count, int feb29Type, const DateTime& start, const QDateTime& end); + int combineDurations(const KCal::RecurrenceRule*, const KCal::RecurrenceRule*, QDate& end) const; + int longestWeeklyInterval(const QBitArray& days, int frequency); + + static Feb29Type mDefaultFeb29; + Feb29Type mFeb29Type; // yearly recurrence on Feb 29th (leap years) / Mar 1st (non-leap years) + mutable int mCachedType; +}; + +#endif // KARECURRENCE_H diff --git a/kalarm/latecancel.cpp b/kalarm/latecancel.cpp new file mode 100644 index 000000000..ed5563b1d --- /dev/null +++ b/kalarm/latecancel.cpp @@ -0,0 +1,161 @@ +/* + * latecancel.cpp - widget to specify cancellation if late + * Program: kalarm + * Copyright (C) 2004, 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qwidgetstack.h> +#include <qlayout.h> +#include <qwhatsthis.h> +#include <klocale.h> +#include <kdialog.h> + +#include "checkbox.h" +#include "latecancel.moc" + + +// Collect these widget labels together to ensure consistent wording and +// translations across different modules. +QString LateCancelSelector::i18n_CancelIfLate() { return i18n("Cancel if late"); } +QString LateCancelSelector::i18n_n_CancelIfLate() { return i18n("Ca&ncel if late"); } +QString LateCancelSelector::i18n_AutoCloseWin() { return i18n("Auto-close window after this time"); } +QString LateCancelSelector::i18n_AutoCloseWinLC() { return i18n("Auto-close window after late-cancelation time"); } +QString LateCancelSelector::i18n_i_AutoCloseWinLC() { return i18n("Auto-close w&indow after late-cancelation time"); } + + +LateCancelSelector::LateCancelSelector(bool allowHourMinute, QWidget* parent, const char* name) + : QFrame(parent, name), + mDateOnly(false), + mReadOnly(false), + mAutoCloseShown(false) +{ + QString whatsThis = i18n("If checked, the alarm will be canceled if it cannot be triggered within the " + "specified period after its scheduled time. Possible reasons for not triggering " + "include your being logged off, X not running, or the alarm daemon not running.\n\n" + "If unchecked, the alarm will be triggered at the first opportunity after " + "its scheduled time, regardless of how late it is."); + + setFrameStyle(QFrame::NoFrame); + mLayout = new QVBoxLayout(this, 0, KDialog::spacingHint()); + + mStack = new QWidgetStack(this); + mCheckboxFrame = new QFrame(mStack); + mCheckboxFrame->setFrameStyle(QFrame::NoFrame); + mStack->addWidget(mCheckboxFrame, 1); + QBoxLayout* layout = new QVBoxLayout(mCheckboxFrame, 0, 0); + mCheckbox = new CheckBox(i18n_n_CancelIfLate(), mCheckboxFrame); + mCheckbox->setFixedSize(mCheckbox->sizeHint()); + connect(mCheckbox, SIGNAL(toggled(bool)), SLOT(slotToggled(bool))); + QWhatsThis::add(mCheckbox, whatsThis); + layout->addWidget(mCheckbox, 0, Qt::AlignAuto); + + mTimeSelectorFrame = new QFrame(mStack); + mTimeSelectorFrame->setFrameStyle(QFrame::NoFrame); + mStack->addWidget(mTimeSelectorFrame, 2); + layout = new QVBoxLayout(mTimeSelectorFrame, 0, 0); + mTimeSelector = new TimeSelector(i18n("Cancel if late by 10 minutes", "Ca&ncel if late by"), QString::null, + whatsThis, i18n("Enter how late will cause the alarm to be canceled"), + allowHourMinute, mTimeSelectorFrame); + connect(mTimeSelector, SIGNAL(toggled(bool)), SLOT(slotToggled(bool))); + layout->addWidget(mTimeSelector); + mLayout->addWidget(mStack); + + layout = new QHBoxLayout(mLayout, KDialog::spacingHint()); + layout->addSpacing(3*KDialog::spacingHint()); + mAutoClose = new CheckBox(i18n_AutoCloseWin(), this); + mAutoClose->setFixedSize(mAutoClose->sizeHint()); + QWhatsThis::add(mAutoClose, i18n("Automatically close the alarm window after the expiry of the late-cancelation period")); + layout->addWidget(mAutoClose); + layout->addStretch(); + + mAutoClose->hide(); + mAutoClose->setEnabled(false); +} + +/****************************************************************************** +* Set the read-only status. +*/ +void LateCancelSelector::setReadOnly(bool ro) +{ + if ((int)ro != (int)mReadOnly) + { + mReadOnly = ro; + mCheckbox->setReadOnly(mReadOnly); + mTimeSelector->setReadOnly(mReadOnly); + mAutoClose->setReadOnly(mReadOnly); + } +} + +int LateCancelSelector::minutes() const +{ + return mTimeSelector->minutes(); +} + +void LateCancelSelector::setMinutes(int minutes, bool dateOnly, TimePeriod::Units defaultUnits) +{ + slotToggled(minutes); + mTimeSelector->setMinutes(minutes, dateOnly, defaultUnits); +} + +void LateCancelSelector::setDateOnly(bool dateOnly) +{ + if (dateOnly != mDateOnly) + { + mDateOnly = dateOnly; + if (mTimeSelector->isChecked()) // don't change when it's not visible + mTimeSelector->setDateOnly(dateOnly); + } +} + +void LateCancelSelector::showAutoClose(bool show) +{ + if (show) + mAutoClose->show(); + else + mAutoClose->hide(); + mAutoCloseShown = show; + mLayout->activate(); +} + +bool LateCancelSelector::isAutoClose() const +{ + return mAutoCloseShown && mAutoClose->isEnabled() && mAutoClose->isChecked(); +} + +void LateCancelSelector::setAutoClose(bool autoClose) +{ + mAutoClose->setChecked(autoClose); +} + +/****************************************************************************** +* Called when either of the checkboxes is toggled. +*/ +void LateCancelSelector::slotToggled(bool on) +{ + mCheckbox->setChecked(on); + mTimeSelector->setChecked(on); + if (on) + { + mTimeSelector->setDateOnly(mDateOnly); + mStack->raiseWidget(mTimeSelectorFrame); + } + else + mStack->raiseWidget(mCheckboxFrame); + mAutoClose->setEnabled(on); +} diff --git a/kalarm/latecancel.h b/kalarm/latecancel.h new file mode 100644 index 000000000..ccbc389b3 --- /dev/null +++ b/kalarm/latecancel.h @@ -0,0 +1,69 @@ +/* + * latecancel.h - widget to specify cancellation if late + * Program: kalarm + * Copyright (C) 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LATECANCEL_H +#define LATECANCEL_H + +#include <qframe.h> + +#include "timeperiod.h" +#include "timeselector.h" +class QBoxLayout; +class QWidgetStack; +class CheckBox; + + +class LateCancelSelector : public QFrame +{ + Q_OBJECT + public: + LateCancelSelector(bool allowHourMinute, QWidget* parent, const char* name = 0); + int minutes() const; + void setMinutes(int Minutes, bool dateOnly, TimePeriod::Units defaultUnits); + void setDateOnly(bool dateOnly); + void showAutoClose(bool show); + bool isAutoClose() const; + void setAutoClose(bool autoClose); + bool isReadOnly() const { return mReadOnly; } + void setReadOnly(bool); + + static QString i18n_CancelIfLate(); // plain text of 'Cancel if late' checkbox + static QString i18n_n_CancelIfLate(); // text of 'Cancel if late' checkbox, with 'N' shortcut + static QString i18n_AutoCloseWin(); // plain text of 'Auto-close window after this time' checkbox + static QString i18n_AutoCloseWinLC(); // plain text of 'Auto-close window after late-cancelation time' checkbox + static QString i18n_i_AutoCloseWinLC(); // text of 'Auto-close window after late-cancelation time' checkbox, with 'I' shortcut + + private slots: + void slotToggled(bool); + + private: + QBoxLayout* mLayout; // overall layout for the widget + QWidgetStack* mStack; // contains mCheckboxFrame and mTimeSelectorFrame + QFrame* mCheckboxFrame; + CheckBox* mCheckbox; // displayed when late cancellation is not selected + QFrame* mTimeSelectorFrame; + TimeSelector* mTimeSelector; // displayed when late cancellation is selected + CheckBox* mAutoClose; + bool mDateOnly; // hours/minutes units not allowed + bool mReadOnly; // widget is read-only + bool mAutoCloseShown; // auto-close checkbox is visible +}; + +#endif // LATECANCEL_H diff --git a/kalarm/lib/Makefile.am b/kalarm/lib/Makefile.am new file mode 100644 index 000000000..7d68a252b --- /dev/null +++ b/kalarm/lib/Makefile.am @@ -0,0 +1,22 @@ +INCLUDES = -I$(top_srcdir)/kalarm -I$(top_srcdir) $(all_includes) + +noinst_LTLIBRARIES = libkalarm.la + +libkalarm_la_METASOURCES = AUTO + +libkalarm_la_SOURCES = \ + buttongroup.cpp checkbox.cpp colourcombo.cpp colourlist.cpp \ + combobox.cpp dateedit.cpp datetime.cpp label.cpp messagebox.cpp \ + pushbutton.cpp radiobutton.cpp timeedit.cpp timespinbox.cpp \ + timeperiod.cpp shellprocess.cpp slider.cpp spinbox.cpp spinbox2.cpp \ + lineedit.cpp synchtimer.cpp + +noinst_HEADERS = \ + buttongroup.h checkbox.h colourcombo.h colourlist.h \ + combobox.h dateedit.h datetime.h label.h lineedit.h messagebox.h \ + pushbutton.h radiobutton.h timeedit.h timespinbox.h \ + timeperiod.h shellprocess.h slider.h spinbox.h spinbox2.h \ + synchtimer.h spinbox2private.h + +DOXYGEN_REFERENCES=kdecore kdeui libkdepim +include $(top_srcdir)/admin/Doxyfile.am diff --git a/kalarm/lib/buttongroup.cpp b/kalarm/lib/buttongroup.cpp new file mode 100644 index 000000000..b448df48a --- /dev/null +++ b/kalarm/lib/buttongroup.cpp @@ -0,0 +1,70 @@ +/* + * buttongroup.cpp - QButtonGroup with an extra signal and KDE 2 compatibility + * Program: kalarm + * Copyright (c) 2002, 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "kalarm.h" + +#include <qlayout.h> +#include <qbutton.h> +#include <kdialog.h> + +#include "buttongroup.moc" + + +ButtonGroup::ButtonGroup(QWidget* parent, const char* name) + : QButtonGroup(parent, name) +{ + connect(this, SIGNAL(clicked(int)), SIGNAL(buttonSet(int))); +} + +ButtonGroup::ButtonGroup(const QString& title, QWidget* parent, const char* name) + : QButtonGroup(title, parent, name) +{ + connect(this, SIGNAL(clicked(int)), SIGNAL(buttonSet(int))); +} + +ButtonGroup::ButtonGroup(int strips, Qt::Orientation orient, QWidget* parent, const char* name) + : QButtonGroup(strips, orient, parent, name) +{ + connect(this, SIGNAL(clicked(int)), SIGNAL(buttonSet(int))); +} + +ButtonGroup::ButtonGroup(int strips, Qt::Orientation orient, const QString& title, QWidget* parent, const char* name) + : QButtonGroup(strips, orient, title, parent, name) +{ + connect(this, SIGNAL(clicked(int)), SIGNAL(buttonSet(int))); +} + +/****************************************************************************** + * Inserts a button in the group. + * This should really be a virtual method... + */ +int ButtonGroup::insert(QButton* button, int id) +{ + id = QButtonGroup::insert(button, id); + connect(button, SIGNAL(toggled(bool)), SLOT(slotButtonToggled(bool))); + return id; +} + +/****************************************************************************** + * Called when one of the member buttons is toggled. + */ +void ButtonGroup::slotButtonToggled(bool) +{ + emit buttonSet(selectedId()); +} diff --git a/kalarm/lib/buttongroup.h b/kalarm/lib/buttongroup.h new file mode 100644 index 000000000..1d647b420 --- /dev/null +++ b/kalarm/lib/buttongroup.h @@ -0,0 +1,90 @@ +/* + * buttongroup.h - QButtonGroup with an extra signal and Qt 2 compatibility + * Program: kalarm + * Copyright © 2002,2004,2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#ifndef BUTTONGROUP_H +#define BUTTONGROUP_H + +#include <qbuttongroup.h> + + +/** + * @short A QButtonGroup with signal on new selection, plus Qt 2 compatibility. + * + * The ButtonGroup class provides an enhanced version of the QButtonGroup class. + * + * It emits an additional signal, buttonSet(int), whenever any of its buttons + * changes state, for whatever reason, including programmatic control. (The + * QButtonGroup class only emits signals when buttons are clicked on by the user.) + * The class also provides Qt 2 compatibility. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class ButtonGroup : public QButtonGroup +{ + Q_OBJECT + public: + /** Constructor. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit ButtonGroup(QWidget* parent, const char* name = 0); + /** Constructor. + * @param title The title displayed for this button group. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + ButtonGroup(const QString& title, QWidget* parent, const char* name = 0); + /** Constructor. + * @param strips The number of rows or columns of buttons. + * @param orient The orientation (Qt::Horizontal or Qt::Vertical) of the button group. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + ButtonGroup(int strips, Qt::Orientation orient, QWidget* parent, const char* name = 0); + /** Constructor. + * @param strips The number of rows or columns of buttons. + * @param orient The orientation (Qt::Horizontal or Qt::Vertical) of the button group. + * @param title The title displayed for this button group. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + ButtonGroup(int strips, Qt::Orientation orient, const QString& title, QWidget* parent, const char* name = 0); + /** Inserts a button in the group. + * This overrides the insert() method of QButtonGroup, which should really be a virtual method... + * @param button The button to insert. + * @param id The identifier for the button. + * @return The identifier of the inserted button. + */ + int insert(QButton* button, int id = -1); + /** Sets the button with the specified identifier to be on. If this is an exclusive group, + * all other buttons in the group will be set off. The buttonSet() signal is emitted. + * @param id The identifier of the button to set on. + */ + virtual void setButton(int id) { QButtonGroup::setButton(id); emit buttonSet(id); } + private slots: + void slotButtonToggled(bool); + signals: + /** Signal emitted whenever whenever any button in the group changes state, + * for whatever reason. + * @param id The identifier of the button which is now selected. + */ + void buttonSet(int id); +}; + +#endif // BUTTONGROUP_H diff --git a/kalarm/lib/checkbox.cpp b/kalarm/lib/checkbox.cpp new file mode 100644 index 000000000..c600a4950 --- /dev/null +++ b/kalarm/lib/checkbox.cpp @@ -0,0 +1,133 @@ +/* + * checkbox.cpp - check box with read-only option + * Program: kalarm + * Copyright (c) 2002, 2003 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "checkbox.moc" + + +CheckBox::CheckBox(QWidget* parent, const char* name) + : QCheckBox(parent, name), + mFocusPolicy(focusPolicy()), + mFocusWidget(0), + mReadOnly(false) +{ } + +CheckBox::CheckBox(const QString& text, QWidget* parent, const char* name) + : QCheckBox(text, parent, name), + mFocusPolicy(focusPolicy()), + mFocusWidget(0), + mReadOnly(false) +{ } + +/****************************************************************************** +* Set the read-only status. If read-only, the checkbox can be toggled by the +* application, but not by the user. +*/ +void CheckBox::setReadOnly(bool ro) +{ + if ((int)ro != (int)mReadOnly) + { + mReadOnly = ro; + setFocusPolicy(ro ? QWidget::NoFocus : mFocusPolicy); + if (ro) + clearFocus(); + } +} + +/****************************************************************************** +* Specify a widget to receive focus when the checkbox is clicked on. +*/ +void CheckBox::setFocusWidget(QWidget* w, bool enable) +{ + mFocusWidget = w; + mFocusWidgetEnable = enable; + if (w) + connect(this, SIGNAL(clicked()), SLOT(slotClicked())); + else + disconnect(this, SIGNAL(clicked()), this, SLOT(slotClicked())); +} + +/****************************************************************************** +* Called when the checkbox is clicked. +* If it is now checked, focus is transferred to any specified focus widget. +*/ +void CheckBox::slotClicked() +{ + if (mFocusWidget && isChecked()) + { + if (mFocusWidgetEnable) + mFocusWidget->setEnabled(true); + mFocusWidget->setFocus(); + } +} + +/****************************************************************************** +* Event handlers to intercept events if in read-only mode. +* Any events which could change the checkbox state are discarded. +*/ +void CheckBox::mousePressEvent(QMouseEvent* e) +{ + if (mReadOnly) + { + // Swallow up the event if it's the left button + if (e->button() == Qt::LeftButton) + return; + } + QCheckBox::mousePressEvent(e); +} + +void CheckBox::mouseReleaseEvent(QMouseEvent* e) +{ + if (mReadOnly) + { + // Swallow up the event if it's the left button + if (e->button() == Qt::LeftButton) + return; + } + QCheckBox::mouseReleaseEvent(e); +} + +void CheckBox::mouseMoveEvent(QMouseEvent* e) +{ + if (!mReadOnly) + QCheckBox::mouseMoveEvent(e); +} + +void CheckBox::keyPressEvent(QKeyEvent* e) +{ + if (mReadOnly) + switch (e->key()) + { + case Qt::Key_Up: + case Qt::Key_Left: + case Qt::Key_Right: + case Qt::Key_Down: + // Process keys which shift the focus + break; + default: + return; + } + QCheckBox::keyPressEvent(e); +} + +void CheckBox::keyReleaseEvent(QKeyEvent* e) +{ + if (!mReadOnly) + QCheckBox::keyReleaseEvent(e); +} diff --git a/kalarm/lib/checkbox.h b/kalarm/lib/checkbox.h new file mode 100644 index 000000000..72ad4aee3 --- /dev/null +++ b/kalarm/lib/checkbox.h @@ -0,0 +1,88 @@ +/* + * checkbox.h - check box with focus widget and read-only options + * Program: kalarm + * Copyright © 2002,2003,2005,2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef CHECKBOX_H +#define CHECKBOX_H + +#include <qcheckbox.h> + + +/** + * @short A QCheckBox with focus widget and read-only options. + * + * The CheckBox class is a QCheckBox with the ability to transfer focus to another + * widget when checked, and with a read-only option. + * + * Another widget may be specified as the focus widget for the check box. Whenever + * the user clicks on the check box so as to set its state to checked, focus is + * automatically transferred to the focus widget. + * + * The widget may be set as read-only. This has the same effect as disabling it, except + * that its appearance is unchanged. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class CheckBox : public QCheckBox +{ + Q_OBJECT + public: + /** Constructor. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit CheckBox(QWidget* parent, const char* name = 0); + /** Constructor. + * @param text Text to display. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + CheckBox(const QString& text, QWidget* parent, const char* name = 0); + /** Returns true if the widget is read only. */ + bool isReadOnly() const { return mReadOnly; } + /** Sets whether the check box is read-only for the user. If read-only, + * its state cannot be changed by the user. + * @param readOnly True to set the widget read-only, false to set it read-write. + */ + virtual void setReadOnly(bool readOnly); + /** Returns the widget which receives focus when the user selects the check box by clicking on it. */ + QWidget* focusWidget() const { return mFocusWidget; } + /** Specifies a widget to receive focus when the user selects the check box by clicking on it. + * @param widget Widget to receive focus. + * @param enable If true, @p widget will be enabled before receiving focus. If + * false, the enabled state of @p widget will be left unchanged when + * the check box is clicked. + */ + void setFocusWidget(QWidget* widget, bool enable = true); + protected: + virtual void mousePressEvent(QMouseEvent*); + virtual void mouseReleaseEvent(QMouseEvent*); + virtual void mouseMoveEvent(QMouseEvent*); + virtual void keyPressEvent(QKeyEvent*); + virtual void keyReleaseEvent(QKeyEvent*); + protected slots: + void slotClicked(); + private: + QWidget::FocusPolicy mFocusPolicy; // default focus policy for the QCheckBox + QWidget* mFocusWidget; // widget to receive focus when button is clicked on + bool mFocusWidgetEnable; // enable focus widget before setting focus + bool mReadOnly; // value cannot be changed +}; + +#endif // CHECKBOX_H diff --git a/kalarm/lib/colourcombo.cpp b/kalarm/lib/colourcombo.cpp new file mode 100644 index 000000000..d5fa052ac --- /dev/null +++ b/kalarm/lib/colourcombo.cpp @@ -0,0 +1,239 @@ +/* + * colourcombo.cpp - colour selection combo box + * Program: kalarm + * Copyright (c) 2001 - 2003, 2005 by David Jarvie <software@astrojar.org.uk> + * + * Some code taken from kdelibs/kdeui/kcolorcombo.cpp in the KDE libraries: + * Copyright (C) 1997 Martin Jones (mjones@kde.org) + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include <qpainter.h> + +#include <klocale.h> +#include <kcolordialog.h> + +#include "preferences.h" +#include "colourcombo.moc" + + +ColourCombo::ColourCombo(QWidget* parent, const char* name, const QColor& defaultColour) + : QComboBox(parent, name), + mColourList(Preferences::messageColours()), + mSelectedColour(defaultColour), + mCustomColour(255, 255, 255), + mReadOnly(false), + mDisabled(false) +{ + addColours(); + connect(this, SIGNAL(activated(int)), SLOT(slotActivated(int))); + connect(this, SIGNAL(highlighted(int)), SLOT(slotHighlighted(int))); + Preferences::connect(SIGNAL(preferencesChanged()), this, SLOT(slotPreferencesChanged())); +} + +void ColourCombo::setColour(const QColor& colour) +{ + mSelectedColour = colour; + addColours(); +} + +/****************************************************************************** +* Set a new colour selection. +*/ +void ColourCombo::setColours(const ColourList& colours) +{ + mColourList = colours; + if (mSelectedColour != mCustomColour + && !mColourList.contains(mSelectedColour)) + { + // The current colour has been deleted + mSelectedColour = mColourList.count() ? mColourList.first() : mCustomColour; + } + addColours(); +} + +/****************************************************************************** +* Called when the user changes the preference settings. +* If the colour list has changed, update the colours displayed. +*/ +void ColourCombo::slotPreferencesChanged() +{ + const ColourList& prefColours = Preferences::messageColours(); + if (prefColours != mColourList) + setColours(prefColours); // update the display with the new colours +} + +/****************************************************************************** +* Enable or disable the control. +* If it is disabled, its colour is set to the dialog background colour. +*/ +void ColourCombo::setEnabled(bool enable) +{ + if (enable && mDisabled) + { + mDisabled = false; + setColour(mSelectedColour); + } + else if (!enable && !mDisabled) + { + mSelectedColour = color(); + int end = count(); + if (end > 1) + { + // Add a dialog background colour item + QPixmap pm = *pixmap(1); + pm.fill(paletteBackgroundColor()); + insertItem(pm); + setCurrentItem(end); + } + mDisabled = true; + } + QComboBox::setEnabled(enable); +} + +void ColourCombo::slotActivated(int index) +{ + if (index) + mSelectedColour = mColourList[index - 1]; + else + { + if (KColorDialog::getColor(mCustomColour, this) == QDialog::Accepted) + { + QRect rect; + drawCustomItem(rect, false); + } + mSelectedColour = mCustomColour; + } + emit activated(mSelectedColour); +} + +void ColourCombo::slotHighlighted(int index) +{ + mSelectedColour = index ? mColourList[index - 1] : mCustomColour; + emit highlighted(mSelectedColour); +} + +/****************************************************************************** +* Initialise the items in the combo box to one for each colour in the list. +*/ +void ColourCombo::addColours() +{ + clear(); + + for (ColourList::const_iterator it = mColourList.begin(); ; ++it) + { + if (it == mColourList.end()) + { + mCustomColour = mSelectedColour; + break; + } + if (mSelectedColour == *it) + break; + } + + QRect rect; + drawCustomItem(rect, true); + + QPainter painter; + QPixmap pixmap(rect.width(), rect.height()); + int i = 1; + for (ColourList::const_iterator it = mColourList.begin(); it != mColourList.end(); ++i, ++it) + { + painter.begin(&pixmap); + QBrush brush(*it); + painter.fillRect(rect, brush); + painter.end(); + + insertItem(pixmap); + pixmap.detach(); + + if (*it == mSelectedColour.rgb()) + setCurrentItem(i); + } +} + +void ColourCombo::drawCustomItem(QRect& rect, bool insert) +{ + QPen pen; + if (qGray(mCustomColour.rgb()) < 128) + pen.setColor(Qt::white); + else + pen.setColor(Qt::black); + + QPainter painter; + QFontMetrics fm = QFontMetrics(painter.font()); + rect.setRect(0, 0, width(), fm.height() + 4); + QPixmap pixmap(rect.width(), rect.height()); + + painter.begin(&pixmap); + QBrush brush(mCustomColour); + painter.fillRect(rect, brush); + painter.setPen(pen); + painter.drawText(2, fm.ascent() + 2, i18n("Custom...")); + painter.end(); + + if (insert) + insertItem(pixmap); + else + changeItem(pixmap, 0); + pixmap.detach(); +} + +void ColourCombo::setReadOnly(bool ro) +{ + mReadOnly = ro; +} + +void ColourCombo::resizeEvent(QResizeEvent* re) +{ + QComboBox::resizeEvent(re); + addColours(); +} + +void ColourCombo::mousePressEvent(QMouseEvent* e) +{ + if (mReadOnly) + { + // Swallow up the event if it's the left button + if (e->button() == Qt::LeftButton) + return; + } + QComboBox::mousePressEvent(e); +} + +void ColourCombo::mouseReleaseEvent(QMouseEvent* e) +{ + if (!mReadOnly) + QComboBox::mouseReleaseEvent(e); +} + +void ColourCombo::mouseMoveEvent(QMouseEvent* e) +{ + if (!mReadOnly) + QComboBox::mouseMoveEvent(e); +} + +void ColourCombo::keyPressEvent(QKeyEvent* e) +{ + if (!mReadOnly || e->key() == Qt::Key_Escape) + QComboBox::keyPressEvent(e); +} + +void ColourCombo::keyReleaseEvent(QKeyEvent* e) +{ + if (!mReadOnly) + QComboBox::keyReleaseEvent(e); +} diff --git a/kalarm/lib/colourcombo.h b/kalarm/lib/colourcombo.h new file mode 100644 index 000000000..f3e4ebca6 --- /dev/null +++ b/kalarm/lib/colourcombo.h @@ -0,0 +1,102 @@ +/* + * colourcombo.h - colour selection combo box + * Program: kalarm + * Copyright © 2001-2003,2005,2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef COLOURCOMBO_H +#define COLOURCOMBO_H + +#include <qcombobox.h> +#include "colourlist.h" + + +/** + * @short A colour selection combo box whose colour list can be specified. + * + * The ColourCombo class is a combo box allowing the user to select a colour. + * + * It is similar to KColorCombo but allows the list of colours to be restricted to those + * which are specified. The first item in the list is a custom colour entry, which allows + * the user to define an arbitrary colour. The remaining entries in the list are preset + * by the program. + * + * The widget may be set as read-only. This has the same effect as disabling it, except + * that its appearance is unchanged. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class ColourCombo : public QComboBox +{ + Q_OBJECT + Q_PROPERTY(QColor color READ color WRITE setColor) + public: + /** Constructor. + * @param parent The parent object of this widget. + * @param name The name of this widget. + * @param defaultColour The colour which is selected by default. + */ + explicit ColourCombo(QWidget* parent = 0, const char* name = 0, const QColor& defaultColour = 0xFFFFFF); + /** Returns the selected colour. */ + QColor color() const { return mSelectedColour; } + /** Returns the selected colour. */ + QColor colour() const { return mSelectedColour; } + /** Sets the selected colour to @p c. */ + void setColor(const QColor& c) { setColour(c); } + /** Sets the selected colour to @p c. */ + void setColour(const QColor& c); + /** Initialises the list of colours to @p list. */ + void setColours(const ColourList& list); + /** Returns true if the first entry in the list, i.e. the custom colour, is selected. */ + bool isCustomColour() const { return !currentItem(); } + /** Returns true if the widget is read only. */ + bool isReadOnly() const { return mReadOnly; } + /** Sets whether the combo box can be changed by the user. + * @param readOnly True to set the widget read-only, false to set it read-write. + */ + virtual void setReadOnly(bool readOnly); + signals: + /** Signal emitted when a new colour has been selected. */ + void activated(const QColor&); // a new colour box has been selected + /** Signal emitted when a new colour has been highlighted. */ + void highlighted(const QColor&); // a new item has been highlighted + public slots: + /** Enables or disables the widget. */ + virtual void setEnabled(bool enabled); + protected: + virtual void resizeEvent(QResizeEvent*); + virtual void mousePressEvent(QMouseEvent*); + virtual void mouseReleaseEvent(QMouseEvent*); + virtual void mouseMoveEvent(QMouseEvent*); + virtual void keyPressEvent(QKeyEvent*); + virtual void keyReleaseEvent(QKeyEvent*); + private slots: + void slotActivated(int index); + void slotHighlighted(int index); + void slotPreferencesChanged(); + private: + void addColours(); + void drawCustomItem(QRect&, bool insert); + + ColourList mColourList; // the sorted colours to display + QColor mSelectedColour; // currently selected colour + QColor mCustomColour; // current colour of the Custom item + bool mReadOnly; // value cannot be changed + bool mDisabled; +}; + +#endif // COLOURCOMBO_H diff --git a/kalarm/lib/colourlist.cpp b/kalarm/lib/colourlist.cpp new file mode 100644 index 000000000..e02a64466 --- /dev/null +++ b/kalarm/lib/colourlist.cpp @@ -0,0 +1,43 @@ +/* + * colourlist.cpp - an ordered list of colours + * Program: kalarm + * Copyright (C) 2003 by David Jarvie software@astrojar.org.uk + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "colourlist.h" + + +ColourList::ColourList(const QColor* colours) +{ + while (colours->isValid()) + mList.append((*colours++).rgb()); +} + +void ColourList::insert(const QColor& colour) +{ + QRgb rgb = colour.rgb(); + for (QValueListIterator<QRgb> it = mList.begin(); it != mList.end(); ++it) + { + if (rgb <= *it) + { + if (rgb != *it) // don't insert duplicates + mList.insert(it, rgb); + return; + } + } + mList.append(rgb); +} diff --git a/kalarm/lib/colourlist.h b/kalarm/lib/colourlist.h new file mode 100644 index 000000000..ef641c04a --- /dev/null +++ b/kalarm/lib/colourlist.h @@ -0,0 +1,110 @@ +/* + * colourlist.h - an ordered list of colours + * Program: kalarm + * Copyright (C) 2003, 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef COLOURLIST_H +#define COLOURLIST_H + +#include <qtl.h> +#include <qcolor.h> +#include <qvaluelist.h> + + +/** + * @short Represents a sorted list of colours. + * + * The ColourList class holds a list of colours, sorted in RGB value order. + * + * It provides a sorted QValueList of colours in RGB value order, with iterators + * and other access methods which return either QRgb or QColor objects. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class ColourList +{ + public: + typedef size_t size_type; + typedef QValueListConstIterator<QRgb> const_iterator; + + /** Constructs an empty list. */ + ColourList() { } + /** Copy constructor. */ + ColourList(const ColourList& l) : mList(l.mList) { } + /** Constructs a list whose values are preset to the colours in @p list. */ + ColourList(const QValueList<QRgb>& list) : mList(list) { qHeapSort(mList); } + /** Constructs a list whose values are preset to the colours in the @p list. + * Terminate @p list by an invalid colour. + */ + ColourList(const QColor* list); + /** Assignment operator. */ + ColourList& operator=(const ColourList& l) { mList = l.mList; return *this; } + /** Sets the list to comprise the colours in @p list. */ + ColourList& operator=(const QValueList<QRgb>& list) { mList = list; qHeapSort(mList); return *this; } + /** Removes all values from the list. */ + void clear() { mList.clear(); } + /** Adds the specified colour @p c to the list. */ + void insert(const QColor& c); + /** Removes the colour @p c from the list. */ + void remove(const QColor& c) { mList.remove(c.rgb()); } + /** Adds the specified colour @p c to the list. */ + ColourList& operator+=(const QColor& c) { insert(c); return *this; } + /** Adds the colours in @p list to this list. */ + ColourList& operator+=(const ColourList& list) { mList += list.mList; qHeapSort(mList); return *this; } + /** Returns true if the colours in the two lists are the same. */ + bool operator==(const ColourList& l) const { return mList == l.mList; } + /** Returns true if the colours in the two lists differ. */ + bool operator!=(const ColourList& l) const { return mList != l.mList; } + /** Returns the number of colours in the list. */ + size_type count() const { return mList.count(); } + /** Returns true if the list is empty. */ + bool isEmpty() const { return mList.isEmpty(); } + /** Returns an iterator pointing to the first colour in the list. */ + const_iterator begin() const { return mList.begin(); } + /** Returns an iterator pointing past the last colour in the list. */ + const_iterator end() const { return mList.end(); } + /** Returns an iterator pointing to the last colour in the list, or end() if the list is empty. */ + const_iterator fromLast() const { return mList.fromLast(); } + /** Returns an iterator pointing to the colour at position @p i in the list. */ + const_iterator at(size_type i) const { return mList.at(i); } + /** Returns true if the list contains the colour @p c. */ + size_type contains(const QColor& c) const { return mList.contains(c.rgb()); } + /** Returns an iterator pointing to the first occurrence of colour @p c in the list. + * Returns end() if colour @p c is not in the list. + */ + const_iterator find(const QColor& c) const { return mList.find(c.rgb()); } + /** Returns an iterator pointing to the first occurrence of colour @p c in the list, starting. + * from position @p it. Returns end() if colour @p c is not in the list. + */ + const_iterator find(const_iterator it, const QColor& c) const { return mList.find(it, c.rgb()); } + /** Returns the index to the first occurrence of colour @p c in the list. + * Returns -1 if colour @p c is not in the list. + */ + int findIndex(const QColor& c) const { return mList.findIndex(c.rgb()); } + /** Returns the first colour in the list. If the list is empty, the result is undefined. */ + QColor first() const { return QColor(mList.first()); } + /** Returns the last colour in the list. If the list is empty, the result is undefined. */ + QColor last() const { return QColor(mList.last()); } + /** Returns the colour at position @p i in the list. If the item does not exist, the result is undefined. */ + QColor operator[](size_type i) const { return QColor(mList[i]); } + private: + void sort(); + QValueList<QRgb> mList; +}; + +#endif // COLOURLIST_H diff --git a/kalarm/lib/combobox.cpp b/kalarm/lib/combobox.cpp new file mode 100644 index 000000000..7e0bea4bd --- /dev/null +++ b/kalarm/lib/combobox.cpp @@ -0,0 +1,78 @@ +/* + * combobox.cpp - combo box with read-only option + * Program: kalarm + * Copyright (c) 2002 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include <qlineedit.h> +#include "combobox.moc" + + +ComboBox::ComboBox(QWidget* parent, const char* name) + : QComboBox(parent, name), + mReadOnly(false) +{ } + +ComboBox::ComboBox(bool rw, QWidget* parent, const char* name) + : QComboBox(rw, parent, name), + mReadOnly(false) +{ } + +void ComboBox::setReadOnly(bool ro) +{ + if ((int)ro != (int)mReadOnly) + { + mReadOnly = ro; + if (lineEdit()) + lineEdit()->setReadOnly(ro); + } +} + +void ComboBox::mousePressEvent(QMouseEvent* e) +{ + if (mReadOnly) + { + // Swallow up the event if it's the left button + if (e->button() == Qt::LeftButton) + return; + } + QComboBox::mousePressEvent(e); +} + +void ComboBox::mouseReleaseEvent(QMouseEvent* e) +{ + if (!mReadOnly) + QComboBox::mouseReleaseEvent(e); +} + +void ComboBox::mouseMoveEvent(QMouseEvent* e) +{ + if (!mReadOnly) + QComboBox::mouseMoveEvent(e); +} + +void ComboBox::keyPressEvent(QKeyEvent* e) +{ + if (!mReadOnly || e->key() == Qt::Key_Escape) + QComboBox::keyPressEvent(e); +} + +void ComboBox::keyReleaseEvent(QKeyEvent* e) +{ + if (!mReadOnly) + QComboBox::keyReleaseEvent(e); +} diff --git a/kalarm/lib/combobox.h b/kalarm/lib/combobox.h new file mode 100644 index 000000000..d33ac147e --- /dev/null +++ b/kalarm/lib/combobox.h @@ -0,0 +1,69 @@ +/* + * combobox.h - combo box with read-only option + * Program: kalarm + * Copyright © 2002,2005,2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef COMBOBOX_H +#define COMBOBOX_H + +#include <qcombobox.h> + + +/** + * @short A QComboBox with read-only option. + * + * The ComboBox class is a QComboBox with a read-only option. + * + * The widget may be set as read-only. This has the same effect as disabling it, except + * that its appearance is unchanged. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class ComboBox : public QComboBox +{ + Q_OBJECT + public: + /** Constructor. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit ComboBox(QWidget* parent = 0, const char* name = 0); + /** Constructor. + * @param rw True for a read-write combo box, false for a read-only combo box. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit ComboBox(bool rw, QWidget* parent = 0, const char* name = 0); + /** Returns true if the widget is read only. */ + bool isReadOnly() const { return mReadOnly; } + /** Sets whether the combo box is read-only for the user. If read-only, + * its state cannot be changed by the user. + * @param readOnly True to set the widget read-only, false to set it read-write. + */ + virtual void setReadOnly(bool readOnly); + protected: + virtual void mousePressEvent(QMouseEvent*); + virtual void mouseReleaseEvent(QMouseEvent*); + virtual void mouseMoveEvent(QMouseEvent*); + virtual void keyPressEvent(QKeyEvent*); + virtual void keyReleaseEvent(QKeyEvent*); + private: + bool mReadOnly; // value cannot be changed +}; + +#endif // COMBOBOX_H diff --git a/kalarm/lib/dateedit.cpp b/kalarm/lib/dateedit.cpp new file mode 100644 index 000000000..71553d704 --- /dev/null +++ b/kalarm/lib/dateedit.cpp @@ -0,0 +1,122 @@ +/* + * dateedit.cpp - date entry widget + * Program: kalarm + * Copyright © 2002-2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include <kglobal.h> +#include <klocale.h> +#include <kmessagebox.h> + +#include "dateedit.moc" + + +DateEdit::DateEdit(QWidget* parent, const char* name) + : KDateEdit(parent, name) +{ + connect(this, SIGNAL(dateEntered(const QDate&)), SLOT(newDateEntered(const QDate&))); +} + +void DateEdit::setMinDate(const QDate& d, const QString& errorDate) +{ + mMinDate = d; + if (mMinDate.isValid() && date().isValid() && date() < mMinDate) + setDate(mMinDate); + mMinDateErrString = errorDate; +} + +void DateEdit::setMaxDate(const QDate& d, const QString& errorDate) +{ + mMaxDate = d; + if (mMaxDate.isValid() && date().isValid() && date() > mMaxDate) + setDate(mMaxDate); + mMaxDateErrString = errorDate; +} + +void DateEdit::setInvalid() +{ + setDate(QDate()); +} + +// Check a new date against any minimum or maximum date. +void DateEdit::newDateEntered(const QDate& newDate) +{ + if (newDate.isValid()) + { + if (mMinDate.isValid() && newDate < mMinDate) + { + pastLimitMessage(mMinDate, mMinDateErrString, + i18n("Date cannot be earlier than %1")); + setDate(mMinDate); + } + else if (mMaxDate.isValid() && newDate > mMaxDate) + { + pastLimitMessage(mMaxDate, mMaxDateErrString, + i18n("Date cannot be later than %1")); + setDate(mMaxDate); + } + } +} + +void DateEdit::pastLimitMessage(const QDate& limit, const QString& error, const QString& defaultError) +{ + QString errString = error; + if (errString.isNull()) + { + if (limit == QDate::currentDate()) + errString = i18n("today"); + else + errString = KGlobal::locale()->formatDate(limit, true); + errString = defaultError.arg(errString); + } + KMessageBox::sorry(this, errString); +} + +void DateEdit::mousePressEvent(QMouseEvent *e) +{ + if (isReadOnly()) + { + // Swallow up the event if it's the left button + if (e->button() == Qt::LeftButton) + return; + } + KDateEdit::mousePressEvent(e); +} + +void DateEdit::mouseReleaseEvent(QMouseEvent* e) +{ + if (!isReadOnly()) + KDateEdit::mouseReleaseEvent(e); +} + +void DateEdit::mouseMoveEvent(QMouseEvent* e) +{ + if (!isReadOnly()) + KDateEdit::mouseMoveEvent(e); +} + +void DateEdit::keyPressEvent(QKeyEvent* e) +{ + if (!isReadOnly()) + KDateEdit::keyPressEvent(e); +} + +void DateEdit::keyReleaseEvent(QKeyEvent* e) +{ + if (!isReadOnly()) + KDateEdit::keyReleaseEvent(e); +} diff --git a/kalarm/lib/dateedit.h b/kalarm/lib/dateedit.h new file mode 100644 index 000000000..031a96741 --- /dev/null +++ b/kalarm/lib/dateedit.h @@ -0,0 +1,90 @@ +/* + * dateedit.h - date entry widget + * Program: kalarm + * Copyright © 2002-2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#ifndef DATEEDIT_H +#define DATEEDIT_H + +#include <libkdepim/kdateedit.h> + +/** + * @short Date edit widget with range limits. + * + * The DateEdit class provides a date editor with the ability to set limits on the + * dates which can be entered. + * + * Minimum and/or maximum permissible dates may be set, together with corresponding + * error messages. If the user tries to enter a date outside the allowed range, the + * appropriate error message (if any) is output using KMessageBox::sorry(). + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class DateEdit : public KDateEdit +{ + Q_OBJECT + public: + /** Constructor. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit DateEdit(QWidget* parent = 0, const char* name = 0); + /** Returns true if the widget contains a valid date. */ + bool isValid() const { return date().isValid(); } + /** Returns the earliest date which can be entered. + * If there is no minimum date, returns an invalid date. + */ + const QDate& minDate() const { return mMinDate; } + /** Returns the latest date which can be entered. + * If there is no maximum date, returns an invalid date. + */ + const QDate& maxDate() const { return mMaxDate; } + /** Sets the earliest date which can be entered. + * @param date Earliest date allowed. If invalid, any minimum limit is removed. + * @param errorDate Error message to be displayed when a date earlier than + * @p date is entered. Set to QString::null to use the default error message. + */ + void setMinDate(const QDate& date, const QString& errorDate = QString::null); + /** Sets the latest date which can be entered. + * @param date Latest date allowed. If invalid, any maximum limit is removed. + * @param errorDate Error message to be displayed when a date later than + * @p date is entered. Set to QString::null to use the default error message. + */ + void setMaxDate(const QDate& date, const QString& errorDate = QString::null); + /** Sets the date held in the widget to an invalid date. */ + void setInvalid(); + + protected: + virtual void mousePressEvent(QMouseEvent*); + virtual void mouseReleaseEvent(QMouseEvent*); + virtual void mouseMoveEvent(QMouseEvent*); + virtual void keyPressEvent(QKeyEvent*); + virtual void keyReleaseEvent(QKeyEvent*); + + private slots: + void newDateEntered(const QDate&); + + private: + void pastLimitMessage(const QDate& limit, const QString& error, const QString& defaultError); + + QDate mMinDate; // minimum allowed date, or invalid for no minimum + QDate mMaxDate; // maximum allowed date, or invalid for no maximum + QString mMinDateErrString; // error message when entered date < mMinDate + QString mMaxDateErrString; // error message when entered date > mMaxDate +}; + +#endif // DATEEDIT_H diff --git a/kalarm/lib/datetime.cpp b/kalarm/lib/datetime.cpp new file mode 100644 index 000000000..d7f5ee862 --- /dev/null +++ b/kalarm/lib/datetime.cpp @@ -0,0 +1,80 @@ +/* + * datetime.cpp - date/time representation with optional date-only value + * Program: kalarm + * Copyright (C) 2003, 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "kalarm.h" + +#include <kglobal.h> +#include <klocale.h> + +#include "datetime.h" + +QTime DateTime::mStartOfDay; + +QTime DateTime::time() const +{ + return mDateOnly ? mStartOfDay : mDateTime.time(); +} + +QDateTime DateTime::dateTime() const +{ + return mDateOnly ? QDateTime(mDateTime.date(), mStartOfDay) : mDateTime; +} + +QString DateTime::formatLocale(bool shortFormat) const +{ + if (mDateOnly) + return KGlobal::locale()->formatDate(mDateTime.date(), shortFormat); + else if (mTimeValid) + return KGlobal::locale()->formatDateTime(mDateTime, shortFormat); + else + return QString::null; +} + +bool operator==(const DateTime& dt1, const DateTime& dt2) +{ + if (dt1.mDateTime.date() != dt2.mDateTime.date()) + return false; + if (dt1.mDateOnly && dt2.mDateOnly) + return true; + if (!dt1.mDateOnly && !dt2.mDateOnly) + { + bool valid1 = dt1.mTimeValid && dt1.mDateTime.time().isValid(); + bool valid2 = dt2.mTimeValid && dt2.mDateTime.time().isValid(); + if (!valid1 && !valid2) + return true; + if (!valid1 || !valid2) + return false; + return dt1.mDateTime.time() == dt2.mDateTime.time(); + } + return (dt1.mDateOnly ? dt2.mDateTime.time() : dt1.mDateTime.time()) == DateTime::startOfDay(); +} + +bool operator<(const DateTime& dt1, const DateTime& dt2) +{ + if (dt1.mDateTime.date() != dt2.mDateTime.date()) + return dt1.mDateTime.date() < dt2.mDateTime.date(); + if (dt1.mDateOnly && dt2.mDateOnly) + return false; + if (!dt1.mDateOnly && !dt2.mDateOnly) + return dt1.mDateTime.time() < dt2.mDateTime.time(); + QTime t = DateTime::startOfDay(); + if (dt1.mDateOnly) + return t < dt2.mDateTime.time(); + return dt1.mDateTime.time() < t; +} diff --git a/kalarm/lib/datetime.h b/kalarm/lib/datetime.h new file mode 100644 index 000000000..3b0831918 --- /dev/null +++ b/kalarm/lib/datetime.h @@ -0,0 +1,241 @@ +/* + * datetime.h - date/time representation with optional date-only value + * Program: kalarm + * Copyright © 2003,2005,2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#ifndef DATETIME_H +#define DATETIME_H + +#include <qdatetime.h> + + +/** + * @short A QDateTime with date-only option. + * + * The DateTime class holds a date, with or without a time. + * + * DateTime is very similar to the QDateTime class, with the additional option to + * hold a date-only value. This allows a single date-time representation to be used + * for both an event having a specific date and time, and an all-day event. + * + * The time assumed for date-only values is the start-of-day time set by setStartOfDay(). + * + * @author David Jarvie <software@astrojar.org.uk> +*/ +class DateTime +{ + public: + /** Default constructor. + * Constructs an invalid date-time. + */ + DateTime() : mDateOnly(false), mTimeValid(false) { } + /** Constructor for a date-only value. */ + DateTime(const QDate& d) : mDateTime(d), mDateOnly(true) { } + /** Constructor for a date-time value. */ + DateTime(const QDate& d, const QTime& t) + : mDateTime(d, t), mDateOnly(false), mTimeValid(true) { } + /** Constructor for a date-time or date-only value. + * @param dt the date and time to use. + * @param dateOnly True to construct a date-only value; false to construct a date-time value. + */ + DateTime(const QDateTime& dt, bool dateOnly = false) + : mDateTime(dt), mDateOnly(dateOnly), mTimeValid(true) + { if (dateOnly) mDateTime.setTime(QTime()); } + /** Assignment operator. + * Sets the value to a specified date-time or date-only value. + */ + DateTime& operator=(const DateTime& dt) + { mDateTime = dt.mDateTime; mDateOnly = dt.mDateOnly; mTimeValid = dt.mTimeValid; return *this; } + /** Assignment operator. + * Sets the value to a specified date-time. + */ + DateTime& operator=(const QDateTime& dt) + { mDateTime = dt; mDateOnly = false; mTimeValid = true; return *this; } + /** Assignment operator. + * Sets the value to a specified date-only value. + */ + DateTime& operator=(const QDate& d) + { mDateTime.setDate(d); mDateTime.setTime(QTime()); mDateOnly = true; return *this; } + /** Returns true if the date is null and, if it is a date-time value, the time is also null. */ + bool isNull() const { return mDateTime.date().isNull() && (mDateOnly || mDateTime.time().isNull()); } + /** Returns true if the date is valid and, if it is a date-time value, the time is also valid. */ + bool isValid() const { return mDateTime.date().isValid() && (mDateOnly || mTimeValid && mDateTime.time().isValid()); } + /** Returns true if it is date-only value. */ + bool isDateOnly() const { return mDateOnly; } + /** Sets the value to be either date-only or date-time. + * @param d True to set the value to be date-only; false to set it to a date-time value. + */ + void setDateOnly(bool d) { if (d) mDateTime.setTime(QTime()); + else if (mDateOnly) mTimeValid = false; + mDateOnly = d; + } + /** Returns the date part of the value. */ + QDate date() const { return mDateTime.date(); } + /** Returns the time part of the value. + * If the value is date-only, the time returned is the start-of-day time set by setStartOfDay(). + */ + QTime time() const; + /** Returns the date and time of the value. + * If the value is date-only, the time part returned is equal to the start-of-day time set + * by setStartOfDay(). + */ + QDateTime dateTime() const; + /** Returns the date and time of the value. + * if the value is date-only, the time part returned is 00:00:00. + */ + QDateTime rawDateTime() const { return mDateTime; } + /** Sets a date-time or date-only value. + * @param dt the date-time to use. + * @param dateOnly True to set a date-only value; false to set a date-time value. + */ + void set(const QDateTime& dt, bool dateOnly = false) + { + mDateTime = dt; + mDateOnly = dateOnly; + if (dateOnly) + mDateTime.setTime(QTime()); + mTimeValid = true; + } + /** Sets a date-time value. */ + void set(const QDate& d, const QTime& t) + { mDateTime.setDate(d); mDateTime.setTime(t); mDateOnly = false; mTimeValid = true; } + /** Sets the time component of the value. + * The value is converted if necessary to be a date-time value. + */ + void setTime(const QTime& t) { mDateTime.setTime(t); mDateOnly = false; mTimeValid = true; } + /** Sets the value to a specified date-time value. + * @param secs The time_t date-time value, expressed as the number of seconds elapsed + * since 1970-01-01 00:00:00 UTC. + */ + void setTime_t(uint secs) { mDateTime.setTime_t(secs); mDateOnly = false; mTimeValid = true; } + /** Returns a DateTime value @p secs seconds later than the value of this object. + * If this object is date-only, @p secs is first rounded down to a whole number of days + * before adding the value. + */ + DateTime addSecs(int n) const + { + if (mDateOnly) + return DateTime(mDateTime.date().addDays(n / (3600*24)), true); + else + return DateTime(mDateTime.addSecs(n), false); + } + /** Returns a DateTime value @p mins minutes later than the value of this object. + * If this object is date-only, @p mins is first rounded down to a whole number of days + * before adding the value. + */ + DateTime addMins(int n) const + { + if (mDateOnly) + return DateTime(mDateTime.addDays(n / (60*24)), true); + else + return DateTime(mDateTime.addSecs(n * 60), false); + } + /** Returns a DateTime value @p n days later than the value of this object. */ + DateTime addDays(int n) const { return DateTime(mDateTime.addDays(n), mDateOnly); } + /** Returns a DateTime value @p n months later than the value of this object. */ + DateTime addMonths(int n) const { return DateTime(mDateTime.addMonths(n), mDateOnly); } + /** Returns a DateTime value @p n years later than the value of this object. */ + DateTime addYears(int n) const { return DateTime(mDateTime.addYears(n), mDateOnly); } + /** Returns the number of days from this date or date-time to @p dt. */ + int daysTo(const DateTime& dt) const + { return (mDateOnly || dt.mDateOnly) ? mDateTime.date().daysTo(dt.date()) : mDateTime.daysTo(dt.mDateTime); } + /** Returns the number of minutes from this date or date-time to @p dt. + * If either of the values is date-only, the result is calculated by simply + * taking the difference in dates and ignoring the times. + */ + int minsTo(const DateTime& dt) const + { return (mDateOnly || dt.mDateOnly) ? mDateTime.date().daysTo(dt.date()) * 24*60 : mDateTime.secsTo(dt.mDateTime) / 60; } + /** Returns the number of seconds from this date or date-time to @p dt. + * If either of the values is date-only, the result is calculated by simply + * taking the difference in dates and ignoring the times. + */ + int secsTo(const DateTime& dt) const + { return (mDateOnly || dt.mDateOnly) ? mDateTime.date().daysTo(dt.date()) * 24*3600 : mDateTime.secsTo(dt.mDateTime); } + /** Returns the value as a string. + * If it is a date-time, both time and date are included in the output. + * If it is date-only, only the date is included in the output. + */ + QString toString(Qt::DateFormat f = Qt::TextDate) const + { + if (mDateOnly) + return mDateTime.date().toString(f); + else if (mTimeValid) + return mDateTime.toString(f); + else + return QString::null; + } + /** Returns the value as a string. + * If it is a date-time, both time and date are included in the output. + * If it is date-only, only the date is included in the output. + */ + QString toString(const QString& format) const + { + if (mDateOnly) + return mDateTime.date().toString(format); + else if (mTimeValid) + return mDateTime.toString(format); + else + return QString::null; + } + /** Returns the value as a string, formatted according to the user's locale. + * If it is a date-time, both time and date are included in the output. + * If it is date-only, only the date is included in the output. + */ + QString formatLocale(bool shortFormat = true) const; + /** Sets the start-of-day time. + * The default value is midnight (0000 hrs). + */ + static void setStartOfDay(const QTime& sod) { mStartOfDay = sod; } + /** Returns the start-of-day time. */ + static QTime startOfDay() { return mStartOfDay; } + + friend bool operator==(const DateTime& dt1, const DateTime& dt2); + friend bool operator<(const DateTime& dt1, const DateTime& dt2); + + private: + static QTime mStartOfDay; + QDateTime mDateTime; + bool mDateOnly; + bool mTimeValid; // whether the time is potentially valid - applicable only if mDateOnly false +}; + +/** Returns true if the two values are equal. */ +bool operator==(const DateTime& dt1, const DateTime& dt2); +/** Returns true if the @p dt1 is earlier than @p dt2. + * If the two values have the same date, and one value is date-only while the other is a date-time, the + * time used for the date-only value is the start-of-day time set in the KAlarm Preferences dialogue. + */ +bool operator<(const DateTime& dt1, const DateTime& dt2); +/** Returns true if the two values are not equal. */ +inline bool operator!=(const DateTime& dt1, const DateTime& dt2) { return !operator==(dt1, dt2); } +/** Returns true if the @p dt1 is later than @p dt2. + * If the two values have the same date, and one value is date-only while the other is a date-time, the + * time used for the date-only value is the start-of-day time set in the KAlarm Preferences dialogue. + */ +inline bool operator>(const DateTime& dt1, const DateTime& dt2) { return operator<(dt2, dt1); } +/** Returns true if the @p dt1 is later than or equal to @p dt2. + * If the two values have the same date, and one value is date-only while the other is a date-time, the + * time used for the date-only value is the start-of-day time set in the KAlarm Preferences dialogue. + */ +inline bool operator>=(const DateTime& dt1, const DateTime& dt2) { return !operator<(dt1, dt2); } +/** Returns true if the @p dt1 is earlier than or equal to @p dt2. + * If the two values have the same date, and one value is date-only while the other is a date-time, the + * time used for the date-only value is the start-of-day time set in the KAlarm Preferences dialogue. + */ +inline bool operator<=(const DateTime& dt1, const DateTime& dt2) { return !operator<(dt2, dt1); } + +#endif // DATETIME_H diff --git a/kalarm/lib/label.cpp b/kalarm/lib/label.cpp new file mode 100644 index 000000000..c61ce76ad --- /dev/null +++ b/kalarm/lib/label.cpp @@ -0,0 +1,118 @@ +/* + * label.cpp - label with radiobutton buddy option + * Program: kalarm + * Copyright (C) 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" +#include <qradiobutton.h> +#include "label.moc" + + +Label::Label(QWidget* parent, const char* name, WFlags f) + : QLabel(parent, name, f), + mRadioButton(0), + mFocusWidget(0) +{ } + +Label::Label(const QString& text, QWidget* parent, const char* name, WFlags f) + : QLabel(text, parent, name, f), + mRadioButton(0), + mFocusWidget(0) +{ } + +Label::Label(QWidget* buddy, const QString& text, QWidget* parent, const char* name, WFlags f) + : QLabel(buddy, text, parent, name, f), + mRadioButton(0), + mFocusWidget(0) +{ } + +/****************************************************************************** +* Set a buddy widget. +* If it (or its focus proxy) is a radio button, create a focus widget. +* When the accelerator key is pressed, the focus widget then receives focus. +* That event triggers the selection of the radio button. +*/ +void Label::setBuddy(QWidget* bud) +{ + if (mRadioButton) + disconnect(mRadioButton, SIGNAL(destroyed()), this, SLOT(buddyDead())); + QWidget* w = bud; + if (w) + { + while (w->focusProxy()) + w = w->focusProxy(); + if (!w->inherits("QRadioButton")) + w = 0; + } + if (!w) + { + // The buddy widget isn't a radio button + QLabel::setBuddy(bud); + delete mFocusWidget; + mFocusWidget = 0; + mRadioButton = 0; + } + else + { + // The buddy widget is a radio button, so set a different buddy + if (!mFocusWidget) + mFocusWidget = new LabelFocusWidget(this); + QLabel::setBuddy(mFocusWidget); + mRadioButton = (QRadioButton*)bud; + connect(mRadioButton, SIGNAL(destroyed()), this, SLOT(buddyDead())); + } +} + +void Label::buddyDead() +{ + delete mFocusWidget; + mFocusWidget = 0; + mRadioButton = 0; +} + +/****************************************************************************** +* Called when focus is transferred to the label's special focus widget. +* Transfer focus to the radio button and select it. +*/ +void Label::activated() +{ + if (mFocusWidget && mRadioButton) + { + mRadioButton->setFocus(); + mRadioButton->setChecked(true); + } +} + + +/*============================================================================= +* Class: LabelFocusWidget +=============================================================================*/ + +LabelFocusWidget::LabelFocusWidget(QWidget* parent, const char* name) + : QWidget(parent, name) +{ + setFocusPolicy(ClickFocus); + setFixedSize(QSize(1,1)); +} + +void LabelFocusWidget::focusInEvent(QFocusEvent*) +{ + Label* parent = (Label*)parentWidget(); + parent->activated(); + +} diff --git a/kalarm/lib/label.h b/kalarm/lib/label.h new file mode 100644 index 000000000..c65a7fcd6 --- /dev/null +++ b/kalarm/lib/label.h @@ -0,0 +1,96 @@ +/* + * label.h - label with radiobutton buddy option + * Program: kalarm + * Copyright © 2004-2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LABEL_H +#define LABEL_H + +#include <qlabel.h> +class QRadioButton; +class LabelFocusWidget; + +/** + * @short A QLabel with option for a buddy radio button. + * + * The Label class provides a text display, with special behaviour when a radio + * button is set as a buddy. + * + * The Label object in effect acts as if it were part of the buddy radio button, + * in that when the label's accelerator key is pressed, the radio button receives + * focus and is switched on. When a non-radio button is specified as a buddy, the + * behaviour is the same as for QLabel. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class Label : public QLabel +{ + Q_OBJECT + friend class LabelFocusWidget; + public: + /** Constructs an empty label. + * @param parent The parent object of this widget. + * @param name The name of this widget. + * @param f Flags. See QWidget constructor for details. + */ + explicit Label(QWidget* parent, const char* name = 0, WFlags f = 0); + /** Constructs a label that displays @p text. + * @param text Text string to display. + * @param parent The parent object of this widget. + * @param name The name of this widget. + * @param f Flags. See QWidget constructor for details. + */ + Label(const QString& text, QWidget* parent, const char* name = 0, WFlags f = 0); + /** Constructs a label, with a buddy widget, that displays @p text. + * @param buddy Buddy widget which receives the keyboard focus when the + * label's accelerator key is pressed. If @p buddy is a radio + * button, @p buddy is in addition selected when the + * accelerator key is pressed. + * @param text Text string to display. + * @param parent The parent object of this widget. + * @param name The name of this widget. + * @param f Flags. See QWidget constructor for details. + */ + Label(QWidget* buddy, const QString& text, QWidget* parent, const char* name = 0, WFlags f = 0); + /** Sets the label's buddy widget which receives the keyboard focus when the + * label's accelerator key is pressed. If @p buddy is a radio button, + * @p buddy is in addition selected when the accelerator key is pressed. + */ + virtual void setBuddy(QWidget* buddy); + protected: + virtual void drawContents(QPainter* p) { QLabel::drawContents(p); } + private slots: + void buddyDead(); + private: + void activated(); + QRadioButton* mRadioButton; // buddy widget if it's a radio button, else 0 + LabelFocusWidget* mFocusWidget; +}; + + +// Private class for use by Label +class LabelFocusWidget : public QWidget +{ + Q_OBJECT + public: + LabelFocusWidget(QWidget* parent, const char* name = 0); + protected: + virtual void focusInEvent(QFocusEvent*); +}; + +#endif // LABEL_H diff --git a/kalarm/lib/lineedit.cpp b/kalarm/lib/lineedit.cpp new file mode 100644 index 000000000..943d7b2d0 --- /dev/null +++ b/kalarm/lib/lineedit.cpp @@ -0,0 +1,200 @@ +/* + * lineedit.cpp - Line edit widget with extra drag and drop options + * Program: kalarm + * Copyright (C) 2003 - 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qregexp.h> +#include <qdragobject.h> + +#include <kurldrag.h> +#include <kurlcompletion.h> + +#include <libkdepim/maillistdrag.h> +#include <libkdepim/kvcarddrag.h> +#include <libkcal/icaldrag.h> + +#include "lineedit.moc" + + +/*============================================================================= += Class LineEdit += Line edit which accepts drag and drop of text, URLs and/or email addresses. +* It has an option to prevent its contents being selected when it receives += focus. +=============================================================================*/ +LineEdit::LineEdit(Type type, QWidget* parent, const char* name) + : KLineEdit(parent, name), + mType(type), + mNoSelect(false), + mSetCursorAtEnd(false) +{ + init(); +} + +LineEdit::LineEdit(QWidget* parent, const char* name) + : KLineEdit(parent, name), + mType(Text), + mNoSelect(false), + mSetCursorAtEnd(false) +{ + init(); +} + +void LineEdit::init() +{ + if (mType == Url) + { + setCompletionMode(KGlobalSettings::CompletionShell); + KURLCompletion* comp = new KURLCompletion(KURLCompletion::FileCompletion); + comp->setReplaceHome(true); + setCompletionObject(comp); + setAutoDeleteCompletionObject(true); + } + else + setCompletionMode(KGlobalSettings::CompletionNone); +} + +/****************************************************************************** +* Called when the line edit receives focus. +* If 'noSelect' is true, prevent the contents being selected. +*/ +void LineEdit::focusInEvent(QFocusEvent* e) +{ + if (mNoSelect) + QFocusEvent::setReason(QFocusEvent::Other); + KLineEdit::focusInEvent(e); + if (mNoSelect) + { + QFocusEvent::resetReason(); + mNoSelect = false; + } +} + +void LineEdit::setText(const QString& text) +{ + KLineEdit::setText(text); + setCursorPosition(mSetCursorAtEnd ? text.length() : 0); +} + +void LineEdit::dragEnterEvent(QDragEnterEvent* e) +{ + if (KCal::ICalDrag::canDecode(e)) + e->accept(false); // don't accept "text/calendar" objects + e->accept(QTextDrag::canDecode(e) + || KURLDrag::canDecode(e) + || mType != Url && KPIM::MailListDrag::canDecode(e) + || mType == Emails && KVCardDrag::canDecode(e)); +} + +void LineEdit::dropEvent(QDropEvent* e) +{ + QString newText; + QStringList newEmails; + QString txt; + KPIM::MailList mailList; + KURL::List files; + KABC::Addressee::List addrList; + + if (mType != Url + && e->provides(KPIM::MailListDrag::format()) + && KPIM::MailListDrag::decode(e, mailList)) + { + // KMail message(s) - ignore all but the first + if (mailList.count()) + { + if (mType == Emails) + newText = mailList.first().from(); + else + setText(mailList.first().subject()); // replace any existing text + } + } + // This must come before KURLDrag + else if (mType == Emails + && KVCardDrag::canDecode(e) && KVCardDrag::decode(e, addrList)) + { + // KAddressBook entries + for (KABC::Addressee::List::Iterator it = addrList.begin(); it != addrList.end(); ++it) + { + QString em((*it).fullEmail()); + if (!em.isEmpty()) + newEmails.append(em); + } + } + else if (KURLDrag::decode(e, files) && files.count()) + { + // URL(s) + switch (mType) + { + case Url: + // URL entry field - ignore all but the first dropped URL + setText(files.first().prettyURL()); // replace any existing text + break; + case Emails: + { + // Email entry field - ignore all but mailto: URLs + QString mailto = QString::fromLatin1("mailto"); + for (KURL::List::Iterator it = files.begin(); it != files.end(); ++it) + { + if ((*it).protocol() == mailto) + newEmails.append((*it).path()); + } + break; + } + case Text: + newText = files.first().prettyURL(); + break; + } + } + else if (QTextDrag::decode(e, txt)) + { + // Plain text + if (mType == Emails) + { + // Remove newlines from a list of email addresses, and allow an eventual mailto: protocol + QString mailto = QString::fromLatin1("mailto:"); + newEmails = QStringList::split(QRegExp("[\r\n]+"), txt); + for (QStringList::Iterator it = newEmails.begin(); it != newEmails.end(); ++it) + { + if ((*it).startsWith(mailto)) + { + KURL url(*it); + *it = url.path(); + } + } + } + else + { + int newline = txt.find('\n'); + newText = (newline >= 0) ? txt.left(newline) : txt; + } + } + + if (newEmails.count()) + { + newText = newEmails.join(","); + int c = cursorPosition(); + if (c > 0) + newText.prepend(","); + if (c < static_cast<int>(text().length())) + newText.append(","); + } + if (!newText.isEmpty()) + insert(newText); +} diff --git a/kalarm/lib/lineedit.h b/kalarm/lib/lineedit.h new file mode 100644 index 000000000..57f8378ee --- /dev/null +++ b/kalarm/lib/lineedit.h @@ -0,0 +1,94 @@ +/* + * lineedit.h - line edit widget with extra drag and drop options + * Program: kalarm + * Copyright (C) 2003 - 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef LINEEDIT_H +#define LINEEDIT_H + +#include <klineedit.h> + + +/** + * @short Line edit widget with extra drag and drop options. + * + * The LineEdit class is a line edit widget which accepts specified types of drag and + * drop content. + * + * The widget will always accept drag and drop of text, except text/calendar mime type, + * and of URLs. It will accept additional mime types depending on its configuration: + * Text type accepts email address lists. + * Email type accepts email address lists and VCard data (e.g. from KAddressBook). + * + * The class also provides an option to prevent its contents being selected when the + * widget receives focus. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class LineEdit : public KLineEdit +{ + Q_OBJECT + public: + /** Types of drag and drop content which will be accepted. + * @li Text - the line edit contains general text. It accepts text, a URL + * or an email from KMail (the subject line is used). If multiple + * URLs or emails are dropped, only the first is used; the + * rest are ignored. + * @li Url - the line edit contains a URL. It accepts text or a URL. If + * multiple URLs are dropped, only the first URL is used; the + * rest are ignored. + * @li Emails - the line edit contains email addresses. It accepts text, + * mailto: URLs, emails from KMail (the From address is used) + * or vcard data (e.g. from KAddressBook). If multiple emails + * are dropped, only the first is used; the rest are ignored. + * + */ + enum Type { Text, Url, Emails }; + /** Constructor. + * @param type The content type for the line edit. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit LineEdit(Type type, QWidget* parent = 0, const char* name = 0); + /** Constructs a line edit whose content type is Text. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit LineEdit(QWidget* parent = 0, const char* name = 0); + /** Prevents the line edit's contents being selected when the widget receives focus. */ + void setNoSelect() { mNoSelect = true; } + /** Sets whether the cursor should be set at the beginning or end of the text when + * setText() is called. + */ + void setCursorAtEnd(bool end = true) { mSetCursorAtEnd = end; } + public slots: + /** Sets the contents of the line edit to be @p str. */ + virtual void setText(const QString& str); + protected: + virtual void focusInEvent(QFocusEvent*); + virtual void dragEnterEvent(QDragEnterEvent*); + virtual void dropEvent(QDropEvent*); + private: + void init(); + + Type mType; + bool mNoSelect; + bool mSetCursorAtEnd; // setText() should position cursor at end +}; + +#endif // LINEEDIT_H diff --git a/kalarm/lib/messagebox.cpp b/kalarm/lib/messagebox.cpp new file mode 100644 index 000000000..514b45bc2 --- /dev/null +++ b/kalarm/lib/messagebox.cpp @@ -0,0 +1,178 @@ +/* + * messagebox.cpp - enhanced KMessageBox class + * Program: kalarm + * Copyright (C) 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" +#include <kconfig.h> +#include "messagebox.h" + + +KConfig* MessageBox::mConfig = 0; +QMap<QString, KMessageBox::ButtonCode> MessageBox::mContinueDefaults; + + +/****************************************************************************** +* Set the default button for continue/cancel message boxes with the specified +* 'dontAskAgainName'. +*/ +void MessageBox::setContinueDefault(const QString& dontAskAgainName, ButtonCode defaultButton) +{ + mContinueDefaults[dontAskAgainName] = (defaultButton == Cancel ? Cancel : Continue); +} + +/****************************************************************************** +* Get the default button for continue/cancel message boxes with the specified +* 'dontAskAgainName'. +*/ +KMessageBox::ButtonCode MessageBox::getContinueDefault(const QString& dontAskAgainName) +{ + ButtonCode defaultButton = Continue; + if (!dontAskAgainName.isEmpty()) + { + QMap<QString, ButtonCode>::ConstIterator it = mContinueDefaults.find(dontAskAgainName); + if (it != mContinueDefaults.end()) + defaultButton = it.data(); + } + return defaultButton; +} + +/****************************************************************************** +* Continue/cancel message box. +* If 'dontAskAgainName' is specified: +* 1) The message box will only be suppressed if the user chose Continue last time. +* 2) The default button is that last set with either setContinueDefault() or +* warningContinueCancel() for that 'dontAskAgainName' value. If neither method +* has set a default button, Continue is the default. +*/ +int MessageBox::warningContinueCancel(QWidget* parent, const QString& text, const QString& caption, + const KGuiItem& buttonContinue, const QString& dontAskAgainName) +{ + ButtonCode defaultButton = getContinueDefault(dontAskAgainName); + return warningContinueCancel(parent, defaultButton, text, caption, buttonContinue, dontAskAgainName); +} + +/****************************************************************************** +* Continue/cancel message box with the option as to which button is the default. +* If 'dontAskAgainName' is specified, the message box will only be suppressed +* if the user chose Continue last time. +*/ +int MessageBox::warningContinueCancel(QWidget* parent, ButtonCode defaultButton, const QString& text, + const QString& caption, const KGuiItem& buttonContinue, + const QString& dontAskAgainName) +{ + setContinueDefault(dontAskAgainName, defaultButton); + if (defaultButton != Cancel) + return KMessageBox::warningContinueCancel(parent, text, caption, buttonContinue, dontAskAgainName); + + // Cancel is the default button, so we have to use KMessageBox::warningYesNo() + if (!dontAskAgainName.isEmpty()) + { + ButtonCode b; + if (!shouldBeShownYesNo(dontAskAgainName, b) + && b != KMessageBox::Yes) + { + // Notification has been suppressed, but No (alias Cancel) is the default, + // so unsuppress notification. + saveDontShowAgain(dontAskAgainName, true, false); + } + } + return warningYesNo(parent, text, caption, buttonContinue, KStdGuiItem::cancel(), dontAskAgainName); +} + +/****************************************************************************** +* If there is no current setting for whether a non-yes/no message box should be +* shown, set it to 'defaultShow'. +* If a continue/cancel message box has Cancel as the default button, either +* setContinueDefault() or warningContinueCancel() must have been called +* previously to set this for this 'dontShowAgainName' value. +* Reply = true if 'defaultShow' was written. +*/ +bool MessageBox::setDefaultShouldBeShownContinue(const QString& dontShowAgainName, bool defaultShow) +{ + if (dontShowAgainName.isEmpty()) + return false; + // First check whether there is an existing setting + KConfig* config = mConfig ? mConfig : KGlobal::config(); + config->setGroup(QString::fromLatin1("Notification Messages")); + if (config->hasKey(dontShowAgainName)) + return false; + + // There is no current setting, so write one + saveDontShowAgainContinue(dontShowAgainName, !defaultShow); + return true; +} + +/****************************************************************************** +* Return whether a non-yes/no message box should be shown. +* If the message box has Cancel as the default button, either setContinueDefault() +* or warningContinueCancel() must have been called previously to set this for this +* 'dontShowAgainName' value. +*/ +bool MessageBox::shouldBeShownContinue(const QString& dontShowAgainName) +{ + if (getContinueDefault(dontShowAgainName) != Cancel) + return KMessageBox::shouldBeShownContinue(dontShowAgainName); + // Cancel is the default button, so we have to use a yes/no message box + ButtonCode b; + return shouldBeShownYesNo(dontShowAgainName, b); +} + + +/****************************************************************************** +* Save whether the yes/no message box should not be shown again. +* If 'dontShow' is true, the message box will be suppressed and it will return +* 'result'. +*/ +void MessageBox::saveDontShowAgainYesNo(const QString& dontShowAgainName, bool dontShow, ButtonCode result) +{ + saveDontShowAgain(dontShowAgainName, true, dontShow, (result == Yes ? "yes" : "no")); +} + +/****************************************************************************** +* Save whether a non-yes/no message box should not be shown again. +* If 'dontShow' is true, the message box will be suppressed and it will return +* Continue. +* If the message box has Cancel as the default button, either setContinueDefault() +* or warningContinueCancel() must have been called previously to set this for this +* 'dontShowAgainName' value. +*/ +void MessageBox::saveDontShowAgainContinue(const QString& dontShowAgainName, bool dontShow) +{ + if (getContinueDefault(dontShowAgainName) == Cancel) + saveDontShowAgainYesNo(dontShowAgainName, dontShow, Yes); + else + saveDontShowAgain(dontShowAgainName, false, dontShow); +} + +/****************************************************************************** +* Save whether the message box should not be shown again. +*/ +void MessageBox::saveDontShowAgain(const QString& dontShowAgainName, bool yesno, bool dontShow, const char* yesnoResult) +{ + if (dontShowAgainName.isEmpty()) + return; + KConfig* config = mConfig ? mConfig : KGlobal::config(); + config->setGroup(QString::fromLatin1("Notification Messages")); + bool global = (dontShowAgainName[0] == ':'); + if (yesno) + config->writeEntry(dontShowAgainName, QString::fromLatin1(dontShow ? yesnoResult : ""), true, global); + else + config->writeEntry(dontShowAgainName, !dontShow, true, global); + config->sync(); +} diff --git a/kalarm/lib/messagebox.h b/kalarm/lib/messagebox.h new file mode 100644 index 000000000..32e0d7325 --- /dev/null +++ b/kalarm/lib/messagebox.h @@ -0,0 +1,125 @@ +/* + * messagebox.h - enhanced KMessageBox class + * Program: kalarm + * Copyright (C) 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef MESSAGEBOX_H +#define MESSAGEBOX_H + +#include <kstdguiitem.h> +#include <kmessagebox.h> + + +/** + * @short Enhanced KMessageBox. + * + * The MessageBox class provides an extension to KMessageBox, including the option for + * Continue/Cancel message boxes to have a default button of Cancel. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class MessageBox : public KMessageBox +{ + public: + /** MessageBox types. + * @li CONT_CANCEL_DEF_CONT - Continue/Cancel, with Continue as the default button. + * @li CONT_CANCEL_DEF_CANCEL - Continue/Cancel, with Cancel as the default button. + * @li YES_NO_DEF_NO - Yes/No, with No as the default button. + */ + enum AskType { // MessageBox types + CONT_CANCEL_DEF_CONT, // Continue/Cancel, with default = Continue + CONT_CANCEL_DEF_CANCEL, // Continue/Cancel, with default = Cancel + YES_NO_DEF_NO // Yes/No, with default = No + }; + /** Gets the default button for the Continue/Cancel message box with the specified + * "don't ask again" name. + * @param dontAskAgainName The identifier controlling whether the message box is suppressed. + */ + static ButtonCode getContinueDefault(const QString& dontAskAgainName); + /** Sets the default button for the Continue/Cancel message box with the specified + * "don't ask again" name. + * @param dontAskAgainName The identifier controlling whether the message box is suppressed. + * @param defaultButton The default button for the message box. Valid values are Continue or Cancel. + */ + static void setContinueDefault(const QString& dontAskAgainName, ButtonCode defaultButton); + /** Displays a Continue/Cancel message box with the option as to which button is the default. + * @param parent Parent widget + * @param defaultButton The default button for the message box. Valid values are Continue or Cancel. + * @param text Message string + * @param caption Caption (window title) of the message box + * @param buttonContinue The text for the first button (default = i18n("Continue")) + * @param dontAskAgainName If specified, the message box will only be suppressed + * if the user chose Continue last time. + */ + static int warningContinueCancel(QWidget* parent, ButtonCode defaultButton, const QString& text, + const QString& caption = QString::null, + const KGuiItem& buttonContinue = KStdGuiItem::cont(), + const QString& dontAskAgainName = QString::null); + /** Displays a Continue/Cancel message box. + * @param parent Parent widget + * @param text Message string + * @param caption Caption (window title) of the message box + * @param buttonContinue The text for the first button (default = i18n("Continue")) + * @param dontAskAgainName If specified, (1) The message box will only be suppressed + * if the user chose Continue last time, and (2) The default button is that last set + * with either setContinueDefault() or warningContinueCancel() for the same + * @p dontAskAgainName value. If neither method has been used to set a default button, + * Continue is the default. + */ + static int warningContinueCancel(QWidget* parent, const QString& text, const QString& caption = QString::null, + const KGuiItem& buttonContinue = KStdGuiItem::cont(), + const QString& dontAskAgainName = QString::null); + /** If there is no current setting for whether a non-Yes/No message box should be + * shown, sets it to @p defaultShow. + * If a Continue/Cancel message box has Cancel as the default button, either + * setContinueDefault() or warningContinueCancel() must have been called + * previously to set this for the specified @p dontShowAgainName value. + * @return true if @p defaultShow was written. + */ + static bool setDefaultShouldBeShownContinue(const QString& dontShowAgainName, bool defaultShow); + /** Returns whether a non-Yes/No message box should be shown. + * If the message box has Cancel as the default button, either setContinueDefault() + * or warningContinueCancel() must have been called previously to set this for the + * specified @p dontShowAgainName value. + * @param dontShowAgainName The identifier controlling whether the message box is suppressed. + */ + static bool shouldBeShownContinue(const QString& dontShowAgainName); + /** Stores whether the Yes/No message box should or should not be shown again. + * @param dontShowAgainName The identifier controlling whether the message box is suppressed. + * @param dontShow If true, the message box will be suppressed and will return @p result. + * @param result The button code to return if the message box is suppressed. + */ + static void saveDontShowAgainYesNo(const QString& dontShowAgainName, bool dontShow = true, ButtonCode result = No); + /** Stores whether a non-Yes/No message box should or should not be shown again. + * If the message box has Cancel as the default button, either setContinueDefault() + * or warningContinueCancel() must have been called previously to set this for the + * specified @p dontShowAgainName value. + * @param dontShowAgainName The identifier controlling whether the message box is suppressed. + * @param dontShow If true, the message box will be suppressed and will return Continue. + */ + static void saveDontShowAgainContinue(const QString& dontShowAgainName, bool dontShow = true); + /** Sets the KConfig object to be used by the MessageBox class. */ + static void setDontShowAskAgainConfig(KConfig* cfg) { mConfig = cfg; } + + private: + static void saveDontShowAgain(const QString& dontShowAgainName, bool yesno, bool dontShow, const char* yesnoResult = 0); + static KConfig* mConfig; + static QMap<QString, ButtonCode> mContinueDefaults; +}; + +#endif diff --git a/kalarm/lib/pushbutton.cpp b/kalarm/lib/pushbutton.cpp new file mode 100644 index 000000000..8b279082c --- /dev/null +++ b/kalarm/lib/pushbutton.cpp @@ -0,0 +1,102 @@ +/* + * pushbutton.cpp - push button with read-only option + * Program: kalarm + * Copyright (c) 2002 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "pushbutton.moc" + + +PushButton::PushButton(QWidget* parent, const char* name) + : QPushButton(parent, name), + mFocusPolicy(focusPolicy()), + mReadOnly(false) +{ } + +PushButton::PushButton(const QString& text, QWidget* parent, const char* name) + : QPushButton(text, parent, name), + mFocusPolicy(focusPolicy()), + mReadOnly(false) +{ } + +PushButton::PushButton(const QIconSet& icon, const QString& text, QWidget* parent, const char* name) + : QPushButton(icon, text, parent, name), + mFocusPolicy(focusPolicy()), + mReadOnly(false) +{ } + +void PushButton::setReadOnly(bool ro) +{ + if ((int)ro != (int)mReadOnly) + { + mReadOnly = ro; + setFocusPolicy(ro ? QWidget::NoFocus : mFocusPolicy); + if (ro) + clearFocus(); + } +} + +void PushButton::mousePressEvent(QMouseEvent* e) +{ + if (mReadOnly) + { + // Swallow up the event if it's the left button + if (e->button() == Qt::LeftButton) + return; + } + QPushButton::mousePressEvent(e); +} + +void PushButton::mouseReleaseEvent(QMouseEvent* e) +{ + if (mReadOnly) + { + // Swallow up the event if it's the left button + if (e->button() == Qt::LeftButton) + return; + } + QPushButton::mouseReleaseEvent(e); +} + +void PushButton::mouseMoveEvent(QMouseEvent* e) +{ + if (!mReadOnly) + QPushButton::mouseMoveEvent(e); +} + +void PushButton::keyPressEvent(QKeyEvent* e) +{ + if (mReadOnly) + switch (e->key()) + { + case Qt::Key_Up: + case Qt::Key_Left: + case Qt::Key_Right: + case Qt::Key_Down: + // Process keys which shift the focus + break; + default: + return; + } + QPushButton::keyPressEvent(e); +} + +void PushButton::keyReleaseEvent(QKeyEvent* e) +{ + if (!mReadOnly) + QPushButton::keyReleaseEvent(e); +} diff --git a/kalarm/lib/pushbutton.h b/kalarm/lib/pushbutton.h new file mode 100644 index 000000000..6ef0f3a2a --- /dev/null +++ b/kalarm/lib/pushbutton.h @@ -0,0 +1,77 @@ +/* + * pushbutton.h - push button with read-only option + * Program: kalarm + * Copyright © 2002,2003,2005,2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef PUSHBUTTON_H +#define PUSHBUTTON_H + +#include <qpushbutton.h> + + +/** + * @short A QPushButton with read-only option. + * + * The PushButton class is a QPushButton with a read-only option. + * + * The widget may be set as read-only. This has the same effect as disabling it, except + * that its appearance is unchanged. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class PushButton : public QPushButton +{ + Q_OBJECT + Q_PROPERTY(bool readOnly READ isReadOnly WRITE setReadOnly) + public: + /** Constructor. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit PushButton(QWidget* parent, const char* name = 0); + /** Constructor for a push button which displays a text. + * @param text The text to show on the button. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + PushButton(const QString& text, QWidget* parent, const char* name = 0); + /** Constructor for a push button which displays an icon and a text. + * @param icon The icon to show on the button. + * @param text The text to show on the button. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + PushButton(const QIconSet& icon, const QString& text, QWidget* parent, const char* name = 0); + /** Sets whether the push button is read-only for the user. + * @param readOnly True to set the widget read-only, false to enable its action. + */ + virtual void setReadOnly(bool readOnly); + /** Returns true if the widget is read only. */ + virtual bool isReadOnly() const { return mReadOnly; } + protected: + virtual void mousePressEvent(QMouseEvent*); + virtual void mouseReleaseEvent(QMouseEvent*); + virtual void mouseMoveEvent(QMouseEvent*); + virtual void keyPressEvent(QKeyEvent*); + virtual void keyReleaseEvent(QKeyEvent*); + private: + QWidget::FocusPolicy mFocusPolicy; // default focus policy for the QPushButton + bool mReadOnly; // value cannot be changed +}; + +#endif // PUSHBUTTON_H diff --git a/kalarm/lib/radiobutton.cpp b/kalarm/lib/radiobutton.cpp new file mode 100644 index 000000000..d66911eeb --- /dev/null +++ b/kalarm/lib/radiobutton.cpp @@ -0,0 +1,134 @@ +/* + * radiobutton.cpp - radio button with read-only option + * Program: kalarm + * Copyright (c) 2002, 2003 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "radiobutton.moc" + + +RadioButton::RadioButton(QWidget* parent, const char* name) + : QRadioButton(parent, name), + mFocusPolicy(focusPolicy()), + mFocusWidget(0), + mReadOnly(false) +{ } + +RadioButton::RadioButton(const QString& text, QWidget* parent, const char* name) + : QRadioButton(text, parent, name), + mFocusPolicy(focusPolicy()), + mFocusWidget(0), + mReadOnly(false) +{ } + +/****************************************************************************** +* Set the read-only status. If read-only, the button can be toggled by the +* application, but not by the user. +*/ +void RadioButton::setReadOnly(bool ro) +{ + if ((int)ro != (int)mReadOnly) + { + mReadOnly = ro; + setFocusPolicy(ro ? QWidget::NoFocus : mFocusPolicy); + if (ro) + clearFocus(); + } +} + +/****************************************************************************** +* Specify a widget to receive focus when the button is clicked on. +*/ +void RadioButton::setFocusWidget(QWidget* w, bool enable) +{ + mFocusWidget = w; + mFocusWidgetEnable = enable; + if (w) + connect(this, SIGNAL(clicked()), SLOT(slotClicked())); + else + disconnect(this, SIGNAL(clicked()), this, SLOT(slotClicked())); +} + +/****************************************************************************** +* Called when the button is clicked. +* If it is now checked, focus is transferred to any specified focus widget. +*/ +void RadioButton::slotClicked() +{ + if (mFocusWidget && isChecked()) + { + if (mFocusWidgetEnable) + mFocusWidget->setEnabled(true); + mFocusWidget->setFocus(); + } +} + +/****************************************************************************** +* Event handlers to intercept events if in read-only mode. +* Any events which could change the button state are discarded. +*/ +void RadioButton::mousePressEvent(QMouseEvent* e) +{ + if (mReadOnly) + { + // Swallow up the event if it's the left button + if (e->button() == Qt::LeftButton) + return; + } + QRadioButton::mousePressEvent(e); +} + +void RadioButton::mouseReleaseEvent(QMouseEvent* e) +{ + if (mReadOnly) + { + // Swallow up the event if it's the left button + if (e->button() == Qt::LeftButton) + return; + } + QRadioButton::mouseReleaseEvent(e); +} + +void RadioButton::mouseMoveEvent(QMouseEvent* e) +{ + if (!mReadOnly) + QRadioButton::mouseMoveEvent(e); +} + +void RadioButton::keyPressEvent(QKeyEvent* e) +{ + if (mReadOnly) + switch (e->key()) + { + case Qt::Key_Up: + case Qt::Key_Left: + case Qt::Key_Right: + case Qt::Key_Down: + // Process keys which shift the focus + case Qt::Key_Escape: + break; + default: + return; + } + QRadioButton::keyPressEvent(e); +} + +void RadioButton::keyReleaseEvent(QKeyEvent* e) +{ + if (!mReadOnly) + QRadioButton::keyReleaseEvent(e); +} diff --git a/kalarm/lib/radiobutton.h b/kalarm/lib/radiobutton.h new file mode 100644 index 000000000..96bca04dd --- /dev/null +++ b/kalarm/lib/radiobutton.h @@ -0,0 +1,88 @@ +/* + * radiobutton.h - radio button with focus widget and read-only options + * Program: kalarm + * Copyright © 2002,2003,2005,2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef RADIOBUTTON_H +#define RADIOBUTTON_H + +#include <qradiobutton.h> + + +/** + * @short A QRadioButton with focus widget and read-only options. + * + * The RadioButton class is a QRadioButton with the ability to transfer focus to + * another widget when checked, and with a read-only option. + * + * Another widget may be specified as the focus widget for the radio button. Whenever + * the user clicks on the radio button so as to set its state to checked, focus is + * automatically transferred to the focus widget. + * + * The widget may be set as read-only. This has the same effect as disabling it, except + * that its appearance is unchanged. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class RadioButton : public QRadioButton +{ + Q_OBJECT + public: + /** Constructor. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit RadioButton(QWidget* parent, const char* name = 0); + /** Constructor. + * @param text Text to display. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + RadioButton(const QString& text, QWidget* parent, const char* name = 0); + /** Returns true if the widget is read only. */ + bool isReadOnly() const { return mReadOnly; } + /** Sets whether the radio button is read-only for the user. If read-only, + * its state cannot be changed by the user. + * @param readOnly True to set the widget read-only, false to set it read-write. + */ + virtual void setReadOnly(bool readOnly); + /** Returns the widget which receives focus when the button is clicked. */ + QWidget* focusWidget() const { return mFocusWidget; } + /** Specifies a widget to receive focus when the button is clicked. + * @param widget Widget to receive focus. + * @param enable If true, @p widget will be enabled before receiving focus. If + * false, the enabled state of @p widget will be left unchanged when + * the radio button is clicked. + */ + void setFocusWidget(QWidget* widget, bool enable = true); + protected: + virtual void mousePressEvent(QMouseEvent*); + virtual void mouseReleaseEvent(QMouseEvent*); + virtual void mouseMoveEvent(QMouseEvent*); + virtual void keyPressEvent(QKeyEvent*); + virtual void keyReleaseEvent(QKeyEvent*); + protected slots: + void slotClicked(); + private: + QWidget::FocusPolicy mFocusPolicy; // default focus policy for the QRadioButton + QWidget* mFocusWidget; // widget to receive focus when button is clicked on + bool mFocusWidgetEnable; // enable focus widget before setting focus + bool mReadOnly; // value cannot be changed +}; + +#endif // RADIOBUTTON_H diff --git a/kalarm/lib/shellprocess.cpp b/kalarm/lib/shellprocess.cpp new file mode 100644 index 000000000..1e37d2e74 --- /dev/null +++ b/kalarm/lib/shellprocess.cpp @@ -0,0 +1,208 @@ +/* + * shellprocess.cpp - execute a shell process + * Program: kalarm + * Copyright (c) 2004, 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <sys/stat.h> +#include <kapplication.h> +#include <klocale.h> +#include <kdebug.h> + +#include "shellprocess.moc" + + +QCString ShellProcess::mShellName; +QCString ShellProcess::mShellPath; +bool ShellProcess::mInitialised = false; +bool ShellProcess::mAuthorised = false; + + +ShellProcess::ShellProcess(const QString& command) + : KShellProcess(shellName()), + mCommand(command), + mStatus(INACTIVE), + mStdinExit(false) +{ +} + +/****************************************************************************** +* Execute a command. +*/ +bool ShellProcess::start(Communication comm) +{ + if (!authorised()) + { + mStatus = UNAUTHORISED; + return false; + } + KShellProcess::operator<<(mCommand); + connect(this, SIGNAL(wroteStdin(KProcess*)), SLOT(writtenStdin(KProcess*))); + connect(this, SIGNAL(processExited(KProcess*)), SLOT(slotExited(KProcess*))); + if (!KShellProcess::start(KProcess::NotifyOnExit, comm)) + { + mStatus = START_FAIL; + return false; + } + mStatus = RUNNING; + return true; +} + +/****************************************************************************** +* Called when a shell process execution completes. +* Interprets the exit status according to which shell was called, and emits +* a shellExited() signal. +*/ +void ShellProcess::slotExited(KProcess* proc) +{ + kdDebug(5950) << "ShellProcess::slotExited()\n"; + mStdinQueue.clear(); + mStatus = SUCCESS; + if (!proc->normalExit()) + { + kdWarning(5950) << "ShellProcess::slotExited(" << mCommand << ") " << mShellName << ": died/killed\n"; + mStatus = DIED; + } + else + { + // Some shells report if the command couldn't be found, or is not executable + int status = proc->exitStatus(); + if (mShellName == "bash" && (status == 126 || status == 127) + || mShellName == "ksh" && status == 127) + { + kdWarning(5950) << "ShellProcess::slotExited(" << mCommand << ") " << mShellName << ": not found or not executable\n"; + mStatus = NOT_FOUND; + } + } + emit shellExited(this); +} + +/****************************************************************************** +* Write a string to STDIN. +*/ +void ShellProcess::writeStdin(const char* buffer, int bufflen) +{ + QCString scopy(buffer, bufflen+1); // construct a deep copy + bool write = mStdinQueue.isEmpty(); + mStdinQueue.append(scopy); + if (write) + KProcess::writeStdin(mStdinQueue.first(), mStdinQueue.first().length()); +} + +/****************************************************************************** +* Called when output to STDIN completes. +* Send the next queued output, if any. +* Note that buffers written to STDIN must not be freed until the writtenStdin() +* signal has been processed. +*/ +void ShellProcess::writtenStdin(KProcess* proc) +{ + mStdinQueue.pop_front(); // free the buffer which has now been written + if (!mStdinQueue.isEmpty()) + proc->writeStdin(mStdinQueue.first(), mStdinQueue.first().length()); + else if (mStdinExit) + kill(); +} + +/****************************************************************************** +* Tell the process to exit once all STDIN strings have been written. +*/ +void ShellProcess::stdinExit() +{ + if (mStdinQueue.isEmpty()) + kill(); + else + mStdinExit = true; +} + +/****************************************************************************** +* Return the error message corresponding to the command exit status. +* Reply = null string if not yet exited, or if command successful. +*/ +QString ShellProcess::errorMessage() const +{ + switch (mStatus) + { + case UNAUTHORISED: + return i18n("Failed to execute command (shell access not authorized):"); + case START_FAIL: + case NOT_FOUND: + return i18n("Failed to execute command:"); + case DIED: + return i18n("Command execution error:"); + case INACTIVE: + case RUNNING: + case SUCCESS: + default: + return QString::null; + } +} + +/****************************************************************************** +* Determine which shell to use. +* This is a duplication of what KShellProcess does, but we need to know +* which shell is used in order to decide what its exit code means. +*/ +const QCString& ShellProcess::shellPath() +{ + if (mShellPath.isEmpty()) + { + // Get the path to the shell + mShellPath = "/bin/sh"; + QCString envshell = QCString(getenv("SHELL")).stripWhiteSpace(); + if (!envshell.isEmpty()) + { + struct stat fileinfo; + if (stat(envshell.data(), &fileinfo) != -1 // ensure file exists + && !S_ISDIR(fileinfo.st_mode) // and it's not a directory + && !S_ISCHR(fileinfo.st_mode) // and it's not a character device + && !S_ISBLK(fileinfo.st_mode) // and it's not a block device +#ifdef S_ISSOCK + && !S_ISSOCK(fileinfo.st_mode) // and it's not a socket +#endif + && !S_ISFIFO(fileinfo.st_mode) // and it's not a fifo + && !access(envshell.data(), X_OK)) // and it's executable + mShellPath = envshell; + } + + // Get the shell filename with the path stripped off + int i = mShellPath.findRev('/'); + if (i >= 0) + mShellName = mShellPath.mid(i + 1); + else + mShellName = mShellPath; + } + return mShellPath; +} + +/****************************************************************************** +* Check whether shell commands are allowed at all. +*/ +bool ShellProcess::authorised() +{ + if (!mInitialised) + { + mAuthorised = kapp->authorize("shell_access"); + mInitialised = true; + } + return mAuthorised; +} diff --git a/kalarm/lib/shellprocess.h b/kalarm/lib/shellprocess.h new file mode 100644 index 000000000..06a262a8d --- /dev/null +++ b/kalarm/lib/shellprocess.h @@ -0,0 +1,138 @@ +/* + * shellprocess.h - execute a process through the shell + * Program: kalarm + * Copyright © 2004-2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef SHELLPROCESS_H +#define SHELLPROCESS_H + +/** @file shellprocess.h - execute a process through the shell */ + +#include <kprocess.h> + + +/** + * @short Enhanced KShellProcess. + * + * The ShellProcess class runs a shell command and interprets the shell exit status + * as far as possible. It blocks execution if shell access is prohibited. It buffers + * data written to the process's stdin. + * + * Before executing any command, ShellProcess checks whether shell commands are + * allowed at all. If not (e.g. if the user is running in kiosk mode), it blocks + * execution. + * + * Derived from KShellProcess, this class additionally tries to interpret the shell + * exit status. Different shells use different exit codes. Currently, if bash or ksh + * report that the command could not be found or could not be executed, the NOT_FOUND + * status is returned. + * + * Writes to the process's stdin are buffered, so that unlike with KShellProcess, there + * is no need to wait for the write to complete before writing again. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class ShellProcess : public KShellProcess +{ + Q_OBJECT + public: + /** Current status of the shell process. + * @li INACTIVE - start() has not yet been called to run the command. + * @li RUNNING - the command is currently running. + * @li SUCCESS - the command appears to have exited successfully. + * @li UNAUTHORISED - shell commands are not authorised for this user. + * @li DIED - the command didn't exit cleanly, i.e. was killed or died. + * @li NOT_FOUND - the command was either not found or not executable. + * @li START_FAIL - the command couldn't be started for other reasons. + */ + enum Status { + INACTIVE, // start() has not yet been called to run the command + RUNNING, // command is currently running + SUCCESS, // command appears to have exited successfully + UNAUTHORISED, // shell commands are not authorised for this user + DIED, // command didn't exit cleanly, i.e. was killed or died + NOT_FOUND, // command either not found or not executable + START_FAIL // command couldn't be started for other reasons + }; + /** Constructor. + * @param command The command line to be run when start() is called. + */ + explicit ShellProcess(const QString& command); + /** Executes the configured command. + * @param comm Which communication links should be established to the child process + * (stdin/stdout/stderr). + */ + bool start(Communication comm = NoCommunication); + /** Returns the current status of the shell process. */ + Status status() const { return mStatus; } + /** Returns whether the command was run successfully. + * @return True if the command has been run and appears to have exited successfully. + */ + bool normalExit() const { return mStatus == SUCCESS; } + /** Returns the command configured to be run. */ + const QString& command() const { return mCommand; } + /** Returns the error message corresponding to the command exit status. + * @return Error message if an error occurred. Null string if the command has not yet + * exited, or if the command ran successfully. + */ + QString errorMessage() const; + /** Writes a string to the process's STDIN. */ + void writeStdin(const char* buffer, int bufflen); + /** Tell the process to exit once any outstanding STDIN strings have been written. */ + void stdinExit(); + /** Returns whether the user is authorised to run shell commands. Shell commands may + * be prohibited in kiosk mode, for example. + */ + static bool authorised(); + /** Determines which shell to use. + * @return file name of shell, excluding path. + */ + static const QCString& shellName() { shellPath(); return mShellName; } + /** Determines which shell to use. + * @return path name of shell. + */ + static const QCString& shellPath(); + + signals: + /** Signal emitted when the shell process execution completes. It is not emitted + * if start() did not attempt to start the command execution, e.g. in kiosk mode. + */ + void shellExited(ShellProcess*); + + private slots: + void writtenStdin(KProcess*); + void slotExited(KProcess*); + + private: + // Prohibit the following inherited methods + ShellProcess& operator<<(const QString&); + ShellProcess& operator<<(const QCString&); + ShellProcess& operator<<(const QStringList&); + ShellProcess& operator<<(const char*); + + static QCString mShellName; // name of shell to be used + static QCString mShellPath; // path of shell to be used + static bool mInitialised; // true once static data has been initialised + static bool mAuthorised; // true if shell commands are authorised + QString mCommand; // copy of command to be executed + QValueList<QCString> mStdinQueue; // queued strings to send to STDIN + Status mStatus; // current execution status + bool mStdinExit; // exit once STDIN queue has been written +}; + +#endif // SHELLPROCESS_H diff --git a/kalarm/lib/slider.cpp b/kalarm/lib/slider.cpp new file mode 100644 index 000000000..1a0999645 --- /dev/null +++ b/kalarm/lib/slider.cpp @@ -0,0 +1,85 @@ +/* + * slider.cpp - slider control with read-only option + * Program: kalarm + * Copyright (c) 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "slider.moc" + + +Slider::Slider(QWidget* parent, const char* name) + : QSlider(parent, name), + mReadOnly(false) +{ } + +Slider::Slider(Orientation o, QWidget* parent, const char* name) + : QSlider(o, parent, name), + mReadOnly(false) +{ } + +Slider::Slider(int minval, int maxval, int pageStep, int value, Orientation o, QWidget* parent, const char* name) + : QSlider(minval, maxval, pageStep, value, o, parent, name), + mReadOnly(false) +{ } + +/****************************************************************************** +* Set the read-only status. If read-only, the slider can be moved by the +* application, but not by the user. +*/ +void Slider::setReadOnly(bool ro) +{ + mReadOnly = ro; +} + +/****************************************************************************** +* Event handlers to intercept events if in read-only mode. +* Any events which could change the slider value are discarded. +*/ +void Slider::mousePressEvent(QMouseEvent* e) +{ + if (mReadOnly) + { + // Swallow up the event if it's the left button + if (e->button() == Qt::LeftButton) + return; + } + QSlider::mousePressEvent(e); +} + +void Slider::mouseReleaseEvent(QMouseEvent* e) +{ + if (!mReadOnly) + QSlider::mouseReleaseEvent(e); +} + +void Slider::mouseMoveEvent(QMouseEvent* e) +{ + if (!mReadOnly) + QSlider::mouseMoveEvent(e); +} + +void Slider::keyPressEvent(QKeyEvent* e) +{ + if (!mReadOnly || e->key() == Qt::Key_Escape) + QSlider::keyPressEvent(e); +} + +void Slider::keyReleaseEvent(QKeyEvent* e) +{ + if (!mReadOnly) + QSlider::keyReleaseEvent(e); +} diff --git a/kalarm/lib/slider.h b/kalarm/lib/slider.h new file mode 100644 index 000000000..17635e68a --- /dev/null +++ b/kalarm/lib/slider.h @@ -0,0 +1,80 @@ +/* + * slider.h - slider control with read-only option + * Program: kalarm + * Copyright © 2004,2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef SLIDER_H +#define SLIDER_H + +#include <qslider.h> + + +/** + * @short A QSlider with read-only option. + * + * The Slider class is a QSlider with a read-only option. + * + * The widget may be set as read-only. This has the same effect as disabling it, except + * that its appearance is unchanged. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class Slider : public QSlider +{ + Q_OBJECT + Q_PROPERTY(bool readOnly READ isReadOnly WRITE setReadOnly) + public: + /** Constructor. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit Slider(QWidget* parent = 0, const char* name = 0); + /** Constructor. + * @param orient The orientation of the slider, either Qt::Horizonal or Qt::Vertical. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit Slider(Orientation orient, QWidget* parent = 0, const char* name = 0); + /** Constructor. + * @param minValue The minimum value which the slider can have. + * @param maxValue The maximum value which the slider can have. + * @param pageStep The page step increment. + * @param value The initial value for the slider. + * @param orient The orientation of the slider, either Qt::Horizonal or Qt::Vertical. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + Slider(int minValue, int maxValue, int pageStep, int value, Orientation orient, + QWidget* parent = 0, const char* name = 0); + /** Returns true if the slider is read only. */ + bool isReadOnly() const { return mReadOnly; } + /** Sets whether the slider is read-only for the user. + * @param readOnly True to set the widget read-only, false to set it read-write. + */ + virtual void setReadOnly(bool readOnly); + protected: + virtual void mousePressEvent(QMouseEvent*); + virtual void mouseReleaseEvent(QMouseEvent*); + virtual void mouseMoveEvent(QMouseEvent*); + virtual void keyPressEvent(QKeyEvent*); + virtual void keyReleaseEvent(QKeyEvent*); + private: + bool mReadOnly; // value cannot be changed by the user +}; + +#endif // SLIDER_H diff --git a/kalarm/lib/spinbox.cpp b/kalarm/lib/spinbox.cpp new file mode 100644 index 000000000..bb9ef2441 --- /dev/null +++ b/kalarm/lib/spinbox.cpp @@ -0,0 +1,476 @@ +/* + * spinbox.cpp - spin box with read-only option and shift-click step value + * Program: kalarm + * Copyright © 2002,2004,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include <kdeversion.h> +#include <qlineedit.h> +#include <qobjectlist.h> +#include "spinbox.moc" + + +SpinBox::SpinBox(QWidget* parent, const char* name) + : QSpinBox(0, 99999, 1, parent, name), + mMinValue(QSpinBox::minValue()), + mMaxValue(QSpinBox::maxValue()) +{ + init(); +} + +SpinBox::SpinBox(int minValue, int maxValue, int step, QWidget* parent, const char* name) + : QSpinBox(minValue, maxValue, step, parent, name), + mMinValue(minValue), + mMaxValue(maxValue) +{ + init(); +} + +void SpinBox::init() +{ + int step = QSpinBox::lineStep(); + mLineStep = step; + mLineShiftStep = step; + mCurrentButton = NO_BUTTON; + mShiftMouse = false; + mShiftMinBound = false; + mShiftMaxBound = false; + mSelectOnStep = true; + mReadOnly = false; + mSuppressSignals = false; + mEdited = false; + + // Find the spin widgets which are part of the spin boxes, in order to + // handle their shift-button presses. + QObjectList* spinwidgets = queryList("QSpinWidget", 0, false, true); + QSpinWidget* spin = (QSpinWidget*)spinwidgets->getFirst(); + if (spin) + spin->installEventFilter(this); // handle shift-button presses + delete spinwidgets; + editor()->installEventFilter(this); // handle shift-up/down arrow presses + +#if KDE_IS_VERSION(3,1,90) + // Detect when the text field is edited + connect(editor(), SIGNAL(textChanged(const QString&)), SLOT(textEdited())); +#endif +} + +void SpinBox::setReadOnly(bool ro) +{ + if ((int)ro != (int)mReadOnly) + { + mReadOnly = ro; + editor()->setReadOnly(ro); + if (ro) + setShiftStepping(false, mCurrentButton); + } +} + +int SpinBox::bound(int val) const +{ + return (val < mMinValue) ? mMinValue : (val > mMaxValue) ? mMaxValue : val; +} + +void SpinBox::setMinValue(int val) +{ + mMinValue = val; + QSpinBox::setMinValue(val); + mShiftMinBound = false; +} + +void SpinBox::setMaxValue(int val) +{ + mMaxValue = val; + QSpinBox::setMaxValue(val); + mShiftMaxBound = false; +} + +void SpinBox::setLineStep(int step) +{ + mLineStep = step; + if (!mShiftMouse) + QSpinBox::setLineStep(step); +} + +void SpinBox::setLineShiftStep(int step) +{ + mLineShiftStep = step; + if (mShiftMouse) + QSpinBox::setLineStep(step); +} + +void SpinBox::stepUp() +{ + int step = QSpinBox::lineStep(); + addValue(step); + emit stepped(step); +} + +void SpinBox::stepDown() +{ + int step = -QSpinBox::lineStep(); + addValue(step); + emit stepped(step); +} + +/****************************************************************************** +* Adds a positive or negative increment to the current value, wrapping as appropriate. +* If 'current' is true, any temporary 'shift' values for the range are used instead +* of the real maximum and minimum values. +*/ +void SpinBox::addValue(int change, bool current) +{ + int newval = value() + change; + int maxval = current ? QSpinBox::maxValue() : mMaxValue; + int minval = current ? QSpinBox::minValue() : mMinValue; + if (wrapping()) + { + int range = maxval - minval + 1; + if (newval > maxval) + newval = minval + (newval - maxval - 1) % range; + else if (newval < minval) + newval = maxval - (minval - 1 - newval) % range; + } + else + { + if (newval > maxval) + newval = maxval; + else if (newval < minval) + newval = minval; + } + setValue(newval); +} + +void SpinBox::valueChange() +{ + if (!mSuppressSignals) + { + int val = value(); + if (mShiftMinBound && val >= mMinValue) + { + // Reinstate the minimum bound now that the value has returned to the normal range. + QSpinBox::setMinValue(mMinValue); + mShiftMinBound = false; + } + if (mShiftMaxBound && val <= mMaxValue) + { + // Reinstate the maximum bound now that the value has returned to the normal range. + QSpinBox::setMaxValue(mMaxValue); + mShiftMaxBound = false; + } + + bool focus = !mSelectOnStep && hasFocus(); + if (focus) + clearFocus(); // prevent selection of the spin box text + QSpinBox::valueChange(); + if (focus) + setFocus(); + } +} + +/****************************************************************************** +* Called whenever the line edit text is changed. +*/ +void SpinBox::textEdited() +{ + mEdited = true; +} + +void SpinBox::updateDisplay() +{ + mEdited = false; + QSpinBox::updateDisplay(); +} + +/****************************************************************************** +* Receives events destined for the spin widget or for the edit field. +*/ +bool SpinBox::eventFilter(QObject* obj, QEvent* e) +{ + if (obj == editor()) + { + int step = 0; + bool shift = false; + switch (e->type()) + { + case QEvent::KeyPress: + { + // Up and down arrow keys step the value + QKeyEvent* ke = (QKeyEvent*)e; + int key = ke->key(); + if (key == Qt::Key_Up) + step = 1; + else if (key == Qt::Key_Down) + step = -1; + shift = ((ke->state() & (Qt::ShiftButton | Qt::AltButton)) == Qt::ShiftButton); + break; + } + case QEvent::Wheel: + { + QWheelEvent* we = (QWheelEvent*)e; + step = (we->delta() > 0) ? 1 : -1; + shift = ((we->state() & (Qt::ShiftButton | Qt::AltButton)) == Qt::ShiftButton); + break; + } +#if KDE_IS_VERSION(3,1,90) + case QEvent::Leave: + if (mEdited) + interpretText(); + break; +#endif + default: + break; + } + if (step) + { + if (mReadOnly) + return true; // discard up/down arrow keys or wheel + if (shift) + { + // Shift stepping + int val = value(); + if (step > 0) + step = mLineShiftStep - val % mLineShiftStep; + else + step = - ((val + mLineShiftStep - 1) % mLineShiftStep + 1); + } + else + step = (step > 0) ? mLineStep : -mLineStep; + addValue(step, false); + return true; + } + } + else + { + int etype = e->type(); // avoid switch compile warnings + switch (etype) + { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonDblClick: + { + QMouseEvent* me = (QMouseEvent*)e; + if (me->button() == Qt::LeftButton) + { + // It's a left button press. Set normal or shift stepping as appropriate. + if (mReadOnly) + return true; // discard the event + mCurrentButton = whichButton(me->pos()); + if (mCurrentButton == NO_BUTTON) + return true; + bool shift = (me->state() & (Qt::ShiftButton | Qt::AltButton)) == Qt::ShiftButton; + if (setShiftStepping(shift, mCurrentButton)) + return true; // hide the event from the spin widget + return false; // forward event to the destination widget + } + break; + } + case QEvent::MouseButtonRelease: + { + QMouseEvent* me = (QMouseEvent*)e; + if (me->button() == Qt::LeftButton && mShiftMouse) + { + setShiftStepping(false, mCurrentButton); // cancel shift stepping + return false; // forward event to the destination widget + } + break; + } + case QEvent::MouseMove: + { + QMouseEvent* me = (QMouseEvent*)e; + if (me->state() & Qt::LeftButton) + { + // The left button is down. Track which spin button it's in. + if (mReadOnly) + return true; // discard the event + int newButton = whichButton(me->pos()); + if (newButton != mCurrentButton) + { + // The mouse has moved to a new spin button. + // Set normal or shift stepping as appropriate. + mCurrentButton = newButton; + bool shift = (me->state() & (Qt::ShiftButton | Qt::AltButton)) == Qt::ShiftButton; + if (setShiftStepping(shift, mCurrentButton)) + return true; // hide the event from the spin widget + } + return false; // forward event to the destination widget + } + break; + } + case QEvent::Wheel: + { + QWheelEvent* we = (QWheelEvent*)e; + bool shift = (we->state() & (Qt::ShiftButton | Qt::AltButton)) == Qt::ShiftButton; + if (setShiftStepping(shift, (we->delta() > 0 ? UP : DOWN))) + return true; // hide the event from the spin widget + return false; // forward event to the destination widget + } + case QEvent::KeyPress: + case QEvent::KeyRelease: + case QEvent::AccelOverride: // this is needed to receive Shift presses! + { + QKeyEvent* ke = (QKeyEvent*)e; + int key = ke->key(); + int state = ke->state(); + if ((state & Qt::LeftButton) + && (key == Qt::Key_Shift || key == Qt::Key_Alt)) + { + // The left mouse button is down, and the Shift or Alt key has changed + if (mReadOnly) + return true; // discard the event + state ^= (key == Qt::Key_Shift) ? Qt::ShiftButton : Qt::AltButton; // new state + bool shift = (state & (Qt::ShiftButton | Qt::AltButton)) == Qt::ShiftButton; + if ((!shift && mShiftMouse) || (shift && !mShiftMouse)) + { + // The effective shift state has changed. + // Set normal or shift stepping as appropriate. + if (setShiftStepping(shift, mCurrentButton)) + return true; // hide the event from the spin widget + } + } + break; + } + } + } + return QSpinBox::eventFilter(obj, e); +} + +/****************************************************************************** +* Set spin widget stepping to the normal or shift increment. +*/ +bool SpinBox::setShiftStepping(bool shift, int currentButton) +{ + if (currentButton == NO_BUTTON) + shift = false; + if (shift && !mShiftMouse) + { + /* The value is to be stepped to a multiple of the shift increment. + * Adjust the value so that after the spin widget steps it, it will be correct. + * Then, if the mouse button is held down, the spin widget will continue to + * step by the shift amount. + */ + int val = value(); + int step = (currentButton == UP) ? mLineShiftStep : (currentButton == DOWN) ? -mLineShiftStep : 0; + int adjust = shiftStepAdjustment(val, step); + mShiftMouse = true; + if (adjust) + { + /* The value is to be stepped by other than the shift increment, + * presumably because it is being set to a multiple of the shift + * increment. Achieve this by making the adjustment here, and then + * allowing the normal step processing to complete the job by + * adding/subtracting the normal shift increment. + */ + if (!wrapping()) + { + // Prevent the step from going past the spinbox's range, or + // to the minimum value if that has a special text unless it is + // already at the minimum value + 1. + int newval = val + adjust + step; + int svt = specialValueText().isEmpty() ? 0 : 1; + int minval = mMinValue + svt; + if (newval <= minval || newval >= mMaxValue) + { + // Stepping to the minimum or maximum value + if (svt && newval <= mMinValue && val == mMinValue) + newval = mMinValue; + else + newval = (newval <= minval) ? minval : mMaxValue; + QSpinBox::setValue(newval); + emit stepped(step); + return true; + } + + // If the interim value will lie outside the spinbox's range, + // temporarily adjust the range to allow the value to be set. + int tempval = val + adjust; + if (tempval < mMinValue) + { + QSpinBox::setMinValue(tempval); + mShiftMinBound = true; + } + else if (tempval > mMaxValue) + { + QSpinBox::setMaxValue(tempval); + mShiftMaxBound = true; + } + } + + // Don't process changes since this new value will be stepped immediately + mSuppressSignals = true; + bool blocked = signalsBlocked(); + blockSignals(true); + addValue(adjust, true); + blockSignals(blocked); + mSuppressSignals = false; + } + QSpinBox::setLineStep(mLineShiftStep); + } + else if (!shift && mShiftMouse) + { + // Reinstate to normal (non-shift) stepping + QSpinBox::setLineStep(mLineStep); + QSpinBox::setMinValue(mMinValue); + QSpinBox::setMaxValue(mMaxValue); + mShiftMinBound = mShiftMaxBound = false; + mShiftMouse = false; + } + return false; +} + +/****************************************************************************** +* Return the initial adjustment to the value for a shift step up or down. +* The default is to step up or down to the nearest multiple of the shift +* increment, so the adjustment returned is for stepping up the decrement +* required to round down to a multiple of the shift increment <= current value, +* or for stepping down the increment required to round up to a multiple of the +* shift increment >= current value. +* This method's caller then adjusts the resultant value if necessary to cater +* for the widget's minimum/maximum value, and wrapping. +* This should really be a static method, but it needs to be virtual... +*/ +int SpinBox::shiftStepAdjustment(int oldValue, int shiftStep) +{ + if (oldValue == 0 || shiftStep == 0) + return 0; + if (shiftStep > 0) + { + if (oldValue >= 0) + return -(oldValue % shiftStep); + else + return (-oldValue - 1) % shiftStep + 1 - shiftStep; + } + else + { + shiftStep = -shiftStep; + if (oldValue >= 0) + return shiftStep - ((oldValue - 1) % shiftStep + 1); + else + return (-oldValue) % shiftStep; + } +} + +/****************************************************************************** +* Find which spin widget button a mouse event is in. +*/ +int SpinBox::whichButton(const QPoint& pos) +{ + if (upRect().contains(pos)) + return UP; + if (downRect().contains(pos)) + return DOWN; + return NO_BUTTON; +} diff --git a/kalarm/lib/spinbox.h b/kalarm/lib/spinbox.h new file mode 100644 index 000000000..0df910233 --- /dev/null +++ b/kalarm/lib/spinbox.h @@ -0,0 +1,156 @@ +/* + * spinbox.h - spin box with shift-click step value and read-only option + * Program: kalarm + * Copyright © 2002-2006,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef SPINBOX_H +#define SPINBOX_H + +#include <qspinbox.h> + + +/** + * @short Spin box with accelerated shift key stepping and read-only option. + * + * The SpinBox class provides a QSpinBox with accelerated stepping using the shift key. + * + * A separate step increment may optionally be specified for use when the shift key is + * held down. Typically this would be larger than the normal step. Then, when the user + * clicks the spin buttons, he/she can increment or decrement the value faster by holding + * the shift key down. + * + * The widget may be set as read-only. This has the same effect as disabling it, except + * that its appearance is unchanged. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class SpinBox : public QSpinBox +{ + Q_OBJECT + public: + /** Constructor. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit SpinBox(QWidget* parent = 0, const char* name = 0); + /** Constructor. + * @param minValue The minimum value which the spin box can have. + * @param maxValue The maximum value which the spin box can have. + * @param step The (unshifted) step interval. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + SpinBox(int minValue, int maxValue, int step = 1, QWidget* parent = 0, const char* name = 0); + /** Returns true if the widget is read only. */ + bool isReadOnly() const { return mReadOnly; } + /** Sets whether the spin box can be changed by the user. + * @param readOnly True to set the widget read-only, false to set it read-write. + */ + virtual void setReadOnly(bool readOnly); + /** Returns whether the spin box value text is selected when its value is stepped. */ + bool selectOnStep() const { return mSelectOnStep; } + /** Sets whether the spin box value text should be selected when its value is stepped. */ + void setSelectOnStep(bool sel) { mSelectOnStep = sel; } + /** Adds a value to the current value of the spin box. */ + void addValue(int change) { addValue(change, false); } + /** Returns the minimum value of the spin box. */ + int minValue() const { return mMinValue; } + /** Returns the maximum value of the spin box. */ + int maxValue() const { return mMaxValue; } + /** Sets the minimum value of the spin box. */ + void setMinValue(int val); + /** Sets the maximum value of the spin box. */ + void setMaxValue(int val); + /** Sets the minimum and maximum values of the spin box. */ + void setRange(int minValue, int maxValue) { setMinValue(minValue); setMaxValue(maxValue); } + /** Returns the specified value clamped to the range of the spin box. */ + int bound(int val) const; + /** Returns the unshifted step increment, i.e. the amount by which the spin box value + * changes when a spin button is clicked without the shift key being pressed. + */ + int lineStep() const { return mLineStep; } + /** Sets the unshifted step increment, i.e. the amount by which the spin box value + * changes when a spin button is clicked without the shift key being pressed. + */ + void setLineStep(int step); + /** Returns the shifted step increment, i.e. the amount by which the spin box value + * changes when a spin button is clicked while the shift key is pressed. + */ + int lineShiftStep() const { return mLineShiftStep; } + /** Sets the shifted step increment, i.e. the amount by which the spin box value + * changes when a spin button is clicked while the shift key is pressed. + */ + void setLineShiftStep(int step); + public slots: + /** Increments the value of the spin box by the unshifted step increment. */ + virtual void stepUp(); + /** Decrements the value of the spin box by the unshifted step increment. */ + virtual void stepDown(); + signals: + /** Signal emitted when the spin box's value is stepped (by the shifted or unshifted increment). + * @param step The requested step in the spin box's value. Note that the actual change in value + * may have been less than this. + */ + void stepped(int step); + + protected: + /** A virtual method called whenever the value of the spin box has changed. */ + virtual void valueChange(); + /** Returns the initial adjustment to the value for a shift step up or down. + * The default is to step up or down to the nearest multiple of the shift + * increment, so the adjustment returned is for stepping up the decrement + * required to round down to a multiple of the shift increment <= current value, + * or for stepping down the increment required to round up to a multiple of the + * shift increment >= current value. + * This method's caller then adjusts the resultant value if necessary to cater + * for the widget's minimum/maximum value, and wrapping. + * This should really be a static method, but it needs to be virtual... + */ + virtual int shiftStepAdjustment(int oldValue, int shiftStep); + /** Receives events destined for the spin widget or for the edit field. */ + virtual bool eventFilter(QObject*, QEvent*); + /** Updates the contents of the embedded QLineEdit to reflect the current value + * using mapValueToText(). Also enables/disables the up/down push buttons accordingly. + */ + virtual void updateDisplay(); + + private slots: + void textEdited(); + private: + void init(); + void addValue(int change, bool current); + int whichButton(const QPoint&); + bool setShiftStepping(bool, int currentButton); + + enum { NO_BUTTON, UP, DOWN }; + + int mMinValue; + int mMaxValue; + int mLineStep; // step when spin arrows are pressed + int mLineShiftStep; // step when spin arrows are pressed with shift key + int mCurrentButton; // current spin widget button + bool mShiftMouse; // true while left button is being held down with shift key + bool mShiftMinBound; // true if a temporary minimum bound has been set during shift stepping + bool mShiftMaxBound; // true if a temporary maximum bound has been set during shift stepping + bool mSelectOnStep; // select the editor text whenever spin buttons are clicked (default) + bool mReadOnly; // value cannot be changed + bool mSuppressSignals; + bool mEdited; // text field has been edited +}; + +#endif // SPINBOX_H diff --git a/kalarm/lib/spinbox2.cpp b/kalarm/lib/spinbox2.cpp new file mode 100644 index 000000000..313d3eb56 --- /dev/null +++ b/kalarm/lib/spinbox2.cpp @@ -0,0 +1,511 @@ +/* + * spinbox2.cpp - spin box with extra pair of spin buttons (for Qt 3) + * Program: kalarm + * Copyright © 2001-2005,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include <qglobal.h> + +#include <stdlib.h> + +#include <qstyle.h> +#include <qobjectlist.h> +#include <qapplication.h> +#include <qpixmap.h> +#include <qcursor.h> +#include <qtimer.h> +#include <qwmatrix.h> + +#include "spinbox2.moc" +#include "spinbox2private.moc" + + +/* List of styles which need to display the extra pair of spin buttons as a + * left-to-right mirror image. This is only necessary when, for example, the + * corners of widgets are rounded. For most styles, it is better not to mirror + * the spin widgets so as to keep the normal lighting/shading on either side. + */ +static const char* mirrorStyles[] = { + "PlastikStyle", + 0 // list terminator +}; +static bool mirrorStyle(const QStyle&); + + +int SpinBox2::mReverseLayout = -1; + +SpinBox2::SpinBox2(QWidget* parent, const char* name) + : QFrame(parent, name), + mReverseWithLayout(true) +{ + mUpdown2Frame = new QFrame(this); + mSpinboxFrame = new QFrame(this); + mUpdown2 = new ExtraSpinBox(mUpdown2Frame, "updown2"); +// mSpinbox = new MainSpinBox(0, 1, 1, this, mSpinboxFrame); + mSpinbox = new MainSpinBox(this, mSpinboxFrame); + init(); +} + +SpinBox2::SpinBox2(int minValue, int maxValue, int step, int step2, QWidget* parent, const char* name) + : QFrame(parent, name), + mReverseWithLayout(true) +{ + mUpdown2Frame = new QFrame(this); + mSpinboxFrame = new QFrame(this); + mUpdown2 = new ExtraSpinBox(minValue, maxValue, step2, mUpdown2Frame, "updown2"); + mSpinbox = new MainSpinBox(minValue, maxValue, step, this, mSpinboxFrame); + setSteps(step, step2); + init(); +} + +void SpinBox2::init() +{ + if (mReverseLayout < 0) + mReverseLayout = QApplication::reverseLayout() ? 1 : 0; + mMinValue = mSpinbox->minValue(); + mMaxValue = mSpinbox->maxValue(); + mLineStep = mSpinbox->lineStep(); + mLineShiftStep = mSpinbox->lineShiftStep(); + mPageStep = mUpdown2->lineStep(); + mPageShiftStep = mUpdown2->lineShiftStep(); + mSpinbox->setSelectOnStep(false); // default + mUpdown2->setSelectOnStep(false); // always false + setFocusProxy(mSpinbox); + mUpdown2->setFocusPolicy(QWidget::NoFocus); + mSpinMirror = new SpinMirror(mUpdown2, mUpdown2Frame, this); + if (!mirrorStyle(style())) + mSpinMirror->hide(); // hide mirrored spin buttons when they are inappropriate + connect(mSpinbox, SIGNAL(valueChanged(int)), SLOT(valueChange())); + connect(mSpinbox, SIGNAL(valueChanged(int)), SIGNAL(valueChanged(int))); + connect(mSpinbox, SIGNAL(valueChanged(const QString&)), SIGNAL(valueChanged(const QString&))); + connect(mUpdown2, SIGNAL(stepped(int)), SLOT(stepPage(int))); + connect(mUpdown2, SIGNAL(styleUpdated()), SLOT(updateMirror())); +} + +void SpinBox2::setReadOnly(bool ro) +{ + if (static_cast<int>(ro) != static_cast<int>(mSpinbox->isReadOnly())) + { + mSpinbox->setReadOnly(ro); + mUpdown2->setReadOnly(ro); + mSpinMirror->setReadOnly(ro); + } +} + +void SpinBox2::setReverseWithLayout(bool reverse) +{ + if (reverse != mReverseWithLayout) + { + mReverseWithLayout = reverse; + setSteps(mLineStep, mPageStep); + setShiftSteps(mLineShiftStep, mPageShiftStep); + } +} + +void SpinBox2::setEnabled(bool enabled) +{ + QFrame::setEnabled(enabled); + updateMirror(); +} + +void SpinBox2::setWrapping(bool on) +{ + mSpinbox->setWrapping(on); + mUpdown2->setWrapping(on); +} + +QRect SpinBox2::up2Rect() const +{ + return mUpdown2->upRect(); +} + +QRect SpinBox2::down2Rect() const +{ + return mUpdown2->downRect(); +} + +void SpinBox2::setLineStep(int step) +{ + mLineStep = step; + if (reverseButtons()) + mUpdown2->setLineStep(step); // reverse layout, but still set the right buttons + else + mSpinbox->setLineStep(step); +} + +void SpinBox2::setSteps(int line, int page) +{ + mLineStep = line; + mPageStep = page; + if (reverseButtons()) + { + mUpdown2->setLineStep(line); // reverse layout, but still set the right buttons + mSpinbox->setLineStep(page); + } + else + { + mSpinbox->setLineStep(line); + mUpdown2->setLineStep(page); + } +} + +void SpinBox2::setShiftSteps(int line, int page) +{ + mLineShiftStep = line; + mPageShiftStep = page; + if (reverseButtons()) + { + mUpdown2->setLineShiftStep(line); // reverse layout, but still set the right buttons + mSpinbox->setLineShiftStep(page); + } + else + { + mSpinbox->setLineShiftStep(line); + mUpdown2->setLineShiftStep(page); + } +} + +void SpinBox2::setButtonSymbols(QSpinBox::ButtonSymbols newSymbols) +{ + if (mSpinbox->buttonSymbols() == newSymbols) + return; + mSpinbox->setButtonSymbols(newSymbols); + mUpdown2->setButtonSymbols(newSymbols); +} + +int SpinBox2::bound(int val) const +{ + return (val < mMinValue) ? mMinValue : (val > mMaxValue) ? mMaxValue : val; +} + +void SpinBox2::setMinValue(int val) +{ + mMinValue = val; + mSpinbox->setMinValue(val); + mUpdown2->setMinValue(val); +} + +void SpinBox2::setMaxValue(int val) +{ + mMaxValue = val; + mSpinbox->setMaxValue(val); + mUpdown2->setMaxValue(val); +} + +void SpinBox2::valueChange() +{ + int val = mSpinbox->value(); + bool blocked = mUpdown2->signalsBlocked(); + mUpdown2->blockSignals(true); + mUpdown2->setValue(val); + mUpdown2->blockSignals(blocked); +} + +/****************************************************************************** +* Called when the widget is about to be displayed. +* (At construction time, the spin button widths cannot be determined correctly, +* so we need to wait until now to definitively rearrange the widget.) +*/ +void SpinBox2::showEvent(QShowEvent*) +{ + arrange(); +} + +QSize SpinBox2::sizeHint() const +{ + getMetrics(); + QSize size = mSpinbox->sizeHint(); + size.setWidth(size.width() - xSpinbox + wUpdown2 + wGap); + return size; +} + +QSize SpinBox2::minimumSizeHint() const +{ + getMetrics(); + QSize size = mSpinbox->minimumSizeHint(); + size.setWidth(size.width() - xSpinbox + wUpdown2 + wGap); + return size; +} + +void SpinBox2::styleChange(QStyle&) +{ + if (mirrorStyle(style())) + mSpinMirror->show(); // show rounded corners with Plastik etc. + else + mSpinMirror->hide(); // keep normal shading with other styles + arrange(); +} + +/****************************************************************************** +* Called when the extra pair of spin buttons has repainted after a style change. +* Updates the mirror image of the spin buttons. +*/ +void SpinBox2::updateMirror() +{ + mSpinMirror->setNormalButtons(QPixmap::grabWidget(mUpdown2Frame, 0, 0)); +} + +/****************************************************************************** +* Set the positions and sizes of all the child widgets. +*/ +void SpinBox2::arrange() +{ + getMetrics(); + QRect arrowRect = QStyle::visualRect(QRect(0, 0, wUpdown2, height()), this); + mUpdown2Frame->setGeometry(arrowRect); + mUpdown2->setGeometry(-xUpdown2, 0, mUpdown2->width(), height()); + mSpinboxFrame->setGeometry(QStyle::visualRect(QRect(wUpdown2 + wGap, 0, width() - wUpdown2 - wGap, height()), this)); + mSpinbox->setGeometry(-xSpinbox, 0, mSpinboxFrame->width() + xSpinbox, height()); + mSpinMirror->resize(wUpdown2, mUpdown2->height()); + mSpinMirror->setGeometry(arrowRect); +//mSpinMirror->setGeometry(QStyle::visualRect(QRect(0, 11, wUpdown2, height()), this)); + mSpinMirror->setNormalButtons(QPixmap::grabWidget(mUpdown2Frame, 0, 0)); +} + +/****************************************************************************** +* Calculate the width and position of the extra pair of spin buttons. +* Style-specific adjustments are made for a better appearance. +*/ +void SpinBox2::getMetrics() const +{ + QRect rect = mUpdown2->style().querySubControlMetrics(QStyle::CC_SpinWidget, mUpdown2, QStyle::SC_SpinWidgetButtonField); + if (style().inherits("PlastikStyle")) + rect.setLeft(rect.left() - 1); // Plastik excludes left border from spin widget rectangle + xUpdown2 = mReverseLayout ? 0 : rect.left(); + wUpdown2 = mUpdown2->width() - rect.left(); + xSpinbox = mSpinbox->style().querySubControlMetrics(QStyle::CC_SpinWidget, mSpinbox, QStyle::SC_SpinWidgetEditField).left(); + wGap = 0; + + // Make style-specific adjustments for a better appearance + if (style().inherits("QMotifPlusStyle")) + { + xSpinbox = 0; // show the edit control left border + wGap = 2; // leave a space to the right of the left-hand pair of spin buttons + } +} + +/****************************************************************************** +* Called when the extra pair of spin buttons is clicked to step the value. +* Normally this is a page step, but with a right-to-left language where the +* button functions are reversed, this is a line step. +*/ +void SpinBox2::stepPage(int step) +{ + if (abs(step) == mUpdown2->lineStep()) + mSpinbox->setValue(mUpdown2->value()); + else + { + // It's a shift step + int oldValue = mSpinbox->value(); + if (!reverseButtons()) + { + // The button pairs have the normal function. + // Page shift stepping - step up or down to a multiple of the + // shift page increment, leaving unchanged the part of the value + // which is the remainder from the page increment. + if (oldValue >= 0) + oldValue -= oldValue % mUpdown2->lineStep(); + else + oldValue += (-oldValue) % mUpdown2->lineStep(); + } + int adjust = mSpinbox->shiftStepAdjustment(oldValue, step); + if (adjust == -step + && ((step > 0 && oldValue + step >= mSpinbox->maxValue()) + || (step < 0 && oldValue + step <= mSpinbox->minValue()))) + adjust = 0; // allow stepping to the minimum or maximum value + mSpinbox->addValue(adjust + step); + } + bool focus = mSpinbox->selectOnStep() && mUpdown2->hasFocus(); + if (focus) + mSpinbox->selectAll(); + + // Make the covering arrows image show the pressed arrow + mSpinMirror->redraw(QPixmap::grabWidget(mUpdown2Frame, 0, 0)); +} + + +/*============================================================================= += Class SpinBox2::MainSpinBox +=============================================================================*/ + +/****************************************************************************** +* Return the initial adjustment to the value for a shift step up or down, for +* the main (visible) spin box. +* Normally this is a line step, but with a right-to-left language where the +* button functions are reversed, this is a page step. +*/ +int SpinBox2::MainSpinBox::shiftStepAdjustment(int oldValue, int shiftStep) +{ + if (owner->reverseButtons()) + { + // The button pairs have the opposite function from normal. + // Page shift stepping - step up or down to a multiple of the + // shift page increment, leaving unchanged the part of the value + // which is the remainder from the page increment. + if (oldValue >= 0) + oldValue -= oldValue % lineStep(); + else + oldValue += (-oldValue) % lineStep(); + } + return SpinBox::shiftStepAdjustment(oldValue, shiftStep); +} + + +/*============================================================================= += Class ExtraSpinBox +=============================================================================*/ + +/****************************************************************************** +* Repaint the widget. +* If it's the first time since a style change, tell the parent SpinBox2 to +* update the SpinMirror with the new unpressed button image. We make the +* presumably reasonable assumption that when a style change occurs, the spin +* buttons are unpressed. +*/ +void ExtraSpinBox::paintEvent(QPaintEvent* e) +{ + SpinBox::paintEvent(e); + if (mNewStylePending) + { + mNewStylePending = false; + emit styleUpdated(); + } +} + + +/*============================================================================= += Class SpinMirror +=============================================================================*/ + +SpinMirror::SpinMirror(SpinBox* spinbox, QFrame* spinFrame, QWidget* parent, const char* name) + : QCanvasView(new QCanvas, parent, name), + mSpinbox(spinbox), + mSpinFrame(spinFrame), + mReadOnly(false) +{ + setVScrollBarMode(QScrollView::AlwaysOff); + setHScrollBarMode(QScrollView::AlwaysOff); + setFrameStyle(QFrame::NoFrame); + + // Find the spin widget which is part of the spin box, in order to + // pass on its shift-button presses. + QObjectList* spinwidgets = spinbox->queryList("QSpinWidget", 0, false, true); + mSpinWidget = (SpinBox*)spinwidgets->getFirst(); + delete spinwidgets; +} + +void SpinMirror::setNormalButtons(const QPixmap& px) +{ + mNormalButtons = px; + redraw(mNormalButtons); +} + +void SpinMirror::redraw() +{ + redraw(QPixmap::grabWidget(mSpinFrame, 0, 0)); +} + +void SpinMirror::redraw(const QPixmap& px) +{ + QCanvas* c = canvas(); + c->setBackgroundPixmap(px); + c->setAllChanged(); + c->update(); +} + +void SpinMirror::resize(int w, int h) +{ + canvas()->resize(w, h); + QCanvasView::resize(w, h); + resizeContents(w, h); + setWorldMatrix(QWMatrix(-1, 0, 0, 1, w - 1, 0)); // mirror left to right +} + +/****************************************************************************** +* Pass on all mouse events to the spinbox which we're covering up. +*/ +void SpinMirror::contentsMouseEvent(QMouseEvent* e) +{ + if (mReadOnly) + return; + QPoint pt = contentsToViewport(e->pos()); + pt.setX(pt.x() + mSpinbox->upRect().left()); + QApplication::postEvent(mSpinWidget, new QMouseEvent(e->type(), pt, e->button(), e->state())); + + // If the mouse button has been pressed or released, refresh the spin buttons + switch (e->type()) + { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + QTimer::singleShot(0, this, SLOT(redraw())); + break; + default: + break; + } +} + +/****************************************************************************** +* Pass on all mouse events to the spinbox which we're covering up. +*/ +void SpinMirror::contentsWheelEvent(QWheelEvent* e) +{ + if (mReadOnly) + return; + QPoint pt = contentsToViewport(e->pos()); + pt.setX(pt.x() + mSpinbox->upRect().left()); + QApplication::postEvent(mSpinWidget, new QWheelEvent(pt, e->delta(), e->state(), e->orientation())); +} + +/****************************************************************************** +* Pass on to the main spinbox events which are needed to activate mouseover and +* other graphic effects when the mouse cursor enters and leaves the widget. +*/ +bool SpinMirror::event(QEvent* e) +{ + switch (e->type()) + { + case QEvent::Leave: + case QEvent::Enter: + QApplication::postEvent(mSpinWidget, new QEvent(e->type())); + QTimer::singleShot(0, this, SLOT(redraw())); + break; + case QEvent::FocusIn: + mSpinbox->setFocus(); + QTimer::singleShot(0, this, SLOT(redraw())); + break; + default: + break; + } + return QCanvasView::event(e); +} + + +/*============================================================================= += Local functions +=============================================================================*/ + +/****************************************************************************** +* Determine whether the extra pair of spin buttons needs to be mirrored +* left-to-right in the specified style. +*/ +static bool mirrorStyle(const QStyle& style) +{ + for (const char** s = mirrorStyles; *s; ++s) + if (style.inherits(*s)) + return true; + return false; +} diff --git a/kalarm/lib/spinbox2.h b/kalarm/lib/spinbox2.h new file mode 100644 index 000000000..772eaee70 --- /dev/null +++ b/kalarm/lib/spinbox2.h @@ -0,0 +1,317 @@ +/* + * spinbox2.h - spin box with extra pair of spin buttons (for Qt 3) + * Program: kalarm + * Copyright © 2001-2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef SPINBOX2_H +#define SPINBOX2_H + +#include <qglobal.h> +#include <qlineedit.h> + +class SpinMirror; +class ExtraSpinBox; +#include "spinbox.h" + + +/** + * @short Spin box with a pair of spin buttons on either side. + * + * The SpinBox2 class provides a spin box with two pairs of spin buttons, one on either side. + * + * It is designed as a base class for implementing such facilities as time spin boxes, where + * the hours and minutes values are separately displayed in the edit field. When the + * appropriate step increments are configured, the left spin arrows can then be used to + * change the hours value, while the right spin arrows can be used to change the minutes + * value. + * + * Rather than using SpinBox2 directly for time entry, use in preference TimeSpinBox or + * TimeEdit classes which are tailored from SpinBox2 for this purpose. + * + * Separate step increments may optionally be specified for use when the shift key is + * held down. Typically these would be larger than the normal steps. Then, when the user + * clicks the spin buttons, he/she can increment or decrement the value faster by holding + * the shift key down. + * + * The widget may be set as read-only. This has the same effect as disabling it, except + * that its appearance is unchanged. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class SpinBox2 : public QFrame +{ + Q_OBJECT + public: + /** Constructor. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit SpinBox2(QWidget* parent = 0, const char* name = 0); + /** Constructor. + * @param minValue The minimum value which the spin box can have. + * @param maxValue The maximum value which the spin box can have. + * @param step The (unshifted) step interval for the right-hand spin buttons. + * @param step2 The (unshifted) step interval for the left-hand spin buttons. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + SpinBox2(int minValue, int maxValue, int step = 1, int step2 = 1, + QWidget* parent = 0, const char* name = 0); + /** Sets whether the spin box can be changed by the user. + * @param readOnly True to set the widget read-only, false to set it read-write. + */ + virtual void setReadOnly(bool readOnly); + /** Returns true if the widget is read only. */ + bool isReadOnly() const { return mSpinbox->isReadOnly(); } + /** Sets whether the spin box value text should be selected when its value is stepped. */ + void setSelectOnStep(bool sel) { mSpinbox->setSelectOnStep(sel); } + /** Sets whether the spin button pairs should be reversed for a right-to-left language. + * The default is for them to be reversed. + */ + void setReverseWithLayout(bool reverse); + /** Returns whether the spin button pairs will be reversed for a right-to-left language. */ + bool reverseButtons() const { return mReverseLayout && !mReverseWithLayout; } + + /** Returns the spin box's text, including any prefix() and suffix(). */ + QString text() const { return mSpinbox->text(); } + /** Returns the prefix for the spin box's text. */ + virtual QString prefix() const { return mSpinbox->prefix(); } + /** Returns the suffix for the spin box's text. */ + virtual QString suffix() const { return mSpinbox->suffix(); } + /** Returns the spin box's text with no prefix(), suffix() or leading or trailing whitespace. */ + virtual QString cleanText() const { return mSpinbox->cleanText(); } + + /** Sets the special-value text which, if non-null, is displayed instead of a numeric + * value when the current value is equal to minValue(). + */ + virtual void setSpecialValueText(const QString& text) { mSpinbox->setSpecialValueText(text); } + /** Returns the special-value text which, if non-null, is displayed instead of a numeric + * value when the current value is equal to minValue(). + */ + QString specialValueText() const { return mSpinbox->specialValueText(); } + + /** Sets whether it is possible to step the value from the highest value to the + * lowest value and vice versa. + */ + virtual void setWrapping(bool on); + /** Returns whether it is possible to step the value from the highest value to the + * lowest value and vice versa. + */ + bool wrapping() const { return mSpinbox->wrapping(); } + + /** Set the text alignment of the widget */ + void setAlignment(int a) { mSpinbox->setAlignment(a); } + /** Sets the button symbols to use (arrows or plus/minus). */ + virtual void setButtonSymbols(QSpinBox::ButtonSymbols); + /** Returns the button symbols currently in use (arrows or plus/minus). */ + QSpinBox::ButtonSymbols buttonSymbols() const { return mSpinbox->buttonSymbols(); } + + /** Sets the validator to @p v. The validator controls what keyboard input is accepted + * when the user is editing the value field. + */ + virtual void setValidator(const QValidator* v) { mSpinbox->setValidator(v); } + /** Returns the current validator. The validator controls what keyboard input is accepted + * when the user is editing the value field. + */ + const QValidator* validator() const { return mSpinbox->validator(); } + + virtual QSize sizeHint() const; + virtual QSize minimumSizeHint() const; + + /** Returns the minimum value of the spin box. */ + int minValue() const { return mMinValue; } + /** Returns the maximum value of the spin box. */ + int maxValue() const { return mMaxValue; } + /** Sets the minimum value of the spin box. */ + virtual void setMinValue(int val); + /** Sets the maximum value of the spin box. */ + virtual void setMaxValue(int val); + /** Sets the minimum and maximum values of the spin box. */ + void setRange(int minValue, int maxValue) { setMinValue(minValue); setMaxValue(maxValue); } + /** Returns the current value of the spin box. */ + int value() const { return mSpinbox->value(); } + /** Returns the specified value clamped to the range of the spin box. */ + int bound(int val) const; + + /** Returns the geometry of the right-hand "up" button. */ + QRect upRect() const { return mSpinbox->upRect(); } + /** Returns the geometry of the right-hand "down" button. */ + QRect downRect() const { return mSpinbox->downRect(); } + /** Returns the geometry of the left-hand "up" button. */ + QRect up2Rect() const; + /** Returns the geometry of the left-hand "down" button. */ + QRect down2Rect() const; + + /** Returns the unshifted step increment for the right-hand spin buttons, + * i.e. the amount by which the spin box value changes when a right-hand + * spin button is clicked without the shift key being pressed. + */ + int lineStep() const { return mLineStep; } + /** Returns the shifted step increment for the right-hand spin buttons, + * i.e. the amount by which the spin box value changes when a right-hand + * spin button is clicked while the shift key is pressed. + */ + int lineShiftStep() const { return mLineShiftStep; } + /** Returns the unshifted step increment for the left-hand spin buttons, + * i.e. the amount by which the spin box value changes when a left-hand + * spin button is clicked without the shift key being pressed. + */ + int pageStep() const { return mPageStep; } + /** Returns the shifted step increment for the left-hand spin buttons, + * i.e. the amount by which the spin box value changes when a left-hand + * spin button is clicked while the shift key is pressed. + */ + int pageShiftStep() const { return mPageShiftStep; } + /** Sets the unshifted step increment for the right-hand spin buttons, + * i.e. the amount by which the spin box value changes when a right-hand + * spin button is clicked without the shift key being pressed. + */ + void setLineStep(int step); + /** Sets the unshifted step increments for the two pairs of spin buttons, + * i.e. the amount by which the spin box value changes when a spin button + * is clicked without the shift key being pressed. + * @param line The step increment for the right-hand spin buttons. + * @param page The step increment for the left-hand spin buttons. + */ + void setSteps(int line, int page); + /** Sets the shifted step increments for the two pairs of spin buttons, + * i.e. the amount by which the spin box value changes when a spin button + * is clicked while the shift key is pressed. + * @param line The shift step increment for the right-hand spin buttons. + * @param page The shift step increment for the left-hand spin buttons. + */ + void setShiftSteps(int line, int page); + + /** Increments the current value by adding the unshifted step increment for + * the left-hand spin buttons. + */ + void addPage() { addValue(mPageStep); } + /** Decrements the current value by subtracting the unshifted step increment for + * the left-hand spin buttons. + */ + void subtractPage() { addValue(-mPageStep); } + /** Increments the current value by adding the unshifted step increment for + * the right-hand spin buttons. + */ + void addLine() { addValue(mLineStep); } + /** Decrements the current value by subtracting the unshifted step increment for + * the right-hand spin buttons. + */ + void subtractLine() { addValue(-mLineStep); } + /** Adjusts the current value by adding @p change. */ + void addValue(int change) { mSpinbox->addValue(change); } + + public slots: + /** Sets the current value to @p val. */ + virtual void setValue(int val) { mSpinbox->setValue(val); } + /** Sets the prefix which is prepended to the start of the displayed text. */ + virtual void setPrefix(const QString& text) { mSpinbox->setPrefix(text); } + /** Sets the suffix which is prepended to the start of the displayed text. */ + virtual void setSuffix(const QString& text) { mSpinbox->setSuffix(text); } + /** Increments the current value by adding the unshifted step increment for + * the right-hand spin buttons. + */ + virtual void stepUp() { addValue(mLineStep); } + /** Decrements the current value by subtracting the unshifted step increment for + * the right-hand spin buttons. + */ + virtual void stepDown() { addValue(-mLineStep); } + /** Increments the current value by adding the unshifted step increment for + * the left-hand spin buttons. + */ + virtual void pageUp() { addValue(mPageStep); } + /** Decrements the current value by subtracting the unshifted step increment for + * the left-hand spin buttons. + */ + virtual void pageDown() { addValue(-mPageStep); } + /** Selects all the text in the spin box's editor. */ + virtual void selectAll() { mSpinbox->selectAll(); } + /** Sets whether the widget is enabled. */ + virtual void setEnabled(bool enabled); + + signals: + /** Signal which is emitted whenever the value of the spin box changes. */ + void valueChanged(int value); + /** Signal which is emitted whenever the value of the spin box changes. */ + void valueChanged(const QString& valueText); + + protected: + virtual QString mapValueToText(int v) { return mSpinbox->mapValToText(v); } + virtual int mapTextToValue(bool* ok) { return mSpinbox->mapTextToVal(ok); } + virtual void resizeEvent(QResizeEvent*) { arrange(); } + virtual void showEvent(QShowEvent*); + virtual void styleChange(QStyle&); + virtual void getMetrics() const; + + mutable int wUpdown2; // width of second spin widget + mutable int xUpdown2; // x offset of visible area in 'mUpdown2' + mutable int xSpinbox; // x offset of visible area in 'mSpinbox' + mutable int wGap; // gap between mUpdown2Frame and mSpinboxFrame + + protected slots: + virtual void valueChange(); + virtual void stepPage(int); + + private slots: + void updateMirror(); + + private: + void init(); + void arrange(); + int whichButton(QObject* spinWidget, const QPoint&); + void setShiftStepping(bool on); + + // Visible spin box class. + // Declared here to allow use of mSpinBox in inline methods. + class MainSpinBox : public SpinBox + { + public: + MainSpinBox(SpinBox2* sb2, QWidget* parent, const char* name = 0) + : SpinBox(parent, name), owner(sb2) { } + MainSpinBox(int minValue, int maxValue, int step, SpinBox2* sb2, QWidget* parent, const char* name = 0) + : SpinBox(minValue, maxValue, step, parent, name), owner(sb2) { } + void setAlignment(int a) { editor()->setAlignment(a); } + virtual QString mapValueToText(int v) { return owner->mapValueToText(v); } + virtual int mapTextToValue(bool* ok) { return owner->mapTextToValue(ok); } + QString mapValToText(int v) { return SpinBox::mapValueToText(v); } + int mapTextToVal(bool* ok) { return SpinBox::mapTextToValue(ok); } + virtual int shiftStepAdjustment(int oldValue, int shiftStep); + private: + SpinBox2* owner; // owner SpinBox2 + }; + + enum { NO_BUTTON = -1, UP, DOWN, UP2, DOWN2 }; + + static int mReverseLayout; // widgets are mirrored right to left + QFrame* mUpdown2Frame; // contains visible part of the extra pair of spin buttons + QFrame* mSpinboxFrame; // contains the main spin box + ExtraSpinBox* mUpdown2; // the extra pair of spin buttons + MainSpinBox* mSpinbox; // the visible spin box + SpinMirror* mSpinMirror; // image of the extra pair of spin buttons + int mMinValue; + int mMaxValue; + int mLineStep; // right button increment + int mLineShiftStep; // right button increment with shift pressed + int mPageStep; // left button increment + int mPageShiftStep; // left button increment with shift pressed + bool mReverseWithLayout; // reverse button positions if reverse layout (default = true) + + friend class MainSpinBox; +}; + +#endif // SPINBOX2_H diff --git a/kalarm/lib/spinbox2private.h b/kalarm/lib/spinbox2private.h new file mode 100644 index 000000000..74fbeb66c --- /dev/null +++ b/kalarm/lib/spinbox2private.h @@ -0,0 +1,92 @@ +/* + * spinbox2private.h - private classes for SpinBox2 (for Qt 3) + * Program: kalarm + * Copyright © 2005,2006,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef SPINBOX2PRIVATE_H +#define SPINBOX2PRIVATE_H + +#include <qcanvas.h> +#include "spinbox.h" + + +/*============================================================================= += Class ExtraSpinBox +* Extra pair of spin buttons for SpinBox2. +* The widget is actually a whole spin box, but only the buttons are displayed. +=============================================================================*/ + +class ExtraSpinBox : public SpinBox +{ + Q_OBJECT + public: + explicit ExtraSpinBox(QWidget* parent, const char* name = 0) + : SpinBox(parent, name), mNewStylePending(false) { } + ExtraSpinBox(int minValue, int maxValue, int step, QWidget* parent, const char* name = 0) + : SpinBox(minValue, maxValue, step, parent, name), mNewStylePending(false) { } + signals: + void styleUpdated(); + protected: + virtual void paintEvent(QPaintEvent*); + virtual void styleChange(QStyle&) { mNewStylePending = true; } + private: + bool mNewStylePending; // style has changed, but not yet repainted +}; + + +/*============================================================================= += Class SpinMirror +* Displays the left-to-right mirror image of a pair of spin buttons, for use +* as the extra spin buttons in a SpinBox2. All mouse clicks are passed on to +* the real extra pair of spin buttons for processing. +* Mirroring in this way allows styles with rounded corners to display correctly. +=============================================================================*/ + +class SpinMirror : public QCanvasView +{ + Q_OBJECT + public: + explicit SpinMirror(SpinBox*, QFrame* spinFrame, QWidget* parent = 0, const char* name = 0); + void setReadOnly(bool ro) { mReadOnly = ro; } + bool isReadOnly() const { return mReadOnly; } + void setNormalButtons(const QPixmap&); + void redraw(const QPixmap&); + + public slots: + virtual void resize(int w, int h); + void redraw(); + + protected: + virtual void contentsMousePressEvent(QMouseEvent* e) { contentsMouseEvent(e); } + virtual void contentsMouseReleaseEvent(QMouseEvent* e) { contentsMouseEvent(e); } + virtual void contentsMouseMoveEvent(QMouseEvent* e) { contentsMouseEvent(e); } + virtual void contentsMouseDoubleClickEvent(QMouseEvent* e) { contentsMouseEvent(e); } + virtual void contentsWheelEvent(QWheelEvent*); + virtual bool event(QEvent*); + + private: + void contentsMouseEvent(QMouseEvent*); + + SpinBox* mSpinbox; // spinbox whose spin buttons are being mirrored + QFrame* mSpinFrame; // widget containing mSpinbox's spin buttons + QWidget* mSpinWidget; // spin buttons in mSpinbox + QPixmap mNormalButtons; // image of spin buttons in unpressed state + bool mReadOnly; // value cannot be changed +}; + +#endif // SPINBOX2PRIVATE_H diff --git a/kalarm/lib/synchtimer.cpp b/kalarm/lib/synchtimer.cpp new file mode 100644 index 000000000..1b681d6eb --- /dev/null +++ b/kalarm/lib/synchtimer.cpp @@ -0,0 +1,233 @@ +/* + * synchtimer.cpp - timers which synchronise to time boundaries + * Program: kalarm + * Copyright (C) 2004, 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" +#include <qtimer.h> +#include <kdebug.h> +#include "synchtimer.moc" + + +/*============================================================================= += Class: SynchTimer += Virtual base class for application-wide timer synchronised to a time boundary. +=============================================================================*/ + +SynchTimer::SynchTimer() +{ + mTimer = new QTimer(this, "mTimer"); +} + +SynchTimer::~SynchTimer() +{ + delete mTimer; + mTimer = 0; +} + +/****************************************************************************** +* Connect to the timer. The timer is started if necessary. +*/ +void SynchTimer::connecT(QObject* receiver, const char* member) +{ + Connection connection(receiver, member); + if (mConnections.find(connection) != mConnections.end()) + return; // the slot is already connected, so ignore request + connect(mTimer, SIGNAL(timeout()), receiver, member); + mConnections.append(connection); + if (!mTimer->isActive()) + { + connect(mTimer, SIGNAL(timeout()), this, SLOT(slotTimer())); + start(); + } +} + +/****************************************************************************** +* Disconnect from the timer. The timer is stopped if no longer needed. +*/ +void SynchTimer::disconnecT(QObject* receiver, const char* member) +{ + if (mTimer) + { + mTimer->disconnect(receiver, member); + if (member) + mConnections.remove(Connection(receiver, member)); + else + { + for (QValueList<Connection>::Iterator it = mConnections.begin(); it != mConnections.end(); ) + { + if ((*it).receiver == receiver) + it = mConnections.remove(it); + else + ++it; + } + } + if (mConnections.isEmpty()) + { + mTimer->disconnect(); + mTimer->stop(); + } + } +} + + +/*============================================================================= += Class: MinuteTimer += Application-wide timer synchronised to the minute boundary. +=============================================================================*/ + +MinuteTimer* MinuteTimer::mInstance = 0; + +MinuteTimer* MinuteTimer::instance() +{ + if (!mInstance) + mInstance = new MinuteTimer; + return mInstance; +} + +/****************************************************************************** +* Called when the timer triggers, or to start the timer. +* Timers can under some circumstances wander off from the correct trigger time, +* so rather than setting a 1 minute interval, calculate the correct next +* interval each time it triggers. +*/ +void MinuteTimer::slotTimer() +{ + kdDebug(5950) << "MinuteTimer::slotTimer()" << endl; + int interval = 62 - QTime::currentTime().second(); + mTimer->start(interval * 1000, true); // execute a single shot +} + + +/*============================================================================= += Class: DailyTimer += Application-wide timer synchronised to midnight. +=============================================================================*/ + +QValueList<DailyTimer*> DailyTimer::mFixedTimers; + +DailyTimer::DailyTimer(const QTime& timeOfDay, bool fixed) + : mTime(timeOfDay), + mFixed(fixed) +{ + if (fixed) + mFixedTimers.append(this); +} + +DailyTimer::~DailyTimer() +{ + if (mFixed) + mFixedTimers.remove(this); +} + +DailyTimer* DailyTimer::fixedInstance(const QTime& timeOfDay, bool create) +{ + for (QValueList<DailyTimer*>::Iterator it = mFixedTimers.begin(); it != mFixedTimers.end(); ++it) + if ((*it)->mTime == timeOfDay) + return *it; + return create ? new DailyTimer(timeOfDay, true) : 0; +} + +/****************************************************************************** +* Disconnect from the timer signal which triggers at the given fixed time of day. +* If there are no remaining connections to that timer, it is destroyed. +*/ +void DailyTimer::disconnect(const QTime& timeOfDay, QObject* receiver, const char* member) +{ + DailyTimer* timer = fixedInstance(timeOfDay, false); + if (!timer) + return; + timer->disconnecT(receiver, member); + if (!timer->hasConnections()) + delete timer; +} + +/****************************************************************************** +* Change the time at which the variable timer triggers. +*/ +void DailyTimer::changeTime(const QTime& newTimeOfDay, bool triggerMissed) +{ + if (mFixed) + return; + if (mTimer->isActive()) + { + mTimer->stop(); + bool triggerNow = false; + if (triggerMissed) + { + QTime now = QTime::currentTime(); + if (now >= newTimeOfDay && now < mTime) + { + // The trigger time is now earlier and it has already arrived today. + // Trigger a timer event immediately. + triggerNow = true; + } + } + mTime = newTimeOfDay; + if (triggerNow) + mTimer->start(0, true); // trigger immediately + else + start(); + } + else + mTime = newTimeOfDay; +} + +/****************************************************************************** +* Initialise the timer to trigger at the specified time. +* This will either be today or tomorrow, depending on whether the trigger time +* has already passed. +*/ +void DailyTimer::start() +{ + // TIMEZONE = local time + QDateTime now = QDateTime::currentDateTime(); + // Find out whether to trigger today or tomorrow. + // In preference, use the last trigger date to determine this, since + // that will avoid possible errors due to daylight savings time changes. + bool today; + if (mLastDate.isValid()) + today = (mLastDate < now.date()); + else + today = (now.time() < mTime); + QDateTime next; + if (today) + next = QDateTime(now.date(), mTime); + else + next = QDateTime(now.date().addDays(1), mTime); + uint interval = next.toTime_t() - now.toTime_t(); + mTimer->start(interval * 1000, true); // execute a single shot + kdDebug(5950) << "DailyTimer::start(at " << mTime.hour() << ":" << mTime.minute() << "): interval = " << interval/3600 << ":" << (interval/60)%60 << ":" << interval%60 << endl; +} + +/****************************************************************************** +* Called when the timer triggers. +* Set the timer to trigger again tomorrow at the specified time. +* Note that if daylight savings time changes occur, this will not be 24 hours +* from now. +*/ +void DailyTimer::slotTimer() +{ + // TIMEZONE = local time + QDateTime now = QDateTime::currentDateTime(); + mLastDate = now.date(); + QDateTime next = QDateTime(mLastDate.addDays(1), mTime); + uint interval = next.toTime_t() - now.toTime_t(); + mTimer->start(interval * 1000, true); // execute a single shot + kdDebug(5950) << "DailyTimer::slotTimer(at " << mTime.hour() << ":" << mTime.minute() << "): interval = " << interval/3600 << ":" << (interval/60)%60 << ":" << interval%60 << endl; +} diff --git a/kalarm/lib/synchtimer.h b/kalarm/lib/synchtimer.h new file mode 100644 index 000000000..551fd1687 --- /dev/null +++ b/kalarm/lib/synchtimer.h @@ -0,0 +1,198 @@ +/* + * synchtimer.h - timers which synchronise to time boundaries + * Program: kalarm + * Copyright (C) 2004, 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef SYNCHTIMER_H +#define SYNCHTIMER_H + +/* @file synchtimer.h - timers which synchronise to time boundaries */ + +#include <qobject.h> +#include <qvaluelist.h> +#include <qcstring.h> +#include <qdatetime.h> +class QTimer; + +/** SynchTimer is a virtual base class for application-wide timers synchronised + * to a time boundary. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class SynchTimer : public QObject +{ + Q_OBJECT + public: + virtual ~SynchTimer(); + + struct Connection + { + Connection() { } + Connection(QObject* r, const char* s) : receiver(r), slot(s) { } + bool operator==(const Connection& c) const { return receiver == c.receiver && slot == c.slot; } + QObject* receiver; + const QCString slot; + }; + protected: + SynchTimer(); + virtual void start() = 0; + void connecT(QObject* receiver, const char* member); + void disconnecT(QObject* receiver, const char* member = 0); + bool hasConnections() const { return !mConnections.isEmpty(); } + + QTimer* mTimer; + + protected slots: + virtual void slotTimer() = 0; + + private slots: + void slotReceiverGone(QObject* r) { disconnecT(r); } + + private: + SynchTimer(const SynchTimer&); // prohibit copying + QValueList<Connection> mConnections; // list of current clients +}; + + +/** MinuteTimer is an application-wide timer synchronised to the minute boundary. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class MinuteTimer : public SynchTimer +{ + Q_OBJECT + public: + virtual ~MinuteTimer() { mInstance = 0; } + /** Connect to the timer signal. + * @param receiver Receiving object. + * @param member Slot to activate. + */ + static void connect(QObject* receiver, const char* member) + { instance()->connecT(receiver, member); } + /** Disconnect from the timer signal. + * @param receiver Receiving object. + * @param member Slot to disconnect. If null, all slots belonging to + * @p receiver will be disconnected. + */ + static void disconnect(QObject* receiver, const char* member = 0) + { if (mInstance) mInstance->disconnecT(receiver, member); } + + protected: + MinuteTimer() : SynchTimer() { } + static MinuteTimer* instance(); + virtual void start() { slotTimer(); } + + protected slots: + virtual void slotTimer(); + + private: + static MinuteTimer* mInstance; // the one and only instance +}; + + +/** DailyTimer is an application-wide timer synchronised to a specified time of day, local time. + * + * Daily timers come in two flavours: fixed, which can only be accessed through static methods, + * and variable, whose time can be adjusted and which are accessed through non-static methods. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class DailyTimer : public SynchTimer +{ + Q_OBJECT + public: + virtual ~DailyTimer(); + /** Connect to the timer signal which triggers at the given fixed time of day. + * A new timer is created if necessary. + * @param timeOfDay Time at which the timer is to trigger. + * @param receiver Receiving object. + * @param member Slot to activate. + */ + static void connect(const QTime& timeOfDay, QObject* receiver, const char* member) + { fixedInstance(timeOfDay)->connecT(receiver, member); } + /** Disconnect from the timer signal which triggers at the given fixed time of day. + * If there are no remaining connections to that timer, it is destroyed. + * @param timeOfDay Time at which the timer triggers. + * @param receiver Receiving object. + * @param member Slot to disconnect. If null, all slots belonging to + * @p receiver will be disconnected. + */ + static void disconnect(const QTime& timeOfDay, QObject* receiver, const char* member = 0); + /** Change the time at which this variable timer triggers. + * @param newTimeOfDay New time at which the timer should trigger. + * @param triggerMissed If true, and if @p newTimeOfDay < @p oldTimeOfDay, and if the current + * time is between @p newTimeOfDay and @p oldTimeOfDay, the timer will be + * triggered immediately so as to avoid missing today's trigger. + */ + void changeTime(const QTime& newTimeOfDay, bool triggerMissed = true); + /** Return the current time of day at which this variable timer triggers. */ + QTime timeOfDay() const { return mTime; } + + protected: + /** Construct an instance. + * The constructor is protected to ensure that for variable timers, only derived classes + * can construct instances. This ensures that multiple timers are not created for the same + * use. + */ + DailyTimer(const QTime&, bool fixed); + /** Return the instance which triggers at the specified fixed time of day, + * optionally creating a new instance if necessary. + * @param timeOfDay Time at which the timer triggers. + * @param create If true, create a new instance if none already exists + * for @p timeOfDay. + * @return The instance for @p timeOfDay, or 0 if it does not exist. + */ + static DailyTimer* fixedInstance(const QTime& timeOfDay, bool create = true); + virtual void start(); + + protected slots: + virtual void slotTimer(); + + private: + static QValueList<DailyTimer*> mFixedTimers; // list of timers whose trigger time is fixed + QTime mTime; + QDate mLastDate; // the date on which the timer was last triggered + bool mFixed; // the time at which the timer triggers cannot be changed +}; + + +/** MidnightTimer is an application-wide timer synchronised to midnight, local time. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class MidnightTimer +{ + public: + /** Connect to the timer signal. + * @param receiver Receiving object. + * @param member Slot to activate. + */ + static void connect(QObject* receiver, const char* member) + { DailyTimer::connect(QTime(0,0), receiver, member); } + /** Disconnect from the timer signal. + * @param receiver Receiving object. + * @param member Slot to disconnect. If null, all slots belonging to + * @p receiver will be disconnected. + */ + static void disconnect(QObject* receiver, const char* member = 0) + { DailyTimer::disconnect(QTime(0,0), receiver, member); } + +}; + +#endif // SYNCHTIMER_H + diff --git a/kalarm/lib/timeedit.cpp b/kalarm/lib/timeedit.cpp new file mode 100644 index 000000000..8aabb90b6 --- /dev/null +++ b/kalarm/lib/timeedit.cpp @@ -0,0 +1,207 @@ +/* + * timeedit.cpp - time-of-day edit widget, with AM/PM shown depending on locale + * Program: kalarm + * Copyright (C) 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <kglobal.h> +#include <klocale.h> + +#include "combobox.h" +#include "timespinbox.h" +#include "timeedit.moc" + + +TimeEdit::TimeEdit(QWidget* parent, const char* name) + : QHBox(parent, name), + mAmPm(0), + mAmIndex(-1), + mPmIndex(-1), + mReadOnly(false) +{ + bool use12hour = KGlobal::locale()->use12Clock(); + mSpinBox = new TimeSpinBox(!use12hour, this); + mSpinBox->setFixedSize(mSpinBox->sizeHint()); + connect(mSpinBox, SIGNAL(valueChanged(int)), SLOT(slotValueChanged(int))); + if (use12hour) + { + mAmPm = new ComboBox(this); + setAmPmCombo(1, 1); // add "am" and "pm" options to the combo box + mAmPm->setFixedSize(mAmPm->sizeHint()); + connect(mAmPm, SIGNAL(highlighted(int)), SLOT(slotAmPmChanged(int))); + } +} + +void TimeEdit::setReadOnly(bool ro) +{ + if (ro != mReadOnly) + { + mReadOnly = ro; + mSpinBox->setReadOnly(ro); + if (mAmPm) + mAmPm->setReadOnly(ro); + } +} + +int TimeEdit::value() const +{ + return mSpinBox->value(); +} + +bool TimeEdit::isValid() const +{ + return mSpinBox->isValid(); +} + +/****************************************************************************** + * Set the edit value as valid or invalid. + * If newly invalid, the value is displayed as asterisks. + * If newly valid, the value is set to the minimum value. + */ +void TimeEdit::setValid(bool valid) +{ + bool oldValid = mSpinBox->isValid(); + if (valid && !oldValid + || !valid && oldValid) + { + mSpinBox->setValid(valid); + if (mAmPm) + mAmPm->setCurrentItem(0); + } +} + +/****************************************************************************** + * Set the widget's value. + */ +void TimeEdit::setValue(int minutes) +{ + if (mAmPm) + { + int i = (minutes >= 720) ? mPmIndex : mAmIndex; + mAmPm->setCurrentItem(i >= 0 ? i : 0); + } + mSpinBox->setValue(minutes); +} + +bool TimeEdit::wrapping() const +{ + return mSpinBox->wrapping(); +} + +void TimeEdit::setWrapping(bool on) +{ + mSpinBox->setWrapping(on); +} + +int TimeEdit::minValue() const +{ + return mSpinBox->minValue(); +} + +int TimeEdit::maxValue() const +{ + return mSpinBox->maxValue(); +} + +void TimeEdit::setMinValue(int minutes) +{ + if (mAmPm) + setAmPmCombo((minutes < 720 ? 1 : 0), -1); // insert/remove "am" in combo box + mSpinBox->setMinValue(minutes); +} + +void TimeEdit::setMaxValue(int minutes) +{ + if (mAmPm) + setAmPmCombo(-1, (minutes < 720 ? 0 : 1)); // insert/remove "pm" in combo box + mSpinBox->setMaxValue(minutes); +} + +/****************************************************************************** + * Called when the spin box value has changed. + */ +void TimeEdit::slotValueChanged(int value) +{ + if (mAmPm) + { + bool pm = (mAmPm->currentItem() == mPmIndex); + if (pm && value < 720) + mAmPm->setCurrentItem(mAmIndex); + else if (!pm && value >= 720) + mAmPm->setCurrentItem(mPmIndex); + } + emit valueChanged(value); +} + +/****************************************************************************** + * Called when a new selection has been made by the user in the AM/PM combo box. + * Adjust the current time value by 12 hours. + */ +void TimeEdit::slotAmPmChanged(int item) +{ + if (mAmPm) + { + int value = mSpinBox->value(); + if (item == mPmIndex && value < 720) + mSpinBox->setValue(value + 720); + else if (item != mPmIndex && value >= 720) + mSpinBox->setValue(value - 720); + } +} + +/****************************************************************************** + * Set up the AM/PM combo box to contain the specified items. + */ +void TimeEdit::setAmPmCombo(int am, int pm) +{ + if (am > 0 && mAmIndex < 0) + { + // Insert "am" + mAmIndex = 0; + mAmPm->insertItem(KGlobal::locale()->translate("am"), mAmIndex); + if (mPmIndex >= 0) + mPmIndex = 1; + mAmPm->setCurrentItem(mPmIndex >= 0 ? mPmIndex : mAmIndex); + } + else if (am == 0 && mAmIndex >= 0) + { + // Remove "am" + mAmPm->removeItem(mAmIndex); + mAmIndex = -1; + if (mPmIndex >= 0) + mPmIndex = 0; + mAmPm->setCurrentItem(mPmIndex); + } + + if (pm > 0 && mPmIndex < 0) + { + // Insert "pm" + mPmIndex = mAmIndex + 1; + mAmPm->insertItem(KGlobal::locale()->translate("pm"), mPmIndex); + if (mAmIndex < 0) + mAmPm->setCurrentItem(mPmIndex); + } + else if (pm == 0 && mPmIndex >= 0) + { + // Remove "pm" + mAmPm->removeItem(mPmIndex); + mPmIndex = -1; + mAmPm->setCurrentItem(mAmIndex); + } +} diff --git a/kalarm/lib/timeedit.h b/kalarm/lib/timeedit.h new file mode 100644 index 000000000..5888c7665 --- /dev/null +++ b/kalarm/lib/timeedit.h @@ -0,0 +1,122 @@ +/* + * timeedit.h - time-of-day edit widget, with AM/PM shown depending on locale + * Program: kalarm + * Copyright (C) 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TIMEEDIT_H +#define TIMEEDIT_H + +#include <qdatetime.h> +#include <qhbox.h> + +class ComboBox; +class TimeSpinBox; + + +/** + * @short Widget to enter a time of day. + * + * The TimeEdit class provides a widget to enter a time of day in hours and minutes, + * using a 12- or 24-hour clock according to the user's locale settings. + * + * It displays a TimeSpinBox widget to enter hours and minutes. If a 12-hour clock is + * being used, it also displays a combo box to select am or pm. + * + * TimeSpinBox displays a spin box with two pairs of spin buttons, one for hours and + * one for minutes. It provides accelerated stepping using the spin buttons, when the + * shift key is held down (inherited from SpinBox2). The default shift steps are 5 + * minutes and 6 hours. + * + * The widget may be set as read-only. This has the same effect as disabling it, except + * that its appearance is unchanged. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class TimeEdit : public QHBox +{ + Q_OBJECT + public: + /** Constructor. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit TimeEdit(QWidget* parent = 0, const char* name = 0); + /** Returns true if the widget is read only. */ + bool isReadOnly() const { return mReadOnly; } + /** Sets whether the widget is read-only for the user. If read-only, + * the time cannot be edited and the spin buttons and am/pm combo box + * are inactive. + * @param readOnly True to set the widget read-only, false to set it read-write. + */ + virtual void setReadOnly(bool readOnly); + /** Returns true if the widget contains a valid value. */ + bool isValid() const; + /** Sets whether the edit value is valid. + * If newly invalid, the value is displayed as asterisks. + * If newly valid, the value is set to the minimum value. + * @param valid True to set the value valid, false to set it invalid. + */ + void setValid(bool valid); + /** Returns the entered time as a value in minutes. */ + int value() const; + /** Returns the entered time as a QTime value. */ + QTime time() const { int m = value(); return QTime(m/60, m%60); } + /** Returns true if it is possible to step the value from the highest value to the lowest value and vice versa. */ + bool wrapping() const; + /** Sets whether it is possible to step the value from the highest value to the lowest value and vice versa. + * @param on True to enable wrapping, else false. + */ + void setWrapping(bool on); + /** Returns the minimum value of the widget in minutes. */ + int minValue() const; + /** Returns the maximum value of the widget in minutes. */ + int maxValue() const; + /** Returns the maximum value of the widget as a QTime value. */ + QTime maxTime() const { int mv = maxValue(); return QTime(mv/60, mv%60); } + /** Sets the minimum value of the widget. */ + void setMinValue(int minutes); + /** Sets the maximum value of the widget. */ + void setMaxValue(int minutes); + /** Sets the maximum value of the widget. */ + void setMaxValue(const QTime& time) { setMaxValue(time.hour()*60 + time.minute()); } + public slots: + /** Sets the value of the widget. */ + virtual void setValue(int minutes); + /** Sets the value of the widget. */ + void setValue(const QTime& t) { setValue(t.hour()*60 + t.minute()); } + signals: + /** This signal is emitted every time the value of the widget changes + * (for whatever reason). + * @param minutes The new value. + */ + void valueChanged(int minutes); + + private slots: + void slotValueChanged(int); + void slotAmPmChanged(int item); + private: + void setAmPmCombo(int am, int pm); + + TimeSpinBox* mSpinBox; // always holds the 24-hour time + ComboBox* mAmPm; + int mAmIndex; // mAmPm index to "am", or -1 if none + int mPmIndex; // mAmPm index to "pm", or -1 if none + bool mReadOnly; // the widget is read only +}; + +#endif // TIMEEDIT_H diff --git a/kalarm/lib/timeperiod.cpp b/kalarm/lib/timeperiod.cpp new file mode 100644 index 000000000..22a7c3179 --- /dev/null +++ b/kalarm/lib/timeperiod.cpp @@ -0,0 +1,384 @@ +/* + * timeperiod.h - time period data entry widget + * Program: kalarm + * Copyright © 2003,2004,2007,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qwidgetstack.h> +#include <qwhatsthis.h> + +#include <klocale.h> +#include <kdialog.h> + +#include "combobox.h" +#include "spinbox.h" +#include "timespinbox.h" +#include "timeperiod.moc" + + +// Collect these widget labels together to ensure consistent wording and +// translations across different modules. +QString TimePeriod::i18n_minutes() { return i18n("minutes"); } +QString TimePeriod::i18n_Minutes() { return i18n("Minutes"); } +QString TimePeriod::i18n_hours_mins() { return i18n("hours/minutes"); } +QString TimePeriod::i18n_Hours_Mins() { return i18n("Hours/Minutes"); } +QString TimePeriod::i18n_days() { return i18n("days"); } +QString TimePeriod::i18n_Days() { return i18n("Days"); } +QString TimePeriod::i18n_weeks() { return i18n("weeks"); } +QString TimePeriod::i18n_Weeks() { return i18n("Weeks"); } + +static const int maxMinutes = 1000*60-1; // absolute maximum value for hours:minutes = 999H59M + +/*============================================================================= += Class TimePeriod += Contains a time unit combo box, plus a time spinbox, to select a time period. +=============================================================================*/ + +TimePeriod::TimePeriod(bool allowHourMinute, QWidget* parent, const char* name) + : QHBox(parent, name), + mMaxDays(9999), + mNoHourMinute(!allowHourMinute), + mReadOnly(false) +{ + setSpacing(KDialog::spacingHint()); + + mSpinStack = new QWidgetStack(this); + mSpinBox = new SpinBox(mSpinStack); + mSpinBox->setLineStep(1); + mSpinBox->setLineShiftStep(10); + mSpinBox->setRange(1, mMaxDays); + connect(mSpinBox, SIGNAL(valueChanged(int)), SLOT(slotDaysChanged(int))); + mSpinStack->addWidget(mSpinBox, 0); + + mTimeSpinBox = new TimeSpinBox(0, 99999, mSpinStack); + mTimeSpinBox->setRange(1, maxMinutes); // max 999H59M + connect(mTimeSpinBox, SIGNAL(valueChanged(int)), SLOT(slotTimeChanged(int))); + mSpinStack->addWidget(mTimeSpinBox, 1); + + mSpinStack->setFixedSize(mSpinBox->sizeHint().expandedTo(mTimeSpinBox->sizeHint())); + mHourMinuteRaised = mNoHourMinute; + showHourMin(!mNoHourMinute); + + mUnitsCombo = new ComboBox(false, this); + if (mNoHourMinute) + mDateOnlyOffset = 2; + else + { + mDateOnlyOffset = 0; + mUnitsCombo->insertItem(i18n_minutes()); + mUnitsCombo->insertItem(i18n_hours_mins()); + } + mUnitsCombo->insertItem(i18n_days()); + mUnitsCombo->insertItem(i18n_weeks()); + mMaxUnitShown = WEEKS; + mUnitsCombo->setFixedSize(mUnitsCombo->sizeHint()); + connect(mUnitsCombo, SIGNAL(activated(int)), SLOT(slotUnitsSelected(int))); + + setFocusProxy(mUnitsCombo); + setTabOrder(mUnitsCombo, mSpinStack); +} + +void TimePeriod::setReadOnly(bool ro) +{ + if (ro != mReadOnly) + { + mReadOnly = ro; + mSpinBox->setReadOnly(ro); + mTimeSpinBox->setReadOnly(ro); + mUnitsCombo->setReadOnly(ro); + } +} + +/****************************************************************************** +* Set whether the editor text is to be selected whenever spin buttons are +* clicked. Default is to select them. +*/ +void TimePeriod::setSelectOnStep(bool sel) +{ + mSpinBox->setSelectOnStep(sel); + mTimeSpinBox->setSelectOnStep(sel); +} + +/****************************************************************************** +* Set the input focus on the count field. +*/ +void TimePeriod::setFocusOnCount() +{ + mSpinStack->setFocus(); +} + +/****************************************************************************** +* Set the maximum values for the hours:minutes and days/weeks spinboxes. +* If 'hourmin' = 0, the hours:minutes maximum is left unchanged. +*/ +void TimePeriod::setMaximum(int hourmin, int days) +{ + int oldmins = minutes(); + if (hourmin > 0) + { + if (hourmin > maxMinutes) + hourmin = maxMinutes; + mTimeSpinBox->setRange(1, hourmin); + } + mMaxDays = (days >= 0) ? days : 0; + adjustDayWeekShown(); + setUnitRange(); + int mins = minutes(); + if (mins != oldmins) + emit valueChanged(mins); +} + +/****************************************************************************** + * Get the specified number of minutes. + * Reply = 0 if error. + */ +int TimePeriod::minutes() const +{ + int factor = 0; + switch (mUnitsCombo->currentItem() + mDateOnlyOffset) + { + case HOURS_MINUTES: + return mTimeSpinBox->value(); + case MINUTES: factor = 1; break; + case DAYS: factor = 24*60; break; + case WEEKS: factor = 7*24*60; break; + } + return mSpinBox->value() * factor; +} + +/****************************************************************************** +* Initialise the controls with a specified time period. +* The time unit combo-box is initialised to 'defaultUnits', but if 'dateOnly' +* is true, it will never be initialised to minutes or hours/minutes. +*/ +void TimePeriod::setMinutes(int mins, bool dateOnly, TimePeriod::Units defaultUnits) +{ + int oldmins = minutes(); + if (!dateOnly && mNoHourMinute) + dateOnly = true; + int item; + if (mins) + { + int count = mins; + if (mins % (24*60)) + item = (defaultUnits == MINUTES && count <= mSpinBox->maxValue()) ? MINUTES : HOURS_MINUTES; + else if (mins % (7*24*60)) + { + item = DAYS; + count = mins / (24*60); + } + else + { + item = WEEKS; + count = mins / (7*24*60); + } + if (item < mDateOnlyOffset) + item = mDateOnlyOffset; + else if (item > mMaxUnitShown) + item = mMaxUnitShown; + mUnitsCombo->setCurrentItem(item - mDateOnlyOffset); + if (item == HOURS_MINUTES) + mTimeSpinBox->setValue(count); + else + mSpinBox->setValue(count); + item = setDateOnly(mins, dateOnly, false); + } + else + { + item = defaultUnits; + if (item < mDateOnlyOffset) + item = mDateOnlyOffset; + else if (item > mMaxUnitShown) + item = mMaxUnitShown; + mUnitsCombo->setCurrentItem(item - mDateOnlyOffset); + if (dateOnly && !mDateOnlyOffset || !dateOnly && mDateOnlyOffset) + item = setDateOnly(mins, dateOnly, false); + } + showHourMin(item == HOURS_MINUTES && !mNoHourMinute); + + int newmins = minutes(); + if (newmins != oldmins) + emit valueChanged(newmins); +} + +/****************************************************************************** +* Enable/disable hours/minutes units (if hours/minutes were permitted in the +* constructor). +*/ +TimePeriod::Units TimePeriod::setDateOnly(int mins, bool dateOnly, bool signal) +{ + int oldmins = 0; + if (signal) + oldmins = minutes(); + int index = mUnitsCombo->currentItem(); + Units units = static_cast<Units>(index + mDateOnlyOffset); + if (!mNoHourMinute) + { + if (!dateOnly && mDateOnlyOffset) + { + // Change from date-only to allow hours/minutes + mUnitsCombo->insertItem(i18n_minutes(), 0); + mUnitsCombo->insertItem(i18n_hours_mins(), 1); + mDateOnlyOffset = 0; + adjustDayWeekShown(); + mUnitsCombo->setCurrentItem(index += 2); + } + else if (dateOnly && !mDateOnlyOffset) + { + // Change from allowing hours/minutes to date-only + mUnitsCombo->removeItem(0); + mUnitsCombo->removeItem(0); + mDateOnlyOffset = 2; + if (index > 2) + index -= 2; + else + index = 0; + adjustDayWeekShown(); + mUnitsCombo->setCurrentItem(index); + if (units == HOURS_MINUTES || units == MINUTES) + { + // Set units to days and round up the warning period + units = DAYS; + mUnitsCombo->setCurrentItem(DAYS - mDateOnlyOffset); + mSpinBox->setValue((mins + 1439) / 1440); + } + showHourMin(false); + } + } + + if (signal) + { + int newmins = minutes(); + if (newmins != oldmins) + emit valueChanged(newmins); + } + return units; +} + +/****************************************************************************** +* Adjust the days/weeks units shown to suit the maximum days limit. +*/ +void TimePeriod::adjustDayWeekShown() +{ + Units newMaxUnitShown = (mMaxDays >= 7) ? WEEKS : (mMaxDays || mDateOnlyOffset) ? DAYS : HOURS_MINUTES; + if (newMaxUnitShown > mMaxUnitShown) + { + if (mMaxUnitShown < DAYS) + mUnitsCombo->insertItem(i18n_days()); + if (newMaxUnitShown == WEEKS) + mUnitsCombo->insertItem(i18n_weeks()); + } + else if (newMaxUnitShown < mMaxUnitShown) + { + if (mMaxUnitShown == WEEKS) + mUnitsCombo->removeItem(WEEKS - mDateOnlyOffset); + if (newMaxUnitShown < DAYS) + mUnitsCombo->removeItem(DAYS - mDateOnlyOffset); + } + mMaxUnitShown = newMaxUnitShown; +} + +/****************************************************************************** +* Set the maximum value which may be entered into the day/week count field, +* depending on the current unit selection. +*/ +void TimePeriod::setUnitRange() +{ + int maxval; + switch (static_cast<Units>(mUnitsCombo->currentItem() + mDateOnlyOffset)) + { + case WEEKS: + maxval = mMaxDays / 7; + if (maxval) + break; + mUnitsCombo->setCurrentItem(DAYS - mDateOnlyOffset); + // fall through to DAYS + case DAYS: + maxval = mMaxDays ? mMaxDays : 1; + break; + case MINUTES: + maxval = mTimeSpinBox->maxValue(); + break; + case HOURS_MINUTES: + default: + return; + } + mSpinBox->setRange(1, maxval); +} + +/****************************************************************************** +* Called when a new item is made current in the time units combo box. +*/ +void TimePeriod::slotUnitsSelected(int index) +{ + setUnitRange(); + showHourMin(index + mDateOnlyOffset == HOURS_MINUTES); + emit valueChanged(minutes()); +} + +/****************************************************************************** +* Called when the value of the days/weeks spin box changes. +*/ +void TimePeriod::slotDaysChanged(int) +{ + if (!mHourMinuteRaised) + emit valueChanged(minutes()); +} + +/****************************************************************************** +* Called when the value of the time spin box changes. +*/ +void TimePeriod::slotTimeChanged(int value) +{ + if (mHourMinuteRaised) + emit valueChanged(value); +} + +/****************************************************************************** + * Set the currently displayed count widget. + */ +void TimePeriod::showHourMin(bool hourMinute) +{ + if (hourMinute != mHourMinuteRaised) + { + mHourMinuteRaised = hourMinute; + if (hourMinute) + { + mSpinStack->raiseWidget(mTimeSpinBox); + mSpinStack->setFocusProxy(mTimeSpinBox); + } + else + { + mSpinStack->raiseWidget(mSpinBox); + mSpinStack->setFocusProxy(mSpinBox); + } + } +} + +/****************************************************************************** + * Set separate WhatsThis texts for the count spinboxes and the units combobox. + * If the hours:minutes text is omitted, both spinboxes are set to the same + * WhatsThis text. + */ +void TimePeriod::setWhatsThis(const QString& units, const QString& dayWeek, const QString& hourMin) +{ + QWhatsThis::add(mUnitsCombo, units); + QWhatsThis::add(mSpinBox, dayWeek); + QWhatsThis::add(mTimeSpinBox, (hourMin.isNull() ? dayWeek : hourMin)); +} diff --git a/kalarm/lib/timeperiod.h b/kalarm/lib/timeperiod.h new file mode 100644 index 000000000..ef1a3b5c3 --- /dev/null +++ b/kalarm/lib/timeperiod.h @@ -0,0 +1,146 @@ +/* + * timeperiod.cpp - time period data entry widget + * Program: kalarm + * Copyright © 2003,2004,2007,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TIMEPERIOD_H +#define TIMEPERIOD_H + +#include <qhbox.h> +#include <qstring.h> + +class QWidgetStack; +class ComboBox; +class SpinBox; +class TimeSpinBox; + + +/** + * @short Time period entry widget. + * + * The TimePeriod class provides a widget for entering a time period as a number of + * weeks, days, hours and minutes, or minutes. + * + * It displays a combo box to select the time units (weeks, days, hours and minutes, or + * minutes) alongside a spin box to enter the number of units. The type of spin box + * displayed alters according to the units selection: day, week and minute values are + * entered in a normal spin box, while hours and minutes are entered in a time spin box + * (with two pairs of spin buttons, one for hours and one for minutes). + * + * The widget may be set as read-only. This has the same effect as disabling it, except + * that its appearance is unchanged. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class TimePeriod : public QHBox +{ + Q_OBJECT + public: + /** Units for the time period. + * @li MINUTES - the time period is entered as a number of minutes. + * @li HOURS_MINUTES - the time period is entered as an hours/minutes value. + * @li DAYS - the time period is entered as a number of days. + * @li WEEKS - the time period is entered as a number of weeks. + */ + enum Units { MINUTES, HOURS_MINUTES, DAYS, WEEKS }; + + /** Constructor. + * @param allowMinute Set false to prevent hours/minutes or minutes from + * being allowed as units; only days and weeks can ever be used, + * regardless of other method calls. Set true to allow minutes, + * hours/minutes, days or weeks as units. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + TimePeriod(bool allowMinute, QWidget* parent, const char* name = 0); + /** Returns true if the widget is read only. */ + bool isReadOnly() const { return mReadOnly; } + /** Sets whether the widget is read-only for the user. If read-only, + * the time period cannot be edited and the units combo box is inactive. + * @param readOnly True to set the widget read-only, false to set it read-write. + */ + virtual void setReadOnly(bool readOnly); + /** Gets the entered time period expressed in minutes. */ + int minutes() const; + /** Initialises the time period value. + * @param minutes The value of the time period to set, expressed as a number of minutes. + * @param dateOnly True to restrict the units available in the combo box to days or weeks. + * @param defaultUnits The units to display initially in the combo box. + */ + void setMinutes(int minutes, bool dateOnly, Units defaultUnits); + /** Enables or disables minutes and hours/minutes units in the combo box. To + * disable minutes and hours/minutes, set @p dateOnly true; to enable minutes + * and hours/minutes, set @p dateOnly false. But note that minutes and + * hours/minutes cannot be enabled if it was disallowed in the constructor. + */ + void setDateOnly(bool dateOnly) { setDateOnly(minutes(), dateOnly, true); } + /** Sets the maximum values for the minutes and hours/minutes, and days/weeks + * spin boxes. + * Set @p hourmin = 0 to leave the minutes and hours/minutes maximum unchanged. + */ + void setMaximum(int hourmin, int days); + /** Sets whether the editor text is to be selected whenever spin buttons are + * clicked. The default is to select it. + */ + void setSelectOnStep(bool select); + /** Sets the input focus to the count field. */ + void setFocusOnCount(); + /** Sets separate WhatsThis texts for the count spin boxes and the units combo box. + * If @p hourMin is omitted, both spin boxes are set to the same WhatsThis text. + */ + void setWhatsThis(const QString& units, const QString& dayWeek, const QString& hourMin = QString::null); + + static QString i18n_minutes(); // text of 'minutes' units, lower case + static QString i18n_Minutes(); // text of 'Minutes' units, initial capitals + static QString i18n_hours_mins(); // text of 'hours/minutes' units, lower case + static QString i18n_Hours_Mins(); // text of 'Hours/Minutes' units, initial capitals + static QString i18n_days(); // text of 'days' units, lower case + static QString i18n_Days(); // text of 'Days' units, initial capital + static QString i18n_weeks(); // text of 'weeks' units, lower case + static QString i18n_Weeks(); // text of 'Weeks' units, initial capital + + signals: + /** This signal is emitted whenever the value held in the widget changes. + * @param minutes The current value of the time period, expressed in minutes. + */ + void valueChanged(int minutes); // value has changed + + private slots: + void slotUnitsSelected(int index); + void slotDaysChanged(int); + void slotTimeChanged(int minutes); + + private: + Units setDateOnly(int minutes, bool dateOnly, bool signal); + void setUnitRange(); + void showHourMin(bool hourMin); + void adjustDayWeekShown(); + + QWidgetStack* mSpinStack; // displays either the days/weeks or hours:minutes spinbox + SpinBox* mSpinBox; // the minutes/days/weeks value spinbox + TimeSpinBox* mTimeSpinBox; // the hours:minutes value spinbox + ComboBox* mUnitsCombo; + int mMaxDays; // maximum day count + int mDateOnlyOffset; // for mUnitsCombo: 2 if minutes & hours/minutes are disabled, else 0 + Units mMaxUnitShown; // for mUnitsCombo: maximum units shown + bool mNoHourMinute; // hours/minutes cannot be displayed, ever + bool mReadOnly; // the widget is read only + bool mHourMinuteRaised; // hours:minutes spinbox is currently displayed +}; + +#endif // TIMEPERIOD_H diff --git a/kalarm/lib/timespinbox.cpp b/kalarm/lib/timespinbox.cpp new file mode 100644 index 000000000..7a22cd230 --- /dev/null +++ b/kalarm/lib/timespinbox.cpp @@ -0,0 +1,364 @@ +/* + * timespinbox.cpp - time spinbox widget + * Program: kalarm + * Copyright © 2001-2004,2007,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qvalidator.h> +#include <qlineedit.h> +#include <klocale.h> + +#include "timespinbox.moc" + + +class TimeSpinBox::TimeValidator : public QValidator +{ + public: + TimeValidator(int minMin, int maxMin, QWidget* parent, const char* name = 0) + : QValidator(parent, name), + minMinute(minMin), maxMinute(maxMin), m12Hour(false), mPm(false) { } + virtual State validate(QString&, int&) const; + int minMinute, maxMinute; + bool m12Hour; + bool mPm; +}; + + +/*============================================================================= += Class TimeSpinBox += This is a spin box displaying a time in the format hh:mm, with a pair of += spin buttons for each of the hour and minute values. += It can operate in 3 modes: += 1) a time of day using the 24-hour clock. += 2) a time of day using the 12-hour clock. The value is held as 0:00 - 23:59, += but is displayed as 12:00 - 11:59. This is for use in a TimeEdit widget. += 3) a length of time, not restricted to the length of a day. +=============================================================================*/ + +/****************************************************************************** + * Construct a wrapping 00:00 - 23:59, or 12:00 - 11:59 time spin box. + */ +TimeSpinBox::TimeSpinBox(bool use24hour, QWidget* parent, const char* name) + : SpinBox2(0, 1439, 1, 60, parent, name), + mMinimumValue(0), + m12Hour(!use24hour), + mPm(false), + mInvalid(false), + mEnteredSetValue(false) +{ + mValidator = new TimeValidator(0, 1439, this, "TimeSpinBox validator"); + mValidator->m12Hour = m12Hour; + setValidator(mValidator); + setWrapping(true); + setReverseWithLayout(false); // keep buttons the same way round even if right-to-left language + setShiftSteps(5, 360); // shift-left button increments 5 min / 6 hours + setSelectOnStep(false); + setAlignment(Qt::AlignHCenter); + connect(this, SIGNAL(valueChanged(int)), SLOT(slotValueChanged(int))); +} + +/****************************************************************************** + * Construct a non-wrapping time spin box. + */ +TimeSpinBox::TimeSpinBox(int minMinute, int maxMinute, QWidget* parent, const char* name) + : SpinBox2(minMinute, maxMinute, 1, 60, parent, name), + mMinimumValue(minMinute), + m12Hour(false), + mInvalid(false), + mEnteredSetValue(false) +{ + mValidator = new TimeValidator(minMinute, maxMinute, this, "TimeSpinBox validator"); + setValidator(mValidator); + setReverseWithLayout(false); // keep buttons the same way round even if right-to-left language + setShiftSteps(5, 300); // shift-left button increments 5 min / 5 hours + setSelectOnStep(false); + setAlignment(QApplication::reverseLayout() ? Qt::AlignLeft : Qt::AlignRight); +} + +QString TimeSpinBox::shiftWhatsThis() +{ + return i18n("Press the Shift key while clicking the spin buttons to adjust the time by a larger step (6 hours / 5 minutes)."); +} + +QTime TimeSpinBox::time() const +{ + return QTime(value() / 60, value() % 60); +} + +QString TimeSpinBox::mapValueToText(int v) +{ + if (m12Hour) + { + if (v < 60) + v += 720; // convert 0:nn to 12:nn + else if (v >= 780) + v -= 720; // convert 13 - 23 hours to 1 - 11 + } + QString s; + s.sprintf((wrapping() ? "%02d:%02d" : "%d:%02d"), v/60, v%60); + return s; +} + +/****************************************************************************** + * Convert the user-entered text to a value in minutes. + * The allowed formats are: + * [hour]:[minute], where minute must be non-blank, or + * hhmm, 4 digits, where hour < 24. + */ +int TimeSpinBox::mapTextToValue(bool* ok) +{ + QString text = cleanText(); + int colon = text.find(':'); + if (colon >= 0) + { + // [h]:m format for any time value + QString hour = text.left(colon).stripWhiteSpace(); + QString minute = text.mid(colon + 1).stripWhiteSpace(); + if (!minute.isEmpty()) + { + bool okmin; + bool okhour = true; + int m = minute.toUInt(&okmin); + int h = 0; + if (!hour.isEmpty()) + h = hour.toUInt(&okhour); + if (okhour && okmin && m < 60) + { + if (m12Hour) + { + if (h == 0 || h > 12) + h = 100; // error + else if (h == 12) + h = 0; // convert 12:nn to 0:nn + if (mPm) + h += 12; // convert to PM + } + int t = h * 60 + m; + if (t >= mMinimumValue && t <= maxValue()) + { + if (ok) + *ok = true; + return t; + } + } + } + } + else if (text.length() == 4) + { + // hhmm format for time of day + bool okn; + int mins = text.toUInt(&okn); + if (okn) + { + int m = mins % 100; + int h = mins / 100; + if (m12Hour) + { + if (h == 0 || h > 12) + h = 100; // error + else if (h == 12) + h = 0; // convert 12:nn to 0:nn + if (mPm) + h += 12; // convert to PM + } + int t = h * 60 + m; + if (h < 24 && m < 60 && t >= mMinimumValue && t <= maxValue()) + { + if (ok) + *ok = true; + return t; + } + } + + } + if (ok) + *ok = false; + return 0; +} + +/****************************************************************************** + * Set the spin box as valid or invalid. + * If newly invalid, the value is displayed as asterisks. + * If newly valid, the value is set to the minimum value. + */ +void TimeSpinBox::setValid(bool valid) +{ + if (valid && mInvalid) + { + mInvalid = false; + if (value() < mMinimumValue) + SpinBox2::setValue(mMinimumValue); + setSpecialValueText(QString()); + SpinBox2::setMinValue(mMinimumValue); + } + else if (!valid && !mInvalid) + { + mInvalid = true; + SpinBox2::setMinValue(mMinimumValue - 1); + setSpecialValueText(QString::fromLatin1("**:**")); + SpinBox2::setValue(mMinimumValue - 1); + } +} + +/****************************************************************************** +* Set the spin box's minimum value. +*/ +void TimeSpinBox::setMinValue(int minutes) +{ + mMinimumValue = minutes; + SpinBox2::setMinValue(mMinimumValue - (mInvalid ? 1 : 0)); +} + +/****************************************************************************** + * Set the spin box's value. + */ +void TimeSpinBox::setValue(int minutes) +{ + if (!mEnteredSetValue) + { + mEnteredSetValue = true; + mPm = (minutes >= 720); + if (minutes > maxValue()) + setValid(false); + else + { + if (mInvalid) + { + mInvalid = false; + setSpecialValueText(QString()); + SpinBox2::setMinValue(mMinimumValue); + } + SpinBox2::setValue(minutes); + mEnteredSetValue = false; + } + } +} + +/****************************************************************************** + * Step the spin box value. + * If it was invalid, set it valid and set the value to the minimum. + */ +void TimeSpinBox::stepUp() +{ + if (mInvalid) + setValid(true); + else + SpinBox2::stepUp(); +} + +void TimeSpinBox::stepDown() +{ + if (mInvalid) + setValid(true); + else + SpinBox2::stepDown(); +} + +bool TimeSpinBox::isValid() const +{ + return value() >= mMinimumValue; +} + +void TimeSpinBox::slotValueChanged(int value) +{ + mPm = mValidator->mPm = (value >= 720); +} + +QSize TimeSpinBox::sizeHint() const +{ + QSize sz = SpinBox2::sizeHint(); + QFontMetrics fm(font()); + return QSize(sz.width() + fm.width(":"), sz.height()); +} + +QSize TimeSpinBox::minimumSizeHint() const +{ + QSize sz = SpinBox2::minimumSizeHint(); + QFontMetrics fm(font()); + return QSize(sz.width() + fm.width(":"), sz.height()); +} + +/****************************************************************************** + * Validate the time spin box input. + * The entered time must either be 4 digits, or it must contain a colon, but + * hours may be blank. + */ +QValidator::State TimeSpinBox::TimeValidator::validate(QString& text, int& /*cursorPos*/) const +{ + QString cleanText = text.stripWhiteSpace(); + if (cleanText.isEmpty()) + return QValidator::Intermediate; + QValidator::State state = QValidator::Acceptable; + QString hour; + bool ok; + int hr = 0; + int mn = 0; + int colon = cleanText.find(':'); + if (colon >= 0) + { + QString minute = cleanText.mid(colon + 1); + if (minute.isEmpty()) + state = QValidator::Intermediate; + else if ((mn = minute.toUInt(&ok)) >= 60 || !ok) + return QValidator::Invalid; + + hour = cleanText.left(colon); + } + else if (maxMinute >= 1440) + { + // The hhmm form of entry is only allowed for time-of-day, i.e. <= 2359 + hour = cleanText; + state = QValidator::Intermediate; + } + else + { + if (cleanText.length() > 4) + return QValidator::Invalid; + if (cleanText.length() < 4) + state = QValidator::Intermediate; + hour = cleanText.left(2); + QString minute = cleanText.mid(2); + if (!minute.isEmpty() + && ((mn = minute.toUInt(&ok)) >= 60 || !ok)) + return QValidator::Invalid; + } + + if (!hour.isEmpty()) + { + hr = hour.toUInt(&ok); + if (m12Hour) + { + if (hr == 0 || hr > 12) + hr = 100; // error; + else if (hr == 12) + hr = 0; // convert 12:nn to 0:nn + if (mPm) + hr += 12; // convert to PM + } + if (!ok || hr > maxMinute/60) + return QValidator::Invalid; + } + if (state == QValidator::Acceptable) + { + int t = hr * 60 + mn; + if (t < minMinute || t > maxMinute) + return QValidator::Invalid; + } + return state; +} diff --git a/kalarm/lib/timespinbox.h b/kalarm/lib/timespinbox.h new file mode 100644 index 000000000..275fdf309 --- /dev/null +++ b/kalarm/lib/timespinbox.h @@ -0,0 +1,127 @@ +/* + * timespinbox.h - time spinbox widget + * Program: kalarm + * Copyright © 2001-2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TIMESPINBOX_H +#define TIMESPINBOX_H + +#include <qdatetime.h> +#include "spinbox2.h" + + +/** + * @short Hours/minutes time entry widget. + * + * The TimeSpinBox class provides a widget to enter a time consisting of an hours/minutes + * value. It can hold a time in any of 3 modes: a time of day using the 24-hour clock; a + * time of day using the 12-hour clock; or a length of time not restricted to 24 hours. + * + * Derived from SpinBox2, it displays a spin box with two pairs of spin buttons, one + * for hours and one for minutes. It provides accelerated stepping using the spin + * buttons, when the shift key is held down (inherited from SpinBox2). The default + * shift steps are 5 minutes and 6 hours. + * + * The widget may be set as read-only. This has the same effect as disabling it, except + * that its appearance is unchanged. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class TimeSpinBox : public SpinBox2 +{ + Q_OBJECT + public: + /** Constructor for a wrapping time spin box which can be used to enter a time of day. + * @param use24hour True for entry of 24-hour clock times (range 00:00 to 23:59). + * False for entry of 12-hour clock times (range 12:00 to 11:59). + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit TimeSpinBox(bool use24hour, QWidget* parent = 0, const char* name = 0); + /** Constructor for a non-wrapping time spin box which can be used to enter a length of time. + * @param minMinute The minimum value which the spin box can hold, in minutes. + * @param maxMinute The maximum value which the spin box can hold, in minutes. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + TimeSpinBox(int minMinute, int maxMinute, QWidget* parent = 0, const char* name = 0); + /** Returns true if the spin box holds a valid value. + * An invalid value is displayed as asterisks. + */ + bool isValid() const; + /** Sets the spin box as holding a valid or invalid value. + * If newly invalid, the value is displayed as asterisks. + * If newly valid, the value is set to the minimum value. + */ + void setValid(bool); + /** Returns the current value held in the spin box. + * If an invalid value is displayed, returns a value lower than the minimum value. + */ + QTime time() const; + /** Sets the maximum value which can be held in the spin box. + * @param minutes The maximum value expressed in minutes. + */ + virtual void setMinValue(int minutes); + /** Sets the maximum value which can be held in the spin box. + * @param minutes The maximum value expressed in minutes. + */ + virtual void setMaxValue(int minutes) { SpinBox2::setMaxValue(minutes); } + /** Sets the maximum value which can be held in the spin box. */ + void setMaxValue(const QTime& t) { SpinBox2::setMaxValue(t.hour()*60 + t.minute()); } + /** Returns the maximum value which can be held in the spin box. */ + QTime maxTime() const { int mv = maxValue(); return QTime(mv/60, mv%60); } + /** Returns a text describing use of the shift key as an accelerator for + * the spin buttons, designed for incorporation into WhatsThis texts. + */ + static QString shiftWhatsThis(); + + virtual QSize sizeHint() const; + virtual QSize minimumSizeHint() const; + + public slots: + /** Sets the value of the spin box. + * @param minutes The new value of the spin box, expressed in minutes. + */ + virtual void setValue(int minutes); + /** Sets the value of the spin box. */ + void setValue(const QTime& t) { setValue(t.hour()*60 + t.minute()); } + /** Increments the spin box value. + * If the value was previously invalid, the spin box is set to the minimum value. + */ + virtual void stepUp(); + /** Decrements the spin box value. + * If the value was previously invalid, the spin box is set to the minimum value. + */ + virtual void stepDown(); + + protected: + virtual QString mapValueToText(int v); + virtual int mapTextToValue(bool* ok); + private slots: + void slotValueChanged(int value); + private: + class TimeValidator; + TimeValidator* mValidator; + int mMinimumValue; // real minimum value, excluding special value for "**:**" + bool m12Hour; // use 12-hour clock + bool mPm; // use PM for manually entered values (with 12-hour clock) + bool mInvalid; // value is currently invalid (asterisks) + bool mEnteredSetValue; // to prevent infinite recursion in setValue() +}; + +#endif // TIMESPINBOX_H diff --git a/kalarm/main.cpp b/kalarm/main.cpp new file mode 100644 index 000000000..0e1906ce2 --- /dev/null +++ b/kalarm/main.cpp @@ -0,0 +1,131 @@ +/* + * main.cpp + * Program: kalarm + * Copyright © 2001-2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <stdlib.h> + +#include <kcmdlineargs.h> +#include <kaboutdata.h> +#include <klocale.h> +#include <kdebug.h> + +#include "kalarmapp.h" + +#define PROGRAM_NAME "kalarm" + +static KCmdLineOptions options[] = +{ + { "a", 0, 0 }, + { "ack-confirm", I18N_NOOP("Prompt for confirmation when alarm is acknowledged"), 0 }, + { "A", 0, 0 }, + { "attach <url>", I18N_NOOP("Attach file to email (repeat as needed)"), 0 }, + { "auto-close", I18N_NOOP("Auto-close alarm window after --late-cancel period"), 0 }, + { "bcc", I18N_NOOP("Blind copy email to self"), 0 }, + { "b", 0, 0 }, + { "beep", I18N_NOOP("Beep when message is displayed"), 0 }, + { "colour", 0, 0 }, + { "c", 0, 0 }, + { "color <color>", I18N_NOOP("Message background color (name or hex 0xRRGGBB)"), 0 }, + { "colourfg", 0, 0 }, + { "C", 0, 0 }, + { "colorfg <color>", I18N_NOOP("Message foreground color (name or hex 0xRRGGBB)"), 0 }, + { "calendarURL <url>", I18N_NOOP("URL of calendar file"), 0 }, + { "cancelEvent <eventID>", I18N_NOOP("Cancel alarm with the specified event ID"), 0 }, + { "d", 0, 0 }, + { "disable", I18N_NOOP("Disable the alarm"), 0 }, + { "e", 0, 0 }, + { "!exec <commandline>", I18N_NOOP("Execute a shell command line"), 0 }, + { "edit <eventID>", I18N_NOOP("Display the alarm edit dialog to edit the specified alarm"), 0 }, + { "n", 0, 0 }, + { "edit-new", I18N_NOOP("Display the alarm edit dialog to edit a new alarm"), 0 }, + { "edit-new-preset <templateName>", I18N_NOOP("Display the alarm edit dialog, preset with a template"), 0 }, + { "f", 0, 0 }, + { "file <url>", I18N_NOOP("File to display"), 0 }, + { "F", 0, 0 }, + { "from-id <ID>", I18N_NOOP("KMail identity to use as sender of email"), 0 }, + { "handleEvent <eventID>", I18N_NOOP("Trigger or cancel alarm with the specified event ID"), 0 }, + { "i", 0, 0 }, + { "interval <period>", I18N_NOOP("Interval between alarm repetitions"), 0 }, + { "k", 0, 0 }, + { "korganizer", I18N_NOOP("Show alarm as an event in KOrganizer"), 0 }, + { "l", 0, 0 }, + { "late-cancel <period>", I18N_NOOP("Cancel alarm if more than 'period' late when triggered"), "1" }, + { "L", 0, 0 }, + { "login", I18N_NOOP("Repeat alarm at every login"), 0 }, + { "m", 0, 0 }, + { "mail <address>", I18N_NOOP("Send an email to the given address (repeat as needed)"), 0 }, + { "p", 0, 0 }, + { "play <url>", I18N_NOOP("Audio file to play once"), 0 }, +#ifndef WITHOUT_ARTS + { "P", 0, 0 }, + { "play-repeat <url>", I18N_NOOP("Audio file to play repeatedly"), 0 }, +#endif + { "recurrence <spec>", I18N_NOOP("Specify alarm recurrence using iCalendar syntax"), 0 }, + { "R", 0, 0 }, + { "reminder <period>", I18N_NOOP("Display reminder in advance of alarm"), 0 }, + { "reminder-once <period>", I18N_NOOP("Display reminder once, before first alarm recurrence"), 0 }, + { "r", 0, 0 }, + { "repeat <count>", I18N_NOOP("Number of times to repeat alarm (including initial occasion)"), 0 }, + { "reset", I18N_NOOP("Reset the alarm scheduling daemon"), 0 }, + { "s", 0, 0 }, + { "speak", I18N_NOOP("Speak the message when it is displayed"), 0 }, + { "stop", I18N_NOOP("Stop the alarm scheduling daemon"), 0 }, + { "S", 0, 0 }, + { "subject", I18N_NOOP("Email subject line"), 0 }, + { "t", 0, 0 }, + { "time <time>", I18N_NOOP("Trigger alarm at time [[[yyyy-]mm-]dd-]hh:mm, or date yyyy-mm-dd"), 0 }, + { "tray", I18N_NOOP("Display system tray icon"), 0 }, + { "triggerEvent <eventID>", I18N_NOOP("Trigger alarm with the specified event ID"), 0 }, + { "u", 0, 0 }, + { "until <time>", I18N_NOOP("Repeat until time [[[yyyy-]mm-]dd-]hh:mm, or date yyyy-mm-dd"), 0 }, +#ifndef WITHOUT_ARTS + { "V", 0, 0 }, + { "volume <percent>", I18N_NOOP("Volume to play audio file"), 0 }, +#endif + { "+[message]", I18N_NOOP("Message text to display"), 0 }, + KCmdLineLastOption +}; + + +int main(int argc, char *argv[]) +{ + KAboutData aboutData(PROGRAM_NAME, I18N_NOOP("KAlarm"), KALARM_VERSION, + I18N_NOOP("Personal alarm message, command and email scheduler for KDE"), + KAboutData::License_GPL, + "Copyright 2001-2007, David Jarvie", 0, "http://www.astrojar.org.uk/kalarm"); + aboutData.addAuthor("David Jarvie", 0, "software@astrojar.org.uk"); + + KCmdLineArgs::init(argc, argv, &aboutData); + KCmdLineArgs::addCmdLineOptions(options); + KUniqueApplication::addCmdLineOptions(); + + if (!KAlarmApp::start()) + { + // An instance of the application is already running + exit(0); + } + + // This is the first time through + kdDebug(5950) << "main(): initialising\n"; + KAlarmApp* app = KAlarmApp::getInstance(); + app->restoreSession(); + return app->exec(); +} diff --git a/kalarm/mainwindow.cpp b/kalarm/mainwindow.cpp new file mode 100644 index 000000000..01ddc83d3 --- /dev/null +++ b/kalarm/mainwindow.cpp @@ -0,0 +1,1424 @@ +/* + * mainwindow.cpp - main application window + * Program: kalarm + * Copyright © 2001-2007 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qiconset.h> +#include <qdragobject.h> +#include <qheader.h> + +#include <kmenubar.h> +#include <ktoolbar.h> +#include <kpopupmenu.h> +#include <kaccel.h> +#include <kaction.h> +#include <kactionclasses.h> +#include <kstdaction.h> +#include <kiconloader.h> +#include <kmessagebox.h> +#include <kurldrag.h> +#include <klocale.h> +#include <kglobalsettings.h> +#include <kconfig.h> +#include <kkeydialog.h> +#include <kedittoolbar.h> +#include <kaboutdata.h> +#include <dcopclient.h> +#include <kdebug.h> + +#include <libkdepim/maillistdrag.h> +#include <libkmime/kmime_content.h> +#include <libkcal/calendarlocal.h> +#include <libkcal/icaldrag.h> + +#include "alarmcalendar.h" +#include "alarmevent.h" +#include "alarmlistview.h" +#include "alarmtext.h" +#include "birthdaydlg.h" +#include "daemon.h" +#include "editdlg.h" +#include "functions.h" +#include "kalarmapp.h" +#include "kamail.h" +#include "prefdlg.h" +#include "preferences.h" +#include "synchtimer.h" +#include "templatepickdlg.h" +#include "templatedlg.h" +#include "traywindow.h" +#include "mainwindow.moc" + +using namespace KCal; + +static const char* UI_FILE = "kalarmui.rc"; +static const char* WINDOW_NAME = "MainWindow"; + +static const QString VIEW_GROUP = QString::fromLatin1("View"); +static const QString SHOW_TIME_KEY = QString::fromLatin1("ShowAlarmTime"); +static const QString SHOW_TIME_TO_KEY = QString::fromLatin1("ShowTimeToAlarm"); +static const QString SHOW_ARCHIVED_KEY = QString::fromLatin1("ShowArchivedAlarms"); +static const QString SHOW_RESOURCES_KEY = QString::fromLatin1("ShowResources"); + +static QString undoText; +static QString undoTextStripped; +static QString undoIcon; +static KShortcut undoShortcut; +static QString redoText; +static QString redoTextStripped; +static QString redoIcon; +static KShortcut redoShortcut; + + +/*============================================================================= += Class: MainWindow +=============================================================================*/ + +MainWindow::WindowList MainWindow::mWindowList; +TemplateDlg* MainWindow::mTemplateDlg = 0; + +// Collect these widget labels together to ensure consistent wording and +// translations across different modules. +QString MainWindow::i18n_a_ShowAlarmTimes() { return i18n("Show &Alarm Times"); } +QString MainWindow::i18n_m_ShowAlarmTime() { return i18n("Show alarm ti&me"); } +QString MainWindow::i18n_o_ShowTimeToAlarms() { return i18n("Show Time t&o Alarms"); } +QString MainWindow::i18n_l_ShowTimeToAlarm() { return i18n("Show time unti&l alarm"); } +QString MainWindow::i18n_ShowExpiredAlarms() { return i18n("Show Expired Alarms"); } +QString MainWindow::i18n_e_ShowExpiredAlarms() { return i18n("Show &Expired Alarms"); } +QString MainWindow::i18n_HideExpiredAlarms() { return i18n("Hide Expired Alarms"); } +QString MainWindow::i18n_e_HideExpiredAlarms() { return i18n("Hide &Expired Alarms"); } + + +/****************************************************************************** +* Construct an instance. +* To avoid resize() events occurring while still opening the calendar (and +* resultant crashes), the calendar is opened before constructing the instance. +*/ +MainWindow* MainWindow::create(bool restored) +{ + theApp()->checkCalendarDaemon(); // ensure calendar is open and daemon started + return new MainWindow(restored); +} + +MainWindow::MainWindow(bool restored) + : MainWindowBase(0, "MainWin", WGroupLeader | WStyle_ContextHelp | WDestructiveClose), + mMinuteTimerActive(false), + mHiddenTrayParent(false) +{ + kdDebug(5950) << "MainWindow::MainWindow()\n"; + setAutoSaveSettings(QString::fromLatin1(WINDOW_NAME)); // save window sizes etc. + setPlainCaption(kapp->aboutData()->programName()); + KConfig* config = KGlobal::config(); + config->setGroup(VIEW_GROUP); + mShowExpired = config->readBoolEntry(SHOW_ARCHIVED_KEY, false); + mShowTime = config->readBoolEntry(SHOW_TIME_KEY, true); + mShowTimeTo = config->readBoolEntry(SHOW_TIME_TO_KEY, false); + if (!restored) + { + QSize s; + if (KAlarm::readConfigWindowSize(WINDOW_NAME, s)) + resize(s); + } + config->setGroup(QString::fromLatin1(WINDOW_NAME)); + QValueList<int> order = config->readIntListEntry(QString::fromLatin1("ColumnOrder")); + + setAcceptDrops(true); // allow drag-and-drop onto this window + if (!mShowTimeTo) + mShowTime = true; // ensure at least one time column is visible + mListView = new AlarmListView(order, this, "listView"); + mListView->selectTimeColumns(mShowTime, mShowTimeTo); + mListView->showExpired(mShowExpired); + setCentralWidget(mListView); + mListView->refresh(); // populate the alarm list + mListView->clearSelection(); + + connect(mListView, SIGNAL(itemDeleted()), SLOT(slotDeletion())); + connect(mListView, SIGNAL(selectionChanged()), SLOT(slotSelection())); + connect(mListView, SIGNAL(contextMenuRequested(QListViewItem*, const QPoint&, int)), + SLOT(slotContextMenuRequested(QListViewItem*, const QPoint&, int))); + connect(mListView, SIGNAL(mouseButtonClicked(int, QListViewItem*, const QPoint&, int)), + SLOT(slotMouseClicked(int, QListViewItem*, const QPoint&, int))); + connect(mListView, SIGNAL(executed(QListViewItem*)), SLOT(slotDoubleClicked(QListViewItem*))); + connect(mListView->header(), SIGNAL(indexChange(int, int, int)), SLOT(columnsReordered())); + initActions(); + + mWindowList.append(this); + if (mWindowList.count() == 1 && Daemon::isDcopHandlerReady()) + { + // It's the first main window, and the DCOP handler is ready + if (theApp()->wantRunInSystemTray()) + theApp()->displayTrayIcon(true, this); // create system tray icon for run-in-system-tray mode + else if (theApp()->trayWindow()) + theApp()->trayWindow()->setAssocMainWindow(this); // associate this window with the system tray icon + } + setUpdateTimer(); +} + +MainWindow::~MainWindow() +{ + kdDebug(5950) << "MainWindow::~MainWindow()\n"; + mWindowList.remove(this); + if (theApp()->trayWindow()) + { + if (isTrayParent()) + delete theApp()->trayWindow(); + else + theApp()->trayWindow()->removeWindow(this); + } + MinuteTimer::disconnect(this); + mMinuteTimerActive = false; // to ensure that setUpdateTimer() works correctly + setUpdateTimer(); + MainWindow* main = mainMainWindow(); + if (main) + KAlarm::writeConfigWindowSize(WINDOW_NAME, main->size()); + KToolBar* tb = toolBar(); + if (tb) + tb->saveSettings(KGlobal::config(), "Toolbars"); + KGlobal::config()->sync(); // save any new window size to disc + theApp()->quitIf(); +} + +/****************************************************************************** +* Save settings to the session managed config file, for restoration +* when the program is restored. +*/ +void MainWindow::saveProperties(KConfig* config) +{ + config->writeEntry(QString::fromLatin1("HiddenTrayParent"), isTrayParent() && isHidden()); + config->writeEntry(QString::fromLatin1("ShowExpired"), mShowExpired); + config->writeEntry(QString::fromLatin1("ShowTime"), mShowTime); + config->writeEntry(QString::fromLatin1("ShowTimeTo"), mShowTimeTo); +} + +/****************************************************************************** +* Read settings from the session managed config file. +* This function is automatically called whenever the app is being +* restored. Read in whatever was saved in saveProperties(). +*/ +void MainWindow::readProperties(KConfig* config) +{ + mHiddenTrayParent = config->readBoolEntry(QString::fromLatin1("HiddenTrayParent")); + mShowExpired = config->readBoolEntry(QString::fromLatin1("ShowExpired")); + mShowTime = config->readBoolEntry(QString::fromLatin1("ShowTime")); + mShowTimeTo = config->readBoolEntry(QString::fromLatin1("ShowTimeTo")); +} + +/****************************************************************************** +* Get the main main window, i.e. the parent of the system tray icon, or if +* none, the first main window to be created. Visible windows take precedence +* over hidden ones. +*/ +MainWindow* MainWindow::mainMainWindow() +{ + MainWindow* tray = theApp()->trayWindow() ? theApp()->trayWindow()->assocMainWindow() : 0; + if (tray && tray->isVisible()) + return tray; + for (WindowList::Iterator it = mWindowList.begin(); it != mWindowList.end(); ++it) + if ((*it)->isVisible()) + return *it; + if (tray) + return tray; + if (mWindowList.isEmpty()) + return 0; + return mWindowList.first(); +} + +/****************************************************************************** +* Check whether this main window is the parent of the system tray icon. +*/ +bool MainWindow::isTrayParent() const +{ + return theApp()->wantRunInSystemTray() && theApp()->trayMainWindow() == this; +} + +/****************************************************************************** +* Close all main windows. +*/ +void MainWindow::closeAll() +{ + while (!mWindowList.isEmpty()) + delete mWindowList.first(); // N.B. the destructor removes the window from the list +} + +/****************************************************************************** +* Called when the window's size has changed (before it is painted). +* Sets the last column in the list view to extend at least to the right hand +* edge of the list view. +* Records the new size in the config file. +*/ +void MainWindow::resizeEvent(QResizeEvent* re) +{ + // Save the window's new size only if it's the first main window + if (mainMainWindow() == this) + KAlarm::writeConfigWindowSize(WINDOW_NAME, re->size()); + MainWindowBase::resizeEvent(re); +} + +/****************************************************************************** +* Called when the window is first displayed. +* Sets the last column in the list view to extend at least to the right hand +* edge of the list view. +*/ +void MainWindow::showEvent(QShowEvent* se) +{ + setUpdateTimer(); + slotUpdateTimeTo(); + MainWindowBase::showEvent(se); +} + +/****************************************************************************** +* Display the window. +*/ +void MainWindow::show() +{ + MainWindowBase::show(); + if (mMenuError) + { + // Show error message now that the main window has been displayed. + // Waiting until now lets the user easily associate the message with + // the main window which is faulty. + KMessageBox::error(this, i18n("Failure to create menus\n(perhaps %1 missing or corrupted)").arg(QString::fromLatin1(UI_FILE))); + mMenuError = false; + } +} + +/****************************************************************************** +* Called after the window is hidden. +*/ +void MainWindow::hideEvent(QHideEvent* he) +{ + setUpdateTimer(); + MainWindowBase::hideEvent(he); +} + +/****************************************************************************** +* Called when the list's column order is changed. +* Save the new column order as the default the next time the program is run. +*/ +void MainWindow::columnsReordered() +{ + KConfig* config = KGlobal::config(); + config->setGroup(QString::fromLatin1(WINDOW_NAME)); + config->writeEntry(QString::fromLatin1("ColumnOrder"), mListView->columnOrder()); + config->sync(); +} + +/****************************************************************************** +* Initialise the menu, toolbar and main window actions. +*/ +void MainWindow::initActions() +{ + KActionCollection* actions = actionCollection(); + mActionTemplates = new KAction(i18n("&Templates..."), 0, this, SLOT(slotTemplates()), actions, "templates"); + mActionNew = KAlarm::createNewAlarmAction(i18n("&New..."), this, SLOT(slotNew()), actions, "new"); + mActionNewFromTemplate = KAlarm::createNewFromTemplateAction(i18n("New &From Template"), this, SLOT(slotNewFromTemplate(const KAEvent&)), actions, "newFromTempl"); + mActionCreateTemplate = new KAction(i18n("Create Tem&plate..."), 0, this, SLOT(slotNewTemplate()), actions, "createTemplate"); + mActionCopy = new KAction(i18n("&Copy..."), "editcopy", Qt::SHIFT+Qt::Key_Insert, this, SLOT(slotCopy()), actions, "copy"); + mActionModify = new KAction(i18n("&Edit..."), "edit", Qt::CTRL+Qt::Key_E, this, SLOT(slotModify()), actions, "modify"); + mActionDelete = new KAction(i18n("&Delete"), "editdelete", Qt::Key_Delete, this, SLOT(slotDelete()), actions, "delete"); + mActionReactivate = new KAction(i18n("Reac&tivate"), 0, Qt::CTRL+Qt::Key_R, this, SLOT(slotReactivate()), actions, "undelete"); + mActionEnable = new KAction(QString::null, 0, Qt::CTRL+Qt::Key_B, this, SLOT(slotEnable()), actions, "disable"); + mActionView = new KAction(i18n("&View"), "viewmag", Qt::CTRL+Qt::Key_W, this, SLOT(slotView()), actions, "view"); + mActionShowTime = new KToggleAction(i18n_a_ShowAlarmTimes(), Qt::CTRL+Qt::Key_M, this, SLOT(slotShowTime()), actions, "showAlarmTimes"); + mActionShowTime->setCheckedState(i18n("Hide &Alarm Times")); + mActionShowTimeTo = new KToggleAction(i18n_o_ShowTimeToAlarms(), Qt::CTRL+Qt::Key_I, this, SLOT(slotShowTimeTo()), actions, "showTimeToAlarms"); + mActionShowTimeTo->setCheckedState(i18n("Hide Time t&o Alarms")); + mActionShowExpired = new KToggleAction(i18n_e_ShowExpiredAlarms(), "history", Qt::CTRL+Qt::Key_P, this, SLOT(slotShowExpired()), actions, "showExpiredAlarms"); + mActionShowExpired->setCheckedState(i18n_e_HideExpiredAlarms()); + mActionToggleTrayIcon = new KToggleAction(i18n("Show in System &Tray"), 0, this, SLOT(slotToggleTrayIcon()), actions, "showInSystemTray"); + mActionToggleTrayIcon->setCheckedState(i18n("Hide From System &Tray")); + new KAction(i18n("Import &Alarms..."), 0, this, SLOT(slotImportAlarms()), actions, "importAlarms"); + new KAction(i18n("Import &Birthdays..."), 0, this, SLOT(slotBirthdays()), actions, "importBirthdays"); + new KAction(i18n("&Refresh Alarms"), "reload", 0, this, SLOT(slotResetDaemon()), actions, "refreshAlarms"); + Daemon::createAlarmEnableAction(actions, "alarmEnable"); + if (undoText.isNull()) + { + // Get standard texts, etc., for Undo and Redo actions + KAction* act = KStdAction::undo(this, 0, actions); + undoIcon = act->icon(); + undoShortcut = act->shortcut(); + undoText = act->text(); + undoTextStripped = KAlarm::stripAccel(undoText); + delete act; + act = KStdAction::redo(this, 0, actions); + redoIcon = act->icon(); + redoShortcut = act->shortcut(); + redoText = act->text(); + redoTextStripped = KAlarm::stripAccel(redoText); + delete act; + } + mActionUndo = new KToolBarPopupAction(undoText, undoIcon, undoShortcut, this, SLOT(slotUndo()), actions, "edit_undo"); + mActionRedo = new KToolBarPopupAction(redoText, redoIcon, redoShortcut, this, SLOT(slotRedo()), actions, "edit_redo"); + KStdAction::find(mListView, SLOT(slotFind()), actions); + mActionFindNext = KStdAction::findNext(mListView, SLOT(slotFindNext()), actions); + mActionFindPrev = KStdAction::findPrev(mListView, SLOT(slotFindPrev()), actions); + KStdAction::selectAll(mListView, SLOT(slotSelectAll()), actions); + KStdAction::deselect(mListView, SLOT(slotDeselect()), actions); + KStdAction::quit(this, SLOT(slotQuit()), actions); + KStdAction::keyBindings(this, SLOT(slotConfigureKeys()), actions); + KStdAction::configureToolbars(this, SLOT(slotConfigureToolbar()), actions); + KStdAction::preferences(this, SLOT(slotPreferences()), actions); + setStandardToolBarMenuEnabled(true); + createGUI(UI_FILE); + + mContextMenu = static_cast<KPopupMenu*>(factory()->container("listContext", this)); + mActionsMenu = static_cast<KPopupMenu*>(factory()->container("actions", this)); + mMenuError = (!mContextMenu || !mActionsMenu); + connect(mActionsMenu, SIGNAL(aboutToShow()), SLOT(updateActionsMenu())); + connect(mActionUndo->popupMenu(), SIGNAL(aboutToShow()), SLOT(slotInitUndoMenu())); + connect(mActionUndo->popupMenu(), SIGNAL(activated(int)), SLOT(slotUndoItem(int))); + connect(mActionRedo->popupMenu(), SIGNAL(aboutToShow()), SLOT(slotInitRedoMenu())); + connect(mActionRedo->popupMenu(), SIGNAL(activated(int)), SLOT(slotRedoItem(int))); + connect(Undo::instance(), SIGNAL(changed(const QString&, const QString&)), SLOT(slotUndoStatus(const QString&, const QString&))); + connect(mListView, SIGNAL(findActive(bool)), SLOT(slotFindActive(bool))); + Preferences::connect(SIGNAL(preferencesChanged()), this, SLOT(slotPrefsChanged())); + connect(theApp(), SIGNAL(trayIconToggled()), SLOT(updateTrayIconAction())); + + // Set menu item states + setEnableText(true); + mActionShowTime->setChecked(mShowTime); + mActionShowTimeTo->setChecked(mShowTimeTo); + mActionShowExpired->setChecked(mShowExpired); + slotPrefsChanged(); // set the correct text for this action + mActionUndo->setEnabled(Undo::haveUndo()); + mActionRedo->setEnabled(Undo::haveRedo()); + mActionFindNext->setEnabled(false); + mActionFindPrev->setEnabled(false); + + mActionCopy->setEnabled(false); + mActionModify->setEnabled(false); + mActionDelete->setEnabled(false); + mActionReactivate->setEnabled(false); + mActionView->setEnabled(false); + mActionEnable->setEnabled(false); + mActionCreateTemplate->setEnabled(false); + + KToolBar* tb = toolBar(); + if (tb) + tb->applySettings(KGlobal::config(), "Toolbars"); + + Undo::emitChanged(); // set the Undo/Redo menu texts + Daemon::checkStatus(); + Daemon::monitoringAlarms(); +} + +/****************************************************************************** +* Enable or disable the Templates menu item in every main window instance. +*/ +void MainWindow::enableTemplateMenuItem(bool enable) +{ + for (WindowList::Iterator it = mWindowList.begin(); it != mWindowList.end(); ++it) + (*it)->mActionTemplates->setEnabled(enable); +} + +/****************************************************************************** +* Refresh the alarm list in every main window instance. +*/ +void MainWindow::refresh() +{ + kdDebug(5950) << "MainWindow::refresh()\n"; + for (WindowList::Iterator it = mWindowList.begin(); it != mWindowList.end(); ++it) + (*it)->mListView->refresh(); +} + +/****************************************************************************** +* Refresh the alarm list in every main window instance which is displaying +* expired alarms. +* Called when an expired alarm setting changes in the user preferences. +*/ +void MainWindow::updateExpired() +{ + kdDebug(5950) << "MainWindow::updateExpired()\n"; + bool enableShowExpired = Preferences::expiredKeepDays(); + for (WindowList::Iterator it = mWindowList.begin(); it != mWindowList.end(); ++it) + { + MainWindow* w = *it; + if (w->mShowExpired) + { + if (!enableShowExpired) + w->slotShowExpired(); + else + w->mListView->refresh(); + } + w->mActionShowExpired->setEnabled(enableShowExpired); + } +} + +/****************************************************************************** +* Start or stop the timer which updates the time-to-alarm values every minute. +* Should be called whenever a main window is created or destroyed, or shown or +* hidden. +*/ +void MainWindow::setUpdateTimer() +{ + // Check whether any windows need to be updated + MainWindow* needTimer = 0; + MainWindow* timerWindow = 0; + for (WindowList::Iterator it = mWindowList.begin(); it != mWindowList.end(); ++it) + { + MainWindow* w = *it; + if (w->isVisible() && w->mListView->showingTimeTo()) + needTimer = w; + if (w->mMinuteTimerActive) + timerWindow = w; + } + + // Start or stop the update timer if necessary + if (needTimer && !timerWindow) + { + // Timeout every minute. + needTimer->mMinuteTimerActive = true; + MinuteTimer::connect(needTimer, SLOT(slotUpdateTimeTo())); + kdDebug(5950) << "MainWindow::setUpdateTimer(): started timer" << endl; + } + else if (!needTimer && timerWindow) + { + timerWindow->mMinuteTimerActive = false; + MinuteTimer::disconnect(timerWindow); + kdDebug(5950) << "MainWindow::setUpdateTimer(): stopped timer" << endl; + } +} +/****************************************************************************** +* Update the time-to-alarm values for each main window which is displaying them. +*/ +void MainWindow::slotUpdateTimeTo() +{ + kdDebug(5950) << "MainWindow::slotUpdateTimeTo()" << endl; + for (WindowList::Iterator it = mWindowList.begin(); it != mWindowList.end(); ++it) + { + MainWindow* w = *it; + if (w->isVisible() && w->mListView->showingTimeTo()) + w->mListView->updateTimeToAlarms(); + } +} + +/****************************************************************************** +* Select an alarm in the displayed list. +*/ +void MainWindow::selectEvent(const QString& eventID) +{ + mListView->clearSelection(); + AlarmListViewItem* item = mListView->getEntry(eventID); + if (item) + { + mListView->setSelected(item, true); + mListView->setCurrentItem(item); + mListView->ensureItemVisible(item); + } +} + +/****************************************************************************** +* Called when the New button is clicked to edit a new alarm to add to the list. +*/ +void MainWindow::slotNew() +{ + executeNew(this); +} + +/****************************************************************************** +* Execute a New Alarm dialog, optionally either presetting it to the supplied +* event, or setting the action and text. +*/ +void MainWindow::executeNew(MainWindow* win, const KAEvent* evnt, KAEvent::Action action, const AlarmText& text) +{ + EditAlarmDlg editDlg(false, i18n("New Alarm"), win, 0, evnt); + if (!text.isEmpty()) + editDlg.setAction(action, text); + if (editDlg.exec() == QDialog::Accepted) + { + KAEvent event; + editDlg.getEvent(event); + + // Add the alarm to the displayed lists and to the calendar file + if (KAlarm::addEvent(event, (win ? win->mListView : 0), &editDlg) == KAlarm::UPDATE_KORG_ERR) + KAlarm::displayKOrgUpdateError(&editDlg, KAlarm::KORG_ERR_ADD, 1); + Undo::saveAdd(event); + + KAlarm::outputAlarmWarnings(&editDlg, &event); + } +} + +/****************************************************************************** +* Called when a template is selected from the New From Template popup menu. +* Executes a New Alarm dialog, preset from the selected template. +*/ +void MainWindow::slotNewFromTemplate(const KAEvent& tmplate) +{ + executeNew(this, &tmplate); +} + +/****************************************************************************** +* Called when the New Template button is clicked to create a new template +* based on the currently selected alarm. +*/ +void MainWindow::slotNewTemplate() +{ + AlarmListViewItem* item = mListView->selectedItem(); + if (item) + { + KAEvent event = item->event(); + TemplateDlg::createTemplate(&event, this); + } +} + +/****************************************************************************** +* Called when the Copy button is clicked to edit a copy of an existing alarm, +* to add to the list. +*/ +void MainWindow::slotCopy() +{ + AlarmListViewItem* item = mListView->selectedItem(); + if (item) + executeNew(this, &item->event()); +} + +/****************************************************************************** +* Called when the Modify button is clicked to edit the currently highlighted +* alarm in the list. +*/ +void MainWindow::slotModify() +{ + AlarmListViewItem* item = mListView->selectedItem(); + if (item) + { + KAEvent event = item->event(); + executeEdit(event, this); + } +} + +/****************************************************************************** +* Open the Edit Alarm dialogue to edit the specified alarm. +*/ +void MainWindow::executeEdit(KAEvent& event, MainWindow* win) +{ + EditAlarmDlg editDlg(false, i18n("Edit Alarm"), win, 0, &event); + if (editDlg.exec() == QDialog::Accepted) + { + KAEvent newEvent; + bool changeDeferral = !editDlg.getEvent(newEvent); + + // Update the event in the displays and in the calendar file + AlarmListView* view = win ? win->mListView : 0; + if (changeDeferral) + { + // The only change has been to an existing deferral + if (KAlarm::updateEvent(newEvent, view, &editDlg, true, false) != KAlarm::UPDATE_OK) // keep the same event ID + return; // failed to save event + } + else + { + if (KAlarm::modifyEvent(event, newEvent, view, &editDlg) == KAlarm::UPDATE_KORG_ERR) + KAlarm::displayKOrgUpdateError(&editDlg, KAlarm::KORG_ERR_MODIFY, 1); + } + Undo::saveEdit(event, newEvent); + + KAlarm::outputAlarmWarnings(&editDlg, &newEvent); + } +} + +/****************************************************************************** +* Called when the View button is clicked to view the currently highlighted +* alarm in the list. +*/ +void MainWindow::slotView() +{ + AlarmListViewItem* item = mListView->selectedItem(); + if (item) + { + KAEvent event = item->event(); + EditAlarmDlg editDlg(false, (event.expired() ? i18n("Expired Alarm") + " [" + i18n("read-only") + ']' + : i18n("View Alarm")), + this, 0, &event, true); + editDlg.exec(); + } +} + +/****************************************************************************** +* Called when the Delete button is clicked to delete the currently highlighted +* alarms in the list. +*/ +void MainWindow::slotDelete() +{ + QValueList<EventListViewItemBase*> items = mListView->selectedItems(); + // Copy the events to be deleted, in case any are deleted by being + // triggered while the confirmation prompt is displayed. + QValueList<KAEvent> events; + QValueList<KAEvent> origEvents; + for (QValueList<EventListViewItemBase*>::Iterator iit = items.begin(); iit != items.end(); ++iit) + { + AlarmListViewItem* item = (AlarmListViewItem*)(*iit); + events.append(item->event()); + origEvents.append(item->event()); + } + if (Preferences::confirmAlarmDeletion()) + { + int n = items.count(); + if (KMessageBox::warningContinueCancel(this, i18n("Do you really want to delete the selected alarm?", + "Do you really want to delete the %n selected alarms?", n), + i18n("Delete Alarm", "Delete Alarms", n), + KGuiItem(i18n("&Delete"), "editdelete"), + Preferences::CONFIRM_ALARM_DELETION) + != KMessageBox::Continue) + return; + } + + int warnErr = 0; + int warnKOrg = 0; + AlarmCalendar::activeCalendar()->startUpdate(); // prevent multiple saves of the calendars until we're finished + AlarmCalendar::expiredCalendar()->startUpdate(); + for (QValueList<KAEvent>::Iterator it = events.begin(); it != events.end(); ++it) + { + // Delete the event from the calendar and displays + switch (KAlarm::deleteEvent(*it)) + { + case KAlarm::UPDATE_ERROR: + case KAlarm::UPDATE_FAILED: + case KAlarm::SAVE_FAILED: + ++warnErr; + break; + case KAlarm::UPDATE_KORG_ERR: + ++warnKOrg; + break; + default: + break; + } + } + if (!AlarmCalendar::activeCalendar()->endUpdate()) // save the calendars now + warnErr = events.count(); + AlarmCalendar::expiredCalendar()->endUpdate(); + Undo::saveDeletes(origEvents); + + if (warnErr) + KAlarm::displayUpdateError(this, KAlarm::UPDATE_FAILED, KAlarm::ERR_DELETE, warnErr); + else if (warnKOrg) + KAlarm::displayKOrgUpdateError(this, KAlarm::KORG_ERR_DELETE, warnKOrg); +} + +/****************************************************************************** +* Called when the Reactivate button is clicked to reinstate the currently +* highlighted expired alarms in the list. +*/ +void MainWindow::slotReactivate() +{ + int warnErr = 0; + int warnKOrg = 0; + QValueList<KAEvent> events; + QValueList<EventListViewItemBase*> items = mListView->selectedItems(); + mListView->clearSelection(); + AlarmCalendar::activeCalendar()->startUpdate(); // prevent multiple saves of the calendars until we're finished + AlarmCalendar::expiredCalendar()->startUpdate(); + for (QValueList<EventListViewItemBase*>::Iterator it = items.begin(); it != items.end(); ++it) + { + // Add the alarm to the displayed lists and to the calendar file + AlarmListViewItem* item = (AlarmListViewItem*)(*it); + KAEvent event = item->event(); + events.append(event); + switch (KAlarm::reactivateEvent(event, mListView, true)) + { + case KAlarm::UPDATE_ERROR: + case KAlarm::UPDATE_FAILED: + case KAlarm::SAVE_FAILED: + ++warnErr; + break; + case KAlarm::UPDATE_KORG_ERR: + ++warnKOrg; + break; + default: + break; + } + } + if (!AlarmCalendar::activeCalendar()->endUpdate()) // save the calendars now + warnErr = items.count(); + AlarmCalendar::expiredCalendar()->endUpdate(); + Undo::saveReactivates(events); + + if (warnErr) + KAlarm::displayUpdateError(this, KAlarm::UPDATE_FAILED, KAlarm::ERR_REACTIVATE, warnErr); + else if (warnKOrg) + KAlarm::displayKOrgUpdateError(this, KAlarm::KORG_ERR_ADD, warnKOrg); +} + +/****************************************************************************** +* Called when the Enable/Disable button is clicked to enable or disable the +* currently highlighted alarms in the list. +*/ +void MainWindow::slotEnable() +{ + bool enable = mActionEnableEnable; // save since changed in response to KAlarm::enableEvent() + int warnErr = 0; + QValueList<EventListViewItemBase*> items = mListView->selectedItems(); + AlarmCalendar::activeCalendar()->startUpdate(); // prevent multiple saves of the calendars until we're finished + for (QValueList<EventListViewItemBase*>::Iterator it = items.begin(); it != items.end(); ++it) + { + AlarmListViewItem* item = (AlarmListViewItem*)(*it); + KAEvent event = item->event(); + + // Enable the alarm in the displayed lists and in the calendar file + if (KAlarm::enableEvent(event, mListView, enable) != KAlarm::UPDATE_OK) + ++warnErr; + } + if (!AlarmCalendar::activeCalendar()->endUpdate()) // save the calendars now + warnErr = items.count(); + if (warnErr) + KAlarm::displayUpdateError(this, KAlarm::UPDATE_FAILED, KAlarm::ERR_ADD, warnErr); +} + +/****************************************************************************** +* Called when the Show Alarm Times menu item is selected or deselected. +*/ +void MainWindow::slotShowTime() +{ + mShowTime = !mShowTime; + mActionShowTime->setChecked(mShowTime); + if (!mShowTime && !mShowTimeTo) + slotShowTimeTo(); // at least one time column must be displayed + else + { + mListView->selectTimeColumns(mShowTime, mShowTimeTo); + KConfig* config = KGlobal::config(); + config->setGroup(VIEW_GROUP); + config->writeEntry(SHOW_TIME_KEY, mShowTime); + config->writeEntry(SHOW_TIME_TO_KEY, mShowTimeTo); + } +} + +/****************************************************************************** +* Called when the Show Time To Alarms menu item is selected or deselected. +*/ +void MainWindow::slotShowTimeTo() +{ + mShowTimeTo = !mShowTimeTo; + mActionShowTimeTo->setChecked(mShowTimeTo); + if (!mShowTimeTo && !mShowTime) + slotShowTime(); // at least one time column must be displayed + else + { + mListView->selectTimeColumns(mShowTime, mShowTimeTo); + KConfig* config = KGlobal::config(); + config->setGroup(VIEW_GROUP); + config->writeEntry(SHOW_TIME_KEY, mShowTime); + config->writeEntry(SHOW_TIME_TO_KEY, mShowTimeTo); + } + setUpdateTimer(); +} + +/****************************************************************************** +* Called when the Show Expired Alarms menu item is selected or deselected. +*/ +void MainWindow::slotShowExpired() +{ + mShowExpired = !mShowExpired; + mActionShowExpired->setChecked(mShowExpired); + mActionShowExpired->setToolTip(mShowExpired ? i18n_HideExpiredAlarms() : i18n_ShowExpiredAlarms()); + mListView->showExpired(mShowExpired); + mListView->refresh(); + KConfig* config = KGlobal::config(); + config->setGroup(VIEW_GROUP); + config->writeEntry(SHOW_ARCHIVED_KEY, mShowExpired); +} + +/****************************************************************************** +* Called when the Import Alarms menu item is selected, to merge alarms from an +* external calendar into the current calendars. +*/ +void MainWindow::slotImportAlarms() +{ + if (AlarmCalendar::importAlarms(this)) + mListView->refresh(); +} + +/****************************************************************************** +* Called when the Import Birthdays menu item is selected, to display birthdays +* from the address book for selection as alarms. +*/ +void MainWindow::slotBirthdays() +{ + BirthdayDlg dlg(this); + if (dlg.exec() == QDialog::Accepted) + { + QValueList<KAEvent> events = dlg.events(); + if (events.count()) + { + mListView->clearSelection(); + int warnErr = 0; + int warnKOrg = 0; + for (QValueList<KAEvent>::Iterator ev = events.begin(); ev != events.end(); ++ev) + { + // Add alarm to the displayed lists and to the calendar file + switch (KAlarm::addEvent(*ev, mListView)) + { + case KAlarm::UPDATE_ERROR: + case KAlarm::UPDATE_FAILED: + case KAlarm::SAVE_FAILED: + ++warnErr; + break; + case KAlarm::UPDATE_KORG_ERR: + ++warnKOrg; + break; + default: + break; + } + } + if (warnErr) + KAlarm::displayUpdateError(this, KAlarm::UPDATE_FAILED, KAlarm::ERR_ADD, warnErr); + else if (warnKOrg) + KAlarm::displayKOrgUpdateError(this, KAlarm::KORG_ERR_ADD, warnKOrg); + KAlarm::outputAlarmWarnings(&dlg); + } + } +} + +/****************************************************************************** +* Called when the Templates menu item is selected, to display the alarm +* template editing dialogue. +*/ +void MainWindow::slotTemplates() +{ + if (!mTemplateDlg) + { + mTemplateDlg = TemplateDlg::create(this); + enableTemplateMenuItem(false); // disable menu item in all windows + connect(mTemplateDlg, SIGNAL(finished()), SLOT(slotTemplatesEnd())); + mTemplateDlg->show(); + } +} + +/****************************************************************************** +* Called when the alarm template editing dialogue has exited. +*/ +void MainWindow::slotTemplatesEnd() +{ + if (mTemplateDlg) + { + mTemplateDlg->delayedDestruct(); // this deletes the dialogue once it is safe to do so + mTemplateDlg = 0; + enableTemplateMenuItem(true); // re-enable menu item in all windows + } +} + +/****************************************************************************** +* Called when the Display System Tray Icon menu item is selected. +*/ +void MainWindow::slotToggleTrayIcon() +{ + theApp()->displayTrayIcon(!theApp()->trayIconDisplayed(), this); +} + +/****************************************************************************** +* Called when the user preferences have changed. +*/ +void MainWindow::slotPrefsChanged() +{ + mActionShowExpired->setEnabled(Preferences::expiredKeepDays()); + updateTrayIconAction(); +} + +/****************************************************************************** +* Called when the system tray icon is created or destroyed. +* Set the system tray icon menu text according to whether or not the system +* tray icon is currently visible. +*/ +void MainWindow::updateTrayIconAction() +{ + mActionToggleTrayIcon->setEnabled(theApp()->haveSystemTray() && !theApp()->wantRunInSystemTray()); + mActionToggleTrayIcon->setChecked(theApp()->trayIconDisplayed()); +} + +/****************************************************************************** +* Called when the Actions menu is about to be displayed. +* Update the status of the Alarms Enabled menu item. +*/ +void MainWindow::updateActionsMenu() +{ + Daemon::checkStatus(); // update the Alarms Enabled item status +} + +/****************************************************************************** +* Called when the active status of Find changes. +*/ +void MainWindow::slotFindActive(bool active) +{ + mActionFindNext->setEnabled(active); + mActionFindPrev->setEnabled(active); +} + +/****************************************************************************** +* Called when the Undo action is selected. +*/ +void MainWindow::slotUndo() +{ + Undo::undo(this, KAlarm::stripAccel(mActionUndo->text())); +} + +/****************************************************************************** +* Called when the Redo action is selected. +*/ +void MainWindow::slotRedo() +{ + Undo::redo(this, KAlarm::stripAccel(mActionRedo->text())); +} + +/****************************************************************************** +* Called when an Undo item is selected. +*/ +void MainWindow::slotUndoItem(int id) +{ + Undo::undo(id, this, Undo::actionText(Undo::UNDO, id)); +} + +/****************************************************************************** +* Called when a Redo item is selected. +*/ +void MainWindow::slotRedoItem(int id) +{ + Undo::redo(id, this, Undo::actionText(Undo::REDO, id)); +} + +/****************************************************************************** +* Called when the Undo menu is about to show. +* Populates the menu. +*/ +void MainWindow::slotInitUndoMenu() +{ + initUndoMenu(mActionUndo->popupMenu(), Undo::UNDO); +} + +/****************************************************************************** +* Called when the Redo menu is about to show. +* Populates the menu. +*/ +void MainWindow::slotInitRedoMenu() +{ + initUndoMenu(mActionRedo->popupMenu(), Undo::REDO); +} + +/****************************************************************************** +* Populate the undo or redo menu. +*/ +void MainWindow::initUndoMenu(KPopupMenu* menu, Undo::Type type) +{ + menu->clear(); + const QString& action = (type == Undo::UNDO) ? undoTextStripped : redoTextStripped; + QValueList<int> ids = Undo::ids(type); + for (QValueList<int>::ConstIterator it = ids.begin(); it != ids.end(); ++it) + { + int id = *it; + QString actText = Undo::actionText(type, id); + QString descrip = Undo::description(type, id); + QString text = descrip.isEmpty() + ? i18n("Undo/Redo [action]", "%1 %2").arg(action).arg(actText) + : i18n("Undo [action]: message", "%1 %2: %3").arg(action).arg(actText).arg(descrip); + menu->insertItem(text, id); + } +} + +/****************************************************************************** +* Called when the status of the Undo or Redo list changes. +* Change the Undo or Redo text to include the action which would be undone/redone. +*/ +void MainWindow::slotUndoStatus(const QString& undo, const QString& redo) +{ + if (undo.isNull()) + { + mActionUndo->setEnabled(false); + mActionUndo->setText(undoText); + } + else + { + mActionUndo->setEnabled(true); + mActionUndo->setText(QString("%1 %2").arg(undoText).arg(undo)); + } + if (redo.isNull()) + { + mActionRedo->setEnabled(false); + mActionRedo->setText(redoText); + } + else + { + mActionRedo->setEnabled(true); + mActionRedo->setText(QString("%1 %2").arg(redoText).arg(redo)); + } +} + +/****************************************************************************** +* Called when the Reset Daemon menu item is selected. +*/ +void MainWindow::slotResetDaemon() +{ + KAlarm::resetDaemon(); +} + +/****************************************************************************** +* Called when the "Configure KAlarm" menu item is selected. +*/ +void MainWindow::slotPreferences() +{ + KAlarmPrefDlg::display(); +} + +/****************************************************************************** +* Called when the Configure Keys menu item is selected. +*/ +void MainWindow::slotConfigureKeys() +{ + KKeyDialog::configure(actionCollection(), this); +} + +/****************************************************************************** +* Called when the Configure Toolbars menu item is selected. +*/ +void MainWindow::slotConfigureToolbar() +{ + saveMainWindowSettings(KGlobal::config(), WINDOW_NAME); + KEditToolbar dlg(factory()); + connect(&dlg, SIGNAL(newToolbarConfig()), this, SLOT(slotNewToolbarConfig())); + dlg.exec(); +} + +/****************************************************************************** +* Called when OK or Apply is clicked in the Configure Toolbars dialog, to save +* the new configuration. +*/ +void MainWindow::slotNewToolbarConfig() +{ + createGUI(UI_FILE); + applyMainWindowSettings(KGlobal::config(), WINDOW_NAME); +} + +/****************************************************************************** +* Called when the Quit menu item is selected. +*/ +void MainWindow::slotQuit() +{ + theApp()->doQuit(this); +} + +/****************************************************************************** +* Called when the user or the session manager attempts to close the window. +*/ +void MainWindow::closeEvent(QCloseEvent* ce) +{ + if (!theApp()->sessionClosingDown() && isTrayParent()) + { + // The user (not the session manager) wants to close the window. + // It's the parent window of the system tray icon, so just hide + // it to prevent the system tray icon closing. + hide(); + theApp()->quitIf(); + ce->ignore(); + } + else + ce->accept(); +} + +/****************************************************************************** +* Called when an item is deleted from the ListView. +* Disables the actions if no item is still selected. +*/ +void MainWindow::slotDeletion() +{ + if (!mListView->selectedCount()) + { + kdDebug(5950) << "MainWindow::slotDeletion(true)\n"; + mActionCreateTemplate->setEnabled(false); + mActionCopy->setEnabled(false); + mActionModify->setEnabled(false); + mActionView->setEnabled(false); + mActionDelete->setEnabled(false); + mActionReactivate->setEnabled(false); + mActionEnable->setEnabled(false); + } +} + +/****************************************************************************** +* Called when the drag cursor enters the window. +*/ +void MainWindow::dragEnterEvent(QDragEnterEvent* e) +{ + executeDragEnterEvent(e); +} + +/****************************************************************************** +* Called when the drag cursor enters a main or system tray window, to accept +* or reject the dragged object. +*/ +void MainWindow::executeDragEnterEvent(QDragEnterEvent* e) +{ + if (KCal::ICalDrag::canDecode(e)) + e->accept(!AlarmListView::dragging()); // don't accept "text/calendar" objects from KAlarm + else + e->accept(QTextDrag::canDecode(e) + || KURLDrag::canDecode(e) + || KPIM::MailListDrag::canDecode(e)); +} + +/****************************************************************************** +* Called when an object is dropped on the window. +* If the object is recognised, the edit alarm dialog is opened appropriately. +*/ +void MainWindow::dropEvent(QDropEvent* e) +{ + executeDropEvent(this, e); +} + +static QString getMailHeader(const char* header, KMime::Content& content) +{ + KMime::Headers::Base* hd = content.getHeaderByType(header); + return hd ? hd->asUnicodeString() : QString::null; +} + +/****************************************************************************** +* Called when an object is dropped on a main or system tray window, to +* evaluate the action required and extract the text. +*/ +void MainWindow::executeDropEvent(MainWindow* win, QDropEvent* e) +{ + KAEvent::Action action = KAEvent::MESSAGE; + QString text; + QByteArray bytes; + AlarmText alarmText; + KPIM::MailList mailList; + KURL::List files; + KCal::CalendarLocal calendar(QString::fromLatin1("UTC")); + calendar.setLocalTime(); // default to local time (i.e. no time zone) +#ifndef NDEBUG + QCString fmts; + for (int idbg = 0; e->format(idbg); ++idbg) + { + if (idbg) fmts += ", "; + fmts += e->format(idbg); + } + kdDebug(5950) << "MainWindow::executeDropEvent(): " << fmts << endl; +#endif + + /* The order of the tests below matters, since some dropped objects + * provide more than one mime type. + * Don't change them without careful thought !! + */ + if (e->provides("message/rfc822") + && !(bytes = e->encodedData("message/rfc822")).isEmpty()) + { + // Email message(s). Ignore all but the first. + kdDebug(5950) << "MainWindow::executeDropEvent(email)" << endl; + QCString mails(bytes.data(), bytes.size()); + KMime::Content content; + content.setContent(mails); + content.parse(); + QString body; + if (content.textContent()) + content.textContent()->decodedText(body, true, true); // strip trailing newlines & spaces + unsigned long sernum = 0; + if (e->provides(KPIM::MailListDrag::format()) + && KPIM::MailListDrag::decode(e, mailList) + && mailList.count()) + { + // Get its KMail serial number to allow the KMail message + // to be called up from the alarm message window. + sernum = mailList.first().serialNumber(); + } + alarmText.setEmail(getMailHeader("To", content), + getMailHeader("From", content), + getMailHeader("Cc", content), + getMailHeader("Date", content), + getMailHeader("Subject", content), + body, sernum); + } + else if (KURLDrag::decode(e, files) && files.count()) + { + kdDebug(5950) << "MainWindow::executeDropEvent(URL)" << endl; + action = KAEvent::FILE; + alarmText.setText(files.first().prettyURL()); + } + else if (e->provides(KPIM::MailListDrag::format()) + && KPIM::MailListDrag::decode(e, mailList)) + { + // KMail message(s). Ignore all but the first. + kdDebug(5950) << "MainWindow::executeDropEvent(KMail_list)" << endl; + if (!mailList.count()) + return; + KPIM::MailSummary& summary = mailList.first(); + QDateTime dt; + dt.setTime_t(summary.date()); + QString body = KAMail::getMailBody(summary.serialNumber()); + alarmText.setEmail(summary.to(), summary.from(), QString::null, + KGlobal::locale()->formatDateTime(dt), summary.subject(), + body, summary.serialNumber()); + } + else if (KCal::ICalDrag::decode(e, &calendar)) + { + // iCalendar - ignore all but the first event + kdDebug(5950) << "MainWindow::executeDropEvent(iCalendar)" << endl; + KCal::Event::List events = calendar.rawEvents(); + if (!events.isEmpty()) + { + KAEvent ev(*events.first()); + executeNew(win, &ev); + } + return; + } + else if (QTextDrag::decode(e, text)) + { + kdDebug(5950) << "MainWindow::executeDropEvent(text)" << endl; + alarmText.setText(text); + } + else + return; + + if (!alarmText.isEmpty()) + executeNew(win, 0, action, alarmText); +} + +/****************************************************************************** +* Called when the selected items in the ListView changes. +* Selects the new current item, and enables the actions appropriately. +*/ +void MainWindow::slotSelection() +{ + // Find which item has been selected, and whether more than one is selected + QValueList<EventListViewItemBase*> items = mListView->selectedItems(); + int count = items.count(); + AlarmListViewItem* item = (AlarmListViewItem*)((count == 1) ? items.first() : 0); + bool enableReactivate = true; + bool enableEnableDisable = true; + bool enableEnable = false; + bool enableDisable = false; + QDateTime now = QDateTime::currentDateTime(); + for (QValueList<EventListViewItemBase*>::Iterator it = items.begin(); it != items.end(); ++it) + { + const KAEvent& event = ((AlarmListViewItem*)(*it))->event(); + if (enableReactivate + && (!event.expired() || !event.occursAfter(now, true))) + enableReactivate = false; + if (enableEnableDisable) + { + if (event.expired()) + enableEnableDisable = enableEnable = enableDisable = false; + else + { + if (!enableEnable && !event.enabled()) + enableEnable = true; + if (!enableDisable && event.enabled()) + enableDisable = true; + } + } + } + + kdDebug(5950) << "MainWindow::slotSelection(true)\n"; + mActionCreateTemplate->setEnabled(count == 1); + mActionCopy->setEnabled(count == 1); + mActionModify->setEnabled(item && !mListView->expired(item)); + mActionView->setEnabled(count == 1); + mActionDelete->setEnabled(count); + mActionReactivate->setEnabled(count && enableReactivate); + mActionEnable->setEnabled(enableEnable || enableDisable); + if (enableEnable || enableDisable) + setEnableText(enableEnable); +} + +/****************************************************************************** +* Called when a context menu is requested either by a mouse click or by a +* key press. +*/ +void MainWindow::slotContextMenuRequested(QListViewItem* item, const QPoint& pt, int) +{ + kdDebug(5950) << "MainWindow::slotContextMenuRequested()" << endl; + if (mContextMenu) + mContextMenu->popup(pt); +} + +/****************************************************************************** +* Called when the mouse is clicked on the ListView. +* Deselects the current item and disables the actions if appropriate. +* Note that if a right button click is handled by slotContextMenuRequested(). +*/ +void MainWindow::slotMouseClicked(int button, QListViewItem* item, const QPoint& pt, int) +{ + if (button != Qt::RightButton && !item) + { + kdDebug(5950) << "MainWindow::slotMouseClicked(left)" << endl; + mListView->clearSelection(); + mActionCreateTemplate->setEnabled(false); + mActionCopy->setEnabled(false); + mActionModify->setEnabled(false); + mActionView->setEnabled(false); + mActionDelete->setEnabled(false); + mActionReactivate->setEnabled(false); + mActionEnable->setEnabled(false); + } +} + +/****************************************************************************** +* Called when the mouse is double clicked on the ListView. +* Displays the Edit Alarm dialog, for the clicked item if applicable. +*/ +void MainWindow::slotDoubleClicked(QListViewItem* item) +{ + kdDebug(5950) << "MainWindow::slotDoubleClicked()\n"; + if (item) + { + if (mListView->expired((AlarmListViewItem*)item)) + slotView(); + else + slotModify(); + } + else + slotNew(); +} + +/****************************************************************************** +* Set the text of the Enable/Disable menu action. +*/ +void MainWindow::setEnableText(bool enable) +{ + mActionEnableEnable = enable; + mActionEnable->setText(enable ? i18n("Ena&ble") : i18n("Disa&ble")); +} + +/****************************************************************************** +* Display or hide the specified main window. +* This should only be called when the application doesn't run in the system tray. +*/ +MainWindow* MainWindow::toggleWindow(MainWindow* win) +{ + if (win && mWindowList.find(win) != mWindowList.end()) + { + // A window is specified (and it exists) + if (win->isVisible()) + { + // The window is visible, so close it + win->close(); + return 0; + } + else + { + // The window is hidden, so display it + win->hide(); // in case it's on a different desktop + win->showNormal(); + win->raise(); + win->setActiveWindow(); + return win; + } + } + + // No window is specified, or the window doesn't exist. Open a new one. + win = create(); + win->show(); + return win; +} diff --git a/kalarm/mainwindow.h b/kalarm/mainwindow.h new file mode 100644 index 000000000..7a11b8b86 --- /dev/null +++ b/kalarm/mainwindow.h @@ -0,0 +1,182 @@ +/* + * mainwindow.h - main application window + * Program: kalarm + * Copyright © 2001-2007 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +/** @file mainwindow.h - main application window */ + +#include "alarmevent.h" +#include "alarmtext.h" +#include "mainwindowbase.h" +#include "undo.h" + +class QListViewItem; +class KAction; +class KToggleAction; +class KToolBarPopupAction; +class KPopupMenu; +class ActionAlarmsEnabled; +class AlarmListView; +class TemplateDlg; +class TemplateMenuAction; + + +class MainWindow : public MainWindowBase +{ + Q_OBJECT + + public: + static MainWindow* create(bool restored = false); + ~MainWindow(); + bool isTrayParent() const; + bool isHiddenTrayParent() const { return mHiddenTrayParent; } + bool showingExpired() const { return mShowExpired; } + void selectEvent(const QString& eventID); + + static void refresh(); + static void updateExpired(); + static void addEvent(const KAEvent&, MainWindow*); + static void executeNew(MainWindow* w = 0, KAEvent::Action a = KAEvent::MESSAGE, const AlarmText& t = AlarmText()) + { executeNew(w, 0, a, t); } + static void executeNew(const KAEvent& e, MainWindow* w = 0) + { executeNew(w, &e); } + static void executeEdit(KAEvent&, MainWindow* = 0); + static void executeDragEnterEvent(QDragEnterEvent*); + static void executeDropEvent(MainWindow*, QDropEvent*); + static void closeAll(); + static MainWindow* toggleWindow(MainWindow*); + static MainWindow* mainMainWindow(); + static MainWindow* firstWindow() { return mWindowList.first(); } + static int count() { return mWindowList.count(); } + + static QString i18n_a_ShowAlarmTimes(); // text of 'Show Alarm Times' checkbox, with 'A' shortcut + static QString i18n_m_ShowAlarmTime(); // text of 'Show alarm time' checkbox, with 'M' shortcut + static QString i18n_o_ShowTimeToAlarms(); // text of 'Show Time to Alarms' checkbox, with 'O' shortcut + static QString i18n_l_ShowTimeToAlarm(); // text of 'Show time until alarm' checkbox, with 'L' shortcut + static QString i18n_ShowExpiredAlarms(); // plain text of 'Show Expired Alarms' action + static QString i18n_e_ShowExpiredAlarms(); // text of 'Show Expired Alarms' checkbox, with 'E' shortcut + static QString i18n_HideExpiredAlarms(); // plain text of 'Hide Expired Alarms' action + static QString i18n_e_HideExpiredAlarms(); // text of 'Hide Expired Alarms' action, with 'E' shortcut + + public slots: + virtual void show(); + + protected: + virtual void resizeEvent(QResizeEvent*); + virtual void showEvent(QShowEvent*); + virtual void hideEvent(QHideEvent*); + virtual void closeEvent(QCloseEvent*); + virtual void dragEnterEvent(QDragEnterEvent*); + virtual void dropEvent(QDropEvent*); + virtual void saveProperties(KConfig*); + virtual void readProperties(KConfig*); + + private slots: + void slotNew(); + void slotNewFromTemplate(const KAEvent&); + void slotNewTemplate(); + void slotCopy(); + void slotModify(); + void slotDelete(); + void slotReactivate(); + void slotView(); + void slotEnable(); + void slotToggleTrayIcon(); + void slotResetDaemon(); + void slotImportAlarms(); + void slotBirthdays(); + void slotTemplates(); + void slotTemplatesEnd(); + void slotPreferences(); + void slotConfigureKeys(); + void slotConfigureToolbar(); + void slotNewToolbarConfig(); + void slotQuit(); + void slotDeletion(); + void slotSelection(); + void slotContextMenuRequested(QListViewItem*, const QPoint&, int); + void slotMouseClicked(int button, QListViewItem*, const QPoint&, int); + void slotDoubleClicked(QListViewItem*); + void slotShowTime(); + void slotShowTimeTo(); + void slotShowExpired(); + void slotUpdateTimeTo(); + void slotUndo(); + void slotUndoItem(int id); + void slotRedo(); + void slotRedoItem(int id); + void slotInitUndoMenu(); + void slotInitRedoMenu(); + void slotUndoStatus(const QString&, const QString&); + void slotFindActive(bool); + void slotPrefsChanged(); + void updateTrayIconAction(); + void updateActionsMenu(); + void columnsReordered(); + + private: + typedef QValueList<MainWindow*> WindowList; + + MainWindow(bool restored); + void createListView(bool recreate); + void initActions(); + void setEnableText(bool enable); + static KAEvent::Action getDropAction(QDropEvent*, QString& text); + static void executeNew(MainWindow*, const KAEvent*, KAEvent::Action = KAEvent::MESSAGE, const AlarmText& = AlarmText()); + static void initUndoMenu(KPopupMenu*, Undo::Type); + static void setUpdateTimer(); + static void enableTemplateMenuItem(bool); + + static WindowList mWindowList; // active main windows + static TemplateDlg* mTemplateDlg; // the one and only template dialogue + + AlarmListView* mListView; + KAction* mActionTemplates; + KAction* mActionNew; + TemplateMenuAction* mActionNewFromTemplate; + KAction* mActionCreateTemplate; + KAction* mActionCopy; + KAction* mActionModify; + KAction* mActionView; + KAction* mActionDelete; + KAction* mActionReactivate; + KAction* mActionEnable; + KAction* mActionFindNext; + KAction* mActionFindPrev; + KToolBarPopupAction* mActionUndo; + KToolBarPopupAction* mActionRedo; + KToggleAction* mActionToggleTrayIcon; + KToggleAction* mActionShowTime; + KToggleAction* mActionShowTimeTo; + KToggleAction* mActionShowExpired; + KPopupMenu* mActionsMenu; + KPopupMenu* mContextMenu; + bool mMinuteTimerActive; // minute timer is active + bool mHiddenTrayParent; // on session restoration, hide this window + bool mShowExpired; // include expired alarms in the displayed list + bool mShowTime; // show alarm times + bool mShowTimeTo; // show time-to-alarms + bool mActionEnableEnable; // Enable/Disable action is set to "Enable" + bool mMenuError; // error occurred creating menus: need to show error message +}; + +#endif // MAINWINDOW_H + diff --git a/kalarm/mainwindowbase.cpp b/kalarm/mainwindowbase.cpp new file mode 100644 index 000000000..2f0cfa16b --- /dev/null +++ b/kalarm/mainwindowbase.cpp @@ -0,0 +1,50 @@ +/* + * mainwindowbase.cpp - base class for main application windows + * Program: kalarm + * Copyright (C) 2002, 2003 by David Jarvie software@astrojar.org.uk + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" +#include <kdeversion.h> +#include "kalarmapp.h" +#include "mainwindowbase.moc" + + +/****************************************************************************** +* Called when a close event is received. +* Only quits the application if there is no system tray icon displayed. +*/ +void MainWindowBase::closeEvent(QCloseEvent* ce) +{ + disableQuit = theApp()->trayIconDisplayed(); + KMainWindow::closeEvent(ce); + disableQuit = false; + ce->accept(); // allow window to close even if it's the last main window +} + +/****************************************************************************** +* Called when the window is being closed. +* Returns true if the application should quit. +*/ +bool MainWindowBase::queryExit() +{ +#if KDE_IS_VERSION(3,1,90) + if (kapp->sessionSaving()) + return true; +#endif + return disableQuit ? false : KMainWindow::queryExit(); +} diff --git a/kalarm/mainwindowbase.h b/kalarm/mainwindowbase.h new file mode 100644 index 000000000..32e339738 --- /dev/null +++ b/kalarm/mainwindowbase.h @@ -0,0 +1,51 @@ +/* + * mainwindowbase.h - base class for main application windows + * Program: kalarm + * Copyright © 2002 by David Jarvie software@astrojar.org.uk + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef MAINWINDOWBASE_H +#define MAINWINDOWBASE_H + +#include <kmainwindow.h> + + +/** + * The MainWindowBase class is a base class for KAlarm's main window and message window. + * When the window is closed, it only allows the application to quit if there is no + * system tray icon displayed. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class MainWindowBase : public KMainWindow +{ + Q_OBJECT + + public: + explicit MainWindowBase(QWidget* parent = 0, const char* name = 0, WFlags f = WType_TopLevel | WDestructiveClose) + : KMainWindow(parent, name, f), disableQuit(false) { } + + protected: + virtual void closeEvent(QCloseEvent*); + virtual bool queryExit(); + + private: + bool disableQuit; // allow the application to quit +}; + +#endif // MAINWINDOWBASE_H + diff --git a/kalarm/messagewin.cpp b/kalarm/messagewin.cpp new file mode 100644 index 000000000..acee6b9b3 --- /dev/null +++ b/kalarm/messagewin.cpp @@ -0,0 +1,1727 @@ +/* + * messagewin.cpp - displays an alarm message + * Program: kalarm + * Copyright © 2001-2009 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <stdlib.h> +#include <string.h> + +#include <qfile.h> +#include <qfileinfo.h> +#include <qlayout.h> +#include <qpushbutton.h> +#include <qlabel.h> +#include <qwhatsthis.h> +#include <qtooltip.h> +#include <qdragobject.h> +#include <qtextedit.h> +#include <qtimer.h> + +#include <kstandarddirs.h> +#include <kaction.h> +#include <kstdguiitem.h> +#include <kaboutdata.h> +#include <klocale.h> +#include <kconfig.h> +#include <kiconloader.h> +#include <kdialog.h> +#include <ktextbrowser.h> +#include <kglobalsettings.h> +#include <kmimetype.h> +#include <kmessagebox.h> +#include <kwin.h> +#include <kwinmodule.h> +#include <kprocess.h> +#include <kio/netaccess.h> +#include <knotifyclient.h> +#include <kpushbutton.h> +#ifdef WITHOUT_ARTS +#include <kaudioplayer.h> +#else +#include <arts/kartsdispatcher.h> +#include <arts/kartsserver.h> +#include <arts/kplayobjectfactory.h> +#include <arts/kplayobject.h> +#endif +#include <dcopclient.h> +#include <kdebug.h> + +#include "alarmcalendar.h" +#include "deferdlg.h" +#include "editdlg.h" +#include "functions.h" +#include "kalarmapp.h" +#include "mainwindow.h" +#include "preferences.h" +#include "synchtimer.h" +#include "messagewin.moc" + +using namespace KCal; + +#ifndef WITHOUT_ARTS +static const char* KMIX_APP_NAME = "kmix"; +static const char* KMIX_DCOP_OBJECT = "Mixer0"; +static const char* KMIX_DCOP_WINDOW = "kmix-mainwindow#1"; +#endif +static const char* KMAIL_DCOP_OBJECT = "KMailIface"; + +// The delay for enabling message window buttons if a zero delay is +// configured, i.e. the windows are placed far from the cursor. +static const int proximityButtonDelay = 1000; // (milliseconds) +static const int proximityMultiple = 10; // multiple of button height distance from cursor for proximity + +static bool wantModal(); + +// A text label widget which can be scrolled and copied with the mouse +class MessageText : public QTextEdit +{ + public: + MessageText(const QString& text, const QString& context = QString::null, QWidget* parent = 0, const char* name = 0) + : QTextEdit(text, context, parent, name) + { + setReadOnly(true); + setWordWrap(QTextEdit::NoWrap); + } + int scrollBarHeight() const { return horizontalScrollBar()->height(); } + int scrollBarWidth() const { return verticalScrollBar()->width(); } + virtual QSize sizeHint() const { return QSize(contentsWidth() + scrollBarWidth(), contentsHeight() + scrollBarHeight()); } +}; + + +class MWMimeSourceFactory : public QMimeSourceFactory +{ + public: + MWMimeSourceFactory(const QString& absPath, KTextBrowser*); + virtual ~MWMimeSourceFactory(); + virtual const QMimeSource* data(const QString& abs_name) const; + private: + // Prohibit the following methods + virtual void setData(const QString&, QMimeSource*) {} + virtual void setExtensionType(const QString&, const char*) {} + + QString mTextFile; + QCString mMimeType; + mutable const QMimeSource* mLast; +}; + + +// Basic flags for the window +static const Qt::WFlags WFLAGS = Qt::WStyle_StaysOnTop | Qt::WDestructiveClose; + +// Error message bit masks +enum { + ErrMsg_Speak = 0x01, + ErrMsg_AudioFile = 0x02, + ErrMsg_Volume = 0x04 +}; + + +QValueList<MessageWin*> MessageWin::mWindowList; +QMap<QString, unsigned> MessageWin::mErrorMessages; + + +/****************************************************************************** +* Construct the message window for the specified alarm. +* Other alarms in the supplied event may have been updated by the caller, so +* the whole event needs to be stored for updating the calendar file when it is +* displayed. +*/ +MessageWin::MessageWin(const KAEvent& event, const KAAlarm& alarm, bool reschedule_event, bool allowDefer) + : MainWindowBase(0, "MessageWin", WFLAGS | Qt::WGroupLeader | Qt::WStyle_ContextHelp + | (wantModal() ? 0 : Qt::WX11BypassWM)), + mMessage(event.cleanText()), + mFont(event.font()), + mBgColour(event.bgColour()), + mFgColour(event.fgColour()), + mDateTime((alarm.type() & KAAlarm::REMINDER_ALARM) ? event.mainDateTime(true) : alarm.dateTime(true)), + mEventID(event.id()), + mAudioFile(event.audioFile()), + mVolume(event.soundVolume()), + mFadeVolume(event.fadeVolume()), + mFadeSeconds(QMIN(event.fadeSeconds(), 86400)), + mDefaultDeferMinutes(event.deferDefaultMinutes()), + mAlarmType(alarm.type()), + mAction(event.action()), + mKMailSerialNumber(event.kmailSerialNumber()), + mRestoreHeight(0), + mAudioRepeat(event.repeatSound()), + mConfirmAck(event.confirmAck()), + mShowEdit(!mEventID.isEmpty()), + mNoDefer(!allowDefer || alarm.repeatAtLogin()), + mInvalid(false), + mArtsDispatcher(0), + mPlayObject(0), + mOldVolume(-1), + mEvent(event), + mEditButton(0), + mDeferButton(0), + mSilenceButton(0), + mDeferDlg(0), + mWinModule(0), + mFlags(event.flags()), + mLateCancel(event.lateCancel()), + mErrorWindow(false), + mNoPostAction(alarm.type() & KAAlarm::REMINDER_ALARM), + mRecreating(false), + mBeep(event.beep()), + mSpeak(event.speak()), + mRescheduleEvent(reschedule_event), + mShown(false), + mPositioning(false), + mNoCloseConfirm(false), + mDisableDeferral(false) +{ + kdDebug(5950) << "MessageWin::MessageWin(event)" << endl; + // Set to save settings automatically, but don't save window size. + // File alarm window size is saved elsewhere. + setAutoSaveSettings(QString::fromLatin1("MessageWin"), false); + initView(); + mWindowList.append(this); + if (event.autoClose()) + mCloseTime = alarm.dateTime().dateTime().addSecs(event.lateCancel() * 60); +} + +/****************************************************************************** +* Construct the message window for a specified error message. +*/ +MessageWin::MessageWin(const KAEvent& event, const DateTime& alarmDateTime, const QStringList& errmsgs) + : MainWindowBase(0, "MessageWin", WFLAGS | Qt::WGroupLeader | Qt::WStyle_ContextHelp), + mMessage(event.cleanText()), + mDateTime(alarmDateTime), + mEventID(event.id()), + mAlarmType(KAAlarm::MAIN_ALARM), + mAction(event.action()), + mKMailSerialNumber(0), + mErrorMsgs(errmsgs), + mRestoreHeight(0), + mConfirmAck(false), + mShowEdit(false), + mNoDefer(true), + mInvalid(false), + mArtsDispatcher(0), + mPlayObject(0), + mEvent(event), + mEditButton(0), + mDeferButton(0), + mSilenceButton(0), + mDeferDlg(0), + mWinModule(0), + mErrorWindow(true), + mNoPostAction(true), + mRecreating(false), + mRescheduleEvent(false), + mShown(false), + mPositioning(false), + mNoCloseConfirm(false), + mDisableDeferral(false) +{ + kdDebug(5950) << "MessageWin::MessageWin(errmsg)" << endl; + initView(); + mWindowList.append(this); +} + +/****************************************************************************** +* Construct the message window for restoration by session management. +* The window is initialised by readProperties(). +*/ +MessageWin::MessageWin() + : MainWindowBase(0, "MessageWin", WFLAGS), + mArtsDispatcher(0), + mPlayObject(0), + mEditButton(0), + mDeferButton(0), + mSilenceButton(0), + mDeferDlg(0), + mWinModule(0), + mErrorWindow(false), + mRecreating(false), + mRescheduleEvent(false), + mShown(false), + mPositioning(false), + mNoCloseConfirm(false), + mDisableDeferral(false) +{ + kdDebug(5950) << "MessageWin::MessageWin(restore)\n"; + mWindowList.append(this); +} + +/****************************************************************************** +* Destructor. Perform any post-alarm actions before tidying up. +*/ +MessageWin::~MessageWin() +{ + kdDebug(5950) << "MessageWin::~MessageWin(" << mEventID << ")" << endl; + stopPlay(); + delete mWinModule; + mWinModule = 0; + mErrorMessages.remove(mEventID); + mWindowList.remove(this); + if (!mRecreating) + { + if (!mNoPostAction && !mEvent.postAction().isEmpty()) + theApp()->alarmCompleted(mEvent); + if (!mWindowList.count()) + theApp()->quitIf(); + } +} + +/****************************************************************************** +* Construct the message window. +*/ +void MessageWin::initView() +{ + bool reminder = (!mErrorWindow && (mAlarmType & KAAlarm::REMINDER_ALARM)); + int leading = fontMetrics().leading(); + setCaption((mAlarmType & KAAlarm::REMINDER_ALARM) ? i18n("Reminder") : i18n("Message")); + QWidget* topWidget = new QWidget(this, "messageWinTop"); + setCentralWidget(topWidget); + QVBoxLayout* topLayout = new QVBoxLayout(topWidget, KDialog::marginHint(), KDialog::spacingHint()); + + if (mDateTime.isValid()) + { + // Show the alarm date/time, together with an "Advance reminder" text where appropriate + QFrame* frame = 0; + QVBoxLayout* layout = topLayout; + if (reminder) + { + frame = new QFrame(topWidget); + frame->setFrameStyle(QFrame::Box | QFrame::Raised); + topLayout->addWidget(frame, 0, Qt::AlignHCenter); + layout = new QVBoxLayout(frame, leading + frame->frameWidth(), leading); + } + + // Alarm date/time + QLabel* label = new QLabel(frame ? frame : topWidget); + label->setText(mDateTime.isDateOnly() + ? KGlobal::locale()->formatDate(mDateTime.date(), true) + : KGlobal::locale()->formatDateTime(mDateTime.dateTime())); + if (!frame) + label->setFrameStyle(QFrame::Box | QFrame::Raised); + label->setFixedSize(label->sizeHint()); + layout->addWidget(label, 0, Qt::AlignHCenter); + QWhatsThis::add(label, + i18n("The scheduled date/time for the message (as opposed to the actual time of display).")); + + if (frame) + { + label = new QLabel(frame); + label->setText(i18n("Reminder")); + label->setFixedSize(label->sizeHint()); + layout->addWidget(label, 0, Qt::AlignHCenter); + frame->setFixedSize(frame->sizeHint()); + } + } + + if (!mErrorWindow) + { + // It's a normal alarm message window + switch (mAction) + { + case KAEvent::FILE: + { + // Display the file name + QLabel* label = new QLabel(mMessage, topWidget); + label->setFrameStyle(QFrame::Box | QFrame::Raised); + label->setFixedSize(label->sizeHint()); + QWhatsThis::add(label, i18n("The file whose contents are displayed below")); + topLayout->addWidget(label, 0, Qt::AlignHCenter); + + // Display contents of file + bool opened = false; + bool dir = false; + QString tmpFile; + KURL url(mMessage); + if (KIO::NetAccess::download(url, tmpFile, MainWindow::mainMainWindow())) + { + QFile qfile(tmpFile); + QFileInfo info(qfile); + if (!(dir = info.isDir())) + { + opened = true; + KTextBrowser* view = new KTextBrowser(topWidget, "fileContents"); + MWMimeSourceFactory msf(tmpFile, view); + view->setMinimumSize(view->sizeHint()); + topLayout->addWidget(view); + + // Set the default size to 20 lines square. + // Note that after the first file has been displayed, this size + // is overridden by the user-set default stored in the config file. + // So there is no need to calculate an accurate size. + int h = 20*view->fontMetrics().lineSpacing() + 2*view->frameWidth(); + view->resize(QSize(h, h).expandedTo(view->sizeHint())); + QWhatsThis::add(view, i18n("The contents of the file to be displayed")); + } + KIO::NetAccess::removeTempFile(tmpFile); + } + if (!opened) + { + // File couldn't be opened + bool exists = KIO::NetAccess::exists(url, true, MainWindow::mainMainWindow()); + mErrorMsgs += dir ? i18n("File is a folder") : exists ? i18n("Failed to open file") : i18n("File not found"); + } + break; + } + case KAEvent::MESSAGE: + { + // Message label + // Using MessageText instead of QLabel allows scrolling and mouse copying + MessageText* text = new MessageText(mMessage, QString::null, topWidget); + text->setFrameStyle(QFrame::NoFrame); + text->setPaper(mBgColour); + text->setPaletteForegroundColor(mFgColour); + text->setFont(mFont); + int lineSpacing = text->fontMetrics().lineSpacing(); + QSize s = text->sizeHint(); + int h = s.height(); + text->setMaximumHeight(h + text->scrollBarHeight()); + text->setMinimumHeight(QMIN(h, lineSpacing*4)); + text->setMaximumWidth(s.width() + text->scrollBarWidth()); + QWhatsThis::add(text, i18n("The alarm message")); + int vspace = lineSpacing/2; + int hspace = lineSpacing - KDialog::marginHint(); + topLayout->addSpacing(vspace); + topLayout->addStretch(); + // Don't include any horizontal margins if message is 2/3 screen width + if (!mWinModule) + mWinModule = new KWinModule(0, KWinModule::INFO_DESKTOP); + if (text->sizeHint().width() >= mWinModule->workArea().width()*2/3) + topLayout->addWidget(text, 1, Qt::AlignHCenter); + else + { + QBoxLayout* layout = new QHBoxLayout(topLayout); + layout->addSpacing(hspace); + layout->addWidget(text, 1, Qt::AlignHCenter); + layout->addSpacing(hspace); + } + if (!reminder) + topLayout->addStretch(); + break; + } + case KAEvent::COMMAND: + case KAEvent::EMAIL: + default: + break; + } + + if (reminder) + { + // Reminder: show remaining time until the actual alarm + mRemainingText = new QLabel(topWidget); + mRemainingText->setFrameStyle(QFrame::Box | QFrame::Raised); + mRemainingText->setMargin(leading); + if (mDateTime.isDateOnly() || QDate::currentDate().daysTo(mDateTime.date()) > 0) + { + setRemainingTextDay(); + MidnightTimer::connect(this, SLOT(setRemainingTextDay())); // update every day + } + else + { + setRemainingTextMinute(); + MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute + } + topLayout->addWidget(mRemainingText, 0, Qt::AlignHCenter); + topLayout->addSpacing(KDialog::spacingHint()); + topLayout->addStretch(); + } + } + else + { + // It's an error message + switch (mAction) + { + case KAEvent::EMAIL: + { + // Display the email addresses and subject. + QFrame* frame = new QFrame(topWidget); + frame->setFrameStyle(QFrame::Box | QFrame::Raised); + QWhatsThis::add(frame, i18n("The email to send")); + topLayout->addWidget(frame, 0, Qt::AlignHCenter); + QGridLayout* grid = new QGridLayout(frame, 2, 2, KDialog::marginHint(), KDialog::spacingHint()); + + QLabel* label = new QLabel(i18n("Email addressee", "To:"), frame); + label->setFixedSize(label->sizeHint()); + grid->addWidget(label, 0, 0, Qt::AlignLeft); + label = new QLabel(mEvent.emailAddresses("\n"), frame); + label->setFixedSize(label->sizeHint()); + grid->addWidget(label, 0, 1, Qt::AlignLeft); + + label = new QLabel(i18n("Email subject", "Subject:"), frame); + label->setFixedSize(label->sizeHint()); + grid->addWidget(label, 1, 0, Qt::AlignLeft); + label = new QLabel(mEvent.emailSubject(), frame); + label->setFixedSize(label->sizeHint()); + grid->addWidget(label, 1, 1, Qt::AlignLeft); + break; + } + case KAEvent::COMMAND: + case KAEvent::FILE: + case KAEvent::MESSAGE: + default: + // Just display the error message strings + break; + } + } + + if (!mErrorMsgs.count()) + topWidget->setBackgroundColor(mBgColour); + else + { + setCaption(i18n("Error")); + QBoxLayout* layout = new QHBoxLayout(topLayout); + layout->setMargin(2*KDialog::marginHint()); + layout->addStretch(); + QLabel* label = new QLabel(topWidget); + label->setPixmap(DesktopIcon("error")); + label->setFixedSize(label->sizeHint()); + layout->addWidget(label, 0, Qt::AlignRight); + QBoxLayout* vlayout = new QVBoxLayout(layout); + for (QStringList::Iterator it = mErrorMsgs.begin(); it != mErrorMsgs.end(); ++it) + { + label = new QLabel(*it, topWidget); + label->setFixedSize(label->sizeHint()); + vlayout->addWidget(label, 0, Qt::AlignLeft); + } + layout->addStretch(); + } + + QGridLayout* grid = new QGridLayout(1, 4); + topLayout->addLayout(grid); + grid->setColStretch(0, 1); // keep the buttons right-adjusted in the window + int gridIndex = 1; + + // Close button + mOkButton = new KPushButton(KStdGuiItem::close(), topWidget); + // Prevent accidental acknowledgement of the message if the user is typing when the window appears + mOkButton->clearFocus(); + mOkButton->setFocusPolicy(QWidget::ClickFocus); // don't allow keyboard selection + mOkButton->setFixedSize(mOkButton->sizeHint()); + connect(mOkButton, SIGNAL(clicked()), SLOT(close())); + grid->addWidget(mOkButton, 0, gridIndex++, AlignHCenter); + QWhatsThis::add(mOkButton, i18n("Acknowledge the alarm")); + + if (mShowEdit) + { + // Edit button + mEditButton = new QPushButton(i18n("&Edit..."), topWidget); + mEditButton->setFocusPolicy(QWidget::ClickFocus); // don't allow keyboard selection + mEditButton->setFixedSize(mEditButton->sizeHint()); + connect(mEditButton, SIGNAL(clicked()), SLOT(slotEdit())); + grid->addWidget(mEditButton, 0, gridIndex++, AlignHCenter); + QWhatsThis::add(mEditButton, i18n("Edit the alarm.")); + } + + if (!mNoDefer) + { + // Defer button + mDeferButton = new QPushButton(i18n("&Defer..."), topWidget); + mDeferButton->setFocusPolicy(QWidget::ClickFocus); // don't allow keyboard selection + mDeferButton->setFixedSize(mDeferButton->sizeHint()); + connect(mDeferButton, SIGNAL(clicked()), SLOT(slotDefer())); + grid->addWidget(mDeferButton, 0, gridIndex++, AlignHCenter); + QWhatsThis::add(mDeferButton, + i18n("Defer the alarm until later.\n" + "You will be prompted to specify when the alarm should be redisplayed.")); + + setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more + } + +#ifndef WITHOUT_ARTS + if (!mAudioFile.isEmpty() && (mVolume || mFadeVolume > 0)) + { + // Silence button to stop sound repetition + QPixmap pixmap = MainBarIcon("player_stop"); + mSilenceButton = new QPushButton(topWidget); + mSilenceButton->setPixmap(pixmap); + mSilenceButton->setFixedSize(mSilenceButton->sizeHint()); + connect(mSilenceButton, SIGNAL(clicked()), SLOT(stopPlay())); + grid->addWidget(mSilenceButton, 0, gridIndex++, AlignHCenter); + QToolTip::add(mSilenceButton, i18n("Stop sound")); + QWhatsThis::add(mSilenceButton, i18n("Stop playing the sound")); + // To avoid getting in a mess, disable the button until sound playing has been set up + mSilenceButton->setEnabled(false); + } +#endif + + KIconLoader iconLoader; + if (mKMailSerialNumber) + { + // KMail button + QPixmap pixmap = iconLoader.loadIcon(QString::fromLatin1("kmail"), KIcon::MainToolbar); + mKMailButton = new QPushButton(topWidget); + mKMailButton->setPixmap(pixmap); + mKMailButton->setFixedSize(mKMailButton->sizeHint()); + connect(mKMailButton, SIGNAL(clicked()), SLOT(slotShowKMailMessage())); + grid->addWidget(mKMailButton, 0, gridIndex++, AlignHCenter); + QToolTip::add(mKMailButton, i18n("Locate this email in KMail", "Locate in KMail")); + QWhatsThis::add(mKMailButton, i18n("Locate and highlight this email in KMail")); + } + else + mKMailButton = 0; + + // KAlarm button + QPixmap pixmap = iconLoader.loadIcon(QString::fromLatin1(kapp->aboutData()->appName()), KIcon::MainToolbar); + mKAlarmButton = new QPushButton(topWidget); + mKAlarmButton->setPixmap(pixmap); + mKAlarmButton->setFixedSize(mKAlarmButton->sizeHint()); + connect(mKAlarmButton, SIGNAL(clicked()), SLOT(displayMainWindow())); + grid->addWidget(mKAlarmButton, 0, gridIndex++, AlignHCenter); + QString actKAlarm = i18n("Activate KAlarm"); + QToolTip::add(mKAlarmButton, actKAlarm); + QWhatsThis::add(mKAlarmButton, actKAlarm); + + // Disable all buttons initially, to prevent accidental clicking on if they happen to be + // under the mouse just as the window appears. + mOkButton->setEnabled(false); + if (mDeferButton) + mDeferButton->setEnabled(false); + if (mEditButton) + mEditButton->setEnabled(false); + if (mKMailButton) + mKMailButton->setEnabled(false); + mKAlarmButton->setEnabled(false); + + topLayout->activate(); + setMinimumSize(QSize(grid->sizeHint().width() + 2*KDialog::marginHint(), sizeHint().height())); + + bool modal = !(getWFlags() & Qt::WX11BypassWM); + + unsigned long wstate = (modal ? NET::Modal : 0) | NET::Sticky | NET::KeepAbove; + WId winid = winId(); + KWin::setState(winid, wstate); + KWin::setOnAllDesktops(winid, true); +} + +/****************************************************************************** +* Set the remaining time text in a reminder window. +* Called at the start of every day (at the user-defined start-of-day time). +*/ +void MessageWin::setRemainingTextDay() +{ + QString text; + int days = QDate::currentDate().daysTo(mDateTime.date()); + if (days <= 0 && !mDateTime.isDateOnly()) + { + // The alarm is due today, so start refreshing every minute + MidnightTimer::disconnect(this, SLOT(setRemainingTextDay())); + setRemainingTextMinute(); + MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute + } + else + { + if (days <= 0) + text = i18n("Today"); + else if (days % 7) + text = i18n("Tomorrow", "in %n days' time", days); + else + text = i18n("in 1 week's time", "in %n weeks' time", days/7); + } + mRemainingText->setText(text); +} + +/****************************************************************************** +* Set the remaining time text in a reminder window. +* Called on every minute boundary. +*/ +void MessageWin::setRemainingTextMinute() +{ + QString text; + int mins = (QDateTime::currentDateTime().secsTo(mDateTime.dateTime()) + 59) / 60; + if (mins < 60) + text = i18n("in 1 minute's time", "in %n minutes' time", (mins > 0 ? mins : 0)); + else if (mins % 60 == 0) + text = i18n("in 1 hour's time", "in %n hours' time", mins/60); + else if (mins % 60 == 1) + text = i18n("in 1 hour 1 minute's time", "in %n hours 1 minute's time", mins/60); + else + text = i18n("in 1 hour %1 minutes' time", "in %n hours %1 minutes' time", mins/60).arg(mins%60); + mRemainingText->setText(text); +} + +/****************************************************************************** +* Save settings to the session managed config file, for restoration +* when the program is restored. +*/ +void MessageWin::saveProperties(KConfig* config) +{ + if (mShown && !mErrorWindow) + { + config->writeEntry(QString::fromLatin1("EventID"), mEventID); + config->writeEntry(QString::fromLatin1("AlarmType"), mAlarmType); + config->writeEntry(QString::fromLatin1("Message"), mMessage); + config->writeEntry(QString::fromLatin1("Type"), mAction); + config->writeEntry(QString::fromLatin1("Font"), mFont); + config->writeEntry(QString::fromLatin1("BgColour"), mBgColour); + config->writeEntry(QString::fromLatin1("FgColour"), mFgColour); + config->writeEntry(QString::fromLatin1("ConfirmAck"), mConfirmAck); + if (mDateTime.isValid()) + { + config->writeEntry(QString::fromLatin1("Time"), mDateTime.dateTime()); + config->writeEntry(QString::fromLatin1("DateOnly"), mDateTime.isDateOnly()); + } + if (mCloseTime.isValid()) + config->writeEntry(QString::fromLatin1("Expiry"), mCloseTime); +#ifndef WITHOUT_ARTS + if (mAudioRepeat && mSilenceButton && mSilenceButton->isEnabled()) + { + // Only need to restart sound file playing if it's being repeated + config->writePathEntry(QString::fromLatin1("AudioFile"), mAudioFile); + config->writeEntry(QString::fromLatin1("Volume"), static_cast<int>(mVolume * 100)); + } +#endif + config->writeEntry(QString::fromLatin1("Speak"), mSpeak); + config->writeEntry(QString::fromLatin1("Height"), height()); + config->writeEntry(QString::fromLatin1("DeferMins"), mDefaultDeferMinutes); + config->writeEntry(QString::fromLatin1("NoDefer"), mNoDefer); + config->writeEntry(QString::fromLatin1("NoPostAction"), mNoPostAction); + config->writeEntry(QString::fromLatin1("KMailSerial"), mKMailSerialNumber); + } + else + config->writeEntry(QString::fromLatin1("Invalid"), true); +} + +/****************************************************************************** +* Read settings from the session managed config file. +* This function is automatically called whenever the app is being restored. +* Read in whatever was saved in saveProperties(). +*/ +void MessageWin::readProperties(KConfig* config) +{ + mInvalid = config->readBoolEntry(QString::fromLatin1("Invalid"), false); + mEventID = config->readEntry(QString::fromLatin1("EventID")); + mAlarmType = KAAlarm::Type(config->readNumEntry(QString::fromLatin1("AlarmType"))); + mMessage = config->readEntry(QString::fromLatin1("Message")); + mAction = KAEvent::Action(config->readNumEntry(QString::fromLatin1("Type"))); + mFont = config->readFontEntry(QString::fromLatin1("Font")); + mBgColour = config->readColorEntry(QString::fromLatin1("BgColour")); + mFgColour = config->readColorEntry(QString::fromLatin1("FgColour")); + mConfirmAck = config->readBoolEntry(QString::fromLatin1("ConfirmAck")); + QDateTime invalidDateTime; + QDateTime dt = config->readDateTimeEntry(QString::fromLatin1("Time"), &invalidDateTime); + bool dateOnly = config->readBoolEntry(QString::fromLatin1("DateOnly")); + mDateTime.set(dt, dateOnly); + mCloseTime = config->readDateTimeEntry(QString::fromLatin1("Expiry"), &invalidDateTime); +#ifndef WITHOUT_ARTS + mAudioFile = config->readPathEntry(QString::fromLatin1("AudioFile")); + mVolume = static_cast<float>(config->readNumEntry(QString::fromLatin1("Volume"))) / 100; + mFadeVolume = -1; + mFadeSeconds = 0; + if (!mAudioFile.isEmpty()) + mAudioRepeat = true; +#endif + mSpeak = config->readBoolEntry(QString::fromLatin1("Speak")); + mRestoreHeight = config->readNumEntry(QString::fromLatin1("Height")); + mDefaultDeferMinutes = config->readNumEntry(QString::fromLatin1("DeferMins")); + mNoDefer = config->readBoolEntry(QString::fromLatin1("NoDefer")); + mNoPostAction = config->readBoolEntry(QString::fromLatin1("NoPostAction")); + mKMailSerialNumber = config->readUnsignedLongNumEntry(QString::fromLatin1("KMailSerial")); + mShowEdit = false; + kdDebug(5950) << "MessageWin::readProperties(" << mEventID << ")" << endl; + if (mAlarmType != KAAlarm::INVALID_ALARM) + { + // Recreate the event from the calendar file (if possible) + if (!mEventID.isEmpty()) + { + const Event* kcalEvent = AlarmCalendar::activeCalendar()->event(mEventID); + if (!kcalEvent) + { + // It's not in the active calendar, so try the displaying calendar + AlarmCalendar* cal = AlarmCalendar::displayCalendar(); + if (cal->isOpen()) + kcalEvent = cal->event(KAEvent::uid(mEventID, KAEvent::DISPLAYING)); + } + if (kcalEvent) + { + mEvent.set(*kcalEvent); + mEvent.setUid(KAEvent::ACTIVE); // in case it came from the display calendar + mShowEdit = true; + } + } + initView(); + } +} + +/****************************************************************************** +* Returns the existing message window (if any) which is displaying the event +* with the specified ID. +*/ +MessageWin* MessageWin::findEvent(const QString& eventID) +{ + for (QValueList<MessageWin*>::Iterator it = mWindowList.begin(); it != mWindowList.end(); ++it) + { + MessageWin* w = *it; + if (w->mEventID == eventID && !w->mErrorWindow) + return w; + } + return 0; +} + +/****************************************************************************** +* Beep and play the audio file, as appropriate. +*/ +void MessageWin::playAudio() +{ + if (mBeep) + { + // Beep using two methods, in case the sound card/speakers are switched off or not working + KNotifyClient::beep(); // beep through the sound card & speakers + QApplication::beep(); // beep through the internal speaker + } + if (!mAudioFile.isEmpty()) + { + if (!mVolume && mFadeVolume <= 0) + return; // ensure zero volume doesn't play anything +#ifdef WITHOUT_ARTS + QString play = mAudioFile; + QString file = QString::fromLatin1("file:"); + if (mAudioFile.startsWith(file)) + play = mAudioFile.mid(file.length()); + KAudioPlayer::play(QFile::encodeName(play)); +#else + // An audio file is specified. Because loading it may take some time, + // call it on a timer to allow the window to display first. + QTimer::singleShot(0, this, SLOT(slotPlayAudio())); +#endif + } + else if (mSpeak) + { + // The message is to be spoken. In case of error messges, + // call it on a timer to allow the window to display first. + QTimer::singleShot(0, this, SLOT(slotSpeak())); + } +} + +/****************************************************************************** +* Speak the message. +* Called asynchronously to avoid delaying the display of the message. +*/ +void MessageWin::slotSpeak() +{ + DCOPClient* client = kapp->dcopClient(); + if (!client->isApplicationRegistered("kttsd")) + { + // kttsd is not running, so start it + QString error; + if (kapp->startServiceByDesktopName("kttsd", QStringList(), &error)) + { + kdDebug(5950) << "MessageWin::slotSpeak(): failed to start kttsd: " << error << endl; + if (!haveErrorMessage(ErrMsg_Speak)) + { + KMessageBox::detailedError(0, i18n("Unable to speak message"), error); + clearErrorMessage(ErrMsg_Speak); + } + return; + } + } + QByteArray data; + QDataStream arg(data, IO_WriteOnly); + arg << mMessage << ""; + if (!client->send("kttsd", "KSpeech", "sayMessage(QString,QString)", data)) + { + kdDebug(5950) << "MessageWin::slotSpeak(): sayMessage() DCOP error" << endl; + if (!haveErrorMessage(ErrMsg_Speak)) + { + KMessageBox::detailedError(0, i18n("Unable to speak message"), i18n("DCOP Call sayMessage failed")); + clearErrorMessage(ErrMsg_Speak); + } + } +} + +/****************************************************************************** +* Play the audio file. +* Called asynchronously to avoid delaying the display of the message. +*/ +void MessageWin::slotPlayAudio() +{ +#ifndef WITHOUT_ARTS + // First check that it exists, to avoid possible crashes if the filename is badly specified + MainWindow* mmw = MainWindow::mainMainWindow(); + KURL url(mAudioFile); + if (!url.isValid() || !KIO::NetAccess::exists(url, true, mmw) + || !KIO::NetAccess::download(url, mLocalAudioFile, mmw)) + { + kdError(5950) << "MessageWin::playAudio(): Open failure: " << mAudioFile << endl; + if (!haveErrorMessage(ErrMsg_AudioFile)) + { + KMessageBox::error(this, i18n("Cannot open audio file:\n%1").arg(mAudioFile)); + clearErrorMessage(ErrMsg_AudioFile); + } + return; + } + if (!mArtsDispatcher) + { + mFadeTimer = 0; + mPlayTimer = new QTimer(this); + connect(mPlayTimer, SIGNAL(timeout()), SLOT(checkAudioPlay())); + mArtsDispatcher = new KArtsDispatcher; + mPlayedOnce = false; + mAudioFileStart = QTime::currentTime(); + initAudio(true); + if (!mPlayObject->object().isNull()) + checkAudioPlay(); + if (!mUsingKMix && mVolume >= 0) + { + // Output error message now that everything else has been done. + // (Outputting it earlier would delay things until it is acknowledged.) + kdWarning(5950) << "Unable to set master volume (KMix: " << mKMixError << ")\n"; + if (!haveErrorMessage(ErrMsg_Volume)) + { + KMessageBox::information(this, i18n("Unable to set master volume\n(Error accessing KMix:\n%1)").arg(mKMixError), + QString::null, QString::fromLatin1("KMixError")); + clearErrorMessage(ErrMsg_Volume); + } + } + } +#endif +} + +#ifndef WITHOUT_ARTS +/****************************************************************************** +* Set up the audio file for playing. +*/ +void MessageWin::initAudio(bool firstTime) +{ + KArtsServer aserver; + Arts::SoundServerV2 sserver = aserver.server(); + KDE::PlayObjectFactory factory(sserver); + mPlayObject = factory.createPlayObject(mLocalAudioFile, true); + if (firstTime) + { + // Save the existing sound volume setting for restoration afterwards, + // and set the desired volume for the alarm. + mUsingKMix = false; + float volume = mVolume; // initial volume + if (volume >= 0) + { + // The volume has been specified + if (mFadeVolume >= 0) + volume = mFadeVolume; // fading, so adjust the initial volume + + // Get the current master volume from KMix + int vol = getKMixVolume(); + if (vol >= 0) + { + mOldVolume = vol; // success + mUsingKMix = true; + setKMixVolume(static_cast<int>(volume * 100)); + } + } + if (!mUsingKMix) + { + /* Adjust within the current master volume, because either + * a) the volume is not specified, in which case we want to play + * at 100% of the current master volume setting, or + * b) KMix is not available to set the master volume. + */ + mOldVolume = sserver.outVolume().scaleFactor(); // save volume for restoration afterwards + sserver.outVolume().scaleFactor(volume >= 0 ? volume : 1); + } + } + mSilenceButton->setEnabled(true); + mPlayed = false; + connect(mPlayObject, SIGNAL(playObjectCreated()), SLOT(checkAudioPlay())); + if (!mPlayObject->object().isNull()) + checkAudioPlay(); +} +#endif + +/****************************************************************************** +* Called when the audio file has loaded and is ready to play, or on a timer +* when play is expected to have completed. +* If it is ready to play, start playing it (for the first time or repeated). +* If play has not yet completed, wait a bit longer. +*/ +void MessageWin::checkAudioPlay() +{ +#ifndef WITHOUT_ARTS + if (!mPlayObject) + return; + if (mPlayObject->state() == Arts::posIdle) + { + // The file has loaded and is ready to play, or play has completed + if (mPlayedOnce && !mAudioRepeat) + { + // Play has completed + stopPlay(); + return; + } + + // Start playing the file, either for the first time or again + kdDebug(5950) << "MessageWin::checkAudioPlay(): start\n"; + if (!mPlayedOnce) + { + // Start playing the file for the first time + QTime now = QTime::currentTime(); + mAudioFileLoadSecs = mAudioFileStart.secsTo(now); + if (mAudioFileLoadSecs < 0) + mAudioFileLoadSecs += 86400; + if (mVolume >= 0 && mFadeVolume >= 0 && mFadeSeconds > 0) + { + // Set up volume fade + mAudioFileStart = now; + mFadeTimer = new QTimer(this); + connect(mFadeTimer, SIGNAL(timeout()), SLOT(slotFade())); + mFadeTimer->start(1000); // adjust volume every second + } + mPlayedOnce = true; + } + if (mAudioFileLoadSecs < 3) + { + /* The aRts library takes several attempts before a PlayObject can + * be replayed, leaving a gap of perhaps 5 seconds between plays. + * So if loading the file takes a short time, it's better to reload + * the PlayObject rather than try to replay the same PlayObject. + */ + if (mPlayed) + { + // Playing has completed. Start playing again. + delete mPlayObject; + initAudio(false); + if (mPlayObject->object().isNull()) + return; + } + mPlayed = true; + mPlayObject->play(); + } + else + { + // The file is slow to load, so attempt to replay the PlayObject + static Arts::poTime t0((long)0, (long)0, 0, std::string()); + Arts::poTime current = mPlayObject->currentTime(); + if (current.seconds || current.ms) + mPlayObject->seek(t0); + else + mPlayObject->play(); + } + } + + // The sound file is still playing + Arts::poTime overall = mPlayObject->overallTime(); + Arts::poTime current = mPlayObject->currentTime(); + int time = 1000*(overall.seconds - current.seconds) + overall.ms - current.ms; + if (time < 0) + time = 0; + kdDebug(5950) << "MessageWin::checkAudioPlay(): wait for " << (time+100) << "ms\n"; + mPlayTimer->start(time + 100, true); +#endif +} + +/****************************************************************************** +* Called when play completes, the Silence button is clicked, or the window is +* closed, to reset the sound volume and terminate audio access. +*/ +void MessageWin::stopPlay() +{ +#ifndef WITHOUT_ARTS + if (mArtsDispatcher) + { + // Restore the sound volume to what it was before the sound file + // was played, provided that nothing else has modified it since. + if (!mUsingKMix) + { + KArtsServer aserver; + Arts::StereoVolumeControl svc = aserver.server().outVolume(); + float currentVolume = svc.scaleFactor(); + float eventVolume = mVolume; + if (eventVolume < 0) + eventVolume = 1; + if (currentVolume == eventVolume) + svc.scaleFactor(mOldVolume); + } + else if (mVolume >= 0) + { + int eventVolume = static_cast<int>(mVolume * 100); + int currentVolume = getKMixVolume(); + // Volume returned isn't always exactly equal to volume set + if (currentVolume < 0 || abs(currentVolume - eventVolume) < 5) + setKMixVolume(static_cast<int>(mOldVolume)); + } + } + delete mPlayObject; mPlayObject = 0; + delete mArtsDispatcher; mArtsDispatcher = 0; + if (!mLocalAudioFile.isEmpty()) + { + KIO::NetAccess::removeTempFile(mLocalAudioFile); // removes it only if it IS a temporary file + mLocalAudioFile = QString::null; + } + if (mSilenceButton) + mSilenceButton->setEnabled(false); +#endif +} + +/****************************************************************************** +* Called every second to fade the volume when the audio file starts playing. +*/ +void MessageWin::slotFade() +{ +#ifndef WITHOUT_ARTS + QTime now = QTime::currentTime(); + int elapsed = mAudioFileStart.secsTo(now); + if (elapsed < 0) + elapsed += 86400; // it's the next day + float volume; + if (elapsed >= mFadeSeconds) + { + // The fade has finished. Set to normal volume. + volume = mVolume; + delete mFadeTimer; + mFadeTimer = 0; + if (!mVolume) + { + kdDebug(5950) << "MessageWin::slotFade(0)\n"; + stopPlay(); + return; + } + } + else + volume = mFadeVolume + ((mVolume - mFadeVolume) * elapsed) / mFadeSeconds; + kdDebug(5950) << "MessageWin::slotFade(" << volume << ")\n"; + if (mArtsDispatcher) + { + if (mUsingKMix) + setKMixVolume(static_cast<int>(volume * 100)); + else if (mArtsDispatcher) + { + KArtsServer aserver; + aserver.server().outVolume().scaleFactor(volume); + } + } +#endif +} + +#ifndef WITHOUT_ARTS +/****************************************************************************** +* Get the master volume from KMix. +* Reply < 0 if failure. +*/ +int MessageWin::getKMixVolume() +{ + if (!KAlarm::runProgram(KMIX_APP_NAME, KMIX_DCOP_WINDOW, mKMixName, mKMixError)) // start KMix if it isn't already running + return -1; + QByteArray data, replyData; + QCString replyType; + QDataStream arg(data, IO_WriteOnly); + if (!kapp->dcopClient()->call(mKMixName, KMIX_DCOP_OBJECT, "masterVolume()", data, replyType, replyData) + || replyType != "int") + return -1; + int result; + QDataStream reply(replyData, IO_ReadOnly); + reply >> result; + return (result >= 0) ? result : 0; +} + +/****************************************************************************** +* Set the master volume using KMix. +*/ +void MessageWin::setKMixVolume(int percent) +{ + if (!mUsingKMix) + return; + if (!KAlarm::runProgram(KMIX_APP_NAME, KMIX_DCOP_WINDOW, mKMixName, mKMixError)) // start KMix if it isn't already running + return; + QByteArray data; + QDataStream arg(data, IO_WriteOnly); + arg << percent; + if (!kapp->dcopClient()->send(mKMixName, KMIX_DCOP_OBJECT, "setMasterVolume(int)", data)) + kdError(5950) << "MessageWin::setKMixVolume(): kmix dcop call failed\n"; +} +#endif + +/****************************************************************************** +* Raise the alarm window, re-output any required audio notification, and +* reschedule the alarm in the calendar file. +*/ +void MessageWin::repeat(const KAAlarm& alarm) +{ + if (mDeferDlg) + { + // Cancel any deferral dialogue so that the user notices something's going on, + // and also because the deferral time limit will have changed. + delete mDeferDlg; + mDeferDlg = 0; + } + const Event* kcalEvent = mEventID.isNull() ? 0 : AlarmCalendar::activeCalendar()->event(mEventID); + if (kcalEvent) + { + mAlarmType = alarm.type(); // store new alarm type for use if it is later deferred + if (!mDeferDlg || Preferences::modalMessages()) + { + raise(); + playAudio(); + } + KAEvent event(*kcalEvent); + mDeferButton->setEnabled(true); + setDeferralLimit(event); // ensure that button is disabled when alarm can't be deferred any more + theApp()->alarmShowing(event, mAlarmType, mDateTime); + } +} + +/****************************************************************************** +* Display the window. +* If windows are being positioned away from the mouse cursor, it is initially +* positioned at the top left to slightly reduce the number of times the +* windows need to be moved in showEvent(). +*/ +void MessageWin::show() +{ + if (mCloseTime.isValid()) + { + // Set a timer to auto-close the window + int delay = QDateTime::currentDateTime().secsTo(mCloseTime); + if (delay < 0) + delay = 0; + QTimer::singleShot(delay * 1000, this, SLOT(close())); + if (!delay) + return; // don't show the window if auto-closing is already due + } + if (Preferences::messageButtonDelay() == 0) + move(0, 0); + MainWindowBase::show(); +} + +/****************************************************************************** +* Returns the window's recommended size exclusive of its frame. +* For message windows, the size if limited to fit inside the working area of +* the desktop. +*/ +QSize MessageWin::sizeHint() const +{ + if (mAction != KAEvent::MESSAGE) + return MainWindowBase::sizeHint(); + if (!mWinModule) + mWinModule = new KWinModule(0, KWinModule::INFO_DESKTOP); + QSize frame = frameGeometry().size(); + QSize contents = geometry().size(); + QSize desktop = mWinModule->workArea().size(); + QSize maxSize(desktop.width() - (frame.width() - contents.width()), + desktop.height() - (frame.height() - contents.height())); + return MainWindowBase::sizeHint().boundedTo(maxSize); +} + +/****************************************************************************** +* Called when the window is shown. +* The first time, output any required audio notification, and reschedule or +* delete the event from the calendar file. +*/ +void MessageWin::showEvent(QShowEvent* se) +{ + MainWindowBase::showEvent(se); + if (!mShown) + { + if (mErrorWindow) + enableButtons(); // don't bother repositioning error messages + else + { + /* Set the window size. + * Note that the frame thickness is not yet known when this + * method is called, so for large windows the size needs to be + * set again later. + */ + QSize s = sizeHint(); // fit the window round the message + if (mAction == KAEvent::FILE && !mErrorMsgs.count()) + KAlarm::readConfigWindowSize("FileMessage", s); + resize(s); + + mButtonDelay = Preferences::messageButtonDelay() * 1000; + if (!mButtonDelay) + { + /* Try to ensure that the window can't accidentally be acknowledged + * by the user clicking the mouse just as it appears. + * To achieve this, move the window so that the OK button is as far away + * from the cursor as possible. If the buttons are still too close to the + * cursor, disable the buttons for a short time. + * N.B. This can't be done in show(), since the geometry of the window + * is not known until it is displayed. Unfortunately by moving the + * window in showEvent(), a flicker is unavoidable. + * See the Qt documentation on window geometry for more details. + */ + // PROBLEM: The frame size is not known yet! + + /* Find the usable area of the desktop or, if the desktop comprises + * multiple screens, the usable area of the current screen. (If the + * message is displayed on a screen other than that currently being + * worked with, it might not be noticed.) + */ + QPoint cursor = QCursor::pos(); + if (!mWinModule) + mWinModule = new KWinModule(0, KWinModule::INFO_DESKTOP); + QRect desk = mWinModule->workArea(); + QDesktopWidget* dw = QApplication::desktop(); + if (dw->numScreens() > 1) + desk &= dw->screenGeometry(dw->screenNumber(cursor)); + + QRect frame = frameGeometry(); + QRect rect = geometry(); + // Find the offsets from the outside of the frame to the edges of the OK button + QRect button(mOkButton->mapToParent(QPoint(0, 0)), mOkButton->mapToParent(mOkButton->rect().bottomRight())); + int buttonLeft = button.left() + rect.left() - frame.left(); + int buttonRight = width() - button.right() + frame.right() - rect.right(); + int buttonTop = button.top() + rect.top() - frame.top(); + int buttonBottom = height() - button.bottom() + frame.bottom() - rect.bottom(); + + int centrex = (desk.width() + buttonLeft - buttonRight) / 2; + int centrey = (desk.height() + buttonTop - buttonBottom) / 2; + int x = (cursor.x() < centrex) ? desk.right() - frame.width() : desk.left(); + int y = (cursor.y() < centrey) ? desk.bottom() - frame.height() : desk.top(); + + // Find the enclosing rectangle for the new button positions + // and check if the cursor is too near + QRect buttons = mOkButton->geometry().unite(mKAlarmButton->geometry()); + buttons.moveBy(rect.left() + x - frame.left(), rect.top() + y - frame.top()); + int minDistance = proximityMultiple * mOkButton->height(); + if ((abs(cursor.x() - buttons.left()) < minDistance + || abs(cursor.x() - buttons.right()) < minDistance) + && (abs(cursor.y() - buttons.top()) < minDistance + || abs(cursor.y() - buttons.bottom()) < minDistance)) + mButtonDelay = proximityButtonDelay; // too near - disable buttons initially + + if (x != frame.left() || y != frame.top()) + { + mPositioning = true; + move(x, y); + } + } + if (!mPositioning) + displayComplete(); // play audio, etc. + if (mAction == KAEvent::MESSAGE) + { + // Set the window size once the frame size is known + QTimer::singleShot(0, this, SLOT(setMaxSize())); + } + } + mShown = true; + } +} + +/****************************************************************************** +* Called when the window has been moved. +*/ +void MessageWin::moveEvent(QMoveEvent* e) +{ + MainWindowBase::moveEvent(e); + if (mPositioning) + { + // The window has just been initially positioned + mPositioning = false; + displayComplete(); // play audio, etc. + } +} + +/****************************************************************************** +* Reset the iniital window size if it exceeds the working area of the desktop. +*/ +void MessageWin::setMaxSize() +{ + QSize s = sizeHint(); + if (width() > s.width() || height() > s.height()) + resize(s); +} + +/****************************************************************************** +* Called when the window has been displayed properly (in its correct position), +* to play sounds and reschedule the event. +*/ +void MessageWin::displayComplete() +{ + playAudio(); + if (mRescheduleEvent) + theApp()->alarmShowing(mEvent, mAlarmType, mDateTime); + + // Enable the window's buttons either now or after the configured delay + if (mButtonDelay > 0) + QTimer::singleShot(mButtonDelay, this, SLOT(enableButtons())); + else + enableButtons(); +} + +/****************************************************************************** +* Enable the window's buttons. +*/ +void MessageWin::enableButtons() +{ + mOkButton->setEnabled(true); + mKAlarmButton->setEnabled(true); + if (mDeferButton && !mDisableDeferral) + mDeferButton->setEnabled(true); + if (mEditButton) + mEditButton->setEnabled(true); + if (mKMailButton) + mKMailButton->setEnabled(true); +} + +/****************************************************************************** +* Called when the window's size has changed (before it is painted). +*/ +void MessageWin::resizeEvent(QResizeEvent* re) +{ + if (mRestoreHeight) + { + // Restore the window height on session restoration + if (mRestoreHeight != re->size().height()) + { + QSize size = re->size(); + size.setHeight(mRestoreHeight); + resize(size); + } + else if (isVisible()) + mRestoreHeight = 0; + } + else + { + if (mShown && mAction == KAEvent::FILE && !mErrorMsgs.count()) + KAlarm::writeConfigWindowSize("FileMessage", re->size()); + MainWindowBase::resizeEvent(re); + } +} + +/****************************************************************************** +* Called when a close event is received. +* Only quits the application if there is no system tray icon displayed. +*/ +void MessageWin::closeEvent(QCloseEvent* ce) +{ + // Don't prompt or delete the alarm from the display calendar if the session is closing + if (!mErrorWindow && !theApp()->sessionClosingDown()) + { + if (mConfirmAck && !mNoCloseConfirm) + { + // Ask for confirmation of acknowledgement. Use warningYesNo() because its default is No. + if (KMessageBox::warningYesNo(this, i18n("Do you really want to acknowledge this alarm?"), + i18n("Acknowledge Alarm"), i18n("&Acknowledge"), KStdGuiItem::cancel()) + != KMessageBox::Yes) + { + ce->ignore(); + return; + } + } + if (!mEventID.isNull()) + { + // Delete from the display calendar + KAlarm::deleteDisplayEvent(KAEvent::uid(mEventID, KAEvent::DISPLAYING)); + } + } + MainWindowBase::closeEvent(ce); +} + +/****************************************************************************** +* Called when the KMail button is clicked. +* Tells KMail to display the email message displayed in this message window. +*/ +void MessageWin::slotShowKMailMessage() +{ + kdDebug(5950) << "MessageWin::slotShowKMailMessage()\n"; + if (!mKMailSerialNumber) + return; + QString err = KAlarm::runKMail(false); + if (!err.isNull()) + { + KMessageBox::sorry(this, err); + return; + } + QCString replyType; + QByteArray data, replyData; + QDataStream arg(data, IO_WriteOnly); + arg << (Q_UINT32)mKMailSerialNumber << QString::null; + if (kapp->dcopClient()->call("kmail", KMAIL_DCOP_OBJECT, "showMail(Q_UINT32,QString)", data, replyType, replyData) + && replyType == "bool") + { + bool result; + QDataStream replyStream(replyData, IO_ReadOnly); + replyStream >> result; + if (result) + return; // success + } + kdError(5950) << "MessageWin::slotShowKMailMessage(): kmail dcop call failed\n"; + KMessageBox::sorry(this, i18n("Unable to locate this email in KMail")); +} + +/****************************************************************************** +* Called when the Edit... button is clicked. +* Displays the alarm edit dialog. +*/ +void MessageWin::slotEdit() +{ + kdDebug(5950) << "MessageWin::slotEdit()" << endl; + EditAlarmDlg editDlg(false, i18n("Edit Alarm"), this, "editDlg", &mEvent); + if (editDlg.exec() == QDialog::Accepted) + { + KAEvent event; + editDlg.getEvent(event); + + // Update the displayed lists and the calendar file + KAlarm::UpdateStatus status; + if (AlarmCalendar::activeCalendar()->event(mEventID)) + { + // The old alarm hasn't expired yet, so replace it + status = KAlarm::modifyEvent(mEvent, event, 0, &editDlg); + Undo::saveEdit(mEvent, event); + } + else + { + // The old event has expired, so simply create a new one + status = KAlarm::addEvent(event, 0, &editDlg); + Undo::saveAdd(event); + } + + if (status == KAlarm::UPDATE_KORG_ERR) + KAlarm::displayKOrgUpdateError(&editDlg, KAlarm::KORG_ERR_MODIFY, 1); + KAlarm::outputAlarmWarnings(&editDlg, &event); + + // Close the alarm window + mNoCloseConfirm = true; // allow window to close without confirmation prompt + close(); + } +} + +/****************************************************************************** +* Set up to disable the defer button when the deferral limit is reached. +*/ +void MessageWin::setDeferralLimit(const KAEvent& event) +{ + if (mDeferButton) + { + mDeferLimit = event.deferralLimit().dateTime(); + MidnightTimer::connect(this, SLOT(checkDeferralLimit())); // check every day + mDisableDeferral = false; + checkDeferralLimit(); + } +} + +/****************************************************************************** +* Check whether the deferral limit has been reached. +* If so, disable the Defer button. +* N.B. Ideally, just a single QTimer::singleShot() call would be made to disable +* the defer button at the corret time. But for a 32-bit integer, the +* milliseconds parameter overflows in about 25 days, so instead a daily +* check is done until the day when the deferral limit is reached, followed +* by a non-overflowing QTimer::singleShot() call. +*/ +void MessageWin::checkDeferralLimit() +{ + if (!mDeferButton || !mDeferLimit.isValid()) + return; + int n = QDate::currentDate().daysTo(mDeferLimit.date()); + if (n > 0) + return; + MidnightTimer::disconnect(this, SLOT(checkDeferralLimit())); + if (n == 0) + { + // The deferral limit will be reached today + n = QTime::currentTime().secsTo(mDeferLimit.time()); + if (n > 0) + { + QTimer::singleShot(n * 1000, this, SLOT(checkDeferralLimit())); + return; + } + } + mDeferButton->setEnabled(false); + mDisableDeferral = true; +} + +/****************************************************************************** +* Called when the Defer... button is clicked. +* Displays the defer message dialog. +*/ +void MessageWin::slotDefer() +{ + mDeferDlg = new DeferAlarmDlg(i18n("Defer Alarm"), QDateTime::currentDateTime().addSecs(60), + false, this, "deferDlg"); + if (mDefaultDeferMinutes > 0) + mDeferDlg->setDeferMinutes(mDefaultDeferMinutes); + mDeferDlg->setLimit(mEventID); + if (!Preferences::modalMessages()) + lower(); + if (mDeferDlg->exec() == QDialog::Accepted) + { + DateTime dateTime = mDeferDlg->getDateTime(); + int delayMins = mDeferDlg->deferMinutes(); + const Event* kcalEvent = mEventID.isNull() ? 0 : AlarmCalendar::activeCalendar()->event(mEventID); + if (kcalEvent) + { + // The event still exists in the calendar file. + KAEvent event(*kcalEvent); + bool repeat = event.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true); + event.setDeferDefaultMinutes(delayMins); + KAlarm::updateEvent(event, 0, mDeferDlg, true, !repeat); + if (event.deferred()) + mNoPostAction = true; + } + else + { + KAEvent event; + kcalEvent = AlarmCalendar::displayCalendar()->event(KAEvent::uid(mEventID, KAEvent::DISPLAYING)); + if (kcalEvent) + { + event.reinstateFromDisplaying(KAEvent(*kcalEvent)); + event.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true); + } + else + { + // The event doesn't exist any more !?!, so create a new one + event.set(dateTime.dateTime(), mMessage, mBgColour, mFgColour, mFont, mAction, mLateCancel, mFlags); + event.setAudioFile(mAudioFile, mVolume, mFadeVolume, mFadeSeconds); + event.setArchive(); + event.setEventID(mEventID); + } + event.setDeferDefaultMinutes(delayMins); + // Add the event back into the calendar file, retaining its ID + // and not updating KOrganizer + KAlarm::addEvent(event, 0, mDeferDlg, true, false); + if (event.deferred()) + mNoPostAction = true; + if (kcalEvent) + { + event.setUid(KAEvent::EXPIRED); + KAlarm::deleteEvent(event, false); + } + } + if (theApp()->wantRunInSystemTray()) + { + // Alarms are to be displayed only if the system tray icon is running, + // so start it if necessary so that the deferred alarm will be shown. + theApp()->displayTrayIcon(true); + } + mNoCloseConfirm = true; // allow window to close without confirmation prompt + close(); + } + else + raise(); + delete mDeferDlg; + mDeferDlg = 0; +} + +/****************************************************************************** +* Called when the KAlarm icon button in the message window is clicked. +* Displays the main window, with the appropriate alarm selected. +*/ +void MessageWin::displayMainWindow() +{ + KAlarm::displayMainWindowSelected(mEventID); +} + +/****************************************************************************** +* Check whether the specified error message is already displayed for this +* alarm, and note that it will now be displayed. +* Reply = true if message is already displayed. +*/ +bool MessageWin::haveErrorMessage(unsigned msg) const +{ + if (!mErrorMessages.contains(mEventID)) + mErrorMessages.insert(mEventID, 0); + bool result = (mErrorMessages[mEventID] & msg); + mErrorMessages[mEventID] |= msg; + return result; +} + +void MessageWin::clearErrorMessage(unsigned msg) const +{ + if (mErrorMessages.contains(mEventID)) + { + if (mErrorMessages[mEventID] == msg) + mErrorMessages.remove(mEventID); + else + mErrorMessages[mEventID] &= ~msg; + } +} + + +/****************************************************************************** +* Check whether the message window should be modal, i.e. with title bar etc. +* Normally this follows the Preferences setting, but if there is a full screen +* window displayed, on X11 the message window has to bypass the window manager +* in order to display on top of it (which has the side effect that it will have +* no window decoration). +*/ +bool wantModal() +{ + bool modal = Preferences::modalMessages(); + if (modal) + { + KWinModule wm(0, KWinModule::INFO_DESKTOP); + KWin::WindowInfo wi = KWin::windowInfo(wm.activeWindow(), NET::WMState); + modal = !(wi.valid() && wi.hasState(NET::FullScreen)); + } + return modal; +} + + +/*============================================================================= += Class MWMimeSourceFactory +* Gets the mime type of a text file from not only its extension (as per +* QMimeSourceFactory), but also from its contents. This allows the detection +* of plain text files without file name extensions. +=============================================================================*/ +MWMimeSourceFactory::MWMimeSourceFactory(const QString& absPath, KTextBrowser* view) + : QMimeSourceFactory(), + mMimeType("text/plain"), + mLast(0) +{ + view->setMimeSourceFactory(this); + QString type = KMimeType::findByPath(absPath)->name(); + switch (KAlarm::fileType(type)) + { + case KAlarm::TextPlain: + case KAlarm::TextFormatted: + mMimeType = type.latin1(); + // fall through to 'TextApplication' + case KAlarm::TextApplication: + default: + // It's assumed to be a text file + mTextFile = absPath; + view->QTextBrowser::setSource(absPath); + break; + + case KAlarm::Image: + // It's an image file + QString text = "<img source=\""; + text += absPath; + text += "\">"; + view->setText(text); + break; + } + setFilePath(QFileInfo(absPath).dirPath(true)); +} + +MWMimeSourceFactory::~MWMimeSourceFactory() +{ + delete mLast; +} + +const QMimeSource* MWMimeSourceFactory::data(const QString& abs_name) const +{ + if (abs_name == mTextFile) + { + QFileInfo fi(abs_name); + if (fi.isReadable()) + { + QFile f(abs_name); + if (f.open(IO_ReadOnly) && f.size()) + { + QByteArray ba(f.size()); + f.readBlock(ba.data(), ba.size()); + QStoredDrag* sr = new QStoredDrag(mMimeType); + sr->setEncodedData(ba); + delete mLast; + mLast = sr; + return sr; + } + } + } + return QMimeSourceFactory::data(abs_name); +} diff --git a/kalarm/messagewin.h b/kalarm/messagewin.h new file mode 100644 index 000000000..0a10b39ce --- /dev/null +++ b/kalarm/messagewin.h @@ -0,0 +1,165 @@ +/* + * messagewin.h - displays an alarm message + * Program: kalarm + * Copyright © 2001-2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef MESSAGEWIN_H +#define MESSAGEWIN_H + +/** @file messagewin.h - displays an alarm message */ + +#include <qmap.h> + +#include "mainwindowbase.h" +#include "alarmevent.h" + +class QPushButton; +class KPushButton; +class QLabel; +class QTimer; +class KWinModule; +class AlarmTimeWidget; +class DeferAlarmDlg; +class KArtsDispatcher; +namespace KDE { class PlayObject; } + +/** + * MessageWin: A window to display an alarm message + */ +class MessageWin : public MainWindowBase +{ + Q_OBJECT + public: + MessageWin(); // for session management restoration only + MessageWin(const KAEvent&, const KAAlarm&, bool reschedule_event = true, bool allowDefer = true); + MessageWin(const KAEvent&, const DateTime& alarmDateTime, const QStringList& errmsgs); + ~MessageWin(); + void repeat(const KAAlarm&); + void setRecreating() { mRecreating = true; } + const DateTime& dateTime() { return mDateTime; } + KAAlarm::Type alarmType() const { return mAlarmType; } + bool hasDefer() const { return !!mDeferButton; } + bool isValid() const { return !mInvalid; } + virtual void show(); + virtual QSize sizeHint() const; + static int instanceCount() { return mWindowList.count(); } + static MessageWin* findEvent(const QString& eventID); + + protected: + virtual void showEvent(QShowEvent*); + virtual void moveEvent(QMoveEvent*); + virtual void resizeEvent(QResizeEvent*); + virtual void closeEvent(QCloseEvent*); + virtual void saveProperties(KConfig*); + virtual void readProperties(KConfig*); + + private slots: + void slotEdit(); + void slotDefer(); + void checkDeferralLimit(); + void displayMainWindow(); + void slotShowKMailMessage(); + void slotSpeak(); + void slotPlayAudio(); + void checkAudioPlay(); + void stopPlay(); + void slotFade(); + void enableButtons(); + void setRemainingTextDay(); + void setRemainingTextMinute(); + void setMaxSize(); + + private: + void initView(); +#ifndef WITHOUT_ARTS + void initAudio(bool firstTime); + int getKMixVolume(); + void setKMixVolume(int percent); +#endif + void displayComplete(); + void playAudio(); + void setDeferralLimit(const KAEvent&); + bool haveErrorMessage(unsigned msg) const; + void clearErrorMessage(unsigned msg) const; + + static QValueList<MessageWin*> mWindowList; // list of existing message windows + static QMap<QString, unsigned> mErrorMessages; // error messages currently displayed, by event ID + // Properties needed by readProperties() + QString mMessage; + QFont mFont; + QColor mBgColour, mFgColour; + DateTime mDateTime; // date/time displayed in the message window + QDateTime mCloseTime; // time at which window should be auto-closed + QString mEventID; + QString mAudioFile; + float mVolume; + float mFadeVolume; + int mFadeSeconds; + int mDefaultDeferMinutes; + KAAlarm::Type mAlarmType; + KAEvent::Action mAction; + unsigned long mKMailSerialNumber; // if email text, message's KMail serial number, else 0 + QStringList mErrorMsgs; + int mRestoreHeight; + bool mAudioRepeat; + bool mConfirmAck; + bool mShowEdit; // display the Edit button + bool mNoDefer; // don't display a Defer option + bool mInvalid; // restored window is invalid + // Sound file playing + KArtsDispatcher* mArtsDispatcher; + KDE::PlayObject* mPlayObject; + QCString mKMixName; // DCOP name for KMix + QString mKMixError; // error message starting KMix + QTimer* mPlayTimer; // timer for repeating the sound file + QTimer* mFadeTimer; // timer for fading the sound volume + float mOldVolume; // volume before volume was set for sound file + QString mLocalAudioFile; // local copy of audio file + QTime mAudioFileStart; // time when audio file loading first started, or when play first started + int mAudioFileLoadSecs; // how many seconds it took to load audio file + bool mPlayedOnce; // the sound file has started playing at least once + bool mPlayed; // the PlayObject->play() has been called + // Miscellaneous + KAEvent mEvent; // the whole event, for updating the calendar file + QLabel* mRemainingText; // the remaining time (for a reminder window) + KPushButton* mOkButton; + QPushButton* mEditButton; + QPushButton* mDeferButton; + QPushButton* mSilenceButton; + QPushButton* mKAlarmButton; + QPushButton* mKMailButton; + DeferAlarmDlg* mDeferDlg; + QDateTime mDeferLimit; // last time to which the message can currently be deferred + mutable KWinModule* mWinModule; + int mFlags; + int mLateCancel; + int mButtonDelay; // delay (ms) after window is shown before buttons are enabled + bool mErrorWindow; // the window is simply an error message + bool mNoPostAction; // don't execute any post-alarm action + bool mRecreating; // window is about to be deleted and immediately recreated + bool mBeep; + bool mSpeak; // the message should be spoken via kttsd + bool mRescheduleEvent; // true to delete event after message has been displayed + bool mShown; // true once the window has been displayed + bool mPositioning; // true when the window is being positioned initially + bool mNoCloseConfirm; // the Defer or Edit button is closing the dialog + bool mUsingKMix; // master volume is being set using kmix + bool mDisableDeferral; // true if past deferral limit, so don't enable Defer button +}; + +#endif // MESSAGEWIN_H diff --git a/kalarm/pickfileradio.cpp b/kalarm/pickfileradio.cpp new file mode 100644 index 000000000..c00276502 --- /dev/null +++ b/kalarm/pickfileradio.cpp @@ -0,0 +1,182 @@ +/* + * pickfileradio.cpp - radio button with an associated file picker + * Program: kalarm + * Copyright (C) 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qbuttongroup.h> +#include <qpushbutton.h> +#include <qtimer.h> + +#include <kdebug.h> + +#include "lineedit.h" +#include "pickfileradio.moc" + + +PickFileRadio::PickFileRadio(QPushButton* button, LineEdit* edit, const QString& text, QButtonGroup* parent, const char* name) + : RadioButton(text, parent, name), + mGroup(parent), + mEdit(edit), + mButton(button), + mLastId(-1), // set to an invalid value + mRevertId(false) +{ + Q_ASSERT(parent); + Q_ASSERT(button); + mButton->setEnabled(false); + connect(mButton, SIGNAL(clicked()), SLOT(slotPickFile())); + if (mEdit) + mEdit->setEnabled(false); + connect(mGroup, SIGNAL(buttonSet(int)), SLOT(slotSelectionChanged(int))); +} + +PickFileRadio::PickFileRadio(const QString& text, QButtonGroup* parent, const char* name) + : RadioButton(text, parent, name), + mGroup(parent), + mEdit(0), + mButton(0), + mLastId(-1), // set to an invalid value + mRevertId(false) +{ + Q_ASSERT(parent); +} + +void PickFileRadio::init(QPushButton* button, LineEdit* edit) +{ + Q_ASSERT(button); + mEdit = edit; + mButton = button; + mButton->setEnabled(false); + connect(mButton, SIGNAL(clicked()), SLOT(slotPickFile())); + if (mEdit) + mEdit->setEnabled(false); + connect(mGroup, SIGNAL(buttonSet(int)), SLOT(slotSelectionChanged(int))); + setReadOnly(RadioButton::isReadOnly()); +} + +void PickFileRadio::setReadOnly(bool ro) +{ + RadioButton::setReadOnly(ro); + if (mButton) + { + if (mEdit) + mEdit->setReadOnly(ro); + if (ro) + mButton->hide(); + else + mButton->show(); + } +} + +void PickFileRadio::setFile(const QString& file) +{ + mFile = file; +} + +QString PickFileRadio::file() const +{ + return mEdit ? mEdit->text() : mFile; +} + +/****************************************************************************** +* Set the radio button enabled or disabled. +* Adjusts the enabled/disabled state of other controls appropriately. +*/ +void PickFileRadio::setEnabled(bool enable) +{ + Q_ASSERT(mButton); + RadioButton::setEnabled(enable); + enable = enable && mGroup->selected() == this; + if (enable) + { + if (!pickFileIfNone()) + enable = false; // revert to previously selected type + } + mButton->setEnabled(enable); + if (mEdit) + mEdit->setEnabled(enable); +} + +/****************************************************************************** +* Called when the selected radio button changes. +*/ +void PickFileRadio::slotSelectionChanged(int id) +{ + if (id == mLastId || mRevertId) + return; + int radioId = mGroup->id(this); + if (mLastId == radioId) + { + mButton->setEnabled(false); + if (mEdit) + mEdit->setEnabled(false); + } + else if (id == radioId) + { + if (!pickFileIfNone()) + return; // revert to previously selected type + mButton->setEnabled(true); + if (mEdit) + mEdit->setEnabled(true); + } + mLastId = id; +} + +/****************************************************************************** +* Prompt for a file name if there is none currently entered. +*/ +bool PickFileRadio::pickFileIfNone() +{ + if (mEdit) + mFile = mEdit->text(); + if (!mFile.isEmpty()) + return true; + slotPickFile(); + return !mFile.isEmpty(); +} + +/****************************************************************************** +* Called when the file picker button is clicked. +*/ +void PickFileRadio::slotPickFile() +{ + mFile = pickFile(); + if (mEdit) + mEdit->setText(mFile); + if (mFile.isEmpty()) + { + // No file is selected, so revert to the previous radio button selection. + // But wait a moment before setting the radio button, or it won't work. + mRevertId = true; // prevent picker dialogue popping up twice + QTimer::singleShot(0, this, SLOT(setLastId())); + } +} + +/****************************************************************************** +* Select the previously selected radio button in the group. +*/ +void PickFileRadio::setLastId() +{ + if (mLastId == -1) + setOn(false); // we don't know the previous selection, so just turn this button off + else + mGroup->setButton(mLastId); + mRevertId = false; +} diff --git a/kalarm/pickfileradio.h b/kalarm/pickfileradio.h new file mode 100644 index 000000000..5486a8a6e --- /dev/null +++ b/kalarm/pickfileradio.h @@ -0,0 +1,120 @@ +/* + * pickfileradio.h - radio button with an associated file picker + * Program: kalarm + * Copyright (C) 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef PICKFILERADIO_H +#define PICKFILERADIO_H + +/** @file pickfileradio.h - radio button with an associated file picker */ + +#include "radiobutton.h" + +class QPushButton; +class LineEdit; + +/** + * @short Radio button with associated file picker controls. + * + * The PickFileRadio class is a radio button with an associated button to choose + * a file, and an optional file name edit box. Its purpose is to ensure that while + * the radio button is selected, the chosen file name is never blank. + * + * To achieve this, whenever the button is newly selected and the + * file name is currently blank, the file picker dialogue is displayed to choose a + * file. If the dialogue exits without a file being chosen, the radio button selection + * is reverted to the previously selected button in the parent button group. + * + * The class handles the activation of the file picker dialogue (via a virtual method + * which must be supplied by deriving a class from this one). It also handles all + * enabling and disabling of the browse button and edit box when the enable state of + * the radio button is changed, and when the radio button selection changes. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class PickFileRadio : public RadioButton +{ + Q_OBJECT + public: + /** Constructor. + * @param button Push button to invoke the file picker dialogue. + * @param edit File name edit widget, or null if there is none. + * @param text Radio button's text. + * @param parent Button group which is to be the parent object for the radio button. + * @param name The name of this widget. + */ + PickFileRadio(QPushButton* button, LineEdit* edit, const QString& text, QButtonGroup* parent, const char* name = 0); + /** Constructor. + * The init() method must be called before the widget can be used. + * @param text Radio button's text. + * @param parent Button group which is to be the parent object for the radio button. + * @param name The name of this widget. + */ + PickFileRadio(const QString& text, QButtonGroup* parent, const char* name = 0); + /** Initialises the widget. + * @param button Push button to invoke the file picker dialogue. + * @param edit File name edit widget, or null if there is none. + */ + void init(QPushButton* button, LineEdit* edit = 0); + /** Sets whether the radio button and associated widgets are read-only for the user. + * If read-only, their states cannot be changed by the user. + * @param readOnly True to set the widgets read-only, false to set them read-write. + */ + virtual void setReadOnly(bool readOnly); + /** Chooses a file, for example by displaying a file selection dialogue. + * This method is called when the push button is clicked - the client + * should not activate a file selection dialogue directly. + * @return Selected file name, or QString::null if no selection made. + */ + virtual QString pickFile() = 0; + /** Notifies the widget of the currently selected file name. + * This should only be used when no file name edit box is used. + * It should be called to initialise the widget's data, and also any time the file + * name is changed without using the push button. + */ + void setFile(const QString& file); + /** Returns the currently selected file name. */ + QString file() const; + /** Returns the associated file name edit widget, or null if none. */ + LineEdit* fileEdit() const { return mEdit; } + /** Returns the associated file browse push button. */ + QPushButton* pushButton() const { return mButton; } + + public slots: + /** Enables or disables the radio button, and adjusts the enabled state of the + * associated browse button and file name edit box. + */ + virtual void setEnabled(bool); + + private slots: + void slotSelectionChanged(int id); + void slotPickFile(); + void setLastId(); + + private: + bool pickFileIfNone(); + + QButtonGroup* mGroup; // button group which radio button is in + LineEdit* mEdit; // file name edit box, or null if none + QPushButton* mButton; // push button to pick a file + QString mFile; // saved file name (if mEdit is null) + int mLastId; // previous radio button selected + bool mRevertId; // true to revert to the previous radio button selection +}; + +#endif // PICKFILERADIO_H diff --git a/kalarm/pixmaps/Makefile.am b/kalarm/pixmaps/Makefile.am new file mode 100644 index 000000000..e901fb442 --- /dev/null +++ b/kalarm/pixmaps/Makefile.am @@ -0,0 +1,3 @@ +kalarmicondir = $(kde_datadir)/kalarm/icons +kalarmicon_ICON = AUTO +KDE_ICON = kalarm diff --git a/kalarm/pixmaps/cr16-action-file.png b/kalarm/pixmaps/cr16-action-file.png Binary files differnew file mode 100644 index 000000000..037c2da98 --- /dev/null +++ b/kalarm/pixmaps/cr16-action-file.png diff --git a/kalarm/pixmaps/cr16-action-kalarm.png b/kalarm/pixmaps/cr16-action-kalarm.png Binary files differnew file mode 100644 index 000000000..ae4234de7 --- /dev/null +++ b/kalarm/pixmaps/cr16-action-kalarm.png diff --git a/kalarm/pixmaps/cr16-action-message.png b/kalarm/pixmaps/cr16-action-message.png Binary files differnew file mode 100644 index 000000000..d0f509d6d --- /dev/null +++ b/kalarm/pixmaps/cr16-action-message.png diff --git a/kalarm/pixmaps/cr16-action-new_from_template.png b/kalarm/pixmaps/cr16-action-new_from_template.png Binary files differnew file mode 100644 index 000000000..03b26d886 --- /dev/null +++ b/kalarm/pixmaps/cr16-action-new_from_template.png diff --git a/kalarm/pixmaps/cr16-action-playsound.png b/kalarm/pixmaps/cr16-action-playsound.png Binary files differnew file mode 100644 index 000000000..31e51f04d --- /dev/null +++ b/kalarm/pixmaps/cr16-action-playsound.png diff --git a/kalarm/pixmaps/cr22-action-kalarm.png b/kalarm/pixmaps/cr22-action-kalarm.png Binary files differnew file mode 100644 index 000000000..e2e174529 --- /dev/null +++ b/kalarm/pixmaps/cr22-action-kalarm.png diff --git a/kalarm/pixmaps/cr22-action-kalarm_disabled.png b/kalarm/pixmaps/cr22-action-kalarm_disabled.png Binary files differnew file mode 100644 index 000000000..5a98c4d4f --- /dev/null +++ b/kalarm/pixmaps/cr22-action-kalarm_disabled.png diff --git a/kalarm/pixmaps/cr22-action-new_from_template.png b/kalarm/pixmaps/cr22-action-new_from_template.png Binary files differnew file mode 100644 index 000000000..1eb518376 --- /dev/null +++ b/kalarm/pixmaps/cr22-action-new_from_template.png diff --git a/kalarm/prefdlg.cpp b/kalarm/prefdlg.cpp new file mode 100644 index 000000000..2b07c4995 --- /dev/null +++ b/kalarm/prefdlg.cpp @@ -0,0 +1,1363 @@ +/* + * prefdlg.cpp - program preferences dialog + * Program: kalarm + * Copyright © 2001-2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qobjectlist.h> +#include <qlayout.h> +#include <qbuttongroup.h> +#include <qvbox.h> +#include <qlineedit.h> +#include <qcheckbox.h> +#include <qradiobutton.h> +#include <qpushbutton.h> +#include <qcombobox.h> +#include <qwhatsthis.h> +#include <qtooltip.h> +#include <qstyle.h> + +#include <kglobal.h> +#include <klocale.h> +#include <kstandarddirs.h> +#include <kshell.h> +#include <kmessagebox.h> +#include <kaboutdata.h> +#include <kapplication.h> +#include <kiconloader.h> +#include <kcolorcombo.h> +#include <kstdguiitem.h> +#ifdef Q_WS_X11 +#include <kwin.h> +#endif +#include <kdebug.h> + +#include <kalarmd/kalarmd.h> + +#include "alarmcalendar.h" +#include "alarmtimewidget.h" +#include "daemon.h" +#include "editdlg.h" +#include "fontcolour.h" +#include "functions.h" +#include "kalarmapp.h" +#include "kamail.h" +#include "label.h" +#include "latecancel.h" +#include "mainwindow.h" +#include "preferences.h" +#include "radiobutton.h" +#include "recurrenceedit.h" +#ifndef WITHOUT_ARTS +#include "sounddlg.h" +#endif +#include "soundpicker.h" +#include "specialactions.h" +#include "timeedit.h" +#include "timespinbox.h" +#include "traywindow.h" +#include "prefdlg.moc" + +// Command strings for executing commands in different types of terminal windows. +// %t = window title parameter +// %c = command to execute in terminal +// %w = command to execute in terminal, with 'sleep 86400' appended +// %C = temporary command file to execute in terminal +// %W = temporary command file to execute in terminal, with 'sleep 86400' appended +static QString xtermCommands[] = { + QString::fromLatin1("xterm -sb -hold -title %t -e %c"), + QString::fromLatin1("konsole --noclose -T %t -e ${SHELL:-sh} -c %c"), + QString::fromLatin1("gnome-terminal -t %t -e %W"), + QString::fromLatin1("eterm --pause -T %t -e %C"), // some systems use eterm... + QString::fromLatin1("Eterm --pause -T %t -e %C"), // while some use Eterm + QString::fromLatin1("rxvt -title %t -e ${SHELL:-sh} -c %w"), + QString::null // end of list indicator - don't change! +}; + + +/*============================================================================= += Class KAlarmPrefDlg +=============================================================================*/ + +KAlarmPrefDlg* KAlarmPrefDlg::mInstance = 0; + +void KAlarmPrefDlg::display() +{ + if (!mInstance) + { + mInstance = new KAlarmPrefDlg; + mInstance->show(); + } + else + { +#ifdef Q_WS_X11 + KWin::WindowInfo info = KWin::windowInfo(mInstance->winId(), static_cast<unsigned long>(NET::WMGeometry | NET::WMDesktop)); + KWin::setCurrentDesktop(info.desktop()); +#endif + mInstance->showNormal(); // un-minimise it if necessary + mInstance->raise(); + mInstance->setActiveWindow(); + } +} + +KAlarmPrefDlg::KAlarmPrefDlg() + : KDialogBase(IconList, i18n("Preferences"), Help | Default | Ok | Apply | Cancel, Ok, 0, "PrefDlg", false, true) +{ + setWFlags(Qt::WDestructiveClose); + setIconListAllVisible(true); + + QVBox* frame = addVBoxPage(i18n("General"), i18n("General"), DesktopIcon("misc")); + mMiscPage = new MiscPrefTab(frame); + + frame = addVBoxPage(i18n("Email"), i18n("Email Alarm Settings"), DesktopIcon("mail_generic")); + mEmailPage = new EmailPrefTab(frame); + + frame = addVBoxPage(i18n("View"), i18n("View Settings"), DesktopIcon("view_choose")); + mViewPage = new ViewPrefTab(frame); + + frame = addVBoxPage(i18n("Font & Color"), i18n("Default Font and Color"), DesktopIcon("colorize")); + mFontColourPage = new FontColourPrefTab(frame); + + frame = addVBoxPage(i18n("Edit"), i18n("Default Alarm Edit Settings"), DesktopIcon("edit")); + mEditPage = new EditPrefTab(frame); + + restore(); + adjustSize(); +} + +KAlarmPrefDlg::~KAlarmPrefDlg() +{ + mInstance = 0; +} + +// Restore all defaults in the options... +void KAlarmPrefDlg::slotDefault() +{ + kdDebug(5950) << "KAlarmPrefDlg::slotDefault()" << endl; + mFontColourPage->setDefaults(); + mEmailPage->setDefaults(); + mViewPage->setDefaults(); + mEditPage->setDefaults(); + mMiscPage->setDefaults(); +} + +void KAlarmPrefDlg::slotHelp() +{ + kapp->invokeHelp("preferences"); +} + +// Apply the preferences that are currently selected +void KAlarmPrefDlg::slotApply() +{ + kdDebug(5950) << "KAlarmPrefDlg::slotApply()" << endl; + QString errmsg = mEmailPage->validate(); + if (!errmsg.isEmpty()) + { + showPage(pageIndex(mEmailPage->parentWidget())); + if (KMessageBox::warningYesNo(this, errmsg) != KMessageBox::Yes) + { + mValid = false; + return; + } + } + errmsg = mEditPage->validate(); + if (!errmsg.isEmpty()) + { + showPage(pageIndex(mEditPage->parentWidget())); + KMessageBox::sorry(this, errmsg); + mValid = false; + return; + } + mValid = true; + mFontColourPage->apply(false); + mEmailPage->apply(false); + mViewPage->apply(false); + mEditPage->apply(false); + mMiscPage->apply(false); + Preferences::syncToDisc(); +} + +// Apply the preferences that are currently selected +void KAlarmPrefDlg::slotOk() +{ + kdDebug(5950) << "KAlarmPrefDlg::slotOk()" << endl; + mValid = true; + slotApply(); + if (mValid) + KDialogBase::slotOk(); +} + +// Discard the current preferences and close the dialogue +void KAlarmPrefDlg::slotCancel() +{ + kdDebug(5950) << "KAlarmPrefDlg::slotCancel()" << endl; + restore(); + KDialogBase::slotCancel(); +} + +// Discard the current preferences and use the present ones +void KAlarmPrefDlg::restore() +{ + kdDebug(5950) << "KAlarmPrefDlg::restore()" << endl; + mFontColourPage->restore(); + mEmailPage->restore(); + mViewPage->restore(); + mEditPage->restore(); + mMiscPage->restore(); +} + + +/*============================================================================= += Class PrefsTabBase +=============================================================================*/ +int PrefsTabBase::mIndentWidth = 0; + +PrefsTabBase::PrefsTabBase(QVBox* frame) + : QWidget(frame), + mPage(frame) +{ + if (!mIndentWidth) + mIndentWidth = style().subRect(QStyle::SR_RadioButtonIndicator, this).width(); +} + +void PrefsTabBase::apply(bool syncToDisc) +{ + Preferences::save(syncToDisc); +} + + + +/*============================================================================= += Class MiscPrefTab +=============================================================================*/ + +MiscPrefTab::MiscPrefTab(QVBox* frame) + : PrefsTabBase(frame) +{ + // Get alignment to use in QGridLayout (AlignAuto doesn't work correctly there) + int alignment = QApplication::reverseLayout() ? Qt::AlignRight : Qt::AlignLeft; + + QGroupBox* group = new QButtonGroup(i18n("Run Mode"), mPage, "modeGroup"); + QGridLayout* grid = new QGridLayout(group, 6, 2, KDialog::marginHint(), KDialog::spacingHint()); + grid->setColStretch(2, 1); + grid->addColSpacing(0, indentWidth()); + grid->addColSpacing(1, indentWidth()); + grid->addRowSpacing(0, fontMetrics().lineSpacing()/2); + + // Run-on-demand radio button + mRunOnDemand = new QRadioButton(i18n("&Run only on demand"), group, "runDemand"); + mRunOnDemand->setFixedSize(mRunOnDemand->sizeHint()); + connect(mRunOnDemand, SIGNAL(toggled(bool)), SLOT(slotRunModeToggled(bool))); + QWhatsThis::add(mRunOnDemand, + i18n("Check to run KAlarm only when required.\n\n" + "Notes:\n" + "1. Alarms are displayed even when KAlarm is not running, since alarm monitoring is done by the alarm daemon.\n" + "2. With this option selected, the system tray icon can be displayed or hidden independently of KAlarm.")); + grid->addMultiCellWidget(mRunOnDemand, 1, 1, 0, 2, alignment); + + // Run-in-system-tray radio button + mRunInSystemTray = new QRadioButton(i18n("Run continuously in system &tray"), group, "runTray"); + mRunInSystemTray->setFixedSize(mRunInSystemTray->sizeHint()); + connect(mRunInSystemTray, SIGNAL(toggled(bool)), SLOT(slotRunModeToggled(bool))); + QWhatsThis::add(mRunInSystemTray, + i18n("Check to run KAlarm continuously in the KDE system tray.\n\n" + "Notes:\n" + "1. With this option selected, closing the system tray icon will quit KAlarm.\n" + "2. You do not need to select this option in order for alarms to be displayed, since alarm monitoring is done by the alarm daemon." + " Running in the system tray simply provides easy access and a status indication.")); + grid->addMultiCellWidget(mRunInSystemTray, 2, 2, 0, 2, alignment); + + // Run continuously options + mDisableAlarmsIfStopped = new QCheckBox(i18n("Disa&ble alarms while not running"), group, "disableAl"); + mDisableAlarmsIfStopped->setFixedSize(mDisableAlarmsIfStopped->sizeHint()); + connect(mDisableAlarmsIfStopped, SIGNAL(toggled(bool)), SLOT(slotDisableIfStoppedToggled(bool))); + QWhatsThis::add(mDisableAlarmsIfStopped, + i18n("Check to disable alarms whenever KAlarm is not running. Alarms will only appear while the system tray icon is visible.")); + grid->addMultiCellWidget(mDisableAlarmsIfStopped, 3, 3, 1, 2, alignment); + + mQuitWarn = new QCheckBox(i18n("Warn before &quitting"), group, "disableAl"); + mQuitWarn->setFixedSize(mQuitWarn->sizeHint()); + QWhatsThis::add(mQuitWarn, + i18n("Check to display a warning prompt before quitting KAlarm.")); + grid->addWidget(mQuitWarn, 4, 2, alignment); + + mAutostartTrayIcon = new QCheckBox(i18n("Autostart at &login"), group, "autoTray"); +#ifdef AUTOSTART_BY_KALARMD + connect(mAutostartTrayIcon, SIGNAL(toggled(bool)), SLOT(slotAutostartToggled(bool))); +#endif + grid->addMultiCellWidget(mAutostartTrayIcon, 5, 5, 0, 2, alignment); + + // Autostart alarm daemon + mAutostartDaemon = new QCheckBox(i18n("Start alarm monitoring at lo&gin"), group, "startDaemon"); + mAutostartDaemon->setFixedSize(mAutostartDaemon->sizeHint()); + connect(mAutostartDaemon, SIGNAL(clicked()), SLOT(slotAutostartDaemonClicked())); + QWhatsThis::add(mAutostartDaemon, + i18n("Automatically start alarm monitoring whenever you start KDE, by running the alarm daemon (%1).\n\n" + "This option should always be checked unless you intend to discontinue use of KAlarm.") + .arg(QString::fromLatin1(DAEMON_APP_NAME))); + grid->addMultiCellWidget(mAutostartDaemon, 6, 6, 0, 2, alignment); + + group->setFixedHeight(group->sizeHint().height()); + + // Start-of-day time + QHBox* itemBox = new QHBox(mPage); + QHBox* box = new QHBox(itemBox); // this is to control the QWhatsThis text display area + box->setSpacing(KDialog::spacingHint()); + QLabel* label = new QLabel(i18n("&Start of day for date-only alarms:"), box); + mStartOfDay = new TimeEdit(box); + mStartOfDay->setFixedSize(mStartOfDay->sizeHint()); + label->setBuddy(mStartOfDay); + static const QString startOfDayText = i18n("The earliest time of day at which a date-only alarm (i.e. " + "an alarm with \"any time\" specified) will be triggered."); + QWhatsThis::add(box, QString("%1\n\n%2").arg(startOfDayText).arg(TimeSpinBox::shiftWhatsThis())); + itemBox->setStretchFactor(new QWidget(itemBox), 1); // left adjust the controls + itemBox->setFixedHeight(box->sizeHint().height()); + + // Confirm alarm deletion? + itemBox = new QHBox(mPage); // this is to allow left adjustment + mConfirmAlarmDeletion = new QCheckBox(i18n("Con&firm alarm deletions"), itemBox, "confirmDeletion"); + mConfirmAlarmDeletion->setMinimumSize(mConfirmAlarmDeletion->sizeHint()); + QWhatsThis::add(mConfirmAlarmDeletion, + i18n("Check to be prompted for confirmation each time you delete an alarm.")); + itemBox->setStretchFactor(new QWidget(itemBox), 1); // left adjust the controls + itemBox->setFixedHeight(itemBox->sizeHint().height()); + + // Expired alarms + group = new QGroupBox(i18n("Expired Alarms"), mPage); + grid = new QGridLayout(group, 2, 2, KDialog::marginHint(), KDialog::spacingHint()); + grid->setColStretch(1, 1); + grid->addColSpacing(0, indentWidth()); + grid->addRowSpacing(0, fontMetrics().lineSpacing()/2); + mKeepExpired = new QCheckBox(i18n("Keep alarms after e&xpiry"), group, "keepExpired"); + mKeepExpired->setFixedSize(mKeepExpired->sizeHint()); + connect(mKeepExpired, SIGNAL(toggled(bool)), SLOT(slotExpiredToggled(bool))); + QWhatsThis::add(mKeepExpired, + i18n("Check to store alarms after expiry or deletion (except deleted alarms which were never triggered).")); + grid->addMultiCellWidget(mKeepExpired, 1, 1, 0, 1, alignment); + + box = new QHBox(group); + box->setSpacing(KDialog::spacingHint()); + mPurgeExpired = new QCheckBox(i18n("Discard ex&pired alarms after:"), box, "purgeExpired"); + mPurgeExpired->setMinimumSize(mPurgeExpired->sizeHint()); + connect(mPurgeExpired, SIGNAL(toggled(bool)), SLOT(slotExpiredToggled(bool))); + mPurgeAfter = new SpinBox(box); + mPurgeAfter->setMinValue(1); + mPurgeAfter->setLineShiftStep(10); + mPurgeAfter->setMinimumSize(mPurgeAfter->sizeHint()); + mPurgeAfterLabel = new QLabel(i18n("da&ys"), box); + mPurgeAfterLabel->setMinimumSize(mPurgeAfterLabel->sizeHint()); + mPurgeAfterLabel->setBuddy(mPurgeAfter); + QWhatsThis::add(box, + i18n("Uncheck to store expired alarms indefinitely. Check to enter how long expired alarms should be stored.")); + grid->addWidget(box, 2, 1, alignment); + + mClearExpired = new QPushButton(i18n("Clear Expired Alar&ms"), group); + mClearExpired->setFixedSize(mClearExpired->sizeHint()); + connect(mClearExpired, SIGNAL(clicked()), SLOT(slotClearExpired())); + QWhatsThis::add(mClearExpired, + i18n("Delete all existing expired alarms.")); + grid->addWidget(mClearExpired, 3, 1, alignment); + group->setFixedHeight(group->sizeHint().height()); + + // Terminal window to use for command alarms + group = new QGroupBox(i18n("Terminal for Command Alarms"), mPage); + QWhatsThis::add(group, + i18n("Choose which application to use when a command alarm is executed in a terminal window")); + grid = new QGridLayout(group, 1, 3, KDialog::marginHint(), KDialog::spacingHint()); + grid->addRowSpacing(0, fontMetrics().lineSpacing()/2); + int row = 0; + + mXtermType = new QButtonGroup(group); + mXtermType->hide(); + QString whatsThis = i18n("The parameter is a command line, e.g. 'xterm -e'", "Check to execute command alarms in a terminal window by '%1'"); + int index = 0; + mXtermFirst = -1; + for (mXtermCount = 0; !xtermCommands[mXtermCount].isNull(); ++mXtermCount) + { + QString cmd = xtermCommands[mXtermCount]; + QStringList args = KShell::splitArgs(cmd); + if (args.isEmpty() || KStandardDirs::findExe(args[0]).isEmpty()) + continue; + QRadioButton* radio = new QRadioButton(args[0], group); + radio->setMinimumSize(radio->sizeHint()); + mXtermType->insert(radio, mXtermCount); + if (mXtermFirst < 0) + mXtermFirst = mXtermCount; // note the id of the first button + cmd.replace("%t", kapp->aboutData()->programName()); + cmd.replace("%c", "<command>"); + cmd.replace("%w", "<command; sleep>"); + cmd.replace("%C", "[command]"); + cmd.replace("%W", "[command; sleep]"); + QWhatsThis::add(radio, whatsThis.arg(cmd)); + grid->addWidget(radio, (row = index/3 + 1), index % 3, Qt::AlignAuto); + ++index; + } + + box = new QHBox(group); + grid->addMultiCellWidget(box, row + 1, row + 1, 0, 2, Qt::AlignAuto); + QRadioButton* radio = new QRadioButton(i18n("Other:"), box); + radio->setFixedSize(radio->sizeHint()); + connect(radio, SIGNAL(toggled(bool)), SLOT(slotOtherTerminalToggled(bool))); + mXtermType->insert(radio, mXtermCount); + if (mXtermFirst < 0) + mXtermFirst = mXtermCount; // note the id of the first button + mXtermCommand = new QLineEdit(box); + QWhatsThis::add(box, + i18n("Enter the full command line needed to execute a command in your chosen terminal window. " + "By default the alarm's command string will be appended to what you enter here. " + "See the KAlarm Handbook for details of special codes to tailor the command line.")); + + mPage->setStretchFactor(new QWidget(mPage), 1); // top adjust the widgets +} + +void MiscPrefTab::restore() +{ + mAutostartDaemon->setChecked(Daemon::autoStart()); + bool systray = Preferences::mRunInSystemTray; + mRunInSystemTray->setChecked(systray); + mRunOnDemand->setChecked(!systray); + mDisableAlarmsIfStopped->setChecked(Preferences::mDisableAlarmsIfStopped); + mQuitWarn->setChecked(Preferences::quitWarn()); + mAutostartTrayIcon->setChecked(Preferences::mAutostartTrayIcon); + mConfirmAlarmDeletion->setChecked(Preferences::confirmAlarmDeletion()); + mStartOfDay->setValue(Preferences::mStartOfDay); + setExpiredControls(Preferences::mExpiredKeepDays); + QString xtermCmd = Preferences::cmdXTermCommand(); + int id = mXtermFirst; + if (!xtermCmd.isEmpty()) + { + for ( ; id < mXtermCount; ++id) + { + if (mXtermType->find(id) && xtermCmd == xtermCommands[id]) + break; + } + } + mXtermType->setButton(id); + mXtermCommand->setEnabled(id == mXtermCount); + mXtermCommand->setText(id == mXtermCount ? xtermCmd : ""); + slotDisableIfStoppedToggled(true); +} + +void MiscPrefTab::apply(bool syncToDisc) +{ + // First validate anything entered in Other X-terminal command + int xtermID = mXtermType->selectedId(); + if (xtermID >= mXtermCount) + { + QString cmd = mXtermCommand->text(); + if (cmd.isEmpty()) + xtermID = -1; // 'Other' is only acceptable if it's non-blank + else + { + QStringList args = KShell::splitArgs(cmd); + cmd = args.isEmpty() ? QString::null : args[0]; + if (KStandardDirs::findExe(cmd).isEmpty()) + { + mXtermCommand->setFocus(); + if (KMessageBox::warningContinueCancel(this, i18n("Command to invoke terminal window not found:\n%1").arg(cmd)) + != KMessageBox::Continue) + return; + } + } + } + if (xtermID < 0) + { + xtermID = mXtermFirst; + mXtermType->setButton(mXtermFirst); + } + + bool systray = mRunInSystemTray->isChecked(); + Preferences::mRunInSystemTray = systray; + Preferences::mDisableAlarmsIfStopped = mDisableAlarmsIfStopped->isChecked(); + if (mQuitWarn->isEnabled()) + Preferences::setQuitWarn(mQuitWarn->isChecked()); + Preferences::mAutostartTrayIcon = mAutostartTrayIcon->isChecked(); +#ifdef AUTOSTART_BY_KALARMD + bool newAutostartDaemon = mAutostartDaemon->isChecked() || Preferences::mAutostartTrayIcon; +#else + bool newAutostartDaemon = mAutostartDaemon->isChecked(); +#endif + if (newAutostartDaemon != Daemon::autoStart()) + Daemon::enableAutoStart(newAutostartDaemon); + Preferences::setConfirmAlarmDeletion(mConfirmAlarmDeletion->isChecked()); + int sod = mStartOfDay->value(); + Preferences::mStartOfDay.setHMS(sod/60, sod%60, 0); + Preferences::mExpiredKeepDays = !mKeepExpired->isChecked() ? 0 + : mPurgeExpired->isChecked() ? mPurgeAfter->value() : -1; + Preferences::mCmdXTermCommand = (xtermID < mXtermCount) ? xtermCommands[xtermID] : mXtermCommand->text(); + PrefsTabBase::apply(syncToDisc); +} + +void MiscPrefTab::setDefaults() +{ + mAutostartDaemon->setChecked(true); + bool systray = Preferences::default_runInSystemTray; + mRunInSystemTray->setChecked(systray); + mRunOnDemand->setChecked(!systray); + mDisableAlarmsIfStopped->setChecked(Preferences::default_disableAlarmsIfStopped); + mQuitWarn->setChecked(Preferences::default_quitWarn); + mAutostartTrayIcon->setChecked(Preferences::default_autostartTrayIcon); + mConfirmAlarmDeletion->setChecked(Preferences::default_confirmAlarmDeletion); + mStartOfDay->setValue(Preferences::default_startOfDay); + setExpiredControls(Preferences::default_expiredKeepDays); + mXtermType->setButton(mXtermFirst); + mXtermCommand->setEnabled(false); + slotDisableIfStoppedToggled(true); +} + +void MiscPrefTab::slotAutostartDaemonClicked() +{ + if (!mAutostartDaemon->isChecked() + && KMessageBox::warningYesNo(this, + i18n("You should not uncheck this option unless you intend to discontinue use of KAlarm"), + QString::null, KStdGuiItem::cont(), KStdGuiItem::cancel() + ) != KMessageBox::Yes) + mAutostartDaemon->setChecked(true); +} + +void MiscPrefTab::slotRunModeToggled(bool) +{ + bool systray = mRunInSystemTray->isOn(); + mAutostartTrayIcon->setText(systray ? i18n("Autostart at &login") : i18n("Autostart system tray &icon at login")); + QWhatsThis::add(mAutostartTrayIcon, (systray ? i18n("Check to run KAlarm whenever you start KDE.") + : i18n("Check to display the system tray icon whenever you start KDE."))); + mDisableAlarmsIfStopped->setEnabled(systray); + slotDisableIfStoppedToggled(true); +} + +/****************************************************************************** +* If autostart at login is selected, the daemon must be autostarted so that it +* can autostart KAlarm, in which case disable the daemon autostart option. +*/ +void MiscPrefTab::slotAutostartToggled(bool) +{ +#ifdef AUTOSTART_BY_KALARMD + mAutostartDaemon->setEnabled(!mAutostartTrayIcon->isChecked()); +#endif +} + +void MiscPrefTab::slotDisableIfStoppedToggled(bool) +{ + bool enable = mDisableAlarmsIfStopped->isEnabled() && mDisableAlarmsIfStopped->isChecked(); + mQuitWarn->setEnabled(enable); +} + +void MiscPrefTab::setExpiredControls(int purgeDays) +{ + mKeepExpired->setChecked(purgeDays); + mPurgeExpired->setChecked(purgeDays > 0); + mPurgeAfter->setValue(purgeDays > 0 ? purgeDays : 0); + slotExpiredToggled(true); +} + +void MiscPrefTab::slotExpiredToggled(bool) +{ + bool keep = mKeepExpired->isChecked(); + bool after = keep && mPurgeExpired->isChecked(); + mPurgeExpired->setEnabled(keep); + mPurgeAfter->setEnabled(after); + mPurgeAfterLabel->setEnabled(keep); + mClearExpired->setEnabled(keep); +} + +void MiscPrefTab::slotClearExpired() +{ + AlarmCalendar* cal = AlarmCalendar::expiredCalendarOpen(); + if (cal) + cal->purgeAll(); +} + +void MiscPrefTab::slotOtherTerminalToggled(bool on) +{ + mXtermCommand->setEnabled(on); +} + + +/*============================================================================= += Class EmailPrefTab +=============================================================================*/ + +EmailPrefTab::EmailPrefTab(QVBox* frame) + : PrefsTabBase(frame), + mAddressChanged(false), + mBccAddressChanged(false) +{ + QHBox* box = new QHBox(mPage); + box->setSpacing(2*KDialog::spacingHint()); + QLabel* label = new QLabel(i18n("Email client:"), box); + mEmailClient = new ButtonGroup(box); + mEmailClient->hide(); + RadioButton* radio = new RadioButton(i18n("&KMail"), box, "kmail"); + radio->setMinimumSize(radio->sizeHint()); + mEmailClient->insert(radio, Preferences::KMAIL); + radio = new RadioButton(i18n("&Sendmail"), box, "sendmail"); + radio->setMinimumSize(radio->sizeHint()); + mEmailClient->insert(radio, Preferences::SENDMAIL); + connect(mEmailClient, SIGNAL(buttonSet(int)), SLOT(slotEmailClientChanged(int))); + box->setFixedHeight(box->sizeHint().height()); + QWhatsThis::add(box, + i18n("Choose how to send email when an email alarm is triggered.\n" + "KMail: The email is sent automatically via KMail. KMail is started first if necessary.\n" + "Sendmail: The email is sent automatically. This option will only work if " + "your system is configured to use sendmail or a sendmail compatible mail transport agent.")); + + box = new QHBox(mPage); // this is to allow left adjustment + mEmailCopyToKMail = new QCheckBox(i18n("Co&py sent emails into KMail's %1 folder").arg(KAMail::i18n_sent_mail()), box); + mEmailCopyToKMail->setFixedSize(mEmailCopyToKMail->sizeHint()); + QWhatsThis::add(mEmailCopyToKMail, + i18n("After sending an email, store a copy in KMail's %1 folder").arg(KAMail::i18n_sent_mail())); + box->setStretchFactor(new QWidget(box), 1); // left adjust the controls + box->setFixedHeight(box->sizeHint().height()); + + // Your Email Address group box + QGroupBox* group = new QGroupBox(i18n("Your Email Address"), mPage); + QGridLayout* grid = new QGridLayout(group, 6, 3, KDialog::marginHint(), KDialog::spacingHint()); + grid->addRowSpacing(0, fontMetrics().lineSpacing()/2); + grid->setColStretch(1, 1); + + // 'From' email address controls ... + label = new Label(EditAlarmDlg::i18n_f_EmailFrom(), group); + label->setFixedSize(label->sizeHint()); + grid->addWidget(label, 1, 0); + mFromAddressGroup = new ButtonGroup(group); + mFromAddressGroup->hide(); + connect(mFromAddressGroup, SIGNAL(buttonSet(int)), SLOT(slotFromAddrChanged(int))); + + // Line edit to enter a 'From' email address + radio = new RadioButton(group); + mFromAddressGroup->insert(radio, Preferences::MAIL_FROM_ADDR); + radio->setFixedSize(radio->sizeHint()); + label->setBuddy(radio); + grid->addWidget(radio, 1, 1); + mEmailAddress = new QLineEdit(group); + connect(mEmailAddress, SIGNAL(textChanged(const QString&)), SLOT(slotAddressChanged())); + QString whatsThis = i18n("Your email address, used to identify you as the sender when sending email alarms."); + QWhatsThis::add(radio, whatsThis); + QWhatsThis::add(mEmailAddress, whatsThis); + radio->setFocusWidget(mEmailAddress); + grid->addWidget(mEmailAddress, 1, 2); + + // 'From' email address to be taken from Control Centre + radio = new RadioButton(i18n("&Use address from Control Center"), group); + radio->setFixedSize(radio->sizeHint()); + mFromAddressGroup->insert(radio, Preferences::MAIL_FROM_CONTROL_CENTRE); + QWhatsThis::add(radio, + i18n("Check to use the email address set in the KDE Control Center, to identify you as the sender when sending email alarms.")); + grid->addMultiCellWidget(radio, 2, 2, 1, 2, Qt::AlignAuto); + + // 'From' email address to be picked from KMail's identities when the email alarm is configured + radio = new RadioButton(i18n("Use KMail &identities"), group); + radio->setFixedSize(radio->sizeHint()); + mFromAddressGroup->insert(radio, Preferences::MAIL_FROM_KMAIL); + QWhatsThis::add(radio, + i18n("Check to use KMail's email identities to identify you as the sender when sending email alarms. " + "For existing email alarms, KMail's default identity will be used. " + "For new email alarms, you will be able to pick which of KMail's identities to use.")); + grid->addMultiCellWidget(radio, 3, 3, 1, 2, Qt::AlignAuto); + + // 'Bcc' email address controls ... + grid->addRowSpacing(4, KDialog::spacingHint()); + label = new Label(i18n("'Bcc' email address", "&Bcc:"), group); + label->setFixedSize(label->sizeHint()); + grid->addWidget(label, 5, 0); + mBccAddressGroup = new ButtonGroup(group); + mBccAddressGroup->hide(); + connect(mBccAddressGroup, SIGNAL(buttonSet(int)), SLOT(slotBccAddrChanged(int))); + + // Line edit to enter a 'Bcc' email address + radio = new RadioButton(group); + radio->setFixedSize(radio->sizeHint()); + mBccAddressGroup->insert(radio, Preferences::MAIL_FROM_ADDR); + label->setBuddy(radio); + grid->addWidget(radio, 5, 1); + mEmailBccAddress = new QLineEdit(group); + whatsThis = i18n("Your email address, used for blind copying email alarms to yourself. " + "If you want blind copies to be sent to your account on the computer which KAlarm runs on, you can simply enter your user login name."); + QWhatsThis::add(radio, whatsThis); + QWhatsThis::add(mEmailBccAddress, whatsThis); + radio->setFocusWidget(mEmailBccAddress); + grid->addWidget(mEmailBccAddress, 5, 2); + + // 'Bcc' email address to be taken from Control Centre + radio = new RadioButton(i18n("Us&e address from Control Center"), group); + radio->setFixedSize(radio->sizeHint()); + mBccAddressGroup->insert(radio, Preferences::MAIL_FROM_CONTROL_CENTRE); + QWhatsThis::add(radio, + i18n("Check to use the email address set in the KDE Control Center, for blind copying email alarms to yourself.")); + grid->addMultiCellWidget(radio, 6, 6, 1, 2, Qt::AlignAuto); + + group->setFixedHeight(group->sizeHint().height()); + + box = new QHBox(mPage); // this is to allow left adjustment + mEmailQueuedNotify = new QCheckBox(i18n("&Notify when remote emails are queued"), box); + mEmailQueuedNotify->setFixedSize(mEmailQueuedNotify->sizeHint()); + QWhatsThis::add(mEmailQueuedNotify, + i18n("Display a notification message whenever an email alarm has queued an email for sending to a remote system. " + "This could be useful if, for example, you have a dial-up connection, so that you can then ensure that the email is actually transmitted.")); + box->setStretchFactor(new QWidget(box), 1); // left adjust the controls + box->setFixedHeight(box->sizeHint().height()); + + mPage->setStretchFactor(new QWidget(mPage), 1); // top adjust the widgets +} + +void EmailPrefTab::restore() +{ + mEmailClient->setButton(Preferences::mEmailClient); + mEmailCopyToKMail->setChecked(Preferences::emailCopyToKMail()); + setEmailAddress(Preferences::mEmailFrom, Preferences::mEmailAddress); + setEmailBccAddress((Preferences::mEmailBccFrom == Preferences::MAIL_FROM_CONTROL_CENTRE), Preferences::mEmailBccAddress); + mEmailQueuedNotify->setChecked(Preferences::emailQueuedNotify()); + mAddressChanged = mBccAddressChanged = false; +} + +void EmailPrefTab::apply(bool syncToDisc) +{ + int client = mEmailClient->id(mEmailClient->selected()); + Preferences::mEmailClient = (client >= 0) ? Preferences::MailClient(client) : Preferences::default_emailClient; + Preferences::mEmailCopyToKMail = mEmailCopyToKMail->isChecked(); + Preferences::setEmailAddress(static_cast<Preferences::MailFrom>(mFromAddressGroup->selectedId()), mEmailAddress->text().stripWhiteSpace()); + Preferences::setEmailBccAddress((mBccAddressGroup->selectedId() == Preferences::MAIL_FROM_CONTROL_CENTRE), mEmailBccAddress->text().stripWhiteSpace()); + Preferences::setEmailQueuedNotify(mEmailQueuedNotify->isChecked()); + PrefsTabBase::apply(syncToDisc); +} + +void EmailPrefTab::setDefaults() +{ + mEmailClient->setButton(Preferences::default_emailClient); + setEmailAddress(Preferences::default_emailFrom(), Preferences::default_emailAddress); + setEmailBccAddress((Preferences::default_emailBccFrom == Preferences::MAIL_FROM_CONTROL_CENTRE), Preferences::default_emailBccAddress); + mEmailQueuedNotify->setChecked(Preferences::default_emailQueuedNotify); +} + +void EmailPrefTab::setEmailAddress(Preferences::MailFrom from, const QString& address) +{ + mFromAddressGroup->setButton(from); + mEmailAddress->setText(from == Preferences::MAIL_FROM_ADDR ? address.stripWhiteSpace() : QString()); +} + +void EmailPrefTab::setEmailBccAddress(bool useControlCentre, const QString& address) +{ + mBccAddressGroup->setButton(useControlCentre ? Preferences::MAIL_FROM_CONTROL_CENTRE : Preferences::MAIL_FROM_ADDR); + mEmailBccAddress->setText(useControlCentre ? QString() : address.stripWhiteSpace()); +} + +void EmailPrefTab::slotEmailClientChanged(int id) +{ + mEmailCopyToKMail->setEnabled(id == Preferences::SENDMAIL); +} + +void EmailPrefTab::slotFromAddrChanged(int id) +{ + mEmailAddress->setEnabled(id == Preferences::MAIL_FROM_ADDR); + mAddressChanged = true; +} + +void EmailPrefTab::slotBccAddrChanged(int id) +{ + mEmailBccAddress->setEnabled(id == Preferences::MAIL_FROM_ADDR); + mBccAddressChanged = true; +} + +QString EmailPrefTab::validate() +{ + if (mAddressChanged) + { + mAddressChanged = false; + QString errmsg = validateAddr(mFromAddressGroup, mEmailAddress, KAMail::i18n_NeedFromEmailAddress()); + if (!errmsg.isEmpty()) + return errmsg; + } + if (mBccAddressChanged) + { + mBccAddressChanged = false; + return validateAddr(mBccAddressGroup, mEmailBccAddress, i18n("No valid 'Bcc' email address is specified.")); + } + return QString::null; +} + +QString EmailPrefTab::validateAddr(ButtonGroup* group, QLineEdit* addr, const QString& msg) +{ + QString errmsg = i18n("%1\nAre you sure you want to save your changes?").arg(msg); + switch (group->selectedId()) + { + case Preferences::MAIL_FROM_CONTROL_CENTRE: + if (!KAMail::controlCentreAddress().isEmpty()) + return QString::null; + errmsg = i18n("No email address is currently set in the KDE Control Center. %1").arg(errmsg); + break; + case Preferences::MAIL_FROM_KMAIL: + if (KAMail::identitiesExist()) + return QString::null; + errmsg = i18n("No KMail identities currently exist. %1").arg(errmsg); + break; + case Preferences::MAIL_FROM_ADDR: + if (!addr->text().stripWhiteSpace().isEmpty()) + return QString::null; + break; + } + return errmsg; +} + + +/*============================================================================= += Class FontColourPrefTab +=============================================================================*/ + +FontColourPrefTab::FontColourPrefTab(QVBox* frame) + : PrefsTabBase(frame) +{ + mFontChooser = new FontColourChooser(mPage, 0, false, QStringList(), i18n("Message Font && Color"), true, false); + mPage->setStretchFactor(mFontChooser, 1); + + QFrame* layoutBox = new QFrame(mPage); + QHBoxLayout* hlayout = new QHBoxLayout(layoutBox); + QVBoxLayout* colourLayout = new QVBoxLayout(hlayout, KDialog::spacingHint()); + hlayout->addStretch(); + + QHBox* box = new QHBox(layoutBox); // to group widgets for QWhatsThis text + box->setSpacing(KDialog::spacingHint()/2); + colourLayout->addWidget(box); + QLabel* label1 = new QLabel(i18n("Di&sabled alarm color:"), box); + box->setStretchFactor(new QWidget(box), 1); + mDisabledColour = new KColorCombo(box); + label1->setBuddy(mDisabledColour); + QWhatsThis::add(box, + i18n("Choose the text color in the alarm list for disabled alarms.")); + + box = new QHBox(layoutBox); // to group widgets for QWhatsThis text + box->setSpacing(KDialog::spacingHint()/2); + colourLayout->addWidget(box); + QLabel* label2 = new QLabel(i18n("E&xpired alarm color:"), box); + box->setStretchFactor(new QWidget(box), 1); + mExpiredColour = new KColorCombo(box); + label2->setBuddy(mExpiredColour); + QWhatsThis::add(box, + i18n("Choose the text color in the alarm list for expired alarms.")); +} + +void FontColourPrefTab::restore() +{ + mFontChooser->setBgColour(Preferences::mDefaultBgColour); + mFontChooser->setColours(Preferences::mMessageColours); + mFontChooser->setFont(Preferences::mMessageFont); + mDisabledColour->setColor(Preferences::mDisabledColour); + mExpiredColour->setColor(Preferences::mExpiredColour); +} + +void FontColourPrefTab::apply(bool syncToDisc) +{ + Preferences::mDefaultBgColour = mFontChooser->bgColour(); + Preferences::mMessageColours = mFontChooser->colours(); + Preferences::mMessageFont = mFontChooser->font(); + Preferences::mDisabledColour = mDisabledColour->color(); + Preferences::mExpiredColour = mExpiredColour->color(); + PrefsTabBase::apply(syncToDisc); +} + +void FontColourPrefTab::setDefaults() +{ + mFontChooser->setBgColour(Preferences::default_defaultBgColour); + mFontChooser->setColours(Preferences::default_messageColours); + mFontChooser->setFont(Preferences::default_messageFont()); + mDisabledColour->setColor(Preferences::default_disabledColour); + mExpiredColour->setColor(Preferences::default_expiredColour); +} + + +/*============================================================================= += Class EditPrefTab +=============================================================================*/ + +EditPrefTab::EditPrefTab(QVBox* frame) + : PrefsTabBase(frame) +{ + // Get alignment to use in QLabel::setAlignment(alignment | Qt::WordBreak) + // (AlignAuto doesn't work correctly there) + int alignment = QApplication::reverseLayout() ? Qt::AlignRight : Qt::AlignLeft; + + int groupTopMargin = fontMetrics().lineSpacing()/2; + QString defsetting = i18n("The default setting for \"%1\" in the alarm edit dialog."); + QString soundSetting = i18n("Check to select %1 as the default setting for \"%2\" in the alarm edit dialog."); + + // DISPLAY ALARMS + QGroupBox* group = new QGroupBox(i18n("Display Alarms"), mPage); + QBoxLayout* layout = new QVBoxLayout(group, KDialog::marginHint(), KDialog::spacingHint()); + layout->addSpacing(groupTopMargin); + + mConfirmAck = new QCheckBox(EditAlarmDlg::i18n_k_ConfirmAck(), group, "defConfAck"); + mConfirmAck->setMinimumSize(mConfirmAck->sizeHint()); + QWhatsThis::add(mConfirmAck, defsetting.arg(EditAlarmDlg::i18n_ConfirmAck())); + layout->addWidget(mConfirmAck, 0, Qt::AlignAuto); + + mAutoClose = new QCheckBox(LateCancelSelector::i18n_i_AutoCloseWinLC(), group, "defAutoClose"); + mAutoClose->setMinimumSize(mAutoClose->sizeHint()); + QWhatsThis::add(mAutoClose, defsetting.arg(LateCancelSelector::i18n_AutoCloseWin())); + layout->addWidget(mAutoClose, 0, Qt::AlignAuto); + + QHBox* box = new QHBox(group); + box->setSpacing(KDialog::spacingHint()); + layout->addWidget(box); + QLabel* label = new QLabel(i18n("Reminder &units:"), box); + label->setFixedSize(label->sizeHint()); + mReminderUnits = new QComboBox(box, "defWarnUnits"); + mReminderUnits->insertItem(TimePeriod::i18n_Minutes(), TimePeriod::MINUTES); + mReminderUnits->insertItem(TimePeriod::i18n_Hours_Mins(), TimePeriod::HOURS_MINUTES); + mReminderUnits->insertItem(TimePeriod::i18n_Days(), TimePeriod::DAYS); + mReminderUnits->insertItem(TimePeriod::i18n_Weeks(), TimePeriod::WEEKS); + mReminderUnits->setFixedSize(mReminderUnits->sizeHint()); + label->setBuddy(mReminderUnits); + QWhatsThis::add(box, + i18n("The default units for the reminder in the alarm edit dialog.")); + box->setStretchFactor(new QWidget(box), 1); // left adjust the control + + mSpecialActionsButton = new SpecialActionsButton(EditAlarmDlg::i18n_SpecialActions(), box); + mSpecialActionsButton->setFixedSize(mSpecialActionsButton->sizeHint()); + + // SOUND + QButtonGroup* bgroup = new QButtonGroup(SoundPicker::i18n_Sound(), mPage, "soundGroup"); + layout = new QVBoxLayout(bgroup, KDialog::marginHint(), KDialog::spacingHint()); + layout->addSpacing(groupTopMargin); + + QBoxLayout* hlayout = new QHBoxLayout(layout, KDialog::spacingHint()); + mSound = new QComboBox(false, bgroup, "defSound"); + mSound->insertItem(SoundPicker::i18n_None()); // index 0 + mSound->insertItem(SoundPicker::i18n_Beep()); // index 1 + mSound->insertItem(SoundPicker::i18n_File()); // index 2 + if (theApp()->speechEnabled()) + mSound->insertItem(SoundPicker::i18n_Speak()); // index 3 + mSound->setMinimumSize(mSound->sizeHint()); + QWhatsThis::add(mSound, defsetting.arg(SoundPicker::i18n_Sound())); + hlayout->addWidget(mSound); + hlayout->addStretch(1); + +#ifndef WITHOUT_ARTS + mSoundRepeat = new QCheckBox(i18n("Repea&t sound file"), bgroup, "defRepeatSound"); + mSoundRepeat->setMinimumSize(mSoundRepeat->sizeHint()); + QWhatsThis::add(mSoundRepeat, i18n("sound file \"Repeat\" checkbox", "The default setting for sound file \"%1\" in the alarm edit dialog.").arg(SoundDlg::i18n_Repeat())); + hlayout->addWidget(mSoundRepeat); +#endif + + box = new QHBox(bgroup); // this is to control the QWhatsThis text display area + box->setSpacing(KDialog::spacingHint()); + mSoundFileLabel = new QLabel(i18n("Sound &file:"), box); + mSoundFileLabel->setFixedSize(mSoundFileLabel->sizeHint()); + mSoundFile = new QLineEdit(box); + mSoundFileLabel->setBuddy(mSoundFile); + mSoundFileBrowse = new QPushButton(box); + mSoundFileBrowse->setPixmap(SmallIcon("fileopen")); + mSoundFileBrowse->setFixedSize(mSoundFileBrowse->sizeHint()); + connect(mSoundFileBrowse, SIGNAL(clicked()), SLOT(slotBrowseSoundFile())); + QToolTip::add(mSoundFileBrowse, i18n("Choose a sound file")); + QWhatsThis::add(box, + i18n("Enter the default sound file to use in the alarm edit dialog.")); + box->setFixedHeight(box->sizeHint().height()); + layout->addWidget(box); + bgroup->setFixedHeight(bgroup->sizeHint().height()); + + // COMMAND ALARMS + group = new QGroupBox(i18n("Command Alarms"), mPage); + layout = new QVBoxLayout(group, KDialog::marginHint(), KDialog::spacingHint()); + layout->addSpacing(groupTopMargin); + layout = new QHBoxLayout(layout, KDialog::spacingHint()); + + mCmdScript = new QCheckBox(EditAlarmDlg::i18n_p_EnterScript(), group, "defCmdScript"); + mCmdScript->setMinimumSize(mCmdScript->sizeHint()); + QWhatsThis::add(mCmdScript, defsetting.arg(EditAlarmDlg::i18n_EnterScript())); + layout->addWidget(mCmdScript); + layout->addStretch(); + + mCmdXterm = new QCheckBox(EditAlarmDlg::i18n_w_ExecInTermWindow(), group, "defCmdXterm"); + mCmdXterm->setMinimumSize(mCmdXterm->sizeHint()); + QWhatsThis::add(mCmdXterm, defsetting.arg(EditAlarmDlg::i18n_ExecInTermWindow())); + layout->addWidget(mCmdXterm); + + // EMAIL ALARMS + group = new QGroupBox(i18n("Email Alarms"), mPage); + layout = new QVBoxLayout(group, KDialog::marginHint(), KDialog::spacingHint()); + layout->addSpacing(groupTopMargin); + + // BCC email to sender + mEmailBcc = new QCheckBox(EditAlarmDlg::i18n_e_CopyEmailToSelf(), group, "defEmailBcc"); + mEmailBcc->setMinimumSize(mEmailBcc->sizeHint()); + QWhatsThis::add(mEmailBcc, defsetting.arg(EditAlarmDlg::i18n_CopyEmailToSelf())); + layout->addWidget(mEmailBcc, 0, Qt::AlignAuto); + + // MISCELLANEOUS + // Show in KOrganizer + mCopyToKOrganizer = new QCheckBox(EditAlarmDlg::i18n_g_ShowInKOrganizer(), mPage, "defShowKorg"); + mCopyToKOrganizer->setMinimumSize(mCopyToKOrganizer->sizeHint()); + QWhatsThis::add(mCopyToKOrganizer, defsetting.arg(EditAlarmDlg::i18n_ShowInKOrganizer())); + + // Late cancellation + box = new QHBox(mPage); + box->setSpacing(KDialog::spacingHint()); + mLateCancel = new QCheckBox(LateCancelSelector::i18n_n_CancelIfLate(), box, "defCancelLate"); + mLateCancel->setMinimumSize(mLateCancel->sizeHint()); + QWhatsThis::add(mLateCancel, defsetting.arg(LateCancelSelector::i18n_CancelIfLate())); + box->setStretchFactor(new QWidget(box), 1); // left adjust the control + + // Recurrence + QHBox* itemBox = new QHBox(box); // this is to control the QWhatsThis text display area + itemBox->setSpacing(KDialog::spacingHint()); + label = new QLabel(i18n("&Recurrence:"), itemBox); + label->setFixedSize(label->sizeHint()); + mRecurPeriod = new QComboBox(itemBox, "defRecur"); + mRecurPeriod->insertItem(RecurrenceEdit::i18n_NoRecur()); + mRecurPeriod->insertItem(RecurrenceEdit::i18n_AtLogin()); + mRecurPeriod->insertItem(RecurrenceEdit::i18n_HourlyMinutely()); + mRecurPeriod->insertItem(RecurrenceEdit::i18n_Daily()); + mRecurPeriod->insertItem(RecurrenceEdit::i18n_Weekly()); + mRecurPeriod->insertItem(RecurrenceEdit::i18n_Monthly()); + mRecurPeriod->insertItem(RecurrenceEdit::i18n_Yearly()); + mRecurPeriod->setFixedSize(mRecurPeriod->sizeHint()); + label->setBuddy(mRecurPeriod); + QWhatsThis::add(itemBox, + i18n("The default setting for the recurrence rule in the alarm edit dialog.")); + box->setFixedHeight(itemBox->sizeHint().height()); + + // How to handle February 29th in yearly recurrences + QVBox* vbox = new QVBox(mPage); // this is to control the QWhatsThis text display area + vbox->setSpacing(KDialog::spacingHint()); + label = new QLabel(i18n("In non-leap years, repeat yearly February 29th alarms on:"), vbox); + label->setAlignment(alignment | Qt::WordBreak); + itemBox = new QHBox(vbox); + itemBox->setSpacing(2*KDialog::spacingHint()); + mFeb29 = new QButtonGroup(itemBox); + mFeb29->hide(); + QWidget* widget = new QWidget(itemBox); + widget->setFixedWidth(3*KDialog::spacingHint()); + QRadioButton* radio = new QRadioButton(i18n("February 2&8th"), itemBox); + radio->setMinimumSize(radio->sizeHint()); + mFeb29->insert(radio, KARecurrence::FEB29_FEB28); + radio = new QRadioButton(i18n("March &1st"), itemBox); + radio->setMinimumSize(radio->sizeHint()); + mFeb29->insert(radio, KARecurrence::FEB29_MAR1); + radio = new QRadioButton(i18n("Do ¬ repeat"), itemBox); + radio->setMinimumSize(radio->sizeHint()); + mFeb29->insert(radio, KARecurrence::FEB29_FEB29); + itemBox->setFixedHeight(itemBox->sizeHint().height()); + QWhatsThis::add(vbox, + i18n("For yearly recurrences, choose what date, if any, alarms due on February 29th should occur in non-leap years.\n" + "Note that the next scheduled occurrence of existing alarms is not re-evaluated when you change this setting.")); + + mPage->setStretchFactor(new QWidget(mPage), 1); // top adjust the widgets +} + +void EditPrefTab::restore() +{ + mAutoClose->setChecked(Preferences::mDefaultAutoClose); + mConfirmAck->setChecked(Preferences::mDefaultConfirmAck); + mReminderUnits->setCurrentItem(Preferences::mDefaultReminderUnits); + mSpecialActionsButton->setActions(Preferences::mDefaultPreAction, Preferences::mDefaultPostAction); + mSound->setCurrentItem(soundIndex(Preferences::mDefaultSoundType)); + mSoundFile->setText(Preferences::mDefaultSoundFile); +#ifndef WITHOUT_ARTS + mSoundRepeat->setChecked(Preferences::mDefaultSoundRepeat); +#endif + mCmdScript->setChecked(Preferences::mDefaultCmdScript); + mCmdXterm->setChecked(Preferences::mDefaultCmdLogType == EditAlarmDlg::EXEC_IN_TERMINAL); + mEmailBcc->setChecked(Preferences::mDefaultEmailBcc); + mCopyToKOrganizer->setChecked(Preferences::mDefaultCopyToKOrganizer); + mLateCancel->setChecked(Preferences::mDefaultLateCancel); + mRecurPeriod->setCurrentItem(recurIndex(Preferences::mDefaultRecurPeriod)); + mFeb29->setButton(Preferences::mDefaultFeb29Type); +} + +void EditPrefTab::apply(bool syncToDisc) +{ + Preferences::mDefaultAutoClose = mAutoClose->isChecked(); + Preferences::mDefaultConfirmAck = mConfirmAck->isChecked(); + Preferences::mDefaultReminderUnits = static_cast<TimePeriod::Units>(mReminderUnits->currentItem()); + Preferences::mDefaultPreAction = mSpecialActionsButton->preAction(); + Preferences::mDefaultPostAction = mSpecialActionsButton->postAction(); + switch (mSound->currentItem()) + { + case 3: Preferences::mDefaultSoundType = SoundPicker::SPEAK; break; + case 2: Preferences::mDefaultSoundType = SoundPicker::PLAY_FILE; break; + case 1: Preferences::mDefaultSoundType = SoundPicker::BEEP; break; + case 0: + default: Preferences::mDefaultSoundType = SoundPicker::NONE; break; + } + Preferences::mDefaultSoundFile = mSoundFile->text(); +#ifndef WITHOUT_ARTS + Preferences::mDefaultSoundRepeat = mSoundRepeat->isChecked(); +#endif + Preferences::mDefaultCmdScript = mCmdScript->isChecked(); + Preferences::mDefaultCmdLogType = (mCmdXterm->isChecked() ? EditAlarmDlg::EXEC_IN_TERMINAL : EditAlarmDlg::DISCARD_OUTPUT); + Preferences::mDefaultEmailBcc = mEmailBcc->isChecked(); + Preferences::mDefaultCopyToKOrganizer = mCopyToKOrganizer->isChecked(); + Preferences::mDefaultLateCancel = mLateCancel->isChecked() ? 1 : 0; + switch (mRecurPeriod->currentItem()) + { + case 6: Preferences::mDefaultRecurPeriod = RecurrenceEdit::ANNUAL; break; + case 5: Preferences::mDefaultRecurPeriod = RecurrenceEdit::MONTHLY; break; + case 4: Preferences::mDefaultRecurPeriod = RecurrenceEdit::WEEKLY; break; + case 3: Preferences::mDefaultRecurPeriod = RecurrenceEdit::DAILY; break; + case 2: Preferences::mDefaultRecurPeriod = RecurrenceEdit::SUBDAILY; break; + case 1: Preferences::mDefaultRecurPeriod = RecurrenceEdit::AT_LOGIN; break; + case 0: + default: Preferences::mDefaultRecurPeriod = RecurrenceEdit::NO_RECUR; break; + } + int feb29 = mFeb29->selectedId(); + Preferences::mDefaultFeb29Type = (feb29 >= 0) ? static_cast<KARecurrence::Feb29Type>(feb29) : Preferences::default_defaultFeb29Type; + PrefsTabBase::apply(syncToDisc); +} + +void EditPrefTab::setDefaults() +{ + mAutoClose->setChecked(Preferences::default_defaultAutoClose); + mConfirmAck->setChecked(Preferences::default_defaultConfirmAck); + mReminderUnits->setCurrentItem(Preferences::default_defaultReminderUnits); + mSpecialActionsButton->setActions(Preferences::default_defaultPreAction, Preferences::default_defaultPostAction); + mSound->setCurrentItem(soundIndex(Preferences::default_defaultSoundType)); + mSoundFile->setText(Preferences::default_defaultSoundFile); +#ifndef WITHOUT_ARTS + mSoundRepeat->setChecked(Preferences::default_defaultSoundRepeat); +#endif + mCmdScript->setChecked(Preferences::default_defaultCmdScript); + mCmdXterm->setChecked(Preferences::default_defaultCmdLogType == EditAlarmDlg::EXEC_IN_TERMINAL); + mEmailBcc->setChecked(Preferences::default_defaultEmailBcc); + mCopyToKOrganizer->setChecked(Preferences::default_defaultCopyToKOrganizer); + mLateCancel->setChecked(Preferences::default_defaultLateCancel); + mRecurPeriod->setCurrentItem(recurIndex(Preferences::default_defaultRecurPeriod)); + mFeb29->setButton(Preferences::default_defaultFeb29Type); +} + +void EditPrefTab::slotBrowseSoundFile() +{ + QString defaultDir; + QString url = SoundPicker::browseFile(defaultDir, mSoundFile->text()); + if (!url.isEmpty()) + mSoundFile->setText(url); +} + +int EditPrefTab::soundIndex(SoundPicker::Type type) +{ + switch (type) + { + case SoundPicker::SPEAK: return 3; + case SoundPicker::PLAY_FILE: return 2; + case SoundPicker::BEEP: return 1; + case SoundPicker::NONE: + default: return 0; + } +} + +int EditPrefTab::recurIndex(RecurrenceEdit::RepeatType type) +{ + switch (type) + { + case RecurrenceEdit::ANNUAL: return 6; + case RecurrenceEdit::MONTHLY: return 5; + case RecurrenceEdit::WEEKLY: return 4; + case RecurrenceEdit::DAILY: return 3; + case RecurrenceEdit::SUBDAILY: return 2; + case RecurrenceEdit::AT_LOGIN: return 1; + case RecurrenceEdit::NO_RECUR: + default: return 0; + } +} + +QString EditPrefTab::validate() +{ + if (mSound->currentItem() == SoundPicker::PLAY_FILE && mSoundFile->text().isEmpty()) + { + mSoundFile->setFocus(); + return i18n("You must enter a sound file when %1 is selected as the default sound type").arg(SoundPicker::i18n_File());; + } + return QString::null; +} + + +/*============================================================================= += Class ViewPrefTab +=============================================================================*/ + +ViewPrefTab::ViewPrefTab(QVBox* frame) + : PrefsTabBase(frame) +{ + QGroupBox* group = new QGroupBox(i18n("System Tray Tooltip"), mPage); + QGridLayout* grid = new QGridLayout(group, 5, 3, KDialog::marginHint(), KDialog::spacingHint()); + grid->setColStretch(2, 1); + grid->addColSpacing(0, indentWidth()); + grid->addColSpacing(1, indentWidth()); + grid->addRowSpacing(0, fontMetrics().lineSpacing()/2); + + mTooltipShowAlarms = new QCheckBox(i18n("Show next &24 hours' alarms"), group, "tooltipShow"); + mTooltipShowAlarms->setMinimumSize(mTooltipShowAlarms->sizeHint()); + connect(mTooltipShowAlarms, SIGNAL(toggled(bool)), SLOT(slotTooltipAlarmsToggled(bool))); + QWhatsThis::add(mTooltipShowAlarms, + i18n("Specify whether to include in the system tray tooltip, a summary of alarms due in the next 24 hours")); + grid->addMultiCellWidget(mTooltipShowAlarms, 1, 1, 0, 2, Qt::AlignAuto); + + QHBox* box = new QHBox(group); + box->setSpacing(KDialog::spacingHint()); + mTooltipMaxAlarms = new QCheckBox(i18n("Ma&ximum number of alarms to show:"), box, "tooltipMax"); + mTooltipMaxAlarms->setMinimumSize(mTooltipMaxAlarms->sizeHint()); + connect(mTooltipMaxAlarms, SIGNAL(toggled(bool)), SLOT(slotTooltipMaxToggled(bool))); + mTooltipMaxAlarmCount = new SpinBox(1, 99, 1, box); + mTooltipMaxAlarmCount->setLineShiftStep(5); + mTooltipMaxAlarmCount->setMinimumSize(mTooltipMaxAlarmCount->sizeHint()); + QWhatsThis::add(box, + i18n("Uncheck to display all of the next 24 hours' alarms in the system tray tooltip. " + "Check to enter an upper limit on the number to be displayed.")); + grid->addMultiCellWidget(box, 2, 2, 1, 2, Qt::AlignAuto); + + mTooltipShowTime = new QCheckBox(MainWindow::i18n_m_ShowAlarmTime(), group, "tooltipTime"); + mTooltipShowTime->setMinimumSize(mTooltipShowTime->sizeHint()); + connect(mTooltipShowTime, SIGNAL(toggled(bool)), SLOT(slotTooltipTimeToggled(bool))); + QWhatsThis::add(mTooltipShowTime, + i18n("Specify whether to show in the system tray tooltip, the time at which each alarm is due")); + grid->addMultiCellWidget(mTooltipShowTime, 3, 3, 1, 2, Qt::AlignAuto); + + mTooltipShowTimeTo = new QCheckBox(MainWindow::i18n_l_ShowTimeToAlarm(), group, "tooltipTimeTo"); + mTooltipShowTimeTo->setMinimumSize(mTooltipShowTimeTo->sizeHint()); + connect(mTooltipShowTimeTo, SIGNAL(toggled(bool)), SLOT(slotTooltipTimeToToggled(bool))); + QWhatsThis::add(mTooltipShowTimeTo, + i18n("Specify whether to show in the system tray tooltip, how long until each alarm is due")); + grid->addMultiCellWidget(mTooltipShowTimeTo, 4, 4, 1, 2, Qt::AlignAuto); + + box = new QHBox(group); // this is to control the QWhatsThis text display area + box->setSpacing(KDialog::spacingHint()); + mTooltipTimeToPrefixLabel = new QLabel(i18n("&Prefix:"), box); + mTooltipTimeToPrefixLabel->setFixedSize(mTooltipTimeToPrefixLabel->sizeHint()); + mTooltipTimeToPrefix = new QLineEdit(box); + mTooltipTimeToPrefixLabel->setBuddy(mTooltipTimeToPrefix); + QWhatsThis::add(box, + i18n("Enter the text to be displayed in front of the time until the alarm, in the system tray tooltip")); + box->setFixedHeight(box->sizeHint().height()); + grid->addWidget(box, 5, 2, Qt::AlignAuto); + group->setMaximumHeight(group->sizeHint().height()); + + mModalMessages = new QCheckBox(i18n("Message &windows have a title bar and take keyboard focus"), mPage, "modalMsg"); + mModalMessages->setMinimumSize(mModalMessages->sizeHint()); + QWhatsThis::add(mModalMessages, + i18n("Specify the characteristics of alarm message windows:\n" + "- If checked, the window is a normal window with a title bar, which grabs keyboard input when it is displayed.\n" + "- If unchecked, the window does not interfere with your typing when " + "it is displayed, but it has no title bar and cannot be moved or resized.")); + + QHBox* itemBox = new QHBox(mPage); // this is to control the QWhatsThis text display area + box = new QHBox(itemBox); + box->setSpacing(KDialog::spacingHint()); + QLabel* label = new QLabel(i18n("System tray icon &update interval:"), box); + mDaemonTrayCheckInterval = new SpinBox(1, 9999, 1, box, "daemonCheck"); + mDaemonTrayCheckInterval->setLineShiftStep(10); + mDaemonTrayCheckInterval->setMinimumSize(mDaemonTrayCheckInterval->sizeHint()); + label->setBuddy(mDaemonTrayCheckInterval); + label = new QLabel(i18n("seconds"), box); + QWhatsThis::add(box, + i18n("How often to update the system tray icon to indicate whether or not the Alarm Daemon is monitoring alarms.")); + itemBox->setStretchFactor(new QWidget(itemBox), 1); // left adjust the controls + itemBox->setFixedHeight(box->sizeHint().height()); + + mPage->setStretchFactor(new QWidget(mPage), 1); // top adjust the widgets +} + +void ViewPrefTab::restore() +{ + setTooltip(Preferences::mTooltipAlarmCount, + Preferences::mShowTooltipAlarmTime, + Preferences::mShowTooltipTimeToAlarm, + Preferences::mTooltipTimeToPrefix); + mModalMessages->setChecked(Preferences::mModalMessages); + mDaemonTrayCheckInterval->setValue(Preferences::mDaemonTrayCheckInterval); +} + +void ViewPrefTab::apply(bool syncToDisc) +{ + int n = mTooltipShowAlarms->isChecked() ? -1 : 0; + if (n && mTooltipMaxAlarms->isChecked()) + n = mTooltipMaxAlarmCount->value(); + Preferences::mTooltipAlarmCount = n; + Preferences::mShowTooltipAlarmTime = mTooltipShowTime->isChecked(); + Preferences::mShowTooltipTimeToAlarm = mTooltipShowTimeTo->isChecked(); + Preferences::mTooltipTimeToPrefix = mTooltipTimeToPrefix->text(); + Preferences::mModalMessages = mModalMessages->isChecked(); + Preferences::mDaemonTrayCheckInterval = mDaemonTrayCheckInterval->value(); + PrefsTabBase::apply(syncToDisc); +} + +void ViewPrefTab::setDefaults() +{ + setTooltip(Preferences::default_tooltipAlarmCount, + Preferences::default_showTooltipAlarmTime, + Preferences::default_showTooltipTimeToAlarm, + Preferences::default_tooltipTimeToPrefix); + mModalMessages->setChecked(Preferences::default_modalMessages); + mDaemonTrayCheckInterval->setValue(Preferences::default_daemonTrayCheckInterval); +} + +void ViewPrefTab::setTooltip(int maxAlarms, bool time, bool timeTo, const QString& prefix) +{ + if (!timeTo) + time = true; // ensure that at least one time option is ticked + + // Set the states of the controls without calling signal + // handlers, since these could change the checkboxes' states. + mTooltipShowAlarms->blockSignals(true); + mTooltipShowTime->blockSignals(true); + mTooltipShowTimeTo->blockSignals(true); + + mTooltipShowAlarms->setChecked(maxAlarms); + mTooltipMaxAlarms->setChecked(maxAlarms > 0); + mTooltipMaxAlarmCount->setValue(maxAlarms > 0 ? maxAlarms : 1); + mTooltipShowTime->setChecked(time); + mTooltipShowTimeTo->setChecked(timeTo); + mTooltipTimeToPrefix->setText(prefix); + + mTooltipShowAlarms->blockSignals(false); + mTooltipShowTime->blockSignals(false); + mTooltipShowTimeTo->blockSignals(false); + + // Enable/disable controls according to their states + slotTooltipTimeToToggled(timeTo); + slotTooltipAlarmsToggled(maxAlarms); +} + +void ViewPrefTab::slotTooltipAlarmsToggled(bool on) +{ + mTooltipMaxAlarms->setEnabled(on); + mTooltipMaxAlarmCount->setEnabled(on && mTooltipMaxAlarms->isChecked()); + mTooltipShowTime->setEnabled(on); + mTooltipShowTimeTo->setEnabled(on); + on = on && mTooltipShowTimeTo->isChecked(); + mTooltipTimeToPrefix->setEnabled(on); + mTooltipTimeToPrefixLabel->setEnabled(on); +} + +void ViewPrefTab::slotTooltipMaxToggled(bool on) +{ + mTooltipMaxAlarmCount->setEnabled(on && mTooltipMaxAlarms->isEnabled()); +} + +void ViewPrefTab::slotTooltipTimeToggled(bool on) +{ + if (!on && !mTooltipShowTimeTo->isChecked()) + mTooltipShowTimeTo->setChecked(true); +} + +void ViewPrefTab::slotTooltipTimeToToggled(bool on) +{ + if (!on && !mTooltipShowTime->isChecked()) + mTooltipShowTime->setChecked(true); + on = on && mTooltipShowTimeTo->isEnabled(); + mTooltipTimeToPrefix->setEnabled(on); + mTooltipTimeToPrefixLabel->setEnabled(on); +} diff --git a/kalarm/prefdlg.h b/kalarm/prefdlg.h new file mode 100644 index 000000000..a5f1ee9a1 --- /dev/null +++ b/kalarm/prefdlg.h @@ -0,0 +1,270 @@ +/* + * prefdlg.h - program preferences dialog + * Program: kalarm + * Copyright © 2001-2007 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef PREFDLG_H +#define PREFDLG_H + +#include <qsize.h> +#include <qdatetime.h> +#include <ktabctl.h> +#include <kdialogbase.h> + +#include "preferences.h" +#include "recurrenceedit.h" +#include "soundpicker.h" + +class QButtonGroup; +class QCheckBox; +class QRadioButton; +class QPushButton; +class QComboBox; +class QLineEdit; +class KColorCombo; +class FontColourChooser; +class ButtonGroup; +class TimeEdit; +class SpinBox; +class SpecialActionsButton; + +class FontColourPrefTab; +class EditPrefTab; +class EmailPrefTab; +class ViewPrefTab; +class MiscPrefTab; + + +// The Preferences dialog +class KAlarmPrefDlg : public KDialogBase +{ + Q_OBJECT + public: + static void display(); + ~KAlarmPrefDlg(); + + FontColourPrefTab* mFontColourPage; + EditPrefTab* mEditPage; + EmailPrefTab* mEmailPage; + ViewPrefTab* mViewPage; + MiscPrefTab* mMiscPage; + + protected slots: + virtual void slotOk(); + virtual void slotApply(); + virtual void slotHelp(); + virtual void slotDefault(); + virtual void slotCancel(); + + private: + KAlarmPrefDlg(); + void restore(); + + static KAlarmPrefDlg* mInstance; + bool mValid; +}; + +// Base class for each tab in the Preferences dialog +class PrefsTabBase : public QWidget +{ + Q_OBJECT + public: + PrefsTabBase(QVBox*); + + void setPreferences(); + virtual void restore() = 0; + virtual void apply(bool syncToDisc) = 0; + virtual void setDefaults() = 0; + static int indentWidth() { return mIndentWidth; } + + protected: + QVBox* mPage; + + private: + static int mIndentWidth; // indent width for checkboxes etc. +}; + + +// Miscellaneous tab of the Preferences dialog +class MiscPrefTab : public PrefsTabBase +{ + Q_OBJECT + public: + MiscPrefTab(QVBox*); + + virtual void restore(); + virtual void apply(bool syncToDisc); + virtual void setDefaults(); + + private slots: + void slotAutostartDaemonClicked(); + void slotRunModeToggled(bool); + void slotDisableIfStoppedToggled(bool); + void slotExpiredToggled(bool); + void slotClearExpired(); + void slotOtherTerminalToggled(bool); +//#ifdef AUTOSTART_BY_KALARMD + void slotAutostartToggled(bool); +//#endif + + private: + void setExpiredControls(int purgeDays); + + QCheckBox* mAutostartDaemon; + QRadioButton* mRunInSystemTray; + QRadioButton* mRunOnDemand; + QCheckBox* mDisableAlarmsIfStopped; + QCheckBox* mQuitWarn; + QCheckBox* mAutostartTrayIcon; + QCheckBox* mConfirmAlarmDeletion; + QCheckBox* mKeepExpired; + QCheckBox* mPurgeExpired; + SpinBox* mPurgeAfter; + QLabel* mPurgeAfterLabel; + QPushButton* mClearExpired; + TimeEdit* mStartOfDay; + QButtonGroup* mXtermType; + QLineEdit* mXtermCommand; + int mXtermFirst; // id of first terminal window radio button + int mXtermCount; // number of terminal window types +}; + + +// Email tab of the Preferences dialog +class EmailPrefTab : public PrefsTabBase +{ + Q_OBJECT + public: + EmailPrefTab(QVBox*); + + QString validate(); + virtual void restore(); + virtual void apply(bool syncToDisc); + virtual void setDefaults(); + + private slots: + void slotEmailClientChanged(int); + void slotFromAddrChanged(int); + void slotBccAddrChanged(int); + void slotAddressChanged() { mAddressChanged = true; } + + private: + void setEmailAddress(Preferences::MailFrom, const QString& address); + void setEmailBccAddress(bool useControlCentre, const QString& address); + QString validateAddr(ButtonGroup*, QLineEdit* addr, const QString& msg); + + ButtonGroup* mEmailClient; + ButtonGroup* mFromAddressGroup; + QLineEdit* mEmailAddress; + ButtonGroup* mBccAddressGroup; + QLineEdit* mEmailBccAddress; + QCheckBox* mEmailQueuedNotify; + QCheckBox* mEmailCopyToKMail; + bool mAddressChanged; + bool mBccAddressChanged; +}; + + +// Edit defaults tab of the Preferences dialog +class EditPrefTab : public PrefsTabBase +{ + Q_OBJECT + public: + EditPrefTab(QVBox*); + + QString validate(); + virtual void restore(); + virtual void apply(bool syncToDisc); + virtual void setDefaults(); + + private slots: + void slotBrowseSoundFile(); + + private: + QCheckBox* mAutoClose; + QCheckBox* mConfirmAck; + QComboBox* mReminderUnits; + SpecialActionsButton* mSpecialActionsButton; + QCheckBox* mCmdScript; + QCheckBox* mCmdXterm; + QCheckBox* mEmailBcc; + QComboBox* mSound; + QLabel* mSoundFileLabel; + QLineEdit* mSoundFile; + QPushButton* mSoundFileBrowse; + QCheckBox* mSoundRepeat; + QCheckBox* mCopyToKOrganizer; + QCheckBox* mLateCancel; + QComboBox* mRecurPeriod; + QButtonGroup* mFeb29; + + static int soundIndex(SoundPicker::Type); + static int recurIndex(RecurrenceEdit::RepeatType); +}; + + +// View tab of the Preferences dialog +class ViewPrefTab : public PrefsTabBase +{ + Q_OBJECT + public: + ViewPrefTab(QVBox*); + + virtual void restore(); + virtual void apply(bool syncToDisc); + virtual void setDefaults(); + + private slots: + void slotTooltipAlarmsToggled(bool); + void slotTooltipMaxToggled(bool); + void slotTooltipTimeToggled(bool); + void slotTooltipTimeToToggled(bool); + + private: + void setTooltip(int maxAlarms, bool time, bool timeTo, const QString& prefix); + + QCheckBox* mTooltipShowAlarms; + QCheckBox* mTooltipMaxAlarms; + SpinBox* mTooltipMaxAlarmCount; + QCheckBox* mTooltipShowTime; + QCheckBox* mTooltipShowTimeTo; + QLineEdit* mTooltipTimeToPrefix; + QLabel* mTooltipTimeToPrefixLabel; + QCheckBox* mModalMessages; + SpinBox* mDaemonTrayCheckInterval; +}; + + +// Font & Colour tab of the Preferences dialog +class FontColourPrefTab : public PrefsTabBase +{ + Q_OBJECT + public: + FontColourPrefTab(QVBox*); + + virtual void restore(); + virtual void apply(bool syncToDisc); + virtual void setDefaults(); + + private: + FontColourChooser* mFontChooser; + KColorCombo* mDisabledColour; + KColorCombo* mExpiredColour; +}; + +#endif // PREFDLG_H diff --git a/kalarm/preferences.cpp b/kalarm/preferences.cpp new file mode 100644 index 000000000..ad96098e0 --- /dev/null +++ b/kalarm/preferences.cpp @@ -0,0 +1,705 @@ +/* + * preferences.cpp - program preference settings + * Program: kalarm + * Copyright © 2001-2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <kglobal.h> +#include <kconfig.h> +#include <kstandarddirs.h> +#include <kapplication.h> +#include <kglobalsettings.h> +#include <kmessagebox.h> + +#include <libkpimidentities/identity.h> +#include <libkpimidentities/identitymanager.h> + +#include "daemon.h" +#include "functions.h" +#include "kamail.h" +#include "messagebox.h" +#include "preferences.moc" + + +static QString translateXTermPath(KConfig*, const QString& cmdline, bool write); + +Preferences* Preferences::mInstance = 0; + +// Default config file settings +QColor defaultMessageColours[] = { Qt::red, Qt::green, Qt::blue, Qt::cyan, Qt::magenta, Qt::yellow, Qt::white, Qt::lightGray, Qt::black, QColor() }; +const ColourList Preferences::default_messageColours(defaultMessageColours); +const QColor Preferences::default_defaultBgColour(Qt::red); +const QColor Preferences::default_defaultFgColour(Qt::black); +QFont Preferences::mDefault_messageFont; // initialised in constructor +const QTime Preferences::default_startOfDay(0, 0); +const bool Preferences::default_runInSystemTray = true; +const bool Preferences::default_disableAlarmsIfStopped = true; +const bool Preferences::default_quitWarn = true; +const bool Preferences::default_autostartTrayIcon = true; +const bool Preferences::default_confirmAlarmDeletion = true; +const bool Preferences::default_modalMessages = true; +const int Preferences::default_messageButtonDelay = 0; // (seconds) +const int Preferences::default_tooltipAlarmCount = 5; +const bool Preferences::default_showTooltipAlarmTime = true; +const bool Preferences::default_showTooltipTimeToAlarm = true; +const QString Preferences::default_tooltipTimeToPrefix = QString::fromLatin1("+"); +const int Preferences::default_daemonTrayCheckInterval = 10; // (seconds) +const bool Preferences::default_emailCopyToKMail = false; +const bool Preferences::default_emailQueuedNotify = false; +const QColor Preferences::default_disabledColour(Qt::lightGray); +const QColor Preferences::default_expiredColour(Qt::darkRed); +const int Preferences::default_expiredKeepDays = 7; +const QString Preferences::default_defaultSoundFile = QString::null; +const float Preferences::default_defaultSoundVolume = -1; +const int Preferences::default_defaultLateCancel = 0; +const bool Preferences::default_defaultAutoClose = false; +const bool Preferences::default_defaultCopyToKOrganizer = false; +const bool Preferences::default_defaultSoundRepeat = false; +const SoundPicker::Type Preferences::default_defaultSoundType = SoundPicker::NONE; +const bool Preferences::default_defaultConfirmAck = false; +const bool Preferences::default_defaultCmdScript = false; +const EditAlarmDlg::CmdLogType Preferences::default_defaultCmdLogType = EditAlarmDlg::DISCARD_OUTPUT; +const bool Preferences::default_defaultEmailBcc = false; +const QString Preferences::default_emailAddress = QString::null; +const QString Preferences::default_emailBccAddress = QString::null; +const Preferences::MailClient Preferences::default_emailClient = KMAIL; +const Preferences::MailFrom Preferences::default_emailBccFrom = MAIL_FROM_CONTROL_CENTRE; +const RecurrenceEdit::RepeatType Preferences::default_defaultRecurPeriod = RecurrenceEdit::NO_RECUR; +const KARecurrence::Feb29Type Preferences::default_defaultFeb29Type = KARecurrence::FEB29_MAR1; +const TimePeriod::Units Preferences::default_defaultReminderUnits = TimePeriod::HOURS_MINUTES; +const QString Preferences::default_defaultPreAction; +const QString Preferences::default_defaultPostAction; + +Preferences::MailFrom Preferences::default_emailFrom() +{ + return KAMail::identitiesExist() ? MAIL_FROM_KMAIL : MAIL_FROM_CONTROL_CENTRE; +} + +// Active config file settings +ColourList Preferences::mMessageColours; +QColor Preferences::mDefaultBgColour; +QFont Preferences::mMessageFont; +QTime Preferences::mStartOfDay; +bool Preferences::mRunInSystemTray; +bool Preferences::mDisableAlarmsIfStopped; +bool Preferences::mAutostartTrayIcon; +KARecurrence::Feb29Type Preferences::mDefaultFeb29Type; +bool Preferences::mModalMessages; +int Preferences::mMessageButtonDelay; +int Preferences::mTooltipAlarmCount; +bool Preferences::mShowTooltipAlarmTime; +bool Preferences::mShowTooltipTimeToAlarm; +QString Preferences::mTooltipTimeToPrefix; +int Preferences::mDaemonTrayCheckInterval; +QString Preferences::mEmailAddress; +QString Preferences::mEmailBccAddress; +Preferences::MailClient Preferences::mEmailClient; +Preferences::MailFrom Preferences::mEmailFrom; +Preferences::MailFrom Preferences::mEmailBccFrom; +bool Preferences::mEmailCopyToKMail; +QString Preferences::mCmdXTermCommand; +QColor Preferences::mDisabledColour; +QColor Preferences::mExpiredColour; +int Preferences::mExpiredKeepDays; +// Default settings for Edit Alarm dialog +QString Preferences::mDefaultSoundFile; +float Preferences::mDefaultSoundVolume; +int Preferences::mDefaultLateCancel; +bool Preferences::mDefaultAutoClose; +bool Preferences::mDefaultCopyToKOrganizer; +SoundPicker::Type Preferences::mDefaultSoundType; +bool Preferences::mDefaultSoundRepeat; +bool Preferences::mDefaultConfirmAck; +bool Preferences::mDefaultEmailBcc; +bool Preferences::mDefaultCmdScript; +EditAlarmDlg::CmdLogType Preferences::mDefaultCmdLogType; +QString Preferences::mDefaultCmdLogFile; +RecurrenceEdit::RepeatType Preferences::mDefaultRecurPeriod; +TimePeriod::Units Preferences::mDefaultReminderUnits; +QString Preferences::mDefaultPreAction; +QString Preferences::mDefaultPostAction; +// Change tracking +QTime Preferences::mOldStartOfDay; +bool Preferences::mStartOfDayChanged; + + +static const QString defaultFeb29RecurType = QString::fromLatin1("Mar1"); +static const QString defaultEmailClient = QString::fromLatin1("kmail"); + +// Config file entry names +static const QString GENERAL_SECTION = QString::fromLatin1("General"); +static const QString VERSION_NUM = QString::fromLatin1("Version"); +static const QString MESSAGE_COLOURS = QString::fromLatin1("MessageColours"); +static const QString MESSAGE_BG_COLOUR = QString::fromLatin1("MessageBackgroundColour"); +static const QString MESSAGE_FONT = QString::fromLatin1("MessageFont"); +static const QString RUN_IN_SYSTEM_TRAY = QString::fromLatin1("RunInSystemTray"); +static const QString DISABLE_IF_STOPPED = QString::fromLatin1("DisableAlarmsIfStopped"); +static const QString AUTOSTART_TRAY = QString::fromLatin1("AutostartTray"); +static const QString FEB29_RECUR_TYPE = QString::fromLatin1("Feb29Recur"); +static const QString MODAL_MESSAGES = QString::fromLatin1("ModalMessages"); +static const QString MESSAGE_BUTTON_DELAY = QString::fromLatin1("MessageButtonDelay"); +static const QString TOOLTIP_ALARM_COUNT = QString::fromLatin1("TooltipAlarmCount"); +static const QString TOOLTIP_ALARM_TIME = QString::fromLatin1("ShowTooltipAlarmTime"); +static const QString TOOLTIP_TIME_TO_ALARM = QString::fromLatin1("ShowTooltipTimeToAlarm"); +static const QString TOOLTIP_TIME_TO_PREFIX = QString::fromLatin1("TooltipTimeToPrefix"); +static const QString DAEMON_TRAY_INTERVAL = QString::fromLatin1("DaemonTrayCheckInterval"); +static const QString EMAIL_CLIENT = QString::fromLatin1("EmailClient"); +static const QString EMAIL_COPY_TO_KMAIL = QString::fromLatin1("EmailCopyToKMail"); +static const QString EMAIL_FROM = QString::fromLatin1("EmailFrom"); +static const QString EMAIL_BCC_ADDRESS = QString::fromLatin1("EmailBccAddress"); +static const QString CMD_XTERM_COMMAND = QString::fromLatin1("CmdXTerm"); +static const QString START_OF_DAY = QString::fromLatin1("StartOfDay"); +static const QString START_OF_DAY_CHECK = QString::fromLatin1("Sod"); +static const QString DISABLED_COLOUR = QString::fromLatin1("DisabledColour"); +static const QString EXPIRED_COLOUR = QString::fromLatin1("ExpiredColour"); +static const QString EXPIRED_KEEP_DAYS = QString::fromLatin1("ExpiredKeepDays"); +static const QString DEFAULTS_SECTION = QString::fromLatin1("Defaults"); +static const QString DEF_LATE_CANCEL = QString::fromLatin1("DefLateCancel"); +static const QString DEF_AUTO_CLOSE = QString::fromLatin1("DefAutoClose"); +static const QString DEF_CONFIRM_ACK = QString::fromLatin1("DefConfirmAck"); +static const QString DEF_COPY_TO_KORG = QString::fromLatin1("DefCopyKOrg"); +static const QString DEF_SOUND_TYPE = QString::fromLatin1("DefSoundType"); +static const QString DEF_SOUND_FILE = QString::fromLatin1("DefSoundFile"); +static const QString DEF_SOUND_VOLUME = QString::fromLatin1("DefSoundVolume"); +static const QString DEF_SOUND_REPEAT = QString::fromLatin1("DefSoundRepeat"); +static const QString DEF_CMD_SCRIPT = QString::fromLatin1("DefCmdScript"); +static const QString DEF_CMD_LOG_TYPE = QString::fromLatin1("DefCmdLogType"); +static const QString DEF_LOG_FILE = QString::fromLatin1("DefLogFile"); +static const QString DEF_EMAIL_BCC = QString::fromLatin1("DefEmailBcc"); +static const QString DEF_RECUR_PERIOD = QString::fromLatin1("DefRecurPeriod"); +static const QString DEF_REMIND_UNITS = QString::fromLatin1("RemindUnits"); +static const QString DEF_PRE_ACTION = QString::fromLatin1("DefPreAction"); +static const QString DEF_POST_ACTION = QString::fromLatin1("DefPostAction"); + +// Config file entry name for temporary use +static const QString TEMP = QString::fromLatin1("Temp"); + +// Values for EmailFrom entry +static const QString FROM_CONTROL_CENTRE = QString::fromLatin1("@ControlCenter"); +static const QString FROM_KMAIL = QString::fromLatin1("@KMail"); + +// Config file entry names for notification messages +const QString Preferences::QUIT_WARN = QString::fromLatin1("QuitWarn"); +const QString Preferences::CONFIRM_ALARM_DELETION = QString::fromLatin1("ConfirmAlarmDeletion"); +const QString Preferences::EMAIL_QUEUED_NOTIFY = QString::fromLatin1("EmailQueuedNotify"); + +static const int SODxor = 0x82451630; +inline int Preferences::startOfDayCheck() +{ + // Combine with a 'random' constant to prevent 'clever' people fiddling the + // value, and thereby screwing things up. + return QTime().msecsTo(mStartOfDay) ^ SODxor; +} + + +void Preferences::initialise() +{ + if (!mInstance) + { + // Initialise static variables here to avoid static initialisation + // sequencing errors. + mDefault_messageFont = QFont(KGlobalSettings::generalFont().family(), 16, QFont::Bold); + + mInstance = new Preferences; + + convertOldPrefs(); // convert preferences written by previous KAlarm versions + read(); + + // Set the default button for the Quit warning message box to Cancel + MessageBox::setContinueDefault(QUIT_WARN, KMessageBox::Cancel); + MessageBox::setDefaultShouldBeShownContinue(QUIT_WARN, default_quitWarn); + MessageBox::setDefaultShouldBeShownContinue(EMAIL_QUEUED_NOTIFY, default_emailQueuedNotify); + MessageBox::setDefaultShouldBeShownContinue(CONFIRM_ALARM_DELETION, default_confirmAlarmDeletion); + } +} + +void Preferences::connect(const char* signal, const QObject* receiver, const char* member) +{ + initialise(); + QObject::connect(mInstance, signal, receiver, member); +} + +void Preferences::emitStartOfDayChanged() +{ + emit startOfDayChanged(mOldStartOfDay); +} + +void Preferences::emitPreferencesChanged() +{ + emit preferencesChanged(); +} + +/****************************************************************************** +* Read preference values from the config file. +*/ +void Preferences::read() +{ + initialise(); + + KConfig* config = KGlobal::config(); + config->setGroup(GENERAL_SECTION); + QStringList cols = config->readListEntry(MESSAGE_COLOURS); + if (!cols.count()) + mMessageColours = default_messageColours; + else + { + mMessageColours.clear(); + for (QStringList::Iterator it = cols.begin(); it != cols.end(); ++it) + { + QColor c((*it)); + if (c.isValid()) + mMessageColours.insert(c); + } + } + mDefaultBgColour = config->readColorEntry(MESSAGE_BG_COLOUR, &default_defaultBgColour); + mMessageFont = config->readFontEntry(MESSAGE_FONT, &mDefault_messageFont); + mRunInSystemTray = config->readBoolEntry(RUN_IN_SYSTEM_TRAY, default_runInSystemTray); + mDisableAlarmsIfStopped = config->readBoolEntry(DISABLE_IF_STOPPED, default_disableAlarmsIfStopped); + mAutostartTrayIcon = config->readBoolEntry(AUTOSTART_TRAY, default_autostartTrayIcon); + mModalMessages = config->readBoolEntry(MODAL_MESSAGES, default_modalMessages); + mMessageButtonDelay = config->readNumEntry(MESSAGE_BUTTON_DELAY, default_messageButtonDelay); + if (mMessageButtonDelay > 10) + mMessageButtonDelay = 10; // prevent windows being unusable for a long time + if (mMessageButtonDelay < -1) + mMessageButtonDelay = -1; + mTooltipAlarmCount = static_cast<int>(config->readUnsignedNumEntry(TOOLTIP_ALARM_COUNT, default_tooltipAlarmCount)); + if (mTooltipAlarmCount < 1) + mTooltipAlarmCount = 1; + mShowTooltipAlarmTime = config->readBoolEntry(TOOLTIP_ALARM_TIME, default_showTooltipAlarmTime); + mShowTooltipTimeToAlarm = config->readBoolEntry(TOOLTIP_TIME_TO_ALARM, default_showTooltipTimeToAlarm); + mTooltipTimeToPrefix = config->readEntry(TOOLTIP_TIME_TO_PREFIX, default_tooltipTimeToPrefix); + mDaemonTrayCheckInterval = static_cast<int>(config->readUnsignedNumEntry(DAEMON_TRAY_INTERVAL, default_daemonTrayCheckInterval)); + if (mDaemonTrayCheckInterval < 1) + mDaemonTrayCheckInterval = 1; + QCString client = config->readEntry(EMAIL_CLIENT, defaultEmailClient).local8Bit(); // don't use readPathEntry() here (values are hard-coded) + mEmailClient = (client == "sendmail" ? SENDMAIL : KMAIL); + mEmailCopyToKMail = config->readBoolEntry(EMAIL_COPY_TO_KMAIL, default_emailCopyToKMail); + QString from = config->readEntry(EMAIL_FROM, emailFrom(default_emailFrom(), false, false)); + mEmailFrom = emailFrom(from); + QString bccFrom = config->readEntry(EMAIL_BCC_ADDRESS, emailFrom(default_emailBccFrom, false, true)); + mEmailBccFrom = emailFrom(bccFrom); + if (mEmailFrom == MAIL_FROM_CONTROL_CENTRE || mEmailBccFrom == MAIL_FROM_CONTROL_CENTRE) + mEmailAddress = mEmailBccAddress = KAMail::controlCentreAddress(); + if (mEmailFrom == MAIL_FROM_ADDR) + mEmailAddress = from; + if (mEmailBccFrom == MAIL_FROM_ADDR) + mEmailBccAddress = bccFrom; + mCmdXTermCommand = translateXTermPath(config, config->readEntry(CMD_XTERM_COMMAND), false); + QDateTime defStartOfDay(QDate(1900,1,1), default_startOfDay); + mStartOfDay = config->readDateTimeEntry(START_OF_DAY, &defStartOfDay).time(); + mOldStartOfDay.setHMS(0,0,0); + int sod = config->readNumEntry(START_OF_DAY_CHECK, 0); + if (sod) + mOldStartOfDay = mOldStartOfDay.addMSecs(sod ^ SODxor); + mDisabledColour = config->readColorEntry(DISABLED_COLOUR, &default_disabledColour); + mExpiredColour = config->readColorEntry(EXPIRED_COLOUR, &default_expiredColour); + mExpiredKeepDays = config->readNumEntry(EXPIRED_KEEP_DAYS, default_expiredKeepDays); + + config->setGroup(DEFAULTS_SECTION); + mDefaultLateCancel = static_cast<int>(config->readUnsignedNumEntry(DEF_LATE_CANCEL, default_defaultLateCancel)); + mDefaultAutoClose = config->readBoolEntry(DEF_AUTO_CLOSE, default_defaultAutoClose); + mDefaultConfirmAck = config->readBoolEntry(DEF_CONFIRM_ACK, default_defaultConfirmAck); + mDefaultCopyToKOrganizer = config->readBoolEntry(DEF_COPY_TO_KORG, default_defaultCopyToKOrganizer); + int soundType = config->readNumEntry(DEF_SOUND_TYPE, default_defaultSoundType); + mDefaultSoundType = (soundType < 0 || soundType > SoundPicker::SPEAK) + ? default_defaultSoundType : (SoundPicker::Type)soundType; + mDefaultSoundVolume = static_cast<float>(config->readDoubleNumEntry(DEF_SOUND_VOLUME, default_defaultSoundVolume)); +#ifdef WITHOUT_ARTS + mDefaultSoundRepeat = false; +#else + mDefaultSoundRepeat = config->readBoolEntry(DEF_SOUND_REPEAT, default_defaultSoundRepeat); +#endif + mDefaultSoundFile = config->readPathEntry(DEF_SOUND_FILE); + mDefaultCmdScript = config->readBoolEntry(DEF_CMD_SCRIPT, default_defaultCmdScript); + int logType = config->readNumEntry(DEF_CMD_LOG_TYPE, default_defaultCmdLogType); + mDefaultCmdLogType = (logType < EditAlarmDlg::DISCARD_OUTPUT || logType > EditAlarmDlg::EXEC_IN_TERMINAL) + ? default_defaultCmdLogType : (EditAlarmDlg::CmdLogType)logType; + mDefaultCmdLogFile = config->readPathEntry(DEF_LOG_FILE); + mDefaultEmailBcc = config->readBoolEntry(DEF_EMAIL_BCC, default_defaultEmailBcc); + int recurPeriod = config->readNumEntry(DEF_RECUR_PERIOD, default_defaultRecurPeriod); + mDefaultRecurPeriod = (recurPeriod < RecurrenceEdit::SUBDAILY || recurPeriod > RecurrenceEdit::ANNUAL) + ? default_defaultRecurPeriod : (RecurrenceEdit::RepeatType)recurPeriod; + QCString feb29 = config->readEntry(FEB29_RECUR_TYPE, defaultFeb29RecurType).local8Bit(); + mDefaultFeb29Type = (feb29 == "Mar1") ? KARecurrence::FEB29_MAR1 : (feb29 == "Feb28") ? KARecurrence::FEB29_FEB28 : KARecurrence::FEB29_FEB29; + QString remindUnits = config->readEntry(DEF_REMIND_UNITS); + mDefaultReminderUnits = (remindUnits == QString::fromLatin1("Minutes")) ? TimePeriod::MINUTES + : (remindUnits == QString::fromLatin1("HoursMinutes")) ? TimePeriod::HOURS_MINUTES + : (remindUnits == QString::fromLatin1("Days")) ? TimePeriod::DAYS + : (remindUnits == QString::fromLatin1("Weeks")) ? TimePeriod::WEEKS : default_defaultReminderUnits; + mDefaultPreAction = config->readEntry(DEF_PRE_ACTION, default_defaultPreAction); + mDefaultPostAction = config->readEntry(DEF_POST_ACTION, default_defaultPostAction); + mInstance->emitPreferencesChanged(); + mStartOfDayChanged = (mStartOfDay != mOldStartOfDay); + if (mStartOfDayChanged) + { + mInstance->emitStartOfDayChanged(); + mOldStartOfDay = mStartOfDay; + } +} + +/****************************************************************************** +* Save preference values to the config file. +*/ +void Preferences::save(bool syncToDisc) +{ + KConfig* config = KGlobal::config(); + config->setGroup(GENERAL_SECTION); + config->writeEntry(VERSION_NUM, KALARM_VERSION); + QStringList colours; + for (ColourList::const_iterator it = mMessageColours.begin(); it != mMessageColours.end(); ++it) + colours.append(QColor(*it).name()); + config->writeEntry(MESSAGE_COLOURS, colours); + config->writeEntry(MESSAGE_BG_COLOUR, mDefaultBgColour); + config->writeEntry(MESSAGE_FONT, mMessageFont); + config->writeEntry(RUN_IN_SYSTEM_TRAY, mRunInSystemTray); + config->writeEntry(DISABLE_IF_STOPPED, mDisableAlarmsIfStopped); + config->writeEntry(AUTOSTART_TRAY, mAutostartTrayIcon); + config->writeEntry(MODAL_MESSAGES, mModalMessages); + config->writeEntry(MESSAGE_BUTTON_DELAY, mMessageButtonDelay); + config->writeEntry(TOOLTIP_ALARM_COUNT, mTooltipAlarmCount); + config->writeEntry(TOOLTIP_ALARM_TIME, mShowTooltipAlarmTime); + config->writeEntry(TOOLTIP_TIME_TO_ALARM, mShowTooltipTimeToAlarm); + config->writeEntry(TOOLTIP_TIME_TO_PREFIX, mTooltipTimeToPrefix); + config->writeEntry(DAEMON_TRAY_INTERVAL, mDaemonTrayCheckInterval); + config->writeEntry(EMAIL_CLIENT, (mEmailClient == SENDMAIL ? "sendmail" : "kmail")); + config->writeEntry(EMAIL_COPY_TO_KMAIL, mEmailCopyToKMail); + config->writeEntry(EMAIL_FROM, emailFrom(mEmailFrom, true, false)); + config->writeEntry(EMAIL_BCC_ADDRESS, emailFrom(mEmailBccFrom, true, true)); + config->writeEntry(CMD_XTERM_COMMAND, translateXTermPath(config, mCmdXTermCommand, true)); + config->writeEntry(START_OF_DAY, QDateTime(QDate(1900,1,1), mStartOfDay)); + // Start-of-day check value is only written once the start-of-day time has been processed. + config->writeEntry(DISABLED_COLOUR, mDisabledColour); + config->writeEntry(EXPIRED_COLOUR, mExpiredColour); + config->writeEntry(EXPIRED_KEEP_DAYS, mExpiredKeepDays); + + config->setGroup(DEFAULTS_SECTION); + config->writeEntry(DEF_LATE_CANCEL, mDefaultLateCancel); + config->writeEntry(DEF_AUTO_CLOSE, mDefaultAutoClose); + config->writeEntry(DEF_CONFIRM_ACK, mDefaultConfirmAck); + config->writeEntry(DEF_COPY_TO_KORG, mDefaultCopyToKOrganizer); + config->writeEntry(DEF_SOUND_TYPE, mDefaultSoundType); + config->writePathEntry(DEF_SOUND_FILE, mDefaultSoundFile); + config->writeEntry(DEF_SOUND_VOLUME, static_cast<double>(mDefaultSoundVolume)); + config->writeEntry(DEF_SOUND_REPEAT, mDefaultSoundRepeat); + config->writeEntry(DEF_CMD_SCRIPT, mDefaultCmdScript); + config->writeEntry(DEF_CMD_LOG_TYPE, mDefaultCmdLogType); + config->writePathEntry(DEF_LOG_FILE, mDefaultCmdLogFile); + config->writeEntry(DEF_EMAIL_BCC, mDefaultEmailBcc); + config->writeEntry(DEF_RECUR_PERIOD, mDefaultRecurPeriod); + config->writeEntry(FEB29_RECUR_TYPE, (mDefaultFeb29Type == KARecurrence::FEB29_MAR1 ? "Mar1" : mDefaultFeb29Type == KARecurrence::FEB29_FEB28 ? "Feb28" : "None")); + QString value; + switch (mDefaultReminderUnits) + { + case TimePeriod::MINUTES: value = QString::fromLatin1("Minutes"); break; + case TimePeriod::HOURS_MINUTES: value = QString::fromLatin1("HoursMinutes"); break; + case TimePeriod::DAYS: value = QString::fromLatin1("Days"); break; + case TimePeriod::WEEKS: value = QString::fromLatin1("Weeks"); break; + default: value = QString::null; break; + } + config->writeEntry(DEF_REMIND_UNITS, value); + config->writeEntry(DEF_PRE_ACTION, mDefaultPreAction); + config->writeEntry(DEF_POST_ACTION, mDefaultPostAction); + + if (syncToDisc) + config->sync(); + mInstance->emitPreferencesChanged(); + if (mStartOfDay != mOldStartOfDay) + { + mInstance->emitStartOfDayChanged(); + mOldStartOfDay = mStartOfDay; + } +} + +void Preferences::syncToDisc() +{ + KGlobal::config()->sync(); +} + +void Preferences::updateStartOfDayCheck() +{ + KConfig* config = KGlobal::config(); + config->setGroup(GENERAL_SECTION); + config->writeEntry(START_OF_DAY_CHECK, startOfDayCheck()); + config->sync(); + mStartOfDayChanged = false; +} + +QString Preferences::emailFrom(Preferences::MailFrom from, bool useAddress, bool bcc) +{ + switch (from) + { + case MAIL_FROM_KMAIL: + return FROM_KMAIL; + case MAIL_FROM_CONTROL_CENTRE: + return FROM_CONTROL_CENTRE; + case MAIL_FROM_ADDR: + return useAddress ? (bcc ? mEmailBccAddress : mEmailAddress) : QString::null; + default: + return QString::null; + } +} + +Preferences::MailFrom Preferences::emailFrom(const QString& str) +{ + if (str == FROM_KMAIL) + return MAIL_FROM_KMAIL; + if (str == FROM_CONTROL_CENTRE) + return MAIL_FROM_CONTROL_CENTRE; + return MAIL_FROM_ADDR; +} + +/****************************************************************************** +* Get user's default 'From' email address. +*/ +QString Preferences::emailAddress() +{ + switch (mEmailFrom) + { + case MAIL_FROM_KMAIL: + return KAMail::identityManager()->defaultIdentity().fullEmailAddr(); + case MAIL_FROM_CONTROL_CENTRE: + return KAMail::controlCentreAddress(); + case MAIL_FROM_ADDR: + return mEmailAddress; + default: + return QString::null; + } +} + +QString Preferences::emailBccAddress() +{ + switch (mEmailBccFrom) + { + case MAIL_FROM_CONTROL_CENTRE: + return KAMail::controlCentreAddress(); + case MAIL_FROM_ADDR: + return mEmailBccAddress; + default: + return QString::null; + } +} + +void Preferences::setEmailAddress(Preferences::MailFrom from, const QString& address) +{ + switch (from) + { + case MAIL_FROM_KMAIL: + break; + case MAIL_FROM_CONTROL_CENTRE: + mEmailAddress = KAMail::controlCentreAddress(); + break; + case MAIL_FROM_ADDR: + mEmailAddress = address; + break; + default: + return; + } + mEmailFrom = from; +} + +void Preferences::setEmailBccAddress(bool useControlCentre, const QString& address) +{ + if (useControlCentre) + mEmailBccAddress = KAMail::controlCentreAddress(); + else + mEmailBccAddress = address; + mEmailBccFrom = useControlCentre ? MAIL_FROM_CONTROL_CENTRE : MAIL_FROM_ADDR; +} + +/****************************************************************************** +* Called to allow or suppress output of the specified message dialog, where the +* dialog has a checkbox to turn notification off. +*/ +void Preferences::setNotify(const QString& messageID, bool notify) +{ + MessageBox::saveDontShowAgainContinue(messageID, !notify); +} + +/****************************************************************************** +* Return whether the specified message dialog is output, where the dialog has +* a checkbox to turn notification off. +* Reply = false if message has been suppressed (by preferences or by selecting +* "don't ask again") +* = true in all other cases. +*/ +bool Preferences::notifying(const QString& messageID) +{ + return MessageBox::shouldBeShownContinue(messageID); +} + +/****************************************************************************** +* If the preferences were written by a previous version of KAlarm, do any +* necessary conversions. +*/ +void Preferences::convertOldPrefs() +{ + KConfig* config = KGlobal::config(); + config->setGroup(GENERAL_SECTION); + int version = KAlarm::getVersionNumber(config->readEntry(VERSION_NUM)); + if (version >= KAlarm::Version(1,4,22)) + return; // config format is up to date + + if (version <= KAlarm::Version(1,4,21)) + { + // Convert KAlarm 1.4.21 preferences + static const QString OLD_REMIND_UNITS = QString::fromLatin1("DefRemindUnits"); + config->setGroup(DEFAULTS_SECTION); + int intUnit = config->readNumEntry(OLD_REMIND_UNITS, 0); + QString strUnit = (intUnit == 1) ? QString::fromLatin1("Days") + : (intUnit == 2) ? QString::fromLatin1("Weeks") + : QString::fromLatin1("HoursMinutes"); + config->deleteEntry(OLD_REMIND_UNITS); + config->writeEntry(DEF_REMIND_UNITS, strUnit); + } + + if (version <= KAlarm::Version(1,4,20)) + { + // Convert KAlarm 1.4.20 preferences + static const QString VIEW_SECTION = QString::fromLatin1("View"); + static const QString SHOW_ARCHIVED_ALARMS = QString::fromLatin1("ShowArchivedAlarms"); + static const QString SHOW_EXPIRED_ALARMS = QString::fromLatin1("ShowExpiredAlarms"); + static const QString SHOW_ALARM_TIME = QString::fromLatin1("ShowAlarmTime"); + static const QString SHOW_TIME_TO_ALARM = QString::fromLatin1("ShowTimeToAlarm"); + config->setGroup(GENERAL_SECTION); + bool showExpired = config->readBoolEntry(SHOW_EXPIRED_ALARMS, false); + bool showTime = config->readBoolEntry(SHOW_ALARM_TIME, true); + bool showTimeTo = config->readBoolEntry(SHOW_TIME_TO_ALARM, false); + config->deleteEntry(SHOW_EXPIRED_ALARMS); + config->deleteEntry(SHOW_ALARM_TIME); + config->deleteEntry(SHOW_TIME_TO_ALARM); + config->setGroup(VIEW_SECTION); + config->writeEntry(SHOW_ARCHIVED_ALARMS, showExpired); + config->writeEntry(SHOW_ALARM_TIME, showTime); + config->writeEntry(SHOW_TIME_TO_ALARM, showTimeTo); + } + + if (version <= KAlarm::Version(1,4,5)) + { + // Convert KAlarm 1.4.5 preferences + static const QString DEF_SOUND = QString::fromLatin1("DefSound"); + config->setGroup(DEFAULTS_SECTION); + bool sound = config->readBoolEntry(DEF_SOUND, false); + if (!sound) + config->writeEntry(DEF_SOUND_TYPE, SoundPicker::NONE); + config->deleteEntry(DEF_SOUND); + } + + if (version < KAlarm::Version(1,3,0)) + { + // Convert KAlarm pre-1.3 preferences + static const QString EMAIL_ADDRESS = QString::fromLatin1("EmailAddress"); + static const QString EMAIL_USE_CTRL_CENTRE = QString::fromLatin1("EmailUseControlCenter"); + static const QString EMAIL_BCC_USE_CTRL_CENTRE = QString::fromLatin1("EmailBccUseControlCenter"); + QMap<QString, QString> entries = config->entryMap(GENERAL_SECTION); + if (entries.find(EMAIL_FROM) == entries.end() + && entries.find(EMAIL_USE_CTRL_CENTRE) != entries.end()) + { + // Preferences were written by KAlarm pre-1.2.1 + config->setGroup(GENERAL_SECTION); + bool useCC = false; + bool bccUseCC = false; + const bool default_emailUseControlCentre = true; + const bool default_emailBccUseControlCentre = true; + useCC = config->readBoolEntry(EMAIL_USE_CTRL_CENTRE, default_emailUseControlCentre); + // EmailBccUseControlCenter was missing in preferences written by KAlarm pre-0.9.5 + bccUseCC = config->hasKey(EMAIL_BCC_USE_CTRL_CENTRE) + ? config->readBoolEntry(EMAIL_BCC_USE_CTRL_CENTRE, default_emailBccUseControlCentre) + : useCC; + config->writeEntry(EMAIL_FROM, (useCC ? FROM_CONTROL_CENTRE : config->readEntry(EMAIL_ADDRESS))); + config->writeEntry(EMAIL_BCC_ADDRESS, (bccUseCC ? FROM_CONTROL_CENTRE : config->readEntry(EMAIL_BCC_ADDRESS))); + config->deleteEntry(EMAIL_ADDRESS); + config->deleteEntry(EMAIL_BCC_USE_CTRL_CENTRE); + config->deleteEntry(EMAIL_USE_CTRL_CENTRE); + } + // Convert KAlarm 1.2 preferences + static const QString DEF_CMD_XTERM = QString::fromLatin1("DefCmdXterm"); + config->setGroup(DEFAULTS_SECTION); + if (config->hasKey(DEF_CMD_XTERM)) + { + config->writeEntry(DEF_CMD_LOG_TYPE, + (config->readBoolEntry(DEF_CMD_XTERM, false) ? EditAlarmDlg::EXEC_IN_TERMINAL : EditAlarmDlg::DISCARD_OUTPUT)); + config->deleteEntry(DEF_CMD_XTERM); + } + } + config->setGroup(GENERAL_SECTION); + config->writeEntry(VERSION_NUM, KALARM_VERSION); + config->sync(); +} + +/****************************************************************************** +* Translate an X terminal command path to/from config file format. +* Note that only a home directory specification at the start of the path is +* translated, so there's no need to worry about missing out some of the +* executable's path due to quotes etc. +* N.B. Calling KConfig::read/writePathEntry() on the entire command line +* causes a crash on some systems, so it's necessary to extract the +* executable path first before processing. +*/ +QString translateXTermPath(KConfig* config, const QString& cmdline, bool write) +{ + QString params; + QString cmd = cmdline; + if (cmdline.isEmpty()) + return cmdline; + // Strip any leading quote + QChar quote = cmdline[0]; + char q = static_cast<char>(quote); + bool quoted = (q == '"' || q == '\''); + if (quoted) + cmd = cmdline.mid(1); + // Split the command at the first non-escaped space + for (int i = 0, count = cmd.length(); i < count; ++i) + { + switch (cmd[i].latin1()) + { + case '\\': + ++i; + continue; + case '"': + case '\'': + if (cmd[i] != quote) + continue; + // fall through to ' ' + case ' ': + params = cmd.mid(i); + cmd = cmd.left(i); + break; + default: + continue; + } + break; + } + // Translate any home directory specification at the start of the + // executable's path. + if (write) + { + config->writePathEntry(TEMP, cmd); + cmd = config->readEntry(TEMP); + } + else + { + config->writeEntry(TEMP, cmd); + cmd = config->readPathEntry(TEMP); + } + config->deleteEntry(TEMP); + if (quoted) + return quote + cmd + params; + else + return cmd + params; +} diff --git a/kalarm/preferences.h b/kalarm/preferences.h new file mode 100644 index 000000000..f0eeb4ea6 --- /dev/null +++ b/kalarm/preferences.h @@ -0,0 +1,235 @@ +/* + * preferences.h - program preference settings + * Program: kalarm + * Copyright © 2001-2007 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef PREFERENCES_H +#define PREFERENCES_H + +#include "kalarm.h" + +#include <qobject.h> +#include <qcolor.h> +#include <qfont.h> +#include <qdatetime.h> +#include <qvaluelist.h> +class QWidget; + +#include "colourlist.h" +#include "editdlg.h" +#include "karecurrence.h" +#include "recurrenceedit.h" +#include "soundpicker.h" +#include "timeperiod.h" + + +// Settings configured in the Preferences dialog +class Preferences : public QObject +{ + Q_OBJECT + public: + enum MailClient { SENDMAIL, KMAIL }; + enum MailFrom { MAIL_FROM_KMAIL, MAIL_FROM_CONTROL_CENTRE, MAIL_FROM_ADDR }; + enum CmdLogType { DISCARD_OUTPUT, LOG_TO_FILE, EXEC_IN_TERMINAL }; + + static void initialise(); + static void save(bool syncToDisc = true); + static void syncToDisc(); + static void updateStartOfDayCheck(); + static void connect(const char* signal, const QObject* receiver, const char* member); + + // Access to settings + static const ColourList& messageColours() { return mMessageColours; } + static QColor defaultBgColour() { return mDefaultBgColour; } + static QColor defaultFgColour() { return default_defaultFgColour; } + static const QFont& messageFont() { return mMessageFont; } + static const QTime& startOfDay() { return mStartOfDay; } + static bool hasStartOfDayChanged() { return mStartOfDayChanged; } + static bool runInSystemTray() { return mRunInSystemTray; } + static bool disableAlarmsIfStopped() { return mDisableAlarmsIfStopped; } + static bool quitWarn() { return notifying(QUIT_WARN); } + static void setQuitWarn(bool yes) { setNotify(QUIT_WARN, yes); } + static bool autostartTrayIcon() { return mAutostartTrayIcon; } + static bool confirmAlarmDeletion() { return notifying(CONFIRM_ALARM_DELETION); } + static void setConfirmAlarmDeletion(bool yes){ setNotify(CONFIRM_ALARM_DELETION, yes); } + static bool modalMessages() { return mModalMessages; } + static int messageButtonDelay() { return mMessageButtonDelay; } + static int tooltipAlarmCount() { return mTooltipAlarmCount; } + static bool showTooltipAlarmTime() { return mShowTooltipAlarmTime; } + static bool showTooltipTimeToAlarm() { return mShowTooltipTimeToAlarm; } + static const QString& tooltipTimeToPrefix() { return mTooltipTimeToPrefix; } + static int daemonTrayCheckInterval() { return mDaemonTrayCheckInterval; } + static MailClient emailClient() { return mEmailClient; } + static bool emailCopyToKMail() { return mEmailCopyToKMail && mEmailClient == SENDMAIL; } + static bool emailQueuedNotify() { return notifying(EMAIL_QUEUED_NOTIFY); } + static void setEmailQueuedNotify(bool yes) { setNotify(EMAIL_QUEUED_NOTIFY, yes); } + static MailFrom emailFrom() { return mEmailFrom; } + static bool emailBccUseControlCentre() { return mEmailBccFrom == MAIL_FROM_CONTROL_CENTRE; } + static QString emailAddress(); + static QString emailBccAddress(); + static QString cmdXTermCommand() { return mCmdXTermCommand; } + static QColor disabledColour() { return mDisabledColour; } + static QColor expiredColour() { return mExpiredColour; } + static int expiredKeepDays() { return mExpiredKeepDays; } + static SoundPicker::Type defaultSoundType() { return mDefaultSoundType; } + static const QString& defaultSoundFile() { return mDefaultSoundFile; } + static float defaultSoundVolume() { return mDefaultSoundVolume; } + static bool defaultSoundRepeat() { return mDefaultSoundRepeat; } + static int defaultLateCancel() { return mDefaultLateCancel; } + static bool defaultAutoClose() { return mDefaultAutoClose; } + static bool defaultConfirmAck() { return mDefaultConfirmAck; } + static bool defaultCopyToKOrganizer() { return mDefaultCopyToKOrganizer; } + static bool defaultCmdScript() { return mDefaultCmdScript; } + static EditAlarmDlg::CmdLogType + defaultCmdLogType() { return mDefaultCmdLogType; } + static QString defaultCmdLogFile() { return mDefaultCmdLogFile; } + static bool defaultEmailBcc() { return mDefaultEmailBcc; } + static RecurrenceEdit::RepeatType + defaultRecurPeriod() { return mDefaultRecurPeriod; } + static KARecurrence::Feb29Type + defaultFeb29Type() { return mDefaultFeb29Type; } + static TimePeriod::Units defaultReminderUnits() { return mDefaultReminderUnits; } + static const QString& defaultPreAction() { return mDefaultPreAction; } + static const QString& defaultPostAction() { return mDefaultPostAction; } + + // Config file entry names for notification messages + static const QString QUIT_WARN; + static const QString CONFIRM_ALARM_DELETION; + static const QString EMAIL_QUEUED_NOTIFY; + + // Default values for settings + static const ColourList default_messageColours; + static const QColor default_defaultBgColour; + static const QColor default_defaultFgColour; + static const QFont& default_messageFont() { return mDefault_messageFont; }; + static const QTime default_startOfDay; + static const bool default_runInSystemTray; + static const bool default_disableAlarmsIfStopped; + static const bool default_quitWarn; + static const bool default_autostartTrayIcon; + static const bool default_confirmAlarmDeletion; + static const bool default_modalMessages; + static const int default_messageButtonDelay; + static const int default_tooltipAlarmCount; + static const bool default_showTooltipAlarmTime; + static const bool default_showTooltipTimeToAlarm; + static const QString default_tooltipTimeToPrefix; + static const int default_daemonTrayCheckInterval; + static const MailClient default_emailClient; + static const bool default_emailCopyToKMail; + static MailFrom default_emailFrom(); + static const bool default_emailQueuedNotify; + static const MailFrom default_emailBccFrom; + static const QString default_emailAddress; + static const QString default_emailBccAddress; + static const QColor default_disabledColour; + static const QColor default_expiredColour; + static const int default_expiredKeepDays; + static const QString default_defaultSoundFile; + static const float default_defaultSoundVolume; + static const int default_defaultLateCancel; + static const bool default_defaultAutoClose; + static const bool default_defaultCopyToKOrganizer; + static const SoundPicker::Type default_defaultSoundType; + static const bool default_defaultSoundRepeat; + static const bool default_defaultConfirmAck; + static const bool default_defaultCmdScript; + static const EditAlarmDlg::CmdLogType default_defaultCmdLogType; + static const bool default_defaultEmailBcc; + static const RecurrenceEdit::RepeatType default_defaultRecurPeriod; + static const KARecurrence::Feb29Type default_defaultFeb29Type; + static const TimePeriod::Units default_defaultReminderUnits; + static const QString default_defaultPreAction; + static const QString default_defaultPostAction; + + signals: + void preferencesChanged(); + void startOfDayChanged(const QTime& oldStartOfDay); + + private: + Preferences() { } // only one instance allowed + void emitPreferencesChanged(); + void emitStartOfDayChanged(); + + static void read(); + static void convertOldPrefs(); + static int startOfDayCheck(); + static QString emailFrom(MailFrom, bool useAddress, bool bcc); + static MailFrom emailFrom(const QString&); + static void setNotify(const QString& messageID, bool notify); + static bool notifying(const QString& messageID); + + static Preferences* mInstance; + static QFont mDefault_messageFont; + static QString mEmailAddress; + static QString mEmailBccAddress; + + // All the following members are accessed by the Preferences dialog classes + friend class MiscPrefTab; + friend class EditPrefTab; + friend class ViewPrefTab; + friend class FontColourPrefTab; + friend class EmailPrefTab; + static void setEmailAddress(MailFrom, const QString& address); + static void setEmailBccAddress(bool useControlCentre, const QString& address); + static ColourList mMessageColours; + static QColor mDefaultBgColour; + static QFont mMessageFont; + static QTime mStartOfDay; + static bool mRunInSystemTray; + static bool mDisableAlarmsIfStopped; + static bool mAutostartTrayIcon; + static bool mModalMessages; + static int mMessageButtonDelay; // 0 = scatter; -1 = no delay, no scatter; >0 = delay, no scatter + static int mTooltipAlarmCount; + static bool mShowTooltipAlarmTime; + static bool mShowTooltipTimeToAlarm; + static QString mTooltipTimeToPrefix; + static int mDaemonTrayCheckInterval; + static MailClient mEmailClient; + static MailFrom mEmailFrom; + static MailFrom mEmailBccFrom; + static bool mEmailCopyToKMail; + static QString mCmdXTermCommand; + static QColor mDisabledColour; + static QColor mExpiredColour; + static int mExpiredKeepDays; // 0 = don't keep, -1 = keep indefinitely + // Default settings for Edit Alarm dialog + static QString mDefaultSoundFile; + static float mDefaultSoundVolume; + static int mDefaultLateCancel; + static bool mDefaultAutoClose; + static bool mDefaultCopyToKOrganizer; + static SoundPicker::Type mDefaultSoundType; + static bool mDefaultSoundRepeat; + static bool mDefaultConfirmAck; + static bool mDefaultEmailBcc; + static bool mDefaultCmdScript; + static EditAlarmDlg::CmdLogType mDefaultCmdLogType; + static QString mDefaultCmdLogFile; + static RecurrenceEdit::RepeatType mDefaultRecurPeriod; + static KARecurrence::Feb29Type mDefaultFeb29Type; + static TimePeriod::Units mDefaultReminderUnits; + static QString mDefaultPreAction; + static QString mDefaultPostAction; + // Change tracking + static QTime mOldStartOfDay; // previous start-of-day time + static bool mStartOfDayChanged; // start-of-day check value doesn't tally with mStartOfDay +}; + +#endif // PREFERENCES_H diff --git a/kalarm/recurrenceedit.cpp b/kalarm/recurrenceedit.cpp new file mode 100644 index 000000000..c7aef494b --- /dev/null +++ b/kalarm/recurrenceedit.cpp @@ -0,0 +1,1639 @@ +/* + * recurrenceedit.cpp - widget to edit the event's recurrence definition + * Program: kalarm + * Copyright © 2002-2008 by David Jarvie <djarvie@kde.org> + * + * Based originally on KOrganizer module koeditorrecurrence.cpp, + * Copyright (c) 2000,2001 Cornelius Schumacher <schumacher@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qtooltip.h> +#include <qlayout.h> +#include <qvbox.h> +#include <qwidgetstack.h> +#include <qlistbox.h> +#include <qframe.h> +#include <qlabel.h> +#include <qpushbutton.h> +#include <qlineedit.h> +#include <qwhatsthis.h> + +#include <kglobal.h> +#include <klocale.h> +#include <kcalendarsystem.h> +#include <kiconloader.h> +#include <kdialog.h> +#include <kmessagebox.h> +#include <kdebug.h> + +#include <libkcal/event.h> + +#include "alarmevent.h" +#include "alarmtimewidget.h" +#include "checkbox.h" +#include "combobox.h" +#include "dateedit.h" +#include "functions.h" +#include "kalarmapp.h" +#include "karecurrence.h" +#include "preferences.h" +#include "radiobutton.h" +#include "repetition.h" +#include "spinbox.h" +#include "timeedit.h" +#include "timespinbox.h" +#include "buttongroup.h" +using namespace KCal; + +#include "recurrenceedit.moc" +#include "recurrenceeditprivate.moc" + +// Collect these widget labels together to ensure consistent wording and +// translations across different modules. +QString RecurrenceEdit::i18n_Norecur() { return i18n("No recurrence"); } +QString RecurrenceEdit::i18n_NoRecur() { return i18n("No Recurrence"); } +QString RecurrenceEdit::i18n_AtLogin() { return i18n("At Login"); } +QString RecurrenceEdit::i18n_l_Atlogin() { return i18n("At &login"); } +QString RecurrenceEdit::i18n_HourlyMinutely() { return i18n("Hourly/Minutely"); } +QString RecurrenceEdit::i18n_u_HourlyMinutely() { return i18n("Ho&urly/Minutely"); } +QString RecurrenceEdit::i18n_Daily() { return i18n("Daily"); } +QString RecurrenceEdit::i18n_d_Daily() { return i18n("&Daily"); } +QString RecurrenceEdit::i18n_Weekly() { return i18n("Weekly"); } +QString RecurrenceEdit::i18n_w_Weekly() { return i18n("&Weekly"); } +QString RecurrenceEdit::i18n_Monthly() { return i18n("Monthly"); } +QString RecurrenceEdit::i18n_m_Monthly() { return i18n("&Monthly"); } +QString RecurrenceEdit::i18n_Yearly() { return i18n("Yearly"); } +QString RecurrenceEdit::i18n_y_Yearly() { return i18n("&Yearly"); } + + +RecurrenceEdit::RecurrenceEdit(bool readOnly, QWidget* parent, const char* name) + : QFrame(parent, name), + mRule(0), + mRuleButtonType(INVALID_RECUR), + mDailyShown(false), + mWeeklyShown(false), + mMonthlyShown(false), + mYearlyShown(false), + mNoEmitTypeChanged(true), + mReadOnly(readOnly) +{ + QBoxLayout* layout; + QVBoxLayout* topLayout = new QVBoxLayout(this, 0, KDialog::spacingHint()); + + /* Create the recurrence rule Group box which holds the recurrence period + * selection buttons, and the weekly, monthly and yearly recurrence rule + * frames which specify options individual to each of these distinct + * sections of the recurrence rule. Each frame is made visible by the + * selection of its corresponding radio button. + */ + + QGroupBox* recurGroup = new QGroupBox(1, Qt::Vertical, i18n("Recurrence Rule"), this, "recurGroup"); + topLayout->addWidget(recurGroup); + QFrame* ruleFrame = new QFrame(recurGroup, "ruleFrame"); + layout = new QVBoxLayout(ruleFrame, 0); + layout->addSpacing(KDialog::spacingHint()/2); + + layout = new QHBoxLayout(layout, 0); + QBoxLayout* lay = new QVBoxLayout(layout, 0); + mRuleButtonGroup = new ButtonGroup(1, Qt::Horizontal, ruleFrame); + mRuleButtonGroup->setInsideMargin(0); + mRuleButtonGroup->setFrameStyle(QFrame::NoFrame); + lay->addWidget(mRuleButtonGroup); + lay->addStretch(); // top-adjust the interval radio buttons + connect(mRuleButtonGroup, SIGNAL(buttonSet(int)), SLOT(periodClicked(int))); + + mNoneButton = new RadioButton(i18n_Norecur(), mRuleButtonGroup); + mNoneButton->setFixedSize(mNoneButton->sizeHint()); + mNoneButton->setReadOnly(mReadOnly); + QWhatsThis::add(mNoneButton, i18n("Do not repeat the alarm")); + + mAtLoginButton = new RadioButton(i18n_l_Atlogin(), mRuleButtonGroup); + mAtLoginButton->setFixedSize(mAtLoginButton->sizeHint()); + mAtLoginButton->setReadOnly(mReadOnly); + QWhatsThis::add(mAtLoginButton, + i18n("Trigger the alarm at the specified date/time and at every login until then.\n" + "Note that it will also be triggered any time the alarm daemon is restarted.")); + + mSubDailyButton = new RadioButton(i18n_u_HourlyMinutely(), mRuleButtonGroup); + mSubDailyButton->setFixedSize(mSubDailyButton->sizeHint()); + mSubDailyButton->setReadOnly(mReadOnly); + QWhatsThis::add(mSubDailyButton, + i18n("Repeat the alarm at hourly/minutely intervals")); + + mDailyButton = new RadioButton(i18n_d_Daily(), mRuleButtonGroup); + mDailyButton->setFixedSize(mDailyButton->sizeHint()); + mDailyButton->setReadOnly(mReadOnly); + QWhatsThis::add(mDailyButton, + i18n("Repeat the alarm at daily intervals")); + + mWeeklyButton = new RadioButton(i18n_w_Weekly(), mRuleButtonGroup); + mWeeklyButton->setFixedSize(mWeeklyButton->sizeHint()); + mWeeklyButton->setReadOnly(mReadOnly); + QWhatsThis::add(mWeeklyButton, + i18n("Repeat the alarm at weekly intervals")); + + mMonthlyButton = new RadioButton(i18n_m_Monthly(), mRuleButtonGroup); + mMonthlyButton->setFixedSize(mMonthlyButton->sizeHint()); + mMonthlyButton->setReadOnly(mReadOnly); + QWhatsThis::add(mMonthlyButton, + i18n("Repeat the alarm at monthly intervals")); + + mYearlyButton = new RadioButton(i18n_y_Yearly(), mRuleButtonGroup); + mYearlyButton->setFixedSize(mYearlyButton->sizeHint()); + mYearlyButton->setReadOnly(mReadOnly); + QWhatsThis::add(mYearlyButton, + i18n("Repeat the alarm at annual intervals")); + + mNoneButtonId = mRuleButtonGroup->id(mNoneButton); + mAtLoginButtonId = mRuleButtonGroup->id(mAtLoginButton); + mSubDailyButtonId = mRuleButtonGroup->id(mSubDailyButton); + mDailyButtonId = mRuleButtonGroup->id(mDailyButton); + mWeeklyButtonId = mRuleButtonGroup->id(mWeeklyButton); + mMonthlyButtonId = mRuleButtonGroup->id(mMonthlyButton); + mYearlyButtonId = mRuleButtonGroup->id(mYearlyButton); + + // Sub-repetition button + mSubRepetition = new RepetitionButton(i18n("Sub-Repetition"), true, ruleFrame); + mSubRepetition->setFixedSize(mSubRepetition->sizeHint()); + mSubRepetition->setReadOnly(mReadOnly); + connect(mSubRepetition, SIGNAL(needsInitialisation()), SIGNAL(repeatNeedsInitialisation())); + connect(mSubRepetition, SIGNAL(changed()), SIGNAL(frequencyChanged())); + QWhatsThis::add(mSubRepetition, i18n("Set up a repetition within the recurrence, to trigger the alarm multiple times each time the recurrence is due.")); + lay->addSpacing(KDialog::spacingHint()); + lay->addWidget(mSubRepetition); + + lay = new QVBoxLayout(layout); + + lay->addStretch(); + layout = new QHBoxLayout(lay); + + layout->addSpacing(KDialog::marginHint()); + QFrame* divider = new QFrame(ruleFrame); + divider->setFrameStyle(QFrame::VLine | QFrame::Sunken); + layout->addWidget(divider); + layout->addSpacing(KDialog::marginHint()); + + mNoRule = new NoRule(ruleFrame, "noFrame"); + mSubDailyRule = new SubDailyRule(mReadOnly, ruleFrame, "subdayFrame"); + mDailyRule = new DailyRule(mReadOnly, ruleFrame, "dayFrame"); + mWeeklyRule = new WeeklyRule(mReadOnly, ruleFrame, "weekFrame"); + mMonthlyRule = new MonthlyRule(mReadOnly, ruleFrame, "monthFrame"); + mYearlyRule = new YearlyRule(mReadOnly, ruleFrame, "yearFrame"); + + connect(mSubDailyRule, SIGNAL(frequencyChanged()), this, SIGNAL(frequencyChanged())); + connect(mDailyRule, SIGNAL(frequencyChanged()), this, SIGNAL(frequencyChanged())); + connect(mWeeklyRule, SIGNAL(frequencyChanged()), this, SIGNAL(frequencyChanged())); + connect(mMonthlyRule, SIGNAL(frequencyChanged()), this, SIGNAL(frequencyChanged())); + connect(mYearlyRule, SIGNAL(frequencyChanged()), this, SIGNAL(frequencyChanged())); + + mRuleStack = new QWidgetStack(ruleFrame); + layout->addWidget(mRuleStack); + layout->addStretch(1); + mRuleStack->addWidget(mNoRule, 0); + mRuleStack->addWidget(mSubDailyRule, 1); + mRuleStack->addWidget(mDailyRule, 2); + mRuleStack->addWidget(mWeeklyRule, 3); + mRuleStack->addWidget(mMonthlyRule, 4); + mRuleStack->addWidget(mYearlyRule, 5); + layout->addSpacing(KDialog::marginHint()); + + // Create the recurrence range group which contains the controls + // which specify how long the recurrence is to last. + + mRangeButtonGroup = new ButtonGroup(i18n("Recurrence End"), this, "mRangeButtonGroup"); + connect(mRangeButtonGroup, SIGNAL(buttonSet(int)), SLOT(rangeTypeClicked())); + topLayout->addWidget(mRangeButtonGroup); + + QVBoxLayout* vlayout = new QVBoxLayout(mRangeButtonGroup, KDialog::marginHint(), KDialog::spacingHint()); + vlayout->addSpacing(fontMetrics().lineSpacing()/2); + mNoEndDateButton = new RadioButton(i18n("No &end"), mRangeButtonGroup); + mNoEndDateButton->setFixedSize(mNoEndDateButton->sizeHint()); + mNoEndDateButton->setReadOnly(mReadOnly); + QWhatsThis::add(mNoEndDateButton, i18n("Repeat the alarm indefinitely")); + vlayout->addWidget(mNoEndDateButton, 1, Qt::AlignAuto); + QSize size = mNoEndDateButton->size(); + + layout = new QHBoxLayout(vlayout, KDialog::spacingHint()); + mRepeatCountButton = new RadioButton(i18n("End a&fter:"), mRangeButtonGroup); + mRepeatCountButton->setReadOnly(mReadOnly); + QWhatsThis::add(mRepeatCountButton, + i18n("Repeat the alarm for the number of times specified")); + mRepeatCountEntry = new SpinBox(1, 9999, 1, mRangeButtonGroup); + mRepeatCountEntry->setFixedSize(mRepeatCountEntry->sizeHint()); + mRepeatCountEntry->setLineShiftStep(10); + mRepeatCountEntry->setSelectOnStep(false); + mRepeatCountEntry->setReadOnly(mReadOnly); + connect(mRepeatCountEntry, SIGNAL(valueChanged(int)), SLOT(repeatCountChanged(int))); + QWhatsThis::add(mRepeatCountEntry, + i18n("Enter the total number of times to trigger the alarm")); + mRepeatCountButton->setFocusWidget(mRepeatCountEntry); + mRepeatCountLabel = new QLabel(i18n("occurrence(s)"), mRangeButtonGroup); + mRepeatCountLabel->setFixedSize(mRepeatCountLabel->sizeHint()); + layout->addWidget(mRepeatCountButton); + layout->addSpacing(KDialog::spacingHint()); + layout->addWidget(mRepeatCountEntry); + layout->addWidget(mRepeatCountLabel); + layout->addStretch(); + size = size.expandedTo(mRepeatCountButton->sizeHint()); + + layout = new QHBoxLayout(vlayout, KDialog::spacingHint()); + mEndDateButton = new RadioButton(i18n("End &by:"), mRangeButtonGroup); + mEndDateButton->setReadOnly(mReadOnly); + QWhatsThis::add(mEndDateButton, + i18n("Repeat the alarm until the date/time specified.\n\n" + "Note: This applies to the main recurrence only. It does not limit any sub-repetition which will occur regardless after the last main recurrence.")); + mEndDateEdit = new DateEdit(mRangeButtonGroup); + mEndDateEdit->setFixedSize(mEndDateEdit->sizeHint()); + mEndDateEdit->setReadOnly(mReadOnly); + QWhatsThis::add(mEndDateEdit, + i18n("Enter the last date to repeat the alarm")); + mEndDateButton->setFocusWidget(mEndDateEdit); + mEndTimeEdit = new TimeEdit(mRangeButtonGroup); + mEndTimeEdit->setFixedSize(mEndTimeEdit->sizeHint()); + mEndTimeEdit->setReadOnly(mReadOnly); + static const QString lastTimeText = i18n("Enter the last time to repeat the alarm."); + QWhatsThis::add(mEndTimeEdit, QString("%1\n\n%2").arg(lastTimeText).arg(TimeSpinBox::shiftWhatsThis())); + mEndAnyTimeCheckBox = new CheckBox(i18n("Any time"), mRangeButtonGroup); + mEndAnyTimeCheckBox->setFixedSize(mEndAnyTimeCheckBox->sizeHint()); + mEndAnyTimeCheckBox->setReadOnly(mReadOnly); + connect(mEndAnyTimeCheckBox, SIGNAL(toggled(bool)), SLOT(slotAnyTimeToggled(bool))); + QWhatsThis::add(mEndAnyTimeCheckBox, + i18n("Stop repeating the alarm after your first login on or after the specified end date")); + layout->addWidget(mEndDateButton); + layout->addSpacing(KDialog::spacingHint()); + layout->addWidget(mEndDateEdit); + layout->addWidget(mEndTimeEdit); + layout->addWidget(mEndAnyTimeCheckBox); + layout->addStretch(); + size = size.expandedTo(mEndDateButton->sizeHint()); + + // Line up the widgets to the right of the radio buttons + mRepeatCountButton->setFixedSize(size); + mEndDateButton->setFixedSize(size); + + // Create the exceptions group which specifies dates to be excluded + // from the recurrence. + + mExceptionGroup = new QGroupBox(i18n("E&xceptions"), this, "mExceptionGroup"); + topLayout->addWidget(mExceptionGroup); + topLayout->setStretchFactor(mExceptionGroup, 2); + vlayout = new QVBoxLayout(mExceptionGroup, KDialog::marginHint(), KDialog::spacingHint()); + vlayout->addSpacing(fontMetrics().lineSpacing()/2); + layout = new QHBoxLayout(vlayout, KDialog::spacingHint()); + vlayout = new QVBoxLayout(layout); + + mExceptionDateList = new QListBox(mExceptionGroup); + mExceptionDateList->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding)); + connect(mExceptionDateList, SIGNAL(selectionChanged()), SLOT(enableExceptionButtons())); + QWhatsThis::add(mExceptionDateList, + i18n("The list of exceptions, i.e. dates/times excluded from the recurrence")); + vlayout->addWidget(mExceptionDateList); + + if (mReadOnly) + { + mExceptionDateEdit = 0; + mChangeExceptionButton = 0; + mDeleteExceptionButton = 0; + } + else + { + vlayout = new QVBoxLayout(layout); + mExceptionDateEdit = new DateEdit(mExceptionGroup); + mExceptionDateEdit->setFixedSize(mExceptionDateEdit->sizeHint()); + mExceptionDateEdit->setDate(QDate::currentDate()); + QWhatsThis::add(mExceptionDateEdit, + i18n("Enter a date to insert in the exceptions list. " + "Use in conjunction with the Add or Change button below.")); + vlayout->addWidget(mExceptionDateEdit); + + layout = new QHBoxLayout(vlayout, KDialog::spacingHint()); + QPushButton* button = new QPushButton(i18n("Add"), mExceptionGroup); + button->setFixedSize(button->sizeHint()); + connect(button, SIGNAL(clicked()), SLOT(addException())); + QWhatsThis::add(button, + i18n("Add the date entered above to the exceptions list")); + layout->addWidget(button); + + mChangeExceptionButton = new QPushButton(i18n("Change"), mExceptionGroup); + mChangeExceptionButton->setFixedSize(mChangeExceptionButton->sizeHint()); + connect(mChangeExceptionButton, SIGNAL(clicked()), SLOT(changeException())); + QWhatsThis::add(mChangeExceptionButton, + i18n("Replace the currently highlighted item in the exceptions list with the date entered above")); + layout->addWidget(mChangeExceptionButton); + + mDeleteExceptionButton = new QPushButton(i18n("Delete"), mExceptionGroup); + mDeleteExceptionButton->setFixedSize(mDeleteExceptionButton->sizeHint()); + connect(mDeleteExceptionButton, SIGNAL(clicked()), SLOT(deleteException())); + QWhatsThis::add(mDeleteExceptionButton, + i18n("Remove the currently highlighted item from the exceptions list")); + layout->addWidget(mDeleteExceptionButton); + } + + mNoEmitTypeChanged = false; +} + +/****************************************************************************** + * Verify the consistency of the entered data. + * Reply = widget to receive focus on error, or 0 if no error. + */ +QWidget* RecurrenceEdit::checkData(const QDateTime& startDateTime, QString& errorMessage) const +{ + if (mAtLoginButton->isOn()) + return 0; + const_cast<RecurrenceEdit*>(this)->mCurrStartDateTime = startDateTime; + if (mEndDateButton->isChecked()) + { + QWidget* errWidget = 0; + bool noTime = !mEndTimeEdit->isEnabled(); + QDate endDate = mEndDateEdit->date(); + if (endDate < startDateTime.date()) + errWidget = mEndDateEdit; + else if (!noTime && QDateTime(endDate, mEndTimeEdit->time()) < startDateTime) + errWidget = mEndTimeEdit; + if (errWidget) + { + errorMessage = noTime + ? i18n("End date is earlier than start date") + : i18n("End date/time is earlier than start date/time"); + return errWidget; + } + } + if (!mRule) + return 0; + return mRule->validate(errorMessage); +} + +/****************************************************************************** + * Called when a recurrence period radio button is clicked. + */ +void RecurrenceEdit::periodClicked(int id) +{ + RepeatType oldType = mRuleButtonType; + bool none = (id == mNoneButtonId); + bool atLogin = (id == mAtLoginButtonId); + bool subdaily = (id == mSubDailyButtonId); + if (none) + { + mRule = 0; + mRuleButtonType = NO_RECUR; + } + else if (atLogin) + { + mRule = 0; + mRuleButtonType = AT_LOGIN; + mRangeButtonGroup->setButton(mRangeButtonGroup->id(mEndDateButton)); + } + else if (subdaily) + { + mRule = mSubDailyRule; + mRuleButtonType = SUBDAILY; + } + else if (id == mDailyButtonId) + { + mRule = mDailyRule; + mRuleButtonType = DAILY; + mDailyShown = true; + } + else if (id == mWeeklyButtonId) + { + mRule = mWeeklyRule; + mRuleButtonType = WEEKLY; + mWeeklyShown = true; + } + else if (id == mMonthlyButtonId) + { + mRule = mMonthlyRule; + mRuleButtonType = MONTHLY; + mMonthlyShown = true; + } + else if (id == mYearlyButtonId) + { + mRule = mYearlyRule; + mRuleButtonType = ANNUAL; + mYearlyShown = true; + } + else + return; + + if (mRuleButtonType != oldType) + { + mRuleStack->raiseWidget(mRule ? mRule : mNoRule); + if (oldType == NO_RECUR || none) + mRangeButtonGroup->setEnabled(!none); + mExceptionGroup->setEnabled(!(none || atLogin)); + mEndAnyTimeCheckBox->setEnabled(atLogin); + if (!none) + { + mNoEndDateButton->setEnabled(!atLogin); + mRepeatCountButton->setEnabled(!atLogin); + } + rangeTypeClicked(); + mSubRepetition->setEnabled(!(none || atLogin)); + if (!mNoEmitTypeChanged) + emit typeChanged(mRuleButtonType); + } +} + +void RecurrenceEdit::slotAnyTimeToggled(bool on) +{ + QButton* button = mRuleButtonGroup->selected(); + mEndTimeEdit->setEnabled(button == mAtLoginButton && !on + || button == mSubDailyButton && mEndDateButton->isChecked()); +} + +/****************************************************************************** + * Called when a recurrence range type radio button is clicked. + */ +void RecurrenceEdit::rangeTypeClicked() +{ + bool endDate = mEndDateButton->isOn(); + mEndDateEdit->setEnabled(endDate); + mEndTimeEdit->setEnabled(endDate + && (mAtLoginButton->isOn() && !mEndAnyTimeCheckBox->isChecked() + || mSubDailyButton->isOn())); + bool repeatCount = mRepeatCountButton->isOn(); + mRepeatCountEntry->setEnabled(repeatCount); + mRepeatCountLabel->setEnabled(repeatCount); +} + +void RecurrenceEdit::showEvent(QShowEvent*) +{ + if (mRule) + mRule->setFrequencyFocus(); + else + mRuleButtonGroup->selected()->setFocus(); + emit shown(); +} + + /****************************************************************************** +* Return the sub-repetition count within the recurrence, i.e. the number of +* repetitions after the main recurrence. +*/ +int RecurrenceEdit::subRepeatCount(int* subRepeatInterval) const +{ + int count = (mRuleButtonType >= SUBDAILY) ? mSubRepetition->count() : 0; + if (subRepeatInterval) + *subRepeatInterval = count ? mSubRepetition->interval() : 0; + return count; +} + +/****************************************************************************** +* Called when the Sub-Repetition button has been pressed to display the +* sub-repetition dialog. +* Alarm repetition has the following restrictions: +* 1) Not allowed for a repeat-at-login alarm +* 2) For a date-only alarm, the repeat interval must be a whole number of days. +* 3) The overall repeat duration must be less than the recurrence interval. +*/ +void RecurrenceEdit::setSubRepetition(int reminderMinutes, bool dateOnly) +{ + int maxDuration; + switch (mRuleButtonType) + { + case RecurrenceEdit::NO_RECUR: + case RecurrenceEdit::AT_LOGIN: // alarm repeat not allowed + maxDuration = 0; + break; + default: // repeat duration must be less than recurrence interval + { + KAEvent event; + updateEvent(event, false); + maxDuration = event.longestRecurrenceInterval() - reminderMinutes - 1; + break; + } + } + mSubRepetition->initialise(mSubRepetition->interval(), mSubRepetition->count(), dateOnly, maxDuration); + mSubRepetition->setEnabled(mRuleButtonType >= SUBDAILY && maxDuration); +} + +/****************************************************************************** +* Activate the sub-repetition dialog. +*/ +void RecurrenceEdit::activateSubRepetition() +{ + mSubRepetition->activate(); +} + +/****************************************************************************** + * Called when the value of the repeat count field changes, to reset the + * minimum value to 1 if the value was 0. + */ +void RecurrenceEdit::repeatCountChanged(int value) +{ + if (value > 0 && mRepeatCountEntry->minValue() == 0) + mRepeatCountEntry->setMinValue(1); +} + +/****************************************************************************** + * Add the date entered in the exception date edit control to the list of + * exception dates. + */ +void RecurrenceEdit::addException() +{ + if (!mExceptionDateEdit || !mExceptionDateEdit->isValid()) + return; + QDate date = mExceptionDateEdit->date(); + QValueList<QDate>::Iterator it; + int index = 0; + bool insert = true; + for (it = mExceptionDates.begin(); it != mExceptionDates.end(); ++index, ++it) + { + if (date <= *it) + { + insert = (date != *it); + break; + } + } + if (insert) + { + mExceptionDates.insert(it, date); + mExceptionDateList->insertItem(KGlobal::locale()->formatDate(date), index); + } + mExceptionDateList->setCurrentItem(index); + enableExceptionButtons(); +} + +/****************************************************************************** + * Change the currently highlighted exception date to that entered in the + * exception date edit control. + */ +void RecurrenceEdit::changeException() +{ + if (!mExceptionDateEdit || !mExceptionDateEdit->isValid()) + return; + int index = mExceptionDateList->currentItem(); + if (index >= 0 && mExceptionDateList->isSelected(index)) + { + QDate olddate = mExceptionDates[index]; + QDate newdate = mExceptionDateEdit->date(); + if (newdate != olddate) + { + mExceptionDates.remove(mExceptionDates.at(index)); + mExceptionDateList->removeItem(index); + addException(); + } + } +} + +/****************************************************************************** + * Delete the currently highlighted exception date. + */ +void RecurrenceEdit::deleteException() +{ + int index = mExceptionDateList->currentItem(); + if (index >= 0 && mExceptionDateList->isSelected(index)) + { + mExceptionDates.remove(mExceptionDates.at(index)); + mExceptionDateList->removeItem(index); + enableExceptionButtons(); + } +} + +/****************************************************************************** + * Enable/disable the exception group buttons according to whether any item is + * selected in the exceptions listbox. + */ +void RecurrenceEdit::enableExceptionButtons() +{ + int index = mExceptionDateList->currentItem(); + bool enable = (index >= 0 && mExceptionDateList->isSelected(index)); + if (mDeleteExceptionButton) + mDeleteExceptionButton->setEnabled(enable); + if (mChangeExceptionButton) + mChangeExceptionButton->setEnabled(enable); + + // Prevent the exceptions list box receiving keyboard focus is it's empty + mExceptionDateList->setFocusPolicy(mExceptionDateList->count() ? QWidget::WheelFocus : QWidget::NoFocus); +} + +/****************************************************************************** + * Notify this instance of a change in the alarm start date. + */ +void RecurrenceEdit::setStartDate(const QDate& start, const QDate& today) +{ + if (!mReadOnly) + { + setRuleDefaults(start); + if (start < today) + { + mEndDateEdit->setMinDate(today); + if (mExceptionDateEdit) + mExceptionDateEdit->setMinDate(today); + } + else + { + const QString startString = i18n("Date cannot be earlier than start date", "start date"); + mEndDateEdit->setMinDate(start, startString); + if (mExceptionDateEdit) + mExceptionDateEdit->setMinDate(start, startString); + } + } +} + +/****************************************************************************** + * Specify the default recurrence end date. + */ +void RecurrenceEdit::setDefaultEndDate(const QDate& end) +{ + if (!mEndDateButton->isOn()) + mEndDateEdit->setDate(end); +} + +void RecurrenceEdit::setEndDateTime(const DateTime& end) +{ + mEndDateEdit->setDate(end.date()); + mEndTimeEdit->setValue(end.time()); + mEndTimeEdit->setEnabled(!end.isDateOnly()); + mEndAnyTimeCheckBox->setChecked(end.isDateOnly()); +} + +DateTime RecurrenceEdit::endDateTime() const +{ + if (mRuleButtonGroup->selected() == mAtLoginButton && mEndAnyTimeCheckBox->isChecked()) + return DateTime(mEndDateEdit->date()); + return DateTime(mEndDateEdit->date(), mEndTimeEdit->time()); +} + +/****************************************************************************** + * Set all controls to their default values. + */ +void RecurrenceEdit::setDefaults(const QDateTime& from) +{ + mCurrStartDateTime = from; + QDate fromDate = from.date(); + mNoEndDateButton->setChecked(true); + + mSubDailyRule->setFrequency(1); + mDailyRule->setFrequency(1); + mWeeklyRule->setFrequency(1); + mMonthlyRule->setFrequency(1); + mYearlyRule->setFrequency(1); + + setRuleDefaults(fromDate); + mMonthlyRule->setType(MonthYearRule::DATE); // date in month + mYearlyRule->setType(MonthYearRule::DATE); // date in year + + mEndDateEdit->setDate(fromDate); + + mNoEmitTypeChanged = true; + int button; + switch (Preferences::defaultRecurPeriod()) + { + case AT_LOGIN: button = mAtLoginButtonId; break; + case ANNUAL: button = mYearlyButtonId; break; + case MONTHLY: button = mMonthlyButtonId; break; + case WEEKLY: button = mWeeklyButtonId; break; + case DAILY: button = mDailyButtonId; break; + case SUBDAILY: button = mSubDailyButtonId; break; + case NO_RECUR: + default: button = mNoneButtonId; break; + } + mRuleButtonGroup->setButton(button); + mNoEmitTypeChanged = false; + rangeTypeClicked(); + enableExceptionButtons(); + + saveState(); +} + +/****************************************************************************** + * Set the controls for weekly, monthly and yearly rules which have not so far + * been shown, to their default values, depending on the recurrence start date. + */ +void RecurrenceEdit::setRuleDefaults(const QDate& fromDate) +{ + int day = fromDate.day(); + int dayOfWeek = fromDate.dayOfWeek(); + int month = fromDate.month(); + if (!mDailyShown) + mDailyRule->setDays(true); + if (!mWeeklyShown) + mWeeklyRule->setDay(dayOfWeek); + if (!mMonthlyShown) + mMonthlyRule->setDefaultValues(day, dayOfWeek); + if (!mYearlyShown) + mYearlyRule->setDefaultValues(day, dayOfWeek, month); +} + +/****************************************************************************** +* Set the state of all controls to reflect the data in the specified event. +* Set 'keepDuration' true to prevent the recurrence count being adjusted to the +* remaining number of recurrences. +*/ +void RecurrenceEdit::set(const KAEvent& event, bool keepDuration) +{ + setDefaults(event.mainDateTime().dateTime()); + if (event.repeatAtLogin()) + { + mRuleButtonGroup->setButton(mAtLoginButtonId); + mEndDateButton->setChecked(true); + return; + } + mRuleButtonGroup->setButton(mNoneButtonId); + KARecurrence* recurrence = event.recurrence(); + if (!recurrence) + return; + KARecurrence::Type rtype = recurrence->type(); + switch (rtype) + { + case KARecurrence::MINUTELY: + mRuleButtonGroup->setButton(mSubDailyButtonId); + break; + + case KARecurrence::DAILY: + { + mRuleButtonGroup->setButton(mDailyButtonId); + QBitArray rDays = recurrence->days(); + bool set = false; + for (int i = 0; i < 7 && !set; ++i) + set = rDays.testBit(i); + if (set) + mDailyRule->setDays(rDays); + else + mDailyRule->setDays(true); + break; + } + case KARecurrence::WEEKLY: + { + mRuleButtonGroup->setButton(mWeeklyButtonId); + QBitArray rDays = recurrence->days(); + mWeeklyRule->setDays(rDays); + break; + } + case KARecurrence::MONTHLY_POS: // on nth (Tuesday) of the month + { + QValueList<RecurrenceRule::WDayPos> posns = recurrence->monthPositions(); + int i = posns.first().pos(); + if (!i) + { + // It's every (Tuesday) of the month. Convert to a weekly recurrence + // (but ignoring any non-every xxxDay positions). + mRuleButtonGroup->setButton(mWeeklyButtonId); + mWeeklyRule->setFrequency(recurrence->frequency()); + QBitArray rDays(7); + for (QValueList<RecurrenceRule::WDayPos>::ConstIterator it = posns.begin(); it != posns.end(); ++it) + { + if (!(*it).pos()) + rDays.setBit((*it).day() - 1, 1); + } + mWeeklyRule->setDays(rDays); + break; + } + mRuleButtonGroup->setButton(mMonthlyButtonId); + mMonthlyRule->setPosition(i, posns.first().day()); + break; + } + case KARecurrence::MONTHLY_DAY: // on nth day of the month + { + mRuleButtonGroup->setButton(mMonthlyButtonId); + QValueList<int> rmd = recurrence->monthDays(); + int day = (rmd.isEmpty()) ? event.mainDate().day() : rmd.first(); + mMonthlyRule->setDate(day); + break; + } + case KARecurrence::ANNUAL_DATE: // on the nth day of (months...) in the year + case KARecurrence::ANNUAL_POS: // on the nth (Tuesday) of (months...) in the year + { + if (rtype == KARecurrence::ANNUAL_DATE) + { + mRuleButtonGroup->setButton(mYearlyButtonId); + const QValueList<int> rmd = recurrence->monthDays(); + int day = (rmd.isEmpty()) ? event.mainDate().day() : rmd.first(); + mYearlyRule->setDate(day); + mYearlyRule->setFeb29Type(recurrence->feb29Type()); + } + else if (rtype == KARecurrence::ANNUAL_POS) + { + mRuleButtonGroup->setButton(mYearlyButtonId); + QValueList<RecurrenceRule::WDayPos> posns = recurrence->yearPositions(); + mYearlyRule->setPosition(posns.first().pos(), posns.first().day()); + } + mYearlyRule->setMonths(recurrence->yearMonths()); + break; + } + default: + return; + } + + mRule->setFrequency(recurrence->frequency()); + + // Get range information + QDateTime endtime = mCurrStartDateTime; + int duration = recurrence->duration(); + if (duration == -1) + mNoEndDateButton->setChecked(true); + else if (duration) + { + mRepeatCountButton->setChecked(true); + mRepeatCountEntry->setValue(duration); + } + else + { + mEndDateButton->setChecked(true); + endtime = recurrence->endDateTime(); + mEndTimeEdit->setValue(endtime.time()); + } + mEndDateEdit->setDate(endtime.date()); + + // Get exception information + mExceptionDates = event.recurrence()->exDates(); + qHeapSort(mExceptionDates); + mExceptionDateList->clear(); + for (DateList::ConstIterator it = mExceptionDates.begin(); it != mExceptionDates.end(); ++it) + mExceptionDateList->insertItem(KGlobal::locale()->formatDate(*it)); + enableExceptionButtons(); + + // Get repetition within recurrence + mSubRepetition->set(event.repeatInterval(), event.repeatCount()); + + rangeTypeClicked(); + + saveState(); +} + +/****************************************************************************** + * Update the specified KAEvent with the entered recurrence data. + * If 'adjustStart' is true, the start date/time will be adjusted if necessary + * to be the first date/time which recurs on or after the original start. + */ +void RecurrenceEdit::updateEvent(KAEvent& event, bool adjustStart) +{ + // Get end date and repeat count, common to all types of recurring events + QDate endDate; + QTime endTime; + int repeatCount; + if (mNoEndDateButton->isChecked()) + repeatCount = -1; + else if (mRepeatCountButton->isChecked()) + repeatCount = mRepeatCountEntry->value(); + else + { + repeatCount = 0; + endDate = mEndDateEdit->date(); + endTime = mEndTimeEdit->time(); + } + + // Set up the recurrence according to the type selected + QButton* button = mRuleButtonGroup->selected(); + event.setRepeatAtLogin(button == mAtLoginButton); + int frequency = mRule ? mRule->frequency() : 0; + if (button == mSubDailyButton) + { + QDateTime endDateTime(endDate, endTime); + event.setRecurMinutely(frequency, repeatCount, endDateTime); + } + else if (button == mDailyButton) + { + event.setRecurDaily(frequency, mDailyRule->days(), repeatCount, endDate); + } + else if (button == mWeeklyButton) + { + event.setRecurWeekly(frequency, mWeeklyRule->days(), repeatCount, endDate); + } + else if (button == mMonthlyButton) + { + if (mMonthlyRule->type() == MonthlyRule::POS) + { + // It's by position + KAEvent::MonthPos pos; + pos.days.fill(false); + pos.days.setBit(mMonthlyRule->dayOfWeek() - 1); + pos.weeknum = mMonthlyRule->week(); + QValueList<KAEvent::MonthPos> poses; + poses.append(pos); + event.setRecurMonthlyByPos(frequency, poses, repeatCount, endDate); + } + else + { + // It's by day + int daynum = mMonthlyRule->date(); + QValueList<int> daynums; + daynums.append(daynum); + event.setRecurMonthlyByDate(frequency, daynums, repeatCount, endDate); + } + } + else if (button == mYearlyButton) + { + QValueList<int> months = mYearlyRule->months(); + if (mYearlyRule->type() == YearlyRule::POS) + { + // It's by position + KAEvent::MonthPos pos; + pos.days.fill(false); + pos.days.setBit(mYearlyRule->dayOfWeek() - 1); + pos.weeknum = mYearlyRule->week(); + QValueList<KAEvent::MonthPos> poses; + poses.append(pos); + event.setRecurAnnualByPos(frequency, poses, months, repeatCount, endDate); + } + else + { + // It's by date in month + event.setRecurAnnualByDate(frequency, months, mYearlyRule->date(), + mYearlyRule->feb29Type(), repeatCount, endDate); + } + } + else + { + event.setNoRecur(); + return; + } + if (!event.recurs()) + return; // an error occurred setting up the recurrence + if (adjustStart) + event.setFirstRecurrence(); + + // Set up repetition within the recurrence. + // N.B. This requires the main recurrence to be set up first. + int count = mSubRepetition->count(); + if (mRuleButtonType < SUBDAILY) + count = 0; + event.setRepetition(mSubRepetition->interval(), count); + + // Set up exceptions + event.recurrence()->setExDates(mExceptionDates); + + event.setUpdated(); +} + +/****************************************************************************** + * Save the state of all controls. + */ +void RecurrenceEdit::saveState() +{ + mSavedRuleButton = mRuleButtonGroup->selected(); + if (mRule) + mRule->saveState(); + mSavedRangeButton = mRangeButtonGroup->selected(); + if (mSavedRangeButton == mRepeatCountButton) + mSavedRecurCount = mRepeatCountEntry->value(); + else if (mSavedRangeButton == mEndDateButton) + mSavedEndDateTime.set(QDateTime(mEndDateEdit->date(), mEndTimeEdit->time()), mEndAnyTimeCheckBox->isChecked()); + mSavedExceptionDates = mExceptionDates; + mSavedRepeatInterval = mSubRepetition->interval(); + mSavedRepeatCount = mSubRepetition->count(); +} + +/****************************************************************************** + * Check whether any of the controls have changed state since initialisation. + */ +bool RecurrenceEdit::stateChanged() const +{ + if (mSavedRuleButton != mRuleButtonGroup->selected() + || mSavedRangeButton != mRangeButtonGroup->selected() + || mRule && mRule->stateChanged()) + return true; + if (mSavedRangeButton == mRepeatCountButton + && mSavedRecurCount != mRepeatCountEntry->value()) + return true; + if (mSavedRangeButton == mEndDateButton + && mSavedEndDateTime != DateTime(QDateTime(mEndDateEdit->date(), mEndTimeEdit->time()), mEndAnyTimeCheckBox->isChecked())) + return true; + if (mSavedExceptionDates != mExceptionDates + || mSavedRepeatInterval != mSubRepetition->interval() + || mSavedRepeatCount != mSubRepetition->count()) + return true; + return false; +} + + + +/*============================================================================= += Class Rule += Base class for rule widgets, including recurrence frequency. +=============================================================================*/ + +Rule::Rule(const QString& freqText, const QString& freqWhatsThis, bool time, bool readOnly, QWidget* parent, const char* name) + : NoRule(parent, name) +{ + mLayout = new QVBoxLayout(this, 0, KDialog::spacingHint()); + QHBox* freqBox = new QHBox(this); + mLayout->addWidget(freqBox); + QHBox* box = new QHBox(freqBox); // this is to control the QWhatsThis text display area + box->setSpacing(KDialog::spacingHint()); + + QLabel* label = new QLabel(i18n("Recur e&very"), box); + label->setFixedSize(label->sizeHint()); + if (time) + { + mIntSpinBox = 0; + mSpinBox = mTimeSpinBox = new TimeSpinBox(1, 5999, box); + mTimeSpinBox->setFixedSize(mTimeSpinBox->sizeHint()); + mTimeSpinBox->setReadOnly(readOnly); + } + else + { + mTimeSpinBox = 0; + mSpinBox = mIntSpinBox = new SpinBox(1, 999, 1, box); + mIntSpinBox->setFixedSize(mIntSpinBox->sizeHint()); + mIntSpinBox->setReadOnly(readOnly); + } + connect(mSpinBox, SIGNAL(valueChanged(int)), SIGNAL(frequencyChanged())); + label->setBuddy(mSpinBox); + label = new QLabel(freqText, box); + label->setFixedSize(label->sizeHint()); + box->setFixedSize(sizeHint()); + QWhatsThis::add(box, freqWhatsThis); + + new QWidget(freqBox); // left adjust the visible widgets + freqBox->setFixedHeight(freqBox->sizeHint().height()); + freqBox->setFocusProxy(mSpinBox); +} + +int Rule::frequency() const +{ + if (mIntSpinBox) + return mIntSpinBox->value(); + if (mTimeSpinBox) + return mTimeSpinBox->value(); + return 0; +} + +void Rule::setFrequency(int n) +{ + if (mIntSpinBox) + mIntSpinBox->setValue(n); + if (mTimeSpinBox) + mTimeSpinBox->setValue(n); +} + +/****************************************************************************** + * Save the state of all controls. + */ +void Rule::saveState() +{ + mSavedFrequency = frequency(); +} + +/****************************************************************************** + * Check whether any of the controls have changed state since initialisation. + */ +bool Rule::stateChanged() const +{ + return (mSavedFrequency != frequency()); +} + + +/*============================================================================= += Class SubDailyRule += Sub-daily rule widget. +=============================================================================*/ + +SubDailyRule::SubDailyRule(bool readOnly, QWidget* parent, const char* name) + : Rule(i18n("hours:minutes"), + i18n("Enter the number of hours and minutes between repetitions of the alarm"), + true, readOnly, parent, name) +{ } + + +/*============================================================================= += Class DayWeekRule += Daily/weekly rule widget base class. +=============================================================================*/ + +DayWeekRule::DayWeekRule(const QString& freqText, const QString& freqWhatsThis, const QString& daysWhatsThis, + bool readOnly, QWidget* parent, const char* name) + : Rule(freqText, freqWhatsThis, false, readOnly, parent, name), + mSavedDays(7) +{ + QGridLayout* grid = new QGridLayout(layout(), 1, 4, KDialog::spacingHint()); + grid->setRowStretch(0, 1); + + QLabel* label = new QLabel(i18n("On: Tuesday", "O&n:"), this); + label->setFixedSize(label->sizeHint()); + grid->addWidget(label, 0, 0, Qt::AlignRight | Qt::AlignTop); + grid->addColSpacing(1, KDialog::spacingHint()); + + // List the days of the week starting at the user's start day of the week. + // Save the first day of the week, just in case it changes while the dialog is open. + QWidget* box = new QWidget(this); // this is to control the QWhatsThis text display area + QGridLayout* dgrid = new QGridLayout(box, 4, 2, 0, KDialog::spacingHint()); + const KCalendarSystem* calendar = KGlobal::locale()->calendar(); + for (int i = 0; i < 7; ++i) + { + int day = KAlarm::localeDayInWeek_to_weekDay(i); + mDayBox[i] = new CheckBox(calendar->weekDayName(day), box); + mDayBox[i]->setFixedSize(mDayBox[i]->sizeHint()); + mDayBox[i]->setReadOnly(readOnly); + dgrid->addWidget(mDayBox[i], i%4, i/4, Qt::AlignAuto); + } + box->setFixedSize(box->sizeHint()); + QWhatsThis::add(box, daysWhatsThis); + grid->addWidget(box, 0, 2, Qt::AlignAuto); + label->setBuddy(mDayBox[0]); + grid->setColStretch(3, 1); +} + +/****************************************************************************** + * Fetch which days of the week have been ticked. + */ +QBitArray DayWeekRule::days() const +{ + QBitArray ds(7); + ds.fill(false); + for (int i = 0; i < 7; ++i) + if (mDayBox[i]->isChecked()) + ds.setBit(KAlarm::localeDayInWeek_to_weekDay(i) - 1, 1); + return ds; +} + +/****************************************************************************** + * Tick/untick every day of the week. + */ +void DayWeekRule::setDays(bool tick) +{ + for (int i = 0; i < 7; ++i) + mDayBox[i]->setChecked(tick); +} + +/****************************************************************************** + * Tick/untick each day of the week according to the specified bits. + */ +void DayWeekRule::setDays(const QBitArray& days) +{ + for (int i = 0; i < 7; ++i) + { + bool x = days.testBit(KAlarm::localeDayInWeek_to_weekDay(i) - 1); + mDayBox[i]->setChecked(x); + } +} + +/****************************************************************************** + * Tick the specified day of the week, and untick all other days. + */ +void DayWeekRule::setDay(int dayOfWeek) +{ + for (int i = 0; i < 7; ++i) + mDayBox[i]->setChecked(false); + if (dayOfWeek > 0 && dayOfWeek <= 7) + mDayBox[KAlarm::weekDay_to_localeDayInWeek(dayOfWeek)]->setChecked(true); +} + +/****************************************************************************** + * Validate: check that at least one day is selected. + */ +QWidget* DayWeekRule::validate(QString& errorMessage) +{ + for (int i = 0; i < 7; ++i) + if (mDayBox[i]->isChecked()) + return 0; + errorMessage = i18n("No day selected"); + return mDayBox[0]; +} + +/****************************************************************************** + * Save the state of all controls. + */ +void DayWeekRule::saveState() +{ + Rule::saveState(); + mSavedDays = days(); +} + +/****************************************************************************** + * Check whether any of the controls have changed state since initialisation. + */ +bool DayWeekRule::stateChanged() const +{ + return (Rule::stateChanged() + || mSavedDays != days()); +} + + +/*============================================================================= += Class DailyRule += Daily rule widget. +=============================================================================*/ + +DailyRule::DailyRule(bool readOnly, QWidget* parent, const char* name) + : DayWeekRule(i18n("day(s)"), + i18n("Enter the number of days between repetitions of the alarm"), + i18n("Select the days of the week on which the alarm is allowed to occur"), + readOnly, parent, name) +{ } + + +/*============================================================================= += Class WeeklyRule += Weekly rule widget. +=============================================================================*/ + +WeeklyRule::WeeklyRule(bool readOnly, QWidget* parent, const char* name) + : DayWeekRule(i18n("week(s)"), + i18n("Enter the number of weeks between repetitions of the alarm"), + i18n("Select the days of the week on which to repeat the alarm"), + readOnly, parent, name) +{ } + + +/*============================================================================= += Class MonthYearRule += Monthly/yearly rule widget base class. +=============================================================================*/ + +MonthYearRule::MonthYearRule(const QString& freqText, const QString& freqWhatsThis, bool allowEveryWeek, + bool readOnly, QWidget* parent, const char* name) + : Rule(freqText, freqWhatsThis, false, readOnly, parent, name), + mEveryWeek(allowEveryWeek) +{ + mButtonGroup = new ButtonGroup(this); + mButtonGroup->hide(); + + // Month day selector + QHBox* box = new QHBox(this); + box->setSpacing(KDialog::spacingHint()); + layout()->addWidget(box); + + mDayButton = new RadioButton(i18n("On day number in the month", "O&n day"), box); + mDayButton->setFixedSize(mDayButton->sizeHint()); + mDayButton->setReadOnly(readOnly); + mDayButtonId = mButtonGroup->insert(mDayButton); + QWhatsThis::add(mDayButton, i18n("Repeat the alarm on the selected day of the month")); + + mDayCombo = new ComboBox(false, box); + mDayCombo->setSizeLimit(11); + for (int i = 0; i < 31; ++i) + mDayCombo->insertItem(QString::number(i + 1)); + mDayCombo->insertItem(i18n("Last day of month", "Last")); + mDayCombo->setFixedSize(mDayCombo->sizeHint()); + mDayCombo->setReadOnly(readOnly); + QWhatsThis::add(mDayCombo, i18n("Select the day of the month on which to repeat the alarm")); + mDayButton->setFocusWidget(mDayCombo); + connect(mDayCombo, SIGNAL(activated(int)), SLOT(slotDaySelected(int))); + + box->setStretchFactor(new QWidget(box), 1); // left adjust the controls + box->setFixedHeight(box->sizeHint().height()); + + // Month position selector + box = new QHBox(this); + box->setSpacing(KDialog::spacingHint()); + layout()->addWidget(box); + + mPosButton = new RadioButton(i18n("On the 1st Tuesday", "On t&he"), box); + mPosButton->setFixedSize(mPosButton->sizeHint()); + mPosButton->setReadOnly(readOnly); + mPosButtonId = mButtonGroup->insert(mPosButton); + QWhatsThis::add(mPosButton, + i18n("Repeat the alarm on one day of the week, in the selected week of the month")); + + mWeekCombo = new ComboBox(false, box); + mWeekCombo->insertItem(i18n("1st")); + mWeekCombo->insertItem(i18n("2nd")); + mWeekCombo->insertItem(i18n("3rd")); + mWeekCombo->insertItem(i18n("4th")); + mWeekCombo->insertItem(i18n("5th")); + mWeekCombo->insertItem(i18n("Last Monday in March", "Last")); + mWeekCombo->insertItem(i18n("2nd Last")); + mWeekCombo->insertItem(i18n("3rd Last")); + mWeekCombo->insertItem(i18n("4th Last")); + mWeekCombo->insertItem(i18n("5th Last")); + if (mEveryWeek) + { + mWeekCombo->insertItem(i18n("Every (Monday...) in month", "Every")); + mWeekCombo->setSizeLimit(11); + } + QWhatsThis::add(mWeekCombo, i18n("Select the week of the month in which to repeat the alarm")); + mWeekCombo->setFixedSize(mWeekCombo->sizeHint()); + mWeekCombo->setReadOnly(readOnly); + mPosButton->setFocusWidget(mWeekCombo); + + mDayOfWeekCombo = new ComboBox(false, box); + const KCalendarSystem* calendar = KGlobal::locale()->calendar(); + for (int i = 0; i < 7; ++i) + { + int day = KAlarm::localeDayInWeek_to_weekDay(i); + mDayOfWeekCombo->insertItem(calendar->weekDayName(day)); + } + mDayOfWeekCombo->setReadOnly(readOnly); + QWhatsThis::add(mDayOfWeekCombo, i18n("Select the day of the week on which to repeat the alarm")); + + box->setStretchFactor(new QWidget(box), 1); // left adjust the controls + box->setFixedHeight(box->sizeHint().height()); + connect(mButtonGroup, SIGNAL(buttonSet(int)), SLOT(clicked(int))); +} + +MonthYearRule::DayPosType MonthYearRule::type() const +{ + return (mButtonGroup->selectedId() == mDayButtonId) ? DATE : POS; +} + +void MonthYearRule::setType(MonthYearRule::DayPosType type) +{ + mButtonGroup->setButton(type == DATE ? mDayButtonId : mPosButtonId); +} + +void MonthYearRule::setDefaultValues(int dayOfMonth, int dayOfWeek) +{ + --dayOfMonth; + mDayCombo->setCurrentItem(dayOfMonth); + mWeekCombo->setCurrentItem(dayOfMonth / 7); + mDayOfWeekCombo->setCurrentItem(KAlarm::weekDay_to_localeDayInWeek(dayOfWeek)); +} + +int MonthYearRule::date() const +{ + int daynum = mDayCombo->currentItem() + 1; + return (daynum <= 31) ? daynum : 31 - daynum; +} + +int MonthYearRule::week() const +{ + int weeknum = mWeekCombo->currentItem() + 1; + return (weeknum <= 5) ? weeknum : (weeknum == 11) ? 0 : 5 - weeknum; +} + +int MonthYearRule::dayOfWeek() const +{ + return KAlarm::localeDayInWeek_to_weekDay(mDayOfWeekCombo->currentItem()); +} + +void MonthYearRule::setDate(int dayOfMonth) +{ + mButtonGroup->setButton(mDayButtonId); + mDayCombo->setCurrentItem(dayOfMonth > 0 ? dayOfMonth - 1 : dayOfMonth < 0 ? 30 - dayOfMonth : 0); // day 0 shouldn't ever occur +} + +void MonthYearRule::setPosition(int week, int dayOfWeek) +{ + mButtonGroup->setButton(mPosButtonId); + mWeekCombo->setCurrentItem((week > 0) ? week - 1 : (week < 0) ? 4 - week : mEveryWeek ? 10 : 0); + mDayOfWeekCombo->setCurrentItem(KAlarm::weekDay_to_localeDayInWeek(dayOfWeek)); +} + +void MonthYearRule::enableSelection(DayPosType type) +{ + bool date = (type == DATE); + mDayCombo->setEnabled(date); + mWeekCombo->setEnabled(!date); + mDayOfWeekCombo->setEnabled(!date); +} + +void MonthYearRule::clicked(int id) +{ + enableSelection(id == mDayButtonId ? DATE : POS); +} + +void MonthYearRule::slotDaySelected(int index) +{ + daySelected(index <= 30 ? index + 1 : 30 - index); +} + +/****************************************************************************** + * Save the state of all controls. + */ +void MonthYearRule::saveState() +{ + Rule::saveState(); + mSavedType = type(); + if (mSavedType == DATE) + mSavedDay = date(); + else + { + mSavedWeek = week(); + mSavedWeekDay = dayOfWeek(); + } +} + +/****************************************************************************** + * Check whether any of the controls have changed state since initialisation. + */ +bool MonthYearRule::stateChanged() const +{ + if (Rule::stateChanged() + || mSavedType != type()) + return true; + if (mSavedType == DATE) + { + if (mSavedDay != date()) + return true; + } + else + { + if (mSavedWeek != week() + || mSavedWeekDay != dayOfWeek()) + return true; + } + return false; +} + + +/*============================================================================= += Class MonthlyRule += Monthly rule widget. +=============================================================================*/ + +MonthlyRule::MonthlyRule(bool readOnly, QWidget* parent, const char* name) + : MonthYearRule(i18n("month(s)"), + i18n("Enter the number of months between repetitions of the alarm"), + false, readOnly, parent, name) +{ } + + +/*============================================================================= += Class YearlyRule += Yearly rule widget. +=============================================================================*/ + +YearlyRule::YearlyRule(bool readOnly, QWidget* parent, const char* name) + : MonthYearRule(i18n("year(s)"), + i18n("Enter the number of years between repetitions of the alarm"), + true, readOnly, parent, name) +{ + // Set up the month selection widgets + QBoxLayout* hlayout = new QHBoxLayout(layout(), KDialog::spacingHint()); + QLabel* label = new QLabel(i18n("List of months to select", "Months:"), this); + label->setFixedSize(label->sizeHint()); + hlayout->addWidget(label, 0, Qt::AlignAuto | Qt::AlignTop); + + // List the months of the year. + QWidget* w = new QWidget(this); // this is to control the QWhatsThis text display area + hlayout->addWidget(w, 1, Qt::AlignAuto); + QGridLayout* grid = new QGridLayout(w, 4, 3, 0, KDialog::spacingHint()); + const KCalendarSystem* calendar = KGlobal::locale()->calendar(); + int year = QDate::currentDate().year(); + for (int i = 0; i < 12; ++i) + { + mMonthBox[i] = new CheckBox(calendar->monthName(i + 1, year, true), w); + mMonthBox[i]->setFixedSize(mMonthBox[i]->sizeHint()); + mMonthBox[i]->setReadOnly(readOnly); + grid->addWidget(mMonthBox[i], i%3, i/3, Qt::AlignAuto); + } + connect(mMonthBox[1], SIGNAL(toggled(bool)), SLOT(enableFeb29())); + w->setFixedHeight(w->sizeHint().height()); + QWhatsThis::add(w, i18n("Select the months of the year in which to repeat the alarm")); + + // February 29th handling option + QHBox* f29box = new QHBox(this); + layout()->addWidget(f29box); + QHBox* box = new QHBox(f29box); // this is to control the QWhatsThis text display area + box->setSpacing(KDialog::spacingHint()); + mFeb29Label = new QLabel(i18n("February 2&9th alarm in non-leap years:"), box); + mFeb29Label->setFixedSize(mFeb29Label->sizeHint()); + mFeb29Combo = new ComboBox(false, box); + mFeb29Combo->insertItem(i18n("No date", "None")); + mFeb29Combo->insertItem(i18n("1st March (short form)", "1 Mar")); + mFeb29Combo->insertItem(i18n("28th February (short form)", "28 Feb")); + mFeb29Combo->setFixedSize(mFeb29Combo->sizeHint()); + mFeb29Combo->setReadOnly(readOnly); + mFeb29Label->setBuddy(mFeb29Combo); + box->setFixedSize(box->sizeHint()); + QWhatsThis::add(box, + i18n("Select which date, if any, the February 29th alarm should trigger in non-leap years")); + new QWidget(f29box); // left adjust the visible widgets + f29box->setFixedHeight(f29box->sizeHint().height()); +} + +void YearlyRule::setDefaultValues(int dayOfMonth, int dayOfWeek, int month) +{ + MonthYearRule::setDefaultValues(dayOfMonth, dayOfWeek); + --month; + for (int i = 0; i < 12; ++i) + mMonthBox[i]->setChecked(i == month); + setFeb29Type(Preferences::defaultFeb29Type()); + daySelected(dayOfMonth); // enable/disable month checkboxes as appropriate +} + +/****************************************************************************** + * Fetch which months have been checked (1 - 12). + * Reply = true if February has been checked. + */ +QValueList<int> YearlyRule::months() const +{ + QValueList<int> mnths; + for (int i = 0; i < 12; ++i) + if (mMonthBox[i]->isChecked() && mMonthBox[i]->isEnabled()) + mnths.append(i + 1); + return mnths; +} + +/****************************************************************************** + * Check/uncheck each month of the year according to the specified list. + */ +void YearlyRule::setMonths(const QValueList<int>& mnths) +{ + bool checked[12]; + for (int i = 0; i < 12; ++i) + checked[i] = false; + for (QValueListConstIterator<int> it = mnths.begin(); it != mnths.end(); ++it) + checked[(*it) - 1] = true; + for (int i = 0; i < 12; ++i) + mMonthBox[i]->setChecked(checked[i]); + enableFeb29(); +} + +/****************************************************************************** + * Return the date for February 29th alarms in non-leap years. + */ +KARecurrence::Feb29Type YearlyRule::feb29Type() const +{ + if (mFeb29Combo->isEnabled()) + { + switch (mFeb29Combo->currentItem()) + { + case 1: return KARecurrence::FEB29_MAR1; + case 2: return KARecurrence::FEB29_FEB28; + default: break; + } + } + return KARecurrence::FEB29_FEB29; +} + +/****************************************************************************** + * Set the date for February 29th alarms to trigger in non-leap years. + */ +void YearlyRule::setFeb29Type(KARecurrence::Feb29Type type) +{ + int index; + switch (type) + { + default: + case KARecurrence::FEB29_FEB29: index = 0; break; + case KARecurrence::FEB29_MAR1: index = 1; break; + case KARecurrence::FEB29_FEB28: index = 2; break; + } + mFeb29Combo->setCurrentItem(index); +} + +/****************************************************************************** + * Validate: check that at least one month is selected. + */ +QWidget* YearlyRule::validate(QString& errorMessage) +{ + for (int i = 0; i < 12; ++i) + if (mMonthBox[i]->isChecked() && mMonthBox[i]->isEnabled()) + return 0; + errorMessage = i18n("No month selected"); + return mMonthBox[0]; +} + +/****************************************************************************** + * Called when a yearly recurrence type radio button is clicked, + * to enable/disable month checkboxes as appropriate for the date selected. + */ +void YearlyRule::clicked(int id) +{ + MonthYearRule::clicked(id); + daySelected(buttonType(id) == DATE ? date() : 1); +} + +/****************************************************************************** + * Called when a day of the month is selected in a yearly recurrence, to + * disable months for which the day is out of range. + */ +void YearlyRule::daySelected(int day) +{ + mMonthBox[1]->setEnabled(day <= 29); // February + bool enable = (day != 31); + mMonthBox[3]->setEnabled(enable); // April + mMonthBox[5]->setEnabled(enable); // June + mMonthBox[8]->setEnabled(enable); // September + mMonthBox[10]->setEnabled(enable); // November + enableFeb29(); +} + +/****************************************************************************** + * Enable/disable the February 29th combo box depending on whether February + * 29th is selected. + */ +void YearlyRule::enableFeb29() +{ + bool enable = (type() == DATE && date() == 29 && mMonthBox[1]->isChecked() && mMonthBox[1]->isEnabled()); + mFeb29Label->setEnabled(enable); + mFeb29Combo->setEnabled(enable); +} + +/****************************************************************************** + * Save the state of all controls. + */ +void YearlyRule::saveState() +{ + MonthYearRule::saveState(); + mSavedMonths = months(); + mSavedFeb29Type = feb29Type(); +} + +/****************************************************************************** + * Check whether any of the controls have changed state since initialisation. + */ +bool YearlyRule::stateChanged() const +{ + return (MonthYearRule::stateChanged() + || mSavedMonths != months() + || mSavedFeb29Type != feb29Type()); +} diff --git a/kalarm/recurrenceedit.h b/kalarm/recurrenceedit.h new file mode 100644 index 000000000..f366978db --- /dev/null +++ b/kalarm/recurrenceedit.h @@ -0,0 +1,191 @@ +/* + * recurrenceedit.h - widget to edit the event's recurrence definition + * Program: kalarm + * Copyright © 2002-2005,2008 by David Jarvie <djarvie@kde.org> + * + * Based originally on KOrganizer module koeditorrecurrence.h, + * Copyright (c) 2000,2001 Cornelius Schumacher <schumacher@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef RECURRENCEEDIT_H +#define RECURRENCEEDIT_H + +#include <qframe.h> +#include <qdatetime.h> +#include <qvaluelist.h> + +#include "datetime.h" +class QWidgetStack; +class QGroupBox; +class QLabel; +class QListBox; +class QButton; +class QPushButton; +class QBoxLayout; +class SpinBox; +class CheckBox; +class RadioButton; +class DateEdit; +class TimeEdit; +class ButtonGroup; +class RepetitionButton; +class KAEvent; +class Rule; +class NoRule; +class SubDailyRule; +class DailyRule; +class WeeklyRule; +class MonthlyRule; +class YearlyRule; + + +class RecurrenceEdit : public QFrame +{ + Q_OBJECT + public: + // Don't alter the order of these recurrence types + enum RepeatType { INVALID_RECUR = -1, NO_RECUR, AT_LOGIN, SUBDAILY, DAILY, WEEKLY, MONTHLY, ANNUAL }; + + RecurrenceEdit(bool readOnly, QWidget* parent, const char* name = 0); + virtual ~RecurrenceEdit() { } + + /** Set widgets to default values */ + void setDefaults(const QDateTime& from); + /** Initialise according to a specified event */ + void set(const KAEvent&, bool keepDuration); + /** Write recurrence settings into an event */ + void updateEvent(KAEvent&, bool adjustStart); + QWidget* checkData(const QDateTime& startDateTime, QString& errorMessage) const; + RepeatType repeatType() const { return mRuleButtonType; } + bool isTimedRepeatType() const { return mRuleButtonType >= SUBDAILY; } + int subRepeatCount(int* subRepeatInterval = 0) const; + void setSubRepetition(int reminderMinutes, bool dateOnly); + void setStartDate(const QDate&, const QDate& today); + void setDefaultEndDate(const QDate&); + void setEndDateTime(const DateTime&); + DateTime endDateTime() const; + bool stateChanged() const; + void activateSubRepetition(); + + static QString i18n_Norecur(); // text of 'No recurrence' selection, lower case + static QString i18n_NoRecur(); // text of 'No Recurrence' selection, initial capitals + static QString i18n_AtLogin(); // text of 'At Login' selection + static QString i18n_l_Atlogin(); // text of 'At &login' selection, with 'L' shortcut + static QString i18n_HourlyMinutely(); // text of 'Hourly/Minutely' + static QString i18n_u_HourlyMinutely(); // text of 'Ho&urly/Minutely' selection, with 'U' shortcut + static QString i18n_Daily(); // text of 'Daily' selection + static QString i18n_d_Daily(); // text of '&Daily' selection, with 'D' shortcut + static QString i18n_Weekly(); // text of 'Weekly' selection + static QString i18n_w_Weekly(); // text of '&Weekly' selection, with 'W' shortcut + static QString i18n_Monthly(); // text of 'Monthly' selection + static QString i18n_m_Monthly(); // text of '&Monthly' selection, with 'M' shortcut + static QString i18n_Yearly(); // text of 'Yearly' selection + static QString i18n_y_Yearly(); // text of '&Yearly' selection, with 'Y' shortcut + + public slots: + void setDateTime(const QDateTime& start) { mCurrStartDateTime = start; } + + signals: + void shown(); + void typeChanged(int recurType); // returns a RepeatType value + void frequencyChanged(); + void repeatNeedsInitialisation(); + + protected: + virtual void showEvent(QShowEvent*); + + private slots: + void periodClicked(int); + void rangeTypeClicked(); + void repeatCountChanged(int value); + void slotAnyTimeToggled(bool); + void addException(); + void changeException(); + void deleteException(); + void enableExceptionButtons(); + + private: + void setRuleDefaults(const QDate& start); + void saveState(); + + // Main rule box and choices + QWidgetStack* mRuleStack; + Rule* mRule; // current rule widget, or 0 if NoRule + NoRule* mNoRule; + SubDailyRule* mSubDailyRule; + DailyRule* mDailyRule; + WeeklyRule* mWeeklyRule; + MonthlyRule* mMonthlyRule; + YearlyRule* mYearlyRule; + + ButtonGroup* mRuleButtonGroup; + RadioButton* mNoneButton; + RadioButton* mAtLoginButton; + RadioButton* mSubDailyButton; + RadioButton* mDailyButton; + RadioButton* mWeeklyButton; + RadioButton* mMonthlyButton; + RadioButton* mYearlyButton; + int mNoneButtonId; + int mAtLoginButtonId; + int mSubDailyButtonId; + int mDailyButtonId; + int mWeeklyButtonId; + int mMonthlyButtonId; + int mYearlyButtonId; + RepeatType mRuleButtonType; + bool mDailyShown; // daily rule has been displayed at some time or other + bool mWeeklyShown; // weekly rule has been displayed at some time or other + bool mMonthlyShown; // monthly rule has been displayed at some time or other + bool mYearlyShown; // yearly rule has been displayed at some time or other + + // Range + ButtonGroup* mRangeButtonGroup; + RadioButton* mNoEndDateButton; + RadioButton* mRepeatCountButton; + SpinBox* mRepeatCountEntry; + QLabel* mRepeatCountLabel; + RadioButton* mEndDateButton; + DateEdit* mEndDateEdit; + TimeEdit* mEndTimeEdit; + CheckBox* mEndAnyTimeCheckBox; + + // Exceptions + QGroupBox* mExceptionGroup; + QListBox* mExceptionDateList; + DateEdit* mExceptionDateEdit; + QPushButton* mChangeExceptionButton; + QPushButton* mDeleteExceptionButton; + QValueList<QDate> mExceptionDates; + + // Current start date and time + QDateTime mCurrStartDateTime; + RepetitionButton* mSubRepetition; + bool mNoEmitTypeChanged; // suppress typeChanged() signal + bool mReadOnly; + + // Initial state of non-rule controls + QButton* mSavedRuleButton; // which rule button was selected + QButton* mSavedRangeButton; // which range button was selected + int mSavedRecurCount; // recurrence repeat count + DateTime mSavedEndDateTime; // end date/time + QValueList<QDate> mSavedExceptionDates; // exception dates + int mSavedRepeatInterval; // sub-repetition interval (via mSubRepetition button) + int mSavedRepeatCount; // sub-repetition count (via mSubRepetition button) +}; + +#endif // RECURRENCEEDIT_H diff --git a/kalarm/recurrenceeditprivate.h b/kalarm/recurrenceeditprivate.h new file mode 100644 index 000000000..1210f95c6 --- /dev/null +++ b/kalarm/recurrenceeditprivate.h @@ -0,0 +1,196 @@ +/* + * recurrenceeditprivate.h - private classes for recurrenceedit.cpp + * Program: kalarm + * Copyright © 2003,2005,2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef RECURRENCEEDITPRIVATE_H +#define RECURRENCEEDITPRIVATE_H + +#include <qframe.h> +#include <qvaluelist.h> +#include <qbitarray.h> + +class QWidget; +class QVBoxLayout; +class ButtonGroup; +class RadioButton; +class ComboBox; +class CheckBox; +class SpinBox; +class TimeSpinBox; +class QString; + + +class NoRule : public QFrame +{ + public: + NoRule(QWidget* parent, const char* name = 0) : QFrame(parent, name) + { setFrameStyle(QFrame::NoFrame); } + virtual int frequency() const { return 0; } +}; + +class Rule : public NoRule +{ + Q_OBJECT + public: + Rule(const QString& freqText, const QString& freqWhatsThis, bool time, bool readOnly, + QWidget* parent, const char* name = 0); + int frequency() const; + void setFrequency(int); + virtual void setFrequencyFocus() { mSpinBox->setFocus(); } + QVBoxLayout* layout() const { return mLayout; } + virtual QWidget* validate(QString&) { return 0; } + virtual void saveState(); + virtual bool stateChanged() const; + signals: + void frequencyChanged(); + private: + QWidget* mSpinBox; + SpinBox* mIntSpinBox; + TimeSpinBox* mTimeSpinBox; + QVBoxLayout* mLayout; + // Saved state of all controls + int mSavedFrequency; // frequency for the selected rule +}; + +// Subdaily rule choices +class SubDailyRule : public Rule +{ + Q_OBJECT + public: + SubDailyRule(bool readOnly, QWidget* parent, const char* name = 0); +}; + +// Daily/weekly rule choices base class +class DayWeekRule : public Rule +{ + Q_OBJECT + public: + DayWeekRule(const QString& freqText, const QString& freqWhatsThis, const QString& daysWhatsThis, + bool readOnly, QWidget* parent, const char* name = 0); + QBitArray days() const; + void setDays(bool); + void setDays(const QBitArray& days); + void setDay(int dayOfWeek); + virtual QWidget* validate(QString& errorMessage); + virtual void saveState(); + virtual bool stateChanged() const; + private: + CheckBox* mDayBox[7]; + // Saved state of all controls + QBitArray mSavedDays; // ticked days for weekly rule +}; + +// Daily rule choices +class DailyRule : public DayWeekRule +{ + public: + DailyRule(bool readOnly, QWidget* parent, const char* name = 0); +}; + +// Weekly rule choices +class WeeklyRule : public DayWeekRule +{ + public: + WeeklyRule(bool readOnly, QWidget* parent, const char* name = 0); +}; + +// Monthly/yearly rule choices base class +class MonthYearRule : public Rule +{ + Q_OBJECT + public: + enum DayPosType { DATE, POS }; + + MonthYearRule(const QString& freqText, const QString& freqWhatsThis, bool allowEveryWeek, + bool readOnly, QWidget* parent, const char* name = 0); + DayPosType type() const; + int date() const; // if date in month is selected + int week() const; // if position is selected + int dayOfWeek() const; // if position is selected + void setType(DayPosType); + void setDate(int dayOfMonth); + void setPosition(int week, int dayOfWeek); + void setDefaultValues(int dayOfMonth, int dayOfWeek); + virtual void saveState(); + virtual bool stateChanged() const; + signals: + void typeChanged(DayPosType); + protected: + DayPosType buttonType(int id) const { return id == mDayButtonId ? DATE : POS; } + virtual void daySelected(int /*day*/) { } + protected slots: + virtual void clicked(int id); + private slots: + virtual void slotDaySelected(int index); + private: + void enableSelection(DayPosType); + + ButtonGroup* mButtonGroup; + RadioButton* mDayButton; + RadioButton* mPosButton; + ComboBox* mDayCombo; + ComboBox* mWeekCombo; + ComboBox* mDayOfWeekCombo; + int mDayButtonId; + int mPosButtonId; + bool mEveryWeek; // "Every" week is allowed + // Saved state of all controls + int mSavedType; // whether day-of-month or month position radio button was selected + int mSavedDay; // chosen day of month selected item + int mSavedWeek; // chosen month position: selected week item + int mSavedWeekDay; // chosen month position: selected day of week +}; + +// Monthly rule choices +class MonthlyRule : public MonthYearRule +{ + public: + MonthlyRule(bool readOnly, QWidget* parent, const char* name = 0); +}; + +// Yearly rule choices +class YearlyRule : public MonthYearRule +{ + Q_OBJECT + public: + YearlyRule(bool readOnly, QWidget* parent, const char* name = 0); + QValueList<int> months() const; + void setMonths(const QValueList<int>& months); + void setDefaultValues(int dayOfMonth, int dayOfWeek, int month); + KARecurrence::Feb29Type feb29Type() const; + void setFeb29Type(KARecurrence::Feb29Type); + virtual QWidget* validate(QString& errorMessage); + virtual void saveState(); + virtual bool stateChanged() const; + protected: + virtual void daySelected(int day); + protected slots: + virtual void clicked(int id); + private slots: + void enableFeb29(); + private: + CheckBox* mMonthBox[12]; + QLabel* mFeb29Label; + ComboBox* mFeb29Combo; + // Saved state of all controls + QValueList<int> mSavedMonths; // ticked months for yearly rule + int mSavedFeb29Type; // February 29th recurrence type +}; + +#endif // RECURRENCEEDITPRIVATE_H diff --git a/kalarm/reminder.cpp b/kalarm/reminder.cpp new file mode 100644 index 000000000..f31a2d0dd --- /dev/null +++ b/kalarm/reminder.cpp @@ -0,0 +1,159 @@ +/* + * reminder.cpp - reminder setting widget + * Program: kalarm + * Copyright (C) 2003 - 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qlayout.h> +#include <qwhatsthis.h> + +#include <kglobal.h> +#include <klocale.h> +#include <kdialog.h> +#include <kdebug.h> + +#include "preferences.h" +#include "checkbox.h" +#include "timeselector.h" +#include "reminder.moc" + + +// Collect these widget labels together to ensure consistent wording and +// translations across different modules. +QString Reminder::i18n_first_recurrence_only() { return i18n("Reminder for first recurrence only"); } +QString Reminder::i18n_u_first_recurrence_only() { return i18n("Reminder for first rec&urrence only"); } + + +Reminder::Reminder(const QString& caption, const QString& reminderWhatsThis, const QString& valueWhatsThis, + bool allowHourMinute, bool showOnceOnly, QWidget* parent, const char* name) + : QFrame(parent, name), + mReadOnly(false), + mOnceOnlyEnabled(showOnceOnly) +{ + setFrameStyle(QFrame::NoFrame); + QVBoxLayout* topLayout = new QVBoxLayout(this, 0, KDialog::spacingHint()); + + mTime = new TimeSelector(caption, i18n("in advance"), reminderWhatsThis, + valueWhatsThis, allowHourMinute, this, "timeOption"); + mTime->setFixedSize(mTime->sizeHint()); + connect(mTime, SIGNAL(toggled(bool)), SLOT(slotReminderToggled(bool))); + topLayout->addWidget(mTime); + + if (showOnceOnly) + { + QBoxLayout* layout = new QHBoxLayout(topLayout, KDialog::spacingHint()); + layout->addSpacing(3*KDialog::spacingHint()); + mOnceOnly = new CheckBox(i18n_u_first_recurrence_only(), this); + mOnceOnly->setFixedSize(mOnceOnly->sizeHint()); + QWhatsThis::add(mOnceOnly, i18n("Display the reminder only before the first time the alarm is scheduled")); + layout->addWidget(mOnceOnly); + layout->addStretch(); + } + else + mOnceOnly = 0; +} + +/****************************************************************************** +* Set the read-only status. +*/ +void Reminder::setReadOnly(bool ro) +{ + if ((int)ro != (int)mReadOnly) + { + mReadOnly = ro; + mTime->setReadOnly(mReadOnly); + if (mOnceOnly) + mOnceOnly->setReadOnly(mReadOnly); + } +} + +bool Reminder::isReminder() const +{ + return mTime->isChecked(); +} + +bool Reminder::isOnceOnly() const +{ + return mOnceOnly && mOnceOnly->isEnabled() && mOnceOnly->isChecked(); +} + +void Reminder::setOnceOnly(bool onceOnly) +{ + if (mOnceOnly) + mOnceOnly->setChecked(onceOnly); +} + +/****************************************************************************** +* Specify whether the once-only checkbox is allowed to be enabled. +*/ +void Reminder::enableOnceOnly(bool enable) +{ + if (mOnceOnly) + { + mOnceOnlyEnabled = enable; + mOnceOnly->setEnabled(enable && mTime->isChecked()); + } +} + +void Reminder::setMaximum(int hourmin, int days) +{ + mTime->setMaximum(hourmin, days); +} + +/****************************************************************************** + * Get the specified number of minutes in advance of the main alarm the + * reminder is to be. + */ +int Reminder::minutes() const +{ + return mTime->minutes(); +} + +/****************************************************************************** +* Initialise the controls with a specified reminder time. +*/ +void Reminder::setMinutes(int minutes, bool dateOnly) +{ + mTime->setMinutes(minutes, dateOnly, Preferences::defaultReminderUnits()); +} + +/****************************************************************************** +* Set the advance reminder units to days if "Any time" is checked. +*/ +void Reminder::setDateOnly(bool dateOnly) +{ + mTime->setDateOnly(dateOnly); +} + +/****************************************************************************** +* Set the input focus on the count field. +*/ +void Reminder::setFocusOnCount() +{ + mTime->setFocusOnCount(); +} + +/****************************************************************************** +* Called when the Reminder checkbox is toggled. +*/ +void Reminder::slotReminderToggled(bool on) +{ + if (mOnceOnly) + mOnceOnly->setEnabled(on && mOnceOnlyEnabled); +} diff --git a/kalarm/reminder.h b/kalarm/reminder.h new file mode 100644 index 000000000..1403621d2 --- /dev/null +++ b/kalarm/reminder.h @@ -0,0 +1,60 @@ +/* + * reminder.h - reminder setting widget + * Program: kalarm + * Copyright (C) 2003, 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef REMINDER_H +#define REMINDER_H + +#include <qframe.h> + +class TimeSelector; +class CheckBox; + + +class Reminder : public QFrame +{ + Q_OBJECT + public: + Reminder(const QString& caption, const QString& reminderWhatsThis, const QString& valueWhatsThis, + bool allowHourMinute, bool showOnceOnly, QWidget* parent, const char* name = 0); + bool isReminder() const; + bool isOnceOnly() const; + int minutes() const; + void setMinutes(int minutes, bool dateOnly); + void setReadOnly(bool); + void setDateOnly(bool dateOnly); + void setMaximum(int hourmin, int days); + void setFocusOnCount(); + void setOnceOnly(bool); + void enableOnceOnly(bool enable); + + static QString i18n_first_recurrence_only(); // plain text of 'Reminder for first recurrence only' checkbox + static QString i18n_u_first_recurrence_only(); // text of 'Reminder for first recurrence only' checkbox, with 'u' shortcut + + protected slots: + void slotReminderToggled(bool); + + private: + TimeSelector* mTime; + CheckBox* mOnceOnly; + bool mReadOnly; // the widget is read only + bool mOnceOnlyEnabled; // 'mOnceOnly' checkbox is allowed to be enabled +}; + +#endif // REMINDER_H diff --git a/kalarm/repetition.cpp b/kalarm/repetition.cpp new file mode 100644 index 000000000..dc6613ee8 --- /dev/null +++ b/kalarm/repetition.cpp @@ -0,0 +1,364 @@ +/* + * repetition.cpp - pushbutton and dialogue to specify alarm sub-repetition + * Program: kalarm + * Copyright © 2004,2005,2007,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qlayout.h> +#include <qwhatsthis.h> + +#include <kdebug.h> +#include <kdialog.h> +#include <klocale.h> + +#include "buttongroup.h" +#include "radiobutton.h" +#include "spinbox.h" +#include "timeperiod.h" +#include "timeselector.h" +#include "repetition.moc" + + +/*============================================================================= += Class RepetitionButton += Button to display the Alarm Sub-Repetition dialogue. +=============================================================================*/ + +RepetitionButton::RepetitionButton(const QString& caption, bool waitForInitialisation, QWidget* parent, const char* name) + : QPushButton(caption, parent, name), + mDialog(0), + mInterval(0), + mCount(0), + mMaxDuration(-1), + mDateOnly(false), + mWaitForInit(waitForInitialisation), + mReadOnly(false) +{ + setToggleButton(true); + setOn(false); + connect(this, SIGNAL(clicked()), SLOT(slotPressed())); +} + +void RepetitionButton::set(int interval, int count) +{ + mInterval = interval; + mCount = count; + setOn(mInterval && mCount); +} + +/****************************************************************************** +* Set the data for the dialog. +*/ +void RepetitionButton::set(int interval, int count, bool dateOnly, int maxDuration) +{ + mInterval = interval; + mCount = count; + mMaxDuration = maxDuration; + mDateOnly = dateOnly; + setOn(mInterval && mCount); +} + +/****************************************************************************** +* Create the alarm repetition dialog. +* If 'waitForInitialisation' is true, the dialog won't be displayed until set() +* is called to initialise its data. +*/ +void RepetitionButton::activate(bool waitForInitialisation) +{ + if (!mDialog) + mDialog = new RepetitionDlg(i18n("Alarm Sub-Repetition"), mReadOnly, this); + mDialog->set(mInterval, mCount, mDateOnly, mMaxDuration); + if (waitForInitialisation) + emit needsInitialisation(); // request dialog initialisation + else + displayDialog(); // display the dialog now +} + +/****************************************************************************** +* Set the data for the dialog and display it. +* To be called only after needsInitialisation() has been emitted. +*/ +void RepetitionButton::initialise(int interval, int count, bool dateOnly, int maxDuration) +{ + if (maxDuration > 0 && interval > maxDuration) + count = 0; + mCount = count; + mInterval = interval; + mMaxDuration = maxDuration; + mDateOnly = dateOnly; + if (mDialog) + { + mDialog->set(interval, count, dateOnly, maxDuration); + displayDialog(); // display the dialog now + } + else + setOn(mInterval && mCount); +} + +/****************************************************************************** +* Display the alarm sub-repetition dialog. +* Alarm repetition has the following restrictions: +* 1) Not allowed for a repeat-at-login alarm +* 2) For a date-only alarm, the repeat interval must be a whole number of days. +* 3) The overall repeat duration must be less than the recurrence interval. +*/ +void RepetitionButton::displayDialog() +{ + kdDebug(5950) << "RepetitionButton::displayDialog()" << endl; + bool change = false; + if (mReadOnly) + { + mDialog->setReadOnly(true); + mDialog->exec(); + } + else if (mDialog->exec() == QDialog::Accepted) + { + mCount = mDialog->count(); + mInterval = mDialog->interval(); + change = true; + } + setOn(mInterval && mCount); + delete mDialog; + mDialog = 0; + if (change) + emit changed(); // delete dialog first, or initialise() will redisplay dialog +} + + +/*============================================================================= += Class RepetitionDlg += Alarm sub-repetition dialogue. +=============================================================================*/ + +static const int MAX_COUNT = 9999; // maximum range for count spinbox + + +RepetitionDlg::RepetitionDlg(const QString& caption, bool readOnly, QWidget* parent, const char* name) + : KDialogBase(parent, name, true, caption, Ok|Cancel), + mMaxDuration(-1), + mDateOnly(false), + mReadOnly(readOnly) +{ + int spacing = spacingHint(); + QWidget* page = new QWidget(this); + setMainWidget(page); + QVBoxLayout* topLayout = new QVBoxLayout(page, 0, spacing); + + mTimeSelector = new TimeSelector(i18n("Repeat every 10 minutes", "&Repeat every"), QString::null, + i18n("Instead of the alarm triggering just once at each recurrence, " + "checking this option makes the alarm trigger multiple times at each recurrence."), + i18n("Enter the time between repetitions of the alarm"), + true, page); + mTimeSelector->setFixedSize(mTimeSelector->sizeHint()); + connect(mTimeSelector, SIGNAL(valueChanged(int)), SLOT(intervalChanged(int))); + connect(mTimeSelector, SIGNAL(toggled(bool)), SLOT(repetitionToggled(bool))); + topLayout->addWidget(mTimeSelector, 0, Qt::AlignAuto); + + mButtonGroup = new ButtonGroup(page, "buttonGroup"); + connect(mButtonGroup, SIGNAL(buttonSet(int)), SLOT(typeClicked())); + topLayout->addWidget(mButtonGroup); + + QBoxLayout* vlayout = new QVBoxLayout(mButtonGroup, marginHint(), spacing); + QBoxLayout* layout = new QHBoxLayout(vlayout, spacing); + mCountButton = new RadioButton(i18n("&Number of repetitions:"), mButtonGroup); + mCountButton->setFixedSize(mCountButton->sizeHint()); + QWhatsThis::add(mCountButton, + i18n("Check to specify the number of times the alarm should repeat after each recurrence")); + layout->addWidget(mCountButton); + mCount = new SpinBox(1, MAX_COUNT, 1, mButtonGroup); + mCount->setFixedSize(mCount->sizeHint()); + mCount->setLineShiftStep(10); + mCount->setSelectOnStep(false); + connect(mCount, SIGNAL(valueChanged(int)), SLOT(countChanged(int))); + QWhatsThis::add(mCount, + i18n("Enter the number of times to trigger the alarm after its initial occurrence")); + layout->addWidget(mCount); + mCountButton->setFocusWidget(mCount); + layout->addStretch(); + + layout = new QHBoxLayout(vlayout, spacing); + mDurationButton = new RadioButton(i18n("&Duration:"), mButtonGroup); + mDurationButton->setFixedSize(mDurationButton->sizeHint()); + QWhatsThis::add(mDurationButton, + i18n("Check to specify how long the alarm is to be repeated")); + layout->addWidget(mDurationButton); + mDuration = new TimePeriod(true, mButtonGroup); + mDuration->setFixedSize(mDuration->sizeHint()); + connect(mDuration, SIGNAL(valueChanged(int)), SLOT(durationChanged(int))); + QWhatsThis::add(mDuration, + i18n("Enter the length of time to repeat the alarm")); + layout->addWidget(mDuration); + mDurationButton->setFocusWidget(mDuration); + layout->addStretch(); + + mCountButton->setChecked(true); + repetitionToggled(false); + setReadOnly(mReadOnly); +} + +/****************************************************************************** +* Set the state of all controls to reflect the data in the specified alarm. +*/ +void RepetitionDlg::set(int interval, int count, bool dateOnly, int maxDuration) +{ + if (!interval) + count = 0; + else if (!count) + interval = 0; + if (dateOnly != mDateOnly) + { + mDateOnly = dateOnly; + mTimeSelector->setDateOnly(mDateOnly); + mDuration->setDateOnly(mDateOnly); + } + mMaxDuration = maxDuration; + if (mMaxDuration) + { + int maxhm = (mMaxDuration > 0) ? mMaxDuration : 9999; + int maxdw = (mMaxDuration > 0) ? mMaxDuration / 1440 : 9999; + mTimeSelector->setMaximum(maxhm, maxdw); + mDuration->setMaximum(maxhm, maxdw); + } + // Set the units - needed later if the control is unchecked initially. + TimePeriod::Units units = mDateOnly ? TimePeriod::DAYS : TimePeriod::HOURS_MINUTES; + mTimeSelector->setMinutes(interval, mDateOnly, units); + if (!mMaxDuration || !count) + mTimeSelector->setChecked(false); + else + { + bool on = mTimeSelector->isChecked(); + repetitionToggled(on); // enable/disable controls + if (on) + intervalChanged(interval); // ensure mCount range is set + mCount->setValue(count); + mDuration->setMinutes(count * interval, mDateOnly, units); + mCountButton->setChecked(true); + } + mTimeSelector->setEnabled(mMaxDuration); +} + +/****************************************************************************** +* Set the read-only status. +*/ +void RepetitionDlg::setReadOnly(bool ro) +{ + ro = ro || mReadOnly; + mTimeSelector->setReadOnly(ro); + mCountButton->setReadOnly(ro); + mCount->setReadOnly(ro); + mDurationButton->setReadOnly(ro); + mDuration->setReadOnly(ro); +} + +/****************************************************************************** +* Get the period between repetitions in minutes. +*/ +int RepetitionDlg::interval() const +{ + return mTimeSelector->minutes(); +} + +/****************************************************************************** +* Set the entered repeat count. +*/ +int RepetitionDlg::count() const +{ + int interval = mTimeSelector->minutes(); + if (interval) + { + if (mCountButton->isOn()) + return mCount->value(); + if (mDurationButton->isOn()) + return mDuration->minutes() / interval; + } + return 0; // no repetition +} + +/****************************************************************************** +* Called when the time interval widget has changed value. +* Adjust the maximum repetition count accordingly. +*/ +void RepetitionDlg::intervalChanged(int minutes) +{ + if (mTimeSelector->isChecked() && minutes > 0) + { + mCount->setRange(1, (mMaxDuration >= 0 ? mMaxDuration / minutes : MAX_COUNT)); + if (mCountButton->isOn()) + countChanged(mCount->value()); + else + durationChanged(mDuration->minutes()); + } +} + +/****************************************************************************** +* Called when the count spinbox has changed value. +* Adjust the duration accordingly. +*/ +void RepetitionDlg::countChanged(int count) +{ + int interval = mTimeSelector->minutes(); + if (interval) + { + bool blocked = mDuration->signalsBlocked(); + mDuration->blockSignals(true); + mDuration->setMinutes(count * interval, mDateOnly, + (mDateOnly ? TimePeriod::DAYS : TimePeriod::HOURS_MINUTES)); + mDuration->blockSignals(blocked); + } +} + +/****************************************************************************** +* Called when the duration widget has changed value. +* Adjust the count accordingly. +*/ +void RepetitionDlg::durationChanged(int minutes) +{ + int interval = mTimeSelector->minutes(); + if (interval) + { + bool blocked = mCount->signalsBlocked(); + mCount->blockSignals(true); + mCount->setValue(minutes / interval); + mCount->blockSignals(blocked); + } +} + +/****************************************************************************** +* Called when the time period widget is toggled on or off. +*/ +void RepetitionDlg::repetitionToggled(bool on) +{ + if (mMaxDuration == 0) + on = false; + mButtonGroup->setEnabled(on); + mCount->setEnabled(on && mCountButton->isOn()); + mDuration->setEnabled(on && mDurationButton->isOn()); +} + +/****************************************************************************** +* Called when one of the count or duration radio buttons is toggled. +*/ +void RepetitionDlg::typeClicked() +{ + if (mTimeSelector->isChecked()) + { + mCount->setEnabled(mCountButton->isOn()); + mDuration->setEnabled(mDurationButton->isOn()); + } +} diff --git a/kalarm/repetition.h b/kalarm/repetition.h new file mode 100644 index 000000000..64837b875 --- /dev/null +++ b/kalarm/repetition.h @@ -0,0 +1,99 @@ +/* + * repetition.h - pushbutton and dialogue to specify alarm repetition + * Program: kalarm + * Copyright © 2004,2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef REPETITION_H +#define REPETITION_H + +#include <qpushbutton.h> +#include <kdialogbase.h> + +class ButtonGroup; +class RadioButton; +class SpinBox; +class TimeSelector; +class TimePeriod; +class RepetitionDlg; + + +class RepetitionButton : public QPushButton +{ + Q_OBJECT + public: + RepetitionButton(const QString& caption, bool waitForInitialisation, QWidget* parent, const char* name = 0); + void set(int interval, int count); + void set(int interval, int count, bool dateOnly, int maxDuration = -1); + void initialise(int interval, int count, bool dateOnly, int maxDuration = -1); // use only after needsInitialisation() signal + void activate() { activate(false); } + int interval() const { return mInterval; } + int count() const { return mCount; } + virtual void setReadOnly(bool ro) { mReadOnly = ro; } + virtual bool isReadOnly() const { return mReadOnly; } + + signals: + void needsInitialisation(); // dialog has been created and needs set() to be called + void changed(); // the repetition dialog has been edited + + private slots: + void slotPressed() { activate(mWaitForInit); } + + private: + void activate(bool waitForInitialisation); + void displayDialog(); + + RepetitionDlg* mDialog; + int mInterval; // interval between repetitions, in minutes + int mCount; // total number of repetitions, including first occurrence + int mMaxDuration; // maximum allowed duration in minutes, or -1 for infinite + bool mDateOnly; // hours/minutes cannot be displayed + bool mWaitForInit; // emit needsInitialisation() when button pressed, display when initialise() called + bool mReadOnly; +}; + + +class RepetitionDlg : public KDialogBase +{ + Q_OBJECT + public: + RepetitionDlg(const QString& caption, bool readOnly, QWidget* parent = 0, const char* name = 0); + void setReadOnly(bool); + void set(int interval, int count, bool dateOnly = false, int maxDuration = -1); + int interval() const; // get the interval between repetitions, in minutes + int count() const; // get the repeat count + + private slots: + void typeClicked(); + void intervalChanged(int); + void countChanged(int); + void durationChanged(int); + void repetitionToggled(bool); + + private: + TimeSelector* mTimeSelector; + ButtonGroup* mButtonGroup; + RadioButton* mCountButton; + SpinBox* mCount; + RadioButton* mDurationButton; + TimePeriod* mDuration; + int mMaxDuration; // maximum allowed duration in minutes, or -1 for infinite + bool mDateOnly; // hours/minutes cannot be displayed + bool mReadOnly; // the widget is read only +}; + +#endif // REPETITION_H diff --git a/kalarm/sounddlg.cpp b/kalarm/sounddlg.cpp new file mode 100644 index 000000000..fe1df0bad --- /dev/null +++ b/kalarm/sounddlg.cpp @@ -0,0 +1,472 @@ +/* + * sounddlg.cpp - sound file selection and configuration dialog + * Program: kalarm + * Copyright © 2005,2007,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef WITHOUT_ARTS + +#include "kalarm.h" + +#include <qlabel.h> +#include <qhbox.h> +#include <qgroupbox.h> +#include <qlayout.h> +#include <qfile.h> +#include <qdir.h> +#include <qwhatsthis.h> +#include <qtooltip.h> + +#include <klocale.h> +#include <kstandarddirs.h> +#include <kiconloader.h> +#ifdef WITHOUT_ARTS +#include <kaudioplayer.h> +#else +#include <qtimer.h> +#include <arts/kartsdispatcher.h> +#include <arts/kartsserver.h> +#include <arts/kplayobjectfactory.h> +#include <arts/kplayobject.h> +#endif +#include <kmessagebox.h> +#include <kio/netaccess.h> +#include <kdebug.h> + +#include "checkbox.h" +#include "functions.h" +#include "lineedit.h" +#include "mainwindow.h" +#include "pushbutton.h" +#include "slider.h" +#include "soundpicker.h" +#include "spinbox.h" +#include "sounddlg.moc" + + +// Collect these widget labels together to ensure consistent wording and +// translations across different modules. +QString SoundDlg::i18n_SetVolume() { return i18n("Set volume"); } +QString SoundDlg::i18n_v_SetVolume() { return i18n("Set &volume"); } +QString SoundDlg::i18n_Repeat() { return i18n("Repeat"); } +QString SoundDlg::i18n_p_Repeat() { return i18n("Re&peat"); } + +static const char SOUND_DIALOG_NAME[] = "SoundDialog"; + + +SoundDlg::SoundDlg(const QString& file, float volume, float fadeVolume, int fadeSeconds, bool repeat, + const QString& caption, QWidget* parent, const char* name) + : KDialogBase(parent, name, true, caption, Ok|Cancel, Ok, false), + mReadOnly(false), + mArtsDispatcher(0), + mPlayObject(0), + mPlayTimer(0) +{ + QWidget* page = new QWidget(this); + setMainWidget(page); + QVBoxLayout* layout = new QVBoxLayout(page, 0, spacingHint()); + + // File play button + QHBox* box = new QHBox(page); + layout->addWidget(box); + mFilePlay = new QPushButton(box); + mFilePlay->setPixmap(SmallIcon("player_play")); + mFilePlay->setFixedSize(mFilePlay->sizeHint()); + connect(mFilePlay, SIGNAL(clicked()), SLOT(playSound())); + QToolTip::add(mFilePlay, i18n("Test the sound")); + QWhatsThis::add(mFilePlay, i18n("Play the selected sound file.")); + + // File name edit box + mFileEdit = new LineEdit(LineEdit::Url, box); + mFileEdit->setAcceptDrops(true); + QWhatsThis::add(mFileEdit, i18n("Enter the name or URL of a sound file to play.")); + + // File browse button + mFileBrowseButton = new PushButton(box); + mFileBrowseButton->setPixmap(SmallIcon("fileopen")); + mFileBrowseButton->setFixedSize(mFileBrowseButton->sizeHint()); + connect(mFileBrowseButton, SIGNAL(clicked()), SLOT(slotPickFile())); + QToolTip::add(mFileBrowseButton, i18n("Choose a file")); + QWhatsThis::add(mFileBrowseButton, i18n("Select a sound file to play.")); + + // Sound repetition checkbox + mRepeatCheckbox = new CheckBox(i18n_p_Repeat(), page); + mRepeatCheckbox->setFixedSize(mRepeatCheckbox->sizeHint()); + QWhatsThis::add(mRepeatCheckbox, + i18n("If checked, the sound file will be played repeatedly for as long as the message is displayed.")); + layout->addWidget(mRepeatCheckbox); + + // Volume + QGroupBox* group = new QGroupBox(i18n("Volume"), page); + layout->addWidget(group); + QGridLayout* grid = new QGridLayout(group, 4, 3, marginHint(), spacingHint()); + grid->addRowSpacing(0, fontMetrics().height() - marginHint() + spacingHint()); + grid->setColStretch(2, 1); + int indentWidth = 3 * KDialog::spacingHint(); + grid->addColSpacing(0, indentWidth); + grid->addColSpacing(1, indentWidth); + // Get alignment to use in QGridLayout (AlignAuto doesn't work correctly there) + int alignment = QApplication::reverseLayout() ? Qt::AlignRight : Qt::AlignLeft; + + // 'Set volume' checkbox + box = new QHBox(group); + box->setSpacing(spacingHint()); + grid->addMultiCellWidget(box, 1, 1, 0, 2); + mVolumeCheckbox = new CheckBox(i18n_v_SetVolume(), box); + mVolumeCheckbox->setFixedSize(mVolumeCheckbox->sizeHint()); + connect(mVolumeCheckbox, SIGNAL(toggled(bool)), SLOT(slotVolumeToggled(bool))); + QWhatsThis::add(mVolumeCheckbox, + i18n("Select to choose the volume for playing the sound file.")); + + // Volume slider + mVolumeSlider = new Slider(0, 100, 10, 0, Qt::Horizontal, box); + mVolumeSlider->setTickmarks(QSlider::Below); + mVolumeSlider->setTickInterval(10); + mVolumeSlider->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); + QWhatsThis::add(mVolumeSlider, i18n("Choose the volume for playing the sound file.")); + mVolumeCheckbox->setFocusWidget(mVolumeSlider); + + // Fade checkbox + mFadeCheckbox = new CheckBox(i18n("Fade"), group); + mFadeCheckbox->setFixedSize(mFadeCheckbox->sizeHint()); + connect(mFadeCheckbox, SIGNAL(toggled(bool)), SLOT(slotFadeToggled(bool))); + QWhatsThis::add(mFadeCheckbox, + i18n("Select to fade the volume when the sound file first starts to play.")); + grid->addMultiCellWidget(mFadeCheckbox, 2, 2, 1, 2, alignment); + + // Fade time + mFadeBox = new QHBox(group); + mFadeBox->setSpacing(spacingHint()); + grid->addWidget(mFadeBox, 3, 2, alignment); + QLabel* label = new QLabel(i18n("Time period over which to fade the sound", "Fade time:"), mFadeBox); + label->setFixedSize(label->sizeHint()); + mFadeTime = new SpinBox(1, 999, 1, mFadeBox); + mFadeTime->setLineShiftStep(10); + mFadeTime->setFixedSize(mFadeTime->sizeHint()); + label->setBuddy(mFadeTime); + label = new QLabel(i18n("seconds"), mFadeBox); + label->setFixedSize(label->sizeHint()); + QWhatsThis::add(mFadeBox, i18n("Enter how many seconds to fade the sound before reaching the set volume.")); + + // Fade slider + mFadeVolumeBox = new QHBox(group); + mFadeVolumeBox->setSpacing(spacingHint()); + grid->addWidget(mFadeVolumeBox, 4, 2); + label = new QLabel(i18n("Initial volume:"), mFadeVolumeBox); + label->setFixedSize(label->sizeHint()); + mFadeSlider = new Slider(0, 100, 10, 0, Qt::Horizontal, mFadeVolumeBox); + mFadeSlider->setTickmarks(QSlider::Below); + mFadeSlider->setTickInterval(10); + mFadeSlider->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); + label->setBuddy(mFadeSlider); + QWhatsThis::add(mFadeVolumeBox, i18n("Choose the initial volume for playing the sound file.")); + + // Restore the dialogue size from last time + QSize s; + if (KAlarm::readConfigWindowSize(SOUND_DIALOG_NAME, s)) + resize(s); + + // Initialise the control values + mFileEdit->setText(file); + mRepeatCheckbox->setChecked(repeat); + mVolumeCheckbox->setChecked(volume >= 0); + mVolumeSlider->setValue(volume >= 0 ? static_cast<int>(volume*100) : 100); + mFadeCheckbox->setChecked(fadeVolume >= 0); + mFadeSlider->setValue(fadeVolume >= 0 ? static_cast<int>(fadeVolume*100) : 100); + mFadeTime->setValue(fadeSeconds); + slotVolumeToggled(volume >= 0); +} + +SoundDlg::~SoundDlg() +{ + stopPlay(); +} + +/****************************************************************************** +* Set the read-only status of the dialogue. +*/ +void SoundDlg::setReadOnly(bool readOnly) +{ + if (readOnly != mReadOnly) + { + mFileEdit->setReadOnly(readOnly); + mFileBrowseButton->setReadOnly(readOnly); + mRepeatCheckbox->setReadOnly(readOnly); + mVolumeCheckbox->setReadOnly(readOnly); + mVolumeSlider->setReadOnly(readOnly); + mFadeCheckbox->setReadOnly(readOnly); + mFadeTime->setReadOnly(readOnly); + mFadeSlider->setReadOnly(readOnly); + mReadOnly = readOnly; + } +} + +/****************************************************************************** +* Return the entered repetition and volume settings: +* 'volume' is in range 0 - 1, or < 0 if volume is not to be set. +* 'fadeVolume is similar, with 'fadeTime' set to the fade interval in seconds. +* Reply = whether to repeat or not. +*/ +bool SoundDlg::getSettings(float& volume, float& fadeVolume, int& fadeSeconds) const +{ + volume = mVolumeCheckbox->isChecked() ? (float)mVolumeSlider->value() / 100 : -1; + if (mFadeCheckbox->isChecked()) + { + fadeVolume = (float)mFadeSlider->value() / 100; + fadeSeconds = mFadeTime->value(); + } + else + { + fadeVolume = -1; + fadeSeconds = 0; + } + return mRepeatCheckbox->isChecked(); +} + +/****************************************************************************** +* Called when the dialog's size has changed. +* Records the new size in the config file. +*/ +void SoundDlg::resizeEvent(QResizeEvent* re) +{ + if (isVisible()) + KAlarm::writeConfigWindowSize(SOUND_DIALOG_NAME, re->size()); + mVolumeSlider->resize(mFadeSlider->size()); + KDialog::resizeEvent(re); +} + +void SoundDlg::showEvent(QShowEvent* se) +{ + mVolumeSlider->resize(mFadeSlider->size()); + KDialog::showEvent(se); +} + +/****************************************************************************** +* Called when the OK button is clicked. +*/ +void SoundDlg::slotOk() +{ + if (mReadOnly) + reject(); + if (!checkFile()) + return; + accept(); +} + +/****************************************************************************** +* Called when the file browser button is clicked. +*/ +void SoundDlg::slotPickFile() +{ + QString url = SoundPicker::browseFile(mDefaultDir, mFileEdit->text()); + if (!url.isEmpty()) + mFileEdit->setText(url); +} + +/****************************************************************************** +* Called when the file play/stop button is clicked. +*/ +void SoundDlg::playSound() +{ +#ifdef WITHOUT_ARTS + if (checkFile()) + KAudioPlayer::play(QFile::encodeName(mFileName)); +#else + if (mPlayObject) + { + stopPlay(); + return; + } + if (!checkFile()) + return; + KURL url(mFileName); + MainWindow* mmw = MainWindow::mainMainWindow(); + if (!url.isValid() || !KIO::NetAccess::exists(url, true, mmw) + || !KIO::NetAccess::download(url, mLocalAudioFile, mmw)) + { + kdError(5950) << "SoundDlg::playAudio(): Open failure: " << mFileName << endl; + KMessageBox::error(this, i18n("Cannot open audio file:\n%1").arg(mFileName)); + return; + } + mPlayTimer = new QTimer(this); + connect(mPlayTimer, SIGNAL(timeout()), SLOT(checkAudioPlay())); + mArtsDispatcher = new KArtsDispatcher; + mPlayStarted = false; + KArtsServer aserver; + Arts::SoundServerV2 sserver = aserver.server(); + KDE::PlayObjectFactory factory(sserver); + mPlayObject = factory.createPlayObject(mLocalAudioFile, true); + mFilePlay->setPixmap(SmallIcon("player_stop")); + QToolTip::add(mFilePlay, i18n("Stop sound")); + QWhatsThis::add(mFilePlay, i18n("Stop playing the sound")); + connect(mPlayObject, SIGNAL(playObjectCreated()), SLOT(checkAudioPlay())); + if (!mPlayObject->object().isNull()) + checkAudioPlay(); +#endif +} + +/****************************************************************************** +* Called when the audio file has loaded and is ready to play, or on a timer +* when play is expected to have completed. +* If it is ready to play, start playing it (for the first time or repeated). +* If play has not yet completed, wait a bit longer. +*/ +void SoundDlg::checkAudioPlay() +{ +#ifndef WITHOUT_ARTS + if (!mPlayObject) + return; + if (mPlayObject->state() == Arts::posIdle) + { + // The file has loaded and is ready to play, or play has completed + if (mPlayStarted) + { + // Play has completed + stopPlay(); + return; + } + + // Start playing the file + kdDebug(5950) << "SoundDlg::checkAudioPlay(): start\n"; + mPlayStarted = true; + mPlayObject->play(); + } + + // The sound file is still playing + Arts::poTime overall = mPlayObject->overallTime(); + Arts::poTime current = mPlayObject->currentTime(); + int time = 1000*(overall.seconds - current.seconds) + overall.ms - current.ms; + if (time < 0) + time = 0; + kdDebug(5950) << "SoundDlg::checkAudioPlay(): wait for " << (time+100) << "ms\n"; + mPlayTimer->start(time + 100, true); +#endif +} + +/****************************************************************************** +* Called when play completes, the Silence button is clicked, or the window is +* closed, to terminate audio access. +*/ +void SoundDlg::stopPlay() +{ +#ifndef WITHOUT_ARTS + delete mPlayObject; mPlayObject = 0; + delete mArtsDispatcher; mArtsDispatcher = 0; + delete mPlayTimer; mPlayTimer = 0; + if (!mLocalAudioFile.isEmpty()) + { + KIO::NetAccess::removeTempFile(mLocalAudioFile); // removes it only if it IS a temporary file + mLocalAudioFile = QString::null; + } + mFilePlay->setPixmap(SmallIcon("player_play")); + QToolTip::add(mFilePlay, i18n("Test the sound")); + QWhatsThis::add(mFilePlay, i18n("Play the selected sound file.")); +#endif +} + +/****************************************************************************** +* Check whether the specified sound file exists. +* Note that KAudioPlayer::play() can only cope with local files. +*/ +bool SoundDlg::checkFile() +{ + mFileName = mFileEdit->text(); + KURL url; + if (KURL::isRelativeURL(mFileName)) + { + // It's not an absolute URL, so check for an absolute path + QFileInfo f(mFileName); + if (!f.isRelative()) + url.setPath(mFileName); + } + else + url = KURL::fromPathOrURL(mFileName); // it's an absolute URL +#ifdef WITHOUT_ARTS + if (!url.isEmpty()) + { + // It's an absolute path or URL. + // Only allow local files for KAudioPlayer. + if (url.isLocalFile() && KIO::NetAccess::exists(url, true, this)) + { + mFileName = url.path(); + return true; + } + } + else +#else + if (url.isEmpty()) +#endif + { + // It's a relative path. + // Find the first sound resource that contains files. + QStringList soundDirs = KGlobal::dirs()->resourceDirs("sound"); + if (!soundDirs.isEmpty()) + { + QDir dir; + dir.setFilter(QDir::Files | QDir::Readable); + for (QStringList::ConstIterator it = soundDirs.begin(); it != soundDirs.end(); ++it) + { + dir = *it; + if (dir.isReadable() && dir.count() > 2) + { + url.setPath(*it); + url.addPath(mFileName); + if (KIO::NetAccess::exists(url, true, this)) + { + mFileName = url.path(); + return true; + } + } + } + } + url.setPath(QDir::homeDirPath()); + url.addPath(mFileName); + if (KIO::NetAccess::exists(url, true, this)) + { + mFileName = url.path(); + return true; + } + } +#ifdef WITHOUT_ARTS + KMessageBox::sorry(this, i18n("File not found")); + mFileName = QString::null; + return false; +#else + return true; +#endif +} + +/****************************************************************************** +* Called when the Set Volume checkbox is toggled. +*/ +void SoundDlg::slotVolumeToggled(bool on) +{ + mVolumeSlider->setEnabled(on); + mFadeCheckbox->setEnabled(on); + slotFadeToggled(on && mFadeCheckbox->isChecked()); +} + +/****************************************************************************** +* Called when the Fade checkbox is toggled. +*/ +void SoundDlg::slotFadeToggled(bool on) +{ + mFadeBox->setEnabled(on); + mFadeVolumeBox->setEnabled(on); +} + +#endif // #ifndef WITHOUT_ARTS diff --git a/kalarm/sounddlg.h b/kalarm/sounddlg.h new file mode 100644 index 000000000..ee4273b5d --- /dev/null +++ b/kalarm/sounddlg.h @@ -0,0 +1,97 @@ +/* + * sounddlg.h - sound file selection and configuration dialog + * Program: kalarm + * Copyright © 2005,2007,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef SOUNDDLG_H +#define SOUNDDLG_H + +#include <qframe.h> +#include <qstring.h> +#include <kdialogbase.h> + +class QHBox; +class QPushButton; +class KArtsDispatcher; +namespace KDE { class PlayObject; } +class LineEdit; +class PushButton; +class CheckBox; +class SpinBox; +class Slider; + + +class SoundDlg : public KDialogBase +{ + Q_OBJECT + public: + SoundDlg(const QString& file, float volume, float fadeVolume, int fadeSeconds, bool repeat, + const QString& caption, QWidget* parent, const char* name = 0); + ~SoundDlg(); + void setReadOnly(bool); + bool isReadOnly() const { return mReadOnly; } + QString getFile() const { return mFileName; } + bool getSettings(float& volume, float& fadeVolume, int& fadeSeconds) const; + QString defaultDir() const { return mDefaultDir; } + + static QString i18n_SetVolume(); // plain text of Set volume checkbox + static QString i18n_v_SetVolume(); // text of Set volume checkbox, with 'V' shortcut + static QString i18n_Repeat(); // plain text of Repeat checkbox + static QString i18n_p_Repeat(); // text of Repeat checkbox, with 'P' shortcut + + protected: + virtual void showEvent(QShowEvent*); + virtual void resizeEvent(QResizeEvent*); + + protected slots: + virtual void slotOk(); + + private slots: + void slotPickFile(); + void slotVolumeToggled(bool on); + void slotFadeToggled(bool on); + void playSound(); + void checkAudioPlay(); + + private: + void stopPlay(); + bool checkFile(); + + QPushButton* mFilePlay; + LineEdit* mFileEdit; + PushButton* mFileBrowseButton; + CheckBox* mRepeatCheckbox; + CheckBox* mVolumeCheckbox; + Slider* mVolumeSlider; + CheckBox* mFadeCheckbox; + QHBox* mFadeBox; + SpinBox* mFadeTime; + QHBox* mFadeVolumeBox; + Slider* mFadeSlider; + QString mDefaultDir; // current default directory for mFileEdit + QString mFileName; + bool mReadOnly; + // Sound file playing + KArtsDispatcher* mArtsDispatcher; + KDE::PlayObject* mPlayObject; + QTimer* mPlayTimer; // timer for playing the sound file + QString mLocalAudioFile; // local copy of audio file + bool mPlayStarted; // the sound file has started playing +}; + +#endif diff --git a/kalarm/soundpicker.cpp b/kalarm/soundpicker.cpp new file mode 100644 index 000000000..0e0626de9 --- /dev/null +++ b/kalarm/soundpicker.cpp @@ -0,0 +1,292 @@ +/* + * soundpicker.cpp - widget to select a sound file or a beep + * Program: kalarm + * Copyright © 2002,2004-2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qlayout.h> +#include <qregexp.h> +#include <qtooltip.h> +#include <qtimer.h> +#include <qlabel.h> +#include <qhbox.h> +#include <qwhatsthis.h> + +#include <kglobal.h> +#include <klocale.h> +#include <kfiledialog.h> +#include <kstandarddirs.h> +#include <kiconloader.h> +#ifndef WITHOUT_ARTS +#include <arts/kplayobjectfactory.h> +#endif +#include <kdebug.h> + +#include "combobox.h" +#include "functions.h" +#include "kalarmapp.h" +#include "pushbutton.h" +#include "sounddlg.h" +#include "soundpicker.moc" + + +// Collect these widget labels together to ensure consistent wording and +// translations across different modules. +QString SoundPicker::i18n_Sound() { return i18n("An audio sound", "Sound"); } +QString SoundPicker::i18n_None() { return i18n("None"); } +QString SoundPicker::i18n_Beep() { return i18n("Beep"); } +QString SoundPicker::i18n_Speak() { return i18n("Speak"); } +QString SoundPicker::i18n_File() { return i18n("Sound file"); } + + +SoundPicker::SoundPicker(QWidget* parent, const char* name) + : QFrame(parent, name) +{ + setFrameStyle(QFrame::NoFrame); + QHBoxLayout* soundLayout = new QHBoxLayout(this, 0, KDialog::spacingHint()); + mTypeBox = new QHBox(this); // this is to control the QWhatsThis text display area + mTypeBox->setSpacing(KDialog::spacingHint()); + + QLabel* label = new QLabel(i18n("An audio sound", "&Sound:"), mTypeBox); + label->setFixedSize(label->sizeHint()); + + // Sound type combo box + // The order of combo box entries must correspond with the 'Type' enum. + mTypeCombo = new ComboBox(false, mTypeBox); + mTypeCombo->insertItem(i18n_None()); // index NONE + mTypeCombo->insertItem(i18n_Beep()); // index BEEP + mTypeCombo->insertItem(i18n_File()); // index PLAY_FILE + mSpeakShowing = !theApp()->speechEnabled(); + showSpeak(!mSpeakShowing); // index SPEAK (only displayed if appropriate) + connect(mTypeCombo, SIGNAL(activated(int)), SLOT(slotTypeSelected(int))); + label->setBuddy(mTypeCombo); + soundLayout->addWidget(mTypeBox); + + // Sound file picker button + mFilePicker = new PushButton(this); + mFilePicker->setPixmap(SmallIcon("playsound")); + mFilePicker->setFixedSize(mFilePicker->sizeHint()); + connect(mFilePicker, SIGNAL(clicked()), SLOT(slotPickFile())); + QToolTip::add(mFilePicker, i18n("Configure sound file")); + QWhatsThis::add(mFilePicker, i18n("Configure a sound file to play when the alarm is displayed.")); + soundLayout->addWidget(mFilePicker); + + // Initialise the file picker button state and tooltip + mTypeCombo->setCurrentItem(NONE); + mFilePicker->setEnabled(false); +} + +/****************************************************************************** +* Set the read-only status of the widget. +*/ +void SoundPicker::setReadOnly(bool readOnly) +{ + mTypeCombo->setReadOnly(readOnly); +#ifdef WITHOUT_ARTS + mFilePicker->setReadOnly(readOnly); +#endif + mReadOnly = readOnly; +} + +/****************************************************************************** +* Show or hide the Speak option. +*/ +void SoundPicker::showSpeak(bool show) +{ + if (!theApp()->speechEnabled()) + show = false; // speech capability is not installed + if (show == mSpeakShowing) + return; // no change + QString whatsThis = "<p>" + i18n("Choose a sound to play when the message is displayed.") + + "<br>" + i18n("%1: the message is displayed silently.").arg("<b>" + i18n_None() + "</b>") + + "<br>" + i18n("%1: a simple beep is sounded.").arg("<b>" + i18n_Beep() + "</b>") + + "<br>" + i18n("%1: an audio file is played. You will be prompted to choose the file and set play options.").arg("<b>" + i18n_File() + "</b>"); + if (!show && mTypeCombo->currentItem() == SPEAK) + mTypeCombo->setCurrentItem(NONE); + if (mTypeCombo->count() == SPEAK+1) + mTypeCombo->removeItem(SPEAK); // precaution in case of mix-ups + if (show) + { + mTypeCombo->insertItem(i18n_Speak()); + whatsThis += "<br>" + i18n("%1: the message text is spoken.").arg("<b>" + i18n_Speak() + "</b>") + "</p>"; + } + QWhatsThis::add(mTypeBox, whatsThis + "</p>"); + mSpeakShowing = show; +} + +/****************************************************************************** +* Return the currently selected option. +*/ +SoundPicker::Type SoundPicker::sound() const +{ + return static_cast<SoundPicker::Type>(mTypeCombo->currentItem()); +} + +/****************************************************************************** +* Return the selected sound file, if the File option is selected. +* Returns null string if File is not currently selected. +*/ +QString SoundPicker::file() const +{ + return (mTypeCombo->currentItem() == PLAY_FILE) ? mFile : QString::null; +} + +/****************************************************************************** +* Return the specified volumes (range 0 - 1). +* Returns < 0 if beep is currently selected, or if 'set volume' is not selected. +*/ +float SoundPicker::volume(float& fadeVolume, int& fadeSeconds) const +{ + if (mTypeCombo->currentItem() == PLAY_FILE && !mFile.isEmpty()) + { + fadeVolume = mFadeVolume; + fadeSeconds = mFadeSeconds; + return mVolume; + } + else + { + fadeVolume = -1; + fadeSeconds = 0; + return -1; + } +} + +/****************************************************************************** +* Return whether sound file repetition is selected, if the main checkbox is checked. +* Returns false if beep is currently selected. +*/ +bool SoundPicker::repeat() const +{ + return mTypeCombo->currentItem() == PLAY_FILE && !mFile.isEmpty() && mRepeat; +} + +/****************************************************************************** +* Initialise the widget's state. +*/ +void SoundPicker::set(SoundPicker::Type type, const QString& f, float volume, float fadeVolume, int fadeSeconds, bool repeat) +{ + if (type == PLAY_FILE && f.isEmpty()) + type = BEEP; + mFile = f; + mVolume = volume; + mFadeVolume = fadeVolume; + mFadeSeconds = fadeSeconds; + mRepeat = repeat; + mTypeCombo->setCurrentItem(type); // this doesn't trigger slotTypeSelected() + mFilePicker->setEnabled(type == PLAY_FILE); + if (type == PLAY_FILE) + QToolTip::add(mTypeCombo, mFile); + else + QToolTip::remove(mTypeCombo); + mLastType = type; +} + +/****************************************************************************** +* Called when the sound option is changed. +*/ +void SoundPicker::slotTypeSelected(int id) +{ + Type newType = static_cast<Type>(id); + if (newType == mLastType) + return; + QString tooltip; + if (mLastType == PLAY_FILE) + { + mFilePicker->setEnabled(false); + QToolTip::remove(mTypeCombo); + } + else if (newType == PLAY_FILE) + { + if (mFile.isEmpty()) + { + slotPickFile(); + if (mFile.isEmpty()) + return; // revert to previously selected type + } + mFilePicker->setEnabled(true); + QToolTip::add(mTypeCombo, mFile); + } + mLastType = newType; +} + +/****************************************************************************** +* Called when the file picker button is clicked. +*/ +void SoundPicker::slotPickFile() +{ +#ifdef WITHOUT_ARTS + QString url = browseFile(mDefaultDir, mFile); + if (!url.isEmpty()) + mFile = url; +#else + QString file = mFile; + SoundDlg dlg(mFile, mVolume, mFadeVolume, mFadeSeconds, mRepeat, i18n("Sound File"), this, "soundDlg"); + dlg.setReadOnly(mReadOnly); + bool accepted = (dlg.exec() == QDialog::Accepted); + if (mReadOnly) + return; + if (accepted) + { + float volume, fadeVolume; + int fadeTime; + file = dlg.getFile(); + mRepeat = dlg.getSettings(volume, fadeVolume, fadeTime); + mVolume = volume; + mFadeVolume = fadeVolume; + mFadeSeconds = fadeTime; + } + if (!file.isEmpty()) + { + mFile = file; + mDefaultDir = dlg.defaultDir(); + } +#endif + if (mFile.isEmpty()) + { + // No audio file is selected, so revert to previously selected option + mTypeCombo->setCurrentItem(mLastType); + QToolTip::remove(mTypeCombo); + } + else + QToolTip::add(mTypeCombo, mFile); +} + +/****************************************************************************** +* Display a dialogue to choose a sound file, initially highlighting any +* specified file. 'initialFile' must be a full path name or URL. +* 'defaultDir' is updated to the directory containing the chosen file. +* Reply = URL selected. If none is selected, URL.isEmpty() is true. +*/ +QString SoundPicker::browseFile(QString& defaultDir, const QString& initialFile) +{ + static QString kdeSoundDir; // directory containing KDE sound files + if (defaultDir.isEmpty()) + { + if (kdeSoundDir.isNull()) + kdeSoundDir = KGlobal::dirs()->findResourceDir("sound", "KDE_Notify.wav"); + defaultDir = kdeSoundDir; + } +#ifdef WITHOUT_ARTS + QString filter = QString::fromLatin1("*.wav *.mp3 *.ogg|%1\n*|%2").arg(i18n("Sound Files")).arg(i18n("All Files")); +#else + QStringList filters = KDE::PlayObjectFactory::mimeTypes(); + QString filter = filters.join(" "); +#endif + return KAlarm::browseFile(i18n("Choose Sound File"), defaultDir, initialFile, filter, KFile::ExistingOnly, 0, "pickSoundFile"); +} diff --git a/kalarm/soundpicker.h b/kalarm/soundpicker.h new file mode 100644 index 000000000..7cad7f380 --- /dev/null +++ b/kalarm/soundpicker.h @@ -0,0 +1,135 @@ +/* + * soundpicker.h - widget to select a sound file or a beep + * Program: kalarm + * Copyright © 2002,2004-2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef SOUNDPICKER_H +#define SOUNDPICKER_H + +#include <qframe.h> +#include <qstring.h> +#include <kurl.h> + +class QHBox; +class ComboBox; +class PushButton; + + +class SoundPicker : public QFrame +{ + Q_OBJECT + public: + /** Sound options which can be selected for when the alarm is displayed. + * @li NONE - silence. + * @li BEEP - a beep will be sounded. + * @li PLAY_FILE - a sound file will be played. + * @li SPEAK - the message text will be spoken. + */ + enum Type { NONE = 0, BEEP, PLAY_FILE, SPEAK }; + /** Constructor. + * @param parent The parent object of this widget. + * @param name The name of this widget. + */ + explicit SoundPicker(QWidget* parent, const char* name = 0); + /** Initialises the widget's state. + * @param type The option to select. + * @param filename The full path or URL of the sound file to select. If the 'file' option is + * not initially selected, @p filename provides the default should 'file' + * later be selected by the user. + * @param volume The volume to play a sound file, or < 0 for no volume setting. If the + * 'file' option is not initially selected, @p volume provides the default + * should 'file' later be selected by the user. + * @param fadeVolume The initial volume to play a sound file if fading is to be used, or < 0 + * for no fading. If the 'file' option is not initially selected, @p fadeVolume + * provides the default should 'file' later be selected by the user. + * @param fadeSeconds The number of seconds over which the sound file volume should be faded, + * or 0 for no fading. If the 'file' option is not initially selected, + * @p fadeSeconds provides the default should 'file' later be selected by the user. + * @param repeat True to play the sound file repeatedly. If the 'file' option is not initially + * selected, @p repeat provides the default should 'file' later be selected by + * the user. + */ + void set(Type type, const QString& filename, float volume, float fadeVolume, int fadeSeconds, bool repeat); + /** Returns true if the widget is read only for the user. */ + bool isReadOnly() const { return mReadOnly; } + /** Sets whether the widget can be changed the user. + * @param readOnly True to set the widget read-only, false to set it read-write. + */ + void setReadOnly(bool readOnly); + /** Show or hide the 'speak' option. + * If it is to be hidden and it is currently selected, sound is turned off. + */ + void showSpeak(bool show); + /** Returns the selected option. */ + Type sound() const; + /** If the 'file' option is selected, returns the URL of the chosen file. + * Otherwise returns a null string. + */ + QString file() const; + /** Returns the volume and fade characteristics for playing a sound file. + * @param fadeVolume Receives the initial volume if the volume is to be faded, else -1. + * @param fadeSeconds Receives the number of seconds over which the volume is to be faded, else 0. + * @return Volume to play the sound file, or < 0 if the 'file' option is not selected. + */ + float volume(float& fadeVolume, int& fadeSeconds) const; + /** Returns true if a sound file is to be played repeatedly. + * If the 'file' option is not selected, returns false. + */ + bool repeat() const; + /** Returns the current file URL regardless of whether the 'file' option is selected. */ + QString fileSetting() const { return mFile; } + /** Returns the current file repetition setting regardless of whether the 'file' option is selected. */ + bool repeatSetting() const { return mRepeat; } + /** Display a dialogue to choose a sound file, initially highlighting + * @p initialFile if non-null. + * @param initialDir Initial directory to display if @p initialFile is null. If a file is + * chosen, this is updated to the directory containing the chosen file. + * @param initialFile Full path name or URL of file to be highlighted initially. + * If null, no file will be highlighted. + * @return URL selected, in human readable format. If none is selected, URL.isEmpty() is true. + */ + static QString browseFile(QString& initialDir, const QString& initialFile = QString::null); + + static QString i18n_Sound(); // plain text of Sound label + static QString i18n_None(); // plain text of None combo box item + static QString i18n_Beep(); // plain text of Beep combo box item + static QString i18n_Speak(); // plain text of Speak combo box item + static QString i18n_File(); // plain text of File combo box item + + + private slots: + void slotTypeSelected(int id); + void slotPickFile(); + + private: + + ComboBox* mTypeCombo; + QHBox* mTypeBox; + PushButton* mFilePicker; + QString mDefaultDir; + QString mFile; // sound file to play when alarm is triggered + float mVolume; // volume for file, or < 0 to not set volume + float mFadeVolume; // initial volume for file, or < 0 for no fading + int mFadeSeconds; // fade interval in seconds + Type mLastType; // last selected sound option + bool mSpeakShowing; // Speak option is shown in combo box + bool mRepeat; // repeat the sound file + bool mReadOnly; +}; + +#endif // SOUNDPICKER_H diff --git a/kalarm/specialactions.cpp b/kalarm/specialactions.cpp new file mode 100644 index 000000000..a92166eea --- /dev/null +++ b/kalarm/specialactions.cpp @@ -0,0 +1,192 @@ +/* + * specialactions.cpp - widget to specify special alarm actions + * Program: kalarm + * Copyright © 2004,2005,2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qlabel.h> +#include <qlayout.h> +#include <qwhatsthis.h> + +#include <klineedit.h> +#include <kapplication.h> +#include <kaboutdata.h> +#include <klocale.h> +#include <kdebug.h> + +#include "functions.h" +#include "shellprocess.h" +#include "specialactions.moc" + + +/*============================================================================= += Class SpecialActionsButton += Button to display the Special Alarm Actions dialogue. +=============================================================================*/ + +SpecialActionsButton::SpecialActionsButton(const QString& caption, QWidget* parent, const char* name) + : QPushButton(caption, parent, name), + mReadOnly(false) +{ + setToggleButton(true); + setOn(false); + connect(this, SIGNAL(clicked()), SLOT(slotButtonPressed())); + QWhatsThis::add(this, + i18n("Specify actions to execute before and after the alarm is displayed.")); +} + +/****************************************************************************** +* Set the pre- and post-alarm actions. +* The button's pressed state is set to reflect whether any actions are set. +*/ +void SpecialActionsButton::setActions(const QString& pre, const QString& post) +{ + mPreAction = pre; + mPostAction = post; + setOn(!mPreAction.isEmpty() || !mPostAction.isEmpty()); +} + +/****************************************************************************** +* Called when the OK button is clicked. +* Display a font and colour selection dialog and get the selections. +*/ +void SpecialActionsButton::slotButtonPressed() +{ + SpecialActionsDlg dlg(mPreAction, mPostAction, + i18n("Special Alarm Actions"), this, "actionsDlg"); + dlg.setReadOnly(mReadOnly); + if (dlg.exec() == QDialog::Accepted) + { + mPreAction = dlg.preAction(); + mPostAction = dlg.postAction(); + emit selected(); + } + setOn(!mPreAction.isEmpty() || !mPostAction.isEmpty()); +} + + +/*============================================================================= += Class SpecialActionsDlg += Pre- and post-alarm actions dialogue. +=============================================================================*/ + +static const char SPEC_ACT_DIALOG_NAME[] = "SpecialActionsDialog"; + + +SpecialActionsDlg::SpecialActionsDlg(const QString& preAction, const QString& postAction, + const QString& caption, QWidget* parent, const char* name) + : KDialogBase(parent, name, true, caption, Ok|Cancel, Ok, false) +{ + QWidget* page = new QWidget(this); + setMainWidget(page); + QVBoxLayout* layout = new QVBoxLayout(page, 0, spacingHint()); + + mActions = new SpecialActions(page); + mActions->setActions(preAction, postAction); + layout->addWidget(mActions); + layout->addSpacing(KDialog::spacingHint()); + + QSize s; + if (KAlarm::readConfigWindowSize(SPEC_ACT_DIALOG_NAME, s)) + resize(s); +} + +/****************************************************************************** +* Called when the OK button is clicked. +*/ +void SpecialActionsDlg::slotOk() +{ + if (mActions->isReadOnly()) + reject(); + accept(); +} + +/****************************************************************************** +* Called when the dialog's size has changed. +* Records the new size in the config file. +*/ +void SpecialActionsDlg::resizeEvent(QResizeEvent* re) +{ + if (isVisible()) + KAlarm::writeConfigWindowSize(SPEC_ACT_DIALOG_NAME, re->size()); + KDialog::resizeEvent(re); +} + + +/*============================================================================= += Class SpecialActions += Pre- and post-alarm actions widget. +=============================================================================*/ + +SpecialActions::SpecialActions(QWidget* parent, const char* name) + : QWidget(parent, name), + mReadOnly(false) +{ + QBoxLayout* topLayout = new QVBoxLayout(this, 0, KDialog::spacingHint()); + + // Pre-alarm action + QLabel* label = new QLabel(i18n("Pre-a&larm action:"), this); + label->setFixedSize(label->sizeHint()); + topLayout->addWidget(label, 0, Qt::AlignAuto); + + mPreAction = new KLineEdit(this); + label->setBuddy(mPreAction); + QWhatsThis::add(mPreAction, + i18n("Enter a shell command to execute before the alarm is displayed.\n" + "Note that it is executed only when the alarm proper is displayed, not when a reminder or deferred alarm is displayed.\n" + "N.B. KAlarm will wait for the command to complete before displaying the alarm.")); + topLayout->addWidget(mPreAction); + topLayout->addSpacing(KDialog::spacingHint()); + + // Post-alarm action + label = new QLabel(i18n("Post-alar&m action:"), this); + label->setFixedSize(label->sizeHint()); + topLayout->addWidget(label, 0, Qt::AlignAuto); + + mPostAction = new KLineEdit(this); + label->setBuddy(mPostAction); + QWhatsThis::add(mPostAction, + i18n("Enter a shell command to execute after the alarm window is closed.\n" + "Note that it is not executed after closing a reminder window. If you defer " + "the alarm, it is not executed until the alarm is finally acknowledged or closed.")); + topLayout->addWidget(mPostAction); +} + +void SpecialActions::setActions(const QString& pre, const QString& post) +{ + mPreAction->setText(pre); + mPostAction->setText(post); +} + +QString SpecialActions::preAction() const +{ + return mPreAction->text(); +} + +QString SpecialActions::postAction() const +{ + return mPostAction->text(); +} + +void SpecialActions::setReadOnly(bool ro) +{ + mReadOnly = ro; + mPreAction->setReadOnly(mReadOnly); + mPostAction->setReadOnly(mReadOnly); +} diff --git a/kalarm/specialactions.h b/kalarm/specialactions.h new file mode 100644 index 000000000..ba1106d04 --- /dev/null +++ b/kalarm/specialactions.h @@ -0,0 +1,96 @@ +/* + * specialactions.h - widget to specify special alarm actions + * Program: kalarm + * Copyright © 2004,2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef SPECIALACTIONS_H +#define SPECIALACTIONS_H + +#include <kdialogbase.h> +#include <qwidget.h> +#include <qpushbutton.h> + +class KLineEdit; + + +class SpecialActionsButton : public QPushButton +{ + Q_OBJECT + public: + SpecialActionsButton(const QString& caption, QWidget* parent = 0, const char* name = 0); + void setActions(const QString& pre, const QString& post); + const QString& preAction() const { return mPreAction; } + const QString& postAction() const { return mPostAction; } + virtual void setReadOnly(bool ro) { mReadOnly = ro; } + virtual bool isReadOnly() const { return mReadOnly; } + + signals: + void selected(); + + protected slots: + void slotButtonPressed(); + + private: + QString mPreAction; + QString mPostAction; + bool mReadOnly; +}; + + +// Pre- and post-alarm actions widget +class SpecialActions : public QWidget +{ + Q_OBJECT + public: + SpecialActions(QWidget* parent = 0, const char* name = 0); + void setActions(const QString& pre, const QString& post); + QString preAction() const; + QString postAction() const; + void setReadOnly(bool); + bool isReadOnly() const { return mReadOnly; } + + private: + KLineEdit* mPreAction; + KLineEdit* mPostAction; + bool mReadOnly; +}; + + +// Pre- and post-alarm actions dialogue displayed by the push button +class SpecialActionsDlg : public KDialogBase +{ + Q_OBJECT + public: + SpecialActionsDlg(const QString& preAction, const QString& postAction, + const QString& caption, QWidget* parent = 0, const char* name = 0); + QString preAction() const { return mActions->preAction(); } + QString postAction() const { return mActions->postAction(); } + void setReadOnly(bool ro) { mActions->setReadOnly(ro); } + bool isReadOnly() const { return mActions->isReadOnly(); } + + protected: + virtual void resizeEvent(QResizeEvent*); + + protected slots: + virtual void slotOk(); + + private: + SpecialActions* mActions; +}; + +#endif // SPECIALACTIONS_H diff --git a/kalarm/startdaytimer.cpp b/kalarm/startdaytimer.cpp new file mode 100644 index 000000000..41ee3bc83 --- /dev/null +++ b/kalarm/startdaytimer.cpp @@ -0,0 +1,49 @@ +/* + * startdaytimer.cpp - timer triggered at the user-defined start-of-day time + * Program: kalarm + * Copyright (C) 2004, 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include "preferences.h" +#include "startdaytimer.moc" + + +StartOfDayTimer* StartOfDayTimer::mInstance = 0; + +StartOfDayTimer::StartOfDayTimer() + : DailyTimer(Preferences::startOfDay(), false) +{ + Preferences::connect(SIGNAL(startOfDayChanged(const QTime&)), this, SLOT(startOfDayChanged(const QTime&))); +} + +StartOfDayTimer* StartOfDayTimer::instance() +{ + if (!mInstance) + mInstance = new StartOfDayTimer; // receive notifications of change of start-of-day time + return mInstance; +} + +/****************************************************************************** +* Called when the start-of-day time has changed. +* The timer is adjusted and if appropriate timer events are triggered now. +*/ +void StartOfDayTimer::startOfDayChanged(const QTime&) +{ + changeTime(Preferences::startOfDay(), true); +} diff --git a/kalarm/startdaytimer.h b/kalarm/startdaytimer.h new file mode 100644 index 000000000..52f50d8cc --- /dev/null +++ b/kalarm/startdaytimer.h @@ -0,0 +1,66 @@ +/* + * startdaytimer.h - timer triggered at the user-defined start-of-day time + * Program: kalarm + * Copyright © 2004,2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef STARTDAYTIMER_H +#define STARTDAYTIMER_H + +/* @file startdaytimer.h - timer triggered at the user-defined start-of-day time */ + +#include "synchtimer.h" + + +/** StartOfDayTimer is an application-wide timer synchronised to the user-defined + * start-of-day time (set in KAlarm's Preferences dialog). + * It automatically adjusts to any changes in the start-of-day time. + * + * @author David Jarvie <software@astrojar.org.uk> + */ +class StartOfDayTimer : public DailyTimer +{ + Q_OBJECT + public: + virtual ~StartOfDayTimer() { } + /** Connect to the timer signal. + * @param receiver Receiving object. + * @param member Slot to activate. + */ + static void connect(QObject* receiver, const char* member) + { instance()->connecT(receiver, member); } + /** Disconnect from the timer signal. + * @param receiver Receiving object. + * @param member Slot to disconnect. If null, all slots belonging to + * @p receiver will be disconnected. + */ + static void disconnect(QObject* receiver, const char* member = 0) + { if (mInstance) mInstance->disconnecT(receiver, member); } + + protected: + StartOfDayTimer(); + static StartOfDayTimer* instance(); + + private slots: + void startOfDayChanged(const QTime& oldTime); + + private: + static StartOfDayTimer* mInstance; // exists solely to receive signals +}; + +#endif // STARTDAYTIMER_H + diff --git a/kalarm/templatedlg.cpp b/kalarm/templatedlg.cpp new file mode 100644 index 000000000..917c1ea9f --- /dev/null +++ b/kalarm/templatedlg.cpp @@ -0,0 +1,241 @@ +/* + * templatedlg.cpp - dialogue to create, edit and delete alarm templates + * Program: kalarm + * Copyright © 2004-2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qlayout.h> +#include <qpushbutton.h> +#include <qwhatsthis.h> + +#include <klocale.h> +#include <kguiitem.h> +#include <kmessagebox.h> +#include <kaccel.h> +#include <kdebug.h> + +#include "editdlg.h" +#include "alarmcalendar.h" +#include "functions.h" +#include "templatelistview.h" +#include "undo.h" +#include "templatedlg.moc" + +static const char TMPL_DIALOG_NAME[] = "TemplateDialog"; + + +TemplateDlg* TemplateDlg::mInstance = 0; + + +TemplateDlg::TemplateDlg(QWidget* parent, const char* name) + : KDialogBase(KDialogBase::Plain, i18n("Alarm Templates"), Close, Ok, parent, name, false, true) +{ + QWidget* topWidget = plainPage(); + QBoxLayout* topLayout = new QHBoxLayout(topWidget); + topLayout->setSpacing(spacingHint()); + + QBoxLayout* layout = new QVBoxLayout(topLayout); + mTemplateList = new TemplateListView(true, i18n("The list of alarm templates"), topWidget); + mTemplateList->setSelectionMode(QListView::Extended); + mTemplateList->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding)); + connect(mTemplateList, SIGNAL(selectionChanged()), SLOT(slotSelectionChanged())); + layout->addWidget(mTemplateList); + + layout = new QVBoxLayout(topLayout); + QPushButton* button = new QPushButton(i18n("&New..."), topWidget); + connect(button, SIGNAL(clicked()), SLOT(slotNew())); + QWhatsThis::add(button, i18n("Create a new alarm template")); + layout->addWidget(button); + + mEditButton = new QPushButton(i18n("&Edit..."), topWidget); + connect(mEditButton, SIGNAL(clicked()), SLOT(slotEdit())); + QWhatsThis::add(mEditButton, i18n("Edit the currently highlighted alarm template")); + layout->addWidget(mEditButton); + + mCopyButton = new QPushButton(i18n("Co&py"), topWidget); + connect(mCopyButton, SIGNAL(clicked()), SLOT(slotCopy())); + QWhatsThis::add(mCopyButton, + i18n("Create a new alarm template based on a copy of the currently highlighted template")); + layout->addWidget(mCopyButton); + + mDeleteButton = new QPushButton(i18n("&Delete"), topWidget); + connect(mDeleteButton, SIGNAL(clicked()), SLOT(slotDelete())); + QWhatsThis::add(mDeleteButton, i18n("Delete the currently highlighted alarm template")); + layout->addWidget(mDeleteButton); + + KAccel* accel = new KAccel(this); + accel->insert(KStdAccel::SelectAll, mTemplateList, SLOT(slotSelectAll())); + accel->insert(KStdAccel::Deselect, mTemplateList, SLOT(slotDeselect())); + accel->readSettings(); + + mTemplateList->refresh(); + slotSelectionChanged(); // enable/disable buttons as appropriate + + QSize s; + if (KAlarm::readConfigWindowSize(TMPL_DIALOG_NAME, s)) + resize(s); +} + +/****************************************************************************** +* Destructor. +*/ +TemplateDlg::~TemplateDlg() +{ + mInstance = 0; +} + +/****************************************************************************** +* Create an instance, if none already exists. +*/ +TemplateDlg* TemplateDlg::create(QWidget* parent, const char* name) +{ + if (mInstance) + return 0; + mInstance = new TemplateDlg(parent, name); + return mInstance; +} + +/****************************************************************************** +* Called when the New Template button is clicked to create a new template +* based on the currently selected alarm. +*/ +void TemplateDlg::slotNew() +{ + createTemplate(0, this, mTemplateList); +} + +/****************************************************************************** +* Called when the Copy button is clicked to edit a copy of an existing alarm, +* to add to the list. +*/ +void TemplateDlg::slotCopy() +{ + TemplateListViewItem* item = mTemplateList->selectedItem(); + if (item) + { + KAEvent event = item->event(); + createTemplate(&event, mTemplateList); + } +} + +/****************************************************************************** +* Create a new template. +* If 'event' is non-zero, base the new template on an existing event or template. +*/ +void TemplateDlg::createTemplate(const KAEvent* event, QWidget* parent, TemplateListView* view) +{ + EditAlarmDlg editDlg(true, i18n("New Alarm Template"), parent, 0, event); + if (editDlg.exec() == QDialog::Accepted) + { + KAEvent event; + editDlg.getEvent(event); + + // Add the template to the displayed lists and to the calendar file + KAlarm::addTemplate(event, view, &editDlg); + Undo::saveAdd(event); + } +} + +/****************************************************************************** +* Called when the Modify button is clicked to edit the currently highlighted +* alarm in the list. +*/ +void TemplateDlg::slotEdit() +{ + TemplateListViewItem* item = mTemplateList->selectedItem(); + if (item) + { + KAEvent event = item->event(); + EditAlarmDlg editDlg(true, i18n("Edit Alarm Template"), this, 0, &event); + if (editDlg.exec() == QDialog::Accepted) + { + KAEvent newEvent; + editDlg.getEvent(newEvent); + QString id = event.id(); + newEvent.setEventID(id); + + // Update the event in the displays and in the calendar file + KAlarm::updateTemplate(newEvent, mTemplateList, &editDlg); + Undo::saveEdit(event, newEvent); + } + } +} + +/****************************************************************************** +* Called when the Delete button is clicked to delete the currently highlighted +* alarms in the list. +*/ +void TemplateDlg::slotDelete() +{ + QValueList<EventListViewItemBase*> items = mTemplateList->selectedItems(); + int n = items.count(); + if (KMessageBox::warningContinueCancel(this, i18n("Do you really want to delete the selected alarm template?", + "Do you really want to delete the %n selected alarm templates?", n), + i18n("Delete Alarm Template", "Delete Alarm Templates", n), KGuiItem(i18n("&Delete"), "editdelete")) + != KMessageBox::Continue) + return; + + int warnErr = 0; + KAlarm::UpdateStatus status = KAlarm::UPDATE_OK; + QValueList<KAEvent> events; + AlarmCalendar::templateCalendar()->startUpdate(); // prevent multiple saves of the calendar until we're finished + for (QValueList<EventListViewItemBase*>::Iterator it = items.begin(); it != items.end(); ++it) + { + TemplateListViewItem* item = (TemplateListViewItem*)(*it); + events.append(item->event()); + KAlarm::UpdateStatus st = KAlarm::deleteTemplate(item->event()); + if (st != KAlarm::UPDATE_OK) + { + status = st; + ++warnErr; + } + } + if (!AlarmCalendar::templateCalendar()->endUpdate()) // save the calendar now + { + status = KAlarm::SAVE_FAILED; + warnErr = items.count(); + } + Undo::saveDeletes(events); + if (warnErr) + displayUpdateError(this, status, KAlarm::ERR_TEMPLATE, warnErr); +} + +/****************************************************************************** +* Called when the group of items selected changes. +* Enable/disable the buttons depending on whether/how many templates are +* currently highlighted. +*/ +void TemplateDlg::slotSelectionChanged() +{ + int count = mTemplateList->selectedCount(); + mEditButton->setEnabled(count == 1); + mCopyButton->setEnabled(count == 1); + mDeleteButton->setEnabled(count); +} + +/****************************************************************************** +* Called when the dialog's size has changed. +* Records the new size in the config file. +*/ +void TemplateDlg::resizeEvent(QResizeEvent* re) +{ + if (isVisible()) + KAlarm::writeConfigWindowSize(TMPL_DIALOG_NAME, re->size()); + KDialog::resizeEvent(re); +} diff --git a/kalarm/templatedlg.h b/kalarm/templatedlg.h new file mode 100644 index 000000000..c93359861 --- /dev/null +++ b/kalarm/templatedlg.h @@ -0,0 +1,62 @@ +/* + * templatedlg.h - dialogue to create, edit and delete alarm templates + * Program: kalarm + * Copyright (C) 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#ifndef TEMPLATEDLG_H +#define TEMPLATEDLG_H + +#include <kdialogbase.h> + +class QPushButton; +class TemplateListView; +class KAEvent; + + +class TemplateDlg : public KDialogBase +{ + Q_OBJECT + public: + static TemplateDlg* create(QWidget* parent = 0, const char* name = 0); + ~TemplateDlg(); + static void createTemplate(const KAEvent* = 0, QWidget* parent = 0, TemplateListView* = 0); + + signals: + void emptyToggled(bool notEmpty); + + protected: + virtual void resizeEvent(QResizeEvent*); + + private slots: + void slotNew(); + void slotCopy(); + void slotEdit(); + void slotDelete(); + void slotSelectionChanged(); + + private: + TemplateDlg(QWidget* parent, const char* name); + + static TemplateDlg* mInstance; // the current instance, to prevent multiple dialogues + + TemplateListView* mTemplateList; + QPushButton* mEditButton; + QPushButton* mCopyButton; + QPushButton* mDeleteButton; +}; + +#endif // TEMPLATEDLG_H diff --git a/kalarm/templatelistview.cpp b/kalarm/templatelistview.cpp new file mode 100644 index 000000000..bf1e00850 --- /dev/null +++ b/kalarm/templatelistview.cpp @@ -0,0 +1,131 @@ +/* + * templatelistview.cpp - widget showing list of alarm templates + * Program: kalarm + * Copyright (C) 2004, 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <klocale.h> +#include <kdebug.h> + +#include "alarmcalendar.h" +#include "functions.h" +#include "templatelistview.moc" + + +/*============================================================================= += Class: TemplateListView += Displays the list of outstanding alarms. +=============================================================================*/ +QValueList<EventListViewBase*> TemplateListView::mInstanceList; + + +TemplateListView::TemplateListView(bool includeCmdAlarms, const QString& whatsThisText, QWidget* parent, const char* name) + : EventListViewBase(parent, name), + mWhatsThisText(whatsThisText), + mIconColumn(0), + mNameColumn(1), + mExcludeCmdAlarms(!includeCmdAlarms) +{ + addColumn(QString::null); // icon column + addLastColumn(i18n("Name")); + setSorting(mNameColumn); // sort initially by name + setColumnAlignment(mIconColumn, Qt::AlignHCenter); + setColumnWidthMode(mIconColumn, QListView::Maximum); + + mInstanceList.append(this); +} + +TemplateListView::~TemplateListView() +{ + mInstanceList.remove(this); +} + +/****************************************************************************** +* Add all the templates to the list. +*/ +void TemplateListView::populate() +{ + QValueList<KAEvent> templates = KAlarm::templateList(); + for (QValueList<KAEvent>::Iterator it = templates.begin(); it != templates.end(); ++it) + addEntry(*it); +} + +/****************************************************************************** +* Create a new list item for addEntry(). +*/ +EventListViewItemBase* TemplateListView::createItem(const KAEvent& event) +{ + return new TemplateListViewItem(this, event); +} + +/****************************************************************************** +* Returns the QWhatsThis text for a specified column. +*/ +QString TemplateListView::whatsThisText(int column) const +{ + if (column == mIconColumn) + return i18n("Alarm type"); + if (column == mNameColumn) + return i18n("Name of the alarm template"); + return mWhatsThisText; +} + + +/*============================================================================= += Class: TemplateListViewItem += Contains the details of one alarm for display in the TemplateListView. +=============================================================================*/ + +TemplateListViewItem::TemplateListViewItem(TemplateListView* parent, const KAEvent& event) + : EventListViewItemBase(parent, event) +{ + setLastColumnText(); // set the template name column text + + int index; + switch (event.action()) + { + case KAAlarm::FILE: index = 2; break; + case KAAlarm::COMMAND: index = 3; break; + case KAAlarm::EMAIL: index = 4; break; + case KAAlarm::MESSAGE: + default: index = 1; break; + } + mIconOrder.sprintf("%01u", index); + setPixmap(templateListView()->iconColumn(), *eventIcon()); +} + +/****************************************************************************** +* Return the alarm summary text. +*/ +QString TemplateListViewItem::lastColumnText() const +{ + return event().templateName(); +} + +/****************************************************************************** +* Return the column sort order for one item in the list. +*/ +QString TemplateListViewItem::key(int column, bool) const +{ + TemplateListView* listView = templateListView(); + if (column == listView->iconColumn()) + return mIconOrder; + return text(column).lower(); +} + diff --git a/kalarm/templatelistview.h b/kalarm/templatelistview.h new file mode 100644 index 000000000..f420641b9 --- /dev/null +++ b/kalarm/templatelistview.h @@ -0,0 +1,88 @@ +/* + * templatelistview.h - widget showing list of alarm templates + * Program: kalarm + * Copyright (C) 2004, 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TEMPLATELISTVIEW_H +#define TEMPLATELISTVIEW_H + +#include "kalarm.h" + +#include <qmap.h> +#include <klistview.h> + +#include "eventlistviewbase.h" + +class TemplateListView; + + +class TemplateListViewItem : public EventListViewItemBase +{ + public: + TemplateListViewItem(TemplateListView* parent, const KAEvent&); + TemplateListView* templateListView() const { return (TemplateListView*)listView(); } + // Overridden base class methods + TemplateListViewItem* nextSibling() const { return (TemplateListViewItem*)QListViewItem::nextSibling(); } + virtual QString key(int column, bool ascending) const; + protected: + virtual QString lastColumnText() const; + private: + QString mIconOrder; // controls ordering of icon column +}; + + +class TemplateListView : public EventListViewBase +{ + Q_OBJECT + public: + explicit TemplateListView(bool includeCmdAlarms, const QString& whatsThisText, QWidget* parent = 0, const char* name = 0); + ~TemplateListView(); + int iconColumn() const { return mIconColumn; } + int nameColumn() const { return mNameColumn; } + // Overridden base class methods + static void addEvent(const KAEvent& e, EventListViewBase* v) + { EventListViewBase::addEvent(e, mInstanceList, v); } + static void modifyEvent(const KAEvent& e, EventListViewBase* v) + { EventListViewBase::modifyEvent(e.id(), e, mInstanceList, v); } + static void modifyEvent(const QString& oldEventID, const KAEvent& newEvent, EventListViewBase* v) + { EventListViewBase::modifyEvent(oldEventID, newEvent, mInstanceList, v); } + static void deleteEvent(const QString& eventID) + { EventListViewBase::deleteEvent(eventID, mInstanceList); } + TemplateListViewItem* getEntry(const QString& eventID) { return (TemplateListViewItem*)EventListViewBase::getEntry(eventID); } + TemplateListViewItem* selectedItem() const { return (TemplateListViewItem*)EventListViewBase::selectedItem(); } + TemplateListViewItem* currentItem() const { return (TemplateListViewItem*)EventListViewBase::currentItem(); } + TemplateListViewItem* firstChild() const { return (TemplateListViewItem*)EventListViewBase::firstChild(); } + virtual void setSelected(QListViewItem* item, bool selected) { EventListViewBase::setSelected(item, selected); } + virtual void setSelected(TemplateListViewItem* item, bool selected) { EventListViewBase::setSelected(item, selected); } + virtual QValueList<EventListViewBase*> instances() { return mInstanceList; } + + protected: + virtual void populate(); + EventListViewItemBase* createItem(const KAEvent&); + virtual QString whatsThisText(int column) const; + + private: + static QValueList<EventListViewBase*> mInstanceList; + QString mWhatsThisText; // default QWhatsThis text + int mIconColumn; // index to icon column + int mNameColumn; // index to template name column + bool mExcludeCmdAlarms; // omit command alarms from the list +}; + +#endif // TEMPLATELISTVIEW_H + diff --git a/kalarm/templatemenuaction.cpp b/kalarm/templatemenuaction.cpp new file mode 100644 index 000000000..dae3bc127 --- /dev/null +++ b/kalarm/templatemenuaction.cpp @@ -0,0 +1,84 @@ +/* + * templatemenuaction.cpp - menu action to select a template + * Program: kalarm + * Copyright © 2005,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <kactionclasses.h> +#include <kpopupmenu.h> +#include <kdebug.h> + +#include "alarmcalendar.h" +#include "alarmevent.h" +#include "functions.h" +#include "templatemenuaction.moc" + + +TemplateMenuAction::TemplateMenuAction(const QString& label, const QString& icon, QObject* receiver, + const char* slot, KActionCollection* actions, const char* name) + : KActionMenu(label, icon, actions, name) +{ + setDelayed(false); + connect(popupMenu(), SIGNAL(aboutToShow()), SLOT(slotInitMenu())); + connect(popupMenu(), SIGNAL(activated(int)), SLOT(slotSelected(int))); + connect(this, SIGNAL(selected(const KAEvent&)), receiver, slot); +} + +/****************************************************************************** +* Called when the New From Template action is clicked. +* Creates a popup menu listing all alarm templates, in sorted name order. +*/ +void TemplateMenuAction::slotInitMenu() +{ + KPopupMenu* menu = popupMenu(); + menu->clear(); + mOriginalTexts.clear(); + QValueList<KAEvent> templates = KAlarm::templateList(); + for (QValueList<KAEvent>::ConstIterator it = templates.constBegin(); it != templates.constEnd(); ++it) + { + QString name = (*it).templateName(); + // Insert the template in sorted order + QStringList::Iterator tit; + for (tit = mOriginalTexts.begin(); + tit != mOriginalTexts.end() && QString::localeAwareCompare(name, *tit) > 0; + ++tit); + mOriginalTexts.insert(tit, name); + } + for (QStringList::ConstIterator tit = mOriginalTexts.constBegin(); tit != mOriginalTexts.constEnd(); ++tit) + menu->insertItem(*tit); +} + +/****************************************************************************** +* Called when a template is selected from the New From Template popup menu. +* Executes a New Alarm dialog, preset from the selected template. +*/ +void TemplateMenuAction::slotSelected(int id) +{ + KPopupMenu* menu = popupMenu(); + QString item = mOriginalTexts[menu->indexOf(id)]; + if (!item.isEmpty()) + { + AlarmCalendar* cal = AlarmCalendar::templateCalendarOpen(); + if (cal) + { + KAEvent templ = KAEvent::findTemplateName(*cal, item); + emit selected(templ); + } + } +} diff --git a/kalarm/templatemenuaction.h b/kalarm/templatemenuaction.h new file mode 100644 index 000000000..20d9f30a1 --- /dev/null +++ b/kalarm/templatemenuaction.h @@ -0,0 +1,46 @@ +/* + * templatemenuaction.h - menu action to select a template + * Program: kalarm + * Copyright (C) 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TEMPLATEMENUACTION_H +#define TEMPLATEMENUACTION_H + +#include <kactionclasses.h> +class KAEvent; + + +class TemplateMenuAction : public KActionMenu +{ + Q_OBJECT + public: + TemplateMenuAction(const QString& label, const QString& icon, QObject* receiver, + const char* slot, KActionCollection* parent, const char* name = 0); + + signals: + void selected(const KAEvent&); + + private slots: + void slotInitMenu(); + void slotSelected(int id); + + private: + QStringList mOriginalTexts; // menu item texts without added ampersands +}; + +#endif // TEMPLATEMENUACTION_H diff --git a/kalarm/templatepickdlg.cpp b/kalarm/templatepickdlg.cpp new file mode 100644 index 000000000..50ec2b6e9 --- /dev/null +++ b/kalarm/templatepickdlg.cpp @@ -0,0 +1,87 @@ +/* + * templatepickdlg.cpp - dialogue to choose an alarm template + * Program: kalarm + * Copyright © 2004,2008 by David Jarvie <djarvie@kde.org> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qlayout.h> +#include <qwhatsthis.h> + +#include <klocale.h> +#include <kdebug.h> + +#include "functions.h" +#include "shellprocess.h" +#include "templatelistview.h" +#include "templatepickdlg.moc" + +static const char TMPL_PICK_DIALOG_NAME[] = "TemplatePickDialog"; + + +TemplatePickDlg::TemplatePickDlg(QWidget* parent, const char* name) + : KDialogBase(KDialogBase::Plain, i18n("Choose Alarm Template"), Ok|Cancel, Ok, parent, name) +{ + QWidget* topWidget = plainPage(); + QBoxLayout* topLayout = new QVBoxLayout(topWidget); + topLayout->setSpacing(spacingHint()); + + // Display the list of templates, but exclude command alarms if in kiosk mode. + bool includeCmdAlarms = ShellProcess::authorised(); + mTemplateList = new TemplateListView(includeCmdAlarms, i18n("Select a template to base the new alarm on."), topWidget, "list"); + mTemplateList->setSelectionMode(QListView::Single); + mTemplateList->refresh(); // populate the template list + connect(mTemplateList, SIGNAL(selectionChanged()), SLOT(slotSelectionChanged())); + // Require a real double click (even if KDE is in single-click mode) to accept the selection + connect(mTemplateList, SIGNAL(doubleClicked(QListViewItem*, const QPoint&, int)), SLOT(slotOk())); + topLayout->addWidget(mTemplateList); + + slotSelectionChanged(); // enable or disable the OK button + + QSize s; + if (KAlarm::readConfigWindowSize(TMPL_PICK_DIALOG_NAME, s)) + resize(s); +} + +/****************************************************************************** +* Return the currently selected alarm template, or 0 if none. +*/ +const KAEvent* TemplatePickDlg::selectedTemplate() const +{ + return mTemplateList->selectedEvent(); +} + +/****************************************************************************** +* Called when the template selection changes. +* Enable/disable the OK button depending on whether anything is selected. +*/ +void TemplatePickDlg::slotSelectionChanged() +{ + enableButtonOK(mTemplateList->selectedItem()); +} + +/****************************************************************************** +* Called when the dialog's size has changed. +* Records the new size in the config file. +*/ +void TemplatePickDlg::resizeEvent(QResizeEvent* re) +{ + if (isVisible()) + KAlarm::writeConfigWindowSize(TMPL_PICK_DIALOG_NAME, re->size()); + KDialog::resizeEvent(re); +} diff --git a/kalarm/templatepickdlg.h b/kalarm/templatepickdlg.h new file mode 100644 index 000000000..9bc97bfae --- /dev/null +++ b/kalarm/templatepickdlg.h @@ -0,0 +1,43 @@ +/* + * templatepickdlg.h - dialogue to choose an alarm template + * Program: kalarm + * Copyright (C) 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#ifndef TEMPLATEPICKDLG_H +#define TEMPLATEPICKDLG_H + +#include <kdialogbase.h> + +class TemplateListView; +class KAEvent; + + +class TemplatePickDlg : public KDialogBase +{ + Q_OBJECT + public: + TemplatePickDlg(QWidget* parent = 0, const char* name = 0); + const KAEvent* selectedTemplate() const; + protected: + virtual void resizeEvent(QResizeEvent*); + private slots: + void slotSelectionChanged(); + private: + TemplateListView* mTemplateList; +}; + +#endif // TEMPLATEPICKDLG_H diff --git a/kalarm/timeselector.cpp b/kalarm/timeselector.cpp new file mode 100644 index 000000000..0bfc142de --- /dev/null +++ b/kalarm/timeselector.cpp @@ -0,0 +1,159 @@ +/* + * timeselector.cpp - widget to optionally set a time period + * Program: kalarm + * Copyright (C) 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qlayout.h> +#include <qlabel.h> +#include <qhbox.h> +#include <qwhatsthis.h> + +#include <klocale.h> +#include <kdialog.h> +#include <kdebug.h> + +#include "checkbox.h" +#include "timeselector.moc" + + +TimeSelector::TimeSelector(const QString& selectText, const QString& postfix, const QString& selectWhatsThis, + const QString& valueWhatsThis, bool allowHourMinute, QWidget* parent, const char* name) + : QFrame(parent, name), + mLabel(0), + mReadOnly(false) +{ + setFrameStyle(QFrame::NoFrame); + QVBoxLayout* topLayout = new QVBoxLayout(this, 0, KDialog::spacingHint()); + QHBoxLayout* layout = new QHBoxLayout(topLayout, KDialog::spacingHint()); + mSelect = new CheckBox(selectText, this); + mSelect->setFixedSize(mSelect->sizeHint()); + connect(mSelect, SIGNAL(toggled(bool)), SLOT(selectToggled(bool))); + QWhatsThis::add(mSelect, selectWhatsThis); + layout->addWidget(mSelect); + + QHBox* box = new QHBox(this); // to group widgets for QWhatsThis text + box->setSpacing(KDialog::spacingHint()); + layout->addWidget(box); + mPeriod = new TimePeriod(allowHourMinute, box); + mPeriod->setFixedSize(mPeriod->sizeHint()); + mPeriod->setSelectOnStep(false); + connect(mPeriod, SIGNAL(valueChanged(int)), SLOT(periodChanged(int))); + mSelect->setFocusWidget(mPeriod); + mPeriod->setEnabled(false); + + if (!postfix.isEmpty()) + { + mLabel = new QLabel(postfix, box); + QWhatsThis::add(box, valueWhatsThis); + mLabel->setEnabled(false); + } +} + +/****************************************************************************** +* Set the read-only status. +*/ +void TimeSelector::setReadOnly(bool ro) +{ + if ((int)ro != (int)mReadOnly) + { + mReadOnly = ro; + mSelect->setReadOnly(mReadOnly); + mPeriod->setReadOnly(mReadOnly); + } +} + +bool TimeSelector::isChecked() const +{ + return mSelect->isChecked(); +} + +void TimeSelector::setChecked(bool on) +{ + if (on != mSelect->isChecked()) + { + mSelect->setChecked(on); + emit valueChanged(minutes()); + } +} + +void TimeSelector::setMaximum(int hourmin, int days) +{ + mPeriod->setMaximum(hourmin, days); +} + +void TimeSelector::setDateOnly(bool dateOnly) +{ + mPeriod->setDateOnly(dateOnly); +} + +/****************************************************************************** + * Get the specified number of minutes. + * Reply = 0 if unselected. + */ +int TimeSelector::minutes() const +{ + return mSelect->isChecked() ? mPeriod->minutes() : 0; +} + +/****************************************************************************** +* Initialise the controls with a specified time period. +* If minutes = 0, it will be deselected. +* The time unit combo-box is initialised to 'defaultUnits', but if 'dateOnly' +* is true, it will never be initialised to hours/minutes. +*/ +void TimeSelector::setMinutes(int minutes, bool dateOnly, TimePeriod::Units defaultUnits) +{ + mSelect->setChecked(minutes); + mPeriod->setEnabled(minutes); + if (mLabel) + mLabel->setEnabled(minutes); + mPeriod->setMinutes(minutes, dateOnly, defaultUnits); +} + +/****************************************************************************** +* Set the input focus on the count field. +*/ +void TimeSelector::setFocusOnCount() +{ + mPeriod->setFocusOnCount(); +} + +/****************************************************************************** +* Called when the TimeSelector checkbox is toggled. +*/ +void TimeSelector::selectToggled(bool on) +{ + mPeriod->setEnabled(on); + if (mLabel) + mLabel->setEnabled(on); + if (on) + mPeriod->setFocus(); + emit toggled(on); + emit valueChanged(minutes()); +} + +/****************************************************************************** +* Called when the period value changes. +*/ +void TimeSelector::periodChanged(int minutes) +{ + if (mSelect->isChecked()) + emit valueChanged(minutes); +} diff --git a/kalarm/timeselector.h b/kalarm/timeselector.h new file mode 100644 index 000000000..9ba66c098 --- /dev/null +++ b/kalarm/timeselector.h @@ -0,0 +1,61 @@ +/* + * timeselector.h - widget to optionally set a time period + * Program: kalarm + * Copyright (C) 2004 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TIMESELECTOR_H +#define TIMESELECTOR_H + +#include <qframe.h> +#include "timeperiod.h" + +class QLabel; +class CheckBox; + + +class TimeSelector : public QFrame +{ + Q_OBJECT + public: + TimeSelector(const QString& selectText, const QString& postfix, const QString& selectWhatsThis, + const QString& valueWhatsThis, bool allowHourMinute, QWidget* parent, const char* name = 0); + bool isChecked() const; + void setChecked(bool on); + int minutes() const; + void setMinutes(int minutes, bool dateOnly, TimePeriod::Units defaultUnits); + void setReadOnly(bool); + void setDateOnly(bool dateOnly = true); + void setMaximum(int hourmin, int days); + void setFocusOnCount(); + + signals: + void toggled(bool); // selection checkbox has been toggled + void valueChanged(int minutes); // value has changed + + protected slots: + void selectToggled(bool); + void periodChanged(int minutes); + + private: + CheckBox* mSelect; + TimePeriod* mPeriod; + QLabel* mLabel; + bool mReadOnly; // the widget is read only +}; + +#endif // TIMESELECTOR_H diff --git a/kalarm/traywindow.cpp b/kalarm/traywindow.cpp new file mode 100644 index 000000000..e968e02ba --- /dev/null +++ b/kalarm/traywindow.cpp @@ -0,0 +1,361 @@ +/* + * traywindow.cpp - the KDE system tray applet + * Program: kalarm + * Copyright © 2002-2005,2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <stdlib.h> + +#include <qtooltip.h> + +#include <kapplication.h> +#include <klocale.h> +#include <kaboutdata.h> +#include <kpopupmenu.h> +#include <kmessagebox.h> +#include <kstandarddirs.h> +#include <kstdaction.h> +#include <kstdguiitem.h> +#include <kaccel.h> +#include <kconfig.h> +#include <kdebug.h> + +#include "alarmcalendar.h" +#include "alarmlistview.h" +#include "alarmtext.h" +#include "daemon.h" +#include "functions.h" +#include "kalarmapp.h" +#include "mainwindow.h" +#include "messagewin.h" +#include "prefdlg.h" +#include "preferences.h" +#include "templatemenuaction.h" +#include "traywindow.moc" + + +class TrayTooltip : public QToolTip +{ + public: + TrayTooltip(QWidget* parent) : QToolTip(parent) { } + virtual ~TrayTooltip() {} + protected: + virtual void maybeTip(const QPoint&); +}; + +struct TipItem +{ + QDateTime dateTime; + QString text; +}; + + +/*============================================================================= += Class: TrayWindow += The KDE system tray window. +=============================================================================*/ + +TrayWindow::TrayWindow(MainWindow* parent, const char* name) + : KSystemTray((theApp()->wantRunInSystemTray() ? parent : 0), name), + mAssocMainWindow(parent) +{ + kdDebug(5950) << "TrayWindow::TrayWindow()\n"; + // Set up GUI icons + mPixmapEnabled = loadIcon("kalarm"); + mPixmapDisabled = loadIcon("kalarm_disabled"); + if (mPixmapEnabled.isNull() || mPixmapDisabled.isNull()) + KMessageBox::sorry(this, i18n("Cannot load system tray icon.")); + setAcceptDrops(true); // allow drag-and-drop onto this window + + // Set up the context menu + KActionCollection* actcol = actionCollection(); + AlarmEnableAction* a = Daemon::createAlarmEnableAction(actcol, "tAlarmEnable"); + a->plug(contextMenu()); + connect(a, SIGNAL(switched(bool)), SLOT(setEnabledStatus(bool))); + KAlarm::createNewAlarmAction(i18n("&New Alarm..."), this, SLOT(slotNewAlarm()), actcol, "tNew")->plug(contextMenu()); + KAlarm::createNewFromTemplateAction(i18n("New Alarm From &Template"), this, SLOT(slotNewFromTemplate(const KAEvent&)), actcol, "tNewFromTempl")->plug(contextMenu()); + KStdAction::preferences(this, SLOT(slotPreferences()), actcol)->plug(contextMenu()); + + // Replace the default handler for the Quit context menu item + const char* quitName = KStdAction::name(KStdAction::Quit); + actcol->remove(actcol->action(quitName)); + actcol->accel()->remove(quitName); + KStdAction::quit(this, SLOT(slotQuit()), actcol); + + // Set icon to correspond with the alarms enabled menu status + Daemon::checkStatus(); + setEnabledStatus(Daemon::monitoringAlarms()); + + mTooltip = new TrayTooltip(this); +} + +TrayWindow::~TrayWindow() +{ + kdDebug(5950) << "TrayWindow::~TrayWindow()\n"; + delete mTooltip; + mTooltip = 0; + theApp()->removeWindow(this); + emit deleted(); +} + +/****************************************************************************** +* Called just before the context menu is displayed. +* Update the Alarms Enabled item status. +*/ +void TrayWindow::contextMenuAboutToShow(KPopupMenu* menu) +{ + KSystemTray::contextMenuAboutToShow(menu); // needed for KDE <= 3.1 compatibility + Daemon::checkStatus(); +} + +/****************************************************************************** +* Called when the "New Alarm" menu item is selected to edit a new alarm. +*/ +void TrayWindow::slotNewAlarm() +{ + MainWindow::executeNew(); +} + +/****************************************************************************** +* Called when the "New Alarm" menu item is selected to edit a new alarm. +*/ +void TrayWindow::slotNewFromTemplate(const KAEvent& event) +{ + MainWindow::executeNew(event); +} + +/****************************************************************************** +* Called when the "Configure KAlarm" menu item is selected. +*/ +void TrayWindow::slotPreferences() +{ + KAlarmPrefDlg::display(); +} + +/****************************************************************************** +* Called when the Quit context menu item is selected. +*/ +void TrayWindow::slotQuit() +{ + theApp()->doQuit(this); +} + +/****************************************************************************** +* Called when the Alarms Enabled action status has changed. +* Updates the alarms enabled menu item check state, and the icon pixmap. +*/ +void TrayWindow::setEnabledStatus(bool status) +{ + kdDebug(5950) << "TrayWindow::setEnabledStatus(" << (int)status << ")\n"; + setPixmap(status ? mPixmapEnabled : mPixmapDisabled); +} + +/****************************************************************************** +* Called when the mouse is clicked over the panel icon. +* A left click displays the KAlarm main window. +* A middle button click displays the New Alarm window. +*/ +void TrayWindow::mousePressEvent(QMouseEvent* e) +{ + if (e->button() == LeftButton && !theApp()->wantRunInSystemTray()) + { + // Left click: display/hide the first main window + mAssocMainWindow = MainWindow::toggleWindow(mAssocMainWindow); + } + else if (e->button() == MidButton) + MainWindow::executeNew(); // display a New Alarm dialog + else + KSystemTray::mousePressEvent(e); +} + +/****************************************************************************** +* Called when the mouse is released over the panel icon. +* The main window (if not hidden) is raised and made the active window. +* If this is done in mousePressEvent(), it doesn't work. +*/ +void TrayWindow::mouseReleaseEvent(QMouseEvent* e) +{ + if (e->button() == LeftButton && mAssocMainWindow && mAssocMainWindow->isVisible()) + { + mAssocMainWindow->raise(); + mAssocMainWindow->setActiveWindow(); + } + else + KSystemTray::mouseReleaseEvent(e); +} + +/****************************************************************************** +* Called when the drag cursor enters the panel icon. +*/ +void TrayWindow::dragEnterEvent(QDragEnterEvent* e) +{ + MainWindow::executeDragEnterEvent(e); +} + +/****************************************************************************** +* Called when an object is dropped on the panel icon. +* If the object is recognised, the edit alarm dialog is opened appropriately. +*/ +void TrayWindow::dropEvent(QDropEvent* e) +{ + MainWindow::executeDropEvent(0, e); +} + +/****************************************************************************** +* Return the tooltip text showing alarms due in the next 24 hours. +* The limit of 24 hours is because only times, not dates, are displayed. +*/ +void TrayWindow::tooltipAlarmText(QString& text) const +{ + KAEvent event; + const QString& prefix = Preferences::tooltipTimeToPrefix(); + int maxCount = Preferences::tooltipAlarmCount(); + QDateTime now = QDateTime::currentDateTime(); + + // Get today's and tomorrow's alarms, sorted in time order + QValueList<TipItem> items; + QValueList<TipItem>::Iterator iit; + KCal::Event::List events = AlarmCalendar::activeCalendar()->eventsWithAlarms(now.date(), now.addDays(1)); + for (KCal::Event::List::ConstIterator it = events.begin(); it != events.end(); ++it) + { + KCal::Event* kcalEvent = *it; + event.set(*kcalEvent); + if (event.enabled() && !event.expired() && event.action() == KAEvent::MESSAGE) + { + TipItem item; + DateTime dateTime = event.displayDateTime(); + if (dateTime.date() > now.date()) + { + // Ignore alarms after tomorrow at the current clock time + if (dateTime.date() != now.date().addDays(1) + || dateTime.time() >= now.time()) + continue; + } + item.dateTime = dateTime.dateTime(); + + // The alarm is due today, or early tomorrow + bool space = false; + if (Preferences::showTooltipAlarmTime()) + { + item.text += KGlobal::locale()->formatTime(item.dateTime.time()); + item.text += ' '; + space = true; + } + if (Preferences::showTooltipTimeToAlarm()) + { + int mins = (now.secsTo(item.dateTime) + 59) / 60; + if (mins < 0) + mins = 0; + char minutes[3] = "00"; + minutes[0] = (mins%60) / 10 + '0'; + minutes[1] = (mins%60) % 10 + '0'; + if (Preferences::showTooltipAlarmTime()) + item.text += i18n("prefix + hours:minutes", "(%1%2:%3)").arg(prefix).arg(mins/60).arg(minutes); + else + item.text += i18n("prefix + hours:minutes", "%1%2:%3").arg(prefix).arg(mins/60).arg(minutes); + item.text += ' '; + space = true; + } + if (space) + item.text += ' '; + item.text += AlarmText::summary(event); + + // Insert the item into the list in time-sorted order + for (iit = items.begin(); iit != items.end(); ++iit) + { + if (item.dateTime <= (*iit).dateTime) + break; + } + items.insert(iit, item); + } + } + kdDebug(5950) << "TrayWindow::tooltipAlarmText():\n"; + int count = 0; + for (iit = items.begin(); iit != items.end(); ++iit) + { + kdDebug(5950) << "-- " << (count+1) << ") " << (*iit).text << endl; + text += '\n'; + text += (*iit).text; + if (++count == maxCount) + break; + } +} + +/****************************************************************************** +* Called when the associated main window is closed. +*/ +void TrayWindow::removeWindow(MainWindow* win) +{ + if (win == mAssocMainWindow) + mAssocMainWindow = 0; +} + + +#ifdef HAVE_X11_HEADERS + #include <X11/X.h> + #include <X11/Xlib.h> + #include <X11/Xutil.h> +#endif + +/****************************************************************************** +* Check whether the widget is in the system tray. +* Note that it is only sometime AFTER the show event that the system tray +* becomes the widget's parent. So for a definitive status, call this method +* only after waiting a bit... +* Reply = true if the widget is in the system tray, or its status can't be determined. +* = false if it is not currently in the system tray. +*/ +bool TrayWindow::inSystemTray() const +{ +#ifdef HAVE_X11_HEADERS + Window xParent; // receives parent window + Window root; + Window* children = 0; + unsigned int nchildren; + // Find the X parent window of the widget. This is not the same as the Qt parent widget. + if (!XQueryTree(qt_xdisplay(), winId(), &root, &xParent, &children, &nchildren)) + return true; // error determining its parent X window + if (children) + XFree(children); + + // If it is in the system tray, the system tray window will be its X parent. + // Otherwise, the root window will be its X parent. + return xParent != root; +#else + return true; +#endif // HAVE_X11_HEADERS +} + + +/****************************************************************************** +* Displays the appropriate tooltip depending on preference settings. +*/ +void TrayTooltip::maybeTip(const QPoint&) +{ + TrayWindow* parent = (TrayWindow*)parentWidget(); + QString text; + if (Daemon::monitoringAlarms()) + text = kapp->aboutData()->programName(); + else + text = i18n("%1 - disabled").arg(kapp->aboutData()->programName()); + kdDebug(5950) << "TrayTooltip::maybeTip(): " << text << endl; + if (Preferences::tooltipAlarmCount()) + parent->tooltipAlarmText(text); + tip(parent->rect(), text); +} diff --git a/kalarm/traywindow.h b/kalarm/traywindow.h new file mode 100644 index 000000000..e2e2d5d95 --- /dev/null +++ b/kalarm/traywindow.h @@ -0,0 +1,71 @@ +/* + * traywindow.h - the KDE system tray applet + * Program: kalarm + * Copyright © 2002-2004,2007 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TRAYWINDOW_H +#define TRAYWINDOW_H + +#include <qpixmap.h> +#include <ksystemtray.h> +class KPopupMenu; + +class KAEvent; +class MainWindow; +class TrayTooltip; + +class TrayWindow : public KSystemTray +{ + Q_OBJECT + public: + TrayWindow(MainWindow* parent, const char* name = 0); + ~TrayWindow(); + void removeWindow(MainWindow*); + MainWindow* assocMainWindow() const { return mAssocMainWindow; } + void setAssocMainWindow(MainWindow* win) { mAssocMainWindow = win; } + bool inSystemTray() const; + void tooltipAlarmText(QString& text) const; + + public slots: + void slotQuit(); + + signals: + void deleted(); + + protected: + virtual void contextMenuAboutToShow(KPopupMenu*); + virtual void mousePressEvent(QMouseEvent*); + virtual void mouseReleaseEvent(QMouseEvent*); + virtual void dragEnterEvent(QDragEnterEvent*); + virtual void dropEvent(QDropEvent*); + + private slots: + void slotNewAlarm(); + void slotNewFromTemplate(const KAEvent&); + void slotPreferences(); + void setEnabledStatus(bool status); + + private: + friend class TrayTooltip; + + MainWindow* mAssocMainWindow; // main window associated with this, or null + QPixmap mPixmapEnabled, mPixmapDisabled; + TrayTooltip* mTooltip; +}; + +#endif // TRAYWINDOW_H diff --git a/kalarm/undo.cpp b/kalarm/undo.cpp new file mode 100644 index 000000000..bab085ae3 --- /dev/null +++ b/kalarm/undo.cpp @@ -0,0 +1,1127 @@ +/* + * undo.cpp - undo/redo facility + * Program: kalarm + * Copyright © 2005,2006 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarm.h" + +#include <qobject.h> +#include <qstringlist.h> + +#include <kapplication.h> +#include <klocale.h> +#include <kmessagebox.h> +#include <kdebug.h> + +#include "alarmcalendar.h" +#include "alarmevent.h" +#include "alarmtext.h" +#include "functions.h" +#include "undo.moc" + +static int maxCount = 12; + + +class UndoItem +{ + public: + enum Operation { ADD, EDIT, DELETE, REACTIVATE, DEACTIVATE, MULTI }; + UndoItem(); // needed by QValueList + virtual ~UndoItem(); + virtual Operation operation() const = 0; + virtual QString actionText() const = 0; + virtual QString description() const { return QString::null; } + virtual QString eventID() const { return QString::null; } + virtual QString oldEventID() const { return QString::null; } + virtual QString newEventID() const { return QString::null; } + int id() const { return mId; } + Undo::Type type() const { return mType; } + void setType(Undo::Type t) { mType = t; } + KAEvent::Status calendar() const { return mCalendar; } + virtual void setCalendar(KAEvent::Status s) { mCalendar = s; } + virtual UndoItem* restore() = 0; + virtual bool deleteID(const QString& /*id*/) { return false; } + + enum Error { ERR_NONE, ERR_PROG, ERR_NOT_FOUND, ERR_CREATE, ERR_TEMPLATE, ERR_EXPIRED }; + enum Warning { WARN_NONE, WARN_KORG_ADD, WARN_KORG_MODIFY, WARN_KORG_DELETE }; + static int mLastId; + static Error mRestoreError; // error code valid only if restore() returns 0 + static Warning mRestoreWarning; // warning code set by restore() + static int mRestoreWarningCount; // item count for mRestoreWarning (to allow i18n messages to work correctly) + + protected: + UndoItem(Undo::Type); + static QString addDeleteActionText(KAEvent::Status, bool add); + QString description(const KAEvent&) const; + void replaceWith(UndoItem* item) { Undo::replace(this, item); } + + int mId; // unique identifier (only for mType = UNDO, REDO) + Undo::Type mType; // which list (if any) the object is in + KAEvent::Status mCalendar; +}; + +class UndoMultiBase : public UndoItem +{ + public: + UndoMultiBase(Undo::Type t) : UndoItem(t) { } + UndoMultiBase(Undo::Type t, Undo::List& undos) : UndoItem(t), mUndos(undos) { } + ~UndoMultiBase(); + const Undo::List& undos() const { return mUndos; } + protected: + Undo::List mUndos; // this list must always have >= 2 entries +}; + +template <class T> class UndoMulti : public UndoMultiBase +{ + public: + UndoMulti(Undo::Type, const QValueList<KAEvent>&); + UndoMulti(Undo::Type t, Undo::List& undos) : UndoMultiBase(t, undos) { } + virtual Operation operation() const { return MULTI; } + virtual UndoItem* restore(); + virtual bool deleteID(const QString& id); + virtual UndoItem* createRedo(Undo::List&) = 0; +}; + +class UndoAdd : public UndoItem +{ + public: + UndoAdd(Undo::Type, const KAEvent&); + UndoAdd(Undo::Type, const KAEvent&, KAEvent::Status); + virtual Operation operation() const { return ADD; } + virtual QString actionText() const; + virtual QString description() const { return mDescription; } + virtual QString eventID() const { return mEventID; } + virtual QString newEventID() const { return mEventID; } + virtual UndoItem* restore() { return doRestore(); } + protected: + UndoItem* doRestore(bool setArchive = false); + virtual UndoItem* createRedo(const KAEvent&); + private: + QString mEventID; + QString mDescription; +}; + +class UndoEdit : public UndoItem +{ + public: + UndoEdit(Undo::Type, const KAEvent& oldEvent, const QString& newEventID, const QString& description); + ~UndoEdit(); + virtual Operation operation() const { return EDIT; } + virtual QString actionText() const; + virtual QString description() const { return mDescription; } + virtual QString eventID() const { return mNewEventID; } + virtual QString oldEventID() const { return mOldEvent->id(); } + virtual QString newEventID() const { return mNewEventID; } + virtual UndoItem* restore(); + private: + KAEvent* mOldEvent; + QString mNewEventID; + QString mDescription; +}; + +class UndoDelete : public UndoItem +{ + public: + UndoDelete(Undo::Type, const KAEvent&); + ~UndoDelete(); + virtual Operation operation() const { return DELETE; } + virtual QString actionText() const; + virtual QString description() const { return UndoItem::description(*mEvent); } + virtual QString eventID() const { return mEvent->id(); } + virtual QString oldEventID() const { return mEvent->id(); } + virtual UndoItem* restore(); + KAEvent* event() const { return mEvent; } + protected: + virtual UndoItem* createRedo(const KAEvent&); + private: + KAEvent* mEvent; +}; + +class UndoReactivate : public UndoAdd +{ + public: + UndoReactivate(Undo::Type t, const KAEvent& e) : UndoAdd(t, e, KAEvent::ACTIVE) { } + virtual Operation operation() const { return REACTIVATE; } + virtual QString actionText() const; + virtual UndoItem* restore(); + protected: + virtual UndoItem* createRedo(const KAEvent&); +}; + +class UndoDeactivate : public UndoDelete +{ + public: + UndoDeactivate(Undo::Type t, const KAEvent& e) : UndoDelete(t, e) { } + virtual Operation operation() const { return DEACTIVATE; } + virtual QString actionText() const; + virtual UndoItem* restore(); + protected: + virtual UndoItem* createRedo(const KAEvent&); +}; + +class UndoDeletes : public UndoMulti<UndoDelete> +{ + public: + UndoDeletes(Undo::Type t, const QValueList<KAEvent>& events) + : UndoMulti<UndoDelete>(t, events) { } // UNDO only + UndoDeletes(Undo::Type t, Undo::List& undos) + : UndoMulti<UndoDelete>(t, undos) { } + virtual QString actionText() const; + virtual UndoItem* createRedo(Undo::List&); +}; + +class UndoReactivates : public UndoMulti<UndoReactivate> +{ + public: + UndoReactivates(Undo::Type t, const QValueList<KAEvent>& events) + : UndoMulti<UndoReactivate>(t, events) { } // UNDO only + UndoReactivates(Undo::Type t, Undo::List& undos) + : UndoMulti<UndoReactivate>(t, undos) { } + virtual QString actionText() const; + virtual UndoItem* createRedo(Undo::List&); +}; + +Undo* Undo::mInstance = 0; +Undo::List Undo::mUndoList; +Undo::List Undo::mRedoList; + + +/****************************************************************************** +* Create the one and only instance of the Undo class. +*/ +Undo* Undo::instance() +{ + if (!mInstance) + mInstance = new Undo(kapp); + return mInstance; +} + +/****************************************************************************** +* Clear the lists of undo and redo items. +*/ +void Undo::clear() +{ + if (!mUndoList.isEmpty() || !mRedoList.isEmpty()) + { + mInstance->blockSignals(true); + while (mUndoList.count()) + delete mUndoList.first(); // N.B. 'delete' removes the object from the list + while (mRedoList.count()) + delete mRedoList.first(); // N.B. 'delete' removes the object from the list + mInstance->blockSignals(false); + emitChanged(); + } +} + +/****************************************************************************** +* Create an undo item and add it to the list of undos. +* N.B. The base class constructor adds the object to the undo list. +*/ +void Undo::saveAdd(const KAEvent& event) +{ + new UndoAdd(UNDO, event); + emitChanged(); +} + +void Undo::saveEdit(const KAEvent& oldEvent, const KAEvent& newEvent) +{ + new UndoEdit(UNDO, oldEvent, newEvent.id(), AlarmText::summary(newEvent)); + removeRedos(oldEvent.id()); // remove any redos which are made invalid by this edit + emitChanged(); +} + +void Undo::saveDelete(const KAEvent& event) +{ + new UndoDelete(UNDO, event); + removeRedos(event.id()); // remove any redos which are made invalid by this deletion + emitChanged(); +} + +void Undo::saveDeletes(const QValueList<KAEvent>& events) +{ + int count = events.count(); + if (count == 1) + saveDelete(events.first()); + else if (count > 1) + { + new UndoDeletes(UNDO, events); + for (QValueList<KAEvent>::ConstIterator it = events.begin(); it != events.end(); ++it) + removeRedos((*it).id()); // remove any redos which are made invalid by these deletions + emitChanged(); + } +} + +void Undo::saveReactivate(const KAEvent& event) +{ + new UndoReactivate(UNDO, event); + emitChanged(); +} + +void Undo::saveReactivates(const QValueList<KAEvent>& events) +{ + int count = events.count(); + if (count == 1) + saveReactivate(events.first()); + else if (count > 1) + { + new UndoReactivates(UNDO, events); + emitChanged(); + } +} + +/****************************************************************************** +* Remove any redos which are made invalid by a new undo. +*/ +void Undo::removeRedos(const QString& eventID) +{ + QString id = eventID; + for (Iterator it = mRedoList.begin(); it != mRedoList.end(); ) + { + UndoItem* item = *it; +//kdDebug(5950)<<"removeRedos(): "<<item->eventID()<<" (looking for "<<id<<")"<<endl; + if (item->operation() == UndoItem::MULTI) + { + if (item->deleteID(id)) + { + // The old multi-redo was replaced with a new single redo + delete item; + } + ++it; + } + else if (item->eventID() == id) + { + if (item->operation() == UndoItem::EDIT) + id = item->oldEventID(); // continue looking for its post-edit ID + item->setType(NONE); // prevent the destructor removing it from the list + delete item; + it = mRedoList.remove(it); + } + else + ++it; + } +} + +/****************************************************************************** +* Undo or redo a specified item. +* Reply = true if success, or if the item no longer exists. +*/ +bool Undo::undo(Undo::Iterator it, Undo::Type type, QWidget* parent, const QString& action) +{ + UndoItem::mRestoreError = UndoItem::ERR_NONE; + UndoItem::mRestoreWarning = UndoItem::WARN_NONE; + UndoItem::mRestoreWarningCount = 0; + if (it != mUndoList.end() && it != mRedoList.end() && (*it)->type() == type) + { + (*it)->restore(); + delete *it; // N.B. 'delete' removes the object from its list + emitChanged(); + } + + QString err; + switch (UndoItem::mRestoreError) + { + case UndoItem::ERR_NONE: + { + KAlarm::KOrgUpdateError errcode; + switch (UndoItem::mRestoreWarning) + { + case UndoItem::WARN_KORG_ADD: errcode = KAlarm::KORG_ERR_ADD; break; + case UndoItem::WARN_KORG_MODIFY: errcode = KAlarm::KORG_ERR_MODIFY; break; + case UndoItem::WARN_KORG_DELETE: errcode = KAlarm::KORG_ERR_DELETE; break; + case UndoItem::WARN_NONE: + default: + return true; + } + KAlarm::displayKOrgUpdateError(parent, errcode, UndoItem::mRestoreWarningCount); + return true; + } + case UndoItem::ERR_NOT_FOUND: err = i18n("Alarm not found"); break; + case UndoItem::ERR_CREATE: err = i18n("Error recreating alarm"); break; + case UndoItem::ERR_TEMPLATE: err = i18n("Error recreating alarm template"); break; + case UndoItem::ERR_EXPIRED: err = i18n("Cannot reactivate expired alarm"); break; + case UndoItem::ERR_PROG: err = i18n("Program error"); break; + default: err = i18n("Unknown error"); break; + } + KMessageBox::sorry(parent, i18n("Undo-action: message", "%1: %2").arg(action).arg(err)); + return false; +} + +/****************************************************************************** +* Add an undo item to the start of one of the lists. +*/ +void Undo::add(UndoItem* item, bool undo) +{ + if (item) + { + // Limit the number of items stored + int undoCount = mUndoList.count(); + int redoCount = mRedoList.count(); + if (undoCount + redoCount >= maxCount - 1) + { + if (undoCount) + mUndoList.pop_back(); + else + mRedoList.pop_back(); + } + + // Append the new item + List* list = undo ? &mUndoList : &mRedoList; + list->prepend(item); + } +} + +/****************************************************************************** +* Remove an undo item from one of the lists. +*/ +void Undo::remove(UndoItem* item, bool undo) +{ + List* list = undo ? &mUndoList : &mRedoList; + if (!list->isEmpty()) + list->remove(item); +} + +/****************************************************************************** +* Replace an undo item in one of the lists. +*/ +void Undo::replace(UndoItem* old, UndoItem* New) +{ + Type type = old->type(); + List* list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : 0; + if (!list) + return; + Iterator it = list->find(old); + if (it != list->end()) + { + New->setType(type); // ensure the item points to the correct list + *it = New; + old->setType(NONE); // mark the old item as no longer being in a list + } +} + +/****************************************************************************** +* Return the action description of the latest undo/redo item. +*/ +QString Undo::actionText(Undo::Type type) +{ + List* list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : 0; + return (list && !list->isEmpty()) ? list->first()->actionText() : QString::null; +} + +/****************************************************************************** +* Return the action description of the undo/redo item with the specified ID. +*/ +QString Undo::actionText(Undo::Type type, int id) +{ + UndoItem* undo = getItem(id, type); + return undo ? undo->actionText() : QString::null; +} + +/****************************************************************************** +* Return the alarm description of the undo/redo item with the specified ID. +*/ +QString Undo::description(Undo::Type type, int id) +{ + UndoItem* undo = getItem(id, type); + return undo ? undo->description() : QString::null; +} + +/****************************************************************************** +* Return the descriptions of all undo or redo items, in order latest first. +* For alarms which have undergone more than one change, only the first one is +* listed, to force dependent undos to be executed in their correct order. +* If 'ids' is non-null, also returns a list of their corresponding IDs. +*/ +QValueList<int> Undo::ids(Undo::Type type) +{ + QValueList<int> ids; + QStringList ignoreIDs; +//int n=0; + List* list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : 0; + if (!list) + return ids; + for (Iterator it = list->begin(); it != list->end(); ++it) + { + // Check whether this item should be ignored because it is a + // deendent undo. If not, add this item's ID to the ignore list. + UndoItem* item = *it; + bool omit = false; + if (item->operation() == UndoItem::MULTI) + { + // If any item in a multi-undo is disqualified, omit the whole multi-undo + QStringList newIDs; + const Undo::List& undos = ((UndoMultiBase*)item)->undos(); + for (Undo::List::ConstIterator u = undos.begin(); u != undos.end(); ++u) + { + QString evid = (*u)->eventID(); + if (ignoreIDs.find(evid) != ignoreIDs.end()) + omit = true; + else if (omit) + ignoreIDs.append(evid); + else + newIDs.append(evid); + } + if (omit) + { + for (QStringList::ConstIterator i = newIDs.begin(); i != newIDs.end(); ++i) + ignoreIDs.append(*i); + } + } + else + { + omit = (ignoreIDs.find(item->eventID()) != ignoreIDs.end()); + if (!omit) + ignoreIDs.append(item->eventID()); + if (item->operation() == UndoItem::EDIT) + ignoreIDs.append(item->oldEventID()); // continue looking for its post-edit ID + } + if (!omit) + ids.append(item->id()); +//else kdDebug(5950)<<"Undo::ids(): omit "<<item->actionText()<<": "<<item->description()<<endl; + } +//kdDebug(5950)<<"Undo::ids(): "<<n<<" -> "<<ids.count()<<endl; + return ids; +} + +/****************************************************************************** +* Emit the appropriate 'changed' signal. +*/ +void Undo::emitChanged() +{ + if (mInstance) + mInstance->emitChanged(actionText(UNDO), actionText(REDO)); +} + +/****************************************************************************** +* Return the item with the specified ID. +*/ +UndoItem* Undo::getItem(int id, Undo::Type type) +{ + List* list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : 0; + if (list) + { + for (Iterator it = list->begin(); it != list->end(); ++it) + { + if ((*it)->id() == id) + return *it; + } + } + return 0; +} + +/****************************************************************************** +* Find an item with the specified ID. +*/ +Undo::Iterator Undo::findItem(int id, Undo::Type type) +{ + List* list = (type == UNDO) ? &mUndoList : &mRedoList; + Iterator it; + for (it = list->begin(); it != list->end(); ++it) + { + if ((*it)->id() == id) + break; + } + return it; +} + + +/*============================================================================= += Class: UndoItem += A single undo action. +=============================================================================*/ +int UndoItem::mLastId = 0; +UndoItem::Error UndoItem::mRestoreError; +UndoItem::Warning UndoItem::mRestoreWarning; +int UndoItem::mRestoreWarningCount; + +/****************************************************************************** +* Constructor. +* Optionally appends the undo to the list of undos. +*/ +UndoItem::UndoItem(Undo::Type type) + : mId(0), + mType(type) +{ + if (type != Undo::NONE) + { + mId = ++mLastId; + if (mId < 0) + mId = mLastId = 1; // wrap round if we reach a negative number + Undo::add(this, (mType == Undo::UNDO)); + } +} + +/****************************************************************************** +* Destructor. +* Removes the undo from the list (if it's in the list). +*/ +UndoItem::~UndoItem() +{ + if (mType != Undo::NONE) + Undo::remove(this, (mType == Undo::UNDO)); +} + +/****************************************************************************** +* Return the description of an event. +*/ +QString UndoItem::description(const KAEvent& event) const +{ + return (mCalendar == KAEvent::TEMPLATE) ? event.templateName() : AlarmText::summary(event); +} + +/****************************************************************************** +* Return the action description of an add or delete Undo/Redo item for displaying. +*/ +QString UndoItem::addDeleteActionText(KAEvent::Status calendar, bool add) +{ + switch (calendar) + { + case KAEvent::ACTIVE: + if (add) + return i18n("Action to create a new alarm", "New alarm"); + else + return i18n("Action to delete an alarm", "Delete alarm"); + case KAEvent::TEMPLATE: + if (add) + return i18n("Action to create a new alarm template", "New template"); + else + return i18n("Action to delete an alarm template", "Delete template"); + case KAEvent::EXPIRED: + return i18n("Delete expired alarm"); + default: + break; + } + return QString::null; +} + + +/*============================================================================= += Class: UndoMultiBase += Undo item for multiple alarms. +=============================================================================*/ + +template <class T> +UndoMulti<T>::UndoMulti(Undo::Type type, const QValueList<KAEvent>& events) + : UndoMultiBase(type) // UNDO only +{ + for (QValueList<KAEvent>::ConstIterator it = events.begin(); it != events.end(); ++it) + mUndos.append(new T(Undo::NONE, *it)); +} + +UndoMultiBase::~UndoMultiBase() +{ + for (Undo::List::Iterator it = mUndos.begin(); it != mUndos.end(); ++it) + delete *it; +} + +/****************************************************************************** +* Undo the item, i.e. restore multiple alarms which were deleted (or delete +* alarms which were restored). +* Create a redo item to delete (or restore) the alarms again. +* Reply = redo item. +*/ +template <class T> +UndoItem* UndoMulti<T>::restore() +{ + Undo::List newUndos; + for (Undo::List::Iterator it = mUndos.begin(); it != mUndos.end(); ++it) + { + UndoItem* undo = (*it)->restore(); + if (undo) + newUndos.append(undo); + } + if (newUndos.isEmpty()) + return 0; + + // Create a redo item to delete the alarm again + return createRedo(newUndos); +} + +/****************************************************************************** +* If one of the multiple items has the specified ID, delete it. +* If an item is deleted and there is only one item left, the UndoMulti +* instance is removed from its list and replaced by the remaining UndoItem instead. +* Reply = true if this instance was replaced. The caller must delete it. +* = false otherwise. +*/ +template <class T> +bool UndoMulti<T>::deleteID(const QString& id) +{ + for (Undo::List::Iterator it = mUndos.begin(); it != mUndos.end(); ++it) + { + UndoItem* item = *it; + if (item->eventID() == id) + { + // Found a matching entry - remove it + mUndos.remove(it); + if (mUndos.count() == 1) + { + // There is only one entry left after removal. + // Replace 'this' multi instance with the remaining single entry. + replaceWith(item); + return true; + } + else + { + delete item; + return false; + } + } + } + return false; +} + + +/*============================================================================= += Class: UndoAdd += Undo item for alarm creation. +=============================================================================*/ + +UndoAdd::UndoAdd(Undo::Type type, const KAEvent& event) + : UndoItem(type), + mEventID(event.id()) +{ + setCalendar(KAEvent::uidStatus(mEventID)); + mDescription = UndoItem::description(event); // calendar must be set before calling this +} + +UndoAdd::UndoAdd(Undo::Type type, const KAEvent& event, KAEvent::Status cal) + : UndoItem(type), + mEventID(KAEvent::uid(event.id(), cal)) +{ + setCalendar(cal); + mDescription = UndoItem::description(event); // calendar must be set before calling this +} + +/****************************************************************************** +* Undo the item, i.e. delete the alarm which was added. +* Create a redo item to add the alarm back again. +* Reply = redo item. +*/ +UndoItem* UndoAdd::doRestore(bool setArchive) +{ + // Retrieve the current state of the alarm + kdDebug(5950) << "UndoAdd::doRestore(" << mEventID << ")\n"; + const KCal::Event* kcalEvent = AlarmCalendar::getEvent(mEventID); + if (!kcalEvent) + { + mRestoreError = ERR_NOT_FOUND; // alarm is no longer in calendar + return 0; + } + KAEvent event(*kcalEvent); + + // Create a redo item to recreate the alarm. + // Do it now, since 'event' gets modified by KAlarm::deleteEvent() + UndoItem* undo = createRedo(event); + + switch (calendar()) + { + case KAEvent::ACTIVE: + if (setArchive) + event.setArchive(); + // Archive it if it has already triggered + switch (KAlarm::deleteEvent(event, true)) + { + case KAlarm::UPDATE_ERROR: + case KAlarm::UPDATE_FAILED: + case KAlarm::SAVE_FAILED: + mRestoreError = ERR_CREATE; + break; + case KAlarm::UPDATE_KORG_ERR: + mRestoreWarning = WARN_KORG_DELETE; + ++mRestoreWarningCount; + break; + default: + break; + } + break; + case KAEvent::TEMPLATE: + if (KAlarm::deleteTemplate(event) != KAlarm::UPDATE_OK) + mRestoreError = ERR_TEMPLATE; + break; + case KAEvent::EXPIRED: // redoing the deletion of an expired alarm + KAlarm::deleteEvent(event); + break; + default: + delete undo; + mRestoreError = ERR_PROG; + return 0; + } + return undo; +} + +/****************************************************************************** +* Create a redo item to add the alarm back again. +*/ +UndoItem* UndoAdd::createRedo(const KAEvent& event) +{ + Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; + return new UndoDelete(t, event); +} + +/****************************************************************************** +* Return the action description of the Undo item for displaying. +*/ +QString UndoAdd::actionText() const +{ + return addDeleteActionText(calendar(), (type() == Undo::UNDO)); +} + + +/*============================================================================= += Class: UndoEdit += Undo item for alarm edit. +=============================================================================*/ + +UndoEdit::UndoEdit(Undo::Type type, const KAEvent& oldEvent, const QString& newEventID, const QString& description) + : UndoItem(type), + mOldEvent(new KAEvent(oldEvent)), + mNewEventID(newEventID), + mDescription(description) +{ + setCalendar(KAEvent::uidStatus(mNewEventID)); +} + +UndoEdit::~UndoEdit() +{ + delete mOldEvent; +} + +/****************************************************************************** +* Undo the item, i.e. undo an edit to a previously existing alarm. +* Create a redo item to reapply the edit. +* Reply = redo item. +*/ +UndoItem* UndoEdit::restore() +{ + kdDebug(5950) << "UndoEdit::restore(" << mNewEventID << ")\n"; + // Retrieve the current state of the alarm + const KCal::Event* kcalEvent = AlarmCalendar::getEvent(mNewEventID); + if (!kcalEvent) + { + mRestoreError = ERR_NOT_FOUND; // alarm is no longer in calendar + return 0; + } + KAEvent newEvent(*kcalEvent); + + // Create a redo item to restore the edit + Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; + UndoItem* undo = new UndoEdit(t, newEvent, mOldEvent->id(), mDescription); + + switch (calendar()) + { + case KAEvent::ACTIVE: + switch (KAlarm::modifyEvent(newEvent, *mOldEvent, 0)) + { + case KAlarm::UPDATE_ERROR: + case KAlarm::UPDATE_FAILED: + case KAlarm::SAVE_FAILED: + mRestoreError = ERR_CREATE; + break; + case KAlarm::UPDATE_KORG_ERR: + mRestoreWarning = WARN_KORG_MODIFY; + ++mRestoreWarningCount; + break; + default: + break; + } + break; + case KAEvent::TEMPLATE: + if (KAlarm::updateTemplate(*mOldEvent, 0) != KAlarm::UPDATE_OK) + mRestoreError = ERR_TEMPLATE; + break; + case KAEvent::EXPIRED: // editing of expired events is not allowed + default: + delete undo; + mRestoreError = ERR_PROG; + return 0; + } + return undo; +} + +/****************************************************************************** +* Return the action description of the Undo item for displaying. +*/ +QString UndoEdit::actionText() const +{ + switch (calendar()) + { + case KAEvent::ACTIVE: + return i18n("Action to edit an alarm", "Edit alarm"); + case KAEvent::TEMPLATE: + return i18n("Action to edit an alarm template", "Edit template"); + default: + break; + } + return QString::null; +} + + +/*============================================================================= += Class: UndoDelete += Undo item for alarm deletion. +=============================================================================*/ + +UndoDelete::UndoDelete(Undo::Type type, const KAEvent& event) + : UndoItem(type), + mEvent(new KAEvent(event)) +{ + setCalendar(KAEvent::uidStatus(mEvent->id())); +} + +UndoDelete::~UndoDelete() +{ + delete mEvent; +} + +/****************************************************************************** +* Undo the item, i.e. restore an alarm which was deleted. +* Create a redo item to delete the alarm again. +* Reply = redo item. +*/ +UndoItem* UndoDelete::restore() +{ + kdDebug(5950) << "UndoDelete::restore(" << mEvent->id() << ")\n"; + // Restore the original event + switch (calendar()) + { + case KAEvent::ACTIVE: + if (mEvent->toBeArchived()) + { + // It was archived when it was deleted + mEvent->setUid(KAEvent::EXPIRED); + switch (KAlarm::reactivateEvent(*mEvent, 0, true)) + { + case KAlarm::UPDATE_KORG_ERR: + mRestoreWarning = WARN_KORG_ADD; + ++mRestoreWarningCount; + break; + case KAlarm::UPDATE_ERROR: + case KAlarm::UPDATE_FAILED: + case KAlarm::SAVE_FAILED: + mRestoreError = ERR_EXPIRED; + return 0; + case KAlarm::UPDATE_OK: + break; + } + } + else + { + switch (KAlarm::addEvent(*mEvent, 0, 0, true)) + { + case KAlarm::UPDATE_KORG_ERR: + mRestoreWarning = WARN_KORG_ADD; + ++mRestoreWarningCount; + break; + case KAlarm::UPDATE_ERROR: + case KAlarm::UPDATE_FAILED: + case KAlarm::SAVE_FAILED: + mRestoreError = ERR_CREATE; + return 0; + case KAlarm::UPDATE_OK: + break; + } + } + break; + case KAEvent::TEMPLATE: + if (KAlarm::addTemplate(*mEvent, 0) != KAlarm::UPDATE_OK) + { + mRestoreError = ERR_CREATE; + return 0; + } + break; + case KAEvent::EXPIRED: + if (!KAlarm::addExpiredEvent(*mEvent)) + { + mRestoreError = ERR_CREATE; + return 0; + } + break; + default: + mRestoreError = ERR_PROG; + return 0; + } + + // Create a redo item to delete the alarm again + return createRedo(*mEvent); +} + +/****************************************************************************** +* Create a redo item to archive the alarm again. +*/ +UndoItem* UndoDelete::createRedo(const KAEvent& event) +{ + Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; + return new UndoAdd(t, event); +} + +/****************************************************************************** +* Return the action description of the Undo item for displaying. +*/ +QString UndoDelete::actionText() const +{ + return addDeleteActionText(calendar(), (type() == Undo::REDO)); +} + + +/*============================================================================= += Class: UndoDeletes += Undo item for multiple alarm deletion. +=============================================================================*/ + +/****************************************************************************** +* Create a redo item to delete the alarms again. +*/ +UndoItem* UndoDeletes::createRedo(Undo::List& undos) +{ + Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; + return new UndoDeletes(t, undos); +} + +/****************************************************************************** +* Return the action description of the Undo item for displaying. +*/ +QString UndoDeletes::actionText() const +{ + if (mUndos.isEmpty()) + return QString::null; + for (Undo::List::ConstIterator it = mUndos.begin(); it != mUndos.end(); ++it) + { + switch ((*it)->calendar()) + { + case KAEvent::ACTIVE: + return i18n("Delete multiple alarms"); + case KAEvent::TEMPLATE: + return i18n("Delete multiple templates"); + case KAEvent::EXPIRED: + break; // check if they are ALL expired + default: + return QString::null; + } + } + return i18n("Delete multiple expired alarms"); +} + + +/*============================================================================= += Class: UndoReactivate += Undo item for alarm reactivation. +=============================================================================*/ + +/****************************************************************************** +* Undo the item, i.e. re-archive the alarm which was reactivated. +* Create a redo item to reactivate the alarm back again. +* Reply = redo item. +*/ +UndoItem* UndoReactivate::restore() +{ + kdDebug(5950) << "UndoReactivate::restore()\n"; + // Validate the alarm's calendar + switch (calendar()) + { + case KAEvent::ACTIVE: + break; + default: + mRestoreError = ERR_PROG; + return 0; + } + return UndoAdd::doRestore(true); // restore alarm, ensuring that it is re-archived +} + +/****************************************************************************** +* Create a redo item to add the alarm back again. +*/ +UndoItem* UndoReactivate::createRedo(const KAEvent& event) +{ + Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; + return new UndoDeactivate(t, event); +} + +/****************************************************************************** +* Return the action description of the Undo item for displaying. +*/ +QString UndoReactivate::actionText() const +{ + return i18n("Reactivate alarm"); +} + + +/*============================================================================= += Class: UndoDeactivate += Redo item for alarm reactivation. +=============================================================================*/ + +/****************************************************************************** +* Undo the item, i.e. reactivate an alarm which was archived. +* Create a redo item to archive the alarm again. +* Reply = redo item. +*/ +UndoItem* UndoDeactivate::restore() +{ + kdDebug(5950) << "UndoDeactivate::restore()\n"; + // Validate the alarm's calendar + switch (calendar()) + { + case KAEvent::ACTIVE: + break; + default: + mRestoreError = ERR_PROG; + return 0; + } + + return UndoDelete::restore(); +} + +/****************************************************************************** +* Create a redo item to archive the alarm again. +*/ +UndoItem* UndoDeactivate::createRedo(const KAEvent& event) +{ + Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; + return new UndoReactivate(t, event); +} + +/****************************************************************************** +* Return the action description of the Undo item for displaying. +*/ +QString UndoDeactivate::actionText() const +{ + return i18n("Reactivate alarm"); +} + + +/*============================================================================= += Class: UndoReactivates += Undo item for multiple alarm reactivation. +=============================================================================*/ + +/****************************************************************************** +* Create a redo item to reactivate the alarms again. +*/ +UndoItem* UndoReactivates::createRedo(Undo::List& undos) +{ + Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; + return new UndoReactivates(t, undos); +} + +/****************************************************************************** +* Return the action description of the Undo item for displaying. +*/ +QString UndoReactivates::actionText() const +{ + return i18n("Reactivate multiple alarms"); +} diff --git a/kalarm/undo.h b/kalarm/undo.h new file mode 100644 index 000000000..99e06f979 --- /dev/null +++ b/kalarm/undo.h @@ -0,0 +1,93 @@ +/* + * undo.h - undo/redo facility + * Program: kalarm + * Copyright (C) 2005 by David Jarvie <software@astrojar.org.uk> + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef UNDO_H +#define UNDO_H + +/** @file undo.h - undo/redo facility */ + +#include <qvaluelist.h> +#include <qstringlist.h> + +class KAEvent; +class UndoItem; + + +class Undo : public QObject +{ + Q_OBJECT + public: + enum Type { NONE, UNDO, REDO }; + + static Undo* instance(); + static void saveAdd(const KAEvent&); + static void saveEdit(const KAEvent& oldEvent, const KAEvent& newEvent); + static void saveDelete(const KAEvent&); + static void saveDeletes(const QValueList<KAEvent>&); + static void saveReactivate(const KAEvent&); + static void saveReactivates(const QValueList<KAEvent>&); + static bool undo(QWidget* parent, const QString& action) + { return undo(mUndoList.begin(), UNDO, parent, action); } + static bool undo(int id, QWidget* parent, const QString& action) + { return undo(findItem(id, UNDO), UNDO, parent, action); } + static bool redo(QWidget* parent, const QString& action) + { return undo(mRedoList.begin(), REDO, parent, action); } + static bool redo(int id, QWidget* parent, const QString& action) + { return undo(findItem(id, REDO), REDO, parent, action); } + static void clear(); + static bool haveUndo() { return !mUndoList.isEmpty(); } + static bool haveRedo() { return !mRedoList.isEmpty(); } + static QString actionText(Type); + static QString actionText(Type, int id); + static QString description(Type, int id); + static QValueList<int> ids(Type); + static void emitChanged(); + + // Types for use by UndoItem class and its descendants + typedef QValueList<UndoItem*> List; + + signals: + void changed(const QString& undo, const QString& redo); + + protected: + // Methods for use by UndoItem class + static void add(UndoItem*, bool undo); + static void remove(UndoItem*, bool undo); + static void replace(UndoItem* old, UndoItem* New); + + private: + typedef QValueList<UndoItem*>::Iterator Iterator; + + Undo(QObject* parent) : QObject(parent) { } + static void removeRedos(const QString& eventID); + static bool undo(Iterator, Type, QWidget* parent, const QString& action); + static UndoItem* getItem(int id, Type); + static Iterator findItem(int id, Type); + void emitChanged(const QString& undo, const QString& redo) + { emit changed(undo, redo); } + + static Undo* mInstance; // the one and only Undo instance + static List mUndoList; // edit history for undo, latest undo first + static List mRedoList; // edit history for redo, latest redo first + + friend class UndoItem; +}; + +#endif // UNDO_H diff --git a/kalarm/uninstall.desktop b/kalarm/uninstall.desktop new file mode 100644 index 000000000..e1e3e1732 --- /dev/null +++ b/kalarm/uninstall.desktop @@ -0,0 +1,2 @@ +[Desktop Entry] +Hidden=true |