Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added necessary file for packaging and script to start ui and daemon

  • Loading branch information...
commit afd38e571a99818c26216b810a6a3a241ac48344 1 parent 4830021
@khertan authored
Showing with 7,903 additions and 4 deletions.
  1. 0  {khweeteur-experimental → build/lib/khweeteur}/__init__.py
  2. 0  {khweeteur-experimental → build/lib/khweeteur}/bitly.py
  3. +1 −1  {khweeteur-experimental → build/lib/khweeteur}/daemon.py
  4. 0  {khweeteur-experimental → build/lib/khweeteur}/icons/favorite.png
  5. 0  {khweeteur-experimental → build/lib/khweeteur}/icons/general_chat_button.png
  6. 0  {khweeteur-experimental → build/lib/khweeteur}/icons/general_presence_home.png
  7. 0  {khweeteur-experimental → build/lib/khweeteur}/icons/geoloc.png
  8. 0  {khweeteur-experimental → build/lib/khweeteur}/icons/khweeteur.png
  9. 0  {khweeteur-experimental → build/lib/khweeteur}/icons/reply.png
  10. 0  {khweeteur-experimental → build/lib/khweeteur}/icons/retweet.png
  11. 0  {khweeteur-experimental → build/lib/khweeteur}/icons/tasklaunch_sms_chat.png
  12. 0  {khweeteur-experimental → build/lib/khweeteur}/list_model.py
  13. 0  {khweeteur-experimental → build/lib/khweeteur}/list_view.py
  14. 0  {khweeteur-experimental → build/lib/khweeteur}/notifications.py
  15. 0  {khweeteur-experimental → build/lib/khweeteur}/qbadgebutton.py
  16. 0  {khweeteur-experimental → build/lib/khweeteur}/qml_gui.py
  17. +10 −3 {khweeteur-experimental → build/lib/khweeteur}/qwidget_gui.py
  18. 0  {khweeteur-experimental → build/lib/khweeteur}/retriever.py
  19. 0  {khweeteur-experimental → build/lib/khweeteur}/settings.py
  20. 0  {khweeteur-experimental → build/lib/khweeteur}/tweetslist.py
  21. 0  {khweeteur-experimental → build/lib/khweeteur}/twitpic.py
  22. 0  {khweeteur-experimental → build/lib/khweeteur}/twitter.py
  23. +7 −0 build/scripts-2.5/khweeteur
  24. BIN  icons/hicolor/128x128/apps/khweeteur.png
  25. BIN  icons/hicolor/32x32/apps/khweeteur.png
  26. 0  {khweeteur-experimental/qml → icons/hicolor/64x64/apps}/khweeteur.png
  27. BIN  khweeteur-experimental/daemon.pyo
  28. BIN  khweeteur-experimental/retriever.pyo
  29. BIN  khweeteur-experimental/tweetslist.pyo
  30. BIN  khweeteur-experimental/twitter.pyo
  31. +7 −0 khweeteur.desktop
  32. BIN  khweeteur.png
  33. +3 −0  khweeteur.service
  34. +16 −0 khweeteur/__init__.py
  35. +213 −0 khweeteur/bitly.py
  36. +625 −0 khweeteur/daemon.py
  37. BIN  khweeteur/icons/favorite.png
  38. BIN  khweeteur/icons/general_chat_button.png
  39. BIN  khweeteur/icons/general_presence_home.png
  40. BIN  khweeteur/icons/geoloc.png
  41. BIN  khweeteur/icons/khweeteur.png
  42. BIN  khweeteur/icons/reply.png
  43. BIN  khweeteur/icons/retweet.png
  44. BIN  khweeteur/icons/tasklaunch_sms_chat.png
  45. +227 −0 khweeteur/list_model.py
  46. +649 −0 khweeteur/list_view.py
  47. +84 −0 khweeteur/notifications.py
  48. 0  {khweeteur-experimental → khweeteur}/old/__init__.py
  49. 0  {khweeteur-experimental → khweeteur}/old/client.py
  50. 0  {khweeteur-experimental → khweeteur}/old/client2.py
  51. 0  {khweeteur-experimental → khweeteur}/old/daemon.py
  52. 0  {khweeteur-experimental → khweeteur}/old/oauth2/__init__.py
  53. 0  {khweeteur-experimental → khweeteur}/old/oauth2/__init__.pyc
  54. 0  {khweeteur-experimental → khweeteur}/old/oauth2/__init__.pyo
  55. 0  {khweeteur-experimental → khweeteur}/old/oauth2/clients/__init__.py
  56. 0  {khweeteur-experimental → khweeteur}/old/oauth2/clients/__init__.pyc
  57. 0  {khweeteur-experimental → khweeteur}/old/oauth2/clients/imap.py
  58. 0  {khweeteur-experimental → khweeteur}/old/oauth2/clients/imap.pyc
  59. 0  {khweeteur-experimental → khweeteur}/old/oauth2/clients/smtp.py
  60. 0  {khweeteur-experimental → khweeteur}/old/oauth2/clients/smtp.pyc
  61. 0  {khweeteur-experimental → khweeteur}/old/objects.py
  62. 0  {khweeteur-experimental → khweeteur}/old/tweetslist.py
  63. 0  {khweeteur-experimental → khweeteur}/old/tweetslist.pyo
  64. 0  {khweeteur-experimental → khweeteur}/old/tweetslist.qml
  65. 0  {khweeteur-experimental → khweeteur}/old/twitter.py
  66. 0  {khweeteur-experimental → khweeteur}/old/twitter.pyo
  67. +145 −0 khweeteur/qbadgebutton.py
  68. 0  {khweeteur-experimental → khweeteur}/qml/add.png
  69. 0  {khweeteur-experimental → khweeteur}/qml/default.png
  70. 0  {khweeteur-experimental → khweeteur}/qml/fullsize.png
  71. 0  {khweeteur-experimental → khweeteur}/qml/house.png
  72. BIN  khweeteur/qml/khweeteur.png
  73. 0  {khweeteur-experimental → khweeteur}/qml/refresh.png
  74. 0  {khweeteur-experimental → khweeteur}/qml/tweetslist.qml
  75. +106 −0 khweeteur/qml_gui.py
  76. +773 −0 khweeteur/qwidget_gui.py
  77. +216 −0 khweeteur/retriever.py
  78. +448 −0 khweeteur/settings.py
  79. +205 −0 khweeteur/tweetslist.py
  80. +451 −0 khweeteur/twitpic.py
  81. +3,597 −0 khweeteur/twitter.py
  82. BIN  khweeteur_32.png
  83. BIN  khweeteur_64.png
  84. +15 −0 khweeteurd
  85. +7 −0 scripts/khweeteur
  86. +98 −0 setup.py
View
0  khweeteur-experimental/__init__.py → build/lib/khweeteur/__init__.py
File renamed without changes
View
0  khweeteur-experimental/bitly.py → build/lib/khweeteur/bitly.py
File renamed without changes
View
2  khweeteur-experimental/daemon.py → build/lib/khweeteur/daemon.py
@@ -608,7 +608,7 @@ def retrieve(self, options=None):
if __name__ == "__main__":
install_excepthook(__version__)
- daemon = KhweeteurDaemon('/tmp/khweeteur.pid')
+ daemon = KhweeteurDaemon('/var/run/khweeteurd/khweeteurd.pid')
if len(sys.argv) == 2:
if 'start' == sys.argv[1]:
daemon.start()
View
0  khweeteur-experimental/icons/favorite.png → build/lib/khweeteur/icons/favorite.png
File renamed without changes
View
0  ...xperimental/icons/general_chat_button.png → ...b/khweeteur/icons/general_chat_button.png
File renamed without changes
View
0  ...erimental/icons/general_presence_home.png → ...khweeteur/icons/general_presence_home.png
File renamed without changes
View
0  khweeteur-experimental/icons/geoloc.png → build/lib/khweeteur/icons/geoloc.png
File renamed without changes
View
0  khweeteur-experimental/icons/khweeteur.png → build/lib/khweeteur/icons/khweeteur.png
File renamed without changes
View
0  khweeteur-experimental/icons/reply.png → build/lib/khweeteur/icons/reply.png
File renamed without changes
View
0  khweeteur-experimental/icons/retweet.png → build/lib/khweeteur/icons/retweet.png
File renamed without changes
View
0  ...xperimental/icons/tasklaunch_sms_chat.png → ...b/khweeteur/icons/tasklaunch_sms_chat.png
File renamed without changes
View
0  khweeteur-experimental/list_model.py → build/lib/khweeteur/list_model.py
File renamed without changes
View
0  khweeteur-experimental/list_view.py → build/lib/khweeteur/list_view.py
File renamed without changes
View
0  khweeteur-experimental/notifications.py → build/lib/khweeteur/notifications.py
File renamed without changes
View
0  khweeteur-experimental/qbadgebutton.py → build/lib/khweeteur/qbadgebutton.py
File renamed without changes
View
0  khweeteur-experimental/qml_gui.py → build/lib/khweeteur/qml_gui.py
File renamed without changes
View
13 khweeteur-experimental/qwidget_gui.py → build/lib/khweeteur/qwidget_gui.py
@@ -48,6 +48,7 @@ def __init__(self,parent):
@dbus.service.signal(dbus_interface='net.khertan.Khweeteur')
def require_update(self,optional=None):
self.parent.setAttribute(Qt.WA_Maemo5ShowProgressIndicator , True)
+ print 'DEBUG : require_update'
@dbus.service.signal(dbus_interface='net.khertan.Khweeteur',
signature='uussssss')
@@ -61,6 +62,7 @@ def post_tweet(self, \
action = '',
tweet_id = '0',
):
+ print 'DEBUG : post_tweet'
pass
class KhweeteurAbout(QMainWindow):
@@ -380,11 +382,12 @@ def listen_dbus(self):
dbus.set_default_main_loop(self.dbus_loop)
self.bus = dbus.SessionBus()
#Connect the new tweet signal
- self.bus.add_signal_receiver(self.new_tweets, path='/net/khertan/Khweeteur', dbus_interface='net.khertan.Khweeteur', signal_name='new_tweets')
- self.bus.add_signal_receiver(self.stop_spinning, path='/net/khertan/Khweeteur', dbus_interface='net.khertan.Khweeteur', signal_name='refresh_ended')
+ self.bus.add_signal_receiver(self.new_tweets, path='/net/khertan/Khweeteur', dbus_interface='net.khertan.Khweeteur', signal_name='new_tweets')
+ self.bus.add_signal_receiver(self.stop_spinning, path='/net/khertan/Khweeteur', dbus_interface='net.khertan.Khweeteur', signal_name='refresh_ended')
self.dbus_handler = KhweeteurDBusHandler(self)
def stop_spinning(self):
+ print 'DEBUG : stop_spinning'
self.setAttribute(Qt.WA_Maemo5ShowProgressIndicator , False)
def new_tweets(self,count,msg):
@@ -404,12 +407,16 @@ def new_tweets(self,count,msg):
QApplication.processEvents()
if self.model.call == msg:
+ print 'DEBUG : new_tweets model.load'
self.model.load(msg)
+ print 'DEBUG : new_tweet end model.load'
+
+ print 'DEBUG : end new_tweet'
@pyqtSlot()
def show_search(self):
terms = self.sender().text()
- print 'show_search %s' % (terms,)
+ self.tb_search_button.setCounter(0)
self.home_button.setChecked(False)
self.msg_button.setChecked(False)
self.tb_search_button.setChecked(True)
View
0  khweeteur-experimental/retriever.py → build/lib/khweeteur/retriever.py
File renamed without changes
View
0  khweeteur-experimental/settings.py → build/lib/khweeteur/settings.py
File renamed without changes
View
0  khweeteur-experimental/tweetslist.py → build/lib/khweeteur/tweetslist.py
File renamed without changes
View
0  khweeteur-experimental/twitpic.py → build/lib/khweeteur/twitpic.py
File renamed without changes
View
0  khweeteur-experimental/twitter.py → build/lib/khweeteur/twitter.py
File renamed without changes
View
7 build/scripts-2.5/khweeteur
@@ -0,0 +1,7 @@
+#!/bin/sh
+if [ $# = 1 ]
+then
+ exec python /usr/lib/python2.5/site-packages/khweeteur/daemon.py
+else
+ exec python /usr/lib/python2.5/site-packages/khweeteur/__init__.py
+fi
View
BIN  icons/hicolor/128x128/apps/khweeteur.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  icons/hicolor/32x32/apps/khweeteur.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
0  khweeteur-experimental/qml/khweeteur.png → icons/hicolor/64x64/apps/khweeteur.png
File renamed without changes
View
BIN  khweeteur-experimental/daemon.pyo
Binary file not shown
View
BIN  khweeteur-experimental/retriever.pyo
Binary file not shown
View
BIN  khweeteur-experimental/tweetslist.pyo
Binary file not shown
View
BIN  khweeteur-experimental/twitter.pyo
Binary file not shown
View
7 khweeteur.desktop
@@ -0,0 +1,7 @@
+[Desktop Entry]
+Encoding=UTF-8
+Version=1.0
+Type=Application
+Name=Khweeteur
+Exec=/usr/bin/khweeteur_launch.py
+Icon=khweeteur
View
BIN  khweeteur.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
3  khweeteur.service
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=net.khertan.khweeteur
+Exec=/usr/bin/khweeteur_launch.py
View
16 khweeteur/__init__.py
@@ -0,0 +1,16 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2010 Benoît HERVIER
+# Licenced under GPLv3
+
+'''A Twitter client made with Python and Qt'''
+
+from qwidget_gui import Khweeteur
+import os.path
+
+if __name__ == '__main__':
+ from subprocess import Popen
+ Popen(['/usr/bin/python',os.path.join(os.path.dirname(__file__),'daemon.py'),'start'])
+ app = Khweeteur()
+ app.exec_()
View
213 khweeteur/bitly.py
@@ -0,0 +1,213 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2009 Empeeric LTD. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import simplejson
+import urllib,urllib2
+import urlparse
+import string
+
+BITLY_BASE_URL = "http://api.bit.ly/"
+BITLY_API_VERSION = "2.0.1"
+
+VERBS_PARAM = {
+ 'shorten':'longUrl',
+ 'expand':'shortUrl',
+ 'info':'shortUrl',
+ 'stats':'shortUrl',
+ 'errors':'',
+}
+
+class BitlyError(Exception):
+ '''Base class for bitly errors'''
+
+ @property
+ def message(self):
+ '''Returns the first argument used to construct this error.'''
+ return self.args[0]
+
+class Api(object):
+ """ API class for bit.ly """
+ def __init__(self, login, apikey):
+ self.login = login
+ self.apikey = apikey
+ self._urllib = urllib2
+
+ def shorten(self,longURL):
+ """
+ Takes either:
+ A long URL string and returns shortened URL string
+ Or a list of long URL strings and returns a list of shortened URL strings.
+ """
+ if not isinstance(longURL, list):
+ longURL = [longURL]
+
+ for index,url in enumerate(longURL):
+ if not '://' in url:
+ longURL[index] = "http://" + url
+
+ request = self._getURL("shorten",longURL)
+ result = self._fetchUrl(request)
+ json = simplejson.loads(result)
+ self._CheckForError(json)
+
+ res = []
+ for item in json['results'].values():
+ if item['shortKeywordUrl'] == "":
+ res.append(item['shortUrl'])
+ else:
+ res.append(item['shortKeywordUrl'])
+
+ if len(res) == 1:
+ return res[0]
+ else:
+ return res
+
+ def expand(self,shortURL):
+ """ Given a bit.ly url or hash, return long source url """
+ request = self._getURL("expand",shortURL)
+ result = self._fetchUrl(request)
+ json = simplejson.loads(result)
+ self._CheckForError(json)
+ return json['results'][string.split(shortURL, '/')[-1]]['longUrl']
+
+ def info(self,shortURL):
+ """
+ Given a bit.ly url or hash,
+ return information about that page,
+ such as the long source url
+ """
+ request = self._getURL("info",shortURL)
+ result = self._fetchUrl(request)
+ json = simplejson.loads(result)
+ self._CheckForError(json)
+ return json['results'][string.split(shortURL, '/')[-1]]
+
+ def stats(self,shortURL):
+ """ Given a bit.ly url or hash, return traffic and referrer data. """
+ request = self._getURL("stats",shortURL)
+ result = self._fetchUrl(request)
+ json = simplejson.loads(result)
+ self._CheckForError(json)
+ return Stats.NewFromJsonDict(json['results'])
+
+ def errors(self):
+ """ Get a list of bit.ly API error codes. """
+ request = self._getURL("errors","")
+ result = self._fetchUrl(request)
+ json = simplejson.loads(result)
+ self._CheckForError(json)
+ return json['results']
+
+ def setUrllib(self, urllib):
+ '''Override the default urllib implementation.
+
+ Args:
+ urllib: an instance that supports the same API as the urllib2 module
+ '''
+ self._urllib = urllib
+
+ def _getURL(self,verb,paramVal):
+ if not isinstance(paramVal, list):
+ paramVal = [paramVal]
+
+ params = [
+ ('version',BITLY_API_VERSION),
+ ('format','json'),
+ ('login',self.login),
+ ('apiKey',self.apikey),
+ ]
+
+ verbParam = VERBS_PARAM[verb]
+ if verbParam:
+ for val in paramVal:
+ params.append(( verbParam,val ))
+
+ encoded_params = urllib.urlencode(params)
+ return "%s%s?%s" % (BITLY_BASE_URL,verb,encoded_params)
+
+ def _fetchUrl(self,url):
+ '''Fetch a URL
+
+ Args:
+ url: The URL to retrieve
+
+ Returns:
+ A string containing the body of the response.
+ '''
+
+ # Open and return the URL
+ url_data = self._urllib.urlopen(url).read()
+ return url_data
+
+ def _CheckForError(self, data):
+ """Raises a BitlyError if bitly returns an error message.
+
+ Args:
+ data: A python dict created from the bitly json response
+ Raises:
+ BitlyError wrapping the bitly error message if one exists.
+ """
+ # bitly errors are relatively unlikely, so it is faster
+ # to check first, rather than try and catch the exception
+ if 'ERROR' in data or data['statusCode'] == 'ERROR':
+ raise BitlyError, data['errorMessage']
+ for key in data['results']:
+ if type(data['results']) is dict and type(data['results'][key]) is dict:
+ if 'statusCode' in data['results'][key] and data['results'][key]['statusCode'] == 'ERROR':
+ raise BitlyError, data['results'][key]['errorMessage']
+
+class Stats(object):
+ '''A class representing the Statistics returned by the bitly api.
+
+ The Stats structure exposes the following properties:
+ status.user_clicks # read only
+ status.clicks # read only
+ '''
+
+ def __init__(self,user_clicks=None,total_clicks=None):
+ self.user_clicks = user_clicks
+ self.total_clicks = total_clicks
+
+ @staticmethod
+ def NewFromJsonDict(data):
+ '''Create a new instance based on a JSON dict.
+
+ Args:
+ data: A JSON dict, as converted from the JSON in the bitly API
+ Returns:
+ A bitly.Stats instance
+ '''
+ return Stats(user_clicks=data.get('userClicks', None),
+ total_clicks=data.get('clicks', None))
+
+
+if __name__ == '__main__':
+ testURL1="www.yahoo.com"
+ testURL2="www.cnn.com"
+ a=Api(login="pythonbitly",apikey="R_06871db6b7fd31a4242709acaf1b6648")
+ short=a.shorten(testURL1)
+ print "Short URL = %s" % short
+ urlList=[testURL1,testURL2]
+ shortList=a.shorten(urlList)
+ print "Short URL list = %s" % shortList
+ long=a.expand(short)
+ print "Expanded URL = %s" % long
+ info=a.info(short)
+ print "Info: %s" % info
+ stats=a.stats(short)
+ print "User clicks %s, total clicks: %s" % (stats.user_clicks,stats.total_clicks)
+ errors=a.errors()
+ print "Errors: %s" % errors
View
625 khweeteur/daemon.py
@@ -0,0 +1,625 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Benoît HERVIER
+# Licenced under GPLv3
+
+#import sip
+#sip.setapi('QString', 2)
+#sip.setapi('QVariant', 2)
+
+from __future__ import with_statement
+
+import sys
+import time
+from PySide.QtCore import QSettings
+import atexit
+import os
+from signal import SIGTERM
+
+import logging
+
+from retriever import KhweeteurRefreshWorker
+from settings import SUPPORTED_ACCOUNTS
+import gobject
+gobject.threads_init()
+import socket
+import pickle
+import re
+
+__version__ = '0.5.0'
+
+import dbus
+from dbus.mainloop.glib import DBusGMainLoop
+DBusGMainLoop(set_as_default=True)
+import threading
+
+import twitter
+from urllib import urlretrieve
+import urllib2
+import pickle
+import glob
+
+try:
+ from PIL import Image
+except:
+ import Image
+
+from PySide.QtCore import QSettings
+
+from threading import Thread
+
+import logging
+import os
+import os.path
+import dbus
+import dbus.service
+
+
+#A hook to catch errors
+def install_excepthook(version):
+ '''Install an excepthook called at each unexcepted error'''
+ __version__ = version
+
+ def my_excepthook(exctype, value, tb):
+ '''Method which replace the native excepthook'''
+ #traceback give us all the errors information message like
+ # the method, file line ... everything like
+ # we have in the python interpreter
+ import traceback
+ trace_s = ''.join(traceback.format_exception(exctype, value, tb))
+ print 'Except hook called : %s' % (trace_s)
+ formatted_text = "%s Version %s\nTrace : %s" % ('Khweeteur', __version__, trace_s)
+ logging.error(formatted_text)
+
+ sys.excepthook = my_excepthook
+
+
+class Daemon:
+ """
+ A generic daemon class.
+ Usage: subclass the Daemon class and override the run() method
+ """
+
+ def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
+ self.stdin = stdin
+ self.stdout = stdout
+ self.stderr = stderr
+ self.pidfile = pidfile
+
+ def daemonize(self):
+ """
+ do the UNIX double-fork magic, see Stevens' "Advanced
+ Programming in the UNIX Environment" for details (ISBN 0201563177)
+ http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
+ """
+ try:
+ pid = os.fork()
+ if pid > 0:
+ # exit first parent
+ sys.exit(0)
+ except OSError, e:
+ sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
+ sys.exit(1)
+
+ # decouple from parent environment
+ os.chdir("/")
+ os.setsid()
+ os.umask(0)
+
+ # do second fork
+ try:
+ pid = os.fork()
+ if pid > 0:
+ # exit from second parent
+ sys.exit(0)
+ except OSError, e:
+ sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
+ sys.exit(1)
+
+ # redirect standard file descriptors
+ sys.stdout.flush()
+ sys.stderr.flush()
+ si = file(self.stdin, 'r')
+ so = file(self.stdout, 'a+')
+ se = file(self.stderr, 'a+', 0)
+ os.dup2(si.fileno(), sys.stdin.fileno())
+ os.dup2(so.fileno(), sys.stdout.fileno())
+ os.dup2(se.fileno(), sys.stderr.fileno())
+
+ # write pidfile
+ atexit.register(self.delpid)
+ pid = str(os.getpid())
+ file(self.pidfile, 'w+').write("%s\n" % pid)
+
+ def delpid(self):
+ os.remove(self.pidfile)
+
+ def start(self):
+ """
+ Start the daemon
+ """
+ # Check for a pidfile to see if the daemon already runs
+ try:
+ pf = file(self.pidfile, 'r')
+ pid = int(pf.read().strip())
+ pf.close()
+ except IOError:
+ pid = None
+
+ if pid:
+ try:
+ os.kill(pid, 0)
+ message = "pidfile %s already exist. Daemon already running?\n"
+ sys.stderr.write(message % self.pidfile)
+ sys.exit(1)
+
+ except OSError, err:
+ sys.stderr.write('pidfile %s already exist. But daemon is dead.\n' % self.pidfile)
+
+ # Start the daemon
+ self.daemonize()
+ self.run()
+
+ def stop(self):
+ """
+ Stop the daemon
+ """
+ # Get the pid from the pidfile
+ try:
+ pf = file(self.pidfile, 'r')
+ pid = int(pf.read().strip())
+ pf.close()
+ except IOError:
+ pid = None
+
+ if not pid:
+ message = "pidfile %s does not exist. Daemon not running?\n"
+ sys.stderr.write(message % self.pidfile)
+ return # not an error in a restart
+
+ # Try killing the daemon process
+ try:
+ while 1:
+ os.kill(pid, SIGTERM)
+ time.sleep(0.1)
+ except OSError, err:
+ err = str(err)
+ if err.find("No such process") > 0:
+ if os.path.exists(self.pidfile):
+ os.remove(self.pidfile)
+ else:
+ print str(err)
+ sys.exit(1)
+
+ def restart(self):
+ """
+ Restart the daemon
+ """
+ self.stop()
+ self.start()
+
+ def run(self):
+ """
+ You should override this method when you subclass Daemon. It will be called after the process has been
+ daemonized by start() or restart().
+ """
+
+
+class KhweeteurDBusHandler(dbus.service.Object):
+
+ def __init__(self):
+ dbus.service.Object.__init__(self, dbus.SessionBus(), '/net/khertan/Khweeteur')
+ self.m_id = 0
+
+ @dbus.service.signal(dbus_interface='net.khertan.Khweeteur',
+ signature='')
+ def refresh_ended(self):
+ pass
+
+ @dbus.service.signal(dbus_interface='net.khertan.Khweeteur',
+ signature='us')
+ def new_tweets(self, count, ttype):
+ logging.debug('New tweet notification ttype : %s (%s)' % (ttype,str(type(ttype)),))
+ if ttype in ('Mentions', 'DMs'):
+ m_bus = dbus.SystemBus()
+ m_notify = m_bus.get_object('org.freedesktop.Notifications',
+ '/org/freedesktop/Notifications')
+ iface = dbus.Interface(m_notify, 'org.freedesktop.Notifications')
+ m_id = 0
+
+ if ttype == 'DMs':
+ msg = 'New DMs'
+ elif ttype == 'Mentions':
+ msg = 'New mentions'
+ else:
+ msg = 'New tweets'
+ try:
+ self.m_id = iface.Notify('Khweeteur',
+ self.m_id,
+ 'khweeteur',
+ msg,
+ msg,
+ ['default', 'call'],
+ {'category': 'khweeteur-new-tweets',
+ 'desktop-entry': 'khweeteur',
+ 'dbus-callback-default': 'net.khertan.khweeteur /net/khertan/khweeteur net.khertan.khweeteur show_now',
+ 'count': count,
+ 'amount': count},
+ -1)
+ except:
+ pass
+
+
+class KhweeteurDaemon(Daemon):
+
+ def run(self):
+ logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s %(levelname)-8s %(message)s',
+ datefmt='%a, %d %b %Y %H:%M:%S',
+ filename='/home/user/.khweeteur.log',
+ filemode='w')
+
+ self.bus = dbus.SessionBus()
+ self.bus.add_signal_receiver(self.update, path='/net/khertan/Khweeteur', dbus_interface='net.khertan.Khweeteur', signal_name='require_update')
+ self.bus.add_signal_receiver(self.post_tweet, path='/net/khertan/Khweeteur', dbus_interface='net.khertan.Khweeteur', signal_name='post_tweet')
+ self.threads = [] #Here to avoid gc
+
+ #Cache Folder
+ self.cache_path = os.path.join(os.path.expanduser("~"),\
+ '.khweeteur', 'cache')
+ if not os.path.exists(self.cache_path):
+ os.makedirs(self.cache_path)
+ #Post Folder
+ self.post_path = os.path.join(os.path.expanduser("~"),\
+ '.khweeteur', 'topost')
+ if not os.path.exists(self.post_path):
+ os.makedirs(self.post_path)
+
+ self.dbus_handler = KhweeteurDBusHandler()
+
+ loop = gobject.MainLoop()
+ gobject.timeout_add_seconds(1, self.update)
+ logging.debug('Timer added')
+ loop.run()
+
+ def post_tweet(self, \
+ shorten_url=True,\
+ serialize=True,\
+ text='',\
+ lattitude='',
+ longitude='',
+ base_url='',
+ action='',
+ tweet_id='',
+ ):
+ with open(os.path.join(self.post_path, str(time.time())), 'wb') as fhandle:
+ post = {'shorten_url': shorten_url,
+ 'serialize': serialize,
+ 'text': text,
+ 'lattitude': lattitude,
+ 'longitude': longitude,
+ 'base_url': base_url,
+ 'action': action,
+ 'tweet_id': tweet_id,}
+ logging.debug('%s' % (post.__repr__(),))
+ pickle.dump(post, fhandle, pickle.HIGHEST_PROTOCOL)
+ self.do_posts()
+
+ def get_api(self,account):
+ api = \
+ twitter.Api(username=account['consumer_key'],
+ password=account['consumer_secret'],
+ access_token_key=account['token_key'],
+ access_token_secret=account['token_secret'],
+ base_url=account['base_url'],)
+ api.SetUserAgent('Khweeteur')
+
+ return api
+
+ def do_posts(self):
+ settings = QSettings("Khertan Software", "Khweeteur")
+ accounts = []
+ nb_accounts = settings.beginReadArray('accounts')
+ for index in range(nb_accounts):
+ settings.setArrayIndex(index)
+ accounts.append(dict((key, settings.value(key)) for key in settings.allKeys()))
+ settings.endArray()
+
+ logging.debug('Number of account : %s' % len(accounts))
+
+ for item in glob.glob(os.path.join(self.post_path, '*')):
+ logging.debug('Try to post %s' % (item,))
+ try:
+ with open(item, 'rb') as fhandle:
+ post = pickle.load(fhandle)
+ text = post['text']
+ if post['shorten_url'] == 1:
+ urls = re.findall("(?P<url>https?://[^\s]+)", text)
+ if len(urls) > 0:
+ import bitly
+ a = bitly.Api(login='pythonbitly',
+ apikey='R_06871db6b7fd31a4242709acaf1b6648')
+
+ for url in urls:
+ try:
+ short_url = a.shorten(url)
+ text = text.replace(url, short_url)
+ except:
+ pass
+ if post['lattitude'] == '':
+ post['lattitude'] = None
+ else:
+ post['lattitude'] = int(post['lattitude'])
+ if post['longitude'] == '':
+ post['longitude'] = None
+ else:
+ post['longitude'] = int(post['longitude'])
+
+ #Loop on accounts
+ for account in accounts:
+ #Reply
+ if post['action'] == 'reply': #Reply tweet
+ if account['base_url'] == post['base_url'] \
+ and account['use_for_tweet'] == 'true':
+ api = self.get_api(account)
+ if post['serialize'] == 1:
+ api.PostSerializedUpdates(text,
+ in_reply_to_status_id=int(post['tweet_id']),
+ latitude=post['lattitude'], longitude=post['longitude'])
+ else:
+ api.PostUpdate(text,
+ in_reply_to_status_id=int(post['tweet_id']),
+ latitude=post['lattitude'], longitude=post['longitude'])
+ logging.debug('Posted reply %s' % (text,))
+ elif post['action'] == 'retweet':
+ #Retweet
+ if account['base_url'] == post['base_url'] \
+ and account['use_for_tweet'] == 'true':
+ api = self.get_api(account)
+ api.PostRetweet(tweet_id=int(post['tweet_id']))
+ logging.debug('Posted retweet %s' % (post['tweet_id'],))
+ elif post['action'] == 'tweet':
+ #Else "simple" tweet
+ if account['use_for_tweet'] == 'true':
+ api = self.get_api(account)
+ if post['serialize'] == 1:
+ api.PostSerializedUpdates(text,
+ latitude=post['lattitude'], longitude=post['longitude'])
+ else:
+ api.PostUpdate(text,
+ latitude=post['lattitude'], longitude=post['longitude'])
+ logging.debug('Posted %s' % (text,))
+ elif post['action'] == 'delete':
+ if account['base_url'] == post['base_url']:
+ api = self.get_api(account)
+ api.DestroyStatus(int(post['tweet_id']))
+ path = os.path.join(os.path.expanduser('~'), \
+ '.khweeteur', \
+ 'cache', \
+ 'HomeTimeline', \
+ post['tweet_id'])
+ os.remove(path)
+ logging.debug('Deleted %s' % (post['tweet_id'],))
+ elif post['action'] == 'favorite':
+ if account['base_url'] == post['base_url']:
+ api = self.get_api(account)
+ api.CreateFavorite(int(post['tweet_id']))
+ logging.debug('Favorited %s' % (post['tweet_id'],))
+ elif post['action'] == 'follow':
+ if account['base_url'] == post['base_url']:
+ api = self.get_api(account)
+ api.CreateFriendship(int(post['tweet_id']))
+ logging.debug('Follow %s' % (post['tweet_id'],))
+ elif post['action'] == 'unfollow':
+ if account['base_url'] == post['base_url']:
+ api = self.get_api(account)
+ api.DestroyFriendship(int(post['tweet_id']))
+ logging.debug('Follow %s' % (post['tweet_id'],))
+ else:
+ logging.error('Unknow action : %s' % post['action'])
+
+ os.remove(item)
+
+ except twitter.TwitterError, err:
+ if err.message == 'Status is a duplicate':
+ os.remove(item)
+ else:
+ logging.error('Do_posts : %s' % (err.message,))
+ except StandardError, err:
+ logging.error('Do_posts : %s' % (str(err),))
+ #Emitting the error will block the other tweet post
+ #raise #can t post, we will keep the file to do it later
+ except:
+ logging.error('Do_posts : Unknow error')
+
+ def post_twitpic(self, file_path, text):
+ settings = QSettings("Khertan Software", "Khweeteur")
+
+ import twitpic
+ import oauth2 as oauth
+ import simplejson
+
+ nb_accounts = settings.beginReadArray('accounts')
+ for index in range(nb_accounts):
+ settings.setArrayIndex(index)
+ if (settings.value('base_url') == SUPPORTED_ACCOUNTS[0]['base_url']) \
+ and (settings.value('use_for_tweet') == Qt.CheckState):
+ api = twitter.Api(username=settings.value('consumer_key'),
+ password=settings.value('consumer_secret'),
+ access_token_key=settings.value('token_key'),
+ access_token_secret=settings.value('token_secret'),
+ base_url=SUPPORTED_ACCOUNTS[0]['base_url'])
+ twitpic_client = twitpic.TwitPicOAuthClient(
+ consumer_key=settings.value('consumer_key'),
+ consumer_secret=settings.value('consumer_secret'),
+ access_token=api._oauth_token.to_string(),
+ service_key='f9b7357e0dc5473df5f141145e4dceb0')
+
+ params = {}
+ params['media'] = 'file://' + file_path
+ params['message'] = text
+ response = twitpic_client.create('upload', params)
+
+ if 'url' in response:
+ self.post(text=url)
+
+ settings.endArray()
+
+ def update(self, option=None):
+ settings = QSettings("Khertan Software", "Khweeteur")
+ logging.debug('Setting loaded')
+ settings.sync()
+
+ #Verify the default interval
+ if not settings.contains('refresh_interval'):
+ refresh_interval = 600
+ else:
+ refresh_interval = int(settings.value('refresh_interval')) * 60
+ if refresh_interval < 600:
+ refresh_interval = 600
+ logging.debug('refresh interval loaded')
+
+ self.do_posts()
+ self.retrieve()
+ gobject.timeout_add_seconds(refresh_interval, self.update)
+ return False
+
+ def retrieve(self, options=None):
+ settings = QSettings("Khertan Software", "Khweeteur")
+ logging.debug('Setting loaded')
+ try:
+ #Re read the settings
+ settings.sync()
+ logging.debug('Setting synced')
+
+ #Cleaning old thread reference for keep for gc
+ for thread in self.threads:
+ if not thread.isAlive():
+ self.threads.remove(thread)
+ logging.debug('Removed a thread')
+
+ #Remove old tweets in cache according to history prefs
+ try:
+ keep = int(settings.value('tweetHistory'))
+ except:
+ keep = 60
+
+ for root, folders, files in os.walk(self.cache_path):
+ for folder in folders:
+ statuses = []
+ uids = glob.glob(os.path.join(root, folder, '*'))
+ for uid in uids:
+ uid = os.path.basename(uid)
+ try:
+ pkl_file = open(os.path.join(root, folder, uid), 'rb')
+ status = pickle.load(pkl_file)
+ pkl_file.close()
+ statuses.append(status)
+ except StandardError, err:
+ logging.debug('Error in cache cleaning: %s,%s' % (err, os.path.join(root, uid)))
+ statuses.sort(key=lambda status: status.created_at_in_seconds, reverse=True)
+ for status in statuses[keep:]:
+ try:
+ os.remove(os.path.join(root, folder, str(status.id)))
+ except StandardError, err:
+ logging.debug('Cannot remove : %s : %s' % (str(status.id), str(err)))
+
+ nb_searches = settings.beginReadArray('searches')
+ searches = []
+ for index in range(nb_searches):
+ settings.setArrayIndex(index)
+ searches.append(settings.value('terms'))
+ settings.endArray()
+
+ nb_accounts = settings.beginReadArray('accounts')
+ logging.info('Found %s account' % (str(nb_accounts),))
+ for index in range(nb_accounts):
+ settings.setArrayIndex(index)
+ #Worker
+ try:
+ self.threads.append(KhweeteurRefreshWorker(\
+ settings.value('base_url'),
+ settings.value('consumer_key'),
+ settings.value('consumer_secret'),
+ settings.value('token_key'),
+ settings.value('token_secret'),
+ 'HomeTimeline', self.dbus_handler))
+ except Exception, err:
+ logging.error('Timeline : %s' % str(err))
+
+ try:
+ self.threads.append(KhweeteurRefreshWorker(\
+ settings.value('base_url'),
+ settings.value('consumer_key'),
+ settings.value('consumer_secret'),
+ settings.value('token_key'),
+ settings.value('token_secret'),
+ 'Mentions', self.dbus_handler))
+ except Exception, err:
+ logging.error('Mentions : %s' % str(err))
+
+ try:
+ self.threads.append(KhweeteurRefreshWorker(\
+ settings.value('base_url'),
+ settings.value('consumer_key'),
+ settings.value('consumer_secret'),
+ settings.value('token_key'),
+ settings.value('token_secret'),
+ 'DMs', self.dbus_handler))
+ except Exception, err:
+ logging.error('DMs : %s' % str(err))
+
+ #Start searches thread
+ for terms in searches:
+ try:
+ self.threads.append(KhweeteurRefreshWorker(\
+ settings.value('base_url'),
+ settings.value('consumer_key'),
+ settings.value('consumer_secret'),
+ settings.value('token_key'),
+ settings.value('token_secret'),
+ 'Search:'+terms, self.dbus_handler))
+ except Exception, err:
+ logging.error('Search %s: %s' % (terms,str(err)))
+
+ try:
+ for idx, thread in enumerate(self.threads):
+ logging.debug('Try to run Thread : %s' % str(thread))
+ try:
+ self.threads[idx].start()
+ except RuntimeError, err:
+ logging.debug('Attempt to start a thread already running : %s' % (str(err),))
+ except:
+ logging.error('Running Thread error')
+
+ settings.endArray()
+
+ while any([thread.isAlive() for thread in self.threads]):
+ time.sleep(1)
+
+ self.dbus_handler.refresh_ended()
+
+ logging.debug('Finished loop')
+
+ except StandardError, err:
+ logging.exception(str(err))
+ logging.debug(str(err))
+
+
+if __name__ == "__main__":
+ install_excepthook(__version__)
+ daemon = KhweeteurDaemon('/var/run/khweeteurd/khweeteurd.pid')
+ if len(sys.argv) == 2:
+ if 'start' == sys.argv[1]:
+ daemon.start()
+ elif 'stop' == sys.argv[1]:
+ daemon.stop()
+ elif 'restart' == sys.argv[1]:
+ daemon.restart()
+ else:
+ print "Unknown command"
+ sys.exit(2)
+ sys.exit(0)
+ else:
+ print "usage: %s start|stop|restart" % sys.argv[0]
+ sys.exit(2)
View
BIN  khweeteur/icons/favorite.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  khweeteur/icons/general_chat_button.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  khweeteur/icons/general_presence_home.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  khweeteur/icons/geoloc.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  khweeteur/icons/khweeteur.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  khweeteur/icons/reply.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  khweeteur/icons/retweet.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  khweeteur/icons/tasklaunch_sms_chat.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
227 khweeteur/list_model.py
@@ -0,0 +1,227 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2010 Benoît HERVIER
+# Licenced under GPLv3
+
+'''A simple Twitter client made with pyqt4 : QModel'''
+
+#import sip
+#sip.setapi('QString', 2)
+#sip.setapi('QVariant', 2)
+
+import time
+import pickle
+import datetime
+import glob
+from notifications import KhweeteurNotification
+import os
+
+SCREENNAMEROLE = 20
+REPLYTOSCREENNAMEROLE = 21
+REPLYTEXTROLE = 22
+REPLYIDROLE = 25
+IDROLE = 23
+ORIGINROLE = 24
+TIMESTAMPROLE = 26
+RETWEETOFROLE = 27
+ISMEROLE = 28
+PROTECTEDROLE = 28
+USERIDROLE = 29
+
+from PySide.QtCore import QAbstractListModel,QModelIndex, \
+ QThread, \
+ Qt, \
+ QSettings, \
+ QObject, \
+ Signal
+
+from PySide.QtGui import QPixmap
+
+pyqtSignal = Signal
+
+class KhweetsModel(QAbstractListModel):
+
+ """ListModel : A simple list : Start_At,TweetId, Users Screen_name, Tweet Text, Profile Image"""
+
+ dataChanged = pyqtSignal(QModelIndex,QModelIndex)
+
+ def __init__(self):
+ QAbstractListModel.__init__(self)
+
+ # Cache the passed data list as a class member.
+
+ self._items = []
+ self._uids = []
+
+ self._avatars = {}
+ self.now = time.time()
+ self.call = None
+
+ def setLimit(self, limit):
+ self.khweets_limit = limit
+
+ def getCacheFolder(self):
+ return os.path.join(os.path.expanduser("~"), \
+ '.khweeteur','cache', \
+ os.path.normcase(unicode(self.call.replace('/', \
+ '_'))).encode('UTF-8'))
+
+ def rowCount(self, parent=QModelIndex()):
+ return len(self._items)
+
+ def refreshTimestamp(self):
+ self.now = time.time()
+ self.dataChanged.emit(self.createIndex(0, 0),
+ self.createIndex(0,
+ len(self._items)))
+
+ def addStatuses(self, uids):
+ #Optimization
+ folder_path = self.getCacheFolder()
+ pickleload = pickle.load
+ try:
+ keys = []
+ for uid in uids:
+ try:
+ pkl_file = open(os.path.join(folder_path,
+ str(uid)), 'rb')
+ status = pickleload(pkl_file)
+ pkl_file.close()
+
+ #Test if status already exists
+ if status.id not in self._uids:
+ self._uids.append(status.id)
+ self._items.append(status)
+
+ except StandardError, e:
+ print e
+
+ except StandardError, e:
+ print "We shouldn't got this error here :", e
+ import traceback
+ traceback.print_exc()
+
+ self._items.sort()
+ self._uids.sort()
+ self.dataChanged.emit(self.createIndex(0, 0),
+ self.createIndex(0,
+ len(self._items)))
+
+ def destroyStatus(self, index):
+ self._items.pop(index.row())
+ self.dataChanged.emit(self.createIndex(0, 0),
+ self.createIndex(0,
+ len(self._items)))
+
+
+ def load(self,call):
+
+ self.now = time.time()
+
+ if self.call != call:
+ self._items=[]
+ self.call = call
+
+ try:
+ folder = self.getCacheFolder()
+ uids = glob.glob(folder + u'/*')
+ pickleload = pickle.load
+ avatar_path = os.path.join(os.path.expanduser("~"),
+ '.khweeteur','avatars')
+ for uid in uids:
+ if uid not in [status.id for status in self._items]:
+ pkl_file = open(os.path.join(folder,
+ str(uid)), 'rb')
+ status = pickleload(pkl_file)
+ pkl_file.close()
+
+ #Test if status already exists
+ if status not in self._items:
+ self._uids.append(status.id)
+ self._items.append(status)
+ if hasattr(status, 'user'):
+ profile_image = os.path.basename(status.user.profile_image_url.replace('/'
+ , '_'))
+ else:
+ profile_image = '/opt/usr/share/icons/hicolor/64x64/hildon/general_default_avatar.png'
+
+ if profile_image not in self._avatars:
+ try:
+ self._avatars[status.user.profile_image_url] = QPixmap(os.path.join(avatar_path,
+ profile_image))
+ except:
+ pass
+
+ self._items.sort(key=lambda status:status.created_at_in_seconds, reverse=True)
+
+ self.dataChanged.emit(self.createIndex(0, 0),
+ self.createIndex(0,
+ len(self._items)))
+
+ except StandardError, e:
+ print 'unSerialize : ', e
+
+ def data(self, index, role=Qt.DisplayRole):
+
+ if role == Qt.DisplayRole:
+ status = self._items[index.row()]
+ try:
+ if status.truncated:
+ return status.retweeted_status.text
+ else:
+ return status.text
+ except:
+ return status.text
+ elif role == SCREENNAMEROLE:
+ try:
+ return self._items[index.row()].user.screen_name
+ except:
+ return self._items[index.row()].sender_screen_name
+ elif role == IDROLE:
+ return self._items[index.row()].id
+ elif role == REPLYIDROLE:
+ try:
+ return self._items[index.row()].in_reply_to_status_id
+ except:
+ return None
+ elif role == REPLYTOSCREENNAMEROLE:
+ try:
+ return self._items[index.row()].in_reply_to_screen_name
+ except:
+ return None
+ elif role == REPLYTEXTROLE:
+ return self._items[index.row()].in_reply_to_status_text
+ elif role == ORIGINROLE:
+ return self._items[index.row()].base_url
+ elif role == RETWEETOFROLE:
+ try:
+ return self._items[index.row()].retweeted_status
+ except:
+ return None
+ elif role == ISMEROLE:
+ try:
+ return self._items[index.row()].is_me
+ except:
+ return False
+
+ elif role == TIMESTAMPROLE:
+ return self._items[index.row()].GetRelativeCreatedAt(self.now)
+
+ elif role == PROTECTEDROLE:
+ return self._items[index.row()].user.protected
+
+ elif role == USERIDROLE:
+ return self._items[index.row()].user.id
+
+ elif role == Qt.DecorationRole:
+ try:
+ return self._avatars[self._items[index.row()].user.profile_image_url]
+ except:
+ return None
+ else:
+ return None
+
+ def wantsUpdate(self):
+ #QObject.emit(self, SIGNAL('layoutChanged()'))
+ self.layoutChanged.emit()
View
649 khweeteur/list_view.py
@@ -0,0 +1,649 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2010 Benoît HERVIER
+# Licenced under GPLv3
+
+'''A simple Twitter client made with pyqt4 : QListView'''
+
+import sip
+sip.setapi('QString', 2)
+sip.setapi('QVariant', 2)
+
+SCREENNAMEROLE = 20
+REPLYTOSCREENNAMEROLE = 21
+REPLYTEXTROLE = 22
+REPLYIDROLE = 25
+IDROLE = 23
+ORIGINROLE = 24
+TIMESTAMPROLE = 26
+RETWEETOFROLE = 27
+ISMEROLE = 28
+
+
+from PySide.QtGui import QStyledItemDelegate, \
+ QListView, \
+ QColor, \
+ QAbstractItemView, \
+ QFontMetrics, \
+ QFont, \
+ QStyle
+from PySide.QtCore import Qt, \
+ QSize, \
+ QSettings
+
+from settings import KhweeteurPref
+
+class WhiteCustomDelegate(QStyledItemDelegate):
+
+ '''Delegate to do custom draw of the items'''
+
+ def __init__(self, parent):
+ '''Initialization'''
+
+ QStyledItemDelegate.__init__(self, parent)
+
+ self.bg_color = QColor('#FFFFFF')
+ self.bg_alternate_color = QColor('#dddddd')
+ self.user_color = QColor('#7AB4F5')
+ self.time_color = QColor('#7AB4F5')
+ self.replyto_color = QColor('#7AB4F5')
+
+ self.text_color = QColor('#000000')
+ self.separator_color = QColor('#000000')
+
+
+class DefaultCustomDelegate(QStyledItemDelegate):
+
+ '''Delegate to do custom draw of the items'''
+
+ memoized_size = {}
+ memoized_width = {}
+
+ def __init__(self, parent):
+ '''Initialization'''
+
+ QStyledItemDelegate.__init__(self, parent)
+ self.show_avatar = True
+ self.show_screenname = True
+ self.show_timestamp = True
+ self.show_replyto = True
+
+ self.bg_color = QColor('#000000')
+ self.bg_alternate_color = QColor('#333333')
+ self.user_color = QColor('#7AB4F5')
+ self.time_color = QColor('#7AB4F5')
+ self.replyto_color = QColor('#7AB4F5')
+
+ self.text_color = QColor('#FFFFFF')
+ self.separator_color = QColor('#000000')
+
+ self.fm = None
+ self.minifm = None
+
+ self.normFont = None
+ self.miniFont = None
+
+ def sizeHint(self, option, index):
+ '''Custom size calculation of our items'''
+
+ uid = str(index.data(role=IDROLE)) + 'x' + \
+ str(option.rect.width())
+ try:
+ return self.memoized_size[uid]
+ except:
+ size = QStyledItemDelegate.sizeHint(self, option, index)
+ tweet = index.data(Qt.DisplayRole)
+
+ # One time is enought sizeHint need to be fast
+
+ if not self.fm:
+ self.fm = QFontMetrics(option.font)
+ height = self.fm.boundingRect(
+ 0,
+ 0,
+ option.rect.width() - 75,
+ 800,
+ int(Qt.AlignTop) | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap),
+ tweet,
+ ).height() + 40
+
+ if self.show_replyto:
+ reply_name = index.data(role=REPLYTOSCREENNAMEROLE)
+ reply_text = index.data(role=REPLYTEXTROLE)
+ if reply_name and reply_text:
+
+ # One time is enought sizeHint need to be fast
+
+ reply = 'In reply to @' + reply_name + ' : ' \
+ + reply_text
+ if not self.minifm:
+ if not self.miniFont:
+ self.miniFont = QFont(option.font)
+ self.miniFont.setPointSizeF(option.font.pointSizeF()
+ * 0.80)
+ self.minifm = QFontMetrics(self.miniFont)
+ height += self.minifm.boundingRect(
+ 0,
+ 0,
+ option.rect.width() - 75,
+ 800,
+ int(Qt.AlignTop) | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap),
+ reply,
+ ).height()
+ elif reply_name:
+ reply = 'In reply to @' + reply_name
+ if not self.minifm:
+ if not self.miniFont:
+ self.miniFont = QFont(option.font)
+ self.miniFont.setPointSizeF(option.font.pointSizeF()
+ * 0.80)
+ self.minifm = QFontMetrics(self.miniFont)
+ height += self.minifm.boundingRect(
+ 0,
+ 0,
+ option.rect.width() - 75,
+ 800,
+ int(Qt.AlignTop) | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap),
+ reply,
+ ).height()
+
+ if height < 70:
+ height = 70
+
+ self.memoized_size[uid] = QSize(size.width(), height)
+ return self.memoized_size[uid]
+
+ def paint(
+ self,
+ painter,
+ option,
+ index,
+ ):
+ '''Paint our tweet'''
+
+# if not USE_PYSIDE:
+ (x1, y1, x2, y2) = option.rect.getCoords()
+# else:
+ #Work arround Pyside bug #544
+# y1 = option.rect.y()
+# y2 = y1 + option.rect.height()
+# x1 = option.rect.x()
+# x2 = x1 + option.rect.width()
+#
+ # Ugly hack ?
+ if y1 < 0 and y2 < 0:
+ return
+
+ if not self.fm:
+ self.fm = QFontMetrics(option.font)
+
+ model = index.model()
+ tweet = index.data(Qt.DisplayRole)
+ is_me = index.data(ISMEROLE)
+
+ # Instantiate font only one time !
+
+ if not self.normFont:
+ self.normFont = QFont(option.font)
+ self.miniFont = QFont(option.font)
+ self.miniFont.setPointSizeF(option.font.pointSizeF() * 0.80)
+
+ painter.save()
+
+ # Draw alternate ?
+
+ if index.row() % 2 == 0:
+ painter.fillRect(option.rect, self.bg_color)
+ else:
+ painter.fillRect(option.rect, self.bg_alternate_color)
+
+ # highlight selected items
+
+ if option.state & QStyle.State_Selected:
+ painter.fillRect(option.rect, option.palette.highlight())
+
+ # Draw icon
+
+ if self.show_avatar:
+ icon = index.data(Qt.DecorationRole)
+ if icon != None:
+ if is_me:
+ painter.drawPixmap(x2 -60, y1 + 10, 50, 50, icon)
+ else:
+ painter.drawPixmap(x1 + 10, y1 + 10, 50, 50, icon)
+
+ # Draw tweet
+
+ painter.setPen(self.text_color)
+ if is_me:
+ new_rect = \
+ painter.drawText(option.rect.adjusted(4, 5, -70, 0), int(Qt.AlignTop)
+ | int(Qt.AlignRight)
+ | int(Qt.TextWordWrap), tweet)
+ else:
+ new_rect = \
+ painter.drawText(option.rect.adjusted(int(self.show_avatar)
+ * 70, 5, -4, 0), int(Qt.AlignTop)
+ | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap), tweet)
+
+ # Draw Timeline
+
+ if self.show_timestamp:
+ time = index.data(role=TIMESTAMPROLE)
+ painter.setFont(self.miniFont)
+ painter.setPen(self.time_color)
+ if is_me:
+ painter.drawText(option.rect.adjusted(4, 10, -80, -9),
+ int(Qt.AlignBottom) | int(Qt.AlignRight),
+ time)
+ else:
+ painter.drawText(option.rect.adjusted(70, 10, -10, -9),
+ int(Qt.AlignBottom) | int(Qt.AlignRight),
+ time)
+
+ # Draw screenname
+
+ if self.show_screenname:
+ screenname = index.data(SCREENNAMEROLE)
+ retweet_of = index.data(RETWEETOFROLE)
+ if retweet_of:
+ screenname = '%s : Retweet of %s' % (screenname, retweet_of.user.screen_name)
+ painter.setFont(self.miniFont)
+ painter.setPen(self.user_color)
+ if is_me:
+ painter.drawText(option.rect.adjusted(4, 10, -70, -9),
+ int(Qt.AlignBottom) | int(Qt.AlignLeft),
+ screenname)
+ else:
+ painter.drawText(option.rect.adjusted(70, 10, -10, -9),
+ int(Qt.AlignBottom) | int(Qt.AlignLeft),
+ screenname)
+
+ # Draw reply
+
+ if self.show_replyto:
+ reply_name = index.data(role=REPLYTOSCREENNAMEROLE)
+ reply_text = index.data(role=REPLYTEXTROLE)
+ if reply_name and reply_text:
+ reply = 'In reply to ' + reply_name + ' : ' \
+ + reply_text
+ painter.setFont(self.miniFont)
+ painter.setPen(self.replyto_color)
+ if is_me:
+ new_rect = \
+ painter.drawText(option.rect.adjusted(4, new_rect.height() + 5, -70, 0),
+ int(Qt.AlignTop) | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap), reply)
+ else:
+ new_rect = \
+ painter.drawText(option.rect.adjusted(int(self.show_avatar)
+ * 70, new_rect.height() + 5, -4, 0),
+ int(Qt.AlignTop) | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap), reply)
+ elif reply_name:
+ reply = 'In reply to ' + reply_name
+ painter.setFont(self.miniFont)
+ painter.setPen(self.replyto_color)
+ if is_me:
+ new_rect = \
+ painter.drawText(option.rect.adjusted(4, new_rect.height() + 5, -70, 0),
+ int(Qt.AlignTop) | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap), reply)
+ else:
+ new_rect = \
+ painter.drawText(option.rect.adjusted(int(self.show_avatar)
+ * 70, new_rect.height() + 5, -4, 0),
+ int(Qt.AlignTop) | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap), reply)
+
+ # Draw line
+
+ painter.setPen(self.separator_color)
+ painter.drawLine(x1, y2, x2, y2)
+
+ painter.restore()
+
+
+class MiniDefaultCustomDelegate(QStyledItemDelegate):
+
+ '''Delegate to do custom draw of the items'''
+
+ memoized_size = {}
+ memoized_width = {}
+
+ def __init__(self, parent):
+ '''Initialization'''
+
+ QStyledItemDelegate.__init__(self, parent)
+ self.show_avatar = True
+ self.show_screenname = True
+ self.show_timestamp = True
+ self.show_replyto = True
+
+ self.bg_color = QColor('#000000')
+ self.bg_alternate_color = QColor('#333333')
+ self.user_color = QColor('#7AB4F5')
+ self.time_color = QColor('#7AB4F5')
+ self.replyto_color = QColor('#7AB4F5')
+
+ self.text_color = QColor('#FFFFFF')
+ self.separator_color = QColor('#000000')
+
+ self.fm = None
+ self.minifm = None
+
+ self.normFont = None
+ self.miniFont = None
+
+ def sizeHint(self, option, index):
+ '''Custom size calculation of our items'''
+
+ uid = str(index.data(role=IDROLE)) + 'x' + \
+ str(option.rect.width())
+ try:
+ return self.memoized_size[uid]
+ except:
+ size = QStyledItemDelegate.sizeHint(self, option, index)
+ tweet = index.data(Qt.DisplayRole)
+
+ # One time is enought sizeHint need to be fast
+
+ if not self.fm:
+ self.font = QFont(option.font)
+ self.font.setPointSizeF(option.font.pointSizeF()
+ * 0.80)
+ self.fm = QFontMetrics(self.font)
+
+ height = self.fm.boundingRect(
+ 0,
+ 0,
+ option.rect.width() - 75,
+ 800,
+ int(Qt.AlignTop) | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap),
+ tweet,
+ ).height() + 40
+
+ if self.show_replyto:
+ reply_name = index.data(role=REPLYTOSCREENNAMEROLE)
+ reply_text = index.data(role=REPLYTEXTROLE)
+ if reply_name and reply_text:
+
+ # One time is enought sizeHint need to be fast
+
+ reply = 'In reply to @' + reply_name + ' : ' \
+ + reply_text
+ if not self.minifm:
+ if not self.miniFont:
+ self.miniFont = QFont(option.font)
+ self.miniFont.setPointSizeF(option.font.pointSizeF()
+ * 0.60)
+ self.minifm = QFontMetrics(self.miniFont)
+ height += self.minifm.boundingRect(
+ 0,
+ 0,
+ option.rect.width() - 75,
+ 800,
+ int(Qt.AlignTop) | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap),
+ reply,
+ ).height()
+ elif reply_name:
+ reply = 'In reply to @' + reply_name
+ if not self.minifm:
+ if not self.miniFont:
+ self.miniFont = QFont(option.font)
+ self.miniFont.setPointSizeF(option.font.pointSizeF()
+ * 0.60)
+ self.minifm = QFontMetrics(self.miniFont)
+ height += self.minifm.boundingRect(
+ 0,
+ 0,
+ option.rect.width() - 75,
+ 800,
+ int(Qt.AlignTop) | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap),
+ reply,
+ ).height()
+
+ if height < 70:
+ height = 70
+
+ self.memoized_size[uid] = QSize(size.width(), height)
+ return self.memoized_size[uid]
+
+ def paint(
+ self,
+ painter,
+ option,
+ index,
+ ):
+ '''Paint our tweet'''
+
+# if not USE_PYSIDE:
+ (x1, y1, x2, y2) = option.rect.getCoords()
+# else:
+ #Work arround Pyside bug #544
+# y1 = option.rect.y()
+# y2 = y1 + option.rect.height()
+# x1 = option.rect.x()
+# x2 = x1 + option.rect.width()
+#
+ # Ugly hack ?
+ if y1 < 0 and y2 < 0:
+ return
+
+ if not self.fm:
+ self.font = QFont(option.font)
+ self.font.setPointSizeF(option.font.pointSizeF()
+ * 0.80)
+ self.fm = QFontMetrics(self.font)
+
+ model = index.model()
+ tweet = index.data(Qt.DisplayRole)
+ is_me = index.data(ISMEROLE)
+
+ # Instantiate font only one time !
+
+ if not self.normFont:
+ self.normFont = QFont(self.font)
+ self.miniFont = QFont(option.font)
+ self.miniFont.setPointSizeF(option.font.pointSizeF() * 0.60)
+
+ painter.save()
+
+ # Draw alternate ?
+
+ if index.row() % 2 == 0:
+ painter.fillRect(option.rect, self.bg_color)
+ else:
+ painter.fillRect(option.rect, self.bg_alternate_color)
+
+ # highlight selected items
+
+ if option.state & QStyle.State_Selected:
+ painter.fillRect(option.rect, option.palette.highlight())
+
+ # Draw icon
+
+ if self.show_avatar:
+ icon = index.data(Qt.DecorationRole)
+ if icon != None:
+ if is_me:
+ painter.drawPixmap(x2 -60, y1 + 10, 50, 50, icon)
+ else:
+ painter.drawPixmap(x1 + 10, y1 + 10, 50, 50, icon)
+
+ # Draw tweet
+ painter.setFont(self.normFont)
+ painter.setPen(self.text_color)
+ if is_me:
+ new_rect = \
+ painter.drawText(option.rect.adjusted(4, 5, -70, 0), int(Qt.AlignTop)
+ | int(Qt.AlignRight)
+ | int(Qt.TextWordWrap), tweet)
+ else:
+ new_rect = \
+ painter.drawText(option.rect.adjusted(int(self.show_avatar)
+ * 70, 5, -4, 0), int(Qt.AlignTop)
+ | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap), tweet)
+
+ # Draw Timeline
+
+ if self.show_timestamp:
+ time = index.data(role=TIMESTAMPROLE)
+ painter.setFont(self.miniFont)
+ painter.setPen(self.time_color)
+ if is_me:
+ painter.drawText(option.rect.adjusted(4, 10, -80, -9),
+ int(Qt.AlignBottom) | int(Qt.AlignRight),
+ time)
+ else:
+ painter.drawText(option.rect.adjusted(70, 10, -10, -9),
+ int(Qt.AlignBottom) | int(Qt.AlignRight),
+ time)
+
+ # Draw screenname
+
+ if self.show_screenname:
+ screenname = index.data(SCREENNAMEROLE)
+ retweet_of = index.data(RETWEETOFROLE)
+ if retweet_of:
+ screenname = '%s : Retweet of %s' % (screenname, retweet_of.user.screen_name)
+ painter.setFont(self.miniFont)
+ painter.setPen(self.user_color)
+ if is_me:
+ painter.drawText(option.rect.adjusted(4, 10, -70, -9),
+ int(Qt.AlignBottom) | int(Qt.AlignLeft),
+ screenname)
+ else:
+ painter.drawText(option.rect.adjusted(70, 10, -10, -9),
+ int(Qt.AlignBottom) | int(Qt.AlignLeft),
+ screenname)
+
+ # Draw reply
+
+ if self.show_replyto:
+ reply_name = index.data(role=REPLYTOSCREENNAMEROLE)
+ reply_text = index.data(role=REPLYTEXTROLE)
+ if reply_name and reply_text:
+ reply = 'In reply to ' + reply_name + ' : ' \
+ + reply_text
+ painter.setFont(self.miniFont)
+ painter.setPen(self.replyto_color)
+ if is_me:
+ new_rect = \
+ painter.drawText(option.rect.adjusted(4, new_rect.height() + 5, -70, 0),
+ int(Qt.AlignTop) | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap), reply)
+ else:
+ new_rect = \
+ painter.drawText(option.rect.adjusted(int(self.show_avatar)
+ * 70, new_rect.height() + 5, -4, 0),
+ int(Qt.AlignTop) | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap), reply)
+ elif reply_name:
+ reply = 'In reply to ' + reply_name
+ painter.setFont(self.miniFont)
+ painter.setPen(self.replyto_color)
+ if is_me:
+ new_rect = \
+ painter.drawText(option.rect.adjusted(4, new_rect.height() + 5, -70, 0),
+ int(Qt.AlignTop) | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap), reply)
+ else:
+ new_rect = \
+ painter.drawText(option.rect.adjusted(int(self.show_avatar)
+ * 70, new_rect.height() + 5, -4, 0),
+ int(Qt.AlignTop) | int(Qt.AlignLeft)
+ | int(Qt.TextWordWrap), reply)
+
+ # Draw line
+
+ painter.setPen(self.separator_color)
+ painter.drawLine(x1, y2, x2, y2)
+
+ painter.restore()
+
+
+class CoolWhiteCustomDelegate(DefaultCustomDelegate):
+
+ '''Delegate to do custom draw of the items'''
+
+ def __init__(self, parent):
+ '''Initialization'''
+
+ DefaultCustomDelegate.__init__(self, parent)
+
+ self.user_color = QColor('#3399cc')
+ self.replyto_color = QColor('#3399cc')
+ self.time_color = QColor('#94a1a7')
+ self.bg_color = QColor('#edf1f2')
+ self.bg_alternate_color = QColor('#e6eaeb')
+ self.text_color = QColor('#444444')
+ self.separator_color = QColor('#c8cdcf')
+
+
+class CoolGrayCustomDelegate(DefaultCustomDelegate):
+
+ '''Delegate to do custom draw of the items'''
+
+ def __init__(self, parent):
+ '''Initialization'''
+
+ DefaultCustomDelegate.__init__(self, parent)
+
+ self.user_color = QColor('#3399cc')
+ self.time_color = QColor('#94a1a7')
+ self.replyto_color = QColor('#94a1a7')
+ self.bg_color = QColor('#4a5153')
+ self.bg_alternate_color = QColor('#444b4d')
+ self.text_color = QColor('#FFFFFF')
+ self.separator_color = QColor('#333536')
+
+
+class KhweetsView(QListView):
+
+ ''' Model View '''
+
+ def __init__(self, parent=None):
+ QListView.__init__(self, parent)
+ self.setWordWrap(True)
+ self.refreshCustomDelegate()
+ self.setEditTriggers(QAbstractItemView.SelectedClicked)
+ self.setSpacing(0)
+ self.setUniformItemSizes(False)
+ self.setResizeMode(QListView.Adjust)
+ self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
+
+ def keyPressEvent(self, event):
+ if event.key() not in (Qt.Key_Up, Qt.Key_Down):
+ self.parent().switch_tb_edit()
+ self.parent().tb_text.setFocus()
+ self.parent().tb_text.keyPressEvent(event)
+ else:
+ QListView.keyPressEvent(self, event)
+
+ def refreshCustomDelegate(self):
+ settings = QSettings("Khertan Software", "Khweeteur")
+ theme = settings.value('theme')
+ if theme == KhweeteurPref.WHITETHEME:
+ self.custom_delegate = WhiteCustomDelegate(self)
+ elif theme == KhweeteurPref.DEFAULTTHEME:
+ self.custom_delegate = DefaultCustomDelegate(self)
+ elif theme == KhweeteurPref.COOLWHITETHEME:
+ self.custom_delegate = CoolWhiteCustomDelegate(self)
+ elif theme == KhweeteurPref.COOLGRAYTHEME:
+ self.custom_delegate = CoolGrayCustomDelegate(self)
+ elif theme == KhweeteurPref.MINITHEME:
+ self.custom_delegate = MiniDefaultCustomDelegate(self)
+ else:
+ self.custom_delegate = DefaultCustomDelegate(self)
+ self.setItemDelegate(self.custom_delegate)
View
84 khweeteur/notifications.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python2.5
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2010 Benoît HERVIER
+# Licenced under GPLv3
+
+from PySide.QtCore import QObject
+
+try:
+ import dbus
+ import dbus.service
+ from dbus.mainloop.qt import DBusQtMainLoop
+# from dbusobj import KhweeteurDBus
+ noDBUS = False
+except:
+ noDBUS = True
+ print 'No dbus try with pynotify'
+ import pynotify
+
+class KhweeteurNotification(QObject):
+ '''Notification class interface'''
+ def __init__(self):
+ global noDBUS
+ QObject.__init__(self)
+ if not noDBUS:
+ try:
+ self.m_bus = dbus.SystemBus()
+ self.m_notify = self.m_bus.get_object('org.freedesktop.Notifications',
+ '/org/freedesktop/Notifications')
+ self.iface = dbus.Interface(self.m_notify, 'org.freedesktop.Notifications')
+ self.m_id = 0
+ except:
+ noDBUS = True
+
+ def warn(self, message):
+ '''Display an Hildon banner'''
+ if not noDBUS:
+ try:
+ self.iface.SystemNoteDialog(message,0, 'Nothing')
+ except:
+ pass
+ else:
+ if pynotify.init("Khweeteur"):
+ n = pynotify.Notification(message, message)
+ n.show()
+
+ def info(self, message):
+ '''Display an information banner'''
+ if not noDBUS:
+ if isMAEMO:
+ try:
+ self.iface.SystemNoteInfoprint('Khweeteur : '+message)
+ except:
+ pass
+ else:
+ if pynotify.init("Khweeteur"):
+ n = pynotify.Notification(message, message)
+ n.show()
+
+ def notify(self,title, message,category='khweeteur-new-tweets', icon='khweeteur',count=1):
+ '''Create a notification in the style of email one'''
+ if not noDBUS:
+ try:
+ self.m_id = self.iface.Notify('Khweeteur',
+ self.m_id,
+ icon,
+ title,
+ message,
+ ['default','call'],
+ {'category':category,
+ 'desktop-entry':'khweeteur',
+ 'dbus-callback-default':'net.khertan.khweeteur /net/khertan/khweeteur net.khertan.khweeteur show_now',
+ 'count':count,
+ 'amount':count},
+ -1
+ )
+ except:
+ pass
+
+ else:
+ if pynotify.init("Khweeteur"):
+ n = pynotify.Notification(title, message)
+ n.show()
+
View
0  khweeteur-experimental/old/__init__.py → khweeteur/old/__init__.py
File renamed without changes
View
0  khweeteur-experimental/old/client.py → khweeteur/old/client.py
File renamed without changes
View
0  khweeteur-experimental/old/client2.py → khweeteur/old/client2.py
File renamed without changes
View
0  khweeteur-experimental/old/daemon.py → khweeteur/old/daemon.py
File renamed without changes
View
0  ...eteur-experimental/old/oauth2/__init__.py → khweeteur/old/oauth2/__init__.py
File renamed without changes
View
0  ...teur-experimental/old/oauth2/__init__.pyc → khweeteur/old/oauth2/__init__.pyc
File renamed without changes
View
0  ...teur-experimental/old/oauth2/__init__.pyo → khweeteur/old/oauth2/__init__.pyo
File renamed without changes
View
0  ...perimental/old/oauth2/clients/__init__.py → khweeteur/old/oauth2/clients/__init__.py
File renamed without changes
View
0  ...erimental/old/oauth2/clients/__init__.pyc → khweeteur/old/oauth2/clients/__init__.pyc
File renamed without changes
View
0  ...r-experimental/old/oauth2/clients/imap.py → khweeteur/old/oauth2/clients/imap.py
File renamed without changes
View
0  ...-experimental/old/oauth2/clients/imap.pyc → khweeteur/old/oauth2/clients/imap.pyc
File renamed without changes
View
0  ...r-experimental/old/oauth2/clients/smtp.py → khweeteur/old/oauth2/clients/smtp.py
File renamed without changes
View
0  ...-experimental/old/oauth2/clients/smtp.pyc → khweeteur/old/oauth2/clients/smtp.pyc
File renamed without changes
View
0  khweeteur-experimental/old/objects.py → khweeteur/old/objects.py
File renamed without changes
View
0  khweeteur-experimental/old/tweetslist.py → khweeteur/old/tweetslist.py
File renamed without changes
View
0  khweeteur-experimental/old/tweetslist.pyo → khweeteur/old/tweetslist.pyo
File renamed without changes
View
0  khweeteur-experimental/old/tweetslist.qml → khweeteur/old/tweetslist.qml
File renamed without changes
View
0  khweeteur-experimental/old/twitter.py → khweeteur/old/twitter.py
File renamed without changes
View
0  khweeteur-experimental/old/twitter.pyo → khweeteur/old/twitter.pyo
File renamed without changes
View
145 khweeteur/qbadgebutton.py
@@ -0,0 +1,145 @@
+import sys
+from PySide.QtGui import *
+from PySide.QtCore import Qt
+
+class QBadgeButton (QPushButton):
+
+ def __init__ (self, icon = None, text = None, parent = None):
+ if icon:
+ QPushButton.__init__(self, icon, text, parent)
+ elif text:
+ QPushButton.__init__(self, text, parent)
+ else:
+ QPushButton.__init__(self, parent)
+
+ self.badge_counter = 0
+ self.badge_size = 50
+
+ self.redGradient = QRadialGradient(0.0, 0.0, 17.0, self.badge_size - 3, self.badge_size - 3);
+ self.redGradient.setColorAt(0.0, QColor(0xe0, 0x84, 0x9b));
+ self.redGradient.setColorAt(0.5, QColor(0xe9, 0x34, 0x43));
+ self.redGradient.setColorAt(1.0, QColor(0xdc, 0x0c, 0x00));
+
+ def setSize (self, size):
+ self.badge_size = size
+
+ def setCounter (self, counter):
+ self.badge_counter = counter
+ self.update()
+
+ def getCounter (self):
+ return self.badge_counter
+
+ def paintEvent (self, event):
+ QPushButton.paintEvent(self, event)
+ p = QPainter(self)
+ p.setRenderHint(QPainter.TextAntialiasing)
+ p.setRenderHint(QPainter.Antialiasing)
+
+ if self.badge_counter > 0:
+ point = self.rect().topRight()
+ self.drawBadge(p, point.x()-self.badge_size - 1, point.y() + 1, self.badge_size, str(self.badge_counter), QBrush(self.redGradient))
+
+ def fillEllipse (self, painter, x, y, size, brush):
+ path = QPainterPath()
+ path.addEllipse(x, y, size, size);
+ painter.fillPath(path, brush);
+
+ def drawBadge(self, painter, x, y, size, text, brush):
+ painter.setFont(QFont(painter.font().family(), 11, QFont.Bold))
+
+ while ((size - painter.fontMetrics().width(text)) < 10):
+ pointSize = painter.font().pointSize() - 1
+ weight = QFont.Normal if (pointSize < 8) else QFont.Bold
+ painter.setFont(QFont(painter.font().family(), painter.font().pointSize() - 1, weight))
+
+ shadowColor = QColor(0, 0, 0, size)
+ self.fillEllipse(painter, x + 1, y, size, shadowColor)
+ self.fillEllipse(painter, x - 1, y, size, shadowColor)
+ self.fillEllipse(painter, x, y + 1, size, shadowColor)
+ self.fillEllipse(painter, x, y - 1, size, shadowColor)
+
+ painter.setPen(QPen(Qt.white, 2));
+ self.fillEllipse(painter, x, y, size - 3, brush)
+ painter.drawEllipse(x, y, size - 3, size - 3)
+
+ painter.setPen(QPen(Qt.white, 1));
+ painter.drawText(x, y, size - 2, size - 2, Qt.AlignCenter, text);
+
+class QToolBadgeButton (QToolButton):
+
+ def __init__ (self, parent = None):
+ QToolButton.__init__(self, parent)
+
+ self.badge_counter = 0
+ self.badge_size = 25
+
+ self.redGradient = QRadialGradient(0.0, 0.0, 17.0, self.badge_size - 3, self.badge_size - 3);
+ self.redGradient.setColorAt(0.0, QColor(0xe0, 0x84, 0x9b));
+ self.redGradient.setColorAt(0.5, QColor(0xe9, 0x34, 0x43));
+ self.redGradient.setColorAt(1.0, QColor(0xdc, 0x0c, 0x00));
+
+ def setSize (self, size):
+ self.badge_size = size
+
+ def setCounter (self, counter):
+ self.badge_counter = counter
+
+ def getCounter (self):
+ return self.badge_counter
+
+ def paintEvent (self, event):
+ QToolButton.paintEvent(self, event)
+ p = QPainter(self)
+ p.setRenderHint(QPainter.TextAntialiasing)
+ p.setRenderHint(QPainter.Antialiasing)
+ if self.badge_counter > 0:
+ point = self.rect().topRight()
+ self.drawBadge(p, point.x()-self.badge_size, point.y(), self.badge_size, str(self.badge_counter), QBrush(self.redGradient))
+
+ def fillEllipse (self, painter, x, y, size, brush):
+ path = QPainterPath()
+ path.addEllipse(x, y, size, size);
+ painter.fillPath(path, brush);
+
+ def drawBadge(self, painter, x, y, size, text, brush):
+ painter.setFont(QFont(painter.font().family(), 11, QFont.Bold))
+