├── .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 | 
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 |
--------------------------------------------------------------------------------