├── .gitignore ├── LICENSE ├── README.md ├── TODO ├── examples ├── disk_notifier.sh ├── gmail_notifier.py ├── mbox_notifier.sh ├── mpd_notifier.py └── storage_notifier.py ├── init └── systemd │ └── twmnd.service ├── screencast.gif ├── twmn.pro ├── twmnc ├── main.cpp └── twmnc.pro └── twmnd ├── dbusinterface.cpp ├── dbusinterface.h ├── main.cpp ├── message.h ├── settings.cpp ├── settings.h ├── twmnd.pro ├── widget.cpp └── widget.h /.gitignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | bin/ 3 | *.o 4 | moc_* 5 | *.swp 6 | 7 | .idea 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | twmn 2 | ==== 3 | 4 | A notification system for tiling window managers. `twmn` is two things: 5 | 6 | `twmnc`: command line tool to send notifications to `twmnd`. You can also use `notify-send` for a similar purpose, but `twmnc` is more powerful. See `twmnc --help` for more information. 7 | 8 | `twmnd`: daemon listening to notification requests and showing them one after another. Configure it at `~/.config/twmn/twmn.conf`. The file is generated the first time `twmnd` is launched. 9 | 10 | Notifications are shown in a one-line bar called the notification slide. They can be navigated through and activated with shortcuts. 11 | 12 | ![](https://github.com/sboli/twmn/blob/master/screencast.gif) 13 | 14 | See `twmn.conf` for more information. 15 | 16 | 17 | About twmn.conf 18 | --------------- 19 |
 20 | [gui]
 21 | ; Screen number to display notifications on when using a multi-head desktop.
 22 | screen=  ; 0 indexed screen number
 23 | 
 24 | ; WARNING: Deprecated by "screen"
 25 | ; Absolute position from the top-left corner of the slide. You may need it for a multi-screen setup.
 26 | ; You still have to set position in order to choose the slide animation. If empty, twmnd will try
 27 | ; to figure out where to display the slide according to your desktop size and the slide position.
 28 | absolute_position=  ; Supported format: WxH. Width and Height being integers.
 29 | 
 30 | ; Background color.
 31 | background_color=black  ; RBG hex and keywords (eg. lightgray) are supported.
 32 | 
 33 | ; An icon for the layout. Useful only for a layout file.
 34 | icon=  ; Path to image file. Optional.
 35 | 
 36 | ; Font family.
 37 | font=Sans
 38 | 
 39 | ; Font size.
 40 | font_size=13  ; In pixel.
 41 | 
 42 | ; Font variation.
 43 | ; accepted values are:
 44 | ; oblique, italic, ultra-light, light, medium, semi-bold, bold, ultra-bold, heavy, ultra-condensed,
 45 | ; extra-condensed, condensed, semi-condensed, semi-expanded, expanded, extra-expanded, ultra-expanded.
 46 | font_variant=medium
 47 | 
 48 | ; Text color.
 49 | foreground_color=white  ; RBG hex and keywords (eg. lightgray) are supported.
 50 | 
 51 | ; Height of the slide bar. Useful to match your tiling window manager's bar.
 52 | height=18  ; In pixel.
 53 | 
 54 | ; Position of the notification slide.
 55 | position=top_right  ; Accepted values: top_right (tr), top_left (tl), bottom_right (br),
 56 |                     ; bottom_left (bl), top_center (tc), bottom_center (bc), center (c).
 57 | 
 58 | ; moves the position of the slide in +/- pixels on the x or y axis (e.g. "+50" or "-100")
 59 | offset_x=+0 ; default is 0
 60 | offset_y=+0 ; default is 0
 61 | 
 62 | ; The animation to use when the slide appear
 63 | in_animation=38 ; see https://doc.qt.io/qt-5/qeasingcurve.html#Type-enum for types
 64 | 
 65 | ; The in animation's duration
 66 | in_animation_duration=1000 ; in milliseconds
 67 | 
 68 | ; The animation to use whe the slide is closing
 69 | out_animation=13
 70 | 
 71 | ; The out animation's duration
 72 | out_animation_duration=1000 ; in milliseconds
 73 | 
 74 | ; Enable or disable notification bounce when changing notification
 75 | bounce=true  ; true or false
 76 | 
 77 | ; Change bounce duration
 78 | bounce_duration=500 ; in milliseconds
 79 | 
 80 | ; If the character length is more then max_length the text is cut off and "..." is appended
 81 | max_length = -1 ; default is -1 (which means: don't cut off)
 82 | 
 83 | [icons]
 84 | ; An icon. You can add as many as you want.
 85 | NAME=  ; Path to image file. NAME being the icon's custom name.
 86 | 
 87 | 
 88 | [main]
 89 | ; Program/command to be executed on notification activation.
 90 | activate_command=  ; Path to command.
 91 | 
 92 | ; Time each notification remains visible.
 93 | duration=3000  ; In millisecond.
 94 | 
 95 | ; Host address to listen on for notifications.
 96 | host=127.0.0.1
 97 | 
 98 | ; UDP port used for notifications.
 99 | port=9797
100 | 
101 | ; Program/command to play sound with.
102 | sound_command=  ;  Path to command. Leave empty for no sound.
103 | 
104 | 
105 | 106 | 107 | Installation 108 | ------------ 109 | 110 | For [Arch Linux](http://www.archlinux.org/) users, `twmn` is [on the AUR](https://aur.archlinux.org/packages/twmn-git/). 111 | 112 | Otherwise you can install `twmnd` and `twmnc` manually: 113 | 114 | 1. install `boost` and `qt` if they weren't before, including the `widgets` and `x11extras` qt libraries 115 | 2. `git clone https://github.com/sboli/twmn.git` to get `twmn` 116 | 3. `cd twmn/` 117 | 4. `qmake` to generate a Makefile 118 | 5. `make` to compile 119 | 6. `sudo make install` to install `twmnd` and `twmnc` in `/usr/local/bin`. Make sure this folder is in your `$PATH` environment variable. (`export PATH=$PATH:/usr/local/bin`) 120 | 7. for `twmnd` to be always running, add it to your `.xinitrc`, `rc.conf` or else 121 | 122 | The `storage_notifier` example requires `dbus-python` to be installed. The `mpd_notifier` example requires `python-mpd` to be installed and running. 123 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sboli/twmn/15f6633f54f4b70364fe72dccb7efaece91937a7/TODO -------------------------------------------------------------------------------- /examples/disk_notifier.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Checks if disk usage goes over a certain % 3 | # Needs: df, awk 4 | # No rights reserved 5 | # Written by Charles-William Crete 6 | 7 | 8 | # If you want to check usage for only certain disk (in this case /dev/sda and /dev/sdb, 9 | # use this command. You can change the "[ab]" with your own disk 10 | # ([cd] for /dev/sdc and /dev/sdd by example) 11 | # df -hl | awk '/^\/dev\/sd[ab]/ { sum+=$5 } END { print sum }' 12 | # Default will check all drives 13 | 14 | # Max % that the disk can do until notifying you. 15 | MAX_USAGE=5 16 | 17 | # Time in seconds between checks/messages. 18 | # Common values: 19 | # 300 = 5 mins 20 | # 600 = 10 mins 21 | # 1800 = 30 mins 22 | # 3600 = 1 hour 23 | 24 | TIME_IN_BETWEEN=300 25 | 26 | while true; do 27 | 28 | # Get % of disk usage 29 | USAGE=`df -hl | awk '/^\/dev\/sd/ { sum+=$5 } END { print sum }'` 30 | 31 | # If current usage is higher than max usage 32 | if [[ $USAGE -gt $MAX_USAGE ]] ; then 33 | twmnc --title "Disk Usage High" --content "Reached ${USAGE}%" 34 | fi 35 | 36 | # Wait until next check/message 37 | sleep $TIME_IN_BETWEEN 38 | done 39 | 40 | -------------------------------------------------------------------------------- /examples/gmail_notifier.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright 2011 Orphée Lafond-Lummis . No rights reserved. 4 | 5 | """Gmail notifier for twmn. 6 | 7 | 1. Fill in your email and password. 8 | 2. Enable IMAP in Gmail. 9 | 3. Enjoy. 10 | """ 11 | 12 | #TODO: handle disconnection 13 | #TODO: store password in a less sucky way (getpass maybe?) 14 | #TODO: add gmail icon to notification request 15 | 16 | import imaplib 17 | import re 18 | import socket 19 | import time 20 | import os 21 | 22 | def cred_from_muttrc(): 23 | with open(os.path.expanduser('~/.muttrc')) as fin: 24 | data = fin.read() 25 | re_user = re.search(r'set\s+imap_user\s*=\s*"(.*)"', data, re.MULTILINE) 26 | re_pass = re.search(r'set\s+imap_pass\s*=\s*"(.*)"', data, re.MULTILINE) 27 | if re_user: 28 | user = re_user.group(1) 29 | if re_pass: 30 | password = re_pass.group(1) 31 | return user, password 32 | 33 | 34 | # Options 35 | # EMAIL = "USERNAME@gmail.com" 36 | # PASSWORD = "SECRET" 37 | # ... or if ~/.muttrc exists, read it. 38 | EMAIL, PASSWORD = cred_from_muttrc() 39 | FREQUENCY = 30 * 60 # Check emails every 30 minutes 40 | TWMN_PORT = 9797 41 | TWNM_ADDR = "127.0.0.1" 42 | 43 | 44 | def notification(sock, content): 45 | TITLE = "Gmail" 46 | message = "" + \ 47 | "{}".format(TITLE) + \ 48 | "{}".format(content) + \ 49 | "{}".format("email_icon") + \ 50 | "" 51 | sock.send(bytes(message.encode("utf-8"))) 52 | 53 | 54 | if __name__ == "__main__": 55 | # Connect to twmn 56 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 57 | s.connect((TWNM_ADDR, TWMN_PORT)) 58 | 59 | # Login to Gmail and select inbox 60 | try: 61 | gmail = imaplib.IMAP4_SSL('imap.gmail.com', 993) 62 | gmail.login(EMAIL, PASSWORD) 63 | gmail.select(readonly=1) # selects inbox by default 64 | except imaplib.IMAP4.error as err: 65 | notification(s, "Couldn't connect properly.") 66 | raise SystemExit(-1) 67 | 68 | # Check emails at FREQUENCY interval 69 | while True: 70 | unreadCount = re.search(b"UNSEEN (\d+)", \ 71 | gmail.status("INBOX", "(UNSEEN)")[1][0]).group(1) 72 | unreadCount = int(unreadCount) 73 | if unreadCount > 0: 74 | content = "{} unread".format(unreadCount) + \ 75 | " email{}.".format(("s" if unreadCount > 1 else "")) 76 | notification(s, content) 77 | time.sleep(FREQUENCY) 78 | -------------------------------------------------------------------------------- /examples/mbox_notifier.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Check for new mails 3 | # Needs: mailcheck, awk 4 | # No rights reserved 5 | # Written by Jens Oliver John 6 | 7 | while true; do 8 | mailcheck -c | awk ' 9 | { 10 | mbox=$7 11 | gsub(/.+\//, "", mbox); 12 | entry=sprintf("twmnc -t \"%s\" -c \"%s %s message(s)\"", mbox, $3, $4); 13 | system(entry); 14 | }' 15 | sleep 300 16 | done 17 | 18 | -------------------------------------------------------------------------------- /examples/mpd_notifier.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | 4 | import socket 5 | import mpd 6 | import time 7 | 8 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 9 | s.connect(("127.0.0.1", 9797)) 10 | client = mpd.MPDClient() 11 | client.connect("127.0.0.1", "6600") 12 | prev = "" 13 | while True: 14 | current = client.currentsong() 15 | if prev != current: 16 | s.send("" + current["title"] + "" 17 | " from " + current["artist"] + "") 18 | prev = current 19 | time.sleep(1) 20 | -------------------------------------------------------------------------------- /examples/storage_notifier.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | 4 | import dbus 5 | import gobject 6 | import socket 7 | from dbus.mainloop.glib import DBusGMainLoop 8 | 9 | title = "New device found" 10 | twmnd_port = 9797 11 | twmnd_host = "127.0.0.1" 12 | 13 | 14 | class DeviceAddedListener: 15 | def __init__(self): 16 | self.bus = dbus.SystemBus() 17 | self.hal_manager_obj = self.bus.get_object( 18 | "org.freedesktop.Hal", 19 | "/org/freedesktop/Hal/Manager") 20 | self.hal_manager = dbus.Interface(self.hal_manager_obj, 21 | "org.freedesktop.Hal.Manager") 22 | self.hal_manager.connect_to_signal("DeviceAdded", self._filter) 23 | 24 | def _filter(self, udi): 25 | device_obj = self.bus.get_object("org.freedesktop.Hal", udi) 26 | device = dbus.Interface(device_obj, "org.freedesktop.Hal.Device") 27 | 28 | if device.QueryCapability("volume"): 29 | return self.do_something(device) 30 | 31 | def do_something(self, volume): 32 | device = volume.GetProperty("block.device") 33 | label = volume.GetProperty("volume.label") 34 | #fstype = volume.GetProperty("volume.fstype") 35 | #mounted = volume.GetProperty("volume.is_mounted") 36 | #mount_point = volume.GetProperty("volume.mount_point") 37 | try: 38 | size = volume.GetProperty("volume.size") 39 | except: 40 | size = 0 41 | 42 | #print " device_file: %s" % device_file 43 | #print " label: %s" % label 44 | #print " fstype: %s" % fstype 45 | size = float(size) / 1024**3 46 | content = str(label) + '(' + device + ' - ' + ("%.2fGb" % size) + ')' 47 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 48 | s.connect((twmnd_host, twmnd_port)) 49 | mount_dir = "/media/" 50 | if len(label): 51 | mount_dir = mount_dir + label 52 | else: 53 | mount_dir = mount_dir + device.split('/')[2] 54 | 55 | s.send("" + \ 56 | "" + title + "" + \ 57 | "" + content + "" + \ 58 | "" + "" + \ 59 | "") 60 | 61 | if __name__ == "__main__": 62 | DBusGMainLoop(set_as_default=True) 63 | loop = gobject.MainLoop() 64 | DeviceAddedListener() 65 | loop.run() 66 | -------------------------------------------------------------------------------- /init/systemd/twmnd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=twmn server 3 | After=graphical.target 4 | 5 | [Service] 6 | Type=simple 7 | StandardOutput=null 8 | Restart=on-failure 9 | ExecStart=/usr/bin/twmnd 10 | 11 | [Install] 12 | WantedBy=default.target 13 | -------------------------------------------------------------------------------- /screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sboli/twmn/15f6633f54f4b70364fe72dccb7efaece91937a7/screencast.gif -------------------------------------------------------------------------------- /twmn.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = subdirs 2 | SUBDIRS = twmnd twmnc 3 | -------------------------------------------------------------------------------- /twmnc/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #ifndef VERSION 15 | #define VERSION "1.2" 16 | #endif 17 | 18 | const std::string DEFAULT_HOST = "127.0.0.1"; 19 | const int DEFAULT_PORT = 9797; 20 | 21 | /*! 22 | * \brief Read the port from the config file 23 | * \return true if the port was found 24 | * \param port the port to use 25 | */ 26 | bool read_port(int& port) 27 | { 28 | const char* xdgDir = getenv("XDG_CONFIG_HOME"); 29 | if (!xdgDir) 30 | return false; 31 | std::string path = xdgDir ; 32 | path += "/twmn/twmn.conf"; 33 | std::ifstream in(path.c_str()); 34 | if (!in) 35 | return false; 36 | boost::property_tree::ptree tree; 37 | boost::property_tree::ini_parser::read_ini(in, tree); 38 | boost::optional value = tree.get_optional("main.port"); 39 | return value ? port = *value : false; 40 | } 41 | 42 | /*! 43 | * \brief Fill tree with command line options from vm 44 | * \return true if any command line options was found 45 | */ 46 | bool populate_tree(boost::program_options::variables_map& vm, boost::property_tree::ptree& tree) 47 | { 48 | boost::program_options::notify(vm); 49 | boost::property_tree::ptree& root = tree.add("root", ""); 50 | boost::property_tree::ptree empty_root = root; 51 | BOOST_FOREACH(boost::program_options::variables_map::value_type& i, vm) { 52 | boost::any value = i.second.value(); 53 | try { 54 | root.add(i.first, boost::any_cast(value)); 55 | } 56 | catch (const boost::bad_any_cast& e) { 57 | root.add(i.first, boost::any_cast(value)); 58 | } 59 | } 60 | return empty_root != root; // tree not empty 61 | } 62 | 63 | /*! 64 | * \brief Sent tree to the host 65 | */ 66 | void send_tree(const boost::program_options::variables_map& vm, const boost::property_tree::ptree& tree) 67 | { 68 | using namespace boost::asio; 69 | std::ostringstream oss; 70 | boost::property_tree::xml_parser::write_xml(oss, tree); 71 | io_context ios; 72 | ip::udp::socket s(ios, ip::udp::endpoint(ip::udp::v4(), 0)); 73 | int port = vm.count("port") ? vm["port"].as() : 0; 74 | if (!port) 75 | if (!read_port(port)) 76 | port = DEFAULT_PORT; 77 | boost::optional host = tree.get_optional("content.host"); 78 | if (!host) 79 | host = boost::optional(DEFAULT_HOST); 80 | s.send_to(buffer(oss.str()), ip::udp::endpoint(ip::address(ip::make_address_v4(*host)), port)); 81 | ios.run(); 82 | } 83 | 84 | int main(int argc, char** argv) 85 | { 86 | namespace po = boost::program_options; 87 | po::options_description desc(std::string("twmn client version ") + VERSION + "\n Options"); 88 | desc.add_options() 89 | ("help,h", "Show this help") 90 | ("icon,i", po::value(), "The icon to use, either a predefined symbol" 91 | " in the config file or a file path.") 92 | ("title,t", po::value(), "A title for the notification.") 93 | ("content,c", po::value(), "A content for this notification") 94 | ("host,H", po::value(), ("The host ip address (x.x.x.x). Default is " + DEFAULT_HOST).c_str()) 95 | ("port,p", po::value(), "The port to use. Default setting is loaded from the config file.") 96 | ("layout,l", po::value(), "The layout to use. The name of a configuration file.") 97 | ("size,s", po::value(), "The height of this notification. If not specified the configuration file value is used.") 98 | ("pos", po::value(), "Position of this notification. Either top_right, top_left, bottom_right or bottom_left.") 99 | ("fn", po::value(), "The font to use for this notification.") 100 | ("fs", po::value(), "The font size for this notification.") 101 | ("duration,d", po::value(), "How long this notification remains visible") 102 | ("sc", po::value(), "A command to play a specific sound for this notification") 103 | ("bg", po::value(), "The background color") 104 | ("fg", po::value(), "The foreground color, which is the font color.") 105 | ("id", po::value(), "A message id. You should have given an id to the first message in order to modify it") 106 | ("aot", "Always on top. Specially on fullscreen applications, default.") 107 | ("ac", po::value(), "A command to run when the notification is activated.") 108 | ("remote", po::value(), "Remote control for the daemon. Can be either activate, next, previous or hide.") 109 | ("version", po::value(), "Show the version number.") 110 | ; 111 | try { 112 | po::variables_map vm; 113 | boost::property_tree::ptree tree; 114 | po::store(po::parse_command_line(argc, argv, desc), vm); 115 | if (vm.count("help")) { 116 | std::cout << desc << std::endl; 117 | return 0; 118 | } 119 | const bool filled = populate_tree(vm, tree); 120 | if (!filled && argc > 1) { 121 | std::string full_line; 122 | std::for_each(argv+1, argv+argc, (full_line += boost::lambda::_1) += " "); 123 | full_line.erase(full_line.length()-1); 124 | tree.put("root.content", full_line); 125 | } 126 | else if (!filled && argc == 1) 127 | throw std::runtime_error("Empty command line"); 128 | send_tree(vm, tree); 129 | } 130 | catch (const std::exception& e) { 131 | std::cout << desc << std::endl; 132 | } 133 | return 0; 134 | } 135 | -------------------------------------------------------------------------------- /twmnc/twmnc.pro: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | # Automatically generated by qmake (2.01a) Tue Aug 16 14:49:44 2011 3 | ###################################################################### 4 | 5 | CONFIG = console 6 | TEMPLATE = app 7 | TARGET = twmnc 8 | DEPENDPATH += . 9 | INCLUDEPATH += . 10 | DESTDIR = ../bin/ 11 | LIBS += -lboost_program_options -lboost_system -lpthread 12 | target.path += /usr/local/bin 13 | INSTALLS += target 14 | # Input 15 | SOURCES += main.cpp 16 | -------------------------------------------------------------------------------- /twmnd/dbusinterface.cpp: -------------------------------------------------------------------------------- 1 | #include "dbusinterface.h" 2 | #include 3 | #include 4 | 5 | DBusInterface::DBusInterface(QObject* parent) : 6 | QDBusAbstractAdaptor(parent), 7 | serviceName("org.freedesktop.Notifications"), 8 | dbus_conn(QDBusConnection::connectToBus(QDBusConnection::SessionBus, serviceName)) 9 | { 10 | Q_UNUSED(parent); 11 | 12 | if (!dbus_conn.isConnected()) 13 | return; 14 | 15 | if (!dbus_conn.registerService(serviceName)) 16 | return; 17 | 18 | dbus_conn.registerObject("/org/freedesktop/Notifications", this, QDBusConnection::ExportAllSlots); 19 | } 20 | 21 | void DBusInterface::GetCapabilities(QStringList& capabilities) 22 | { 23 | capabilities << "body"; 24 | } 25 | 26 | void DBusInterface::GetServerInformation( 27 | QString& name, QString& vendor, QString& version, QString& specVersion) 28 | { 29 | name = "twmnd"; 30 | vendor = "twmnd"; 31 | version = "1.0"; 32 | specVersion = "0"; 33 | } 34 | 35 | void DBusInterface::CloseNotification(unsigned int id) 36 | { 37 | Q_UNUSED(id); 38 | } 39 | 40 | void DBusInterface::Notify( 41 | const QString& appName, 42 | unsigned int id, 43 | const QString& icon, 44 | const QString& summary, 45 | const QString& body, 46 | const QStringList& actions, 47 | const QVariantMap& hints, 48 | int timeout, 49 | unsigned int& return_id) 50 | { 51 | Q_UNUSED(appName); 52 | Q_UNUSED(actions); 53 | Q_UNUSED(hints); 54 | 55 | Message msg; 56 | if (!body.isEmpty()) 57 | msg.data["content"] = Message::Data(body); 58 | if (!summary.isEmpty()) 59 | msg.data[(msg.data["content"] ? "title" : "content")] = Message::Data(summary); 60 | if (!icon.isEmpty()) 61 | msg.data["icon"] = Message::Data(icon); 62 | if (timeout != -1) 63 | msg.data["duration"] = Message::Data(timeout); 64 | msg.data["id"] = Message::Data(id ? id : ++lastNid); 65 | 66 | if (msg.data["content"]) 67 | { 68 | emit messageReceived(msg); 69 | } 70 | 71 | // reply 72 | return_id = msg.data["id"]->toInt(); 73 | } 74 | 75 | -------------------------------------------------------------------------------- /twmnd/dbusinterface.h: -------------------------------------------------------------------------------- 1 | #ifndef DBUSINTERFACE_H 2 | #define DBUSINTERFACE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "message.h" 9 | 10 | class DBusInterface : public QDBusAbstractAdaptor 11 | { 12 | Q_OBJECT 13 | Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Notifications") 14 | public: 15 | DBusInterface(QObject* parent = 0); 16 | 17 | public slots: 18 | void GetCapabilities(QStringList& capabilities); 19 | void CloseNotification(unsigned int id); 20 | void GetServerInformation( 21 | QString& name, 22 | QString& vendor, 23 | QString& version, 24 | QString& specVersion 25 | ); 26 | void Notify( 27 | const QString& appName, 28 | unsigned int id, 29 | const QString& icon, 30 | const QString& summary, 31 | const QString& body, 32 | const QStringList& actions, 33 | const QVariantMap& hints, 34 | int timeout, 35 | unsigned int& return_id 36 | ); 37 | 38 | signals: 39 | void messageReceived(const Message& msg); 40 | 41 | private: 42 | QString serviceName; 43 | QDBusConnection dbus_conn; 44 | QDBusServiceWatcher serviceWatcher; 45 | unsigned int lastNid; 46 | }; 47 | 48 | #endif // DBUSINTERFACE_H 49 | -------------------------------------------------------------------------------- /twmnd/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "widget.h" 8 | #include "dbusinterface.h" 9 | #include 10 | 11 | 12 | #ifndef VERSION 13 | #define VERSION "1.2" 14 | #endif 15 | 16 | void logOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) 17 | { 18 | #ifdef QT_NO_DEBUG_OUTPUT 19 | Q_UNUSED(type); 20 | Q_UNUSED(msg); 21 | Q_UNUSED(context); 22 | return; 23 | #else 24 | std::cout << "[" << QTime::currentTime().toString("hh:mm:ss").toStdString() << "]"; 25 | switch (type){ 26 | case QtDebugMsg: 27 | std::cout << " " << msg; 28 | break; 29 | case QtWarningMsg: 30 | std::cout << "[warning][" << context.file << '-' << context.line << "] " << msg.toStdString(); 31 | break; 32 | case QtCriticalMsg: 33 | std::cout << "[critical][" << context.file << '-' << context.line << "] " << msg.toStdString(); 34 | break; 35 | case QtFatalMsg: 36 | std::cout << "[fatal][" << context.file << '-' << context.line << "] " << msg.toStdString(); 37 | break; 38 | } 39 | std::cout << std::endl; 40 | #endif 41 | } 42 | 43 | int main(int argc, char *argv[]) 44 | { 45 | std::cout << "Starting twmnd version " << VERSION << std::endl; 46 | qInstallMessageHandler(logOutput); 47 | QApplication a(argc, argv); 48 | QApplication::setQuitOnLastWindowClosed(true); 49 | QApplication::setApplicationName("twmn"); 50 | QPalette p = a.palette(); 51 | p.setBrush(QPalette::Link, QBrush(QColor("black"))); 52 | a.setPalette(p); 53 | QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8")); 54 | const char* wname = "twmn"; 55 | if (argc > 1) { 56 | wname = argv[1]; 57 | } 58 | DBusInterface dbus(&a); 59 | Widget w(wname); 60 | w.connectToDBus(dbus); 61 | return a.exec(); 62 | } 63 | -------------------------------------------------------------------------------- /twmnd/message.h: -------------------------------------------------------------------------------- 1 | #ifndef MESSAGE_H 2 | #define MESSAGE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | struct Message 10 | { 11 | typedef boost::optional Data; 12 | QMap data; 13 | }; 14 | 15 | #endif // MESSAGE_H 16 | -------------------------------------------------------------------------------- /twmnd/settings.cpp: -------------------------------------------------------------------------------- 1 | #include "settings.h" 2 | #include 3 | 4 | Settings::Settings(QString file) : m_file(file) 5 | { 6 | m_defaultSettings["main/port"] = 9797; 7 | m_defaultSettings["main/host"] = "127.0.0.1"; 8 | m_defaultSettings["main/sound_command"] = ""; 9 | m_defaultSettings["main/activate_command"] = ""; 10 | m_defaultSettings["main/duration"] = "3000"; 11 | m_defaultSettings["gui/position"] = "top_right"; 12 | m_defaultSettings["gui/offset_x"] = "0"; 13 | m_defaultSettings["gui/offset_y"] = "0"; 14 | m_defaultSettings["gui/screen"] = ""; 15 | m_defaultSettings["gui/opacity"] = 100; 16 | m_defaultSettings["gui/absolute_position"] = ""; 17 | m_defaultSettings["gui/bounce"] = true; 18 | m_defaultSettings["gui/bounce_duration"] = 500; 19 | m_defaultSettings["gui/height"] = 18; 20 | m_defaultSettings["gui/font"] = "Sans"; 21 | m_defaultSettings["gui/font_size"] = 13; 22 | m_defaultSettings["gui/font_variant"] = "medium"; 23 | m_defaultSettings["gui/foreground_color"] = "#999999"; 24 | m_defaultSettings["gui/background_color"] = "#000000"; 25 | m_defaultSettings["gui/always_on_top"] = true; 26 | m_defaultSettings["gui/in_animation"] = 38; 27 | m_defaultSettings["gui/in_animation_duration"] = 1000; 28 | m_defaultSettings["gui/out_animation"] = 13; 29 | m_defaultSettings["gui/out_animation_duration"] = 1000; 30 | m_defaultSettings["gui/max_length"] = -1; 31 | m_defaultSettings["icons/critical_icon"] = ""; 32 | m_defaultSettings["icons/warning_icon"] = ""; 33 | m_defaultSettings["icons/info_icon"] = ""; 34 | reload(); 35 | } 36 | 37 | Settings::~Settings() 38 | { 39 | } 40 | 41 | void Settings::reload() 42 | { 43 | QSettings settings(QSettings::NativeFormat, QSettings::UserScope, "twmn", m_file); 44 | if (!settings.contains("main/port")) 45 | createDefaults(); 46 | settings.setIniCodec("UTF-8"); 47 | QStringList keys = settings.allKeys(); 48 | m_data.clear(); 49 | foreach (QString i, keys) { 50 | m_data[i] = settings.value(i); 51 | } 52 | } 53 | 54 | void Settings::set(QString setting, const QVariant &value) 55 | { 56 | m_data[setting] = value; 57 | } 58 | 59 | QVariant Settings::get(QString setting) 60 | { 61 | if (!m_data.contains(setting)) { 62 | if (!m_defaultSettings.contains(setting)) { 63 | std::cout << ("Attempt to get a non existing setting.\n" 64 | "You might want to create a key for " + setting + " in the config file.").toStdString(); 65 | qApp->quit(); 66 | } 67 | return m_defaultSettings[setting]; 68 | } 69 | return m_data[setting]; 70 | } 71 | 72 | bool Settings::has(QString setting) 73 | { 74 | return m_data.contains(setting) || m_defaultSettings.contains(setting); 75 | } 76 | 77 | void Settings::createDefaults() 78 | { 79 | QSettings settings(QSettings::NativeFormat, QSettings::UserScope, "twmn", m_file); 80 | settings.setIniCodec("UTF-8"); 81 | settings.clear(); 82 | std::cout << "Creating default settings ... " << std::endl; 83 | for (QMap::const_iterator it = m_data.begin(); it != m_data.end(); ++it) { 84 | m_defaultSettings[it.key()] = it.value(); 85 | } 86 | for (QMap::const_iterator it = m_defaultSettings.begin(); it != m_defaultSettings.end(); ++it) { 87 | settings.setValue(it.key(), it.value()); 88 | } 89 | } 90 | 91 | void Settings::fillWith(const Settings &s) 92 | { 93 | for (QMap::const_iterator it = s.m_data.begin(); it != s.m_data.end(); ++it) { 94 | if (m_data.contains(it.key())) 95 | continue; 96 | else 97 | m_data[it.key()] = it.value(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /twmnd/settings.h: -------------------------------------------------------------------------------- 1 | #ifndef SETTINGS_H 2 | #define SETTINGS_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | /*! 9 | * \brief Gère les paramètres du programme 10 | */ 11 | class Settings 12 | { 13 | public: 14 | Settings(QString file = QApplication::applicationName()); 15 | 16 | /*! 17 | * \brief Sauvegarde les réglages 18 | */ 19 | ~Settings(); 20 | 21 | /*! 22 | * \brief Recharge les paramètres à partir du fichier. 23 | */ 24 | void reload(); 25 | 26 | /*! 27 | * \brief Enregistre le paramètre setting 28 | */ 29 | void set(QString setting, const QVariant& value); 30 | 31 | /*! 32 | * \brief Récupère le paramètre setting 33 | */ 34 | QVariant get(QString setting); 35 | 36 | /*! 37 | * \return True si le paramètre setting existe. 38 | */ 39 | bool has(QString setting); 40 | 41 | /*! 42 | * \brief Create default settings 43 | */ 44 | void createDefaults(); 45 | 46 | /*! 47 | * \brief Complète cette instance avec les donnés de s, sans écraser les données de cette instance. 48 | */ 49 | void fillWith(const Settings& s); 50 | 51 | private: 52 | QMap m_data; 53 | QMap m_defaultSettings; 54 | QString m_file; 55 | }; 56 | 57 | #endif // SETTINGS_H 58 | -------------------------------------------------------------------------------- /twmnd/twmnd.pro: -------------------------------------------------------------------------------- 1 | #------------------------------------------------- 2 | # 3 | # Project created by QtCreator 2011-08-03T18:39:19 4 | # 5 | #------------------------------------------------- 6 | 7 | QT += core gui network widgets dbus 8 | DEFINES += QT_NO_DEBUG_OUTPUT 9 | TARGET = twmnd 10 | TEMPLATE = app 11 | CONFIG += debug 12 | DESTDIR = ../bin/ 13 | QMAKE_CXXFLAGS += -Wall -Werror -pedantic -Wno-long-long 14 | 15 | target.path+=/usr/local/bin 16 | INSTALLS += target 17 | 18 | SOURCES += main.cpp\ 19 | widget.cpp \ 20 | settings.cpp \ 21 | dbusinterface.cpp 22 | 23 | HEADERS += widget.h \ 24 | settings.h \ 25 | dbusinterface.h \ 26 | message.h 27 | -------------------------------------------------------------------------------- /twmnd/widget.cpp: -------------------------------------------------------------------------------- 1 | #include "widget.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include "settings.h" 23 | 24 | Widget::Widget(const char* wname) : m_settings(wname)//, m_shortcutGrabber(this, m_settings) 25 | { 26 | setWindowFlags(Qt::ToolTip); 27 | setAttribute(Qt::WA_TranslucentBackground); 28 | setWindowOpacity(m_settings.get("gui/opacity").toInt() / 100.0); 29 | QPropertyAnimation* anim = new QPropertyAnimation(this); 30 | anim->setTargetObject(this); 31 | m_animation.addAnimation(anim); 32 | anim->setEasingCurve(QEasingCurve::Type(m_settings.get("gui/in_animation").toInt())); 33 | connect(anim, SIGNAL(finished()), this, SLOT(reverseTrigger())); 34 | connectForPosition(m_settings.get("gui/position").toString()); 35 | connect(&m_visible, SIGNAL(timeout()), this, SLOT(reverseStart())); 36 | m_visible.setSingleShot(true); 37 | QHBoxLayout* l = new QHBoxLayout; 38 | l->setSizeConstraint(QLayout::SetNoConstraint); 39 | l->setMargin(0); 40 | l->setContentsMargins(0, 0, 0, 0); 41 | setLayout(l); 42 | l->addWidget(m_contentView["icon"] = new QLabel); 43 | l->addWidget(m_contentView["title"] = new QLabel); 44 | l->addWidget(m_contentView["text"] = new QLabel); 45 | m_contentView["title"]->setOpenExternalLinks(true); 46 | m_contentView["text"]->setOpenExternalLinks(true); 47 | setContextMenuPolicy(Qt::CustomContextMenu); 48 | connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onHide())); 49 | // Let the event loop run 50 | QTimer::singleShot(30, this, SLOT(init())); 51 | } 52 | 53 | Widget::~Widget() 54 | { 55 | } 56 | 57 | void Widget::connectToDBus(const DBusInterface& dbus) 58 | { 59 | connect(&dbus, SIGNAL(messageReceived(Message)), this, SLOT(appendMessageToQueue(Message))); 60 | } 61 | 62 | void Widget::init() 63 | { 64 | int port = m_settings.get("main/port").toInt(); 65 | QHostAddress host = QHostAddress(m_settings.get("main/host").toString()); 66 | if (!m_socket.bind(host, port)) { 67 | qCritical() << "Unable to listen port" << port; 68 | return; 69 | } 70 | connect(&m_socket, SIGNAL(readyRead()), this, SLOT(onDataReceived())); 71 | // m_shortcutGrabber.loadShortcuts(); 72 | } 73 | 74 | void Widget::onDataReceived() 75 | { 76 | boost::property_tree::ptree tree; 77 | Message m; 78 | try { 79 | quint64 size = m_socket.pendingDatagramSize(); 80 | QByteArray data(size, '\0'); 81 | m_socket.readDatagram(data.data(), size); 82 | std::istringstream iss (data.data()); 83 | boost::property_tree::xml_parser::read_xml(iss, tree); 84 | boost::property_tree::ptree& root = tree.get_child("root"); 85 | boost::property_tree::ptree::iterator it; 86 | for (it = root.begin(); it != root.end(); ++it) { 87 | std::cout << it->first << " - " << it->second.get_value() << std::endl; 88 | m.data[QString::fromStdString(it->first)] = boost::optional(it->second.get_value().c_str()); 89 | } 90 | } 91 | catch (const std::exception& e) { 92 | std::cout << "ERROR : " << e.what() << std::endl; 93 | } 94 | if (m.data.contains("remote") && m.data["remote"]) { // a remote control action 95 | processRemoteControl(qvariant_cast(m.data["remote"].get())); 96 | } 97 | else // A notification 98 | appendMessageToQueue(m); 99 | } 100 | 101 | void Widget::processRemoteControl(QString command) 102 | { 103 | if (command == "activate") 104 | onActivate(); 105 | else if (command == "hide") 106 | onHide(); 107 | else if (command == "next") 108 | onNext(); 109 | else if (command == "previous") 110 | onPrevious(); 111 | } 112 | 113 | bool Widget::startMessageCommand(const Message& msg, const char* key) { 114 | if (!msg.data.contains(key)) 115 | return false; 116 | QString command = msg.data[key]->toString(); 117 | if (command.isEmpty()) 118 | return false; 119 | QStringList args = QProcess::splitCommand(command); 120 | QString prog = args.takeFirst(); 121 | QProcess::startDetached(prog, args); 122 | return true; 123 | } 124 | 125 | void Widget::appendMessageToQueue(const Message& msg) 126 | { 127 | if (msg.data["id"] && !m_messageQueue.isEmpty()) { 128 | if (update(msg)) 129 | return; 130 | } 131 | 132 | m_messageQueue.push_back(msg); 133 | QTimer::singleShot(30, this, SLOT(processMessageQueue())); 134 | } 135 | 136 | void Widget::processMessageQueue() 137 | { 138 | if (m_messageQueue.empty()) { 139 | return; 140 | } 141 | 142 | if (m_animation.state() == QAbstractAnimation::Running || 143 | m_visible.isActive()) { 144 | //(m_animation.totalDuration() - m_animation.currentTime()) < 50) { 145 | return; 146 | } 147 | 148 | QFont boldFont = font(); 149 | boldFont.setBold(true); 150 | Message& m = m_messageQueue.front(); 151 | loadDefaults(); 152 | if (m.data["aot"]->toBool()) { 153 | setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint | Qt::BypassWindowManagerHint); 154 | raise(); 155 | } 156 | setupFont(); 157 | setupColors(); 158 | setupIcon(); 159 | setupTitle(); 160 | setupContent(); 161 | connectForPosition(m.data["pos"]->toString()); 162 | m_animation.setDirection(QAnimationGroup::Forward); 163 | int width = computeWidth(); 164 | qobject_cast(m_animation.animationAt(0))->setEasingCurve(QEasingCurve::Type(m_settings.get("gui/in_animation").toInt())); 165 | qobject_cast(m_animation.animationAt(0))->setStartValue(0); 166 | qobject_cast(m_animation.animationAt(0))->setEndValue(width); 167 | m_animation.start(); 168 | startMessageCommand(m, "sc"); 169 | // m_shortcutGrabber.enableShortcuts(); 170 | } 171 | 172 | QRect Widget::getScreenRect() 173 | { 174 | return QGuiApplication::screens().at(m_settings.get("gui/screen").toInt())->geometry(); 175 | } 176 | 177 | void Widget::updateTopLeftAnimation(QVariant value) 178 | { 179 | const int finalHeight = getHeight(); 180 | QPoint p(0, 0); 181 | if (m_settings.has("gui/screen") && !m_settings.get("gui/screen").toString().isEmpty()) { 182 | p = getScreenRect().topLeft(); 183 | } else if (m_settings.has("gui/absolute_position") && !m_settings.get("gui/absolute_position").toString().isEmpty()) { 184 | QPoint tmp = stringToPos(m_settings.get("gui/absolute_position").toString()); 185 | if (!tmp.isNull()) 186 | p = tmp; 187 | } 188 | //setGeometry(p.x(), p.y(), value.toInt(), finalHeight); 189 | int width = computeWidth(); 190 | if (width != -1) 191 | m_computedWidth = width; 192 | int offset_x = m_settings.get("gui/offset_x").toInt(); 193 | int offset_y = m_settings.get("gui/offset_y").toInt(); 194 | setGeometry(value.toInt()-m_computedWidth+offset_x, p.y()+offset_y, m_computedWidth, finalHeight); 195 | layout()->setSpacing(0); 196 | show(); 197 | } 198 | 199 | void Widget::updateTopRightAnimation(QVariant value) 200 | { 201 | const int end = QDesktopWidget().screenGeometry(this).width(); 202 | const int val = value.toInt(); 203 | const int finalHeight = getHeight(); 204 | QPoint p(end, 0); 205 | if (m_settings.has("gui/screen") && !m_settings.get("gui/screen").toString().isEmpty()) { 206 | p = getScreenRect().topRight(); 207 | ++p.rx(); 208 | } else if (m_settings.has("gui/absolute_position") && !m_settings.get("gui/absolute_position").toString().isEmpty()) { 209 | QPoint tmp = stringToPos(m_settings.get("gui/absolute_position").toString()); 210 | if (!tmp.isNull()) { 211 | p = tmp; 212 | } 213 | } 214 | int offset_x = m_settings.get("gui/offset_x").toInt(); 215 | int offset_y = m_settings.get("gui/offset_y").toInt(); 216 | setGeometry(p.x()-val+offset_x, p.y()+offset_y, val, finalHeight); 217 | layout()->setSpacing(0); 218 | show(); 219 | } 220 | 221 | void Widget::updateBottomRightAnimation(QVariant value) 222 | { 223 | const int wend = QDesktopWidget().screenGeometry(this).width(); 224 | const int hend = QDesktopWidget().screenGeometry(this).height(); 225 | const int finalHeight = getHeight(); 226 | const int val = value.toInt(); 227 | QPoint p(wend, hend); 228 | if (m_settings.has("gui/screen") && !m_settings.get("gui/screen").toString().isEmpty()) { 229 | p = getScreenRect().bottomRight(); 230 | ++p.rx(); 231 | ++p.ry(); 232 | } else if (m_settings.has("gui/absolute_position") && !m_settings.get("gui/absolute_position").toString().isEmpty()) { 233 | QPoint tmp = stringToPos(m_settings.get("gui/absolute_position").toString()); 234 | if (!tmp.isNull()) 235 | p = tmp; 236 | } 237 | int offset_x = m_settings.get("gui/offset_x").toInt(); 238 | int offset_y = m_settings.get("gui/offset_y").toInt(); 239 | setGeometry(p.x()-val+offset_x, p.y()-height()+offset_y, val, finalHeight); 240 | layout()->setSpacing(0); 241 | show(); 242 | } 243 | 244 | void Widget::updateBottomLeftAnimation(QVariant value) 245 | { 246 | const int hend = QDesktopWidget().screenGeometry(this).height(); 247 | const int finalHeight = getHeight(); 248 | QPoint p(0, hend); 249 | if (m_settings.has("gui/screen") && !m_settings.get("gui/screen").toString().isEmpty()) { 250 | p = getScreenRect().bottomLeft(); 251 | ++p.ry(); 252 | } else if (m_settings.has("gui/absolute_position") && !m_settings.get("gui/absolute_position").toString().isEmpty()) { 253 | QPoint tmp = stringToPos(m_settings.get("gui/absolute_position").toString()); 254 | if (!tmp.isNull()) 255 | p = tmp; 256 | } 257 | //setGeometry(p.x(), p.y()-height(), val, finalHeight); 258 | int width = computeWidth(); 259 | if (width != -1) 260 | m_computedWidth = width; 261 | int offset_x = m_settings.get("gui/offset_x").toInt(); 262 | int offset_y = m_settings.get("gui/offset_y").toInt(); 263 | setGeometry(value.toInt()-m_computedWidth, p.y()-height()+offset_x, m_computedWidth+offset_y, finalHeight); 264 | layout()->setSpacing(0); 265 | show(); 266 | } 267 | 268 | void Widget::updateTopCenterAnimation(QVariant value) 269 | { 270 | const int finalWidth = qobject_cast(m_animation.animationAt(0))->endValue().toInt(); 271 | const int finalHeight = getHeight(); 272 | const int h = value.toInt() * finalHeight / finalWidth; 273 | const int wend = QDesktopWidget().screenGeometry(this).width(); 274 | 275 | QPoint p1(wend, 0); 276 | QPoint p2(0, 0); 277 | if (m_settings.has("gui/screen") && !m_settings.get("gui/screen").toString().isEmpty()) { 278 | p1 = getScreenRect().topRight(); 279 | ++p1.rx(); 280 | p2 = getScreenRect().topLeft(); 281 | } else if (m_settings.has("gui/absolute_position") && !m_settings.get("gui/absolute_position").toString().isEmpty()) { 282 | QPoint tmp = stringToPos(m_settings.get("gui/absolute_position").toString()); 283 | if (!tmp.isNull()) 284 | p1 = tmp; 285 | } 286 | 287 | int offset_x = m_settings.get("gui/offset_x").toInt(); 288 | int offset_y = m_settings.get("gui/offset_y").toInt(); 289 | setGeometry(((p2.x() - p1.x())/2 - finalWidth/2 + p1.x())+offset_x, p1.y()+offset_y, finalWidth, h); 290 | layout()->setSpacing(0); 291 | show(); 292 | } 293 | 294 | void Widget::updateBottomCenterAnimation(QVariant value) 295 | { 296 | const int finalWidth = qobject_cast(m_animation.animationAt(0))->endValue().toInt(); 297 | const int finalHeight = getHeight(); 298 | const int h = value.toInt() * finalHeight / finalWidth; 299 | const int wend = QDesktopWidget().screenGeometry(this).width(); 300 | const int hend = QDesktopWidget().screenGeometry(this).height(); 301 | QPoint p1(wend, hend); 302 | QPoint p2(0, 0); 303 | if (m_settings.has("gui/screen") && !m_settings.get("gui/screen").toString().isEmpty()) { 304 | p1 = getScreenRect().bottomRight(); 305 | ++p1.rx(); 306 | ++p1.ry(); 307 | p2 = getScreenRect().topLeft(); 308 | } else if (m_settings.has("gui/absolute_position") && !m_settings.get("gui/absolute_position").toString().isEmpty()) { 309 | QPoint tmp = stringToPos(m_settings.get("gui/absolute_position").toString()); 310 | if (!tmp.isNull()) 311 | p1 = tmp; 312 | } 313 | int offset_x = m_settings.get("gui/offset_x").toInt(); 314 | int offset_y = m_settings.get("gui/offset_y").toInt(); 315 | setGeometry(((p2.x() - p1.x())/2 - finalWidth/2 + p1.x())+offset_x, p1.y()-h+offset_y, finalWidth, h); 316 | layout()->setSpacing(0); 317 | show(); 318 | } 319 | 320 | void Widget::updateCenterAnimation(QVariant value) 321 | { 322 | const int finalWidth = qobject_cast(m_animation.animationAt(0))->endValue().toInt(); 323 | const int finalHeight = getHeight(); 324 | const int h = value.toInt() * finalHeight / finalWidth; 325 | const int wend = QDesktopWidget().screenGeometry(this).width(); 326 | const int hend = QDesktopWidget().screenGeometry(this).height(); 327 | QPoint p1(wend, hend); 328 | QPoint p2(0, 0); 329 | if (m_settings.has("gui/screen") && !m_settings.get("gui/screen").toString().isEmpty()) { 330 | p1 = getScreenRect().bottomRight(); 331 | ++p1.rx(); 332 | ++p1.ry(); 333 | p2 = getScreenRect().topLeft(); 334 | } else if (m_settings.has("gui/absolute_position") && !m_settings.get("gui/absolute_position").toString().isEmpty()) { 335 | QPoint tmp = stringToPos(m_settings.get("gui/absolute_position").toString()); 336 | if (!tmp.isNull()) 337 | p1 = tmp; 338 | } 339 | int offset_x = m_settings.get("gui/offset_x").toInt(); 340 | int offset_y = m_settings.get("gui/offset_y").toInt(); 341 | setGeometry(((p2.x() - p1.x())/2 - value.toInt()/2 + p1.x())+offset_x, (p1.y()/2 - h/2)+offset_y, value.toInt(), h); 342 | layout()->setSpacing(0); 343 | show(); 344 | } 345 | 346 | void Widget::startBounce() 347 | { 348 | if (!m_settings.get("gui/bounce").toBool()) { 349 | return; 350 | } 351 | 352 | doneBounce(); 353 | 354 | QPropertyAnimation* anim = new QPropertyAnimation(this); 355 | anim->setTargetObject(this); 356 | m_animation.addAnimation(anim); 357 | 358 | anim->setEasingCurve(QEasingCurve::OutQuad); 359 | anim->setDuration(m_settings.get("gui/bounce_duration").toInt() * 0.25f); 360 | anim->setStartValue(0); 361 | 362 | QString position = m_messageQueue.front().data["pos"]->toString(); 363 | if (position == "top_center" || position == "tc" || 364 | position == "bottom_center" || position == "bc" || 365 | position == "center" || position == "c") 366 | anim->setEndValue(height()); 367 | else 368 | anim->setEndValue(40); 369 | 370 | tmpBouncePos = pos(); 371 | 372 | anim->start(); 373 | 374 | connect(anim, SIGNAL(valueChanged(QVariant)), this, SLOT(updateBounceAnimation(QVariant))); 375 | connect(anim, SIGNAL(finished()), this, SLOT(unbounce())); 376 | } 377 | 378 | void Widget::unbounce() 379 | { 380 | QPropertyAnimation* anim = qobject_cast(m_animation.animationAt(1)); 381 | if (!anim) 382 | return; 383 | disconnect(anim, SIGNAL(finished()), this, SLOT(unbounce())); 384 | connect(anim, SIGNAL(finished()), this, SLOT(doneBounce())); 385 | anim->setDirection(QAnimationGroup::Backward); 386 | anim->setEasingCurve(QEasingCurve::InBounce); 387 | anim->setDuration(m_settings.get("gui/bounce_duration").toInt() * 0.75f); 388 | anim->start(); 389 | } 390 | 391 | void Widget::doneBounce() 392 | { 393 | QPropertyAnimation* anim = 394 | qobject_cast(m_animation.animationAt(1)); 395 | 396 | if(!anim) { 397 | return; 398 | } 399 | 400 | disconnect(anim, SIGNAL(finished()), this, SLOT(unbounce())); 401 | disconnect(anim, SIGNAL(finished()), this, SLOT(doneBounce())); 402 | disconnect(anim, SIGNAL(valueChanged(QVariant)), this, SLOT(updateBounceAnimation(QVariant))); 403 | 404 | m_animation.removeAnimation(anim); 405 | } 406 | 407 | void Widget::updateBounceAnimation(QVariant value) 408 | { 409 | if(m_messageQueue.empty()){ 410 | doneBounce(); 411 | return; 412 | } 413 | 414 | QString position = m_messageQueue.front().data["pos"]->toString(); 415 | if (position == "top_left" || position == "tl" || 416 | position == "bottom_left" || position == "bl") 417 | move(tmpBouncePos.x() + value.toInt(), tmpBouncePos.y()); 418 | else if (position == "top_right" || position == "tr" || 419 | position == "bottom_right" || position == "br") 420 | move(tmpBouncePos.x() - value.toInt(), tmpBouncePos.y()); 421 | else if (position == "top_center" || position == "tc") 422 | move(tmpBouncePos.x(), tmpBouncePos.y() + value.toInt()); 423 | else if (position == "bottom_center" || position == "bc") 424 | move(tmpBouncePos.x(), tmpBouncePos.y() - value.toInt()); 425 | else if (position == "center" || position == "c") 426 | move(tmpBouncePos.x(), tmpBouncePos.y() - value.toInt()); 427 | layout()->setSpacing(0); 428 | show(); 429 | } 430 | 431 | void Widget::reverseTrigger() 432 | { 433 | if (m_animation.direction() == QAnimationGroup::Backward || 434 | m_messageQueue.isEmpty()) { 435 | QTimer::singleShot(30, this, SLOT(processMessageQueue())); 436 | return; 437 | } 438 | 439 | const bool bounce = m_settings.get("gui/bounce").toBool(); 440 | const int duration = m_messageQueue.front().data["duration"]->toInt(); 441 | 442 | const unsigned int minDuration = m_settings.get("gui/bounce_duration").toInt() + 10; 443 | 444 | if (duration == -1) { 445 | m_visible.setInterval(minDuration); 446 | } else { // ensure its visible long enough to bounce 447 | if (bounce) { 448 | m_visible.setInterval((unsigned)duration < minDuration ? minDuration : duration); 449 | } else { 450 | m_visible.setInterval(duration); 451 | } 452 | } 453 | 454 | m_visible.start(); 455 | } 456 | 457 | void Widget::reverseStart() 458 | { 459 | //If last message, play hide animation. 460 | if (m_messageQueue.size() <= 1) { 461 | QPropertyAnimation* bounceAnim = qobject_cast(m_animation.animationAt(1)); 462 | if(bounceAnim) { 463 | if(bounceAnim->state() == QAbstractAnimation::Running){ 464 | return; 465 | } 466 | } 467 | 468 | if (!m_messageQueue.isEmpty()){ 469 | if(m_animation.animationAt(1)){ 470 | doneBounce(); 471 | } 472 | m_messageQueue.pop_front(); 473 | } 474 | 475 | unsigned int duration = m_settings.get("gui/out_animation_duration").toInt(); 476 | if (duration <= 30) 477 | duration = 30; 478 | 479 | QPropertyAnimation* anim = qobject_cast(m_animation.animationAt(0)); 480 | if (!anim) { 481 | return; 482 | } 483 | 484 | disconnect(anim, SIGNAL(valueChanged(QVariant)), this, m_activePositionSlot.c_str()); 485 | 486 | anim->setDirection(QAnimationGroup::Backward); 487 | anim->setEasingCurve(QEasingCurve::Type(m_settings.get("gui/out_animation").toInt())); 488 | anim->setDuration(duration); 489 | anim->setCurrentTime(duration); 490 | 491 | connect(anim, SIGNAL(valueChanged(QVariant)), this, m_activePositionSlot.c_str()); 492 | 493 | anim->start(); 494 | //m_shortcutGrabber.disableShortcuts(); 495 | } else { 496 | autoNext(); 497 | } 498 | } 499 | 500 | int Widget::computeWidth() 501 | { 502 | if (m_messageQueue.isEmpty()) 503 | return -1; 504 | Message& m = m_messageQueue.front(); 505 | QFont boldFont = font(); 506 | boldFont.setBold(true); 507 | int width = 0; 508 | QString text = m_contentView["text"]->text(); 509 | width += QFontMetrics(boldFont).boundingRect(m_contentView["title"]->text()).width(); 510 | if (Qt::mightBeRichText(text)) { 511 | QTextDocument doc; 512 | doc.setUseDesignMetrics(true); 513 | doc.setHtml(text); 514 | doc.setDefaultFont(font()); 515 | width += doc.idealWidth(); 516 | } 517 | else 518 | width += QFontMetrics(font()).boundingRect(text).width(); 519 | if (m.data["icon"]) 520 | width += m_contentView["icon"]->pixmap(Qt::ReturnByValue).width(); 521 | return width; 522 | } 523 | 524 | void Widget::setupFont() 525 | { 526 | Message& m = m_messageQueue.front(); 527 | QFont font; 528 | QString name = m.data["fn"]->toString(); 529 | font.setPixelSize(m.data["fs"]->toInt()); 530 | font.setFamily(name); 531 | QString ss( m.data["fv"]->toString() ); 532 | if (ss == "oblique") 533 | font.setStyle( QFont::StyleOblique ); 534 | else if (ss == "italic") 535 | font.setStyle( QFont::StyleItalic ); 536 | else if (ss == "ultra-light") 537 | font.setWeight( 13 ); 538 | else if (ss == "light") 539 | font.setWeight( QFont::Light ); 540 | else if (ss == "medium") 541 | font.setWeight( 50 ); 542 | else if (ss == "semi-bold") 543 | font.setWeight( QFont::DemiBold ); 544 | else if (ss == "bold") 545 | font.setWeight( QFont::Bold ); 546 | else if (ss == "ultra-bold") 547 | font.setWeight( QFont::Black ); 548 | else if (ss == "heavy") 549 | font.setWeight( 99 ); 550 | else if (ss == "ultra-condensed") 551 | font.setStretch( QFont::UltraCondensed ); 552 | else if (ss == "extra-condensed") 553 | font.setStretch( QFont::ExtraCondensed ); 554 | else if (ss == "condensed") 555 | font.setStretch( QFont::Condensed ); 556 | else if (ss == "semi-condensed") 557 | font.setStretch( QFont::SemiCondensed ); 558 | else if (ss == "semi-expanded") 559 | font.setStretch( QFont::SemiExpanded ); 560 | else if (ss == "expanded") 561 | font.setStretch( QFont::Expanded ); 562 | else if (ss == "extra-expanded") 563 | font.setStretch( QFont::ExtraExpanded ); 564 | else if (ss == "ultra-expanded") 565 | font.setStretch( QFont::UltraExpanded ); 566 | QApplication::setFont(font); 567 | } 568 | 569 | void Widget::setupColors() 570 | { 571 | Message& m = m_messageQueue.front(); 572 | QString bg = m.data["bg"]->toString(); 573 | QString fg = m.data["fg"]->toString(); 574 | QString sheet; 575 | if (!bg.isEmpty()) 576 | sheet += QString("background-color: %1;").arg(bg); 577 | if (!fg.isEmpty()) 578 | sheet += QString("color: %1;").arg(fg); 579 | setStyleSheet(sheet); 580 | } 581 | 582 | void Widget::connectForPosition(QString position) 583 | { 584 | QPropertyAnimation* anim = qobject_cast(m_animation.animationAt(0)); 585 | if (!anim) 586 | return; 587 | int duration = m_settings.get("gui/in_animation_duration").toInt(); 588 | if (duration <= 30) 589 | duration = 30; 590 | if (anim->duration() != duration) 591 | anim->setDuration(duration); 592 | disconnect(anim, SIGNAL(valueChanged(QVariant)), this, SLOT(updateTopLeftAnimation(QVariant))); 593 | disconnect(anim, SIGNAL(valueChanged(QVariant)), this, SLOT(updateTopRightAnimation(QVariant))); 594 | disconnect(anim, SIGNAL(valueChanged(QVariant)), this, SLOT(updateBottomRightAnimation(QVariant))); 595 | disconnect(anim, SIGNAL(valueChanged(QVariant)), this, SLOT(updateBottomLeftAnimation(QVariant))); 596 | disconnect(anim, SIGNAL(valueChanged(QVariant)), this, SLOT(updateTopCenterAnimation(QVariant))); 597 | disconnect(anim, SIGNAL(valueChanged(QVariant)), this, SLOT(updateBottomCenterAnimation(QVariant))); 598 | disconnect(anim, SIGNAL(valueChanged(QVariant)), this, SLOT(updateCenterAnimation(QVariant))); 599 | 600 | if (position == "top_left" || position == "tl") { 601 | m_activePositionSlot = SLOT(updateTopLeftAnimation(QVariant)); 602 | } 603 | else if (position == "top_right" || position == "tr") { 604 | m_activePositionSlot = SLOT(updateTopRightAnimation(QVariant)); 605 | } 606 | else if (position == "bottom_right" || position == "br") { 607 | m_activePositionSlot = SLOT(updateBottomRightAnimation(QVariant)); 608 | } 609 | else if (position == "bottom_left" || position == "bl") { 610 | m_activePositionSlot = SLOT(updateBottomLeftAnimation(QVariant)); 611 | } 612 | else if (position == "top_center" || position == "tc") { 613 | m_activePositionSlot = SLOT(updateTopCenterAnimation(QVariant)); 614 | } 615 | else if (position == "bottom_center" || position == "bc") { 616 | m_activePositionSlot = SLOT(updateBottomCenterAnimation(QVariant)); 617 | } 618 | else if (position == "center" || position == "c") { 619 | m_activePositionSlot = SLOT(updateCenterAnimation(QVariant)); 620 | } 621 | else if (position == "below_cursor" || position == "bcur") { 622 | m_activePositionSlot = SLOT(updateBelowCursorAnimation(QVariant)); 623 | } 624 | else { 625 | // top_right seems to be the classic case so fallback to it. 626 | m_activePositionSlot = SLOT(updateTopRightAnimation(QVariant)); 627 | } 628 | 629 | connect(anim, SIGNAL(valueChanged(QVariant)), this, m_activePositionSlot.c_str()); 630 | } 631 | 632 | void Widget::setupIcon() 633 | { 634 | Message& m = m_messageQueue.front(); 635 | bool done = true; 636 | if (m.data["icon"]) { 637 | QPixmap pix = qvariant_cast(*m.data["icon"]); 638 | if (pix.isNull()) 639 | pix = loadPixmap(m.data["icon"]->toString()); 640 | if (!pix.isNull()) 641 | m.data["icon"].reset(pix); 642 | else if (pix.isNull()) 643 | done = false; 644 | if (pix.height() > m.data["size"]->toInt()) 645 | pix = pix.scaled(m.data["size"]->toInt()-2, m.data["size"]->toInt()-2, Qt::KeepAspectRatio); 646 | m.data["icon"].reset(pix); 647 | m_contentView["icon"]->setPixmap(pix); 648 | m_contentView["icon"]->setMaximumWidth(9999); 649 | } 650 | if (!done) { 651 | m_contentView["icon"]->setPixmap(QPixmap()); 652 | m_contentView["icon"]->setFixedWidth(2); 653 | } 654 | } 655 | 656 | void Widget::setupTitle() 657 | { 658 | QFont boldFont = font(); 659 | boldFont.setBold(true); 660 | Message& m = m_messageQueue.front(); 661 | if (m.data["title"]) { // avoid ugly space if no icon is set 662 | QString text = (m.data["icon"] ? " " : "") + m.data["title"]->toString() + " "; 663 | foreach (QString i, QStringList() << "\n" << "\r" << "
" << "
") 664 | text.replace(i, " "); 665 | 666 | m_contentView["title"]->setText(text + "| "); 667 | m_contentView["title"]->setFont(boldFont); 668 | m_contentView["title"]->setMaximumWidth(9999); 669 | } 670 | else { 671 | m_contentView["title"]->setText(""); 672 | m_contentView["title"]->setFixedWidth(0); 673 | } 674 | } 675 | 676 | void Widget::setupContent() 677 | { 678 | Message& m = m_messageQueue.front(); 679 | if (m.data["content"]) { 680 | QString text = (m.data["icon"] && !m.data["title"] ? " " : "") + m.data["content"]->toString() + " "; 681 | foreach (QString i, QStringList() << "\n" << "\r" << "
" << "
") 682 | text.replace(i, " "); 683 | int max_length = m_settings.get("gui/max_length").toInt(); 684 | if (max_length != -1 && text.size() >= max_length) { 685 | text.resize(max_length); 686 | text.append("..."); 687 | } 688 | m_contentView["text"]->setText(text); 689 | m_contentView["text"]->setMaximumWidth(9999); 690 | } 691 | else { 692 | m_contentView["text"]->setText(""); 693 | m_contentView["text"]->setFixedWidth(0); 694 | } 695 | } 696 | 697 | void Widget::loadDefaults() 698 | { 699 | // "content" << "icon" << "title" << "layout" << "size" << "pos" << "fn" << "fs" << "duration" < "sc" << "bg" << "fg"; 700 | Message& m = m_messageQueue.front(); 701 | Settings* s = &m_settings; 702 | if (m.data["layout"]) { 703 | QString name = m.data["layout"]->toString(); 704 | name.remove(".conf"); 705 | s = new Settings(name); 706 | s->fillWith(m_settings); 707 | qDebug() << "Layout loaded : " << name; 708 | qDebug() << s->get("gui/foreground_color"); 709 | } 710 | if (!m.data["bg"]) 711 | m.data["bg"] = boost::optional(s->get("gui/background_color")); 712 | if (!m.data["fg"]) 713 | m.data["fg"] = boost::optional(s->get("gui/foreground_color")); 714 | if (!m.data["sc"]) 715 | m.data["sc"] = boost::optional(s->get("main/sound_command")); 716 | if (!m.data["duration"]) 717 | m.data["duration"] = boost::optional(s->get("main/duration")); 718 | if (!m.data["fs"]) 719 | m.data["fs"] = boost::optional(s->get("gui/font_size")); 720 | if (!m.data["fn"]) 721 | m.data["fn"] = boost::optional(s->get("gui/font")); 722 | if (!m.data["fv"]) 723 | m.data["fv"] = boost::optional(s->get("gui/font_variant")); 724 | if (!m.data["pos"]) 725 | m.data["pos"] = boost::optional(s->get("gui/position")); 726 | if (!m.data["size"]) 727 | m.data["size"] = boost::optional(s->get("gui/height")); 728 | if (!m.data["icon"]) 729 | m.data["icon"] = loadPixmap(s->has("gui/icon") ? s->get("gui/icon").toString() : ""); 730 | if (!m.data["aot"]) 731 | m.data["aot"] = boost::optional(s->get("gui/always_on_top")); 732 | if (!m.data["ac"]) 733 | m.data["ac"] = boost::optional(s->get("main/activate_command")); 734 | if (!m.data["bounce"]) 735 | m.data["bounce"] = boost::optional(s->get("gui/bounce")); 736 | if (s != &m_settings) 737 | delete s; 738 | } 739 | 740 | QPixmap Widget::loadPixmap(QString pattern) 741 | { 742 | QPixmap icon(pattern); 743 | if (icon.isNull()) { 744 | if (m_settings.has("icons/" + pattern)) 745 | icon = QPixmap(m_settings.get("icons/" + pattern).toString()); 746 | else { 747 | ///TODO: Load standard icons. Surprisingly this doesn't work 748 | //icon = QIcon::fromTheme(pattern).pixmap(999, 999); 749 | //if (icon.isNull()) { 750 | QImage img(1, 1, QImage::Format_ARGB32); 751 | QPainter p; 752 | p.begin(&img); 753 | p.fillRect(0, 0, 1, 1, QBrush(QColor::fromRgb(255, 255, 255, 0))); 754 | p.end(); 755 | icon = QPixmap::fromImage(img); 756 | //} 757 | } 758 | } 759 | return icon; 760 | } 761 | 762 | bool Widget::update(const Message &m) 763 | { 764 | bool found = false; 765 | for (QQueue::iterator it = m_messageQueue.begin(); it != m_messageQueue.end(); ++it) { 766 | if (it->data["id"] && it->data["id"]->toInt() == m.data["id"]->toInt()) { 767 | it->data = m.data; 768 | found = true; 769 | break; 770 | } 771 | } 772 | if (found && !m_messageQueue.isEmpty() && m_messageQueue.front().data["id"] 773 | && m_messageQueue.front().data["id"]->toInt() == m.data["id"]->toInt()) { 774 | loadDefaults(); 775 | setupFont(); 776 | setupColors(); 777 | setupIcon(); 778 | setupTitle(); 779 | setupContent(); 780 | updateFinalWidth(); 781 | connectForPosition(m_messageQueue.front().data["pos"]->toString()); 782 | m_visible.start(); 783 | } 784 | return found; 785 | } 786 | 787 | QPoint Widget::stringToPos(QString string) 788 | { 789 | string.replace("X", "x"); 790 | string.replace("*", "x"); 791 | const QStringList splitted = string.split("x"); 792 | if (string.isEmpty() || !string.contains('x') || splitted.size() < 2) 793 | return QPoint(); 794 | QPoint ret; 795 | ret.setX(QString(splitted[0]).toInt()); 796 | ret.setY(QString(splitted[1]).toInt()); 797 | if (ret.x() < 0) 798 | ret.setX(QDesktopWidget().screenGeometry(this).width() + ret.x()); 799 | if (ret.y() < 0) 800 | ret.setY(QDesktopWidget().screenGeometry(this).height() + ret.y()); 801 | 802 | return ret; 803 | } 804 | 805 | void Widget::updateFinalWidth() 806 | { 807 | if (m_messageQueue.empty()) { 808 | return; 809 | } 810 | 811 | QString position = m_messageQueue.front().data["pos"]->toString(); 812 | int width = computeWidth(); 813 | 814 | qobject_cast(m_animation.animationAt(0))->setEndValue(width); 815 | if (position == "top_left" || position == "tl") 816 | updateTopLeftAnimation(width); 817 | else if (position == "top_right" || position == "tr") 818 | updateTopRightAnimation(width); 819 | else if (position == "bottom_right" || position == "br") 820 | updateBottomRightAnimation(width); 821 | else if (position == "bottom_left" || position == "bl") 822 | updateBottomLeftAnimation(width); 823 | else if (position == "top_center" || position == "tc") 824 | updateTopCenterAnimation(width); 825 | else if (position == "bottom_center" || position == "bc") 826 | updateBottomCenterAnimation(width); 827 | else if (position == "center" || position == "c") 828 | updateCenterAnimation(width); 829 | } 830 | 831 | void Widget::onPrevious() 832 | { 833 | m_visible.start(); 834 | if (m_previousStack.size() < 1) 835 | return; 836 | Message m = m_previousStack.pop(); 837 | m_messageQueue.push_front(m); 838 | loadDefaults(); 839 | setupFont(); 840 | setupColors(); 841 | setupIcon(); 842 | setupTitle(); 843 | setupContent(); 844 | connectForPosition(m_messageQueue.front().data["pos"]->toString()); 845 | updateFinalWidth(); 846 | } 847 | 848 | void Widget::onNext() 849 | { 850 | m_visible.start(); 851 | if (m_messageQueue.size() < 2) 852 | return; 853 | Message m = m_messageQueue.front(); 854 | boost::optional tmpManual = m.data["manually_shown"]; 855 | m.data["manually_shown"] = boost::optional(true); 856 | m_previousStack.push(m); 857 | m_messageQueue.pop_front(); 858 | loadDefaults(); 859 | setupFont(); 860 | setupColors(); 861 | setupIcon(); 862 | setupTitle(); 863 | setupContent(); 864 | connectForPosition(m_messageQueue.front().data["pos"]->toString()); 865 | updateFinalWidth(); 866 | if (m_messageQueue.front().data["bounce"] && !tmpManual) 867 | startBounce(); 868 | } 869 | 870 | void Widget::onActivate() 871 | { 872 | if (!m_messageQueue.isEmpty()) { 873 | if (startMessageCommand(m_messageQueue.front(), "ac")) { 874 | m_messageQueue.front().data["ac"] = ""; 875 | } 876 | } 877 | 878 | if(m_messageQueue.size() > 1) { 879 | onNext(); 880 | } else { 881 | onHide(); 882 | } 883 | } 884 | 885 | void Widget::onHide() 886 | { 887 | m_messageQueue.clear(); 888 | m_visible.setInterval(2); 889 | m_visible.start(); 890 | } 891 | 892 | void Widget::autoNext() 893 | { 894 | Q_ASSERT (m_messageQueue.size() >= 2); 895 | Message&m = m_messageQueue.front(); 896 | // The user already saw it manually. 897 | if (m.data["manually_shown"]) { 898 | m_messageQueue.pop_front(); 899 | reverseStart(); 900 | } 901 | else { 902 | startMessageCommand(*(m_messageQueue.begin()+1), "sc"); 903 | } 904 | onNext(); 905 | } 906 | 907 | void Widget::mousePressEvent(QMouseEvent *e) 908 | { 909 | if (e->button() == Qt::LeftButton) 910 | onActivate(); 911 | QWidget::mousePressEvent(e); 912 | } 913 | 914 | void Widget::wheelEvent(QWheelEvent *e) 915 | { 916 | QPoint angleDelta = e->angleDelta(); 917 | int delta = angleDelta.x() != 0 ? angleDelta.x() : angleDelta.y(); 918 | if (delta > 0) 919 | onPrevious(); 920 | else if (delta < 0) 921 | onNext(); 922 | QWidget::wheelEvent(e); 923 | } 924 | 925 | std::size_t Widget::getHeight() 926 | { 927 | QPropertyAnimation* anim = qobject_cast(m_animation.animationAt(0)); 928 | if(anim->direction() == QAbstractAnimation::Forward 929 | && !m_messageQueue.empty()) { 930 | return m_messageQueue.front().data["size"]->toInt(); 931 | } else { 932 | return height(); 933 | } 934 | } 935 | -------------------------------------------------------------------------------- /twmnd/widget.h: -------------------------------------------------------------------------------- 1 | #ifndef WIDGET_H 2 | #define WIDGET_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "settings.h" 9 | #include "message.h" 10 | #include "dbusinterface.h" 11 | 12 | class Widget : public QWidget 13 | { 14 | Q_OBJECT 15 | public: 16 | Widget(const char* wname); 17 | ~Widget(); 18 | 19 | private slots: 20 | 21 | void init(); 22 | void onDataReceived(); 23 | void appendMessageToQueue(const Message& msg); 24 | void processMessageQueue(); 25 | 26 | void updateTopLeftAnimation(QVariant value); 27 | void updateTopRightAnimation(QVariant value); 28 | void updateBottomRightAnimation(QVariant value); 29 | void updateBottomLeftAnimation(QVariant value); 30 | void updateTopCenterAnimation(QVariant value); 31 | void updateBottomCenterAnimation(QVariant value); 32 | void updateCenterAnimation(QVariant value); 33 | void startBounce(); 34 | void unbounce(); 35 | void doneBounce(); 36 | void updateBounceAnimation(QVariant value); 37 | void reverseTrigger(); 38 | void reverseStart(); 39 | 40 | void updateFinalWidth(); 41 | 42 | 43 | public slots: 44 | // Called from the ShortcutGrabber 45 | void onPrevious(); 46 | 47 | void onNext(); 48 | 49 | /*! 50 | * \brief Run a command when the user activate the notification 51 | */ 52 | void onActivate(); 53 | 54 | /*! 55 | * \brief Hide the notification 56 | */ 57 | void onHide(); 58 | 59 | /*! 60 | * \brief Display the next notification as if the user invoqued onNext() 61 | */ 62 | void autoNext(); 63 | 64 | void mousePressEvent(QMouseEvent *); 65 | 66 | void wheelEvent(QWheelEvent *e); 67 | 68 | /*! 69 | * \brief processRemoteControl Executes a command received from the client 70 | * \param command the command to run [activate|hide|previous|next] 71 | */ 72 | void processRemoteControl(QString command); 73 | 74 | /*! 75 | * \brief Receive signals from DBus. Each sent signal contains deserialized Message structure. 76 | * \param DBusInterface which signals receiving a new DBus message. 77 | */ 78 | void connectToDBus(const DBusInterface& dbus); 79 | 80 | private: 81 | /*! 82 | * \brief Get the final width of the slide after everything is set. 83 | */ 84 | int computeWidth(); 85 | 86 | void setupFont(); 87 | 88 | void setupColors(); 89 | 90 | /*! 91 | * \brief Create an appropriate connection according to the position parameter from Settings 92 | */ 93 | void connectForPosition(QString position); 94 | 95 | /*! 96 | * \brief Set the icon. 97 | */ 98 | void setupIcon(); 99 | 100 | /*! 101 | * \brief Set the "title" widget content according to the front() Message. 102 | */ 103 | void setupTitle(); 104 | 105 | /*! 106 | * \brief Set the "text" widget content according to the front() Message. 107 | */ 108 | void setupContent(); 109 | 110 | /*! 111 | * \brief Load default settings for the front() Message according to the specified profile and configuration files. 112 | */ 113 | void loadDefaults(); 114 | 115 | /*! 116 | * \brief Tries to load a Pixmap from pattern : from a file, from a setting value. 117 | */ 118 | QPixmap loadPixmap(QString pattern); 119 | 120 | /*! 121 | * \brief Update the message m if it's already in the queue 122 | * \return true if a message has been updated 123 | */ 124 | bool update(const Message& m); 125 | 126 | QPoint stringToPos(QString string); 127 | 128 | inline std::size_t getHeight(); 129 | 130 | QRect getScreenRect(); 131 | bool startMessageCommand(const Message& m, const char* key); 132 | 133 | private: 134 | Settings m_settings; 135 | QUdpSocket m_socket; 136 | QMap m_contentView; 137 | QQueue m_messageQueue; 138 | QParallelAnimationGroup m_animation; 139 | QTimer m_visible; 140 | QStack m_previousStack; 141 | QPoint tmpBouncePos; 142 | int m_computedWidth; 143 | 144 | std::string m_activePositionSlot; 145 | }; 146 | 147 | #endif // WIDGET_H 148 | --------------------------------------------------------------------------------