view frontends/sortilege_old/sortilege @ 132:a86607e5cf38

quick_app: self.occupants for group chat are now managed by quick_chat. self.options.profile now support unicode
author Goffi <goffi@goffi.org>
date Fri, 16 Jul 2010 20:12:54 +0800
parents f551e44adb25
children 2a072735e459
line wrap: on
line source

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
sortilege: a SAT frontend
Copyright (C) 2009, 2010  Jérôme Poisson (goffi@goffi.org)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""


from quick_frontend.quick_app import QuickApp
from quick_frontend.quick_chat_list import QuickChatList
from quick_frontend.quick_contact_list import QuickContactList
from quick_frontend.quick_contact_management import QuickContactManagement
import curses
import pdb
from window import Window
from editbox import EditBox
from statusbar import StatusBar
from chat import Chat
from tools.jid  import JID
import logging
from logging import debug, info, error
import locale
import sys, os
import gobject
import time
from curses import ascii
import locale
from signal import signal, SIGWINCH 
import fcntl
import struct
import termios
from boxsizer import BoxSizer


### logging configuration FIXME: put this elsewhere ###
logging.basicConfig(level=logging.CRITICAL,  #TODO: configure it top put messages in a log file
                    format='%(message)s')
###

const_APP_NAME      = "Sortilège"
const_CONTACT_WIDTH = 30

def ttysize():
    """This function return term size.
    Comes from Donn Cave from python list mailing list"""
    buf = 'abcdefgh'
    buf = fcntl.ioctl(0, termios.TIOCGWINSZ, buf)
    row, col, rpx, cpx = struct.unpack('hhhh', buf)
    return row, col

def C(k):
    """return the value of Ctrl+key"""
    return ord(ascii.ctrl(k))

class ChatList(QuickChatList):
    """This class manage the list of chat windows"""
    
    def __init__(self, host):
        QuickChatList.__init__(self, host)
        self.sizer=host.sizer

    def createChat(self, name):
        chat = Chat(name, self.host)
        self.sizer.appendColum(0,chat)
        self.sizer.update()
        return chat 
        
class ContactList(Window, QuickContactList):
    
    def __init__(self, host, CM):
        QuickContactList.__init__(self, CM)
        self.host = host
        self.jid_list = []
        self.__index=0  #indicate which contact is selected (ie: where we are)
        Window.__init__(self, stdscr, stdscr.getmaxyx()[0]-2,const_CONTACT_WIDTH,0,0, True, _("Contact List"), code=code)

    def resize(self, height, width, y, x):
        Window.resize(self, height, width, y, x)
        self.update()

    def resizeAdapt(self):
        """Adapt window size to stdscr size.
        Must be called when stdscr is resized."""
        self.resize(stdscr.getmaxyx()[0]-2,const_CONTACT_WIDTH,0,0)
        self.update()
        
    def registerEnterCB(self, CB):
        self.__enterCB=CB

    def clear_contacts(self):
        """clear all the contact list"""
        del self.jid_list[:]
        self.__index = 0
        self.update()  #FIXME: window is not updated correctly (contacts are still here until C-L)

    def replace(self, jid, groups=None):
        """add a contact to the list"""
        name = self.CM.getAttr(jid,'name')
        self.jid_list.append(jid.short)
        self.update()

    def indexUp(self):
        """increment select contact index"""
        if self.__index < len(self.jid_list)-1:  #we dont want to select a missing contact
            self.__index = self.__index + 1
        self.update()
    
    def indexDown(self):
        """decrement select contact index"""
        if self.__index > 0:
            self.__index = self.__index - 1
        self.update()

    def disconnect(self, jid):
        """for now, we just remove the contact"""
        self.remove(jid)

    def remove(self, jid):
        """remove a contact from the list"""
        self.jid_list.remove(jid.short)
        if self.__index >= len(self.jid_list) and self.__index > 0:  #if select index is out of border, we put it on the last contact
            self.__index = len(self.jid_list)-1
        self.update()

    def update(self):
        """redraw all the window"""
        if self.isHidden():
            return
        Window.update(self)
        self.jid_list.sort()
        begin=0 if self.__index<self.rHeight else self.__index-self.rHeight+1 
        idx=0
        for item in self.jid_list[begin:self.rHeight+begin]:
            attr = curses.A_REVERSE if ( self.isActive() and (idx+begin) == self.__index ) else 0
            centered = item.center(self.rWidth) ## it's nicer in the center :)
            self.addYXStr(idx, 0, centered, attr)
            idx = idx + 1

        self.noutrefresh()

    def handleKey(self, k):
        if k == curses.KEY_UP:
            self.indexDown()
        elif k == curses.KEY_DOWN:
            self.indexUp()
        elif k == ascii.NL:
            if not self.jid_list:
                return
            try:
                self.__enterCB(self.jid_list[self.__index])
            except NameError:
                pass # TODO: thrown an error here
            
class SortilegeApp(QuickApp):
    
    def __init__(self):
        #debug(const_APP_NAME+" init...")

        ## unicode support ##
        locale.setlocale(locale.LC_ALL, '')
        global code
        code = locale.getpreferredencoding()
        self.code=code

        ## main loop setup ##
        self.loop=gobject.MainLoop()
        gobject.io_add_watch(0, gobject.IO_IN, self.loopCB)

        ## misc init stuff ##
        self.CM = QuickContactManagement()
        self.listWins=[]
        self.chatParams={'timestamp':True,
                         'color':True,
                         'short_nick':False}

    def start(self):
        curses.wrapper(self.start_curses)

    def start_curses(self, win):
        global stdscr
        stdscr = win
        self.stdscr = stdscr
        curses.raw() #we handle everything ourself
        curses.curs_set(False)
        stdscr.nodelay(True)

        ## colours ##
        self.color(True)

        ## windows ##
        self.contactList = ContactList(self, self.CM)
        self.editBar = EditBox(stdscr, "> ", self.code)
        self.editBar.activate(False)
        self.statusBar = StatusBar(stdscr, self.code)
        self.statusBar.hide(True)
        self.addWin(self.contactList)
        self.addWin(self.editBar)
        self.addWin(self.statusBar)
        self.sizer=BoxSizer(stdscr)
        self.sizer.appendRow(self.contactList)
        self.sizer.appendRow(self.statusBar)
        self.sizer.appendRow(self.editBar)
        self.currentChat=None

        self.contactList.registerEnterCB(self.onContactChoosed)
        self.editBar.registerEnterCB(self.onTextEntered)

        self.chat_wins=ChatList(self)

        QuickApp.__init__(self)  #XXX: yes it's an unusual place for the constructor of a parent class, but the init order is important
        self.plug_profile()

        signal (SIGWINCH, self.onResize) #we manage SIGWINCH ourselves, because the loop is not called otherwise

        #last but not least, we adapt windows' sizes
        self.sizer.update()
        self.editBar.replace_cur()
        curses.doupdate()

        self.loop.run()

    def addWin(self, win):
        self.listWins.append(win)

    def color(self, activate=True):
        if activate:
            debug (_("Activating colors"))
            curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_BLACK)
            curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK)
        else:
            debug (_("Deactivating colors"))
            curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
            curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK)
            

    def showChat(self, chat):
        debug (_("show chat"))
        if self.currentChat:
            debug (_("hiding %s"), self.currentChat)
            self.chat_wins[self.currentChat].hide()
        self.currentChat=chat
        debug (_("showing %s"), self.currentChat)
        self.chat_wins[self.currentChat].show()
        self.chat_wins[self.currentChat].update()
        

    ### EVENTS ###

    def onContactChoosed(self, jid_txt):
        """Called when a contact is selected in contact list."""
        jid=JID(jid_txt)
        debug (_("contact choosed: %s"), jid)
        self.showChat(jid.short)
        self.statusBar.remove_item(jid.short)
        if len(self.statusBar)==0:
            self.statusBar.hide()
            self.sizer.update()


    def onTextEntered(self, text):
        jid=JID(self.profiles[self.profile]['whoami'])
        self.bridge.sendMessage(self.currentChat, text, profile_key=self.profile)

    def showDialog(self, message, title, type="info"):
        if type==question:
            raise NotImplementedError
        pass


    def presenceUpdate(self, jabber_id, show, priority, statuses, profile):
        QuickApp.presenceUpdate(self, jabber_id, show, priority, statuses, profile)
        self.editBar.replace_cur()
        curses.doupdate()

    def askConfirmation(self, type, id, data):
        #FIXME
        info (_("FIXME: askConfirmation not implemented"))

    def actionResult(self, type, id, data):
        #FIXME
        info (_("FIXME: actionResult not implemented"))

    def newMessage(self, from_jid, msg, type, to_jid, profile):
        QuickApp.newMessage(self, from_jid, msg, type, to_jid, profile)
        sender=JID(from_jid)
        addr=JID(to_jid)
        win = addr if sender.short == self.whoami.short else sender  #FIXME: duplicate code with QuickApp
        if (self.currentChat==None):
            self.currentChat=win.short
            self.showChat(win.short)
            
        # we show the window in the status bar
        if not self.currentChat == win.short:
            self.statusBar.add_item(win.short)
            self.statusBar.show()
        self.sizer.update()
        self.statusBar.update()

        self.editBar.replace_cur()
        curses.doupdate()

    def onResize(self, sig, stack):
        """Called on SIGWINCH.
        resize the screen and launch the loop"""
        height, width = ttysize()
        curses.resizeterm(height, width)
        gobject.idle_add(self.callOnceLoop)
    
    def callOnceLoop(self):
        """Call the loop and return false (for not being called again by gobject mainloop).
        Usefull for calling loop when there is no input in stdin"""
        self.loopCB()
        return False

    def __key_handling(self, k):
        """Handle key and transmit to active window."""

        ### General keys, handled _everytime_ ###
        if k == C('x'):
            if os.getenv('TERM')=='screen':
                os.system('screen -X remove')
            else:
                self.loop.quit()

        ## windows navigation
        elif k == C('l') and not self.contactList.isHidden():
            """We go to the contact list"""
            self.contactList.activate(not self.contactList.isActive())
            if self.currentChat:
                self.editBar.activate(not self.contactList.isActive())

        elif k == curses.KEY_F2:
            self.contactList.hide(not self.contactList.isHidden())
            if self.contactList.isHidden():
                self.contactList.activate(False) #TODO: auto deactivation when hiding ?
                if self.currentChat:
                    self.editBar.activate(True)
            self.sizer.update()

        ## Chat Params ##    
        elif k == C('c'):
            self.chatParams["color"] = not self.chatParams["color"]
            self.color(self.chatParams["color"])
        elif k == C('t'):
            self.chatParams["timestamp"] = not self.chatParams["timestamp"]
            self.chat_wins[self.currentChat].update()
        elif k == C('s'):
            self.chatParams["short_nick"] = not self.chatParams["short_nick"]
            self.chat_wins[self.currentChat].update()

        ## misc ##
        elif k == curses.KEY_RESIZE:
            stdscr.erase()
            height, width = stdscr.getmaxyx()
            if height<5 and width<35:
                stdscr.addstr(_("Pleeeeasse, I can't even breathe !"))
            else:
                for win in self.listWins:
                    win.resizeAdapt()
                for win in self.chat_wins.keys():
                    self.chat_wins[win].resizeAdapt()
                self.sizer.update() # FIXME: everything need to be managed by the sizer

        ## we now throw the key to win handlers ##
        else: 
            for win in self.listWins:
                if win.isActive():
                    win.handleKey(k)
            if self.currentChat:
                self.chat_wins[self.currentChat].handleKey(k)

    def loopCB(self, source="", cb_condition=""):
        """This callback is called by the main loop"""
        #pressed = self.contactList.window.getch()
        pressed = stdscr.getch()
        if pressed != curses.ERR:
            self.__key_handling(pressed)
            self.editBar.replace_cur()
            curses.doupdate()
            

        return True


sat = SortilegeApp()
sat.start()