# s60htmled.py: Simple HTML editor for S60 Ed.3 smartphones # # Copyright (C) Dmitri Brechalov, 2008 # # Project URL: http://code.google.com/p/s60htmled/ # # 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. # # 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. import appuifw2 import e32 import key_codes import os UID = u"e3e34da2" VERSION = '0.7' htmltemplates = ( ''' ''', ''' ''' ) u = lambda s: s.decode('utf-8') s = lambda s: s.encode('utf-8') schedule = appuifw2.schedule def fileBrowser(label, dironly=False, dirname=''): isdir = lambda fname: os.path.isdir(os.path.join(dirname, fname)) isfile = lambda fname: not os.path.isdir(os.path.join(dirname, fname)) markdir = lambda fname: fname + '/' def chkdir(d): # os.path uses '/' as a dir separator but os.path.join d = s(d) # adds no '/' between drive name and file path if not (d.endswith('/') or d.endswith('\\')): return d + '/' # content handler return an error when there is else: # no separator after drive name return d while True: if not dirname: items = map(chkdir, e32.drive_list()) else: lst = os.listdir(dirname) dirs = map(markdir, filter(isdir, lst)) dirs.sort() if not dironly: files = filter(isfile, lst) files.sort() else: files = [] items = ['..'] + dirs + files ans = appuifw2.popup_menu(map(u, items), u(label)) if ans is None: return None fname = items[ans] if fname == '..': return fname fname = os.path.join(dirname, fname) if os.path.isdir(fname): ans = fileBrowser(fname, dironly, fname) if ans != '..': return ans if dironly and ans == '..': return fname else: return fname class xText(object): '''eXtended Text editor ''' def __init__(self): self.editor = appuifw2.Text(move_callback=self.moveEvent, edit_callback=self.changeEvent, skinned=True) self.editor.style = appuifw2.STYLE_BOLD self.fname = None self.tags = [] self.hdr = None self.find_text = u('') self.replace_text = u('') self.old_indicator = self.editor.indicator_text self.funkey_timer = None self.exit_key_handlers = (None, None) # ((u"Label", callback), (u"FnLabel", fn_callback)) def dummy(self): appuifw2.note(u('Not implmented yet!'), 'error') #### Keyboard & Events def quit(self): if self.notSaved(): if not self.fileSave(): return self.app_lock.signal() if appuifw2.app.uid() == UID: appuifw2.app.set_exit() # running as app def bindExitKey(self, handler=None, fnhandler=None): self.exit_key_handler = (handler, fnhandler) def notSaved(self): if self.editor.has_changed: return appuifw2.query(u('File has been changed. Save?'), 'query', ok=u('Yes'), cancel=u('No')) return False def changeEvent(self, pos, num): self.updateIndicator() def moveEvent(self): schedule(self.updateIndicator) def updateIndicator(self): try: text_pos = self.editor.get_pos() text_len = self.editor.len() pos = int(round(float(text_pos) / float(text_len) * 100)) except ZeroDivisionError: pos = 0 self.editor.indicator_text = u('%s%%' % pos) def yesKeyPressed(self): self.bindFunKeys() def bindFunKeys(self): in_time = (self.funkey_timer is not None) if in_time: self.funkey_timer.cancel() self.funkey_timer = e32.Ao_timer() self.funkey_timer.after(1.5, self.rebindFunKeys) pos = self.editor.get_pos() self.editor.bind(key_codes.EKeyUpArrow, lambda : self.arrowKeyPressed(pos, appuifw2.EFPageUp)) self.editor.bind(key_codes.EKeyDownArrow, lambda : self.arrowKeyPressed(pos, appuifw2.EFPageDown)) self.editor.bind(key_codes.EKeyLeftArrow, lambda : self.arrowKeyPressed(pos, appuifw2.EFLineBeg)) self.editor.bind(key_codes.EKeyRightArrow, lambda : self.arrowKeyPressed(pos, appuifw2.EFLineEnd)) self.editor.bind(key_codes.EKeySelect, self.moveMenu) self.editor.bind(key_codes.EKeyYes, self.rebindFunKeys) self.old_indicator = self.editor.indicator_text # appuifw2.app.exit_key_handler = self.rightSoftkeyPressed # appuifw2.app.exit_key_text = u("Entity") fnhandler = self.exit_key_handler[1] if fnhandler is not None: appuifw2.app.exit_key_handler = lambda : self.rightSoftkeyPressed(fnhandler[1]) appuifw2.app.exit_key_text = fnhandler[0] self.editor.indicator_text = u('Func') def rebindFunKeys(self): in_time = (self.funkey_timer is not None) if in_time: self.funkey_timer.cancel() self.funkey_timer = None for key in (key_codes.EKeyUpArrow, key_codes.EKeyDownArrow, key_codes.EKeyLeftArrow, key_codes.EKeyRightArrow, key_codes.EKeySelect): self.editor.bind(key, lambda : None) self.editor.bind(key_codes.EKeyYes, self.yesKeyPressed) self.editor.bind(key_codes.EKeyStar, self.starKeyPressed) self.editor.indicator_text = self.old_indicator # appuifw2.app.exit_key_handler = self.insertTag # appuifw2.app.exit_key_text = u("Tag") handler = self.exit_key_handler[0] if handler is not None: appuifw2.app.exit_key_handler = handler[1] appuifw2.app.exit_key_text = handler[0] def starKeyPressed(self): appuifw2.note(u('Star key pressed')) e32.ao_yeld() def arrowKeyPressed(self, pos, cmd): self.rebindFunKeys() schedule(self.moveCursor, pos, cmd) def rightSoftkeyPressed(self, callback): self.rebindFunKeys() schedule(callback) #### Cursor control, Search and Replace def moveCursor(self, pos, cmd): self.editor.set_pos(pos) self.editor.move(cmd) self.moveEvent() def moveToLine(self, line): self.editor.focus = False self.editor.set_pos(0) for i in range(line-1): self.editor.move(appuifw2.EFLineDown) self.editor.focus = True self.moveEvent() def gotoLine(self): ans = appuifw2.query(u("Goto line"), 'number', 1) if ans: schedule(self.moveToLine, ans) def moveMenu(self): self.rebindFunKeys() ans = appuifw2.popup_menu([u('Top'), u('Bottom'), u('Goto line')]) if ans is not None: if ans == 0: schedule(self.moveCursor, 0, appuifw2.EFNoMovement) elif ans == 1: schedule(self.moveCursor, len(self.editor.get()), appuifw2.EFNoMovement) elif ans == 2: schedule(self.gotoLine) self.moveEvent() def doFind(self, find_text, fwd=True): if fwd: #### XXX FIXME: use get(pos, length) instead! txt = self.editor.get()[self.editor.get_pos():] i = txt.find(find_text) else: #### XXX FIXME: use get(pos, length) instead! txt = self.editor.get()[:self.editor.get_pos()-1] i = txt.rfind(find_text) if i >= 0: if fwd: pos = self.editor.get_pos() + i + len(find_text) else: pos = i + len(find_text) self.editor.set_pos(pos) self.moveEvent() return True appuifw2.note(u("Not found"), "info") return False def doReplace(self, txt): ed = self.editor pos = ed.get_pos()-len(self.find_text) ed.delete(pos, len(self.find_text)) ed.set_pos(pos) ed.add(txt) ed.set_pos(pos + len(txt)) def findTextForward(self): ans = appuifw2.query(u("Find forward"), "text", self.find_text) if ans is None: return self.find_text = ans if self.doFind(self.find_text): pos = self.editor.get_pos() self.editor.set_selection(pos, pos-len(self.find_text)) def findTextBackward(self): ans = appuifw2.query(u("Find backward"), "text", self.find_text) if ans is None: return self.find_text = ans if self.doFind(self.find_text, False): pos = self.editor.get_pos() self.editor.set_selection(pos, pos-len(self.find_text)) def replaceText(self): ans = appuifw2.multi_query(u("Find"), u("Replace")) if ans is None: return self.find_text = ans[0] self.replace_text = ans[1] if self.doFind(self.find_text): self.doReplace(self.replace_text) def findEOL(self): self.doFind(u"\u2029") #### Selection & Clipboard def selectAll(self): self.editor.select_all() def selectNone(self): self.editor.clear_selection() def cut(self): if self.editor.can_cut(): self.editor.cut() else: appuifw2.note(u("Can't cut!"), "error") def copy(self): if self.editor.can_copy(): self.editor.copy() else: appuifw2.note(u("Can't copy!"), "error") def paste(self): if self.editor.can_paste(): self.editor.paste() else: appuifw2.note(u("Can't paste!"), "error") def undo(self): if self.editor.can_undo(): self.editor.undo() else: appuifw2.note(u("Can't undo!"), "error") #### File operations def fileDialog(self, allowNew=False): if allowNew: dirname = fileBrowser('Select directory', dironly=True) if dirname is None: return None ans = appuifw2.query(u('Type new file name:\n' + dirname), 'text') if ans: return os.path.join(dirname, s(ans)) else: return None else: return fileBrowser('Select file') def fileNew(self): if self.notSaved(): if not self.fileSave(): return self.editor.clear() appuifw2.app.title = self.title self.fname = None self.moveEvent() self.editor.has_changed = False def fileOpen(self): if self.notSaved(): if not self.fileSave(): return ans = self.fileDialog() if not ans: return self.fname = ans appuifw2.app.title = u(os.path.split(self.fname)[1]) try: self.editor.set(u(open(self.fname, 'r').read())) self.editor.set_pos(0) self.moveEvent() self.editor.has_changed = False except: appuifw2.note(u('Cannot read file %s!' % self.fname), 'error') self.fileNew() def doSave(self): try: open(self.fname, 'w').write(s(self.editor.get().replace(u"\u2029", u'\r\n'))) self.editor.has_changed = False return True except: appuifw2.note(u('Cannot write file %s!' % self.fname), 'error') return False def fileSaveAs(self): ans = self.fileDialog(True) if not ans: return False self.fname = ans appuifw2.app.title = u(os.path.split(self.fname)[1]) return self.doSave() def fileSave(self): if self.fname is None: return self.fileSaveAs() else: return self.doSave() class HTMLEditor(xText): '''HTML Editor. Uses appuifw2.Text ''' version = VERSION title = u('HTML Editor %s' % (version)) # def __init__(self): # xText.__init__(self) #### Help def aboutDlg(self): appuifw2.query(u('S60 HTML Editor\nVersion %s\n(C) Dmitri Brechalov, 2008' % (self.version)), 'query', ok=u(''), cancel=u('Close')) def showUID(self): appuifw2.query(appuifw2.app.uid(), 'query') def helpDlg(self): topics = (u('Call button works as functional key.\nCall + arrows: Page Up, Page Down, Line Start and Line End.'), u('Press Call + Select to go to the top/bottom of the text or goto line.'), u('Press right softkey to select and insert HTML tag.\nSelect "Custom tag" to insert any tag.'), u('Press Call + Right softkey to insert HTML entity.'), u('More info and fresh version are available at http://code.google.com/p/s60htmled/'), ) for t in topics: if not appuifw2.query(t, 'query', ok=u('Next'), cancel=u('Close')): break #### Extra file operations def fileTemplate(self): if self.notSaved(): if not self.fileSave(): return ans = appuifw2.popup_menu([u('Simple HTML'), u('HTML 4.01 Transitional')], u('Select template')) if ans is None: return self.fileNew() self.editor.set(u(htmltemplates[ans])) self.editor.set_pos(0) self.editor.has_changed = False def launchBrowser(self): if not self.fname or self.notSaved(): if not self.fileSave(): return appuifw2.Content_handler().open(u(self.fname.replace('/', '\\'))) #### Working with tags def attrjoin(self, attrs): return ' ' + ' '.join([ '%s="%s"' % (k, v) for k, v in attrs.items() ]) def addTag(self, tag, attrs=None, needCloseTag=True): (pos, anchor, text) = self.editor.get_selection() if attrs: sattr = self.attrjoin(attrs) else: sattr = '' if needCloseTag: text = '<%s%s>%s' % (tag, sattr, s(text), tag) else: text = '<%s%s/>' % (tag, sattr) if pos > anchor: pos, anchor = anchor, pos self.editor.delete(pos, anchor-pos) self.editor.set_pos(pos) self.editor.add(u(text)) def askAttr(self, tag): result = dict() while True: ans = appuifw2.query(u("<%s%s...>\nAttribute:" % (tag, self.attrjoin(result))), 'text', ) if ans is None: break result[s(ans).lower()] = "" if len(result.keys()) == 0: return None return result def insertTag(self): #### tag (askForAttr, needCloseTag) tags = {'a': (True, True), 'img': (True, False), 'h1': (False, True), 'h2': (False, True), 'h3': (False, True), 'h4': (False, True), 'h5': (False, True), 'h6': (False, True), 'strong': (False, True), 'em': (False, True), 'code': (False, True), 'pre': (False, True), 'u': (False, True), 'b': (False, True), 'i': (False, True), 'p': (True, True), 'ul': (False, True), 'ol': (False, True), 'li': (False, True), 's': (False, True), 'q': (False, True), 'blockquote': (False, True), 'var': (False, True), 'tt': (False, True), 'br': (False, False), 'hr': (False, False), } tlst = tags.keys() tlst.sort() ans = appuifw2.selection_list(map(u, ['Custom...'] + tlst), 1) if ans is None: return if ans == 0: self.insertCustomTag() else: tag = tlst[ans-1] (askForAttr, needCloseTag) = tags[tag] if askForAttr: attr = self.askAttr(tag) else: attr = None self.addTag(tag, attr, needCloseTag) def insertCustomTag(self): ans = appuifw2.query(u('HTML Tag:'), 'text') if not ans: return tag = s(ans).lower() attr = self.askAttr(tag) self.addTag(tag, attr, True) def insertEntity(self): ans = appuifw2.popup_menu(map(u, ('&', '<', '>', '"'))) if ans is None: return self.addEntity(('amp', 'lt', 'gt', 'quot')[ans]) def addEntity(self, ent): self.editor.add(u('&%s;' % ent)) def run(self): self.app_lock = e32.Ao_lock() appuifw2.app.menu = [ (u("File"), ((u("Open"), self.fileOpen), (u("Save"), self.fileSave), (u("Save as"), self.fileSaveAs), (u("New"), self.fileNew), (u("New from template"), self.fileTemplate), (u("View in browser"), self.launchBrowser),)), (u("Edit"), ((u("Undo"), self.undo), (u("Cut"), self.cut), (u("Copy"), self.copy), (u("Paste"), self.paste), (u("Select All"), self.selectAll), (u("Select None"), self.selectNone))), (u("Search"), ((u("Find Forward"), self.findTextForward), (u("Find Backward"), self.findTextBackward), (u("Replace"), self.replaceText))), (u("Help"), ((u("About"), self.aboutDlg),)), (u("Exit"), self.quit) ] self.bindExitKey((u('Tag'), self.insertTag), (u('Entity'), self.insertEntity)) self.editor.has_changed = False self.fileNew() e32.ao_yield() appuifw2.app.body = self.editor old_exit_key_text = appuifw2.app.exit_key_text old_menu_key_text = appuifw2.app.menu_key_text appuifw2.app.menu_key_text = u("Options") self.rebindFunKeys() self.app_lock.wait() appuifw2.app.exit_key_text = old_exit_key_text if __name__ == '__main__': editor = HTMLEditor() editor.run()