diff --git a/.gitignore b/.gitignore
index ce37f30..68cd135 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,10 @@ logs/*
build/*
profiles/*
tmp/*
+tutorial/*
irctest.log
-*.pyc
\ No newline at end of file
+*.pyc
+pesterchum.js
+quirks/*
+!quirks/defaults.py
+*.pkl
diff --git a/CHANGELOG.mkdn b/CHANGELOG.mkdn
index 191275c..5b8cac5 100644
--- a/CHANGELOG.mkdn
+++ b/CHANGELOG.mkdn
@@ -7,6 +7,68 @@ Visit https://github.com/illuminatedwax/pesterchum for git access and source cod
CHANGELOG
---------
+### 3.14.2
+* Individually turn quirks on and off - Kiooeht [evacipatedBox]
+* More canon trollian theme timeline indicators - [binaryCabalist]
+* By mood chum sorting - Kiooeht [evacipatedBox]
+* Chum groups - Kiooeht [evacipatedBox]
+* Turn logging on and off - Kiooeht [evacipatedBox]
+* Customizable idle time - Kiooeht [evacipatedBox]
+* Different sound for memos - Kiooeht [evacipatedBox]
+* Animated smilies - Kiooeht [evacipatedBox]
+* Delete profiles - Kiooeht [evacipatedBox]
+* Customize minimize and close button actions - Kiooeht [evacipatedBox]
+* Receive notices from services you're talking to - Kiooeht [evacipatedBox]
+* Automatically turn off quirks when talking to bots - Kiooeht [evacipatedBox]
+* Rearrange options menu, make tabbed - Kiooeht [evacipatedBox]
+* Rearrange memos window for readability - Kiooeht [evacipatedBox]
+* Give voice to memo users - Kiooeht [evacipatedBox]
+* Theme checking - Kiooeht [evacipatedBox]
+* Display (De)OP/Voice messages in memos - Kiooeht [evacipatedBox]
+* Advanced Mode: Alter IRC user mode - Kiooeht [evacipatedBox]
+* Logviewer chum search - Kiooeht [evacipatedBox]
+* Logviewer log search - Kiooeht [evacipatedBox]
+* Set server and port from command line - Kiooeht [evacipatedBox]
+* Invite-only memos, invite chums to memos - Kiooeht [evacipatedBox]
+* Check Pyqt4 and pygame are installed and correct versions - Kiooeht [evacipatedBox]
+* Advanced Mode: View memo (channel) modes - Kiooeht [evacipatedBox]
+* Quirk groups - Kiooeht [evacipatedBox]
+* CTCP Version reply - Kiooeht [evacipatedBox]
+* Check for Pesterchum updates - Kiooeht [evacipatedBox]
+* Memo OP options: Secret, Invite-only, Mute - Kiooeht [evacipatedBox]
+* Notify user if channel blocks message - Kiooeht [evacipatedBox]
+* Bug reporter - Kiooeht [evacipatedBox]
+* Python quirks (users can create own quirk functions) - Kiooeht [evacipatedBox] (Idea: Lexi [lexicalNuance])
+* Incorporate support for the new randomEncounter - Kiooeht [evacipatedBox] (Idea: Lexi [lexicalNuance])
+* Only GETMOOD for people online (less spam!) - Kiooeht [evacipatedBox] (Idea: Lexi [lexicalNuance])
+* Quirk tester in quirk window - Kiooeht [evacipatedBox] (Idea: [alGore])
+* Show and support giving kick reasons - Kiooeht [evacipatedBox] (Idea: Lexi [lexicalNuance])
+* Make adding quirks into multi-page wizard - Kiooeht [evacipatedBox]
+* Flash the taskbar on new messages - Kiooeht [evacipatedBox]
+* Third beep sound for when your initials are mentioned in memos - Kiooeht [evacipatedBox] (Idea: Lexi [lexicalNuance])
+* Ctrl + click to copy links - Kiooeht [evacipatedBox]
+* Say something when server is full - Kiooeht [evacipatedBox]
+* Ping server if no ping from server to test connection - Kiooeht [evacipatedBox] (Idea: Lexi [lexicalNuance])
+* MSPA comic update notifier - Kiooeht [evacipatedBox]
+* Volume control - Kiooeht [evacipatedBox]
+* Bug fixes
+ * Logviewer updates - Kiooeht [evacipatedBox]
+ * Memo scrollbar thing - Kiooeht [evacipatedBox]
+ * Time arrows in enamel - Kiooeht [evacipatedBox]
+ * Quirk order actually works - Kiooeht [evacipatedBox]
+ * Stay in memos on profile switch - Kiooeht [evacipatedBox]
+ * Auto rejoin memos on reconnect - Kiooeht [evacipatedBox]
+ * De-Op in memos correctly - Kiooeht [evacipatedBox]
+ * Don't blow up if someone's not using Pesterchum in a memo - Kiooeht [evacipatedBox]
+ * Make 'logs' and 'profiles' directories if non-existant - Kiooeht [evacipatedBox]
+ * Don't split messages in bad places - Kiooeht [evacipatedBox]
+ * Chumhandles must match EXACTLY to register mood changes - Kiooeht [evacipatedBox]
+ * Menu bar text colour correct when default system colour isn't black - Kiooeht [evacipatedBox]
+ * End all colour tags and restart them on split messages - Kiooeht [evacipatedBox]
+ * Chat input box right-click menus - Kiooeht [evacipatedBox]
+ * Don't overflow random colours into colourless messages - Kiooeht [evacipatedBox]
+ * Only open links on left click - Kiooeht [evacipatedBox]
+
### 3.14.1
* Pesterchum 3.14 - illuminatedwax [ghostDunk]
* Art - Grimlive [aquaMarinist]
@@ -15,17 +77,16 @@ CHANGELOG
* Quirks reverse() function - illuminatedwax [ghostDunk]
* Timestamps - Kiooeht [evacipatedBox]
* Logviewer - Kiooeht [evacipatedBox]
-* Quirk ordering - alGore
-* # of users in a memo - alGore
+* Quirk ordering - [alGore]
+* # of users in a memo - [alGore]
* @links to users - illuminatedwax [ghostDunk]
-* Support for REPORT and ALT to calSprite built in
-
-BUG FIXES:
-* mixer bug fixed
-* "flags" bug fixed
-* incorrect characters in memos no longer break log file names
-* memos now do not break on case-sensitivity
-* fixed QDB address
-* now lines too long to send in a single message are split up correctly
-* quirk replace bug fixed
-* pesterClientXXX profiles no longer saved
\ No newline at end of file
+* Support for REPORT and ALT to calSprite built in - illuminatedwax [ghostDunk]
+* Bug fixes:
+ * mixer bug fixed
+ * "flags" bug fixed
+ * incorrect characters in memos no longer break log file names
+ * memos now do not break on case-sensitivity
+ * fixed QDB address
+ * now lines too long to send in a single message are split up correctly
+ * quirk replace bug fixed
+ * pesterClientXXX profiles no longer saved
diff --git a/PYQUIRKS.mkdn b/PYQUIRKS.mkdn
new file mode 100644
index 0000000..208b7dc
--- /dev/null
+++ b/PYQUIRKS.mkdn
@@ -0,0 +1,87 @@
+Python Quirk Functions
+===============
+
+Table of Contents
+-----------------
+1. Introduction
+2. Create A Module
+3. Functions In A Module
+4. Command Requirements
+5. Completed Quirk Function
+
+Introduction
+---------------
+Over the course of this short tutorial you will learn:
+
+* How to create your own Quirk Functions
+* VERY basic Python syntax
+
+You will not learn:
+
+* How to write Python
+* How to bake a cake
+
+Throughout this tutorial there will be
+
+Instructions in special boxes.
+If you follow the instructions in these boxes, by the end of this tutorial
+you will have recreated the default reverse() Quirk Function.
+
+
+Create A Module
+-------------------
+All Quirk Function Modules should be created in the 'quirks/' directory. File names must end in '.py'.
+You can have multiple Quirk Functions per Module.
+
+Each Module can also have a 'setup' function which will be called once, the moment the Module is loaded.
+
+
+Create 'reverse.py' in the 'quirks/' directory.
+
+
+Functions In A Module
+--------------------------
+If you've ever done programming before, you should know what a function is. If not, I suggest picking up a good programming book (or e-book).
+
+In Python, function syntax looks like this:
+
+def function_name(myvar1, myvar2):
+
+'def' is used to declare that this is a function, and 'function_name' is obviously the name of your function.
+'myvar1' and 'myvar2' are variables coming into your function. For most of your functions, the only variable being passed will be 'text'.
+
+In Python, curly braces ({}) are not used to declare the beginning and end of a function. Instead, a colon (:) is used to declare the beginning of a function. After that, indentation is used to declare the body and end of a function.
+
+
+def reverserep(text):
+ return text[::-1]
+
+
+Command Requirements
+------------------------
+For a function to be registered as a Quirk Function, it must conform to three simple rules:
+
+1. It must have a command name.
+2. It must take exactly one arguement.
+3. It must return a string.
+
+What is meant by having a command name, is that a name for the Quirk Function has to be defined. This is done by defining the 'command' variable for a function.
+
+function_name.command = "name"
+
+
+reverserep.command = "reverse"
+
+
+Completed Quirk Function
+---------------------------
+Below is the completed, fully working, reverse Quirk Function. After it I will break down the pieces of each line.
+
+def reverserep(text):
+ return text[::-1]
+reverserep.command = "reverse"
+
+
+As stated before, to start a function, you need to use the keyword 'def'. All Quirk Functions must take exactly one argument (in this case 'text').
+In this example, the text is reversed and returned all in one line. 'text[::-1]' is the Pythonic way of reversing a list or string.
+The last line is the most important part. This tells Pesterchum to call this function whenever 'reverse()' is used in a quirk.
diff --git a/README.mkdn b/README.mkdn
new file mode 100644
index 0000000..43c13eb
--- /dev/null
+++ b/README.mkdn
@@ -0,0 +1,781 @@
+Welcome to Pesterchum 3.14.1!
+=============================
+
+WHAT'S NEW?
+-----------
+* Quirks now have a lower(), scramble(), and reverse() function!
+* Timestamps - check your Config!
+* Logviewer - View logs right in Pesterchum!
+* Quirk ordering - order your quirks so they work right!
+* # of users in a memo - You can now see how many users are in a memo.
+* @links to users - typing @ before user's name creates a link
+ that will pester them!
+* Support for REPORT and ALT to calSprite built in -
+ If someone is bothering you, or a canon handle is idle, or
+ for whatever reason, right click their name and go to "Report"
+ to report them to a moderator.
+ If you want to talk to an alt canon handle, just right click
+ the username!
+ if you have an alt handle, register it with calSprite!
+
+Here's some tips to help you get started:
+-----------------------------------------
+
+- You can import your old Pesterchum contacts by going to
+CLIENT->IMPORT and opening your pesterchum.cfg file. This is usually
+in the 2.5 base directory or in Tinychum's data folder.
+- Some themes can be confusing if you haven't used the program
+already! Some hints:
+ * Trollian: Moods are set by clicking the timelines, and you
+can add chums by clicking "Chumproll." Moods correspond to the troll
+that would most likely exhibit them. You can go offline by hitting the
+"Timelines" menu bar.
+
+ * Gold: Add chums by hitting the two chumpeoples in the upper left
+corner. Go offline by clicking the "CHUMHANDLE:" label.
+
+ * Enamel: Add chums by hitting the "CHUMROLL" label. Go offline by
+clicking the upper left hand corner.
+
+- Right-click is your friend! There are useful right click
+options on the chumroll, by clicking the chumhandle in a conversation,
+online userlist, or the list of memo browsers.
+
+Cool features:
+--------------
+
+- Importing from old PC. It can already do your chumlist, soon it will
+import your quirks from 2.5 and TC as well!
+- Profile switching. Instantly switch profiles, loading your color and
+quirks with it.
+- Theme switching and creation. So far this comes with a few official
+themes! But you can also make your own: just make a new directory in
+the themes folder with the proper images and style.js file. The
+style.js file will be documented soon, but feel free to poke at it.
+- Memos. Memos that are a lot more like the ones in the comic and
+allow you to appear at multiple times in one chat.
+- Quirks: Prefix, suffix, simple replace, regexp replace (like in
+2.5), random replacement, and an auto-mispeller :P
+- Chum groups. Organize your chums into collapsible groups for easy
+management.
+- Block/user list
+- Add/block chums directly from a conversation, the userlist, or memo
+userlist.
+- Timestamps saved in logs and shown in conversations if wanted.
+- Logging. Logs are output in bbcode (for easy forum posting), html,
+and plain text.
+- Logviewer for easy log reading inside Pesterchum
+- Idling. You can set yourself idle manually, and the computer will
+set it for you after a configurable amount of time.
+- Improved /me. Any letters immediately following /me will be
+processed correctly. e.g. /me'd rather be fishing -> -- ghostDunk'd
+[GD'D] rather be fishing --
+- Hyperlinks! Now if someone types http://whatever it will turn into a
+link you can just click and follow. No more copy/paste.
+- Memo links. Link your friends to your memos.
+- Smilies. We've added about 30-40 smilies from the forums. There is a
+list later on in this readme.
+- Submit quotes directly to the Pesterchum QDB!
+
+FA%
+---
+Q: Norton says it has a virus and then deletes it!
+A: Read this helpful Norton FAQ:
+
+Alright, here's a guide to by-passing Norton:
+
+* First, to download Pesterchum:
+ 1. Make sure you're on a Moderator account. Moreso for the Norton steps than these ones.
+ 2. Download the .zip file, not the .exe file.
+ 3. Unzip the .zip file onto memory. Pesterchum should now be installed.
+
+* Now, to by-pass Norton:
+ 1. Make sure you're still on a moderator account.
+ 2. Open up Norton.
+ 3. Click on 'Settings' up in the upperright hand corner.
+ 4. Click on 'Anitivirus', off to the upper left. It has a small image of a needle or something similar off to it's side.
+ 5. There's a word that reads 'SONAR protection' halfway to the bottomleft. Off to it's right, there's a bar that's half green. Click on the bar.
+ 6. It will warn you about turning off SONAR. Have it set to turn back on when the system restarts.
+ 7. If done properly, the background for the main page of Norton(what you saw on steps 2-3) has turned an apocaliptic red. Feel free to close Norton now. Keep in mind to stay off suspicious online sites now.
+ 8. Open up Pesterchum, and let the chummy convos begin.
+
+* When finished:
+ 1. Log off of Pesterchum. LOG OFF, NOT CLOSE IT.
+ 2. Then, you can either shut off your comp, and Norton will re-enable SONAR, or you can repeat steps 1-5, except turning the red bar green. If done right, Norton will be it's happy color again.
+ 3, Keep in mind that you must repeat all of this(other than the download) every time you want to get on Pesterchum.
+
+* Hope this is helpful!
+
+(This guide brought to you by the slightly combined efforts of empireomega and Xanaomin)
+
+------------------------------------------------------------------------------------
+
+Q: I can't connect because my school/university/network/stolen wifi is blocking my connection! OR I can't seem to connect to the server at all and I'm not running any firewalls!
+A: Edit your pesterchum.js file. Open it up in notepad or something, and then edit the beginning so it looks like this:
+
+{"port": "1413", ....
+
+where the .... is the rest of the gobbledygook there.
+
+------------------------------------------------------------------------------------
+
+Q: The mood buttons on Pesterchum 6.0 don't match up to what it sets your mood to! What gives?
+A: The mood names are just there to look canon. It is intentional.
+
+------------------------------------------------------------------------------------
+
+Q: I'm appearing as offline to 2.5 users/other users appear the wrong
+mood? What's happeninggggg
+A: The 2.5 people decided to change the mood protocol. When I made
+this program, I decided to go with Tinychat's original protocol (and
+extend it). So some moods will appear wrong between 2.5
+users. (*COUGH*tell them to switch to 3.14*COUGH*)
+
+------------------------------------------------------------------------------------
+
+Q: Pesterchum 2.5 users don't get my /me messages correctly!
+A: That's because they implemented the /me command differently.
+
+------------------------------------------------------------------------------------
+
+Q: Can we resize the main window?
+A: No. This is done so we can offer more flexible UI creation. It's a
+lot easier to make themes that look canon this way.
+
+------------------------------------------------------------------------------------
+
+Q: Can we have different chum rolls for different users?
+A: No. Instead what we now have crum groups to organize people.
+
+------------------------------------------------------------------------------------
+
+Q: Can we delete profiles?
+A: Yes. Go to the profile switcher, choose a profile and press DELETE.
+
+------------------------------------------------------------------------------------
+
+Q: You should make it so you can ban specific time frames in memos.
+A: This was too complicated to implement, and I don't have the UI
+quite figured out. This will probably go in a future update.
+
+
+DOCUMENTATION
+-------------
+
+STARTING
+--------
+
+If this is your first time running Pesterchum 3.14, you need to create
+a new profile. Just type in your chum handle in the box and click the
+color swatch to pick your color. Check the "default" checkbox to make
+this your default profile.
+
+BASIC PESTERING
+---------------
+To begin pestering, first click the "ADD CHUM" button and type in
+their pester handle. The handle must be all lower case except for one
+capital letter. Once you've added that person, they will appear on
+your chumroll. You can double click to begin pestering them, or
+right-click to bring up a menu where you can pester them, block them,
+or remove them from your chumroll. (Or you can select them and hit
+"enter" OR hit the "PESTER" button.)
+
+Once you begin pestering somebody (or they begin pestering you), it
+will bring up the conversation window. Here you can type to your
+chum. Also remember that if you right-click on the area just above the
+Pesterlog, it will bring up a list of options: Quirks Off will turn
+your quirks off, Add Chum will add this chum to your list, and Block
+will block them. (Those last two options are useful if you are being
+pestered by someone you don't have on your list yet!)
+
+While pestering your chum, here are some useful features:
+
+* Type /me to create a system message. "/me facepalms." will generate:
+-- ghostDunk [GD] facepalms. --
+ You can also append 's after /me like so: "/me's computer exploded."
+-- ghostDunk's [GD'S] computer exploded. --
+ In fact, any characters you type after a /me before the space will
+ be added: "/meing is the Ghost Nation's official pastime."
+-- ghostDunking [GDING] is the Ghost Nation's official pastime. --
+
+* Color tags! If you feel the need to talk about The Green Sun or add
+ some appleberry blast to your conversation, just use color
+ tags. These work like in TC 1.5: colored text. But in
+ PC 3.14, you can use type your color in a lot of different ways:
+ - You can use the familiar r,g,b method:
+ "The Green Sun"
+ - You can use HTML tags:
+ "DURR I'M KARKAT AND I'M A HUGE IDIOT"
+ - You can even use plain color names:
+ "D4V3 TH1S 1S SO D3C4D3NT"
+ (list: http://www.w3schools.com/css/css_colornames.asp)
+ - You don't even have to add the if you are lazy. Just use a
+ new color tag whenever you want to change colors and PC 3.14 will
+ add the extra tags for you.
+
+* URLS (anything with http:// in front of it) will automatically be
+ detected and made into a link you can CLICK.
+
+* You can also link people to memos by typing "#" and the name of the
+ menu like so: #R41NBOW_RUMPUS_P4RTYTOWN
+ Clicking the link will open up the memo select menu.
+
+* Smilies! There are a list of smilies at the end of this document;
+ they are based on the MSPA Forum smilies.
+
+* Don't worry about your quirks screwing up any of the above: PC will
+ apply your quirks AFTER it figures out color codes, links, smilies, etc.
+
+* Pressing the up arrow will cycle through a history of your comments,
+ so if you want to retype something, you can pull it up.
+
+* You can submit directly to the Pesterchum Quote Database! If you
+ have a particualarly awesome conversation, you can submit it to the
+ database by simply highlighting the good part of the conversation,
+ right clicking it and choosing "Submit to Pesterchum QDB!"
+
+MEMOS
+-----
+One of the most interesting features to make was the memos, and make
+them as close to the comic as I could without actually inventing time
+travel. So here is the TIME TUTORIAL:
+
+* Joining: When you go CLIENT->MEMOS, you'll see a list of memos pop up
+-- those are memos people already have open. To join one, just
+highlight one of them. If you want to make a new memo, just type it in
+the input. If you'd like to make it secret, so that it doesn't appear
+in the list, check "HIDDEN CHANNEL". Then, choose what timeframe you
+want to appear to be in. So if you wanted to be in the future, you
+could move the slider to the right. You can also enter the time
+manually. Then hit JOIN.
+
+* Explaining time: Time in memos, unlike Homestuck, will not be relative
+to your position. That is, if you choose 4:13 in the future, you will
+not see someone who has set their time as "current" (or "0") in the
+past: you will see them as "current" and yourself as "future." This is
+because we do not have time travel! Memo time setting is basically an
+RP mechanic: you are pretending to be from the future! It will also
+help keep everyone straight: everyone will see the same thing!
+
+* The time slider: The slider shows your current position in the time
+stream. If you want to change your time frame, simply move the slider
+(or type a time in) and hit GO. This will open a new time frame, and
+the next time you type a message, the memo will show that you've
+responded to it in that time frame. You can now switch between your
+time frames simply by clicking the arrows in the right hand
+corner. (THIS COMES IN HANDY IF YOU WANT TO ARGUE WITH YOURSELF.) You
+can have any number of open time frames, and the program will number
+them in the order in which you open them (like in the comic). You can
+have one of your time frames cease responding to the memo by hitting
+"CLOSE." If you open that time frame again, the program will remember
+the number it originally gave it. If you want to be mysteeeeeeeerious,
+you can type in "?" and you will appear as ???.
+
+* The memo viewer list: To the right is a list of people currently
+browsing the memo. A shade icon next to their name means they are the
+"operator" of the memo: meaning they can kick ("ban") people from the
+memo and make other people operators as well. A "ban" is not permanent
+(like in the comic), and the program will ask if you want to reconnect
+to the memo. You kick and op people by right clicking their name in
+the window. You can also add them to your chumroll!
+
+* Inviting people to your memo: You can link to a memo by simply typing
+"#nameofmemo" in any conversation or memo window. So you can say:
+
+ CG: NOW YOU, ME, AND EGBERT NEED TO HAVE A CHAT.
+ CG: CLICK IT.
+ CG: #FRUITYRUMPUSASSHOLEFACTORY
+
+ and it will appear as a link that you can click, which will open the
+ memo chooser window.
+
+CLIENT MENU
+-----------
+
+###OPTIONS:
+* Chum List
+ * Hide Offline Chums: Turning this option on will hide all offline chums
+ off your chumroll.
+
+ * Show Empty Groups: Turning this option on will show empty groups.
+
+ * Show Number of Online Chums: Show number of online chums in each group.
+
+ * Sort Chums: How would you like your chums sorted?
+
+* Conversations
+ * Time Stamps: Turning this on will show timestamps in your chats.
+
+ * 12/24 hour: Formatting for timestamps. Whether you want them in 12 or
+ 24 hour time.
+
+ * Show Seconds: Turning this on will show the seconds in your timestamps.
+
+ * Show OP and Voice Messages in Memos: Whether or not you would like
+ to see messages when people gain/lose OP or Voice.
+
+ * Use animated smilies: To animate or not to animate.
+
+* Interface
+ * Tabbed Conversations: Turns tabbed conversations on and off. Don't
+ worry if you do this in the middle of a conversation, PC will save
+ them for you.
+
+ * Minimize: What do you want the minimize button to do?
+
+ * Close: What do you want the close button to do?
+
+* Sound
+ * Sounds On: Uncheck to shut it the fuck up.
+
+ * Pester Sounds: Uncheck to only turn off Pester sounds.
+
+ * Memo Sounds: Uncheck to only turn off Memo sounds.
+
+ * Memo Mentions: Check to have a separate noise when your initials
+ get mentioned in a memo.
+
+* Logging
+ * Log all Pesters: Log one-on-one chats.
+
+ * Log all Memos: Log everything said in memos.
+
+ * Log Time Stamps for Pesters
+
+ * Log Time Stamps for Memos
+
+* Idle/Updates
+ * Minutes before Idle: How long before you should be considered idle.
+
+ * Check for Pesterchum Updates: How often to check for updates
+ to Pesterchum.
+
+ * Check for MSPA Updates: Check the MSPA site for updates to comics.
+
+* Theme
+ * Pick a Theme
+
+###MEMOS:
+Opens the Memo list as above.
+
+###USERLIST:
+Shows a list of all the users that are currently logged onto
+Pesterchum. Right-click their names and select "ADD CHUM" to add them
+to your chum roll!
+
+###IDLE:
+Make yourself an idle chum. You will appear as idle until you
+uncheck this box, or if you *actually* go idle (stop using the
+computer) for 10 minutes and then come back.
+
+###IMPORT:
+Imports your old Pesterchum 2.0, 2.5 and Tinychum chum
+rolls. This will also import your old quirks from Pesterchum 2.5.
+
+###RECONNECT:
+Forces PC to reconnect to the server.
+
+###EXIT:
+noooooooooooooooooooooooo
+
+PROFILE MENU
+------------
+
+###QUIRKS:
+Opens the quirks menu. More on that below!
+
+###TROLLSLUM:
+Opens up the window where you can view people you've
+blocked. You can add and remove people to the list from here as well.
+
+###COLOR:
+Change your text color here!
+
+###SWITCH:
+Switch your profile! You can have any number of profiles, and
+PC will save your color, quirks, and theme for that profile. Chumrolls
+and block lists are the same for all profiles. Feel free to have
+multiple instances of PC running on two or more handles!
+
+HELP MENU
+---------
+
+###HELP:
+Get taken to a handy dandy tutorial for Pesterchum!
+
+###CALSPRITE:
+Open a chat with calSprite (learn more about calSprite below).
+
+###NICKSERV:
+Open a chat with NickServ. If you don't know what NickServ is, you don't need to.
+
+###ABOUT:
+See which version of Pesterchum you have. Learn about all the awesome people
+that helped bring Pesterchum 3.14 to you!
+
+###REPORT BUG:
+Report any bugs you come across so we can fix them and make Pesterchum
+even better!
+
+CALSPRITE
+---------
+calSprite is the bot that helps moderate canon handle usage! Simply pester
+calSprite with the world "HELP" (turn your quirks off!) and you
+will get instructions on how to use calSprite!
+
+QUIRKS
+------
+There are six kinds of quirks! I'll teach you how to use them all!
+(In this section, I will use quotes ("") around things so it's clearer
+to see exactly what to type! Don't include these quotes when using
+these examples!
+
+Also, note that your quirks will not work until you save them by
+hitting "OK" on the Quirk window.
+
+* Prefix/Suffix:
+This will put text before or after everything you
+say. So for example, we can use prefixes to emulate part of Nepeta or
+Equius' quirks:
+
+ PREFIX: ":33 < "
+ You type: "*ac twitches her friendly whiskers at ct*"
+ Result:
+ AC: :33 < *ac twitches her friendly whiskers at ct*
+
+
+
+ PREFIX: "D --> "
+ You type: "Hi"
+ Result:
+ CT: D --> Hi
+
+
+ Suffixes work the same way, but at the end of the message:
+
+
+ SUFFIX: "!!!"
+ You type: hey there
+ Result:
+ GD: hey there!!!
+
+
+ Remember that it doesn't automatically add a space! You'll need to add
+ it in (see CT and AC examples again!)
+
+* Simple Replace:
+This will simply take a set of characters and replace them with other
+characters.
+ * Let's add a quirk to our Nepeta:
+
+ Replace: "ee" With: "33"
+ You type: "*ac saunters from her dark cave a little bit sleepy from
+ the recent kill*"
+ Result:
+ AC: :33 < *ac saunters from her dark cave a little bit sl33py from the
+ recent kill*
+
+
+ * Let's add two to Equius:
+
+ Replace: "loo" With: "100"
+ Replace: "x" With "%"
+ You type: "look"
+ Result:
+ CT: D --> 100k
+
+ You type: "What are you expecting to accomplish with this"
+ Result:
+ CT: D --> What are you e%pecting to accomplish with this
+
+
+ * Aradia:
+
+ Replace: "o" With: "0"
+ You type: "and the reward would be within our reach"
+ Result:
+ AA: and the reward w0uld be within 0ur reach
+
+
+ Notice that it is CASE SENSITIVE. So in the above case, if you typed
+ "ABSCOND", it would not replace the "O".
+
+ * Sollux:
+
+ Replace: "i" With: "ii"
+ Replace: "s" With: "2"
+
+
+ * Eridan:
+
+ Replace: "v" With: "vv"
+ Replace: "w" With: "ww"
+
+
+ * Feferi:
+
+ Replace: "h" with: ")("
+ Replace: "H" with: ")("
+ Replace: "E" with: "-E"
+
+
+* Regexp Replace:
+This is a more complex kind of replacement. Regexp stands for "regular
+expression", a kind of programming language (yes, it is a language)
+used to find and replace text. PC 3.14 also includes a function to
+handle capitalization (upper()). If you want to learn it on your own,
+I suggest you start with the Python tutorial
+(http://docs.python.org/howto/regex.html) since PC 3.14 uses Python's
+regexps. Check out V2.5's tutorial too, as that is a pretty good start
+as well.
+
+ * Let's start with Karkat. Regexps are just like your every day find and
+ replace: they search for a string that matches what you want to
+ replace, and replaces it with... the replacement.
+ Regexp: "(.)" Replace with: "upper(\1)"
+
+ Three concepts here. Let's look at the regexp. "(.)" has two things
+ going on. The first is that ".". In regexp speak, "." is the wildcard:
+ it will match *any* character -- and just one.
+
+ The parentheses tell the regexp to *save* what's inside them so you
+ can put it back when you replace. That's what the "\1" is for -- it
+ means, "put the match inside parentheses #1 here". You can have any
+ number of parentheses.
+
+ * "upper()" is a function special to PC 3.14 -- it will uppercase
+ anything inside the parentheses. So in this case, upper will uppercase
+ "\1" -- which, as you recall is what we found inside the
+ parentheses. Which was *every* character. So to sum up, it replaces
+ every character with an uppercase version of that character. WHICH
+ MAKES YOU TALK LIKE THIS.
+
+ * Let's look at Terezi next.
+
+ Regexp: "[aA]" Replace with: "4"
+ Regexp: "[iI]" Replace with: "1"
+ Regexp: "[eE]" Replace with: "3"
+ Regexp: "(.)" Replace with: "upper(\1)"
+
+
+ We already know what the last line does. But what's up with those
+ brackets? What's their deal? Basically, in regular expressions,
+ brackets indicate a list of matching characters. So, basically any
+ single character within the brackets will be matched. In this case,
+ either "a" or "A" will be matched and replaced with "4," and likewise,
+ "i" and "I" will be replaced with "1", and "e" and "E" will be
+ replaced with "3."
+
+ Just like there is an "upper()" function, there is also a "lower()"
+ function. It acts just like "upper()" but instead makes everything
+ inside the parentheses lowercase. This allows you to do things like:
+
+ Regexp: "(.)" Replace with: "lower(\1)"
+ You type: "I AM YELLING"
+ Result:
+ GD: i am yelling
+
+
+ Along with the upper and lower functions is a "scramble()" function.
+ The purpose of this function is to randomly scramble anything inside
+ the parentheses.
+
+ Regexp: "(\w)(\w*)(\w)" Replace with: "\1scramble(\2)\3"
+ You type: "hello there"
+ Result:
+ GD: hlelo trhee
+
+
+ This particular regular expression scrambles all of the letters in
+ the middle of a word. Notice that the "h" and "o" at the beginning
+ and end of hello remain in place while the other letters are scrambled.
+
+ You should also know that "^" is a special character in brackets. If
+ placed immediately after the opening bracket (like "[^"), then the
+ brackets instead match every character *except* the ones in the
+ brackets. So, for example, if you wanted to have a quirk where you
+ capitalized all your letters *except* o, you'd do this:
+
+ Regexp: "([^o])" Replace with: "upper(\1)"
+ You type: "hello there"
+ Result:
+ GD: HELLo THERE
+
+
+ You can also specify a *range* of characters inside the brackets, by
+ using the "-" character. [a-z] will match any lowercase letter. You
+ can combine them, too: [a-z0-9] will match any digit and lowercase letter.
+
+ There are also different shortcuts for character types:
+
+ \d matches any digit; same as [0-9]
+ \D matches any non-digit; same as [^0-9]
+ \s matches any spaces
+ \S matches any non-space
+ \w matches any alphanumeric character; same as [a-zA-Z0-9_]
+ \W matches any non-alphanumeric character; same as [^a-zA-Z0-9_]
+
+
+ You can include this inside brackets, too.
+
+ There's also a special character, \\b. What \\b does is make sure that
+ you are at the beginning or end of a word.
+ * So with that knowledge, let's try Kanaya:
+
+ Regexp: \b(\w) Replace with: upper(\1)
+ You type: "i suggest you come to terms with it"
+ Result:
+ GA: I Suggest You Come To Terms With It
+
+
+ Another feature of regular expressions is the ability to match
+ *repeated* characters. There are three repeat characters: the "\*", the
+ "+", "?", and "{m,n}". They work by playing them after the character,
+ or character type you want to match. (So, you could say "\s+" or ".*")
+
+ The "\*" character matches ZERO or more of that character. So, for
+ example, "f\*" would match "f" and "ff" -- and any other character!
+ That's right, every character counts as matching it zero times. Yeah,
+ it's weird. I suggest you use...
+
+ The "+" character matches ONE or more of that character. So, if we
+ wanted to have a character that wanted to elongate their s's so that
+ they used four 's's every time, like sssso, but didn't want to have
+ eight s's when using words with double s's, like pass, we'd do this:
+
+ Regexp: "s+" Replace with: "ssss"
+ You type: "you shall not pass"
+ Result:
+ UU: you sssshall not passss
+
+
+ As for the other two, I can't really think of any useful quirks to be
+ made with them. But to let you know, "?" matches either 0 or 1 of that
+ character, so "trolls?" would match "troll" and "trolls". "{m,n}"
+ matches between m and n characters. (If you leave out 'n', any number
+ of characters more than m will be matched.) So "s{2,4}" will match
+ "ss", "sss", and "ssss" and that's it.
+
+ Now with repeating expressions, we can do something like make EVERY
+ other WORD capitalized:
+
+ Regexp: "(\w+) (\w+)" Replace with: "upper(\1) \2"
+ You type: "this is pretty annoying i bet"
+ Result:
+ GD: THIS is PRETTY annoying I bet
+
+
+ The \1 matches the first word -- which has been matched because the
+ word is alphanumeric characters, repeated once or more -- and \2
+ matches the second word.
+
+ Another operator to use is the "|", which will match more than one set
+ of characters. For example, "black|red" will match "black" or
+ "red". If you want to match something in the middle of words, you have
+ to use parentheses: "(black|red) romance" will match "black romance"
+ and "red romance".
+
+ Finally, there are the "^" and "$" characters. Yes, we already did the
+ "^" character, but this is OUTSIDE of brackets, not INSIDE. "^"
+ matches the beginning of a message, and "$" matches the end of it. You
+ can use this to make more sophisticated prefix and suffix
+ behaviors. For example, if we have a quirk that adds "..." to the end
+ of all our messages, we can set it up so it doesn't do that if we put
+ punctuation [?!.] at the end. So:
+
+ Regexp: "([^?!.])$" Replace with: "\1..."
+
+
+ This will match the end of any message as long as it doesn't have
+ "?", "!", or "." at the end. Then it will replace it with whatever the
+ last character of the sentence was (remember we're replacing it, so we
+ have to put it back!) and add "..." at the end.
+
+ Careful with the beginning and ending replaces -- if you use more than
+ one, you may not get what you expect because they will ALL be applied,
+ one after the other! This is a bug in my opinion, that I plan to fix!
+
+* Random replace:
+Just like the regexp replace, except that instead of just one thing to
+replace it with, you give it a list. PC will then choose from that
+list randomly. So let's say I want to randomly end my sentences with
+either "bro" or "dog":
+
+ Regexp: "$" Replace with: "bro" and "dog"
+
+
+ * You can also imitate Araida's random "ribbits" in between words:
+
+ Regexp: "\s" Replace with: " ribbit ", " ", " ", " ", " ", " ", etc....
+
+
+ where " " is just a blank space added a bunch of times. (You can see
+ how many blank spaces you've added by clicking on the list.) You have
+ to add the spaces because each entry has the same chance of being
+ selected. (Yes, I know this could be improved.) If you add " ribbit "
+ and 9 spaces, " ribbit " will have a 1/10 chance of being picked.
+
+ Also note that if you add more than one prefix or more than one
+ suffix, it will pick randomly from them instead of adding them both!
+
+* Mispeller:
+Be careful with thsi one. The mispeller will randomly mispell x% of
+the words you type -- where x is the percentage you set the slider
+to. I have attempted to mimic SBaHJ mispelling style but whoof knows
+what will happen oh god ive created a mosnter
+
+
+SMILIES
+-------
+Here's a list of smilies:
+
+* :rancorous:
+* :apple:
+* :bathearst:
+* :cathearst:
+* :woeful:
+* :pleasant:
+* :blueghost:
+* :slimer:
+* :candycorn:
+* :cheer:
+* :duhjohn:
+* :datrump:
+* :facepalm:
+* :bonk:
+* :mspa:
+* :gun:
+* :cal:
+* :amazedfirman:
+* :amazed:
+* :chummy:
+* :cool:
+* :smooth:
+* :distraughtfirman
+* :distraught:
+* :insolent:
+* :bemused:
+* :3:
+* :mystified:
+* :pranky:
+* :tense:
+* :record:
+* :squiddle:
+* :tab:
+* :beetip:
+* :flipout:
+* :befuddled:
+* :pumpkin:
+* :trollcool:
+* :jadecry:
+* :ecstatic:
+* :relaxed:
+* :discontent:
+* :devious:
+* :sleek:
+* :detestful:
+* :mirthful:
+* :manipulative:
+* :vigorous:
+* :perky:
+* :acceptant:
diff --git a/TODO b/TODO
deleted file mode 100644
index 1a89703..0000000
--- a/TODO
+++ /dev/null
@@ -1,26 +0,0 @@
-Bugs:
-* weird memo time bug
-* Windows doesn't show style sheet sometimes?? Maybe related to themes.
-* Issues with connecting? Client not closing connection right? People keep getting "nick taken" messages
-* Windows XP SP2: sometimes mouse clicks dont register? must be some kinda crash
-* enamel doesnt have time arrows
-
-Features:
-* different sound for memos/pesters
-* OOC
-* log viewer needs to have BBCode/HTML/Text copy modes
-* random pesters
-* copy quirks between profiles?
-* chum list groups
-* More complex quirks: by-sound
-* Theme checking
-* Spy mode
-* Animated
-
-Mac Bugs:
-* Mac doesn't show tabs right, display gifs, highlighting thing?
-SS: also the background image is broken
-SS: in the one-on-one pester it resizes with the window
-SS: but the memo one doesn't resize
-SS: and the arrows next to the time thing overlap the CLOSE button
-
diff --git a/TODO.mkdn b/TODO.mkdn
new file mode 100644
index 0000000..01264a5
--- /dev/null
+++ b/TODO.mkdn
@@ -0,0 +1,40 @@
+Todo
+===============
+
+Features
+--------
+* OOC
+* log viewer needs to have BBCode/HTML/Text copy modes
+* copy quirks between profiles?
+* More complex quirks: by-sound
+* Spy mode
+* Turn @ and # links on/off?
+* "someone has friended you" notifier
+* Show true bans?
+* Colour saving boxes things?
+* Chum notes?
+
+Bugs
+----
+* weird memo time bug
+* Windows doesn't show style sheet sometimes?? Maybe related to themes.
+* Issues with connecting? Client not closing connection right? People keep getting "nick taken" messages
+* When using mood sort, scroll position jumps to last selected chum
+* When left for a really long time, animations slow down pesterchum
+* Openning userlist resets appearance of OP/voice for anyone that become OP/voice after you joined a memo
+* If pesterchum is open but offline due to a network failure and you open the memos screen, it connects you but doesn't fetch the memo list when it finishes connecting
+* right clicking an offline chum and choosing remove asks you why you're reporting someone, and if you hit cancel the menus stop working
+* Closing a timeclone doesn't actually cease for everyone else
+
+Windows Bugs
+------------
+* XP SP2: sometimes mouse clicks dont register? must be some kinda crash
+* On reconnect and nick change, momentary theme change causes menu items to stop working
+
+Mac Bugs
+--------
+* Mac doesn't show tabs right, display gifs, highlighting thing?
+* SS: also the background image is broken
+* SS: in the one-on-one pester it resizes with the window
+* SS: but the memo one doesn't resize
+* SS: and the arrows next to the time thing overlap the CLOSE button
diff --git a/bugreport.py b/bugreport.py
new file mode 100644
index 0000000..bdf7561
--- /dev/null
+++ b/bugreport.py
@@ -0,0 +1,74 @@
+from PyQt4 import QtGui, QtCore
+import urllib
+import version
+
+class BugReporter(QtGui.QDialog):
+ def __init__(self, parent=None):
+ QtGui.QDialog.__init__(self, parent)
+ self.mainwindow = parent
+ self.setStyleSheet(self.mainwindow.theme["main/defaultwindow/style"])
+ self.setWindowTitle("Report a Bug")
+ self.setModal(False)
+
+ self.title = QtGui.QLabel("Bug Report:")
+
+ layout_0 = QtGui.QVBoxLayout()
+ layout_0.addWidget(self.title)
+
+ layout_0.addWidget(QtGui.QLabel("Operating System (ex. Windows 7, Ubuntu 10.10):"))
+ self.os = QtGui.QLineEdit(self)
+ self.os.setStyleSheet("background:white; font-weight:bold; color:black; font-size: 10pt;")
+ layout_0.addWidget(self.os)
+
+ layout_0.addWidget(QtGui.QLabel("Description of bug:"))
+ descLabel = QtGui.QLabel("Include as much information as possible\n(theme, related options, what you were doing at the time, etc.)")
+ font = descLabel.font()
+ font.setPointSize(8)
+ descLabel.setFont(font)
+ layout_0.addWidget(descLabel)
+
+ self.textarea = QtGui.QTextEdit(self)
+ self.textarea.setStyleSheet("background:white; font-weight:normal; color:black; font-size: 10pt;")
+
+ layout_0.addWidget(self.textarea)
+
+ self.ok = QtGui.QPushButton("SEND", self)
+ self.ok.setDefault(True)
+ self.connect(self.ok, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('sendReport()'))
+ self.cancel = QtGui.QPushButton("CANCEL", self)
+ self.connect(self.cancel, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('reject()'))
+ layout_2 = QtGui.QHBoxLayout()
+ layout_2.addWidget(self.cancel)
+ layout_2.addWidget(self.ok)
+
+ layout_0.addLayout(layout_2)
+
+ self.setLayout(layout_0)
+
+ @QtCore.pyqtSlot()
+ def sendReport(self):
+ name = unicode(self.mainwindow.profile().handle)
+ os = unicode(self.os.text())
+ msg = unicode(self.textarea.toPlainText())
+
+ if len(os) <= 0 or len(msg) <= 0:
+ msgbox = QtGui.QMessageBox()
+ msgbox.setStyleSheet(self.mainwindow.theme["main/defaultwindow/style"])
+ msgbox.setText("You must fill out all fields first!")
+ msgbox.setStandardButtons(QtGui.QMessageBox.Ok)
+ ret = msgbox.exec_()
+ return
+
+ QtGui.QDialog.accept(self)
+ data = urllib.urlencode({"name":name, "version": version._pcVersion, "os":os, "msg":msg})
+ print "Sending..."
+ f = urllib.urlopen("http://distantsphere.com/pc/reporter.php", data)
+ text = f.read()
+ print text
+ if text == "success!":
+ print "Sent!"
+ else:
+ print "Problems ):"
+
diff --git a/convo.py b/convo.py
index fab1ed6..030cdf4 100644
--- a/convo.py
+++ b/convo.py
@@ -9,7 +9,7 @@ from PyQt4 import QtGui, QtCore
from dataobjs import PesterProfile, Mood, PesterHistory
from generic import PesterIcon
-from parsetools import convertTags, lexMessage, splitMessage, mecmd, colorBegin, colorEnd, img2smiley
+from parsetools import convertTags, lexMessage, splitMessage, mecmd, colorBegin, colorEnd, img2smiley, smiledict
class PesterTabWindow(QtGui.QFrame):
def __init__(self, mainwindow, parent=None, convo="convo"):
@@ -19,11 +19,14 @@ class PesterTabWindow(QtGui.QFrame):
self.mainwindow = mainwindow
self.tabs = QtGui.QTabBar(self)
+ self.tabs.setMovable(True)
self.tabs.setTabsClosable(True)
self.connect(self.tabs, QtCore.SIGNAL('currentChanged(int)'),
self, QtCore.SLOT('changeTab(int)'))
self.connect(self.tabs, QtCore.SIGNAL('tabCloseRequested(int)'),
self, QtCore.SLOT('tabClose(int)'))
+ self.connect(self.tabs, QtCore.SIGNAL('tabMoved(int, int)'),
+ self, QtCore.SLOT('tabMoved(int, int)'))
self.initTheme(self.mainwindow.theme)
self.layout = QtGui.QVBoxLayout()
@@ -194,17 +197,77 @@ class PesterTabWindow(QtGui.QFrame):
self.raise_()
convo.raiseChat()
+ @QtCore.pyqtSlot(int, int)
+ def tabMoved(self, to, fr):
+ l = self.tabIndices
+ for i in l:
+ if l[i] == fr:
+ oldpos = i
+ if l[i] == to:
+ newpos = i
+ l[oldpos] = to
+ l[newpos] = fr
+
windowClosed = QtCore.pyqtSignal()
class PesterText(QtGui.QTextEdit):
def __init__(self, theme, parent=None):
QtGui.QTextEdit.__init__(self, parent)
+ if hasattr(self.parent(), 'mainwindow'):
+ self.mainwindow = self.parent().mainwindow
+ else:
+ self.mainwindow = self.parent()
self.initTheme(theme)
self.setReadOnly(True)
self.setMouseTracking(True)
self.textSelected = False
self.connect(self, QtCore.SIGNAL('copyAvailable(bool)'),
self, QtCore.SLOT('textReady(bool)'))
+ self.urls = {}
+ for k in smiledict:
+ self.addAnimation(QtCore.QUrl("smilies/%s" % (smiledict[k])), "smilies/%s" % (smiledict[k]))
+ self.connect(self.mainwindow, QtCore.SIGNAL('animationSetting(bool)'),
+ self, QtCore.SLOT('animateChanged(bool)'))
+ def addAnimation(self, url, fileName):
+ movie = QtGui.QMovie(self)
+ movie.setFileName(fileName)
+ if movie.frameCount() > 1:
+ self.urls[movie] = url
+ self.connect(movie, QtCore.SIGNAL('frameChanged(int)'),
+ self, QtCore.SLOT('animate(int)'))
+ #movie.start()
+ @QtCore.pyqtSlot(int)
+ def animate(self, frame):
+ if self.mainwindow.config.animations():
+ movie = self.sender()
+ url = self.urls[movie].toString()
+ html = unicode(self.toHtml())
+ if html.find(url) != -1:
+ if self.parent().parent():
+ i = self.parent().parent().tabIndices[self.parent().title()]
+ if self.parent().parent().tabs.currentIndex() == i:
+ self.document().addResource(QtGui.QTextDocument.ImageResource,
+ self.urls[movie], movie.currentPixmap())
+ self.setLineWrapColumnOrWidth(self.lineWrapColumnOrWidth())
+ else:
+ self.document().addResource(QtGui.QTextDocument.ImageResource,
+ self.urls[movie], movie.currentPixmap())
+ self.setLineWrapColumnOrWidth(self.lineWrapColumnOrWidth())
+ @QtCore.pyqtSlot(bool)
+ def animateChanged(self, animate):
+ if animate:
+ for m in self.urls:
+ html = unicode(self.toHtml())
+ if html.find(self.urls[m].toString()) != -1:
+ if m.frameCount() > 1:
+ m.start()
+ else:
+ for m in self.urls:
+ html = unicode(self.toHtml())
+ if html.find(self.urls[m].toString()) != -1:
+ if m.frameCount() > 1:
+ m.stop()
+
@QtCore.pyqtSlot(bool)
def textReady(self, ready):
self.textSelected = ready
@@ -222,6 +285,11 @@ class PesterText(QtGui.QTextEdit):
parent = self.parent()
window = parent.mainwindow
me = window.profile()
+ if self.mainwindow.config.animations():
+ for m in self.urls:
+ if convertTags(lexmsg).find(self.urls[m].toString()) != -1:
+ if m.state() == QtGui.QMovie.NotRunning:
+ m.start()
if self.parent().mainwindow.config.showTimeStamps():
if self.parent().mainwindow.config.time12Format():
time = strftime("[%I:%M")
@@ -276,7 +344,7 @@ class PesterText(QtGui.QTextEdit):
lexmsg[0:0] = [colorBegin("" % (color), color),
"%s: " % (initials)]
lexmsg.append(colorEnd(""))
- self.append(time + convertTags(lexmsg))
+ self.append("" + time + convertTags(lexmsg) + "")
if chum is me:
window.chatlog.log(parent.chum.handle, lexmsg)
else:
@@ -299,16 +367,27 @@ class PesterText(QtGui.QTextEdit):
self.parent().clearNewMessage()
QtGui.QTextEdit.focusInEvent(self, event)
+ def keyPressEvent(self, event):
+ if hasattr(self.parent(), 'textInput'):
+ if event.key() not in [QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown, \
+ QtCore.Qt.Key_Up, QtCore.Qt.Key_Down]:
+ self.parent().textInput.keyPressEvent(event)
+ QtGui.QTextEdit.keyPressEvent(self, event)
+
def mousePressEvent(self, event):
- url = self.anchorAt(event.pos())
- if url != "":
- if url[0] == "#" and url != "#pesterchum":
- self.parent().mainwindow.showMemos(url[1:])
- elif url[0] == "@":
- handle = unicode(url[1:])
- self.parent().mainwindow.newConversation(handle)
- else:
- QtGui.QDesktopServices.openUrl(QtCore.QUrl(url, QtCore.QUrl.TolerantMode))
+ if event.button() == QtCore.Qt.LeftButton:
+ url = self.anchorAt(event.pos())
+ if url != "":
+ if url[0] == "#" and url != "#pesterchum":
+ self.parent().mainwindow.showMemos(url[1:])
+ elif url[0] == "@":
+ handle = unicode(url[1:])
+ self.parent().mainwindow.newConversation(handle)
+ else:
+ if event.modifiers() == QtCore.Qt.ControlModifier:
+ QtGui.QApplication.clipboard().setText(url)
+ else:
+ QtGui.QDesktopServices.openUrl(QtCore.QUrl(url, QtCore.QUrl.TolerantMode))
QtGui.QTextEdit.mousePressEvent(self, event)
def mouseMoveEvent(self, event):
QtGui.QTextEdit.mouseMoveEvent(self, event)
@@ -449,8 +528,12 @@ class PesterConvo(QtGui.QFrame):
self.reportchum = QtGui.QAction(self.mainwindow.theme["main/menus/rclickchumlist/report"], self)
self.connect(self.reportchum, QtCore.SIGNAL('triggered()'),
self, QtCore.SLOT('reportThisChum()'))
+ self.logchum = QtGui.QAction(self.mainwindow.theme["main/menus/rclickchumlist/viewlog"], self)
+ self.connect(self.logchum, QtCore.SIGNAL('triggered()'),
+ self, QtCore.SLOT('openChumLogs()'))
self.optionsMenu.addAction(self.quirksOff)
+ self.optionsMenu.addAction(self.logchum)
self.optionsMenu.addAction(self.addChumAction)
self.optionsMenu.addAction(self.blockAction)
self.optionsMenu.addAction(self.reportchum)
@@ -533,6 +616,7 @@ class PesterConvo(QtGui.QFrame):
# ok if it has a tabconvo parent, send that the notify.
if self.parent():
self.parent().notifyNewMessage(self.title())
+ self.mainwindow.gainAttention.emit(self.parent())
# if not change the window title and update system tray
else:
self.newmessage = True
@@ -540,6 +624,7 @@ class PesterConvo(QtGui.QFrame):
def func():
self.showChat()
self.mainwindow.waitingMessages.addMessage(self.title(), func)
+ self.mainwindow.gainAttention.emit(self)
def clearNewMessage(self):
if self.parent():
@@ -591,6 +676,7 @@ class PesterConvo(QtGui.QFrame):
self.addChumAction.setText(self.mainwindow.theme["main/menus/rclickchumlist/addchum"])
self.blockAction.setText(self.mainwindow.theme["main/menus/rclickchumlist/blockchum"])
self.unblockchum.setText(self.mainwindow.theme["main/menus/rclickchumlist/unblockchum"])
+ self.logchum.setText(self.mainwindow.theme["main/menus/rclickchumlist/viewlog"])
self.textArea.changeTheme(theme)
self.textInput.changeTheme(theme)
@@ -605,7 +691,14 @@ class PesterConvo(QtGui.QFrame):
quirks = self.mainwindow.userprofile.quirks
lexmsg = lexMessage(text)
if type(lexmsg[0]) is not mecmd and self.applyquirks:
- lexmsg = quirks.apply(lexmsg)
+ try:
+ lexmsg = quirks.apply(lexmsg)
+ except:
+ msgbox = QtGui.QMessageBox()
+ msgbox.setText("Whoa there! There seems to be a problem.")
+ msgbox.setInformativeText("A quirk seems to be having a problem. (Possibly you're trying to capture a non-existant group?)")
+ msgbox.exec_()
+ return
lexmsgs = splitMessage(lexmsg)
for lm in lexmsgs:
@@ -634,6 +727,15 @@ class PesterConvo(QtGui.QFrame):
@QtCore.pyqtSlot(bool)
def toggleQuirks(self, toggled):
self.applyquirks = not toggled
+ @QtCore.pyqtSlot()
+ def openChumLogs(self):
+ currentChum = self.chum.handle
+ self.mainwindow.chumList.pesterlogviewer = PesterLogViewer(currentChum, self.mainwindow.config, self.mainwindow.theme, self.mainwindow)
+ self.connect(self.mainwindow.chumList.pesterlogviewer, QtCore.SIGNAL('rejected()'),
+ self.mainwindow.chumList, QtCore.SLOT('closeActiveLog()'))
+ self.mainwindow.chumList.pesterlogviewer.show()
+ self.mainwindow.chumList.pesterlogviewer.raise_()
+ self.mainwindow.chumList.pesterlogviewer.activateWindow()
messageSent = QtCore.pyqtSignal(QtCore.QString, QtCore.QString)
windowClosed = QtCore.pyqtSignal(QtCore.QString)
@@ -644,3 +746,6 @@ class PesterConvo(QtGui.QFrame):
"v": {"center": QtCore.Qt.AlignVCenter,
"top": QtCore.Qt.AlignTop,
"bottom": QtCore.Qt.AlignBottom } }
+
+# the import is way down here to avoid recursive imports
+from logviewer import PesterLogViewer
diff --git a/dataobjs.py b/dataobjs.py
index 7547e86..fac8c0b 100644
--- a/dataobjs.py
+++ b/dataobjs.py
@@ -49,8 +49,16 @@ class pesterQuirk(object):
raise ValueError("Quirks must be given a dictionary")
self.quirk = quirk
self.type = self.quirk["type"]
+ if "on" not in self.quirk:
+ self.quirk["on"] = True
+ self.on = self.quirk["on"]
+ if "group" not in self.quirk:
+ self.quirk["group"] = "Miscellaneous"
+ self.group = self.quirk["group"]
def apply(self, string, first=False, last=False):
- if self.type == "prefix":
+ if not self.on:
+ return string
+ elif self.type == "prefix":
return self.quirk["value"] + string
elif self.type == "suffix":
return string + self.quirk["value"]
@@ -119,10 +127,6 @@ class pesterQuirks(object):
def apply(self, lexed, first=False, last=False):
prefix = [q for q in self.quirklist if q.type=='prefix']
suffix = [q for q in self.quirklist if q.type=='suffix']
- replace = [q for q in self.quirklist if
- q.type=='replace' or q.type=='regexp']
- randomrep = [q for q in self.quirklist if q.type=='random']
- spelling = [q for q in self.quirklist if q.type=='spelling']
newlist = []
for (i, o) in enumerate(lexed):
@@ -136,20 +140,16 @@ class pesterQuirks(object):
continue
lastStr = (i == len(lexed)-1)
string = o
- for s in spelling:
- string = s.apply(string)
- for r in randomrep:
- string = r.apply(string, first=(i==0), last=lastStr)
- for r in replace:
- string = r.apply(string, first=(i==0), last=lastStr)
- if i == 0:
- if len(prefix) >= 1:
- myprefix = random.choice(prefix)
- string = myprefix.apply(string)
- if lastStr:
- if len(suffix) >= 1:
- mysuffix = random.choice(suffix)
- string = mysuffix.apply(string)
+ for q in self.quirklist:
+ if q.type != 'prefix' and q.type != 'suffix':
+ if q.type == 'regexp' or q.type == 'random':
+ string = q.apply(string, first=(i==0), last=lastStr)
+ else:
+ string = q.apply(string)
+ elif q.type == 'prefix' and i == 0:
+ string = q.apply(string)
+ elif q.type == 'suffix' and lastStr:
+ string = q.apply(string)
newlist.append(string)
final = []
@@ -165,7 +165,7 @@ class pesterQuirks(object):
yield q
class PesterProfile(object):
- def __init__(self, handle, color=None, mood=Mood("offline"), chumdb=None):
+ def __init__(self, handle, color=None, mood=Mood("offline"), group=None, chumdb=None):
self.handle = handle
if color is None:
if chumdb:
@@ -174,6 +174,12 @@ class PesterProfile(object):
color = QtGui.QColor("black")
self.color = color
self.mood = mood
+ if group is None:
+ if chumdb:
+ group = chumdb.getGroup(handle, "Chums")
+ else:
+ group = "Chums"
+ self.group = group
def initials(self, time=None):
handle = self.handle
caps = [l for l in handle if l.isupper()]
@@ -203,7 +209,8 @@ class PesterProfile(object):
def plaindict(self):
return (self.handle, {"handle": self.handle,
"mood": self.mood.name(),
- "color": unicode(self.color.name())})
+ "color": unicode(self.color.name()),
+ "group": unicode(self.group)})
def blocked(self, config):
return self.handle in config.getBlocklist()
@@ -223,7 +230,7 @@ class PesterProfile(object):
def moodmsg(self, mood, syscolor, theme):
return "-- %s [%s] changed their mood to %s
--" % (syscolor.name(), self.handle, self.colorhtml(), self.initials(), mood.name().upper(), theme["main/chums/moods"][mood.name()]["icon"])
def idlemsg(self, syscolor, verb):
- return "-- %s [%s] %s --" % (syscolor.name(), self.handle, self.colorhtml(), self.initials(), verb)
+ return "-- %s [%s] %s --" % (syscolor.name(), self.handle, self.colorhtml(), self.initials(), verb)
def memoclosemsg(self, syscolor, timeGrammar, verb):
return "%s%s%s %s." % (syscolor.name(), self.colorhtml(), timeGrammar.pcf, self.initials(), timeGrammar.number, verb)
def memoopenmsg(self, syscolor, td, timeGrammar, verb, channel):
@@ -232,18 +239,38 @@ class PesterProfile(object):
initials = pcf+self.initials()
return "%s %s %s %s." % \
(syscolor.name(), self.colorhtml(), initials, timetext, verb, channel[1:].upper().replace("_", " "))
- def memobanmsg(self, opchum, opgrammar, syscolor, timeGrammar):
+ def memobanmsg(self, opchum, opgrammar, syscolor, timeGrammar, reason):
initials = timeGrammar.pcf+self.initials()+timeGrammar.number
opinit = opgrammar.pcf+opchum.initials()+opgrammar.number
- return "%s banned %s from responding to memo." % \
- (opchum.colorhtml(), opinit, self.colorhtml(), initials)
+ if opchum.handle == reason:
+ return "%s banned %s from responding to memo." % \
+ (opchum.colorhtml(), opinit, self.colorhtml(), initials)
+ else:
+ return "%s banned %s from responding to memo: [%s]." % \
+ (opchum.colorhtml(), opinit, self.colorhtml(), initials, unicode(reason))
def memojoinmsg(self, syscolor, td, timeGrammar, verb):
(temporal, pcf, when) = (timeGrammar.temporal, timeGrammar.pcf, timeGrammar.when)
timetext = timeDifference(td)
initials = pcf+self.initials()+timeGrammar.number
- return "%s %s [%s] %s %s." % \
+ return "%s %s [%s] %s %s." % \
(syscolor.name(), self.colorhtml(), temporal, self.handle,
initials, timetext, verb)
+ def memoopmsg(self, opchum, opgrammar, syscolor):
+ opinit = opgrammar.pcf+opchum.initials()+opgrammar.number
+ return "%s made %s an OP." % \
+ (opchum.colorhtml(), opinit, self.colorhtml(), self.initials())
+ def memodeopmsg(self, opchum, opgrammar, syscolor):
+ opinit = opgrammar.pcf+opchum.initials()+opgrammar.number
+ return "%s took away %s's OP powers." % \
+ (opchum.colorhtml(), opinit, self.colorhtml(), self.initials())
+ def memovoicemsg(self, opchum, opgrammar, syscolor):
+ opinit = opgrammar.pcf+opchum.initials()+opgrammar.number
+ return "%s gave %s voice." % \
+ (opchum.colorhtml(), opinit, self.colorhtml(), self.initials())
+ def memodevoicemsg(self, opchum, opgrammar, syscolor):
+ opinit = opgrammar.pcf+opchum.initials()+opgrammar.number
+ return "%s took away %s's voice." % \
+ (opchum.colorhtml(), opinit, self.colorhtml(), self.initials())
@staticmethod
def checkLength(handle):
diff --git a/easyInstaller b/easyInstaller
new file mode 100755
index 0000000..2592a92
--- /dev/null
+++ b/easyInstaller
@@ -0,0 +1,283 @@
+#!/usr/bin/env python
+
+# This program is free software. It comes without any warranty, to
+# the extent permitted by applicable law. You can redistribute it
+# and/or modify it under the terms of the Do What The Fuck You Want
+# To Public License, Version 2, as published by Sam Hocevar. See
+# http://sam.zoy.org/wtfpl/COPYING for more details.
+import sys, traceback
+error = 0
+try:
+ import os, shutil
+ from stat import *
+ from string import Template
+
+ _PLATFORM = sys.platform
+ if _PLATFORM in ['win32','cygwin','darwin','os2','os2emx','riscos','atheos']:
+ print "Whoa there buddy! This installation script isn't meant to be run on your OS."
+ exit()
+
+ if os.getuid() != 0:
+ print "This program must be run as root (sudo)."
+ exit()
+
+ _HOME = os.environ['HOME']
+ _USER = os.environ['SUDO_USER']
+ _UID = int(os.environ['SUDO_UID'])
+ _GID = int(os.environ['SUDO_GID'])
+ # fix home if it's root weirdness
+ if _HOME.find("root") != -1:
+ _HOME = "/home/"+_USER
+
+ def setPermissions(path):
+ os.chown(path, _UID, _GID)
+ for file_ in os.listdir(path):
+ filePath = os.path.join(path,file_)
+ if os.path.isdir(filePath):
+ setPermissions(filePath)
+ else:
+ os.chown(filePath, _UID, _GID)
+
+ def findPesterchum(path):
+ for f in os.listdir(path):
+ filePath = os.path.join(path, f)
+ if os.path.isdir(filePath):
+ if os.path.exists(filePath+"/pesterchum.py"):
+ return filePath
+ else:
+ a=findPesterchum(filePath)
+ if a: return a
+ elif f == "pesterchum.py":
+ return path
+
+ if not os.path.exists(_HOME+"/.pcInstallLoc"):
+ print "Welcome to the Pesterchum 3.14 Easy Installer (for Linux)!\n\
+ Created by Kiooeht [evacipatedBox] May 28th-29th, 2011.\n\
+ License: WTFPL\n\
+ \n\
+ Leaving an option blank will accept it's default [in brackets]\n\
+ Are you ready to begin your MAGICAL JOURNEY?!\n\
+ Of course you are!!! ::::D"
+ # ask user about things
+ while 1:
+ install = raw_input("Install location [~/.pesterchum]: ")
+ if install == "":
+ instLoc = _HOME+"/.pesterchum"
+ break
+ else:
+ if install[0] == "~":
+ install = _HOME+install[1:]
+ if os.path.exists(install[:install.rfind("/")]):
+ instLoc = install
+ break
+ print "No can do"
+ if os.path.exists("/usr/share/applications"):
+ while 1:
+ gnome = raw_input("Create a GNOME menu item? [Y/n]: ")
+ if gnome.lower() == "y" or gnome == "":
+ gnome = True;break
+ elif gnome.lower() == "n":
+ gnome = False;break
+ else:
+ print "herpaderp"
+ while 1:
+ shortcut = raw_input("Create launcher in home directory? [Y/n]: ")
+ if shortcut.lower() == "y" or shortcut == "":
+ shortcut = True;break
+ elif shortcut.lower() == "n":
+ shortcut = False;break
+ else:
+ print "u jelly?"
+
+ # do some shitty install
+ try:
+ fileLoc = findPesterchum(".")
+ except RuntimeError:
+ print "I'm sorry! I was unable to find the pesterchum files :("
+ print "Please put them where I can find them"
+ exit()
+ if not fileLoc:
+ print "I'm sorry! I was unable to find the pesterchum files :("
+ print "Please put them where I can find them"
+ exit()
+ print "Copying files..."
+ ignore = shutil.ignore_patterns('*.pyc')
+ if not os.path.exists(instLoc):
+ shutil.copytree(fileLoc, instLoc, ignore=ignore)
+ if os.path.exists(instLoc+"/pesterchum.js"):
+ f = open(instLoc+"/pesterchum.js")
+ js = f.read()
+ f.close()
+ defa = js.find("\"defaultprofile\"")
+ if defa != -1:
+ start = js.find("\"", js.find(":", defa+1))
+ end = js.find("\"", start+1)
+ party = js[start+1:end]
+ if not os.path.exists(instLoc+"/profiles") or \
+ party+".js" not in os.listdir(instLoc+"/profiles"):
+ print "Protecting you from stupidity..."
+ print " (aka. Deleting reference to non-existant default profile)"
+ #os.remove(instLoc+"/pesterchum.js")
+ f = open(instLoc+"/pesterchum.js", "w")
+ f.write(js[:defa-1]+js[js.find(",", end)+1:])
+ f.close()
+ else:
+ if not os.path.exists(instLoc+"/logs") and os.path.exists(fileLoc+"/logs"):
+ shutil.copytree(fileLoc+"/logs", instLoc+"/logs", ignore=ignore)
+ if not os.path.exists(instLoc+"/profiles") and os.path.exists(fileLoc+"/profiles"):
+ shutil.copytree(fileLoc+"/profiles", instLoc+"/profiles", ignore=ignore)
+ if not os.path.exists(instLoc+"/pesterchum.js") and os.path.exists(fileLoc+"/pesterchum.js"):
+ shutil.copy(fileLoc+"/pesterchum.js", instLoc)
+ shutil.copytree(fileLoc+"/oyoyo", instLoc+"/oyoyo", ignore=ignore)
+ shutil.copytree(fileLoc+"/smilies", instLoc+"/smilies", ignore=ignore)
+ shutil.copytree(fileLoc+"/themes", instLoc+"/themes", ignore=ignore)
+ for f in os.listdir(fileLoc):
+ filePath = os.path.join(fileLoc, f)
+ if not os.path.isdir(filePath) and f != "pesterchum.js":
+ shutil.copy(filePath, instLoc)
+ setPermissions(instLoc)
+ # save the install location
+ f = open(_HOME+"/.pcInstallLoc", "w")
+ f.write(instLoc)
+ f.close()
+ #create a cool executable
+ print "Creating executable... (/usr/local/bin/pesterchum)"
+ f = open("/usr/local/bin/pesterchum", 'w')
+ f.write("#!/bin/sh\ncd "+instLoc+"\n./pesterchum $@")
+ f.close()
+ os.chmod("/usr/local/bin/pesterchum", S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH)
+ # Create a fancy menu item in gnome
+ if gnome:
+ print "Creating menu item..."
+ t = Template("[Desktop Entry]\nEncoding=UTF-8\nVersion=3.14.2\nName=Pesterchum\nComment=IM client based on Homestuck Pesterchum\nCategories=Network;InstantMessaging;\nExec=/usr/local/bin/pesterchum\nIcon=$loc/pesterchum.ico\nTerminal=false\nType=Application")
+ f = open("/usr/share/applications/pesterchum.desktop", "w")
+ f.write(t.safe_substitute(loc=instLoc))
+ f.close()
+ # create shortcut launcher
+ if shortcut:
+ print "Creating launcher..."
+ t = Template("#!/usr/bin/env xdg-open\n[Desktop Entry]\nEncoding=UTF-8\nVersion=3.14.2\nName=Pesterchum\nComment=IM client based on Homestuck Pesterchum\nCategories=Network;InstantMessaging;\nExec=pesterchum\nIcon=$loc/pesterchum.ico\nTerminal=false\nType=Application")
+ f = open(_HOME+"/Pesterchum.desktop", "w")
+ f.write(t.safe_substitute(loc=instLoc))
+ f.close()
+ os.chown(_HOME+"/Pesterchum.desktop", _UID, _GID)
+ os.chmod(_HOME+"/Pesterchum.desktop", S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH)
+ else:
+ print "Welcome to the Pesterchum 3.14 Easy Uninstaller (for Linux)!\n\
+ Created by Kiooeht [evacipatedBox] May 28th-29th, 2011.\n\
+ License: WTFPL\n"
+ while 1:
+ remove = raw_input("Would you like to uninstall Pesterchum? [y/n]: ")
+ if remove.lower() == "y":
+ while 1:
+ killdata = raw_input("Purge your settings, profiles, and logs? [y/N]: ")
+ if killdata.lower() == "n" or killdata == "":
+ killdata = False;break
+ elif killdata.lower() == "y":
+ killdata = True;break
+ else:
+ print "Hmmmmm...?"
+ f = open(_HOME+"/.pcInstallLoc")
+ instLoc = f.readline()
+ f.close()
+ os.remove(_HOME+"/.pcInstallLoc")
+ if killdata:
+ print "Removing files..."
+ shutil.rmtree(instLoc)
+ else:
+ print "Backing up settings, profiles, and logs..."
+ if os.path.exists(instLoc+"/logs"):
+ shutil.move(instLoc+"/logs", "_easyBackupLOGS")
+ if os.path.exists(instLoc+"/profiles"):
+ shutil.move(instLoc+"/profiles", "_easyBackupPROFILES")
+ if os.path.exists(instLoc+"/pesterchum.js"):
+ shutil.move(instLoc+"/pesterchum.js", "_easyBackupSETTINGS")
+ print "Removing files..."
+ shutil.rmtree(instLoc)
+ print "Restoring up settings, profiles, and logs..."
+ os.mkdir(instLoc)
+ if os.path.exists("_easyBackupLOGS"):
+ shutil.move("_easyBackupLOGS", instLoc+"/logs")
+ if os.path.exists("_easyBackupPROFILES"):
+ shutil.move("_easyBackupPROFILES", instLoc+"/profiles")
+ if os.path.exists("_easyBackupSETTINGS"):
+ shutil.move("_easyBackupSETTINGS", instLoc+"/pesterchum.js")
+ setPermissions(instLoc)
+ print "Trashing executable..."
+ os.remove("/usr/local/bin/pesterchum")
+ if os.path.exists("/usr/share/applications/pesterchum.desktop"):
+ print "Maiming menu item..."
+ os.remove("/usr/share/applications/pesterchum.desktop")
+ if os.path.exists(_HOME+"/Pesterchum.desktop"):
+ print "Destroying launcher..."
+ os.remove(_HOME+"/Pesterchum.desktop")
+ elif os.path.exists(_HOME+"/Desktop/Pesterchum.desktop"):
+ print "Destroying launcher..."
+ os.remove(_HOME+"/Desktop/Pesterchum.desktop")
+ else:
+ print "Unable to find launcher, non destroyed"
+ break
+ elif remove.lower() == "n":
+ print "Aborting uninstallation process"
+ break
+ else:
+ print "Invalid input, try again"
+except KeyboardInterrupt:
+ print ""
+except Exception, e:
+ error = -1
+finally:
+ if error == -1:
+ print "Oh noes!! It seems an error has occurred!"
+ lineN = traceback.extract_tb(sys.exc_info()[2])[-1][1]
+ print "The error occurred on line %s:" % lineN
+ formatted_lines = traceback.format_exc().splitlines()
+ print " '" + formatted_lines[-2] + "'"
+ print formatted_lines[-1]
+
+ while 1:
+ print "\nWould you like to (s)end a bug report,"
+ send = raw_input("view the (f)ull error message, or (n)either? [s/f/n]: ")
+ if send.lower() == "n":
+ act = 2;break
+ elif send.lower() == "s":
+ act = 0;break
+ elif send.lower() == "f":
+ print "!---------------BEGIN ERROR MESSAGE---------------!"
+ for l in formatted_lines:
+ print l
+ print "!----------------END ERROR MESSAGE----------------!"
+ send = raw_input("Would you like to send this error message? [y/n]: ")
+ if send.lower() == "y":
+ act = 0;break
+ elif send.lower() == "n":
+ act = 2;break
+ else:
+ print "What was that?"
+ if act == 2:
+ print "Okay"
+ elif act == 0:
+ print "Thank you for taking time out of your day to complete a bug report."
+ print "Fields marked with an asterisk (*) are required."
+ name = raw_input("Your Name: ")
+ while 1:
+ os = raw_input("OS (include version) (ex. Ubuntu 10.10) [*]: ")
+ if os: break
+ else: print "This field is required."
+ while 1:
+ msg = raw_input("Short description of problem [*]: ")
+ if msg: break
+ else: print "This field is required."
+ import urllib, json
+ data = urllib.urlencode({"name":name, "os":os, "msg":msg, "short":formatted_lines[-1], "long":json.dumps(formatted_lines)})
+ try:
+ print "Sending..."
+ f = urllib.urlopen("http://distantsphere.com/pc/easyInstall.php", data)
+ text = f.read()
+ print text
+ if text == "success!":
+ print "Sent!"
+ else:
+ print "There seems to have been a problem sending your bug report! ):"
+ except:
+ print "There seems to have been a problem sending your bug report! ):"
diff --git a/feedparser.py b/feedparser.py
new file mode 100755
index 0000000..bb802df
--- /dev/null
+++ b/feedparser.py
@@ -0,0 +1,2858 @@
+#!/usr/bin/env python
+"""Universal feed parser
+
+Handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds
+
+Visit http://feedparser.org/ for the latest version
+Visit http://feedparser.org/docs/ for the latest documentation
+
+Required: Python 2.1 or later
+Recommended: Python 2.3 or later
+Recommended: CJKCodecs and iconv_codec
+"""
+
+__version__ = "4.1"# + "$Revision: 1.92 $"[11:15] + "-cvs"
+__license__ = """Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE."""
+__author__ = "Mark Pilgrim "
+__contributors__ = ["Jason Diamond ",
+ "John Beimler ",
+ "Fazal Majid ",
+ "Aaron Swartz ",
+ "Kevin Marks "]
+_debug = 0
+
+# HTTP "User-Agent" header to send to servers when downloading feeds.
+# If you are embedding feedparser in a larger application, you should
+# change this to your application name and URL.
+USER_AGENT = "UniversalFeedParser/%s +http://feedparser.org/" % __version__
+
+# HTTP "Accept" header to send to servers when downloading feeds. If you don't
+# want to send an Accept header, set this to None.
+ACCEPT_HEADER = "application/atom+xml,application/rdf+xml,application/rss+xml,application/x-netcdf,application/xml;q=0.9,text/xml;q=0.2,*/*;q=0.1"
+
+# List of preferred XML parsers, by SAX driver name. These will be tried first,
+# but if they're not installed, Python will keep searching through its own list
+# of pre-installed parsers until it finds one that supports everything we need.
+PREFERRED_XML_PARSERS = ["drv_libxml2"]
+
+# If you want feedparser to automatically run HTML markup through HTML Tidy, set
+# this to 1. Requires mxTidy
+# or utidylib .
+TIDY_MARKUP = 0
+
+# List of Python interfaces for HTML Tidy, in order of preference. Only useful
+# if TIDY_MARKUP = 1
+PREFERRED_TIDY_INTERFACES = ["uTidy", "mxTidy"]
+
+# ---------- required modules (should come with any Python distribution) ----------
+import sgmllib, re, sys, copy, urlparse, time, rfc822, types, cgi, urllib, urllib2
+try:
+ from cStringIO import StringIO as _StringIO
+except:
+ from StringIO import StringIO as _StringIO
+
+# ---------- optional modules (feedparser will work without these, but with reduced functionality) ----------
+
+# gzip is included with most Python distributions, but may not be available if you compiled your own
+try:
+ import gzip
+except:
+ gzip = None
+try:
+ import zlib
+except:
+ zlib = None
+
+# If a real XML parser is available, feedparser will attempt to use it. feedparser has
+# been tested with the built-in SAX parser, PyXML, and libxml2. On platforms where the
+# Python distribution does not come with an XML parser (such as Mac OS X 10.2 and some
+# versions of FreeBSD), feedparser will quietly fall back on regex-based parsing.
+try:
+ import xml.sax
+ xml.sax.make_parser(PREFERRED_XML_PARSERS) # test for valid parsers
+ from xml.sax.saxutils import escape as _xmlescape
+ _XML_AVAILABLE = 1
+except:
+ _XML_AVAILABLE = 0
+ def _xmlescape(data):
+ data = data.replace('&', '&')
+ data = data.replace('>', '>')
+ data = data.replace('<', '<')
+ return data
+
+# base64 support for Atom feeds that contain embedded binary data
+try:
+ import base64, binascii
+except:
+ base64 = binascii = None
+
+# cjkcodecs and iconv_codec provide support for more character encodings.
+# Both are available from http://cjkpython.i18n.org/
+try:
+ import cjkcodecs.aliases
+except:
+ pass
+try:
+ import iconv_codec
+except:
+ pass
+
+# chardet library auto-detects character encodings
+# Download from http://chardet.feedparser.org/
+try:
+ import chardet
+ if _debug:
+ import chardet.constants
+ chardet.constants._debug = 1
+except:
+ chardet = None
+
+# ---------- don't touch these ----------
+class ThingsNobodyCaresAboutButMe(Exception): pass
+class CharacterEncodingOverride(ThingsNobodyCaresAboutButMe): pass
+class CharacterEncodingUnknown(ThingsNobodyCaresAboutButMe): pass
+class NonXMLContentType(ThingsNobodyCaresAboutButMe): pass
+class UndeclaredNamespace(Exception): pass
+
+sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*')
+sgmllib.special = re.compile('' % (tag, ''.join([' %s="%s"' % t for t in attrs])), escape=0)
+
+ # match namespaces
+ if tag.find(':') <> -1:
+ prefix, suffix = tag.split(':', 1)
+ else:
+ prefix, suffix = '', tag
+ prefix = self.namespacemap.get(prefix, prefix)
+ if prefix:
+ prefix = prefix + '_'
+
+ # special hack for better tracking of empty textinput/image elements in illformed feeds
+ if (not prefix) and tag not in ('title', 'link', 'description', 'name'):
+ self.intextinput = 0
+ if (not prefix) and tag not in ('title', 'link', 'description', 'url', 'href', 'width', 'height'):
+ self.inimage = 0
+
+ # call special handler (if defined) or default handler
+ methodname = '_start_' + prefix + suffix
+ try:
+ method = getattr(self, methodname)
+ return method(attrsD)
+ except AttributeError:
+ return self.push(prefix + suffix, 1)
+
+ def unknown_endtag(self, tag):
+ if _debug: sys.stderr.write('end %s\n' % tag)
+ # match namespaces
+ if tag.find(':') <> -1:
+ prefix, suffix = tag.split(':', 1)
+ else:
+ prefix, suffix = '', tag
+ prefix = self.namespacemap.get(prefix, prefix)
+ if prefix:
+ prefix = prefix + '_'
+
+ # call special handler (if defined) or default handler
+ methodname = '_end_' + prefix + suffix
+ try:
+ method = getattr(self, methodname)
+ method()
+ except AttributeError:
+ self.pop(prefix + suffix)
+
+ # track inline content
+ if self.incontent and self.contentparams.has_key('type') and not self.contentparams.get('type', 'xml').endswith('xml'):
+ # element declared itself as escaped markup, but it isn't really
+ self.contentparams['type'] = 'application/xhtml+xml'
+ if self.incontent and self.contentparams.get('type') == 'application/xhtml+xml':
+ tag = tag.split(':')[-1]
+ self.handle_data('%s>' % tag, escape=0)
+
+ # track xml:base and xml:lang going out of scope
+ if self.basestack:
+ self.basestack.pop()
+ if self.basestack and self.basestack[-1]:
+ self.baseuri = self.basestack[-1]
+ if self.langstack:
+ self.langstack.pop()
+ if self.langstack: # and (self.langstack[-1] is not None):
+ self.lang = self.langstack[-1]
+
+ def handle_charref(self, ref):
+ # called for each character reference, e.g. for ' ', ref will be '160'
+ if not self.elementstack: return
+ ref = ref.lower()
+ if ref in ('34', '38', '39', '60', '62', 'x22', 'x26', 'x27', 'x3c', 'x3e'):
+ text = '%s;' % ref
+ else:
+ if ref[0] == 'x':
+ c = int(ref[1:], 16)
+ else:
+ c = int(ref)
+ text = unichr(c).encode('utf-8')
+ self.elementstack[-1][2].append(text)
+
+ def handle_entityref(self, ref):
+ # called for each entity reference, e.g. for '©', ref will be 'copy'
+ if not self.elementstack: return
+ if _debug: sys.stderr.write('entering handle_entityref with %s\n' % ref)
+ if ref in ('lt', 'gt', 'quot', 'amp', 'apos'):
+ text = '&%s;' % ref
+ else:
+ # entity resolution graciously donated by Aaron Swartz
+ def name2cp(k):
+ import htmlentitydefs
+ if hasattr(htmlentitydefs, 'name2codepoint'): # requires Python 2.3
+ return htmlentitydefs.name2codepoint[k]
+ k = htmlentitydefs.entitydefs[k]
+ if k.startswith('') and k.endswith(';'):
+ return int(k[2:-1]) # not in latin-1
+ return ord(k)
+ try: name2cp(ref)
+ except KeyError: text = '&%s;' % ref
+ else: text = unichr(name2cp(ref)).encode('utf-8')
+ self.elementstack[-1][2].append(text)
+
+ def handle_data(self, text, escape=1):
+ # called for each block of plain text, i.e. outside of any tag and
+ # not containing any character or entity references
+ if not self.elementstack: return
+ if escape and self.contentparams.get('type') == 'application/xhtml+xml':
+ text = _xmlescape(text)
+ self.elementstack[-1][2].append(text)
+
+ def handle_comment(self, text):
+ # called for each comment, e.g.
+ pass
+
+ def handle_pi(self, text):
+ # called for each processing instruction, e.g.
+ pass
+
+ def handle_decl(self, text):
+ pass
+
+ def parse_declaration(self, i):
+ # override internal declaration handler to handle CDATA blocks
+ if _debug: sys.stderr.write('entering parse_declaration\n')
+ if self.rawdata[i:i+9] == '', i)
+ if k == -1: k = len(self.rawdata)
+ self.handle_data(_xmlescape(self.rawdata[i+9:k]), 0)
+ return k+3
+ else:
+ k = self.rawdata.find('>', i)
+ return k+1
+
+ def mapContentType(self, contentType):
+ contentType = contentType.lower()
+ if contentType == 'text':
+ contentType = 'text/plain'
+ elif contentType == 'html':
+ contentType = 'text/html'
+ elif contentType == 'xhtml':
+ contentType = 'application/xhtml+xml'
+ return contentType
+
+ def trackNamespace(self, prefix, uri):
+ loweruri = uri.lower()
+ if (prefix, loweruri) == (None, 'http://my.netscape.com/rdf/simple/0.9/') and not self.version:
+ self.version = 'rss090'
+ if loweruri == 'http://purl.org/rss/1.0/' and not self.version:
+ self.version = 'rss10'
+ if loweruri == 'http://www.w3.org/2005/atom' and not self.version:
+ self.version = 'atom10'
+ if loweruri.find('backend.userland.com/rss') <> -1:
+ # match any backend.userland.com namespace
+ uri = 'http://backend.userland.com/rss'
+ loweruri = uri
+ if self._matchnamespaces.has_key(loweruri):
+ self.namespacemap[prefix] = self._matchnamespaces[loweruri]
+ self.namespacesInUse[self._matchnamespaces[loweruri]] = uri
+ else:
+ self.namespacesInUse[prefix or ''] = uri
+
+ def resolveURI(self, uri):
+ return _urljoin(self.baseuri or '', uri)
+
+ def decodeEntities(self, element, data):
+ return data
+
+ def push(self, element, expectingText):
+ self.elementstack.append([element, expectingText, []])
+
+ def pop(self, element, stripWhitespace=1):
+ if not self.elementstack: return
+ if self.elementstack[-1][0] != element: return
+
+ element, expectingText, pieces = self.elementstack.pop()
+ output = ''.join(pieces)
+ if stripWhitespace:
+ output = output.strip()
+ if not expectingText: return output
+
+ # decode base64 content
+ if base64 and self.contentparams.get('base64', 0):
+ try:
+ output = base64.decodestring(output)
+ except binascii.Error:
+ pass
+ except binascii.Incomplete:
+ pass
+
+ # resolve relative URIs
+ if (element in self.can_be_relative_uri) and output:
+ output = self.resolveURI(output)
+
+ # decode entities within embedded markup
+ if not self.contentparams.get('base64', 0):
+ output = self.decodeEntities(element, output)
+
+ # remove temporary cruft from contentparams
+ try:
+ del self.contentparams['mode']
+ except KeyError:
+ pass
+ try:
+ del self.contentparams['base64']
+ except KeyError:
+ pass
+
+ # resolve relative URIs within embedded markup
+ if self.mapContentType(self.contentparams.get('type', 'text/html')) in self.html_types:
+ if element in self.can_contain_relative_uris:
+ output = _resolveRelativeURIs(output, self.baseuri, self.encoding)
+
+ # sanitize embedded markup
+ if self.mapContentType(self.contentparams.get('type', 'text/html')) in self.html_types:
+ if element in self.can_contain_dangerous_markup:
+ output = _sanitizeHTML(output, self.encoding)
+
+ if self.encoding and type(output) != type(u''):
+ try:
+ output = unicode(output, self.encoding)
+ except:
+ pass
+
+ # categories/tags/keywords/whatever are handled in _end_category
+ if element == 'category':
+ return output
+
+ # store output in appropriate place(s)
+ if self.inentry and not self.insource:
+ if element == 'content':
+ self.entries[-1].setdefault(element, [])
+ contentparams = copy.deepcopy(self.contentparams)
+ contentparams['value'] = output
+ self.entries[-1][element].append(contentparams)
+ elif element == 'link':
+ self.entries[-1][element] = output
+ if output:
+ self.entries[-1]['links'][-1]['href'] = output
+ else:
+ if element == 'description':
+ element = 'summary'
+ self.entries[-1][element] = output
+ if self.incontent:
+ contentparams = copy.deepcopy(self.contentparams)
+ contentparams['value'] = output
+ self.entries[-1][element + '_detail'] = contentparams
+ elif (self.infeed or self.insource) and (not self.intextinput) and (not self.inimage):
+ context = self._getContext()
+ if element == 'description':
+ element = 'subtitle'
+ context[element] = output
+ if element == 'link':
+ context['links'][-1]['href'] = output
+ elif self.incontent:
+ contentparams = copy.deepcopy(self.contentparams)
+ contentparams['value'] = output
+ context[element + '_detail'] = contentparams
+ return output
+
+ def pushContent(self, tag, attrsD, defaultContentType, expectingText):
+ self.incontent += 1
+ self.contentparams = FeedParserDict({
+ 'type': self.mapContentType(attrsD.get('type', defaultContentType)),
+ 'language': self.lang,
+ 'base': self.baseuri})
+ self.contentparams['base64'] = self._isBase64(attrsD, self.contentparams)
+ self.push(tag, expectingText)
+
+ def popContent(self, tag):
+ value = self.pop(tag)
+ self.incontent -= 1
+ self.contentparams.clear()
+ return value
+
+ def _mapToStandardPrefix(self, name):
+ colonpos = name.find(':')
+ if colonpos <> -1:
+ prefix = name[:colonpos]
+ suffix = name[colonpos+1:]
+ prefix = self.namespacemap.get(prefix, prefix)
+ name = prefix + ':' + suffix
+ return name
+
+ def _getAttribute(self, attrsD, name):
+ return attrsD.get(self._mapToStandardPrefix(name))
+
+ def _isBase64(self, attrsD, contentparams):
+ if attrsD.get('mode', '') == 'base64':
+ return 1
+ if self.contentparams['type'].startswith('text/'):
+ return 0
+ if self.contentparams['type'].endswith('+xml'):
+ return 0
+ if self.contentparams['type'].endswith('/xml'):
+ return 0
+ return 1
+
+ def _itsAnHrefDamnIt(self, attrsD):
+ href = attrsD.get('url', attrsD.get('uri', attrsD.get('href', None)))
+ if href:
+ try:
+ del attrsD['url']
+ except KeyError:
+ pass
+ try:
+ del attrsD['uri']
+ except KeyError:
+ pass
+ attrsD['href'] = href
+ return attrsD
+
+ def _save(self, key, value):
+ context = self._getContext()
+ context.setdefault(key, value)
+
+ def _start_rss(self, attrsD):
+ versionmap = {'0.91': 'rss091u',
+ '0.92': 'rss092',
+ '0.93': 'rss093',
+ '0.94': 'rss094'}
+ if not self.version:
+ attr_version = attrsD.get('version', '')
+ version = versionmap.get(attr_version)
+ if version:
+ self.version = version
+ elif attr_version.startswith('2.'):
+ self.version = 'rss20'
+ else:
+ self.version = 'rss'
+
+ def _start_dlhottitles(self, attrsD):
+ self.version = 'hotrss'
+
+ def _start_channel(self, attrsD):
+ self.infeed = 1
+ self._cdf_common(attrsD)
+ _start_feedinfo = _start_channel
+
+ def _cdf_common(self, attrsD):
+ if attrsD.has_key('lastmod'):
+ self._start_modified({})
+ self.elementstack[-1][-1] = attrsD['lastmod']
+ self._end_modified()
+ if attrsD.has_key('href'):
+ self._start_link({})
+ self.elementstack[-1][-1] = attrsD['href']
+ self._end_link()
+
+ def _start_feed(self, attrsD):
+ self.infeed = 1
+ versionmap = {'0.1': 'atom01',
+ '0.2': 'atom02',
+ '0.3': 'atom03'}
+ if not self.version:
+ attr_version = attrsD.get('version')
+ version = versionmap.get(attr_version)
+ if version:
+ self.version = version
+ else:
+ self.version = 'atom'
+
+ def _end_channel(self):
+ self.infeed = 0
+ _end_feed = _end_channel
+
+ def _start_image(self, attrsD):
+ self.inimage = 1
+ self.push('image', 0)
+ context = self._getContext()
+ context.setdefault('image', FeedParserDict())
+
+ def _end_image(self):
+ self.pop('image')
+ self.inimage = 0
+
+ def _start_textinput(self, attrsD):
+ self.intextinput = 1
+ self.push('textinput', 0)
+ context = self._getContext()
+ context.setdefault('textinput', FeedParserDict())
+ _start_textInput = _start_textinput
+
+ def _end_textinput(self):
+ self.pop('textinput')
+ self.intextinput = 0
+ _end_textInput = _end_textinput
+
+ def _start_author(self, attrsD):
+ self.inauthor = 1
+ self.push('author', 1)
+ _start_managingeditor = _start_author
+ _start_dc_author = _start_author
+ _start_dc_creator = _start_author
+ _start_itunes_author = _start_author
+
+ def _end_author(self):
+ self.pop('author')
+ self.inauthor = 0
+ self._sync_author_detail()
+ _end_managingeditor = _end_author
+ _end_dc_author = _end_author
+ _end_dc_creator = _end_author
+ _end_itunes_author = _end_author
+
+ def _start_itunes_owner(self, attrsD):
+ self.inpublisher = 1
+ self.push('publisher', 0)
+
+ def _end_itunes_owner(self):
+ self.pop('publisher')
+ self.inpublisher = 0
+ self._sync_author_detail('publisher')
+
+ def _start_contributor(self, attrsD):
+ self.incontributor = 1
+ context = self._getContext()
+ context.setdefault('contributors', [])
+ context['contributors'].append(FeedParserDict())
+ self.push('contributor', 0)
+
+ def _end_contributor(self):
+ self.pop('contributor')
+ self.incontributor = 0
+
+ def _start_dc_contributor(self, attrsD):
+ self.incontributor = 1
+ context = self._getContext()
+ context.setdefault('contributors', [])
+ context['contributors'].append(FeedParserDict())
+ self.push('name', 0)
+
+ def _end_dc_contributor(self):
+ self._end_name()
+ self.incontributor = 0
+
+ def _start_name(self, attrsD):
+ self.push('name', 0)
+ _start_itunes_name = _start_name
+
+ def _end_name(self):
+ value = self.pop('name')
+ if self.inpublisher:
+ self._save_author('name', value, 'publisher')
+ elif self.inauthor:
+ self._save_author('name', value)
+ elif self.incontributor:
+ self._save_contributor('name', value)
+ elif self.intextinput:
+ context = self._getContext()
+ context['textinput']['name'] = value
+ _end_itunes_name = _end_name
+
+ def _start_width(self, attrsD):
+ self.push('width', 0)
+
+ def _end_width(self):
+ value = self.pop('width')
+ try:
+ value = int(value)
+ except:
+ value = 0
+ if self.inimage:
+ context = self._getContext()
+ context['image']['width'] = value
+
+ def _start_height(self, attrsD):
+ self.push('height', 0)
+
+ def _end_height(self):
+ value = self.pop('height')
+ try:
+ value = int(value)
+ except:
+ value = 0
+ if self.inimage:
+ context = self._getContext()
+ context['image']['height'] = value
+
+ def _start_url(self, attrsD):
+ self.push('href', 1)
+ _start_homepage = _start_url
+ _start_uri = _start_url
+
+ def _end_url(self):
+ value = self.pop('href')
+ if self.inauthor:
+ self._save_author('href', value)
+ elif self.incontributor:
+ self._save_contributor('href', value)
+ elif self.inimage:
+ context = self._getContext()
+ context['image']['href'] = value
+ elif self.intextinput:
+ context = self._getContext()
+ context['textinput']['link'] = value
+ _end_homepage = _end_url
+ _end_uri = _end_url
+
+ def _start_email(self, attrsD):
+ self.push('email', 0)
+ _start_itunes_email = _start_email
+
+ def _end_email(self):
+ value = self.pop('email')
+ if self.inpublisher:
+ self._save_author('email', value, 'publisher')
+ elif self.inauthor:
+ self._save_author('email', value)
+ elif self.incontributor:
+ self._save_contributor('email', value)
+ _end_itunes_email = _end_email
+
+ def _getContext(self):
+ if self.insource:
+ context = self.sourcedata
+ elif self.inentry:
+ context = self.entries[-1]
+ else:
+ context = self.feeddata
+ return context
+
+ def _save_author(self, key, value, prefix='author'):
+ context = self._getContext()
+ context.setdefault(prefix + '_detail', FeedParserDict())
+ context[prefix + '_detail'][key] = value
+ self._sync_author_detail()
+
+ def _save_contributor(self, key, value):
+ context = self._getContext()
+ context.setdefault('contributors', [FeedParserDict()])
+ context['contributors'][-1][key] = value
+
+ def _sync_author_detail(self, key='author'):
+ context = self._getContext()
+ detail = context.get('%s_detail' % key)
+ if detail:
+ name = detail.get('name')
+ email = detail.get('email')
+ if name and email:
+ context[key] = '%s (%s)' % (name, email)
+ elif name:
+ context[key] = name
+ elif email:
+ context[key] = email
+ else:
+ author = context.get(key)
+ if not author: return
+ emailmatch = re.search(r'''(([a-zA-Z0-9\_\-\.\+]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?))''', author)
+ if not emailmatch: return
+ email = emailmatch.group(0)
+ # probably a better way to do the following, but it passes all the tests
+ author = author.replace(email, '')
+ author = author.replace('()', '')
+ author = author.strip()
+ if author and (author[0] == '('):
+ author = author[1:]
+ if author and (author[-1] == ')'):
+ author = author[:-1]
+ author = author.strip()
+ context.setdefault('%s_detail' % key, FeedParserDict())
+ context['%s_detail' % key]['name'] = author
+ context['%s_detail' % key]['email'] = email
+
+ def _start_subtitle(self, attrsD):
+ self.pushContent('subtitle', attrsD, 'text/plain', 1)
+ _start_tagline = _start_subtitle
+ _start_itunes_subtitle = _start_subtitle
+
+ def _end_subtitle(self):
+ self.popContent('subtitle')
+ _end_tagline = _end_subtitle
+ _end_itunes_subtitle = _end_subtitle
+
+ def _start_rights(self, attrsD):
+ self.pushContent('rights', attrsD, 'text/plain', 1)
+ _start_dc_rights = _start_rights
+ _start_copyright = _start_rights
+
+ def _end_rights(self):
+ self.popContent('rights')
+ _end_dc_rights = _end_rights
+ _end_copyright = _end_rights
+
+ def _start_item(self, attrsD):
+ self.entries.append(FeedParserDict())
+ self.push('item', 0)
+ self.inentry = 1
+ self.guidislink = 0
+ id = self._getAttribute(attrsD, 'rdf:about')
+ if id:
+ context = self._getContext()
+ context['id'] = id
+ self._cdf_common(attrsD)
+ _start_entry = _start_item
+ _start_product = _start_item
+
+ def _end_item(self):
+ self.pop('item')
+ self.inentry = 0
+ _end_entry = _end_item
+
+ def _start_dc_language(self, attrsD):
+ self.push('language', 1)
+ _start_language = _start_dc_language
+
+ def _end_dc_language(self):
+ self.lang = self.pop('language')
+ _end_language = _end_dc_language
+
+ def _start_dc_publisher(self, attrsD):
+ self.push('publisher', 1)
+ _start_webmaster = _start_dc_publisher
+
+ def _end_dc_publisher(self):
+ self.pop('publisher')
+ self._sync_author_detail('publisher')
+ _end_webmaster = _end_dc_publisher
+
+ def _start_published(self, attrsD):
+ self.push('published', 1)
+ _start_dcterms_issued = _start_published
+ _start_issued = _start_published
+
+ def _end_published(self):
+ value = self.pop('published')
+ self._save('published_parsed', _parse_date(value))
+ _end_dcterms_issued = _end_published
+ _end_issued = _end_published
+
+ def _start_updated(self, attrsD):
+ self.push('updated', 1)
+ _start_modified = _start_updated
+ _start_dcterms_modified = _start_updated
+ _start_pubdate = _start_updated
+ _start_dc_date = _start_updated
+
+ def _end_updated(self):
+ value = self.pop('updated')
+ parsed_value = _parse_date(value)
+ self._save('updated_parsed', parsed_value)
+ _end_modified = _end_updated
+ _end_dcterms_modified = _end_updated
+ _end_pubdate = _end_updated
+ _end_dc_date = _end_updated
+
+ def _start_created(self, attrsD):
+ self.push('created', 1)
+ _start_dcterms_created = _start_created
+
+ def _end_created(self):
+ value = self.pop('created')
+ self._save('created_parsed', _parse_date(value))
+ _end_dcterms_created = _end_created
+
+ def _start_expirationdate(self, attrsD):
+ self.push('expired', 1)
+
+ def _end_expirationdate(self):
+ self._save('expired_parsed', _parse_date(self.pop('expired')))
+
+ def _start_cc_license(self, attrsD):
+ self.push('license', 1)
+ value = self._getAttribute(attrsD, 'rdf:resource')
+ if value:
+ self.elementstack[-1][2].append(value)
+ self.pop('license')
+
+ def _start_creativecommons_license(self, attrsD):
+ self.push('license', 1)
+
+ def _end_creativecommons_license(self):
+ self.pop('license')
+
+ def _addTag(self, term, scheme, label):
+ context = self._getContext()
+ tags = context.setdefault('tags', [])
+ if (not term) and (not scheme) and (not label): return
+ value = FeedParserDict({'term': term, 'scheme': scheme, 'label': label})
+ if value not in tags:
+ tags.append(FeedParserDict({'term': term, 'scheme': scheme, 'label': label}))
+
+ def _start_category(self, attrsD):
+ if _debug: sys.stderr.write('entering _start_category with %s\n' % repr(attrsD))
+ term = attrsD.get('term')
+ scheme = attrsD.get('scheme', attrsD.get('domain'))
+ label = attrsD.get('label')
+ self._addTag(term, scheme, label)
+ self.push('category', 1)
+ _start_dc_subject = _start_category
+ _start_keywords = _start_category
+
+ def _end_itunes_keywords(self):
+ for term in self.pop('itunes_keywords').split():
+ self._addTag(term, 'http://www.itunes.com/', None)
+
+ def _start_itunes_category(self, attrsD):
+ self._addTag(attrsD.get('text'), 'http://www.itunes.com/', None)
+ self.push('category', 1)
+
+ def _end_category(self):
+ value = self.pop('category')
+ if not value: return
+ context = self._getContext()
+ tags = context['tags']
+ if value and len(tags) and not tags[-1]['term']:
+ tags[-1]['term'] = value
+ else:
+ self._addTag(value, None, None)
+ _end_dc_subject = _end_category
+ _end_keywords = _end_category
+ _end_itunes_category = _end_category
+
+ def _start_cloud(self, attrsD):
+ self._getContext()['cloud'] = FeedParserDict(attrsD)
+
+ def _start_link(self, attrsD):
+ attrsD.setdefault('rel', 'alternate')
+ attrsD.setdefault('type', 'text/html')
+ attrsD = self._itsAnHrefDamnIt(attrsD)
+ if attrsD.has_key('href'):
+ attrsD['href'] = self.resolveURI(attrsD['href'])
+ expectingText = self.infeed or self.inentry or self.insource
+ context = self._getContext()
+ context.setdefault('links', [])
+ context['links'].append(FeedParserDict(attrsD))
+ if attrsD['rel'] == 'enclosure':
+ self._start_enclosure(attrsD)
+ if attrsD.has_key('href'):
+ expectingText = 0
+ if (attrsD.get('rel') == 'alternate') and (self.mapContentType(attrsD.get('type')) in self.html_types):
+ context['link'] = attrsD['href']
+ else:
+ self.push('link', expectingText)
+ _start_producturl = _start_link
+
+ def _end_link(self):
+ value = self.pop('link')
+ context = self._getContext()
+ if self.intextinput:
+ context['textinput']['link'] = value
+ if self.inimage:
+ context['image']['link'] = value
+ _end_producturl = _end_link
+
+ def _start_guid(self, attrsD):
+ self.guidislink = (attrsD.get('ispermalink', 'true') == 'true')
+ self.push('id', 1)
+
+ def _end_guid(self):
+ value = self.pop('id')
+ self._save('guidislink', self.guidislink and not self._getContext().has_key('link'))
+ if self.guidislink:
+ # guid acts as link, but only if 'ispermalink' is not present or is 'true',
+ # and only if the item doesn't already have a link element
+ self._save('link', value)
+
+ def _start_title(self, attrsD):
+ self.pushContent('title', attrsD, 'text/plain', self.infeed or self.inentry or self.insource)
+ _start_dc_title = _start_title
+ _start_media_title = _start_title
+
+ def _end_title(self):
+ value = self.popContent('title')
+ context = self._getContext()
+ if self.intextinput:
+ context['textinput']['title'] = value
+ elif self.inimage:
+ context['image']['title'] = value
+ _end_dc_title = _end_title
+ _end_media_title = _end_title
+
+ def _start_description(self, attrsD):
+ context = self._getContext()
+ if context.has_key('summary'):
+ self._summaryKey = 'content'
+ self._start_content(attrsD)
+ else:
+ self.pushContent('description', attrsD, 'text/html', self.infeed or self.inentry or self.insource)
+
+ def _start_abstract(self, attrsD):
+ self.pushContent('description', attrsD, 'text/plain', self.infeed or self.inentry or self.insource)
+
+ def _end_description(self):
+ if self._summaryKey == 'content':
+ self._end_content()
+ else:
+ value = self.popContent('description')
+ context = self._getContext()
+ if self.intextinput:
+ context['textinput']['description'] = value
+ elif self.inimage:
+ context['image']['description'] = value
+ self._summaryKey = None
+ _end_abstract = _end_description
+
+ def _start_info(self, attrsD):
+ self.pushContent('info', attrsD, 'text/plain', 1)
+ _start_feedburner_browserfriendly = _start_info
+
+ def _end_info(self):
+ self.popContent('info')
+ _end_feedburner_browserfriendly = _end_info
+
+ def _start_generator(self, attrsD):
+ if attrsD:
+ attrsD = self._itsAnHrefDamnIt(attrsD)
+ if attrsD.has_key('href'):
+ attrsD['href'] = self.resolveURI(attrsD['href'])
+ self._getContext()['generator_detail'] = FeedParserDict(attrsD)
+ self.push('generator', 1)
+
+ def _end_generator(self):
+ value = self.pop('generator')
+ context = self._getContext()
+ if context.has_key('generator_detail'):
+ context['generator_detail']['name'] = value
+
+ def _start_admin_generatoragent(self, attrsD):
+ self.push('generator', 1)
+ value = self._getAttribute(attrsD, 'rdf:resource')
+ if value:
+ self.elementstack[-1][2].append(value)
+ self.pop('generator')
+ self._getContext()['generator_detail'] = FeedParserDict({'href': value})
+
+ def _start_admin_errorreportsto(self, attrsD):
+ self.push('errorreportsto', 1)
+ value = self._getAttribute(attrsD, 'rdf:resource')
+ if value:
+ self.elementstack[-1][2].append(value)
+ self.pop('errorreportsto')
+
+ def _start_summary(self, attrsD):
+ context = self._getContext()
+ if context.has_key('summary'):
+ self._summaryKey = 'content'
+ self._start_content(attrsD)
+ else:
+ self._summaryKey = 'summary'
+ self.pushContent(self._summaryKey, attrsD, 'text/plain', 1)
+ _start_itunes_summary = _start_summary
+
+ def _end_summary(self):
+ if self._summaryKey == 'content':
+ self._end_content()
+ else:
+ self.popContent(self._summaryKey or 'summary')
+ self._summaryKey = None
+ _end_itunes_summary = _end_summary
+
+ def _start_enclosure(self, attrsD):
+ attrsD = self._itsAnHrefDamnIt(attrsD)
+ self._getContext().setdefault('enclosures', []).append(FeedParserDict(attrsD))
+ href = attrsD.get('href')
+ if href:
+ context = self._getContext()
+ if not context.get('id'):
+ context['id'] = href
+
+ def _start_source(self, attrsD):
+ self.insource = 1
+
+ def _end_source(self):
+ self.insource = 0
+ self._getContext()['source'] = copy.deepcopy(self.sourcedata)
+ self.sourcedata.clear()
+
+ def _start_content(self, attrsD):
+ self.pushContent('content', attrsD, 'text/plain', 1)
+ src = attrsD.get('src')
+ if src:
+ self.contentparams['src'] = src
+ self.push('content', 1)
+
+ def _start_prodlink(self, attrsD):
+ self.pushContent('content', attrsD, 'text/html', 1)
+
+ def _start_body(self, attrsD):
+ self.pushContent('content', attrsD, 'application/xhtml+xml', 1)
+ _start_xhtml_body = _start_body
+
+ def _start_content_encoded(self, attrsD):
+ self.pushContent('content', attrsD, 'text/html', 1)
+ _start_fullitem = _start_content_encoded
+
+ def _end_content(self):
+ copyToDescription = self.mapContentType(self.contentparams.get('type')) in (['text/plain'] + self.html_types)
+ value = self.popContent('content')
+ if copyToDescription:
+ self._save('description', value)
+ _end_body = _end_content
+ _end_xhtml_body = _end_content
+ _end_content_encoded = _end_content
+ _end_fullitem = _end_content
+ _end_prodlink = _end_content
+
+ def _start_itunes_image(self, attrsD):
+ self.push('itunes_image', 0)
+ self._getContext()['image'] = FeedParserDict({'href': attrsD.get('href')})
+ _start_itunes_link = _start_itunes_image
+
+ def _end_itunes_block(self):
+ value = self.pop('itunes_block', 0)
+ self._getContext()['itunes_block'] = (value == 'yes') and 1 or 0
+
+ def _end_itunes_explicit(self):
+ value = self.pop('itunes_explicit', 0)
+ self._getContext()['itunes_explicit'] = (value == 'yes') and 1 or 0
+
+if _XML_AVAILABLE:
+ class _StrictFeedParser(_FeedParserMixin, xml.sax.handler.ContentHandler):
+ def __init__(self, baseuri, baselang, encoding):
+ if _debug: sys.stderr.write('trying StrictFeedParser\n')
+ xml.sax.handler.ContentHandler.__init__(self)
+ _FeedParserMixin.__init__(self, baseuri, baselang, encoding)
+ self.bozo = 0
+ self.exc = None
+
+ def startPrefixMapping(self, prefix, uri):
+ self.trackNamespace(prefix, uri)
+
+ def startElementNS(self, name, qname, attrs):
+ namespace, localname = name
+ lowernamespace = str(namespace or '').lower()
+ if lowernamespace.find('backend.userland.com/rss') <> -1:
+ # match any backend.userland.com namespace
+ namespace = 'http://backend.userland.com/rss'
+ lowernamespace = namespace
+ if qname and qname.find(':') > 0:
+ givenprefix = qname.split(':')[0]
+ else:
+ givenprefix = None
+ prefix = self._matchnamespaces.get(lowernamespace, givenprefix)
+ if givenprefix and (prefix == None or (prefix == '' and lowernamespace == '')) and not self.namespacesInUse.has_key(givenprefix):
+ raise UndeclaredNamespace, "'%s' is not associated with a namespace" % givenprefix
+ if prefix:
+ localname = prefix + ':' + localname
+ localname = str(localname).lower()
+ if _debug: sys.stderr.write('startElementNS: qname = %s, namespace = %s, givenprefix = %s, prefix = %s, attrs = %s, localname = %s\n' % (qname, namespace, givenprefix, prefix, attrs.items(), localname))
+
+ # qname implementation is horribly broken in Python 2.1 (it
+ # doesn't report any), and slightly broken in Python 2.2 (it
+ # doesn't report the xml: namespace). So we match up namespaces
+ # with a known list first, and then possibly override them with
+ # the qnames the SAX parser gives us (if indeed it gives us any
+ # at all). Thanks to MatejC for helping me test this and
+ # tirelessly telling me that it didn't work yet.
+ attrsD = {}
+ for (namespace, attrlocalname), attrvalue in attrs._attrs.items():
+ lowernamespace = (namespace or '').lower()
+ prefix = self._matchnamespaces.get(lowernamespace, '')
+ if prefix:
+ attrlocalname = prefix + ':' + attrlocalname
+ attrsD[str(attrlocalname).lower()] = attrvalue
+ for qname in attrs.getQNames():
+ attrsD[str(qname).lower()] = attrs.getValueByQName(qname)
+ self.unknown_starttag(localname, attrsD.items())
+
+ def characters(self, text):
+ self.handle_data(text)
+
+ def endElementNS(self, name, qname):
+ namespace, localname = name
+ lowernamespace = str(namespace or '').lower()
+ if qname and qname.find(':') > 0:
+ givenprefix = qname.split(':')[0]
+ else:
+ givenprefix = ''
+ prefix = self._matchnamespaces.get(lowernamespace, givenprefix)
+ if prefix:
+ localname = prefix + ':' + localname
+ localname = str(localname).lower()
+ self.unknown_endtag(localname)
+
+ def error(self, exc):
+ self.bozo = 1
+ self.exc = exc
+
+ def fatalError(self, exc):
+ self.error(exc)
+ raise exc
+
+class _BaseHTMLProcessor(sgmllib.SGMLParser):
+ elements_no_end_tag = ['area', 'base', 'basefont', 'br', 'col', 'frame', 'hr',
+ 'img', 'input', 'isindex', 'link', 'meta', 'param']
+
+ def __init__(self, encoding):
+ self.encoding = encoding
+ if _debug: sys.stderr.write('entering BaseHTMLProcessor, encoding=%s\n' % self.encoding)
+ sgmllib.SGMLParser.__init__(self)
+
+ def reset(self):
+ self.pieces = []
+ sgmllib.SGMLParser.reset(self)
+
+ def _shorttag_replace(self, match):
+ tag = match.group(1)
+ if tag in self.elements_no_end_tag:
+ return '<' + tag + ' />'
+ else:
+ return '<' + tag + '>' + tag + '>'
+
+ def feed(self, data):
+ data = re.compile(r'', self._shorttag_replace, data) # bug [ 1399464 ] Bad regexp for _shorttag_replace
+ data = re.sub(r'<([^<\s]+?)\s*/>', self._shorttag_replace, data)
+ data = data.replace(''', "'")
+ data = data.replace('"', '"')
+ if self.encoding and type(data) == type(u''):
+ data = data.encode(self.encoding)
+ sgmllib.SGMLParser.feed(self, data)
+
+ def normalize_attrs(self, attrs):
+ # utility method to be called by descendants
+ attrs = [(k.lower(), v) for k, v in attrs]
+ attrs = [(k, k in ('rel', 'type') and v.lower() or v) for k, v in attrs]
+ return attrs
+
+ def unknown_starttag(self, tag, attrs):
+ # called for each start tag
+ # attrs is a list of (attr, value) tuples
+ # e.g. for , tag='pre', attrs=[('class', 'screen')]
+ if _debug: sys.stderr.write('_BaseHTMLProcessor, unknown_starttag, tag=%s\n' % tag)
+ uattrs = []
+ # thanks to Kevin Marks for this breathtaking hack to deal with (valid) high-bit attribute values in UTF-8 feeds
+ for key, value in attrs:
+ if type(value) != type(u''):
+ value = unicode(value, self.encoding)
+ uattrs.append((unicode(key, self.encoding), value))
+ strattrs = u''.join([u' %s="%s"' % (key, value) for key, value in uattrs]).encode(self.encoding)
+ if tag in self.elements_no_end_tag:
+ self.pieces.append('<%(tag)s%(strattrs)s />' % locals())
+ else:
+ self.pieces.append('<%(tag)s%(strattrs)s>' % locals())
+
+ def unknown_endtag(self, tag):
+ # called for each end tag, e.g. for
, tag will be 'pre'
+ # Reconstruct the original end tag.
+ if tag not in self.elements_no_end_tag:
+ self.pieces.append("%(tag)s>" % locals())
+
+ def handle_charref(self, ref):
+ # called for each character reference, e.g. for ' ', ref will be '160'
+ # Reconstruct the original character reference.
+ self.pieces.append('%(ref)s;' % locals())
+
+ def handle_entityref(self, ref):
+ # called for each entity reference, e.g. for '©', ref will be 'copy'
+ # Reconstruct the original entity reference.
+ self.pieces.append('&%(ref)s;' % locals())
+
+ def handle_data(self, text):
+ # called for each block of plain text, i.e. outside of any tag and
+ # not containing any character or entity references
+ # Store the original text verbatim.
+ if _debug: sys.stderr.write('_BaseHTMLProcessor, handle_text, text=%s\n' % text)
+ self.pieces.append(text)
+
+ def handle_comment(self, text):
+ # called for each HTML comment, e.g.
+ # Reconstruct the original comment.
+ self.pieces.append('' % locals())
+
+ def handle_pi(self, text):
+ # called for each processing instruction, e.g.
+ # Reconstruct original processing instruction.
+ self.pieces.append('%(text)s>' % locals())
+
+ def handle_decl(self, text):
+ # called for the DOCTYPE, if present, e.g.
+ #
+ # Reconstruct original DOCTYPE
+ self.pieces.append('' % locals())
+
+ _new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*').match
+ def _scan_name(self, i, declstartpos):
+ rawdata = self.rawdata
+ n = len(rawdata)
+ if i == n:
+ return None, -1
+ m = self._new_declname_match(rawdata, i)
+ if m:
+ s = m.group()
+ name = s.strip()
+ if (i + len(s)) == n:
+ return None, -1 # end of buffer
+ return name.lower(), m.end()
+ else:
+ self.handle_data(rawdata)
+# self.updatepos(declstartpos, i)
+ return None, -1
+
+ def output(self):
+ '''Return processed HTML as a single string'''
+ return ''.join([str(p) for p in self.pieces])
+
+class _LooseFeedParser(_FeedParserMixin, _BaseHTMLProcessor):
+ def __init__(self, baseuri, baselang, encoding):
+ sgmllib.SGMLParser.__init__(self)
+ _FeedParserMixin.__init__(self, baseuri, baselang, encoding)
+
+ def decodeEntities(self, element, data):
+ data = data.replace('<', '<')
+ data = data.replace('<', '<')
+ data = data.replace('>', '>')
+ data = data.replace('>', '>')
+ data = data.replace('&', '&')
+ data = data.replace('&', '&')
+ data = data.replace('"', '"')
+ data = data.replace('"', '"')
+ data = data.replace(''', ''')
+ data = data.replace(''', ''')
+ if self.contentparams.has_key('type') and not self.contentparams.get('type', 'xml').endswith('xml'):
+ data = data.replace('<', '<')
+ data = data.replace('>', '>')
+ data = data.replace('&', '&')
+ data = data.replace('"', '"')
+ data = data.replace(''', "'")
+ return data
+
+class _RelativeURIResolver(_BaseHTMLProcessor):
+ relative_uris = [('a', 'href'),
+ ('applet', 'codebase'),
+ ('area', 'href'),
+ ('blockquote', 'cite'),
+ ('body', 'background'),
+ ('del', 'cite'),
+ ('form', 'action'),
+ ('frame', 'longdesc'),
+ ('frame', 'src'),
+ ('iframe', 'longdesc'),
+ ('iframe', 'src'),
+ ('head', 'profile'),
+ ('img', 'longdesc'),
+ ('img', 'src'),
+ ('img', 'usemap'),
+ ('input', 'src'),
+ ('input', 'usemap'),
+ ('ins', 'cite'),
+ ('link', 'href'),
+ ('object', 'classid'),
+ ('object', 'codebase'),
+ ('object', 'data'),
+ ('object', 'usemap'),
+ ('q', 'cite'),
+ ('script', 'src')]
+
+ def __init__(self, baseuri, encoding):
+ _BaseHTMLProcessor.__init__(self, encoding)
+ self.baseuri = baseuri
+
+ def resolveURI(self, uri):
+ return _urljoin(self.baseuri, uri)
+
+ def unknown_starttag(self, tag, attrs):
+ attrs = self.normalize_attrs(attrs)
+ attrs = [(key, ((tag, key) in self.relative_uris) and self.resolveURI(value) or value) for key, value in attrs]
+ _BaseHTMLProcessor.unknown_starttag(self, tag, attrs)
+
+def _resolveRelativeURIs(htmlSource, baseURI, encoding):
+ if _debug: sys.stderr.write('entering _resolveRelativeURIs\n')
+ p = _RelativeURIResolver(baseURI, encoding)
+ p.feed(htmlSource)
+ return p.output()
+
+class _HTMLSanitizer(_BaseHTMLProcessor):
+ acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', 'b', 'big',
+ 'blockquote', 'br', 'button', 'caption', 'center', 'cite', 'code', 'col',
+ 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', 'em', 'fieldset',
+ 'font', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input',
+ 'ins', 'kbd', 'label', 'legend', 'li', 'map', 'menu', 'ol', 'optgroup',
+ 'option', 'p', 'pre', 'q', 's', 'samp', 'select', 'small', 'span', 'strike',
+ 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th',
+ 'thead', 'tr', 'tt', 'u', 'ul', 'var']
+
+ acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey',
+ 'action', 'align', 'alt', 'axis', 'border', 'cellpadding', 'cellspacing',
+ 'char', 'charoff', 'charset', 'checked', 'cite', 'class', 'clear', 'cols',
+ 'colspan', 'color', 'compact', 'coords', 'datetime', 'dir', 'disabled',
+ 'enctype', 'for', 'frame', 'headers', 'height', 'href', 'hreflang', 'hspace',
+ 'id', 'ismap', 'label', 'lang', 'longdesc', 'maxlength', 'media', 'method',
+ 'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'prompt', 'readonly',
+ 'rel', 'rev', 'rows', 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size',
+ 'span', 'src', 'start', 'summary', 'tabindex', 'target', 'title', 'type',
+ 'usemap', 'valign', 'value', 'vspace', 'width']
+
+ unacceptable_elements_with_end_tag = ['script', 'applet']
+
+ def reset(self):
+ _BaseHTMLProcessor.reset(self)
+ self.unacceptablestack = 0
+
+ def unknown_starttag(self, tag, attrs):
+ if not tag in self.acceptable_elements:
+ if tag in self.unacceptable_elements_with_end_tag:
+ self.unacceptablestack += 1
+ return
+ attrs = self.normalize_attrs(attrs)
+ attrs = [(key, value) for key, value in attrs if key in self.acceptable_attributes]
+ _BaseHTMLProcessor.unknown_starttag(self, tag, attrs)
+
+ def unknown_endtag(self, tag):
+ if not tag in self.acceptable_elements:
+ if tag in self.unacceptable_elements_with_end_tag:
+ self.unacceptablestack -= 1
+ return
+ _BaseHTMLProcessor.unknown_endtag(self, tag)
+
+ def handle_pi(self, text):
+ pass
+
+ def handle_decl(self, text):
+ pass
+
+ def handle_data(self, text):
+ if not self.unacceptablestack:
+ _BaseHTMLProcessor.handle_data(self, text)
+
+def _sanitizeHTML(htmlSource, encoding):
+ p = _HTMLSanitizer(encoding)
+ p.feed(htmlSource)
+ data = p.output()
+ if TIDY_MARKUP:
+ # loop through list of preferred Tidy interfaces looking for one that's installed,
+ # then set up a common _tidy function to wrap the interface-specific API.
+ _tidy = None
+ for tidy_interface in PREFERRED_TIDY_INTERFACES:
+ try:
+ if tidy_interface == "uTidy":
+ from tidy import parseString as _utidy
+ def _tidy(data, **kwargs):
+ return str(_utidy(data, **kwargs))
+ break
+ elif tidy_interface == "mxTidy":
+ from mx.Tidy import Tidy as _mxtidy
+ def _tidy(data, **kwargs):
+ nerrors, nwarnings, data, errordata = _mxtidy.tidy(data, **kwargs)
+ return data
+ break
+ except:
+ pass
+ if _tidy:
+ utf8 = type(data) == type(u'')
+ if utf8:
+ data = data.encode('utf-8')
+ data = _tidy(data, output_xhtml=1, numeric_entities=1, wrap=0, char_encoding="utf8")
+ if utf8:
+ data = unicode(data, 'utf-8')
+ if data.count(''):
+ data = data.split('>', 1)[1]
+ if data.count('= '2.3.3'
+ assert base64 != None
+ user, passw = base64.decodestring(req.headers['Authorization'].split(' ')[1]).split(':')
+ realm = re.findall('realm="([^"]*)"', headers['WWW-Authenticate'])[0]
+ self.add_password(realm, host, user, passw)
+ retry = self.http_error_auth_reqed('www-authenticate', host, req, headers)
+ self.reset_retry_count()
+ return retry
+ except:
+ return self.http_error_default(req, fp, code, msg, headers)
+
+def _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, handlers):
+ """URL, filename, or string --> stream
+
+ This function lets you define parsers that take any input source
+ (URL, pathname to local or network file, or actual data as a string)
+ and deal with it in a uniform manner. Returned object is guaranteed
+ to have all the basic stdio read methods (read, readline, readlines).
+ Just .close() the object when you're done with it.
+
+ If the etag argument is supplied, it will be used as the value of an
+ If-None-Match request header.
+
+ If the modified argument is supplied, it must be a tuple of 9 integers
+ as returned by gmtime() in the standard Python time module. This MUST
+ be in GMT (Greenwich Mean Time). The formatted date/time will be used
+ as the value of an If-Modified-Since request header.
+
+ If the agent argument is supplied, it will be used as the value of a
+ User-Agent request header.
+
+ If the referrer argument is supplied, it will be used as the value of a
+ Referer[sic] request header.
+
+ If handlers is supplied, it is a list of handlers used to build a
+ urllib2 opener.
+ """
+
+ if hasattr(url_file_stream_or_string, 'read'):
+ return url_file_stream_or_string
+
+ if url_file_stream_or_string == '-':
+ return sys.stdin
+
+ if urlparse.urlparse(url_file_stream_or_string)[0] in ('http', 'https', 'ftp'):
+ if not agent:
+ agent = USER_AGENT
+ # test for inline user:password for basic auth
+ auth = None
+ if base64:
+ urltype, rest = urllib.splittype(url_file_stream_or_string)
+ realhost, rest = urllib.splithost(rest)
+ if realhost:
+ user_passwd, realhost = urllib.splituser(realhost)
+ if user_passwd:
+ url_file_stream_or_string = '%s://%s%s' % (urltype, realhost, rest)
+ auth = base64.encodestring(user_passwd).strip()
+ # try to open with urllib2 (to use optional headers)
+ request = urllib2.Request(url_file_stream_or_string)
+ request.add_header('User-Agent', agent)
+ if etag:
+ request.add_header('If-None-Match', etag)
+ if modified:
+ # format into an RFC 1123-compliant timestamp. We can't use
+ # time.strftime() since the %a and %b directives can be affected
+ # by the current locale, but RFC 2616 states that dates must be
+ # in English.
+ short_weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+ months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+ request.add_header('If-Modified-Since', '%s, %02d %s %04d %02d:%02d:%02d GMT' % (short_weekdays[modified[6]], modified[2], months[modified[1] - 1], modified[0], modified[3], modified[4], modified[5]))
+ if referrer:
+ request.add_header('Referer', referrer)
+ if gzip and zlib:
+ request.add_header('Accept-encoding', 'gzip, deflate')
+ elif gzip:
+ request.add_header('Accept-encoding', 'gzip')
+ elif zlib:
+ request.add_header('Accept-encoding', 'deflate')
+ else:
+ request.add_header('Accept-encoding', '')
+ if auth:
+ request.add_header('Authorization', 'Basic %s' % auth)
+ if ACCEPT_HEADER:
+ request.add_header('Accept', ACCEPT_HEADER)
+ request.add_header('A-IM', 'feed') # RFC 3229 support
+ opener = apply(urllib2.build_opener, tuple([_FeedURLHandler()] + handlers))
+ opener.addheaders = [] # RMK - must clear so we only send our custom User-Agent
+ try:
+ return opener.open(request)
+ finally:
+ opener.close() # JohnD
+
+ # try to open with native open function (if url_file_stream_or_string is a filename)
+ try:
+ return open(url_file_stream_or_string)
+ except:
+ pass
+
+ # treat url_file_stream_or_string as string
+ return _StringIO(str(url_file_stream_or_string))
+
+_date_handlers = []
+def registerDateHandler(func):
+ '''Register a date handler function (takes string, returns 9-tuple date in GMT)'''
+ _date_handlers.insert(0, func)
+
+# ISO-8601 date parsing routines written by Fazal Majid.
+# The ISO 8601 standard is very convoluted and irregular - a full ISO 8601
+# parser is beyond the scope of feedparser and would be a worthwhile addition
+# to the Python library.
+# A single regular expression cannot parse ISO 8601 date formats into groups
+# as the standard is highly irregular (for instance is 030104 2003-01-04 or
+# 0301-04-01), so we use templates instead.
+# Please note the order in templates is significant because we need a
+# greedy match.
+_iso8601_tmpl = ['YYYY-?MM-?DD', 'YYYY-MM', 'YYYY-?OOO',
+ 'YY-?MM-?DD', 'YY-?OOO', 'YYYY',
+ '-YY-?MM', '-OOO', '-YY',
+ '--MM-?DD', '--MM',
+ '---DD',
+ 'CC', '']
+_iso8601_re = [
+ tmpl.replace(
+ 'YYYY', r'(?P\d{4})').replace(
+ 'YY', r'(?P\d\d)').replace(
+ 'MM', r'(?P[01]\d)').replace(
+ 'DD', r'(?P[0123]\d)').replace(
+ 'OOO', r'(?P[0123]\d\d)').replace(
+ 'CC', r'(?P\d\d$)')
+ + r'(T?(?P\d{2}):(?P\d{2})'
+ + r'(:(?P\d{2}))?'
+ + r'(?P[+-](?P\d{2})(:(?P\d{2}))?|Z)?)?'
+ for tmpl in _iso8601_tmpl]
+del tmpl
+_iso8601_matches = [re.compile(regex).match for regex in _iso8601_re]
+del regex
+def _parse_date_iso8601(dateString):
+ '''Parse a variety of ISO-8601-compatible formats like 20040105'''
+ m = None
+ for _iso8601_match in _iso8601_matches:
+ m = _iso8601_match(dateString)
+ if m: break
+ if not m: return
+ if m.span() == (0, 0): return
+ params = m.groupdict()
+ ordinal = params.get('ordinal', 0)
+ if ordinal:
+ ordinal = int(ordinal)
+ else:
+ ordinal = 0
+ year = params.get('year', '--')
+ if not year or year == '--':
+ year = time.gmtime()[0]
+ elif len(year) == 2:
+ # ISO 8601 assumes current century, i.e. 93 -> 2093, NOT 1993
+ year = 100 * int(time.gmtime()[0] / 100) + int(year)
+ else:
+ year = int(year)
+ month = params.get('month', '-')
+ if not month or month == '-':
+ # ordinals are NOT normalized by mktime, we simulate them
+ # by setting month=1, day=ordinal
+ if ordinal:
+ month = 1
+ else:
+ month = time.gmtime()[1]
+ month = int(month)
+ day = params.get('day', 0)
+ if not day:
+ # see above
+ if ordinal:
+ day = ordinal
+ elif params.get('century', 0) or \
+ params.get('year', 0) or params.get('month', 0):
+ day = 1
+ else:
+ day = time.gmtime()[2]
+ else:
+ day = int(day)
+ # special case of the century - is the first year of the 21st century
+ # 2000 or 2001 ? The debate goes on...
+ if 'century' in params.keys():
+ year = (int(params['century']) - 1) * 100 + 1
+ # in ISO 8601 most fields are optional
+ for field in ['hour', 'minute', 'second', 'tzhour', 'tzmin']:
+ if not params.get(field, None):
+ params[field] = 0
+ hour = int(params.get('hour', 0))
+ minute = int(params.get('minute', 0))
+ second = int(params.get('second', 0))
+ # weekday is normalized by mktime(), we can ignore it
+ weekday = 0
+ # daylight savings is complex, but not needed for feedparser's purposes
+ # as time zones, if specified, include mention of whether it is active
+ # (e.g. PST vs. PDT, CET). Using -1 is implementation-dependent and
+ # and most implementations have DST bugs
+ daylight_savings_flag = 0
+ tm = [year, month, day, hour, minute, second, weekday,
+ ordinal, daylight_savings_flag]
+ # ISO 8601 time zone adjustments
+ tz = params.get('tz')
+ if tz and tz != 'Z':
+ if tz[0] == '-':
+ tm[3] += int(params.get('tzhour', 0))
+ tm[4] += int(params.get('tzmin', 0))
+ elif tz[0] == '+':
+ tm[3] -= int(params.get('tzhour', 0))
+ tm[4] -= int(params.get('tzmin', 0))
+ else:
+ return None
+ # Python's time.mktime() is a wrapper around the ANSI C mktime(3c)
+ # which is guaranteed to normalize d/m/y/h/m/s.
+ # Many implementations have bugs, but we'll pretend they don't.
+ return time.localtime(time.mktime(tm))
+registerDateHandler(_parse_date_iso8601)
+
+# 8-bit date handling routines written by ytrewq1.
+_korean_year = u'\ub144' # b3e2 in euc-kr
+_korean_month = u'\uc6d4' # bff9 in euc-kr
+_korean_day = u'\uc77c' # c0cf in euc-kr
+_korean_am = u'\uc624\uc804' # bfc0 c0fc in euc-kr
+_korean_pm = u'\uc624\ud6c4' # bfc0 c8c4 in euc-kr
+
+_korean_onblog_date_re = \
+ re.compile('(\d{4})%s\s+(\d{2})%s\s+(\d{2})%s\s+(\d{2}):(\d{2}):(\d{2})' % \
+ (_korean_year, _korean_month, _korean_day))
+_korean_nate_date_re = \
+ re.compile(u'(\d{4})-(\d{2})-(\d{2})\s+(%s|%s)\s+(\d{,2}):(\d{,2}):(\d{,2})' % \
+ (_korean_am, _korean_pm))
+def _parse_date_onblog(dateString):
+ '''Parse a string according to the OnBlog 8-bit date format'''
+ m = _korean_onblog_date_re.match(dateString)
+ if not m: return
+ w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s' % \
+ {'year': m.group(1), 'month': m.group(2), 'day': m.group(3),\
+ 'hour': m.group(4), 'minute': m.group(5), 'second': m.group(6),\
+ 'zonediff': '+09:00'}
+ if _debug: sys.stderr.write('OnBlog date parsed as: %s\n' % w3dtfdate)
+ return _parse_date_w3dtf(w3dtfdate)
+registerDateHandler(_parse_date_onblog)
+
+def _parse_date_nate(dateString):
+ '''Parse a string according to the Nate 8-bit date format'''
+ m = _korean_nate_date_re.match(dateString)
+ if not m: return
+ hour = int(m.group(5))
+ ampm = m.group(4)
+ if (ampm == _korean_pm):
+ hour += 12
+ hour = str(hour)
+ if len(hour) == 1:
+ hour = '0' + hour
+ w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s' % \
+ {'year': m.group(1), 'month': m.group(2), 'day': m.group(3),\
+ 'hour': hour, 'minute': m.group(6), 'second': m.group(7),\
+ 'zonediff': '+09:00'}
+ if _debug: sys.stderr.write('Nate date parsed as: %s\n' % w3dtfdate)
+ return _parse_date_w3dtf(w3dtfdate)
+registerDateHandler(_parse_date_nate)
+
+_mssql_date_re = \
+ re.compile('(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})(\.\d+)?')
+def _parse_date_mssql(dateString):
+ '''Parse a string according to the MS SQL date format'''
+ m = _mssql_date_re.match(dateString)
+ if not m: return
+ w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s' % \
+ {'year': m.group(1), 'month': m.group(2), 'day': m.group(3),\
+ 'hour': m.group(4), 'minute': m.group(5), 'second': m.group(6),\
+ 'zonediff': '+09:00'}
+ if _debug: sys.stderr.write('MS SQL date parsed as: %s\n' % w3dtfdate)
+ return _parse_date_w3dtf(w3dtfdate)
+registerDateHandler(_parse_date_mssql)
+
+# Unicode strings for Greek date strings
+_greek_months = \
+ { \
+ u'\u0399\u03b1\u03bd': u'Jan', # c9e1ed in iso-8859-7
+ u'\u03a6\u03b5\u03b2': u'Feb', # d6e5e2 in iso-8859-7
+ u'\u039c\u03ac\u03ce': u'Mar', # ccdcfe in iso-8859-7
+ u'\u039c\u03b1\u03ce': u'Mar', # cce1fe in iso-8859-7
+ u'\u0391\u03c0\u03c1': u'Apr', # c1f0f1 in iso-8859-7
+ u'\u039c\u03ac\u03b9': u'May', # ccdce9 in iso-8859-7
+ u'\u039c\u03b1\u03ca': u'May', # cce1fa in iso-8859-7
+ u'\u039c\u03b1\u03b9': u'May', # cce1e9 in iso-8859-7
+ u'\u0399\u03bf\u03cd\u03bd': u'Jun', # c9effded in iso-8859-7
+ u'\u0399\u03bf\u03bd': u'Jun', # c9efed in iso-8859-7
+ u'\u0399\u03bf\u03cd\u03bb': u'Jul', # c9effdeb in iso-8859-7
+ u'\u0399\u03bf\u03bb': u'Jul', # c9f9eb in iso-8859-7
+ u'\u0391\u03cd\u03b3': u'Aug', # c1fde3 in iso-8859-7
+ u'\u0391\u03c5\u03b3': u'Aug', # c1f5e3 in iso-8859-7
+ u'\u03a3\u03b5\u03c0': u'Sep', # d3e5f0 in iso-8859-7
+ u'\u039f\u03ba\u03c4': u'Oct', # cfeaf4 in iso-8859-7
+ u'\u039d\u03bf\u03ad': u'Nov', # cdefdd in iso-8859-7
+ u'\u039d\u03bf\u03b5': u'Nov', # cdefe5 in iso-8859-7
+ u'\u0394\u03b5\u03ba': u'Dec', # c4e5ea in iso-8859-7
+ }
+
+_greek_wdays = \
+ { \
+ u'\u039a\u03c5\u03c1': u'Sun', # caf5f1 in iso-8859-7
+ u'\u0394\u03b5\u03c5': u'Mon', # c4e5f5 in iso-8859-7
+ u'\u03a4\u03c1\u03b9': u'Tue', # d4f1e9 in iso-8859-7
+ u'\u03a4\u03b5\u03c4': u'Wed', # d4e5f4 in iso-8859-7
+ u'\u03a0\u03b5\u03bc': u'Thu', # d0e5ec in iso-8859-7
+ u'\u03a0\u03b1\u03c1': u'Fri', # d0e1f1 in iso-8859-7
+ u'\u03a3\u03b1\u03b2': u'Sat', # d3e1e2 in iso-8859-7
+ }
+
+_greek_date_format_re = \
+ re.compile(u'([^,]+),\s+(\d{2})\s+([^\s]+)\s+(\d{4})\s+(\d{2}):(\d{2}):(\d{2})\s+([^\s]+)')
+
+def _parse_date_greek(dateString):
+ '''Parse a string according to a Greek 8-bit date format.'''
+ m = _greek_date_format_re.match(dateString)
+ if not m: return
+ try:
+ wday = _greek_wdays[m.group(1)]
+ month = _greek_months[m.group(3)]
+ except:
+ return
+ rfc822date = '%(wday)s, %(day)s %(month)s %(year)s %(hour)s:%(minute)s:%(second)s %(zonediff)s' % \
+ {'wday': wday, 'day': m.group(2), 'month': month, 'year': m.group(4),\
+ 'hour': m.group(5), 'minute': m.group(6), 'second': m.group(7),\
+ 'zonediff': m.group(8)}
+ if _debug: sys.stderr.write('Greek date parsed as: %s\n' % rfc822date)
+ return _parse_date_rfc822(rfc822date)
+registerDateHandler(_parse_date_greek)
+
+# Unicode strings for Hungarian date strings
+_hungarian_months = \
+ { \
+ u'janu\u00e1r': u'01', # e1 in iso-8859-2
+ u'febru\u00e1ri': u'02', # e1 in iso-8859-2
+ u'm\u00e1rcius': u'03', # e1 in iso-8859-2
+ u'\u00e1prilis': u'04', # e1 in iso-8859-2
+ u'm\u00e1ujus': u'05', # e1 in iso-8859-2
+ u'j\u00fanius': u'06', # fa in iso-8859-2
+ u'j\u00falius': u'07', # fa in iso-8859-2
+ u'augusztus': u'08',
+ u'szeptember': u'09',
+ u'okt\u00f3ber': u'10', # f3 in iso-8859-2
+ u'november': u'11',
+ u'december': u'12',
+ }
+
+_hungarian_date_format_re = \
+ re.compile(u'(\d{4})-([^-]+)-(\d{,2})T(\d{,2}):(\d{2})((\+|-)(\d{,2}:\d{2}))')
+
+def _parse_date_hungarian(dateString):
+ '''Parse a string according to a Hungarian 8-bit date format.'''
+ m = _hungarian_date_format_re.match(dateString)
+ if not m: return
+ try:
+ month = _hungarian_months[m.group(2)]
+ day = m.group(3)
+ if len(day) == 1:
+ day = '0' + day
+ hour = m.group(4)
+ if len(hour) == 1:
+ hour = '0' + hour
+ except:
+ return
+ w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s%(zonediff)s' % \
+ {'year': m.group(1), 'month': month, 'day': day,\
+ 'hour': hour, 'minute': m.group(5),\
+ 'zonediff': m.group(6)}
+ if _debug: sys.stderr.write('Hungarian date parsed as: %s\n' % w3dtfdate)
+ return _parse_date_w3dtf(w3dtfdate)
+registerDateHandler(_parse_date_hungarian)
+
+# W3DTF-style date parsing adapted from PyXML xml.utils.iso8601, written by
+# Drake and licensed under the Python license. Removed all range checking
+# for month, day, hour, minute, and second, since mktime will normalize
+# these later
+def _parse_date_w3dtf(dateString):
+ def __extract_date(m):
+ year = int(m.group('year'))
+ if year < 100:
+ year = 100 * int(time.gmtime()[0] / 100) + int(year)
+ if year < 1000:
+ return 0, 0, 0
+ julian = m.group('julian')
+ if julian:
+ julian = int(julian)
+ month = julian / 30 + 1
+ day = julian % 30 + 1
+ jday = None
+ while jday != julian:
+ t = time.mktime((year, month, day, 0, 0, 0, 0, 0, 0))
+ jday = time.gmtime(t)[-2]
+ diff = abs(jday - julian)
+ if jday > julian:
+ if diff < day:
+ day = day - diff
+ else:
+ month = month - 1
+ day = 31
+ elif jday < julian:
+ if day + diff < 28:
+ day = day + diff
+ else:
+ month = month + 1
+ return year, month, day
+ month = m.group('month')
+ day = 1
+ if month is None:
+ month = 1
+ else:
+ month = int(month)
+ day = m.group('day')
+ if day:
+ day = int(day)
+ else:
+ day = 1
+ return year, month, day
+
+ def __extract_time(m):
+ if not m:
+ return 0, 0, 0
+ hours = m.group('hours')
+ if not hours:
+ return 0, 0, 0
+ hours = int(hours)
+ minutes = int(m.group('minutes'))
+ seconds = m.group('seconds')
+ if seconds:
+ seconds = int(seconds)
+ else:
+ seconds = 0
+ return hours, minutes, seconds
+
+ def __extract_tzd(m):
+ '''Return the Time Zone Designator as an offset in seconds from UTC.'''
+ if not m:
+ return 0
+ tzd = m.group('tzd')
+ if not tzd:
+ return 0
+ if tzd == 'Z':
+ return 0
+ hours = int(m.group('tzdhours'))
+ minutes = m.group('tzdminutes')
+ if minutes:
+ minutes = int(minutes)
+ else:
+ minutes = 0
+ offset = (hours*60 + minutes) * 60
+ if tzd[0] == '+':
+ return -offset
+ return offset
+
+ __date_re = ('(?P\d\d\d\d)'
+ '(?:(?P-|)'
+ '(?:(?P\d\d\d)'
+ '|(?P\d\d)(?:(?P=dsep)(?P\d\d))?))?')
+ __tzd_re = '(?P[-+](?P\d\d)(?::?(?P\d\d))|Z)'
+ __tzd_rx = re.compile(__tzd_re)
+ __time_re = ('(?P\d\d)(?P:|)(?P\d\d)'
+ '(?:(?P=tsep)(?P\d\d(?:[.,]\d+)?))?'
+ + __tzd_re)
+ __datetime_re = '%s(?:T%s)?' % (__date_re, __time_re)
+ __datetime_rx = re.compile(__datetime_re)
+ m = __datetime_rx.match(dateString)
+ if (m is None) or (m.group() != dateString): return
+ gmt = __extract_date(m) + __extract_time(m) + (0, 0, 0)
+ if gmt[0] == 0: return
+ return time.gmtime(time.mktime(gmt) + __extract_tzd(m) - time.timezone)
+registerDateHandler(_parse_date_w3dtf)
+
+def _parse_date_rfc822(dateString):
+ '''Parse an RFC822, RFC1123, RFC2822, or asctime-style date'''
+ data = dateString.split()
+ if data[0][-1] in (',', '.') or data[0].lower() in rfc822._daynames:
+ del data[0]
+ if len(data) == 4:
+ s = data[3]
+ i = s.find('+')
+ if i > 0:
+ data[3:] = [s[:i], s[i+1:]]
+ else:
+ data.append('')
+ dateString = " ".join(data)
+ if len(data) < 5:
+ dateString += ' 00:00:00 GMT'
+ tm = rfc822.parsedate_tz(dateString)
+ if tm:
+ return time.gmtime(rfc822.mktime_tz(tm))
+# rfc822.py defines several time zones, but we define some extra ones.
+# 'ET' is equivalent to 'EST', etc.
+_additional_timezones = {'AT': -400, 'ET': -500, 'CT': -600, 'MT': -700, 'PT': -800}
+rfc822._timezones.update(_additional_timezones)
+registerDateHandler(_parse_date_rfc822)
+
+def _parse_date(dateString):
+ '''Parses a variety of date formats into a 9-tuple in GMT'''
+ for handler in _date_handlers:
+ try:
+ date9tuple = handler(dateString)
+ if not date9tuple: continue
+ if len(date9tuple) != 9:
+ if _debug: sys.stderr.write('date handler function must return 9-tuple\n')
+ raise ValueError
+ map(int, date9tuple)
+ return date9tuple
+ except Exception, e:
+ if _debug: sys.stderr.write('%s raised %s\n' % (handler.__name__, repr(e)))
+ pass
+ return None
+
+def _getCharacterEncoding(http_headers, xml_data):
+ '''Get the character encoding of the XML document
+
+ http_headers is a dictionary
+ xml_data is a raw string (not Unicode)
+
+ This is so much trickier than it sounds, it's not even funny.
+ According to RFC 3023 ('XML Media Types'), if the HTTP Content-Type
+ is application/xml, application/*+xml,
+ application/xml-external-parsed-entity, or application/xml-dtd,
+ the encoding given in the charset parameter of the HTTP Content-Type
+ takes precedence over the encoding given in the XML prefix within the
+ document, and defaults to 'utf-8' if neither are specified. But, if
+ the HTTP Content-Type is text/xml, text/*+xml, or
+ text/xml-external-parsed-entity, the encoding given in the XML prefix
+ within the document is ALWAYS IGNORED and only the encoding given in
+ the charset parameter of the HTTP Content-Type header should be
+ respected, and it defaults to 'us-ascii' if not specified.
+
+ Furthermore, discussion on the atom-syntax mailing list with the
+ author of RFC 3023 leads me to the conclusion that any document
+ served with a Content-Type of text/* and no charset parameter
+ must be treated as us-ascii. (We now do this.) And also that it
+ must always be flagged as non-well-formed. (We now do this too.)
+
+ If Content-Type is unspecified (input was local file or non-HTTP source)
+ or unrecognized (server just got it totally wrong), then go by the
+ encoding given in the XML prefix of the document and default to
+ 'iso-8859-1' as per the HTTP specification (RFC 2616).
+
+ Then, assuming we didn't find a character encoding in the HTTP headers
+ (and the HTTP Content-type allowed us to look in the body), we need
+ to sniff the first few bytes of the XML data and try to determine
+ whether the encoding is ASCII-compatible. Section F of the XML
+ specification shows the way here:
+ http://www.w3.org/TR/REC-xml/#sec-guessing-no-ext-info
+
+ If the sniffed encoding is not ASCII-compatible, we need to make it
+ ASCII compatible so that we can sniff further into the XML declaration
+ to find the encoding attribute, which will tell us the true encoding.
+
+ Of course, none of this guarantees that we will be able to parse the
+ feed in the declared character encoding (assuming it was declared
+ correctly, which many are not). CJKCodecs and iconv_codec help a lot;
+ you should definitely install them if you can.
+ http://cjkpython.i18n.org/
+ '''
+
+ def _parseHTTPContentType(content_type):
+ '''takes HTTP Content-Type header and returns (content type, charset)
+
+ If no charset is specified, returns (content type, '')
+ If no content type is specified, returns ('', '')
+ Both return parameters are guaranteed to be lowercase strings
+ '''
+ content_type = content_type or ''
+ content_type, params = cgi.parse_header(content_type)
+ return content_type, params.get('charset', '').replace("'", '')
+
+ sniffed_xml_encoding = ''
+ xml_encoding = ''
+ true_encoding = ''
+ http_content_type, http_encoding = _parseHTTPContentType(http_headers.get('content-type'))
+ # Must sniff for non-ASCII-compatible character encodings before
+ # searching for XML declaration. This heuristic is defined in
+ # section F of the XML specification:
+ # http://www.w3.org/TR/REC-xml/#sec-guessing-no-ext-info
+ try:
+ if xml_data[:4] == '\x4c\x6f\xa7\x94':
+ # EBCDIC
+ xml_data = _ebcdic_to_ascii(xml_data)
+ elif xml_data[:4] == '\x00\x3c\x00\x3f':
+ # UTF-16BE
+ sniffed_xml_encoding = 'utf-16be'
+ xml_data = unicode(xml_data, 'utf-16be').encode('utf-8')
+ elif (len(xml_data) >= 4) and (xml_data[:2] == '\xfe\xff') and (xml_data[2:4] != '\x00\x00'):
+ # UTF-16BE with BOM
+ sniffed_xml_encoding = 'utf-16be'
+ xml_data = unicode(xml_data[2:], 'utf-16be').encode('utf-8')
+ elif xml_data[:4] == '\x3c\x00\x3f\x00':
+ # UTF-16LE
+ sniffed_xml_encoding = 'utf-16le'
+ xml_data = unicode(xml_data, 'utf-16le').encode('utf-8')
+ elif (len(xml_data) >= 4) and (xml_data[:2] == '\xff\xfe') and (xml_data[2:4] != '\x00\x00'):
+ # UTF-16LE with BOM
+ sniffed_xml_encoding = 'utf-16le'
+ xml_data = unicode(xml_data[2:], 'utf-16le').encode('utf-8')
+ elif xml_data[:4] == '\x00\x00\x00\x3c':
+ # UTF-32BE
+ sniffed_xml_encoding = 'utf-32be'
+ xml_data = unicode(xml_data, 'utf-32be').encode('utf-8')
+ elif xml_data[:4] == '\x3c\x00\x00\x00':
+ # UTF-32LE
+ sniffed_xml_encoding = 'utf-32le'
+ xml_data = unicode(xml_data, 'utf-32le').encode('utf-8')
+ elif xml_data[:4] == '\x00\x00\xfe\xff':
+ # UTF-32BE with BOM
+ sniffed_xml_encoding = 'utf-32be'
+ xml_data = unicode(xml_data[4:], 'utf-32be').encode('utf-8')
+ elif xml_data[:4] == '\xff\xfe\x00\x00':
+ # UTF-32LE with BOM
+ sniffed_xml_encoding = 'utf-32le'
+ xml_data = unicode(xml_data[4:], 'utf-32le').encode('utf-8')
+ elif xml_data[:3] == '\xef\xbb\xbf':
+ # UTF-8 with BOM
+ sniffed_xml_encoding = 'utf-8'
+ xml_data = unicode(xml_data[3:], 'utf-8').encode('utf-8')
+ else:
+ # ASCII-compatible
+ pass
+ xml_encoding_match = re.compile('^<\?.*encoding=[\'"](.*?)[\'"].*\?>').match(xml_data)
+ except:
+ xml_encoding_match = None
+ if xml_encoding_match:
+ xml_encoding = xml_encoding_match.groups()[0].lower()
+ if sniffed_xml_encoding and (xml_encoding in ('iso-10646-ucs-2', 'ucs-2', 'csunicode', 'iso-10646-ucs-4', 'ucs-4', 'csucs4', 'utf-16', 'utf-32', 'utf_16', 'utf_32', 'utf16', 'u16')):
+ xml_encoding = sniffed_xml_encoding
+ acceptable_content_type = 0
+ application_content_types = ('application/xml', 'application/xml-dtd', 'application/xml-external-parsed-entity')
+ text_content_types = ('text/xml', 'text/xml-external-parsed-entity')
+ if (http_content_type in application_content_types) or \
+ (http_content_type.startswith('application/') and http_content_type.endswith('+xml')):
+ acceptable_content_type = 1
+ true_encoding = http_encoding or xml_encoding or 'utf-8'
+ elif (http_content_type in text_content_types) or \
+ (http_content_type.startswith('text/')) and http_content_type.endswith('+xml'):
+ acceptable_content_type = 1
+ true_encoding = http_encoding or 'us-ascii'
+ elif http_content_type.startswith('text/'):
+ true_encoding = http_encoding or 'us-ascii'
+ elif http_headers and (not http_headers.has_key('content-type')):
+ true_encoding = xml_encoding or 'iso-8859-1'
+ else:
+ true_encoding = xml_encoding or 'utf-8'
+ return true_encoding, http_encoding, xml_encoding, sniffed_xml_encoding, acceptable_content_type
+
+def _toUTF8(data, encoding):
+ '''Changes an XML data stream on the fly to specify a new encoding
+
+ data is a raw sequence of bytes (not Unicode) that is presumed to be in %encoding already
+ encoding is a string recognized by encodings.aliases
+ '''
+ if _debug: sys.stderr.write('entering _toUTF8, trying encoding %s\n' % encoding)
+ # strip Byte Order Mark (if present)
+ if (len(data) >= 4) and (data[:2] == '\xfe\xff') and (data[2:4] != '\x00\x00'):
+ if _debug:
+ sys.stderr.write('stripping BOM\n')
+ if encoding != 'utf-16be':
+ sys.stderr.write('trying utf-16be instead\n')
+ encoding = 'utf-16be'
+ data = data[2:]
+ elif (len(data) >= 4) and (data[:2] == '\xff\xfe') and (data[2:4] != '\x00\x00'):
+ if _debug:
+ sys.stderr.write('stripping BOM\n')
+ if encoding != 'utf-16le':
+ sys.stderr.write('trying utf-16le instead\n')
+ encoding = 'utf-16le'
+ data = data[2:]
+ elif data[:3] == '\xef\xbb\xbf':
+ if _debug:
+ sys.stderr.write('stripping BOM\n')
+ if encoding != 'utf-8':
+ sys.stderr.write('trying utf-8 instead\n')
+ encoding = 'utf-8'
+ data = data[3:]
+ elif data[:4] == '\x00\x00\xfe\xff':
+ if _debug:
+ sys.stderr.write('stripping BOM\n')
+ if encoding != 'utf-32be':
+ sys.stderr.write('trying utf-32be instead\n')
+ encoding = 'utf-32be'
+ data = data[4:]
+ elif data[:4] == '\xff\xfe\x00\x00':
+ if _debug:
+ sys.stderr.write('stripping BOM\n')
+ if encoding != 'utf-32le':
+ sys.stderr.write('trying utf-32le instead\n')
+ encoding = 'utf-32le'
+ data = data[4:]
+ newdata = unicode(data, encoding)
+ if _debug: sys.stderr.write('successfully converted %s data to unicode\n' % encoding)
+ declmatch = re.compile('^<\?xml[^>]*?>')
+ newdecl = ''''''
+ if declmatch.search(newdata):
+ newdata = declmatch.sub(newdecl, newdata)
+ else:
+ newdata = newdecl + u'\n' + newdata
+ return newdata.encode('utf-8')
+
+def _stripDoctype(data):
+ '''Strips DOCTYPE from XML document, returns (rss_version, stripped_data)
+
+ rss_version may be 'rss091n' or None
+ stripped_data is the same XML document, minus the DOCTYPE
+ '''
+ entity_pattern = re.compile(r']*?)>', re.MULTILINE)
+ data = entity_pattern.sub('', data)
+ doctype_pattern = re.compile(r']*?)>', re.MULTILINE)
+ doctype_results = doctype_pattern.findall(data)
+ doctype = doctype_results and doctype_results[0] or ''
+ if doctype.lower().count('netscape'):
+ version = 'rss091n'
+ else:
+ version = None
+ data = doctype_pattern.sub('', data)
+ return version, data
+
+def parse(url_file_stream_or_string, etag=None, modified=None, agent=None, referrer=None, handlers=[]):
+ '''Parse a feed from a URL, file, stream, or string'''
+ result = FeedParserDict()
+ result['feed'] = FeedParserDict()
+ result['entries'] = []
+ if _XML_AVAILABLE:
+ result['bozo'] = 0
+ if type(handlers) == types.InstanceType:
+ handlers = [handlers]
+ try:
+ f = _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, handlers)
+ data = f.read()
+ except Exception, e:
+ result['bozo'] = 1
+ result['bozo_exception'] = e
+ data = ''
+ f = None
+
+ # if feed is gzip-compressed, decompress it
+ if f and data and hasattr(f, 'headers'):
+ if gzip and f.headers.get('content-encoding', '') == 'gzip':
+ try:
+ data = gzip.GzipFile(fileobj=_StringIO(data)).read()
+ except Exception, e:
+ # Some feeds claim to be gzipped but they're not, so
+ # we get garbage. Ideally, we should re-request the
+ # feed without the 'Accept-encoding: gzip' header,
+ # but we don't.
+ result['bozo'] = 1
+ result['bozo_exception'] = e
+ data = ''
+ elif zlib and f.headers.get('content-encoding', '') == 'deflate':
+ try:
+ data = zlib.decompress(data, -zlib.MAX_WBITS)
+ except Exception, e:
+ result['bozo'] = 1
+ result['bozo_exception'] = e
+ data = ''
+
+ # save HTTP headers
+ if hasattr(f, 'info'):
+ info = f.info()
+ result['etag'] = info.getheader('ETag')
+ last_modified = info.getheader('Last-Modified')
+ if last_modified:
+ result['modified'] = _parse_date(last_modified)
+ if hasattr(f, 'url'):
+ result['href'] = f.url
+ result['status'] = 200
+ if hasattr(f, 'status'):
+ result['status'] = f.status
+ if hasattr(f, 'headers'):
+ result['headers'] = f.headers.dict
+ if hasattr(f, 'close'):
+ f.close()
+
+ # there are four encodings to keep track of:
+ # - http_encoding is the encoding declared in the Content-Type HTTP header
+ # - xml_encoding is the encoding declared in the ; changed
+# project name
+#2.5 - 7/25/2003 - MAP - changed to Python license (all contributors agree);
+# removed unnecessary urllib code -- urllib2 should always be available anyway;
+# return actual url, status, and full HTTP headers (as result['url'],
+# result['status'], and result['headers']) if parsing a remote feed over HTTP --
+# this should pass all the HTTP tests at ;
+# added the latest namespace-of-the-week for RSS 2.0
+#2.5.1 - 7/26/2003 - RMK - clear opener.addheaders so we only send our custom
+# User-Agent (otherwise urllib2 sends two, which confuses some servers)
+#2.5.2 - 7/28/2003 - MAP - entity-decode inline xml properly; added support for
+# inline and as used in some RSS 2.0 feeds
+#2.5.3 - 8/6/2003 - TvdV - patch to track whether we're inside an image or
+# textInput, and also to return the character encoding (if specified)
+#2.6 - 1/1/2004 - MAP - dc:author support (MarekK); fixed bug tracking
+# nested divs within content (JohnD); fixed missing sys import (JohanS);
+# fixed regular expression to capture XML character encoding (Andrei);
+# added support for Atom 0.3-style links; fixed bug with textInput tracking;
+# added support for cloud (MartijnP); added support for multiple
+# category/dc:subject (MartijnP); normalize content model: 'description' gets
+# description (which can come from description, summary, or full content if no
+# description), 'content' gets dict of base/language/type/value (which can come
+# from content:encoded, xhtml:body, content, or fullitem);
+# fixed bug matching arbitrary Userland namespaces; added xml:base and xml:lang
+# tracking; fixed bug tracking unknown tags; fixed bug tracking content when
+# element is not in default namespace (like Pocketsoap feed);
+# resolve relative URLs in link, guid, docs, url, comments, wfw:comment,
+# wfw:commentRSS; resolve relative URLs within embedded HTML markup in
+# description, xhtml:body, content, content:encoded, title, subtitle,
+# summary, info, tagline, and copyright; added support for pingback and
+# trackback namespaces
+#2.7 - 1/5/2004 - MAP - really added support for trackback and pingback
+# namespaces, as opposed to 2.6 when I said I did but didn't really;
+# sanitize HTML markup within some elements; added mxTidy support (if
+# installed) to tidy HTML markup within some elements; fixed indentation
+# bug in _parse_date (FazalM); use socket.setdefaulttimeout if available
+# (FazalM); universal date parsing and normalization (FazalM): 'created', modified',
+# 'issued' are parsed into 9-tuple date format and stored in 'created_parsed',
+# 'modified_parsed', and 'issued_parsed'; 'date' is duplicated in 'modified'
+# and vice-versa; 'date_parsed' is duplicated in 'modified_parsed' and vice-versa
+#2.7.1 - 1/9/2004 - MAP - fixed bug handling " and '. fixed memory
+# leak not closing url opener (JohnD); added dc:publisher support (MarekK);
+# added admin:errorReportsTo support (MarekK); Python 2.1 dict support (MarekK)
+#2.7.4 - 1/14/2004 - MAP - added workaround for improperly formed
tags in
+# encoded HTML (skadz); fixed unicode handling in normalize_attrs (ChrisL);
+# fixed relative URI processing for guid (skadz); added ICBM support; added
+# base64 support
+#2.7.5 - 1/15/2004 - MAP - added workaround for malformed DOCTYPE (seen on many
+# blogspot.com sites); added _debug variable
+#2.7.6 - 1/16/2004 - MAP - fixed bug with StringIO importing
+#3.0b3 - 1/23/2004 - MAP - parse entire feed with real XML parser (if available);
+# added several new supported namespaces; fixed bug tracking naked markup in
+# description; added support for enclosure; added support for source; re-added
+# support for cloud which got dropped somehow; added support for expirationDate
+#3.0b4 - 1/26/2004 - MAP - fixed xml:lang inheritance; fixed multiple bugs tracking
+# xml:base URI, one for documents that don't define one explicitly and one for
+# documents that define an outer and an inner xml:base that goes out of scope
+# before the end of the document
+#3.0b5 - 1/26/2004 - MAP - fixed bug parsing multiple links at feed level
+#3.0b6 - 1/27/2004 - MAP - added feed type and version detection, result['version']
+# will be one of SUPPORTED_VERSIONS.keys() or empty string if unrecognized;
+# added support for creativeCommons:license and cc:license; added support for
+# full Atom content model in title, tagline, info, copyright, summary; fixed bug
+# with gzip encoding (not always telling server we support it when we do)
+#3.0b7 - 1/28/2004 - MAP - support Atom-style author element in author_detail
+# (dictionary of 'name', 'url', 'email'); map author to author_detail if author
+# contains name + email address
+#3.0b8 - 1/28/2004 - MAP - added support for contributor
+#3.0b9 - 1/29/2004 - MAP - fixed check for presence of dict function; added
+# support for summary
+#3.0b10 - 1/31/2004 - MAP - incorporated ISO-8601 date parsing routines from
+# xml.util.iso8601
+#3.0b11 - 2/2/2004 - MAP - added 'rights' to list of elements that can contain
+# dangerous markup; fiddled with decodeEntities (not right); liberalized
+# date parsing even further
+#3.0b12 - 2/6/2004 - MAP - fiddled with decodeEntities (still not right);
+# added support to Atom 0.2 subtitle; added support for Atom content model
+# in copyright; better sanitizing of dangerous HTML elements with end tags
+# (script, frameset)
+#3.0b13 - 2/8/2004 - MAP - better handling of empty HTML tags (br, hr, img,
+# etc.) in embedded markup, in either HTML or XHTML form (
,
,
)
+#3.0b14 - 2/8/2004 - MAP - fixed CDATA handling in non-wellformed feeds under
+# Python 2.1
+#3.0b15 - 2/11/2004 - MAP - fixed bug resolving relative links in wfw:commentRSS;
+# fixed bug capturing author and contributor URL; fixed bug resolving relative
+# links in author and contributor URL; fixed bug resolvin relative links in
+# generator URL; added support for recognizing RSS 1.0; passed Simon Fell's
+# namespace tests, and included them permanently in the test suite with his
+# permission; fixed namespace handling under Python 2.1
+#3.0b16 - 2/12/2004 - MAP - fixed support for RSS 0.90 (broken in b15)
+#3.0b17 - 2/13/2004 - MAP - determine character encoding as per RFC 3023
+#3.0b18 - 2/17/2004 - MAP - always map description to summary_detail (Andrei);
+# use libxml2 (if available)
+#3.0b19 - 3/15/2004 - MAP - fixed bug exploding author information when author
+# name was in parentheses; removed ultra-problematic mxTidy support; patch to
+# workaround crash in PyXML/expat when encountering invalid entities
+# (MarkMoraes); support for textinput/textInput
+#3.0b20 - 4/7/2004 - MAP - added CDF support
+#3.0b21 - 4/14/2004 - MAP - added Hot RSS support
+#3.0b22 - 4/19/2004 - MAP - changed 'channel' to 'feed', 'item' to 'entries' in
+# results dict; changed results dict to allow getting values with results.key
+# as well as results[key]; work around embedded illformed HTML with half
+# a DOCTYPE; work around malformed Content-Type header; if character encoding
+# is wrong, try several common ones before falling back to regexes (if this
+# works, bozo_exception is set to CharacterEncodingOverride); fixed character
+# encoding issues in BaseHTMLProcessor by tracking encoding and converting
+# from Unicode to raw strings before feeding data to sgmllib.SGMLParser;
+# convert each value in results to Unicode (if possible), even if using
+# regex-based parsing
+#3.0b23 - 4/21/2004 - MAP - fixed UnicodeDecodeError for feeds that contain
+# high-bit characters in attributes in embedded HTML in description (thanks
+# Thijs van de Vossen); moved guid, date, and date_parsed to mapped keys in
+# FeedParserDict; tweaked FeedParserDict.has_key to return True if asking
+# about a mapped key
+#3.0fc1 - 4/23/2004 - MAP - made results.entries[0].links[0] and
+# results.entries[0].enclosures[0] into FeedParserDict; fixed typo that could
+# cause the same encoding to be tried twice (even if it failed the first time);
+# fixed DOCTYPE stripping when DOCTYPE contained entity declarations;
+# better textinput and image tracking in illformed RSS 1.0 feeds
+#3.0fc2 - 5/10/2004 - MAP - added and passed Sam's amp tests; added and passed
+# my blink tag tests
+#3.0fc3 - 6/18/2004 - MAP - fixed bug in _changeEncodingDeclaration that
+# failed to parse utf-16 encoded feeds; made source into a FeedParserDict;
+# duplicate admin:generatorAgent/@rdf:resource in generator_detail.url;
+# added support for image; refactored parse() fallback logic to try other
+# encodings if SAX parsing fails (previously it would only try other encodings
+# if re-encoding failed); remove unichr madness in normalize_attrs now that
+# we're properly tracking encoding in and out of BaseHTMLProcessor; set
+# feed.language from root-level xml:lang; set entry.id from rdf:about;
+# send Accept header
+#3.0 - 6/21/2004 - MAP - don't try iso-8859-1 (can't distinguish between
+# iso-8859-1 and windows-1252 anyway, and most incorrectly marked feeds are
+# windows-1252); fixed regression that could cause the same encoding to be
+# tried twice (even if it failed the first time)
+#3.0.1 - 6/22/2004 - MAP - default to us-ascii for all text/* content types;
+# recover from malformed content-type header parameter with no equals sign
+# ('text/xml; charset:iso-8859-1')
+#3.1 - 6/28/2004 - MAP - added and passed tests for converting HTML entities
+# to Unicode equivalents in illformed feeds (aaronsw); added and
+# passed tests for converting character entities to Unicode equivalents
+# in illformed feeds (aaronsw); test for valid parsers when setting
+# XML_AVAILABLE; make version and encoding available when server returns
+# a 304; add handlers parameter to pass arbitrary urllib2 handlers (like
+# digest auth or proxy support); add code to parse username/password
+# out of url and send as basic authentication; expose downloading-related
+# exceptions in bozo_exception (aaronsw); added __contains__ method to
+# FeedParserDict (aaronsw); added publisher_detail (aaronsw)
+#3.2 - 7/3/2004 - MAP - use cjkcodecs and iconv_codec if available; always
+# convert feed to UTF-8 before passing to XML parser; completely revamped
+# logic for determining character encoding and attempting XML parsing
+# (much faster); increased default timeout to 20 seconds; test for presence
+# of Location header on redirects; added tests for many alternate character
+# encodings; support various EBCDIC encodings; support UTF-16BE and
+# UTF16-LE with or without a BOM; support UTF-8 with a BOM; support
+# UTF-32BE and UTF-32LE with or without a BOM; fixed crashing bug if no
+# XML parsers are available; added support for 'Content-encoding: deflate';
+# send blank 'Accept-encoding: ' header if neither gzip nor zlib modules
+# are available
+#3.3 - 7/15/2004 - MAP - optimize EBCDIC to ASCII conversion; fix obscure
+# problem tracking xml:base and xml:lang if element declares it, child
+# doesn't, first grandchild redeclares it, and second grandchild doesn't;
+# refactored date parsing; defined public registerDateHandler so callers
+# can add support for additional date formats at runtime; added support
+# for OnBlog, Nate, MSSQL, Greek, and Hungarian dates (ytrewq1); added
+# zopeCompatibilityHack() which turns FeedParserDict into a regular
+# dictionary, required for Zope compatibility, and also makes command-
+# line debugging easier because pprint module formats real dictionaries
+# better than dictionary-like objects; added NonXMLContentType exception,
+# which is stored in bozo_exception when a feed is served with a non-XML
+# media type such as 'text/plain'; respect Content-Language as default
+# language if not xml:lang is present; cloud dict is now FeedParserDict;
+# generator dict is now FeedParserDict; better tracking of xml:lang,
+# including support for xml:lang='' to unset the current language;
+# recognize RSS 1.0 feeds even when RSS 1.0 namespace is not the default
+# namespace; don't overwrite final status on redirects (scenarios:
+# redirecting to a URL that returns 304, redirecting to a URL that
+# redirects to another URL with a different type of redirect); add
+# support for HTTP 303 redirects
+#4.0 - MAP - support for relative URIs in xml:base attribute; fixed
+# encoding issue with mxTidy (phopkins); preliminary support for RFC 3229;
+# support for Atom 1.0; support for iTunes extensions; new 'tags' for
+# categories/keywords/etc. as array of dict
+# {'term': term, 'scheme': scheme, 'label': label} to match Atom 1.0
+# terminology; parse RFC 822-style dates with no time; lots of other
+# bug fixes
+#4.1 - MAP - removed socket timeout; added support for chardet library
diff --git a/generic.py b/generic.py
index 9d9d170..b1aa414 100644
--- a/generic.py
+++ b/generic.py
@@ -26,7 +26,7 @@ class PesterList(list):
class PesterIcon(QtGui.QIcon):
def __init__(self, *x, **y):
- QtGui.QIcon.__init__(self, *x, **y)
+ QtGui.QIcon.__init__(self, x[0])
if type(x[0]) in [str, unicode]:
self.icon_pixmap = QtGui.QPixmap(x[0])
else:
@@ -50,6 +50,15 @@ class RightClickList(QtGui.QListWidget):
def getOptionsMenu(self):
return self.optionsMenu
+class RightClickTree(QtGui.QTreeWidget):
+ def contextMenuEvent(self, event):
+ if event.reason() == QtGui.QContextMenuEvent.Mouse:
+ listing = self.itemAt(event.pos())
+ self.setCurrentItem(listing)
+ self.getOptionsMenu().popup(event.globalPos())
+ def getOptionsMenu(self):
+ return self.optionsMenu
+
class MultiTextDialog(QtGui.QDialog):
def __init__(self, title, parent, *queries):
QtGui.QDialog.__init__(self, parent)
diff --git a/irc.py b/irc.py
index b0087cc..b74eac5 100644
--- a/irc.py
+++ b/irc.py
@@ -5,9 +5,11 @@ from oyoyo import helpers
import logging
import random
import socket
+from time import time
from dataobjs import Mood, PesterProfile
from generic import PesterList
+from version import _pcVersion
logging.basicConfig(level=logging.INFO)
@@ -53,7 +55,7 @@ class PesterIRC(QtCore.QThread):
if not res:
logging.debug("false Yield: %s, returning" % res)
return
-
+
def setConnected(self):
self.registeredIRC = True
self.connected.emit()
@@ -87,18 +89,49 @@ class PesterIRC(QtCore.QThread):
self.cli.command_handler.getMood(*chums)
@QtCore.pyqtSlot(PesterList)
def getMoods(self, chums):
- self.cli.command_handler.getMood(*chums)
+ self.cli.command_handler.getMood(*chums)
+ @QtCore.pyqtSlot(QtCore.QString, QtCore.QString)
+ def sendNotice(self, text, handle):
+ h = unicode(handle)
+ t = unicode(text)
+ try:
+ helpers.notice(self.cli, h, t)
+ except socket.error:
+ self.setConnectionBroken()
@QtCore.pyqtSlot(QtCore.QString, QtCore.QString)
def sendMessage(self, text, handle):
h = unicode(handle)
textl = [unicode(text)]
def splittext(l):
if len(l[0]) > 450:
- space = l[0].rfind(" ", 0,450)
+ space = l[0].rfind(" ", 0,430)
if space == -1:
space = 450
+ elif l[0][space+1:space+5] == "":
+ space = space+4
a = l[0][0:space+1]
b = l[0][space+1:]
+ if a.count(" a.count(""):
+ # oh god ctags will break!! D=
+ hanging = []
+ usedends = []
+ c = a.rfind("", c)
+ while d in usedends:
+ d = a.find("", d+1)
+ if d != -1: usedends.append(d)
+ else:
+ f = a.find(">", c)+1
+ hanging.append(a[c:f])
+ c = a.rfind("")):
+ a = a + ""
+ #start them up again in the second part
+ for c in hanging:
+ b = c + b
if len(b) > 0:
return [a] + splittext([b])
else:
@@ -135,6 +168,7 @@ class PesterIRC(QtCore.QThread):
helpers.nick(self.cli, handle)
except socket.error:
self.setConnectionBroken()
+ self.mainwindow.closeConversations(True)
self.updateMood()
@QtCore.pyqtSlot()
def updateMood(self):
@@ -183,6 +217,7 @@ class PesterIRC(QtCore.QThread):
c = unicode(channel)
try:
helpers.join(self.cli, c)
+ helpers.mode(self.cli, c, "", None)
except socket.error:
self.setConnectionBroken()
@QtCore.pyqtSlot(QtCore.QString)
@@ -194,10 +229,15 @@ class PesterIRC(QtCore.QThread):
self.setConnectionBroken()
@QtCore.pyqtSlot(QtCore.QString, QtCore.QString)
def kickUser(self, handle, channel):
+ l = handle.split(":")
c = unicode(channel)
- h = unicode(handle)
+ h = unicode(l[0])
+ if len(l) > 1:
+ reason = unicode(l[1])
+ else:
+ reason = ""
try:
- helpers.kick(self.cli, h, c)
+ helpers.kick(self.cli, h, c, reason)
except socket.error:
self.setConnectionBroken()
@QtCore.pyqtSlot(QtCore.QString, QtCore.QString, QtCore.QString)
@@ -211,21 +251,60 @@ class PesterIRC(QtCore.QThread):
helpers.mode(self.cli, c, m, cmd)
except socket.error:
self.setConnectionBroken()
+ @QtCore.pyqtSlot(QtCore.QString)
+ def channelNames(self, channel):
+ c = unicode(channel)
+ try:
+ helpers.names(self.cli, c)
+ except socket.error:
+ self.setConnectionBroken()
+ @QtCore.pyqtSlot(QtCore.QString, QtCore.QString)
+ def inviteChum(self, handle, channel):
+ h = unicode(handle)
+ c = unicode(channel)
+ try:
+ helpers.invite(self.cli, h, c)
+ except socket.error:
+ self.setConnectionBroken()
+
+ @QtCore.pyqtSlot()
+ def pingServer(self):
+ try:
+ self.cli.send("PING %s" % int(time()))
+ except socket.error:
+ self.setConnectionBroken()
moodUpdated = QtCore.pyqtSignal(QtCore.QString, Mood)
colorUpdated = QtCore.pyqtSignal(QtCore.QString, QtGui.QColor)
messageReceived = QtCore.pyqtSignal(QtCore.QString, QtCore.QString)
memoReceived = QtCore.pyqtSignal(QtCore.QString, QtCore.QString, QtCore.QString)
+ noticeReceived = QtCore.pyqtSignal(QtCore.QString, QtCore.QString)
+ inviteReceived = QtCore.pyqtSignal(QtCore.QString, QtCore.QString)
timeCommand = QtCore.pyqtSignal(QtCore.QString, QtCore.QString, QtCore.QString)
namesReceived = QtCore.pyqtSignal(QtCore.QString, PesterList)
channelListReceived = QtCore.pyqtSignal(PesterList)
nickCollision = QtCore.pyqtSignal(QtCore.QString, QtCore.QString)
myHandleChanged = QtCore.pyqtSignal(QtCore.QString)
+ chanInviteOnly = QtCore.pyqtSignal(QtCore.QString)
+ modesUpdated = QtCore.pyqtSignal(QtCore.QString, QtCore.QString)
connected = QtCore.pyqtSignal()
userPresentUpdate = QtCore.pyqtSignal(QtCore.QString, QtCore.QString,
QtCore.QString)
+ cannotSendToChan = QtCore.pyqtSignal(QtCore.QString, QtCore.QString)
+ tooManyPeeps = QtCore.pyqtSignal()
class PesterHandler(DefaultCommandHandler):
+ def notice(self, nick, chan, msg):
+ try:
+ msg = msg.decode('utf-8')
+ except UnicodeDecodeError:
+ msg = msg.decode('iso-8859-1', 'ignore')
+ handle = nick[0:nick.find("!")]
+ logging.info("---> recv \"NOTICE %s :%s\"" % (handle, msg))
+ if handle == "ChanServ" and chan == self.parent.mainwindow.profile().handle and msg[0:2] == "[#":
+ self.parent.memoReceived.emit(msg[1:msg.index("]")], handle, msg)
+ else:
+ self.parent.noticeReceived.emit(handle, msg)
def privmsg(self, nick, chan, msg):
try:
msg = msg.decode('utf-8')
@@ -236,6 +315,10 @@ class PesterHandler(DefaultCommandHandler):
return
# silently ignore CTCP
if msg[0] == '\x01':
+ handle = nick[0:nick.find("!")]
+ logging.info("---> recv \"CTCP %s :%s\"" % (handle, msg[1:-1]))
+ if msg[1:-1] == "VERSION":
+ helpers.notice(self.parent.cli, handle, "\x01VERSION Pesterchum %s\x01" % (_pcVersion))
return
handle = nick[0:nick.find("!")]
logging.info("---> recv \"PRIVMSG %s :%s\"" % (handle, msg))
@@ -251,7 +334,7 @@ class PesterHandler(DefaultCommandHandler):
mychumhandle = self.mainwindow.profile().handle
mymood = self.mainwindow.profile().mood.value()
if msg.find(mychumhandle, 8) != -1:
- helpers.msg(self.client, "#pesterchum",
+ helpers.msg(self.client, "#pesterchum",
"MOOD >%d" % (mymood))
elif chan[0] == '#':
if msg[0:16] == "PESTERCHUM:TIME>":
@@ -282,9 +365,6 @@ class PesterHandler(DefaultCommandHandler):
mymood = self.mainwindow.profile().mood.value()
helpers.msg(self.client, "#pesterchum", "MOOD >%d" % (mymood))
- chums = self.mainwindow.chumList.chums
- self.getMood(*chums)
-
def nicknameinuse(self, server, cmd, nick, msg):
newnick = "pesterClient%d" % (random.randint(100,999))
helpers.nick(self.client, newnick)
@@ -293,8 +373,9 @@ class PesterHandler(DefaultCommandHandler):
handle = nick[0:nick.find("!")]
self.parent.userPresentUpdate.emit(handle, "", "quit")
self.parent.moodUpdated.emit(handle, Mood("offline"))
- def kick(self, opnick, channel, handle, op):
- self.parent.userPresentUpdate.emit(handle, channel, "kick:%s" % (op))
+ def kick(self, opnick, channel, handle, reason):
+ op = opnick[0:opnick.find("!")]
+ self.parent.userPresentUpdate.emit(handle, channel, "kick:%s:%s" % (op, reason))
# ok i shouldnt be overloading that but am lazy
def part(self, nick, channel, reason="nanchos"):
handle = nick[0:nick.find("!")]
@@ -306,8 +387,35 @@ class PesterHandler(DefaultCommandHandler):
self.parent.userPresentUpdate.emit(handle, channel, "join")
if channel == "#pesterchum":
self.parent.moodUpdated.emit(handle, Mood("chummy"))
- def mode(self, op, channel, mode, handle=""):
- self.parent.userPresentUpdate.emit(handle, channel, mode)
+ def mode(self, op, channel, mode, *handles):
+ if len(handles) <= 0: handles = [""]
+ opnick = op[0:op.find("!")]
+ if op == channel or channel == self.parent.mainwindow.profile().handle:
+ modes = list(self.parent.mainwindow.modes)
+ if modes and modes[0] == "+": modes = modes[1:]
+ if mode[0] == "+":
+ for m in mode[1:]:
+ if m not in modes:
+ modes.extend(m)
+ elif mode[0] == "-":
+ for i in mode[1:]:
+ try:
+ modes.remove(i)
+ except ValueError:
+ pass
+ modes.sort()
+ self.parent.mainwindow.modes = "+" + "".join(modes)
+ modes = []
+ cur = "+"
+ for l in mode:
+ if l in ["+","-"]: cur = l
+ else:
+ modes.append("%s%s" % (cur, l))
+ for (i,m) in enumerate(modes):
+ try:
+ self.parent.userPresentUpdate.emit(handles[i], channel, m+":%s" % (op))
+ except IndexError:
+ self.parent.userPresentUpdate.emit("", channel, m+":%s" % (op))
def nick(self, oldnick, newnick):
oldhandle = oldnick[0:oldnick.find("!")]
if oldhandle == self.mainwindow.profile().handle:
@@ -322,7 +430,7 @@ class PesterHandler(DefaultCommandHandler):
logging.info("---> recv \"NAMES %s: %d names\"" % (channel, len(namelist)))
if not hasattr(self, 'channelnames'):
self.channelnames = {}
- if not self.channelnames.has_key(channel):
+ if channel not in self.channelnames:
self.channelnames[channel] = []
self.channelnames[channel].extend(namelist)
def endofnames(self, server, nick, channel, msg):
@@ -330,6 +438,15 @@ class PesterHandler(DefaultCommandHandler):
pl = PesterList(namelist)
del self.channelnames[channel]
self.parent.namesReceived.emit(channel, pl)
+ if channel == "#pesterchum" and not hasattr(self, "joined"):
+ self.joined = True
+ chums = self.mainwindow.chumList.chums
+ lesschums = []
+ for c in chums:
+ chandle = c.handle
+ if chandle in namelist:
+ lesschums.append(c)
+ self.getMood(*lesschums)
def liststart(self, server, handle, *info):
self.channel_list = []
@@ -347,7 +464,25 @@ class PesterHandler(DefaultCommandHandler):
logging.info("---> recv \"CHANNELS END\"")
self.parent.channelListReceived.emit(pl)
self.channel_list = []
-
+
+ def umodeis(self, server, handle, modes):
+ self.parent.mainwindow.modes = modes
+ def invite(self, sender, you, channel):
+ handle = sender.split('!')[0]
+ self.parent.inviteReceived.emit(handle, channel)
+ def inviteonlychan(self, server, handle, channel, msg):
+ self.parent.chanInviteOnly.emit(channel)
+ def channelmodeis(self, server, handle, channel, modes):
+ self.parent.modesUpdated.emit(channel, modes)
+ def cannotsendtochan(self, server, handle, channel, msg):
+ self.parent.cannotSendToChan.emit(channel, msg)
+ def toomanypeeps(self, *stuff):
+ self.parent.tooManyPeeps.emit()
+
+ def ping(self, prefix, server):
+ self.parent.mainwindow.lastping = int(time())
+ self.client.send('PONG', server)
+
def getMood(self, *chums):
chumglub = "GETMOOD "
for c in chums:
@@ -364,4 +499,4 @@ class PesterHandler(DefaultCommandHandler):
helpers.msg(self.client, "#pesterchum", chumglub)
except socket.error:
self.parent.setConnectionBroken()
-
+
diff --git a/logviewer.py b/logviewer.py
index b1c9474..4b253ea 100644
--- a/logviewer.py
+++ b/logviewer.py
@@ -3,10 +3,36 @@ import codecs
import re
from time import strftime, strptime
from PyQt4 import QtGui, QtCore
-from generic import RightClickList
+from generic import RightClickList, RightClickTree
from parsetools import convertTags
from convo import PesterText
+class PesterLogSearchInput(QtGui.QLineEdit):
+ def __init__(self, theme, parent=None):
+ QtGui.QLineEdit.__init__(self, parent)
+ self.setStyleSheet(theme["convo/input/style"] + "margin-right:0px;")
+ def keyPressEvent(self, event):
+ QtGui.QLineEdit.keyPressEvent(self, event)
+ if hasattr(self.parent(), 'textArea'):
+ if event.key() == QtCore.Qt.Key_Return:
+ self.parent().logSearch(self.text())
+ if self.parent().textArea.find(self.text()):
+ self.parent().textArea.ensureCursorVisible()
+ else:
+ self.parent().logSearch(self.text())
+
+class PesterLogHighlighter(QtGui.QSyntaxHighlighter):
+ def __init__(self, parent):
+ QtGui.QSyntaxHighlighter.__init__(self, parent)
+ self.searchTerm = ""
+ self.hilightstyle = QtGui.QTextCharFormat()
+ self.hilightstyle.setBackground(QtGui.QBrush(QtCore.Qt.green))
+ self.hilightstyle.setForeground(QtGui.QBrush(QtCore.Qt.black))
+ def highlightBlock(self, text):
+ for i in range(0, len(text)-(len(self.searchTerm)-1)):
+ if unicode(text[i:i+len(self.searchTerm)]).lower() == unicode(self.searchTerm).lower():
+ self.setFormat(i, len(self.searchTerm), self.hilightstyle)
+
class PesterLogUserSelect(QtGui.QDialog):
def __init__(self, config, theme, parent):
QtGui.QDialog.__init__(self, parent)
@@ -44,6 +70,9 @@ class PesterLogUserSelect(QtGui.QDialog):
item.setTextColor(QtGui.QColor(self.theme["main/chums/userlistcolor"]))
self.chumsBox.addItem(item)
+ self.search = PesterLogSearchInput(theme, self)
+ self.search.setFocus()
+
self.cancel = QtGui.QPushButton("CANCEL", self)
self.connect(self.cancel, QtCore.SIGNAL('clicked()'),
self, QtCore.SLOT('reject()'))
@@ -58,6 +87,7 @@ class PesterLogUserSelect(QtGui.QDialog):
layout_0 = QtGui.QVBoxLayout()
layout_0.addWidget(instructions)
layout_0.addWidget(self.chumsBox)
+ layout_0.addWidget(self.search)
layout_0.addLayout(layout_ok)
self.setLayout(layout_0)
@@ -65,6 +95,11 @@ class PesterLogUserSelect(QtGui.QDialog):
def selectedchum(self):
return self.chumsBox.currentItem()
+ def logSearch(self, search):
+ found = self.chumsBox.findItems(search, QtCore.Qt.MatchStartsWith)
+ if len(found) > 0 and len(found) < self.chumsBox.count():
+ self.chumsBox.setCurrentItem(found[0])
+
@QtCore.pyqtSlot()
def viewActivatedLog(self):
selectedchum = self.selectedchum().text()
@@ -91,6 +126,7 @@ class PesterLogViewer(QtGui.QDialog):
self.config = config
self.theme = theme
self.parent = parent
+ self.mainwindow = parent
global _datadir
self.handle = parent.profile().handle
self.chum = chum
@@ -138,7 +174,8 @@ class PesterLogViewer(QtGui.QDialog):
self.logList.sort()
self.logList.reverse()
- self.tree = QtGui.QTreeWidget()
+ self.tree = RightClickTree()
+ self.tree.optionsMenu = QtGui.QMenu(self)
self.tree.setFixedSize(260, 300)
self.tree.header().hide()
if theme.has_key("convo/scrollbar"):
@@ -148,6 +185,7 @@ class PesterLogViewer(QtGui.QDialog):
self.connect(self.tree, QtCore.SIGNAL('itemSelectionChanged()'),
self, QtCore.SLOT('loadSelectedLog()'))
self.tree.setSortingEnabled(False)
+
child_1 = None
last = ["",""]
for (i,l) in enumerate(self.logList):
@@ -160,20 +198,40 @@ class PesterLogViewer(QtGui.QDialog):
child_1.addChild(QtGui.QTreeWidgetItem([self.fileToTime(l)]))
last = self.fileToMonthYear(l)
+ self.hilight = PesterLogHighlighter(self.textArea)
if len(self.logList) > 0: self.loadLog(self.logList[0])
+ self.search = PesterLogSearchInput(theme, self)
+ self.search.setFocus()
+ self.find = QtGui.QPushButton("Find", self)
+ font = self.find.font()
+ font.setPointSize(8)
+ self.find.setFont(font)
+ self.find.setDefault(True)
+ self.find.setFixedSize(40, 20)
+ layout_search = QtGui.QHBoxLayout()
+ layout_search.addWidget(self.search)
+ layout_search.addWidget(self.find)
+
+ self.qdb = QtGui.QPushButton("Pesterchum QDB", self)
+ self.qdb.setFixedWidth(260)
+ self.connect(self.qdb, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('openQDB()'))
self.ok = QtGui.QPushButton("CLOSE", self)
- self.ok.setDefault(True)
self.ok.setFixedWidth(80)
self.connect(self.ok, QtCore.SIGNAL('clicked()'),
self, QtCore.SLOT('reject()'))
layout_ok = QtGui.QHBoxLayout()
+ layout_ok.addWidget(self.qdb)
layout_ok.addWidget(self.ok)
layout_ok.setAlignment(self.ok, QtCore.Qt.AlignRight)
layout_logs = QtGui.QHBoxLayout()
layout_logs.addWidget(self.tree)
- layout_logs.addWidget(self.textArea)
+ layout_right = QtGui.QVBoxLayout()
+ layout_right.addWidget(self.textArea)
+ layout_right.addLayout(layout_search)
+ layout_logs.addLayout(layout_right)
layout_0 = QtGui.QVBoxLayout()
layout_0.addWidget(self.instructions)
@@ -187,6 +245,10 @@ class PesterLogViewer(QtGui.QDialog):
if len(self.tree.currentItem().text(0)) > len("September 2011"):
self.loadLog(self.timeToFile(self.tree.currentItem().text(0)))
+ @QtCore.pyqtSlot()
+ def openQDB(self):
+ QtGui.QDesktopServices.openUrl(QtCore.QUrl("http://qdb.pesterchum.net/index.php?p=browse", QtCore.QUrl.TolerantMode))
+
def loadLog(self, fname):
fp = codecs.open("%s/%s/%s/%s/%s" % (self.logpath, self.handle, self.chum, self.format, fname), encoding='utf-8', mode='r')
self.textArea.clear()
@@ -199,6 +261,10 @@ class PesterLogViewer(QtGui.QDialog):
self.textArea.setTextCursor(textCur)
self.instructions.setText("Pesterlog with " +self.chum+ " on " + self.fileToTime(str(fname)))
+ def logSearch(self, search):
+ self.hilight.searchTerm = search
+ self.hilight.rehighlight()
+
def fileToMonthYear(self, fname):
time = strptime(fname[(fname.index(".")+1):fname.index(".txt")], "%Y-%m-%d.%H.%M")
return [strftime("%B", time), strftime("%Y", time)]
@@ -217,15 +283,30 @@ class PesterLogText(PesterText):
def mousePressEvent(self, event):
url = self.anchorAt(event.pos())
if url != "":
- if url[0] != "#":
+ if url[0] == "#" and url != "#pesterchum":
+ self.parent().parent.showMemos(url[1:])
+ elif url[0] == "@":
+ handle = unicode(url[1:])
+ self.parent().parent.newConversation(handle)
+ else:
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url, QtCore.QUrl.TolerantMode))
QtGui.QTextEdit.mousePressEvent(self, event)
def mouseMoveEvent(self, event):
QtGui.QTextEdit.mouseMoveEvent(self, event)
if self.anchorAt(event.pos()):
if self.viewport().cursor().shape != QtCore.Qt.PointingHandCursor:
- url = self.anchorAt(event.pos())
- if url != "" and url[0] != "#":
- self.viewport().setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.viewport().setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
else:
self.viewport().setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
+
+ def contextMenuEvent(self, event):
+ textMenu = self.createStandardContextMenu()
+ if self.textSelected:
+ self.submitLogAction = QtGui.QAction("Submit to Pesterchum QDB", self)
+ self.connect(self.submitLogAction, QtCore.SIGNAL('triggered()'),
+ self, QtCore.SLOT('submitLog()'))
+ textMenu.addAction(self.submitLogAction)
+ a = textMenu.actions()
+ a[0].setText("Copy Plain Text")
+ a[0].setShortcut(self.tr("Ctrl+C"))
+ textMenu.exec_(event.globalPos())
diff --git a/memos.py b/memos.py
index 36066e1..95e7c38 100644
--- a/memos.py
+++ b/memos.py
@@ -8,7 +8,8 @@ from dataobjs import PesterProfile, Mood, PesterHistory
from generic import PesterIcon, RightClickList, mysteryTime
from convo import PesterConvo, PesterInput, PesterText, PesterTabWindow
from parsetools import convertTags, addTimeInitial, timeProtocol, \
- lexMessage, colorBegin, colorEnd, mecmd
+ lexMessage, colorBegin, colorEnd, mecmd, smiledict
+from logviewer import PesterLogViewer
def delta2txt(d, format="pc"):
@@ -122,14 +123,20 @@ class TimeTracker(list):
def setCurrent(self, timed):
self.current = self.index(timed)
def addRecord(self, timed):
- (temporal, pcf, when) = pcfGrammar(timed - timedelta(0))
+ try:
+ (temporal, pcf, when) = pcfGrammar(timed - timedelta(0))
+ except TypeError:
+ (temporal, pcf, when) = pcfGrammar(mysteryTime())
if pcf == "C" or pcf == "?":
return
if timed in self.timerecord[pcf]:
return
self.timerecord[pcf].append(timed)
def getRecord(self, timed):
- (temporal, pcf, when) = pcfGrammar(timed - timedelta(0))
+ try:
+ (temporal, pcf, when) = pcfGrammar(timed - timedelta(0))
+ except TypeError:
+ (temporal, pcf, when) = pcfGrammar(mysteryTime())
if pcf == "C" or pcf == "?":
return 0
if len(self.timerecord[pcf]) > 1:
@@ -154,7 +161,7 @@ class TimeTracker(list):
timed = self.getTime()
return not self.open[timed]
def getTime(self):
- if self.current >= 0:
+ if self.current >= 0:
return self[self.current]
else:
return None
@@ -163,7 +170,10 @@ class TimeTracker(list):
return self.getGrammarTime(timed)
def getGrammarTime(self, timed):
mytime = timedelta(0)
- (temporal, pcf, when) = pcfGrammar(timed - mytime)
+ try:
+ (temporal, pcf, when) = pcfGrammar(timed - mytime)
+ except TypeError:
+ (temporal, pcf, when) = pcfGrammar(mysteryTime())
if timed == mytime:
return TimeGrammar(temporal, pcf, when, 0)
return TimeGrammar(temporal, pcf, when, self.getRecord(timed))
@@ -234,12 +244,21 @@ _ctag_begin = re.compile(r'')
class MemoText(PesterText):
def __init__(self, theme, parent=None):
QtGui.QTextEdit.__init__(self, parent)
+ if hasattr(self.parent(), 'mainwindow'):
+ self.mainwindow = self.parent().mainwindow
+ else:
+ self.mainwindow = self.parent()
self.initTheme(theme)
self.setReadOnly(True)
self.setMouseTracking(True)
self.textSelected = False
self.connect(self, QtCore.SIGNAL('copyAvailable(bool)'),
self, QtCore.SLOT('textReady(bool)'))
+ self.urls = {}
+ for k in smiledict:
+ self.addAnimation(QtCore.QUrl("smilies/%s" % (smiledict[k])), "smilies/%s" % (smiledict[k]))
+ self.connect(self.mainwindow, QtCore.SIGNAL('animationSetting(bool)'),
+ self, QtCore.SLOT('animateChanged(bool)'))
def initTheme(self, theme):
if theme.has_key("memos/scrollbar"):
@@ -255,6 +274,11 @@ class MemoText(PesterText):
parent = self.parent()
window = parent.mainwindow
me = window.profile()
+ if self.mainwindow.config.animations():
+ for m in self.urls:
+ if convertTags(lexmsg).find(self.urls[m].toString()) != -1:
+ if m.state() == QtGui.QMovie.NotRunning:
+ m.start()
chumdb = window.chumdb
if chum is not me: # SO MUCH WH1T3SP4C3 >:]
if type(lexmsg[0]) is colorBegin: # get color tag
@@ -294,14 +318,19 @@ class MemoText(PesterText):
parent.mainwindow.chatlog.log(parent.channel, joinmsg)
time.openCurrentTime()
+ def makeSafe(msg):
+ if msg.count(" msg.count(""):
+ for i in range(msg.count("")):
+ msg = msg + ""
+ return "" + msg + ""
if type(lexmsg[0]) is mecmd:
memsg = chum.memsg(systemColor, lexmsg, time=time.getGrammar())
window.chatlog.log(parent.channel, memsg)
self.append(convertTags(memsg))
else:
- self.append(convertTags(lexmsg))
+ self.append(makeSafe(convertTags(lexmsg)))
window.chatlog.log(parent.channel, lexmsg)
-
+
def changeTheme(self, theme):
self.initTheme(theme)
def submitLogTitle(self):
@@ -342,6 +371,9 @@ class PesterMemo(PesterConvo):
self.opAction = QtGui.QAction(self.mainwindow.theme["main/menus/rclickchumlist/opuser"], self)
self.connect(self.opAction, QtCore.SIGNAL('triggered()'),
self, QtCore.SLOT('opSelectedUser()'))
+ self.voiceAction = QtGui.QAction(self.mainwindow.theme["main/menus/rclickchumlist/voiceuser"], self)
+ self.connect(self.voiceAction, QtCore.SIGNAL('triggered()'),
+ self, QtCore.SLOT('voiceSelectedUser()'))
self.userlist.optionsMenu.addAction(self.addchumAction)
# ban & op list added if we are op
@@ -350,7 +382,32 @@ class PesterMemo(PesterConvo):
self.quirksOff.setCheckable(True)
self.connect(self.quirksOff, QtCore.SIGNAL('toggled(bool)'),
self, QtCore.SLOT('toggleQuirks(bool)'))
+ self.logchum = QtGui.QAction(self.mainwindow.theme["main/menus/rclickchumlist/viewlog"], self)
+ self.connect(self.logchum, QtCore.SIGNAL('triggered()'),
+ self, QtCore.SLOT('openChumLogs()'))
+ self.invitechum = QtGui.QAction(self.mainwindow.theme["main/menus/rclickchumlist/invitechum"], self)
+ self.connect(self.invitechum, QtCore.SIGNAL('triggered()'),
+ self, QtCore.SLOT('inviteChums()'))
self.optionsMenu.addAction(self.quirksOff)
+ self.optionsMenu.addAction(self.logchum)
+ self.optionsMenu.addAction(self.invitechum)
+
+ self.chanModeMenu = QtGui.QMenu("Memo Settings", self)
+ self.chanHide = QtGui.QAction("Hidden", self)
+ self.chanHide.setCheckable(True)
+ self.connect(self.chanHide, QtCore.SIGNAL('toggled(bool)'),
+ self, QtCore.SLOT('hideChan(bool)'))
+ self.chanInvite = QtGui.QAction("Invite-Only", self)
+ self.chanInvite.setCheckable(True)
+ self.connect(self.chanInvite, QtCore.SIGNAL('toggled(bool)'),
+ self, QtCore.SLOT('inviteChan(bool)'))
+ self.chanMod = QtGui.QAction("Mute", self)
+ self.chanMod.setCheckable(True)
+ self.connect(self.chanMod, QtCore.SIGNAL('toggled(bool)'),
+ self, QtCore.SLOT('modChan(bool)'))
+ self.chanModeMenu.addAction(self.chanHide)
+ self.chanModeMenu.addAction(self.chanInvite)
+ self.chanModeMenu.addAction(self.chanMod)
self.timeslider = TimeSlider(QtCore.Qt.Horizontal, self)
self.timeinput = TimeInput(self.timeslider, self)
@@ -405,7 +462,7 @@ class PesterMemo(PesterConvo):
margins = self.mainwindow.theme["memos/margins"]
self.layout.setContentsMargins(margins["left"], margins["top"],
margins["right"], margins["bottom"])
-
+
self.setLayout(self.layout)
if parent:
@@ -459,23 +516,35 @@ class PesterMemo(PesterConvo):
self.setWindowIcon(PesterIcon(theme["memos/memoicon"]))
t = Template(theme["memos/label/text"])
- self.channelLabel.setText(t.safe_substitute(channel=self.channel))
+ if self.mainwindow.advanced and hasattr(self, 'modes'):
+ self.channelLabel.setText(t.safe_substitute(channel=self.channel) + "(%s)" % (self.modes))
+ else:
+ self.channelLabel.setText(t.safe_substitute(channel=self.channel))
self.channelLabel.setStyleSheet(theme["memos/label/style"])
self.channelLabel.setAlignment(self.aligndict["h"][theme["memos/label/align/h"]] | self.aligndict["v"][theme["memos/label/align/v"]])
self.channelLabel.setMaximumHeight(theme["memos/label/maxheight"])
self.channelLabel.setMinimumHeight(theme["memos/label/minheight"])
self.userlist.optionsMenu.setStyleSheet(theme["main/defaultwindow/style"])
- self.userlist.setStyleSheet(theme["memos/userlist/style"])
+ scrolls = "width: 12px; height: 12px; border: 0; padding: 0;"
+ if theme.has_key("main/chums/scrollbar"):
+ self.userlist.setStyleSheet("QListWidget { %s } QScrollBar { %s } QScrollBar::handle { %s } QScrollBar::add-line { %s } QScrollBar::sub-line { %s } QScrollBar:up-arrow { %s } QScrollBar:down-arrow { %s }" % (theme["memos/userlist/style"], theme["main/chums/scrollbar/style"] + scrolls, theme["main/chums/scrollbar/handle"], theme["main/chums/scrollbar/downarrow"], theme["main/chums/scrollbar/uparrow"], theme["main/chums/scrollbar/uarrowstyle"], theme["main/chums/scrollbar/darrowstyle"] ))
+ elif theme.has_key("convo/scrollbar"):
+ self.userlist.setStyleSheet("QListWidget { %s } QScrollBar { %s } QScrollBar::handle { %s } QScrollBar::add-line { %s } QScrollBar::sub-line { %s } QScrollBar:up-arrow { %s } QScrollBar:down-arrow { %s }" % (theme["memos/userlist/style"], theme["convo/scrollbar/style"] + scrolls, theme["convo/scrollbar/handle"], "display:none;", "display:none;", "display:none;", "display:none;" ))
+ else:
+ self.userlist.setStyleSheet("QListWidget { %s } QScrollBar { %s } QScrollBar::handle { %s }" % (theme["memos/userlist/style"], scrolls, "background-color: black;"))
self.userlist.setFixedWidth(theme["memos/userlist/width"])
self.addchumAction.setText(theme["main/menus/rclickchumlist/addchum"])
self.banuserAction.setText(theme["main/menus/rclickchumlist/banuser"])
self.opAction.setText(theme["main/menus/rclickchumlist/opuser"])
+ self.voiceAction.setText(theme["main/menus/rclickchumlist/voiceuser"])
+ self.quirksOff.setText(theme["main/menus/rclickchumlist/quirksoff"])
+ self.logchum.setText(theme["main/menus/rclickchumlist/viewlog"])
self.timeinput.setFixedWidth(theme["memos/time/text/width"])
self.timeinput.setStyleSheet(theme["memos/time/text/style"])
slidercss = "QSlider { %s } QSlider::groove { %s } QSlider::handle { %s }" % (theme["memos/time/slider/style"], theme["memos/time/slider/groove"], theme["memos/time/slider/handle"])
- self.timeslider.setStyleSheet(slidercss)
+ self.timeslider.setStyleSheet(slidercss)
larrow = PesterIcon(self.mainwindow.theme["memos/time/arrows/left"])
self.timeswitchl.setIcon(larrow)
@@ -501,18 +570,26 @@ class PesterMemo(PesterConvo):
if item.op:
icon = PesterIcon(self.mainwindow.theme["memos/op/icon"])
item.setIcon(icon)
+ elif item.voice:
+ icon = PesterIcon(self.mainwindow.theme["memos/voice/icon"])
+ item.setIcon(icon)
def addUser(self, handle):
chumdb = self.mainwindow.chumdb
defaultcolor = QtGui.QColor("black")
op = False
+ voice = False
if handle[0] == '@':
op = True
handle = handle[1:]
if handle == self.mainwindow.profile().handle:
self.userlist.optionsMenu.addAction(self.opAction)
self.userlist.optionsMenu.addAction(self.banuserAction)
+ self.optionsMenu.addMenu(self.chanModeMenu)
self.op = True
+ elif handle[0] == '+':
+ voice = True
+ handle = handle[1:]
item = QtGui.QListWidgetItem(handle)
if handle == self.mainwindow.profile().handle:
color = self.mainwindow.profile().color
@@ -520,10 +597,52 @@ class PesterMemo(PesterConvo):
color = chumdb.getColor(handle, defaultcolor)
item.setTextColor(color)
item.op = op
+ item.voice = voice
if op:
icon = PesterIcon(self.mainwindow.theme["memos/op/icon"])
item.setIcon(icon)
+ elif voice:
+ icon = PesterIcon(self.mainwindow.theme["memos/voice/icon"])
+ item.setIcon(icon)
self.userlist.addItem(item)
+ self.sortUsers()
+
+ def sortUsers(self):
+ users = []
+ listing = self.userlist.item(0)
+ while listing is not None:
+ users.append(self.userlist.takeItem(0))
+ listing = self.userlist.item(0)
+ users.sort(key=lambda x: ((0 if x.op else 1), (0 if x.voice else 1), x.text()))
+ for u in users:
+ self.userlist.addItem(u)
+
+ def updateChanModes(self, modes):
+ if not hasattr(self, 'modes'): self.modes = ""
+ chanmodes = list(str(self.modes))
+ if chanmodes and chanmodes[0] == "+": chanmodes = chanmodes[1:]
+ modes = str(modes)
+ if modes[0] == "+":
+ for m in modes[1:]:
+ if m not in chanmodes:
+ chanmodes.extend(m)
+ if modes.find("s") >= 0: self.chanHide.setChecked(True)
+ if modes.find("i") >= 0: self.chanInvite.setChecked(True)
+ if modes.find("m") >= 0: self.chanMod.setChecked(True)
+ elif modes[0] == "-":
+ for i in modes[1:]:
+ try:
+ chanmodes.remove(i)
+ except ValueError:
+ pass
+ if modes.find("s") >= 0: self.chanHide.setChecked(False)
+ if modes.find("i") >= 0: self.chanInvite.setChecked(False)
+ if modes.find("m") >= 0: self.chanMod.setChecked(False)
+ chanmodes.sort()
+ self.modes = "+" + "".join(chanmodes)
+ if self.mainwindow.advanced:
+ t = Template(self.mainwindow.theme["memos/label/text"])
+ self.channelLabel.setText(t.safe_substitute(channel=self.channel) + "(%s)" % (self.modes))
def timeUpdate(self, handle, cmd):
window = self.mainwindow
@@ -603,6 +722,29 @@ class PesterMemo(PesterConvo):
self.userlist.clear()
for n in self.mainwindow.namesdb[self.channel]:
self.addUser(n)
+ @QtCore.pyqtSlot(QtCore.QString, QtCore.QString)
+ def modesUpdated(self, channel, modes):
+ c = unicode(channel)
+ if c == self.channel:
+ self.updateChanModes(modes)
+
+ @QtCore.pyqtSlot(QtCore.QString)
+ def closeInviteOnly(self, channel):
+ c = unicode(channel)
+ if c == self.channel:
+ self.disconnect(self.mainwindow, QtCore.SIGNAL('inviteOnlyChan(QString)'),
+ self, QtCore.SLOT('closeInviteOnly(QString)'))
+ if self.parent():
+ print self.channel
+ i = self.parent().tabIndices[self.channel]
+ self.parent().tabClose(i)
+ else:
+ self.close()
+ msgbox = QtGui.QMessageBox()
+ msgbox.setText("%s: Invites only!" % (c))
+ msgbox.setInformativeText("This channel is invite-only. You must get an invitation from someone on the inside before entering.")
+ msgbox.setStandardButtons(QtGui.QMessageBox.Ok)
+ ret = msgbox.exec_()
@QtCore.pyqtSlot(QtCore.QString, QtCore.QString, QtCore.QString)
def userPresentChange(self, handle, channel, update):
@@ -613,12 +755,17 @@ class PesterMemo(PesterConvo):
l = update.split(":")
update = l[0]
op = l[1]
+ reason = l[2]
if update == "nick":
l = h.split(":")
oldnick = l[0]
newnick = l[1]
h = oldnick
- if (update in ["join","left", "kick", "+o"]) \
+ if update[0:1] in ["+", "-"]:
+ l = update.split(":")
+ update = l[0]
+ op = l[1]
+ if (update in ["join","left", "kick", "+o", "-o", "+v", "-v"]) \
and channel != self.channel:
return
chums = self.userlist.findItems(h, QtCore.Qt.MatchFlags(0))
@@ -639,6 +786,18 @@ class PesterMemo(PesterConvo):
self.times[h].removeTime(t.getTime())
if update == "nick":
self.addUser(newnick)
+ newchums = self.userlist.findItems(newnick, QtCore.Qt.MatchFlags(0))
+ for nc in newchums:
+ for c in chums:
+ if c.op:
+ nc.op = True
+ icon = PesterIcon(self.mainwindow.theme["memos/op/icon"])
+ nc.setIcon(icon)
+ if c.voice:
+ nc.voice = True
+ icon = PesterIcon(self.mainwindow.theme["memos/voice/icon"])
+ nc.setIcon(icon)
+ self.sortUsers()
elif update == "kick":
if len(chums) == 0:
return
@@ -661,7 +820,7 @@ class PesterMemo(PesterConvo):
opgrammar = self.time.getGrammar()
else:
opgrammar = TimeGrammar("CURRENT", "C", "RIGHT NOW")
- msg = chum.memobanmsg(opchum, opgrammar, systemColor, grammar)
+ msg = chum.memobanmsg(opchum, opgrammar, systemColor, grammar, reason)
self.textArea.append(convertTags(msg))
self.mainwindow.chatlog.log(self.channel, msg)
ttracker.removeTime(ttracker.getTime())
@@ -698,13 +857,131 @@ class PesterMemo(PesterConvo):
serverText = "PESTERCHUM:TIME>"+delta2txt(time, "server")
self.messageSent.emit(serverText, self.title())
elif update == "+o":
- chums = self.userlist.findItems(h, QtCore.Qt.MatchFlags(0))
+ if self.mainwindow.config.opvoiceMessages():
+ chum = PesterProfile(h)
+ if h == self.mainwindow.profile().handle:
+ chum = self.mainwindow.profile()
+ ttracker = self.time
+ curtime = self.time.getTime()
+ elif self.times.has_key(h):
+ ttracker = self.times[h]
+ else:
+ ttracker = TimeTracker(timedelta(0))
+ opchum = PesterProfile(op)
+ if self.times.has_key(op):
+ opgrammar = self.times[op].getGrammar()
+ elif op == self.mainwindow.profile().handle:
+ opgrammar = self.time.getGrammar()
+ else:
+ opgrammar = TimeGrammar("CURRENT", "C", "RIGHT NOW")
+ msg = chum.memoopmsg(opchum, opgrammar, systemColor)
+ self.textArea.append(convertTags(msg))
+ self.mainwindow.chatlog.log(self.channel, msg)
for c in chums:
+ c.op = True
icon = PesterIcon(self.mainwindow.theme["memos/op/icon"])
c.setIcon(icon)
if unicode(c.text()) == self.mainwindow.profile().handle:
self.userlist.optionsMenu.addAction(self.opAction)
+ self.userlist.optionsMenu.addAction(self.voiceAction)
self.userlist.optionsMenu.addAction(self.banuserAction)
+ self.optionsMenu.addMenu(self.chanModeMenu)
+ self.sortUsers()
+ elif update == "-o":
+ self.mainwindow.channelNames.emit(self.channel)
+ if self.mainwindow.config.opvoiceMessages():
+ chum = PesterProfile(h)
+ if h == self.mainwindow.profile().handle:
+ chum = self.mainwindow.profile()
+ ttracker = self.time
+ curtime = self.time.getTime()
+ elif self.times.has_key(h):
+ ttracker = self.times[h]
+ else:
+ ttracker = TimeTracker(timedelta(0))
+ opchum = PesterProfile(op)
+ if self.times.has_key(op):
+ opgrammar = self.times[op].getGrammar()
+ elif op == self.mainwindow.profile().handle:
+ opgrammar = self.time.getGrammar()
+ else:
+ opgrammar = TimeGrammar("CURRENT", "C", "RIGHT NOW")
+ msg = chum.memodeopmsg(opchum, opgrammar, systemColor)
+ self.textArea.append(convertTags(msg))
+ self.mainwindow.chatlog.log(self.channel, msg)
+ for c in chums:
+ c.op = False
+ if c.voice:
+ icon = PesterIcon(self.mainwindow.theme["memos/voice/icon"])
+ c.setIcon(icon)
+ else:
+ icon = QtGui.QIcon()
+ c.setIcon(icon)
+ if unicode(c.text()) == self.mainwindow.profile().handle:
+ self.userlist.optionsMenu.removeAction(self.opAction)
+ self.userlist.optionsMenu.removeAction(self.voiceAction)
+ self.userlist.optionsMenu.removeAction(self.banuserAction)
+ self.optionsMenu.removeAction(self.chanModeMenu.menuAction())
+ self.sortUsers()
+ elif update == "+v":
+ if self.mainwindow.config.opvoiceMessages():
+ chum = PesterProfile(h)
+ if h == self.mainwindow.profile().handle:
+ chum = self.mainwindow.profile()
+ ttracker = self.time
+ curtime = self.time.getTime()
+ elif self.times.has_key(h):
+ ttracker = self.times[h]
+ else:
+ ttracker = TimeTracker(timedelta(0))
+ opchum = PesterProfile(op)
+ if self.times.has_key(op):
+ opgrammar = self.times[op].getGrammar()
+ elif op == self.mainwindow.profile().handle:
+ opgrammar = self.time.getGrammar()
+ else:
+ opgrammar = TimeGrammar("CURRENT", "C", "RIGHT NOW")
+ msg = chum.memovoicemsg(opchum, opgrammar, systemColor)
+ self.textArea.append(convertTags(msg))
+ self.mainwindow.chatlog.log(self.channel, msg)
+ for c in chums:
+ c.voice = True
+ if not c.op:
+ icon = PesterIcon(self.mainwindow.theme["memos/voice/icon"])
+ c.setIcon(icon)
+ self.sortUsers()
+ elif update == "-v":
+ if self.mainwindow.config.opvoiceMessages():
+ chum = PesterProfile(h)
+ if h == self.mainwindow.profile().handle:
+ chum = self.mainwindow.profile()
+ ttracker = self.time
+ curtime = self.time.getTime()
+ elif self.times.has_key(h):
+ ttracker = self.times[h]
+ else:
+ ttracker = TimeTracker(timedelta(0))
+ opchum = PesterProfile(op)
+ if self.times.has_key(op):
+ opgrammar = self.times[op].getGrammar()
+ elif op == self.mainwindow.profile().handle:
+ opgrammar = self.time.getGrammar()
+ else:
+ opgrammar = TimeGrammar("CURRENT", "C", "RIGHT NOW")
+ msg = chum.memodevoicemsg(opchum, opgrammar, systemColor)
+ self.textArea.append(convertTags(msg))
+ self.mainwindow.chatlog.log(self.channel, msg)
+ for c in chums:
+ c.voice = False
+ if c.op:
+ icon = PesterIcon(self.mainwindow.theme["memos/op/icon"])
+ c.setIcon(icon)
+ else:
+ icon = QtGui.QIcon()
+ c.setIcon(icon)
+ self.sortUsers()
+ elif c == self.channel and h == "" and update[0] in ["+","-"]:
+ self.updateChanModes(update)
@QtCore.pyqtSlot()
def addChumSlot(self):
@@ -717,19 +994,61 @@ class PesterMemo(PesterConvo):
if not self.userlist.currentItem():
return
currentHandle = unicode(self.userlist.currentItem().text())
- self.mainwindow.kickUser.emit(currentHandle, self.channel)
+ (reason, ok) = QtGui.QInputDialog.getText(self, "Ban User", "Enter the reason you are banning this user (optional):")
+ if ok:
+ self.mainwindow.kickUser.emit("%s:%s" % (currentHandle, reason), self.channel)
@QtCore.pyqtSlot()
def opSelectedUser(self):
if not self.userlist.currentItem():
return
currentHandle = unicode(self.userlist.currentItem().text())
self.mainwindow.setChannelMode.emit(self.channel, "+o", currentHandle)
+ @QtCore.pyqtSlot()
+ def voiceSelectedUser(self):
+ if not self.userlist.currentItem():
+ return
+ currentHandle = unicode(self.userlist.currentItem().text())
+ self.mainwindow.setChannelMode.emit(self.channel, "+v", currentHandle)
def resetSlider(self, time, send=True):
self.timeinput.setText(delta2txt(time))
self.timeinput.setSlider()
if send:
self.sendtime()
+ @QtCore.pyqtSlot()
+ def openChumLogs(self):
+ currentChum = self.channel
+ self.mainwindow.chumList.pesterlogviewer = PesterLogViewer(currentChum, self.mainwindow.config, self.mainwindow.theme, self.mainwindow)
+ self.connect(self.mainwindow.chumList.pesterlogviewer, QtCore.SIGNAL('rejected()'),
+ self.mainwindow.chumList, QtCore.SLOT('closeActiveLog()'))
+ self.mainwindow.chumList.pesterlogviewer.show()
+ self.mainwindow.chumList.pesterlogviewer.raise_()
+ self.mainwindow.chumList.pesterlogviewer.activateWindow()
+
+ @QtCore.pyqtSlot()
+ def inviteChums(self):
+ if not hasattr(self, 'invitechums'):
+ self.invitechums = None
+ if not self.invitechums:
+ (chum, ok) = QtGui.QInputDialog.getText(self, "Invite to Chat", "Enter the chumhandle of the user you'd like to invite:")
+ if ok:
+ chum = unicode(chum)
+ self.mainwindow.inviteChum.emit(chum, self.channel)
+ self.invitechums = None
+
+ @QtCore.pyqtSlot(bool)
+ def hideChan(self, on):
+ x = ["-","+"][on]
+ self.mainwindow.setChannelMode.emit(self.channel, x+"s", "")
+ @QtCore.pyqtSlot(bool)
+ def inviteChan(self, on):
+ x = ["-","+"][on]
+ self.mainwindow.setChannelMode.emit(self.channel, x+"i", "")
+ @QtCore.pyqtSlot(bool)
+ def modChan(self, on):
+ x = ["-","+"][on]
+ self.mainwindow.setChannelMode.emit(self.channel, x+"m", "")
+
@QtCore.pyqtSlot()
def sendtime(self):
me = self.mainwindow.profile()
diff --git a/menus.py b/menus.py
index f83fa07..407e2d5 100644
--- a/menus.py
+++ b/menus.py
@@ -1,18 +1,21 @@
from PyQt4 import QtGui, QtCore
import re
-from generic import RightClickList, MultiTextDialog
+from os import remove
+from generic import RightClickList, RightClickTree, MultiTextDialog
from dataobjs import pesterQuirk, PesterProfile
from memos import TimeSlider, TimeInput
+from version import _pcVersion
-class PesterQuirkItem(QtGui.QListWidgetItem):
- def __init__(self, quirk, parent):
- QtGui.QListWidgetItem.__init__(self, parent)
+class PesterQuirkItem(QtGui.QTreeWidgetItem):
+ def __init__(self, quirk):
+ parent = None
+ QtGui.QTreeWidgetItem.__init__(self, parent)
self.quirk = quirk
- self.setText(unicode(quirk))
+ self.setText(0, unicode(quirk))
def update(self, quirk):
self.quirk = quirk
- self.setText(unicode(quirk))
+ self.setText(0, unicode(quirk))
def __lt__(self, quirkitem):
"""Sets the order of quirks if auto-sorted by Qt. Obsolete now."""
if self.quirk.type == "prefix":
@@ -22,98 +25,418 @@ class PesterQuirkItem(QtGui.QListWidgetItem):
return True
else:
return False
-class PesterQuirkList(QtGui.QListWidget):
+class PesterQuirkList(QtGui.QTreeWidget):
def __init__(self, mainwindow, parent):
- QtGui.QListWidget.__init__(self, parent)
+ QtGui.QTreeWidget.__init__(self, parent)
self.resize(400, 200)
# make sure we have access to mainwindow info like profiles
- self.mainwindow = mainwindow
+ self.mainwindow = mainwindow
self.setStyleSheet("background:black; color:white;")
- for q in mainwindow.userprofile.quirks:
- item = PesterQuirkItem(q, self)
- self.addItem(item)
- #self.sortItems()
+ self.connect(self, QtCore.SIGNAL('itemChanged(QTreeWidgetItem *, int)'),
+ self, QtCore.SLOT('changeCheckState()'))
+
+ for q in mainwindow.userprofile.quirks:
+ item = PesterQuirkItem(q)
+ self.addItem(item, False)
+ self.changeCheckState()
+ #self.setDragEnabled(True)
+ #self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
+ self.setDropIndicatorShown(True)
+ self.setSortingEnabled(False)
+ self.setIndentation(15)
+ self.header().hide()
+
+ def addItem(self, item, new=True):
+ item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsEnabled)
+ if item.quirk.on:
+ item.setCheckState(0, 2)
+ else:
+ item.setCheckState(0, 0)
+ if new:
+ curgroup = self.currentItem()
+ if curgroup:
+ if curgroup.parent(): curgroup = curgroup.parent()
+ item.quirk.quirk["group"] = item.quirk.group = curgroup.text(0)
+ found = self.findItems(item.quirk.group, QtCore.Qt.MatchExactly)
+ if len(found) > 0:
+ found[0].addChild(item)
+ else:
+ child_1 = QtGui.QTreeWidgetItem([item.quirk.group])
+ self.addTopLevelItem(child_1)
+ child_1.setFlags(child_1.flags() | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsEnabled)
+ child_1.setChildIndicatorPolicy(QtGui.QTreeWidgetItem.DontShowIndicatorWhenChildless)
+ child_1.setCheckState(0,0)
+ child_1.setExpanded(True)
+ child_1.addChild(item)
+ self.changeCheckState()
def currentQuirk(self):
- return self.item(self.currentRow())
+ if type(self.currentItem()) is PesterQuirkItem:
+ return self.currentItem()
+ else: return None
+ @QtCore.pyqtSlot()
def upShiftQuirk(self):
- i = self.currentRow()
- if i > 0:
- shifted_item = self.takeItem(i)
- self.insertItem(i-1,shifted_item)
- self.setCurrentRow(i-1)
+ found = self.findItems(self.currentItem().text(0), QtCore.Qt.MatchExactly)
+ if len(found): # group
+ i = self.indexOfTopLevelItem(found[0])
+ if i > 0:
+ expand = found[0].isExpanded()
+ shifted_item = self.takeTopLevelItem(i)
+ self.insertTopLevelItem(i-1, shifted_item)
+ shifted_item.setExpanded(expand)
+ self.setCurrentItem(shifted_item)
+ else: # quirk
+ found = self.findItems(self.currentItem().text(0), QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive)
+ for f in found:
+ if not f.isSelected(): continue
+ if not f.parent(): continue
+ i = f.parent().indexOfChild(f)
+ if i > 0: # keep in same group
+ p = f.parent()
+ shifted_item = f.parent().takeChild(i)
+ p.insertChild(i-1, shifted_item)
+ self.setCurrentItem(shifted_item)
+ else: # move to another group
+ j = self.indexOfTopLevelItem(f.parent())
+ if j <= 0: continue
+ shifted_item = f.parent().takeChild(i)
+ self.topLevelItem(j-1).addChild(shifted_item)
+ self.setCurrentItem(shifted_item)
+ self.changeCheckState()
+ @QtCore.pyqtSlot()
def downShiftQuirk(self):
- i = self.currentRow()
- if i < self.count() - 1 and i >= 0:
- shifted_item = self.takeItem(i)
- self.insertItem(i+1,shifted_item)
- self.setCurrentRow(i+1)
+ found = self.findItems(self.currentItem().text(0), QtCore.Qt.MatchExactly)
+ if len(found): # group
+ i = self.indexOfTopLevelItem(found[0])
+ if i < self.topLevelItemCount()-1 and i >= 0:
+ expand = found[0].isExpanded()
+ shifted_item = self.takeTopLevelItem(i)
+ self.insertTopLevelItem(i+1, shifted_item)
+ shifted_item.setExpanded(expand)
+ self.setCurrentItem(shifted_item)
+ else: # quirk
+ found = self.findItems(self.currentItem().text(0), QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive)
+ for f in found:
+ if not f.isSelected(): continue
+ if not f.parent(): continue
+ i = f.parent().indexOfChild(f)
+ if i < f.parent().childCount()-1 and i >= 0:
+ p = f.parent()
+ shifted_item = f.parent().takeChild(i)
+ p.insertChild(i+1, shifted_item)
+ self.setCurrentItem(shifted_item)
+ else:
+ j = self.indexOfTopLevelItem(f.parent())
+ if j >= self.topLevelItemCount()-1 or j < 0: continue
+ shifted_item = f.parent().takeChild(i)
+ self.topLevelItem(j+1).insertChild(0, shifted_item)
+ self.setCurrentItem(shifted_item)
+ self.changeCheckState()
@QtCore.pyqtSlot()
def removeCurrent(self):
- i = self.currentRow()
- if i >= 0:
- self.takeItem(i)
+ i = self.currentItem()
+ found = self.findItems(i.text(0), QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive)
+ for f in found:
+ if not f.isSelected(): continue
+ if not f.parent(): # group
+ msgbox = QtGui.QMessageBox()
+ msgbox.setStyleSheet(self.mainwindow.theme["main/defaultwindow/style"])
+ msgbox.setWindowTitle("WARNING!")
+ msgbox.setInformativeText("Are you sure you want to delete the quirk group: %s" % (f.text(0)))
+ msgbox.setStandardButtons(QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel)
+ ret = msgbox.exec_()
+ if ret == QtGui.QMessageBox.Ok:
+ self.takeTopLevelItem(self.indexOfTopLevelItem(f))
+ else:
+ f.parent().takeChild(f.parent().indexOfChild(f))
+ self.changeCheckState()
-class MispellQuirkDialog(QtGui.QDialog):
+ @QtCore.pyqtSlot()
+ def addQuirkGroup(self):
+ if not hasattr(self, 'addgroupdialog'):
+ self.addgroupdialog = None
+ if not self.addgroupdialog:
+ (gname, ok) = QtGui.QInputDialog.getText(self, "Add Group", "Enter a name for the new quirk group:")
+ if ok:
+ gname = unicode(gname)
+ if re.search("[^A-Za-z0-9_\s]", gname) is not None:
+ msgbox = QtGui.QMessageBox()
+ msgbox.setInformativeText("THIS IS NOT A VALID GROUP NAME")
+ msgbox.setStandardButtons(QtGui.QMessageBox.Ok)
+ ret = msgbox.exec_()
+ self.addgroupdialog = None
+ return
+ found = self.findItems(gname, QtCore.Qt.MatchExactly)
+ if found:
+ msgbox = QtGui.QMessageBox()
+ msgbox.setInformativeText("THIS QUIRK GROUP ALREADY EXISTS")
+ msgbox.setStandardButtons(QtGui.QMessageBox.Ok)
+ ret = msgbox.exec_()
+ return
+ child_1 = QtGui.QTreeWidgetItem([gname])
+ self.addTopLevelItem(child_1)
+ child_1.setFlags(child_1.flags() | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsEnabled)
+ child_1.setChildIndicatorPolicy(QtGui.QTreeWidgetItem.DontShowIndicatorWhenChildless)
+ child_1.setCheckState(0,0)
+ child_1.setExpanded(True)
+
+ self.addgroupdialog = None
+
+ @QtCore.pyqtSlot()
+ def changeCheckState(self):
+ index = self.indexOfTopLevelItem(self.currentItem())
+ if index == -1:
+ for i in range(self.topLevelItemCount()):
+ allChecked = True
+ noneChecked = True
+ for j in range(self.topLevelItem(i).childCount()):
+ if self.topLevelItem(i).child(j).checkState(0):
+ noneChecked = False
+ else:
+ allChecked = False
+ if allChecked: self.topLevelItem(i).setCheckState(0, 2)
+ elif noneChecked: self.topLevelItem(i).setCheckState(0, 0)
+ else: self.topLevelItem(i).setCheckState(0, 1)
+ else:
+ state = self.topLevelItem(index).checkState(0)
+ for j in range(self.topLevelItem(index).childCount()):
+ self.topLevelItem(index).child(j).setCheckState(0, state)
+
+from copy import copy
+from convo import PesterInput, PesterText
+from parsetools import convertTags, lexMessage, splitMessage, mecmd, colorBegin, colorEnd, img2smiley, smiledict
+from dataobjs import pesterQuirks, PesterHistory
+class QuirkTesterWindow(QtGui.QDialog):
def __init__(self, parent):
QtGui.QDialog.__init__(self, parent)
- self.setWindowTitle("MISPELLER")
- layout_1 = QtGui.QHBoxLayout()
- zero = QtGui.QLabel("1%", self)
- hund = QtGui.QLabel("100%", self)
- self.slider = QtGui.QSlider(QtCore.Qt.Horizontal, self)
- self.slider.setMinimum(1)
- self.slider.setMaximum(100)
- self.slider.setValue(50)
- layout_1.addWidget(zero)
- layout_1.addWidget(self.slider)
- layout_1.addWidget(hund)
+ self.parent = parent
+ self.mainwindow = parent.mainwindow
+ self.setStyleSheet(self.mainwindow.theme["main/defaultwindow/style"])
+ self.setWindowTitle("Quirk Tester")
+ self.resize(350,300)
- self.ok = QtGui.QPushButton("OK", self)
- self.ok.setDefault(True)
- self.connect(self.ok, QtCore.SIGNAL('clicked()'),
- self, QtCore.SLOT('accept()'))
- self.cancel = QtGui.QPushButton("CANCEL", self)
- self.connect(self.cancel, QtCore.SIGNAL('clicked()'),
- self, QtCore.SLOT('reject()'))
- layout_ok = QtGui.QHBoxLayout()
- layout_ok.addWidget(self.cancel)
- layout_ok.addWidget(self.ok)
+ self.textArea = PesterText(self.mainwindow.theme, self)
+ self.textInput = PesterInput(self.mainwindow.theme, self)
+ self.textInput.setFocus()
+
+ self.connect(self.textInput, QtCore.SIGNAL('returnPressed()'),
+ self, QtCore.SLOT('sentMessage()'))
+
+ self.chumopen = True
+ self.chum = self.mainwindow.profile()
+ self.history = PesterHistory()
layout_0 = QtGui.QVBoxLayout()
- layout_0.addLayout(layout_1)
- layout_0.addLayout(layout_ok)
-
+ layout_0.addWidget(self.textArea)
+ layout_0.addWidget(self.textInput)
self.setLayout(layout_0)
- def getPercentage(self):
- r = self.exec_()
- if r == QtGui.QDialog.Accepted:
- retval = {"percentage": self.slider.value()}
- return retval
+
+ def clearNewMessage(self):
+ pass
+ @QtCore.pyqtSlot()
+ def sentMessage(self):
+ text = unicode(self.textInput.text())
+ if text == "" or text[0:11] == "PESTERCHUM:":
+ return
+ self.history.add(text)
+ quirks = pesterQuirks(self.parent.testquirks())
+ lexmsg = lexMessage(text)
+ if type(lexmsg[0]) is not mecmd:
+ try:
+ lexmsg = quirks.apply(lexmsg)
+ except Exception, e:
+ msgbox = QtGui.QMessageBox()
+ msgbox.setText("Whoa there! There seems to be a problem.")
+ msgbox.setInformativeText("A quirk seems to be having a problem. (Possibly you're trying to capture a non-existant group?)\n\
+ %s" % e)
+ msgbox.exec_()
+ return
+ lexmsgs = splitMessage(lexmsg)
+
+ for lm in lexmsgs:
+ serverMsg = copy(lm)
+ self.addMessage(lm, True)
+ text = convertTags(serverMsg, "ctag")
+ self.textInput.setText("")
+ def addMessage(self, msg, me=True):
+ if type(msg) in [str, unicode]:
+ lexmsg = lexMessage(msg)
else:
- return None
+ lexmsg = msg
+ if me:
+ chum = self.mainwindow.profile()
+ else:
+ chum = self.chum
+ self.textArea.addMessage(lexmsg, chum)
-class RandomQuirkDialog(MultiTextDialog):
- def __init__(self, parent, values={}):
+ def closeEvent(self, event):
+ self.parent.quirktester = None
+
+class PesterQuirkTypes(QtGui.QDialog):
+ def __init__(self, parent, quirk=None):
QtGui.QDialog.__init__(self, parent)
- self.setWindowTitle("RANDOM QUIRK")
- self.inputs = {}
- layout_1 = QtGui.QHBoxLayout()
- regexpl = QtGui.QLabel("REGEXP:", self)
- self.regexp = QtGui.QLineEdit(values.get("regexp",""), self)
- layout_1.addWidget(regexpl)
- layout_1.addWidget(self.regexp)
- replacewithl = QtGui.QLabel("REPLACE WITH:", self)
+ self.mainwindow = parent.mainwindow
+ self.setStyleSheet(self.mainwindow.theme["main/defaultwindow/style"])
+ self.setWindowTitle("Quirk Wizard")
+ self.resize(500,310)
- layout_2 = QtGui.QVBoxLayout()
+ self.quirk = quirk
+ self.pages = QtGui.QStackedWidget(self)
+
+ self.next = QtGui.QPushButton("Next", self)
+ self.next.setDefault(True)
+ self.connect(self.next, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('nextPage()'))
+ self.back = QtGui.QPushButton("Back", self)
+ self.back.setEnabled(False)
+ self.connect(self.back, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('backPage()'))
+ self.cancel = QtGui.QPushButton("Cancel", self)
+ self.connect(self.cancel, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('reject()'))
+ layout_2 = QtGui.QHBoxLayout()
+ layout_2.setAlignment(QtCore.Qt.AlignRight)
+ layout_2.addWidget(self.back)
+ layout_2.addWidget(self.next)
+ layout_2.addSpacing(5)
+ layout_2.addWidget(self.cancel)
+
+ vr = QtGui.QFrame()
+ vr.setFrameShape(QtGui.QFrame.VLine)
+ vr.setFrameShadow(QtGui.QFrame.Sunken)
+ vr2 = QtGui.QFrame()
+ vr2.setFrameShape(QtGui.QFrame.VLine)
+ vr2.setFrameShadow(QtGui.QFrame.Sunken)
+
+ self.funclist = QtGui.QListWidget(self)
+ self.funclist.setStyleSheet("color: #000000; background-color: #FFFFFF;")
+ self.funclist2 = QtGui.QListWidget(self)
+ self.funclist2.setStyleSheet("color: #000000; background-color: #FFFFFF;")
+
+ from parsetools import quirkloader
+ funcs = [q+")" for q in quirkloader.quirks.keys()]
+ funcs.sort()
+ self.funclist.addItems(funcs)
+ self.funclist2.addItems(funcs)
+
+ self.reloadQuirkFuncButton = QtGui.QPushButton("RELOAD FUNCTIONS", self)
+ self.connect(self.reloadQuirkFuncButton, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('reloadQuirkFuncSlot()'))
+ self.reloadQuirkFuncButton2 = QtGui.QPushButton("RELOAD FUNCTIONS", self)
+ self.connect(self.reloadQuirkFuncButton2, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('reloadQuirkFuncSlot()'))
+
+ self.funclist.setMaximumWidth(160)
+ self.funclist.resize(160,50)
+ self.funclist2.setMaximumWidth(160)
+ self.funclist2.resize(160,50)
+ layout_f = QtGui.QVBoxLayout()
+ layout_f.addWidget(QtGui.QLabel("Available Regexp\nFunctions"))
+ layout_f.addWidget(self.funclist)
+ layout_f.addWidget(self.reloadQuirkFuncButton)
+ layout_g = QtGui.QVBoxLayout()
+ layout_g.addWidget(QtGui.QLabel("Available Regexp\nFunctions"))
+ layout_g.addWidget(self.funclist2)
+ layout_g.addWidget(self.reloadQuirkFuncButton2)
+
+ # Pages
+ # Type select
+ widget = QtGui.QWidget()
+ self.pages.addWidget(widget)
+ layout_select = QtGui.QVBoxLayout(widget)
+ layout_select.setAlignment(QtCore.Qt.AlignTop)
+ self.radios = []
+ self.radios.append(QtGui.QRadioButton("Prefix", self))
+ self.radios.append(QtGui.QRadioButton("Suffix", self))
+ self.radios.append(QtGui.QRadioButton("Simple Replace", self))
+ self.radios.append(QtGui.QRadioButton("Regexp Replace", self))
+ self.radios.append(QtGui.QRadioButton("Random Replace", self))
+ self.radios.append(QtGui.QRadioButton("Mispeller", self))
+
+ layout_select.addWidget(QtGui.QLabel("Select Quirk Type:"))
+ for r in self.radios:
+ layout_select.addWidget(r)
+
+ # Prefix
+ widget = QtGui.QWidget()
+ self.pages.addWidget(widget)
+ layout_prefix = QtGui.QVBoxLayout(widget)
+ layout_prefix.setAlignment(QtCore.Qt.AlignTop)
+ layout_prefix.addWidget(QtGui.QLabel("Prefix"))
layout_3 = QtGui.QHBoxLayout()
+ layout_3.addWidget(QtGui.QLabel("Value:"))
+ layout_3.addWidget(QtGui.QLineEdit())
+ layout_prefix.addLayout(layout_3)
+
+ # Suffix
+ widget = QtGui.QWidget()
+ self.pages.addWidget(widget)
+ layout_suffix = QtGui.QVBoxLayout(widget)
+ layout_suffix.setAlignment(QtCore.Qt.AlignTop)
+ layout_suffix.addWidget(QtGui.QLabel("Suffix"))
+ layout_3 = QtGui.QHBoxLayout()
+ layout_3.addWidget(QtGui.QLabel("Value:"))
+ layout_3.addWidget(QtGui.QLineEdit())
+ layout_suffix.addLayout(layout_3)
+
+ # Simple Replace
+ widget = QtGui.QWidget()
+ self.pages.addWidget(widget)
+ layout_replace = QtGui.QVBoxLayout(widget)
+ layout_replace.setAlignment(QtCore.Qt.AlignTop)
+ layout_replace.addWidget(QtGui.QLabel("Simple Replace"))
+ layout_3 = QtGui.QHBoxLayout()
+ layout_3.addWidget(QtGui.QLabel("Replace:"))
+ layout_3.addWidget(QtGui.QLineEdit())
+ layout_replace.addLayout(layout_3)
+ layout_3 = QtGui.QHBoxLayout()
+ layout_3.addWidget(QtGui.QLabel("With:"))
+ layout_3.addWidget(QtGui.QLineEdit())
+ layout_replace.addLayout(layout_3)
+
+ # Regexp Replace
+ widget = QtGui.QWidget()
+ self.pages.addWidget(widget)
+ layout_all = QtGui.QHBoxLayout(widget)
+ layout_regexp = QtGui.QVBoxLayout()
+ layout_regexp.setAlignment(QtCore.Qt.AlignTop)
+ layout_regexp.addWidget(QtGui.QLabel("Regexp Replace"))
+ layout_3 = QtGui.QHBoxLayout()
+ layout_3.addWidget(QtGui.QLabel("Regexp:"))
+ layout_3.addWidget(QtGui.QLineEdit())
+ layout_regexp.addLayout(layout_3)
+ layout_3 = QtGui.QHBoxLayout()
+ layout_3.addWidget(QtGui.QLabel("Replace With:"))
+ layout_3.addWidget(QtGui.QLineEdit())
+ layout_regexp.addLayout(layout_3)
+ layout_all.addLayout(layout_f)
+ layout_all.addWidget(vr)
+ layout_all.addLayout(layout_regexp)
+
+ # Random Replace
+ widget = QtGui.QWidget()
+ self.pages.addWidget(widget)
+ layout_all = QtGui.QHBoxLayout(widget)
+ layout_random = QtGui.QVBoxLayout()
+ layout_random.setAlignment(QtCore.Qt.AlignTop)
+ layout_random.addWidget(QtGui.QLabel("Random Replace"))
+ layout_5 = QtGui.QHBoxLayout()
+ regexpl = QtGui.QLabel("Regexp:", self)
+ self.regexp = QtGui.QLineEdit("", self)
+ layout_5.addWidget(regexpl)
+ layout_5.addWidget(self.regexp)
+ replacewithl = QtGui.QLabel("Replace With:", self)
+ layout_all.addLayout(layout_g)
+ layout_all.addWidget(vr2)
+ layout_all.addLayout(layout_random)
+
+ layout_6 = QtGui.QVBoxLayout()
+ layout_7 = QtGui.QHBoxLayout()
self.replacelist = QtGui.QListWidget(self)
- for v in values.get("list", []):
- item = QtGui.QListWidgetItem(v, self.replacelist)
self.replaceinput = QtGui.QLineEdit(self)
addbutton = QtGui.QPushButton("ADD", self)
self.connect(addbutton, QtCore.SIGNAL('clicked()'),
@@ -121,42 +444,100 @@ class RandomQuirkDialog(MultiTextDialog):
removebutton = QtGui.QPushButton("REMOVE", self)
self.connect(removebutton, QtCore.SIGNAL('clicked()'),
self, QtCore.SLOT('removeRandomString()'))
- layout_3.addWidget(addbutton)
- layout_3.addWidget(removebutton)
- layout_2.addWidget(self.replacelist)
- layout_2.addWidget(self.replaceinput)
- layout_2.addLayout(layout_3)
- layout_1.addLayout(layout_2)
+ layout_7.addWidget(addbutton)
+ layout_7.addWidget(removebutton)
+ layout_6.addLayout(layout_5)
+ layout_6.addWidget(replacewithl)
+ layout_6.addWidget(self.replacelist)
+ layout_6.addWidget(self.replaceinput)
+ layout_6.addLayout(layout_7)
+ layout_random.addLayout(layout_6)
- self.ok = QtGui.QPushButton("OK", self)
- self.ok.setDefault(True)
- self.connect(self.ok, QtCore.SIGNAL('clicked()'),
- self, QtCore.SLOT('accept()'))
- self.cancel = QtGui.QPushButton("CANCEL", self)
- self.connect(self.cancel, QtCore.SIGNAL('clicked()'),
- self, QtCore.SLOT('reject()'))
- layout_ok = QtGui.QHBoxLayout()
- layout_ok.addWidget(self.cancel)
- layout_ok.addWidget(self.ok)
+ # Misspeller
+ widget = QtGui.QWidget()
+ self.pages.addWidget(widget)
+ layout_mispeller = QtGui.QVBoxLayout(widget)
+ layout_mispeller.setAlignment(QtCore.Qt.AlignTop)
+ layout_mispeller.addWidget(QtGui.QLabel("Mispeller"))
+ layout_1 = QtGui.QHBoxLayout()
+ zero = QtGui.QLabel("1%", self)
+ hund = QtGui.QLabel("100%", self)
+ self.current = QtGui.QLabel("50%", self)
+ self.current.setAlignment(QtCore.Qt.AlignHCenter)
+ self.slider = QtGui.QSlider(QtCore.Qt.Horizontal, self)
+ self.slider.setMinimum(1)
+ self.slider.setMaximum(100)
+ self.slider.setValue(50)
+ self.connect(self.slider, QtCore.SIGNAL('valueChanged(int)'),
+ self, QtCore.SLOT('printValue(int)'))
+ layout_1.addWidget(zero)
+ layout_1.addWidget(self.slider)
+ layout_1.addWidget(hund)
+ layout_mispeller.addLayout(layout_1)
+ layout_mispeller.addWidget(self.current)
layout_0 = QtGui.QVBoxLayout()
- layout_0.addLayout(layout_1)
- layout_0.addLayout(layout_ok)
+ layout_0.addWidget(self.pages)
+ layout_0.addLayout(layout_2)
+
+ if quirk:
+ types = ["prefix","suffix","replace","regexp","random","spelling"]
+ for (i,r) in enumerate(self.radios):
+ if i == types.index(quirk.quirk.type):
+ r.setChecked(True)
+ self.changePage(types.index(quirk.quirk.type)+1)
+ page = self.pages.currentWidget().layout()
+ q = quirk.quirk.quirk
+ if q["type"] in ("prefix","suffix"):
+ page.itemAt(1).layout().itemAt(1).widget().setText(q["value"])
+ elif q["type"] == "replace":
+ page.itemAt(1).layout().itemAt(1).widget().setText(q["from"])
+ page.itemAt(2).layout().itemAt(1).widget().setText(q["to"])
+ elif q["type"] == "regexp":
+ page.itemAt(2).layout().itemAt(1).layout().itemAt(1).widget().setText(q["from"])
+ page.itemAt(2).layout().itemAt(2).layout().itemAt(1).widget().setText(q["to"])
+ elif q["type"] == "random":
+ self.regexp.setText(q["from"])
+ for v in q["randomlist"]:
+ item = QtGui.QListWidgetItem(v, self.replacelist)
+ elif q["type"] == "spelling":
+ self.slider.setValue(q["percentage"])
self.setLayout(layout_0)
- def getText(self):
- r = self.exec_()
- if r == QtGui.QDialog.Accepted:
- randomlist = [unicode(self.replacelist.item(i).text())
- for i in range(0,self.replacelist.count())]
- retval = {"from": unicode(self.regexp.text()),
- "randomlist": randomlist }
- return retval
+ def closeEvent(self, event):
+ self.parent().quirkadd = None
+
+ def changePage(self, page):
+ c = self.pages.count()
+ if page >= c or page < 0: return
+ self.back.setEnabled(page > 0)
+ if page >= 1 and page <= 6:
+ self.next.setText("Finish")
else:
- return None
-
+ self.next.setText("Next")
+ self.pages.setCurrentIndex(page)
+ @QtCore.pyqtSlot()
+ def nextPage(self):
+ if self.next.text() == "Finish":
+ self.accept()
+ return
+ cur = self.pages.currentIndex()
+ if cur == 0:
+ for (i,r) in enumerate(self.radios):
+ if r.isChecked():
+ self.changePage(i+1)
+ else:
+ self.changePage(cur+1)
+ @QtCore.pyqtSlot()
+ def backPage(self):
+ cur = self.pages.currentIndex()
+ if cur >= 1 and cur <= 6:
+ self.changePage(0)
+ @QtCore.pyqtSlot(int)
+ def printValue(self, value):
+ self.current.setText(str(value)+"%")
@QtCore.pyqtSlot()
def addRandomString(self):
text = unicode(self.replaceinput.text())
@@ -171,6 +552,17 @@ class RandomQuirkDialog(MultiTextDialog):
self.replacelist.takeItem(self.replacelist.currentRow())
self.replaceinput.setFocus()
+ @QtCore.pyqtSlot()
+ def reloadQuirkFuncSlot(self):
+ from parsetools import reloadQuirkFunctions, quirkloader
+ reloadQuirkFunctions()
+ funcs = [q+")" for q in quirkloader.quirks.keys()]
+ funcs.sort()
+ self.funclist.clear()
+ self.funclist.addItems(funcs)
+ self.funclist2.clear()
+ self.funclist2.addItems(funcs)
+
class PesterChooseQuirks(QtGui.QDialog):
def __init__(self, config, theme, parent):
QtGui.QDialog.__init__(self, parent)
@@ -183,47 +575,34 @@ class PesterChooseQuirks(QtGui.QDialog):
self.quirkList = PesterQuirkList(self.mainwindow, self)
- self.addPrefixButton = QtGui.QPushButton("ADD PREFIX", self)
- self.connect(self.addPrefixButton, QtCore.SIGNAL('clicked()'),
- self, QtCore.SLOT('addPrefixDialog()'))
- self.addSuffixButton = QtGui.QPushButton("ADD SUFFIX", self)
- self.connect(self.addSuffixButton, QtCore.SIGNAL('clicked()'),
- self, QtCore.SLOT('addSuffixDialog()'))
- self.addSimpleReplaceButton = QtGui.QPushButton("SIMPLE REPLACE", self)
- self.connect(self.addSimpleReplaceButton, QtCore.SIGNAL('clicked()'),
- self, QtCore.SLOT('addSimpleReplaceDialog()'))
- self.addRegexpReplaceButton = QtGui.QPushButton("REGEXP REPLACE", self)
- self.connect(self.addRegexpReplaceButton, QtCore.SIGNAL('clicked()'),
- self, QtCore.SLOT('addRegexpDialog()'))
- self.addRandomReplaceButton = QtGui.QPushButton("RANDOM REPLACE", self)
- self.connect(self.addRandomReplaceButton, QtCore.SIGNAL('clicked()'),
- self, QtCore.SLOT('addRandomDialog()'))
+ self.addQuirkButton = QtGui.QPushButton("ADD QUIRK", self)
+ self.connect(self.addQuirkButton, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('addQuirkDialog()'))
- self.addMispellingButton = QtGui.QPushButton("MISPELLER", self)
- self.connect(self.addMispellingButton, QtCore.SIGNAL('clicked()'),
- self, QtCore.SLOT('addSpellDialog()'))
self.upShiftButton = QtGui.QPushButton("^", self)
self.downShiftButton = QtGui.QPushButton("v", self)
+ self.upShiftButton.setToolTip("Move quirk up one")
+ self.downShiftButton.setToolTip("Move quirk down one")
self.connect(self.upShiftButton, QtCore.SIGNAL('clicked()'),
- self, QtCore.SLOT('upShiftQuirk()'))
+ self.quirkList, QtCore.SLOT('upShiftQuirk()'))
self.connect(self.downShiftButton, QtCore.SIGNAL('clicked()'),
- self, QtCore.SLOT('downShiftQuirk()'))
+ self.quirkList, QtCore.SLOT('downShiftQuirk()'))
+
+ self.newGroupButton = QtGui.QPushButton("*", self)
+ self.newGroupButton.setToolTip("New Quirk Group")
+ self.connect(self.newGroupButton, QtCore.SIGNAL('clicked()'),
+ self.quirkList, QtCore.SLOT('addQuirkGroup()'))
layout_quirklist = QtGui.QHBoxLayout() #the nude layout quirklist
layout_shiftbuttons = QtGui.QVBoxLayout() #the shift button layout
layout_shiftbuttons.addWidget(self.upShiftButton)
+ layout_shiftbuttons.addWidget(self.newGroupButton)
layout_shiftbuttons.addWidget(self.downShiftButton)
layout_quirklist.addWidget(self.quirkList)
layout_quirklist.addLayout(layout_shiftbuttons)
layout_1 = QtGui.QHBoxLayout()
- layout_1.addWidget(self.addPrefixButton)
- layout_1.addWidget(self.addSuffixButton)
- layout_1.addWidget(self.addSimpleReplaceButton)
- layout_2 = QtGui.QHBoxLayout()
- layout_2.addWidget(self.addRegexpReplaceButton)
- layout_2.addWidget(self.addRandomReplaceButton)
- layout_2.addWidget(self.addMispellingButton)
+ layout_1.addWidget(self.addQuirkButton)
self.editSelectedButton = QtGui.QPushButton("EDIT", self)
self.connect(self.editSelectedButton, QtCore.SIGNAL('clicked()'),
@@ -239,167 +618,113 @@ class PesterChooseQuirks(QtGui.QDialog):
self.ok.setDefault(True)
self.connect(self.ok, QtCore.SIGNAL('clicked()'),
self, QtCore.SLOT('accept()'))
+ self.test = QtGui.QPushButton("TEST QUIRKS", self)
+ self.connect(self.test, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('testQuirks()'))
self.cancel = QtGui.QPushButton("CANCEL", self)
self.connect(self.cancel, QtCore.SIGNAL('clicked()'),
self, QtCore.SLOT('reject()'))
layout_ok = QtGui.QHBoxLayout()
layout_ok.addWidget(self.cancel)
+ layout_ok.addWidget(self.test)
layout_ok.addWidget(self.ok)
layout_0 = QtGui.QVBoxLayout()
layout_0.addLayout(layout_quirklist)
layout_0.addLayout(layout_1)
- layout_0.addLayout(layout_2)
+ #layout_0.addLayout(layout_2)
layout_0.addLayout(layout_3)
layout_0.addLayout(layout_ok)
+
self.setLayout(layout_0)
def quirks(self):
- return [self.quirkList.item(i).quirk for i in
- range(0,self.quirkList.count())]
-
- # could probably do away with these and just connect to the relevant methods on the quirk list widget
- @QtCore.pyqtSlot()
- def upShiftQuirk(self):
- self.quirkList.upShiftQuirk()
+ u = []
+ for i in range(self.quirkList.topLevelItemCount()):
+ for j in range(self.quirkList.topLevelItem(i).childCount()):
+ u.append(self.quirkList.topLevelItem(i).child(j).quirk)
+ return u
+ #return [self.quirkList.item(i).quirk for i in range(self.quirkList.count())]
+ def testquirks(self):
+ u = []
+ for i in range(self.quirkList.topLevelItemCount()):
+ for j in range(self.quirkList.topLevelItem(i).childCount()):
+ item = self.quirkList.topLevelItem(i).child(j)
+ if (item.checkState(0) == QtCore.Qt.Checked):
+ u.append(item.quirk)
+ return u
@QtCore.pyqtSlot()
- def downShiftQuirk(self):
- self.quirkList.downShiftQuirk()
- #!!!
+ def testQuirks(self):
+ if not hasattr(self, 'quirktester'):
+ self.quirktester = None
+ if self.quirktester:
+ return
+ self.quirktester = QuirkTesterWindow(self)
+ self.quirktester.show()
+
@QtCore.pyqtSlot()
def editSelected(self):
q = self.quirkList.currentQuirk()
+ if not q: return
quirk = q.quirk
- if quirk.type == "prefix":
- self.addPrefixDialog(q)
- elif quirk.type == "suffix":
- self.addSuffixDialog(q)
- elif quirk.type == "replace":
- self.addSimpleReplaceDialog(q)
- elif quirk.type == "regexp":
- self.addRegexpDialog(q)
- elif quirk.type == "random":
- self.addRandomDialog(q)
- elif quirk.type == "spelling":
- self.addSpellDialog(q)
+ self.addQuirkDialog(q)
@QtCore.pyqtSlot()
- def addPrefixDialog(self, qitem=None):
- d = {"label": "Value:", "inputname": "value" }
- if qitem is not None:
- d["value"] = qitem.quirk.quirk["value"]
- pdict = MultiTextDialog("ENTER PREFIX", self, d).getText()
- if pdict is None:
+ def addQuirkDialog(self, quirk=None):
+ if not hasattr(self, 'quirkadd'):
+ self.quirkadd = None
+ if self.quirkadd:
return
- pdict["type"] = "prefix"
- prefix = pesterQuirk(pdict)
- if qitem is None:
- pitem = PesterQuirkItem(prefix, self.quirkList)
- self.quirkList.addItem(pitem)
- else:
- qitem.update(prefix)
- #self.quirkList.sortItems()
+ self.quirkadd = PesterQuirkTypes(self, quirk)
+ self.connect(self.quirkadd, QtCore.SIGNAL('accepted()'),
+ self, QtCore.SLOT('addQuirk()'))
+ self.connect(self.quirkadd, QtCore.SIGNAL('rejected()'),
+ self, QtCore.SLOT('closeQuirk()'))
+ self.quirkadd.show()
+ @QtCore.pyqtSlot()
+ def addQuirk(self):
+ types = ["prefix","suffix","replace","regexp","random","spelling"]
+ vdict = {}
+ vdict["type"] = types[self.quirkadd.pages.currentIndex()-1]
+ page = self.quirkadd.pages.currentWidget().layout()
+ if vdict["type"] in ("prefix","suffix"):
+ vdict["value"] = unicode(page.itemAt(1).layout().itemAt(1).widget().text())
+ elif vdict["type"] == "replace":
+ vdict["from"] = unicode(page.itemAt(1).layout().itemAt(1).widget().text())
+ vdict["to"] = unicode(page.itemAt(2).layout().itemAt(1).widget().text())
+ elif vdict["type"] == "regexp":
+ vdict["from"] = unicode(page.itemAt(2).layout().itemAt(1).layout().itemAt(1).widget().text())
+ vdict["to"] = unicode(page.itemAt(2).layout().itemAt(2).layout().itemAt(1).widget().text())
+ elif vdict["type"] == "random":
+ vdict["from"] = unicode(self.quirkadd.regexp.text())
+ randomlist = [unicode(self.quirkadd.replacelist.item(i).text())
+ for i in range(0,self.quirkadd.replacelist.count())]
+ vdict["randomlist"] = randomlist
+ elif vdict["type"] == "spelling":
+ vdict["percentage"] = self.quirkadd.slider.value()
- @QtCore.pyqtSlot()
- def addSuffixDialog(self, qitem=None):
- d = {"label": "Value:", "inputname": "value" }
- if qitem is not None:
- d["value"] = qitem.quirk.quirk["value"]
- vdict = MultiTextDialog("ENTER SUFFIX", self, d).getText()
- if vdict is None:
- return
- vdict["type"] = "suffix"
- newquirk = pesterQuirk(vdict)
- if qitem is None:
- item = PesterQuirkItem(newquirk, self.quirkList)
- self.quirkList.addItem(item)
- else:
- qitem.update(newquirk)
- #self.quirkList.sortItems()
+ if vdict["type"] in ("regexp", "random"):
+ try:
+ re.compile(vdict["from"])
+ except re.error, e:
+ quirkWarning = QtGui.QMessageBox(self)
+ quirkWarning.setText("Not a valid regular expression!")
+ quirkWarning.setInformativeText("H3R3S WHY DUMP4SS: %s" % (e))
+ quirkWarning.exec_()
+ self.quirkadd = None
+ return
- @QtCore.pyqtSlot()
- def addSimpleReplaceDialog(self, qitem=None):
- d = [{"label": "Replace:", "inputname": "from"}, {"label": "With:", "inputname": "to"}]
- if qitem is not None:
- d[0]["value"] = qitem.quirk.quirk["from"]
- d[1]["value"] = qitem.quirk.quirk["to"]
- vdict = MultiTextDialog("REPLACE", self, *d).getText()
- if vdict is None:
- return
- vdict["type"] = "replace"
- newquirk = pesterQuirk(vdict)
- if qitem is None:
- item = PesterQuirkItem(newquirk, self.quirkList)
+ quirk = pesterQuirk(vdict)
+ if self.quirkadd.quirk is None:
+ item = PesterQuirkItem(quirk)
self.quirkList.addItem(item)
else:
- qitem.update(newquirk)
- #self.quirkList.sortItems()
-
+ self.quirkadd.quirk.update(quirk)
+ self.quirkadd = None
@QtCore.pyqtSlot()
- def addRegexpDialog(self, qitem=None):
- d = [{"label": "Regexp:", "inputname": "from"}, {"label": "Replace With:", "inputname": "to"}]
- if qitem is not None:
- d[0]["value"] = qitem.quirk.quirk["from"]
- d[1]["value"] = qitem.quirk.quirk["to"]
- vdict = MultiTextDialog("REGEXP REPLACE", self, *d).getText()
- if vdict is None:
- return
- vdict["type"] = "regexp"
- try:
- re.compile(vdict["from"])
- except re.error, e:
- quirkWarning = QtGui.QMessageBox(self)
- quirkWarning.setText("Not a valid regular expression!")
- quirkWarning.setInformativeText("H3R3S WHY DUMP4SS: %s" % (e))
- quirkWarning.exec_()
- return
-
- newquirk = pesterQuirk(vdict)
- if qitem is None:
- item = PesterQuirkItem(newquirk, self.quirkList)
- self.quirkList.addItem(item)
- else:
- qitem.update(newquirk)
- #self.quirkList.sortItems()
- @QtCore.pyqtSlot()
- def addRandomDialog(self, qitem=None):
- values = {}
- if qitem is not None:
- values["list"] = qitem.quirk.quirk["randomlist"]
- values["regexp"] = qitem.quirk.quirk["from"]
- vdict = RandomQuirkDialog(self, values).getText()
- if vdict is None:
- return
- vdict["type"] = "random"
- try:
- re.compile(vdict["from"])
- except re.error, e:
- quirkWarning = QtGui.QMessageBox(self)
- quirkWarning.setText("Not a valid regular expression!")
- quirkWarning.setInformativeText("H3R3S WHY DUMP4SS: %s" % (e))
- quirkWarning.exec_()
- return
- newquirk = pesterQuirk(vdict)
- if qitem is None:
- item = PesterQuirkItem(newquirk, self.quirkList)
- self.quirkList.addItem(item)
- else:
- qitem.update(newquirk)
- #self.quirkList.sortItems()
- @QtCore.pyqtSlot()
- def addSpellDialog(self, qitem=None):
- vdict = MispellQuirkDialog(self).getPercentage()
- if vdict is None:
- return
- vdict["type"] = "spelling"
- newquirk = pesterQuirk(vdict)
- if qitem is None:
- item = PesterQuirkItem(newquirk, self.quirkList)
- self.quirkList.addItem(item)
- else:
- qitem.update(newquirk)
- #self.quirkList.sortItems()
+ def closeQuirk(self):
+ self.quirkadd = None
class PesterChooseTheme(QtGui.QDialog):
def __init__(self, config, theme, parent):
@@ -489,6 +814,10 @@ class PesterChooseProfile(QtGui.QDialog):
self.cancel = QtGui.QPushButton("CANCEL", self)
self.connect(self.cancel, QtCore.SIGNAL('clicked()'),
self, QtCore.SLOT('reject()'))
+ if not collision and avail_profiles:
+ self.delete = QtGui.QPushButton("DELETE", self)
+ self.connect(self.delete, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('deleteProfile()'))
layout_ok = QtGui.QHBoxLayout()
layout_ok.addWidget(self.cancel)
layout_ok.addWidget(self.ok)
@@ -505,6 +834,8 @@ class PesterChooseProfile(QtGui.QDialog):
layout_0.addWidget(profileLabel)
layout_0.addWidget(self.profileBox)
layout_0.addLayout(layout_ok)
+ if not collision and avail_profiles:
+ layout_0.addWidget(self.delete)
layout_0.addLayout(layout_2)
self.errorMsg = QtGui.QLabel(self)
self.errorMsg.setStyleSheet("color:red;")
@@ -536,14 +867,66 @@ class PesterChooseProfile(QtGui.QDialog):
return
self.accept()
+ @QtCore.pyqtSlot()
+ def deleteProfile(self):
+ if self.profileBox and self.profileBox.currentIndex() > 0:
+ handle = unicode(self.profileBox.currentText())
+ if handle == self.parent.profile().handle:
+ problem = QtGui.QMessageBox()
+ problem.setStyleSheet(self.theme["main/defaultwindow/style"])
+ problem.setWindowTitle("Problem!")
+ problem.setInformativeText("You can't delete the profile you're currently using!")
+ problem.setStandardButtons(QtGui.QMessageBox.Ok)
+ problem.exec_()
+ return
+ msgbox = QtGui.QMessageBox()
+ msgbox.setStyleSheet(self.theme["main/defaultwindow/style"])
+ msgbox.setWindowTitle("WARNING!")
+ msgbox.setInformativeText("Are you sure you want to delete the profile: %s" % (handle))
+ msgbox.setStandardButtons(QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel)
+ ret = msgbox.exec_()
+ if ret == QtGui.QMessageBox.Ok:
+ try:
+ remove("profiles/%s.js" % (handle))
+ except OSError:
+ problem = QtGui.QMessageBox()
+ problem.setStyleSheet(self.theme["main/defaultwindow/style"])
+ problem.setWindowTitle("Problem!")
+ problem.setInformativeText("There was a problem deleting the profile: %s" % (handle))
+ problem.setStandardButtons(QtGui.QMessageBox.Ok)
+ problem.exec_()
+
class PesterOptions(QtGui.QDialog):
def __init__(self, config, theme, parent):
QtGui.QDialog.__init__(self, parent)
+ self.setWindowTitle("Options")
self.setModal(False)
self.config = config
self.theme = theme
self.setStyleSheet(self.theme["main/defaultwindow/style"])
+ layout_4 = QtGui.QVBoxLayout()
+
+ hr = QtGui.QFrame()
+ hr.setFrameShape(QtGui.QFrame.HLine)
+ hr.setFrameShadow(QtGui.QFrame.Sunken)
+ vr = QtGui.QFrame()
+ vr.setFrameShape(QtGui.QFrame.VLine)
+ vr.setFrameShadow(QtGui.QFrame.Sunken)
+
+ self.tabs = QtGui.QButtonGroup(self)
+ self.connect(self.tabs, QtCore.SIGNAL('buttonClicked(int)'),
+ self, QtCore.SLOT('changePage(int)'))
+ tabNames = ["Chum List", "Conversations", "Interface", "Sound", "Logging", "Idle/Updates", "Theme"]
+ if parent.advanced: tabNames.append("Advanced")
+ for t in tabNames:
+ button = QtGui.QPushButton(t)
+ self.tabs.addButton(button)
+ layout_4.addWidget(button)
+ button.setCheckable(True)
+ self.tabs.button(-2).setChecked(True)
+ self.pages = QtGui.QStackedWidget(self)
+
self.tabcheck = QtGui.QCheckBox("Tabbed Conversations", self)
if self.config.tabs():
self.tabcheck.setChecked(True)
@@ -552,8 +935,29 @@ class PesterOptions(QtGui.QDialog):
self.hideOffline.setChecked(True)
self.soundcheck = QtGui.QCheckBox("Sounds On", self)
+ self.connect(self.soundcheck, QtCore.SIGNAL('stateChanged(int)'),
+ self, QtCore.SLOT('soundChange(int)'))
+ self.chatsoundcheck = QtGui.QCheckBox("Pester Sounds", self)
+ self.chatsoundcheck.setChecked(self.config.chatSound())
+ self.memosoundcheck = QtGui.QCheckBox("Memo Sounds", self)
+ self.memosoundcheck.setChecked(self.config.memoSound())
+ self.namesoundcheck = QtGui.QCheckBox("Memo Mention (initials)", self)
+ self.namesoundcheck.setChecked(self.config.nameSound())
if self.config.soundOn():
self.soundcheck.setChecked(True)
+ else:
+ self.chatsoundcheck.setEnabled(False)
+ self.memosoundcheck.setEnabled(False)
+ self.namesoundcheck.setEnabled(False)
+ self.volume = QtGui.QSlider(QtCore.Qt.Horizontal, self)
+ self.volume.setMinimum(0)
+ self.volume.setMaximum(100)
+ self.volume.setValue(self.config.volume())
+ self.connect(self.volume, QtCore.SIGNAL('valueChanged(int)'),
+ self, QtCore.SLOT('printValue(int)'))
+ self.currentVol = QtGui.QLabel(str(self.config.volume())+"%", self)
+ self.currentVol.setAlignment(QtCore.Qt.AlignHCenter)
+
self.timestampcheck = QtGui.QCheckBox("Time Stamps", self)
if self.config.showTimeStamps():
@@ -570,6 +974,109 @@ class PesterOptions(QtGui.QDialog):
if self.config.showSeconds():
self.secondscheck.setChecked(True)
+ self.memomessagecheck = QtGui.QCheckBox("Show OP and Voice Messages in Memos", self)
+ if self.config.opvoiceMessages():
+ self.memomessagecheck.setChecked(True)
+
+ self.animationscheck = QtGui.QCheckBox("Use animated smilies", self)
+ if self.config.animations():
+ self.animationscheck.setChecked(True)
+ animateLabel = QtGui.QLabel("(Disable if you leave chats open for LOOOONG periods of time)")
+ font = animateLabel.font()
+ font.setPointSize(8)
+ animateLabel.setFont(font)
+
+ self.userlinkscheck = QtGui.QCheckBox("Disable #Memo and @User Links", self)
+ self.userlinkscheck.setChecked(self.config.disableUserLinks())
+ self.userlinkscheck.setVisible(False)
+
+
+ # Will add ability to turn off groups later
+ #self.groupscheck = QtGui.QCheckBox("Use Groups", self)
+ #self.groupscheck.setChecked(self.config.useGroups())
+ self.showemptycheck = QtGui.QCheckBox("Show Empty Groups", self)
+ self.showemptycheck.setChecked(self.config.showEmptyGroups())
+ self.showonlinenumbers = QtGui.QCheckBox("Show Number of Online Chums", self)
+ self.showonlinenumbers.setChecked(self.config.showOnlineNumbers())
+
+ sortLabel = QtGui.QLabel("Sort Chums")
+ self.sortBox = QtGui.QComboBox(self)
+ self.sortBox.addItem("Alphabetically")
+ self.sortBox.addItem("By Mood")
+ method = self.config.sortMethod()
+ if method >= 0 and method < self.sortBox.count():
+ self.sortBox.setCurrentIndex(method)
+ layout_3 = QtGui.QHBoxLayout()
+ layout_3.addWidget(sortLabel)
+ layout_3.addWidget(self.sortBox, 10)
+
+ self.logpesterscheck = QtGui.QCheckBox("Log all Pesters", self)
+ if self.config.logPesters() & self.config.LOG:
+ self.logpesterscheck.setChecked(True)
+ self.logmemoscheck = QtGui.QCheckBox("Log all Memos", self)
+ if self.config.logMemos() & self.config.LOG:
+ self.logmemoscheck.setChecked(True)
+ self.stamppestercheck = QtGui.QCheckBox("Log Time Stamps for Pesters", self)
+ if self.config.logPesters() & self.config.STAMP:
+ self.stamppestercheck.setChecked(True)
+ self.stampmemocheck = QtGui.QCheckBox("Log Time Stamps for Memos", self)
+ if self.config.logMemos() & self.config.STAMP:
+ self.stampmemocheck.setChecked(True)
+
+ self.idleBox = QtGui.QSpinBox(self)
+ self.idleBox.setStyleSheet("background:#FFFFFF")
+ self.idleBox.setRange(1, 1440)
+ self.idleBox.setValue(self.config.idleTime())
+ layout_5 = QtGui.QHBoxLayout()
+ layout_5.addWidget(QtGui.QLabel("Minutes before Idle:"))
+ layout_5.addWidget(self.idleBox)
+
+ self.updateBox = QtGui.QComboBox(self)
+ self.updateBox.addItem("Once a Day")
+ self.updateBox.addItem("Once a Week")
+ self.updateBox.addItem("Only on Start")
+ self.updateBox.addItem("Never")
+ check = self.config.checkForUpdates()
+ if check >= 0 and check < self.updateBox.count():
+ self.updateBox.setCurrentIndex(check)
+ layout_6 = QtGui.QHBoxLayout()
+ layout_6.addWidget(QtGui.QLabel("Check for\nPesterchum Updates:"))
+ layout_6.addWidget(self.updateBox)
+
+ self.mspaCheck = QtGui.QCheckBox("Check for MSPA Updates", self)
+ self.mspaCheck.setChecked(self.config.checkMSPA())
+
+ if parent.randhandler.running:
+ self.randomscheck = QtGui.QCheckBox("Receive Random Encounters")
+ self.randomscheck.setChecked(parent.userprofile.randoms)
+
+ avail_themes = self.config.availableThemes()
+ self.themeBox = QtGui.QComboBox(self)
+ for (i, t) in enumerate(avail_themes):
+ self.themeBox.addItem(t)
+ if t == theme.name:
+ self.themeBox.setCurrentIndex(i)
+
+ self.buttonOptions = ["Minimize to Taskbar", "Minimize to Tray", "Quit"]
+ self.miniBox = QtGui.QComboBox(self)
+ self.miniBox.addItems(self.buttonOptions)
+ self.miniBox.setCurrentIndex(self.config.minimizeAction())
+ self.closeBox = QtGui.QComboBox(self)
+ self.closeBox.addItems(self.buttonOptions)
+ self.closeBox.setCurrentIndex(self.config.closeAction())
+ layout_mini = QtGui.QHBoxLayout()
+ layout_mini.addWidget(QtGui.QLabel("Minimize"))
+ layout_mini.addWidget(self.miniBox)
+ layout_close = QtGui.QHBoxLayout()
+ layout_close.addWidget(QtGui.QLabel("Close"))
+ layout_close.addWidget(self.closeBox)
+
+ if parent.advanced:
+ self.modechange = QtGui.QLineEdit(self)
+ layout_change = QtGui.QHBoxLayout()
+ layout_change.addWidget(QtGui.QLabel("Change:"))
+ layout_change.addWidget(self.modechange)
+
self.ok = QtGui.QPushButton("OK", self)
self.ok.setDefault(True)
self.connect(self.ok, QtCore.SIGNAL('clicked()'),
@@ -581,17 +1088,129 @@ class PesterOptions(QtGui.QDialog):
layout_2.addWidget(self.cancel)
layout_2.addWidget(self.ok)
+ # Tab layouts
+ # Chum List
+ widget = QtGui.QWidget()
+ layout_chumlist = QtGui.QVBoxLayout(widget)
+ layout_chumlist.setAlignment(QtCore.Qt.AlignTop)
+ layout_chumlist.addWidget(self.hideOffline)
+ #layout_chumlist.addWidget(self.groupscheck)
+ layout_chumlist.addWidget(self.showemptycheck)
+ layout_chumlist.addWidget(self.showonlinenumbers)
+ layout_chumlist.addLayout(layout_3)
+ self.pages.addWidget(widget)
+
+ # Conversations
+ widget = QtGui.QWidget()
+ layout_chat = QtGui.QVBoxLayout(widget)
+ layout_chat.setAlignment(QtCore.Qt.AlignTop)
+ layout_chat.addWidget(self.timestampcheck)
+ layout_chat.addWidget(self.timestampBox)
+ layout_chat.addWidget(self.secondscheck)
+ layout_chat.addWidget(self.memomessagecheck)
+ layout_chat.addWidget(self.animationscheck)
+ layout_chat.addWidget(animateLabel)
+ if parent.randhandler.running:
+ layout_chat.addWidget(self.randomscheck)
+ # Re-enable these when it's possible to disable User and Memo links
+ #layout_chat.addWidget(hr)
+ #layout_chat.addWidget(QtGui.QLabel("User and Memo Links"))
+ #layout_chat.addWidget(self.userlinkscheck)
+ self.pages.addWidget(widget)
+
+ # Interface
+ widget = QtGui.QWidget()
+ layout_interface = QtGui.QVBoxLayout(widget)
+ layout_interface.setAlignment(QtCore.Qt.AlignTop)
+ layout_interface.addWidget(self.tabcheck)
+ layout_interface.addLayout(layout_mini)
+ layout_interface.addLayout(layout_close)
+ self.pages.addWidget(widget)
+
+ # Sound
+ widget = QtGui.QWidget()
+ layout_sound = QtGui.QVBoxLayout(widget)
+ layout_sound.setAlignment(QtCore.Qt.AlignTop)
+ layout_sound.addWidget(self.soundcheck)
+ layout_indent = QtGui.QVBoxLayout()
+ layout_indent.addWidget(self.chatsoundcheck)
+ layout_indent.addWidget(self.memosoundcheck)
+ layout_indent.addWidget(self.namesoundcheck)
+ layout_indent.setContentsMargins(22,0,0,0)
+ layout_sound.addLayout(layout_indent)
+ layout_sound.addSpacing(15)
+ layout_sound.addWidget(QtGui.QLabel("Master Volume:", self))
+ layout_sound.addWidget(self.volume)
+ layout_sound.addWidget(self.currentVol)
+ self.pages.addWidget(widget)
+
+ # Logging
+ widget = QtGui.QWidget()
+ layout_logs = QtGui.QVBoxLayout(widget)
+ layout_logs.setAlignment(QtCore.Qt.AlignTop)
+ layout_logs.addWidget(self.logpesterscheck)
+ layout_logs.addWidget(self.logmemoscheck)
+ layout_logs.addWidget(self.stamppestercheck)
+ layout_logs.addWidget(self.stampmemocheck)
+ self.pages.addWidget(widget)
+
+ # Idle/Updates
+ widget = QtGui.QWidget()
+ layout_idle = QtGui.QVBoxLayout(widget)
+ layout_idle.setAlignment(QtCore.Qt.AlignTop)
+ layout_idle.addLayout(layout_5)
+ layout_idle.addLayout(layout_6)
+ layout_idle.addWidget(self.mspaCheck)
+ self.pages.addWidget(widget)
+
+ # Theme
+ widget = QtGui.QWidget()
+ layout_theme = QtGui.QVBoxLayout(widget)
+ layout_theme.setAlignment(QtCore.Qt.AlignTop)
+ layout_theme.addWidget(QtGui.QLabel("Pick a Theme:"))
+ layout_theme.addWidget(self.themeBox)
+ self.pages.addWidget(widget)
+
+ # Advanced
+ if parent.advanced:
+ widget = QtGui.QWidget()
+ layout_advanced = QtGui.QVBoxLayout(widget)
+ layout_advanced.setAlignment(QtCore.Qt.AlignTop)
+ layout_advanced.addWidget(QtGui.QLabel("Current User Mode: %s" % parent.modes))
+ layout_advanced.addLayout(layout_change)
+ self.pages.addWidget(widget)
+
layout_0 = QtGui.QVBoxLayout()
- layout_0.addWidget(self.tabcheck)
- layout_0.addWidget(self.soundcheck)
- layout_0.addWidget(self.hideOffline)
- layout_0.addWidget(self.timestampcheck)
- layout_0.addWidget(self.timestampBox)
- layout_0.addWidget(self.secondscheck)
+ layout_1 = QtGui.QHBoxLayout()
+ layout_1.addLayout(layout_4)
+ layout_1.addWidget(vr)
+ layout_1.addWidget(self.pages)
+ layout_0.addLayout(layout_1)
+ layout_0.addSpacing(30)
layout_0.addLayout(layout_2)
self.setLayout(layout_0)
+ @QtCore.pyqtSlot(int)
+ def changePage(self, page):
+ self.tabs.button(page).setChecked(True)
+ # What is this, I don't even. qt, fuck
+ page = -page - 2
+ self.pages.setCurrentIndex(page)
+ @QtCore.pyqtSlot(int)
+ def soundChange(self, state):
+ if state == 0:
+ self.chatsoundcheck.setEnabled(False)
+ self.memosoundcheck.setEnabled(False)
+ self.namesoundcheck.setEnabled(False)
+ else:
+ self.chatsoundcheck.setEnabled(True)
+ self.memosoundcheck.setEnabled(True)
+ self.namesoundcheck.setEnabled(True)
+ @QtCore.pyqtSlot(int)
+ def printValue(self, v):
+ self.currentVol.setText(str(v)+"%")
+
class PesterUserlist(QtGui.QDialog):
def __init__(self, config, theme, parent):
QtGui.QDialog.__init__(self, parent)
@@ -690,11 +1309,10 @@ class PesterUserlist(QtGui.QDialog):
pesterChum = QtCore.pyqtSignal(QtCore.QString)
-class MemoListItem(QtGui.QListWidgetItem):
+class MemoListItem(QtGui.QTreeWidgetItem):
def __init__(self, channel, usercount):
- QtGui.QListWidgetItem.__init__(self, None)
+ QtGui.QTreeWidgetItem.__init__(self, [channel, str(usercount)])
self.target = channel
- self.setText(channel + " (" + str(usercount) + ")")
class PesterMemoList(QtGui.QDialog):
def __init__(self, parent, channel=""):
@@ -703,19 +1321,25 @@ class PesterMemoList(QtGui.QDialog):
self.theme = parent.theme
self.mainwindow = parent
self.setStyleSheet(self.theme["main/defaultwindow/style"])
- self.resize(200, 300)
+ self.resize(460, 300)
self.label = QtGui.QLabel("MEMOS")
- self.channelarea = RightClickList(self)
+ self.channelarea = RightClickTree(self)
self.channelarea.setStyleSheet(self.theme["main/chums/style"])
self.channelarea.optionsMenu = QtGui.QMenu(self)
+ self.channelarea.setColumnCount(2)
+ self.channelarea.setHeaderLabels(["Memo", "Users"])
+ self.channelarea.setIndentation(0)
+ self.channelarea.setColumnWidth(0,200)
+ self.channelarea.setColumnWidth(1,10)
self.connect(self.channelarea,
- QtCore.SIGNAL('itemActivated(QListWidgetItem *)'),
- self, QtCore.SLOT('joinActivatedMemo(QListWidgetItem *)'))
+ QtCore.SIGNAL('itemDoubleClicked(QTreeWidgetItem *, int)'),
+ self, QtCore.SLOT('joinActivatedMemo()'))
self.orjoinlabel = QtGui.QLabel("OR MAKE A NEW MEMO:")
self.newmemo = QtGui.QLineEdit(channel, self)
self.secretChannel = QtGui.QCheckBox("HIDDEN CHANNEL?", self)
+ self.inviteChannel = QtGui.QCheckBox("INVITATION ONLY?", self)
self.timelabel = QtGui.QLabel("TIMEFRAME:")
self.timeslider = TimeSlider(QtCore.Qt.Horizontal, self)
@@ -732,15 +1356,23 @@ class PesterMemoList(QtGui.QDialog):
layout_ok.addWidget(self.cancel)
layout_ok.addWidget(self.join)
+ layout_left = QtGui.QVBoxLayout()
+ layout_right = QtGui.QVBoxLayout()
+ layout_right.setAlignment(QtCore.Qt.AlignTop)
layout_0 = QtGui.QVBoxLayout()
- layout_0.addWidget(self.label)
- layout_0.addWidget(self.channelarea)
- layout_0.addWidget(self.orjoinlabel)
- layout_0.addWidget(self.newmemo)
- layout_0.addWidget(self.secretChannel)
- layout_0.addWidget(self.timelabel)
- layout_0.addWidget(self.timeslider)
- layout_0.addWidget(self.timeinput)
+ layout_1 = QtGui.QHBoxLayout()
+ layout_left.addWidget(self.label)
+ layout_left.addWidget(self.channelarea)
+ layout_right.addWidget(self.orjoinlabel)
+ layout_right.addWidget(self.newmemo)
+ layout_right.addWidget(self.secretChannel)
+ layout_right.addWidget(self.inviteChannel)
+ layout_right.addWidget(self.timelabel)
+ layout_right.addWidget(self.timeslider)
+ layout_right.addWidget(self.timeinput)
+ layout_1.addLayout(layout_left)
+ layout_1.addLayout(layout_right)
+ layout_0.addLayout(layout_1)
layout_0.addLayout(layout_ok)
self.setLayout(layout_0)
@@ -753,9 +1385,10 @@ class PesterMemoList(QtGui.QDialog):
def updateChannels(self, channels):
for c in channels:
item = MemoListItem(c[0][1:],c[1])
- item.setTextColor(QtGui.QColor(self.theme["main/chums/userlistcolor"]))
- item.setIcon(QtGui.QIcon(self.theme["memos/memoicon"]))
- self.channelarea.addItem(item)
+ item.setTextColor(0, QtGui.QColor(self.theme["main/chums/userlistcolor"]))
+ item.setTextColor(1, QtGui.QColor(self.theme["main/chums/userlistcolor"]))
+ item.setIcon(0, QtGui.QIcon(self.theme["memos/memoicon"]))
+ self.channelarea.addTopLevelItem(item)
def updateTheme(self, theme):
self.theme = theme
@@ -770,9 +1403,8 @@ class PesterMemoList(QtGui.QDialog):
selectedmemo = self.selectedmemo()
if newmemo or selectedmemo:
self.accept()
- @QtCore.pyqtSlot(QtGui.QListWidgetItem)
- def joinActivatedMemo(self, item):
- self.channelarea.setCurrentItem(item)
+ @QtCore.pyqtSlot()
+ def joinActivatedMemo(self):
self.accept()
@@ -806,9 +1438,64 @@ class LoadingScreen(QtGui.QDialog):
tryAgain = QtCore.pyqtSignal()
-class AboutPesterchum(QtGui.QMessageBox):
+class AboutPesterchum(QtGui.QDialog):
def __init__(self, parent=None):
- QtGui.QMessageBox.__init__(self, parent)
- self.setText("P3ST3RCHUM V. 3.14.1")
- self.setInformativeText("Programming by illuminatedwax (ghostDunk), Kiooeht (evacipatedBox), alGore, art by Grimlive (aquaMarinist). Special thanks to ABT and gamblingGenocider.")
+ QtGui.QDialog.__init__(self, parent)
self.mainwindow = parent
+ self.setStyleSheet(self.mainwindow.theme["main/defaultwindow/style"])
+
+ self.title = QtGui.QLabel("P3ST3RCHUM V. %s" % (_pcVersion))
+ self.credits = QtGui.QLabel("Programming by:\n\
+ illuminatedwax (ghostDunk)\n\
+ Kiooeht (evacipatedBox)\n\
+ alGore\n\
+\n\
+Art by:\n\
+ Grimlive (aquaMarinist)\n\
+ binaryCabalist\n\
+\n\
+Special Thanks:\n\
+ ABT\n\
+ gamblingGenocider\n\
+ Lexi (lexicalNuance)\n\
+ Eco-Mono")
+
+ self.ok = QtGui.QPushButton("OK", self)
+ self.connect(self.ok, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('reject()'))
+
+ layout_0 = QtGui.QVBoxLayout()
+ layout_0.addWidget(self.title)
+ layout_0.addWidget(self.credits)
+ layout_0.addWidget(self.ok)
+
+ self.setLayout(layout_0)
+
+class UpdatePesterchum(QtGui.QDialog):
+ def __init__(self, ver, url, parent=None):
+ QtGui.QDialog.__init__(self, parent)
+ self.url = url
+ self.mainwindow = parent
+ self.setStyleSheet(self.mainwindow.theme["main/defaultwindow/style"])
+ self.setWindowTitle("Pesterchum v%s Update" % (ver))
+ self.setModal(False)
+
+ self.title = QtGui.QLabel("An update to Pesterchum is avaliable!")
+
+ layout_0 = QtGui.QVBoxLayout()
+ layout_0.addWidget(self.title)
+
+ self.ok = QtGui.QPushButton("D0WNL04D N0W", self)
+ self.ok.setDefault(True)
+ self.connect(self.ok, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('accept()'))
+ self.cancel = QtGui.QPushButton("CANCEL", self)
+ self.connect(self.cancel, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('reject()'))
+ layout_2 = QtGui.QHBoxLayout()
+ layout_2.addWidget(self.cancel)
+ layout_2.addWidget(self.ok)
+
+ layout_0.addLayout(layout_2)
+
+ self.setLayout(layout_0)
diff --git a/oyoyo/helpers.py b/oyoyo/helpers.py
index c58f3a0..a300f4f 100644
--- a/oyoyo/helpers.py
+++ b/oyoyo/helpers.py
@@ -38,8 +38,8 @@ def names(cli, *channels):
def channel_list(cli):
cli.send("LIST")
-def kick(cli, handle, channel):
- cli.send("KICK %s %s" % (channel, handle))
+def kick(cli, handle, channel, reason=""):
+ cli.send("KICK %s %s %s" % (channel, handle, reason))
def mode(cli, channel, mode, options=None):
cmd = "MODE %s %s" % (channel, mode)
@@ -76,7 +76,7 @@ def quit(cli, msg='gone'):
cli._end = 1
def user(cli, username, realname=None):
- cli.send("USER", realname or username, cli.host, cli.host,
+ cli.send("USER", realname or username, cli.host, cli.host,
realname or username)
_simple = (
@@ -84,6 +84,7 @@ _simple = (
'part',
'nick',
'notice',
+ 'invite',
)
def _addsimple():
import sys
@@ -106,6 +107,6 @@ def _addNumerics():
m = sys.modules[__name__]
for num, name in ircevents.numeric_events.iteritems():
setattr(m, name, numericcmd(num, name))
-
+
_addNumerics()
diff --git a/oyoyo/ircevents.py b/oyoyo/ircevents.py
index 623530f..6d8969b 100644
--- a/oyoyo/ircevents.py
+++ b/oyoyo/ircevents.py
@@ -23,6 +23,7 @@ numeric_events = {
"003": "created",
"004": "myinfo",
"005": "featurelist", # XXX
+ "010": "toomanypeeps",
"200": "tracelink",
"201": "traceconnecting",
"202": "tracehandshake",
diff --git a/parsetools.py b/parsetools.py
index 1090637..83d9276 100644
--- a/parsetools.py
+++ b/parsetools.py
@@ -5,6 +5,7 @@ from datetime import timedelta
from PyQt4 import QtGui
from generic import mysteryTime
+from pyquirks import PythonQuirks
_ctag_begin = re.compile(r'(?i)')
_gtag_begin = re.compile(r'(?i)')
@@ -16,9 +17,15 @@ _handlere = re.compile(r"(\s|^)(@[A-Za-z0-9_]+)")
_imgre = re.compile(r"""(?i)
""")
_mecmdre = re.compile(r"^(/me|PESTERCHUM:ME)(\S*)")
-_functionre = re.compile(r"(upper\(|lower\(|scramble\(|reverse\(|\)|\\[0-9]+)")
+quirkloader = PythonQuirks()
+_functionre = re.compile(r"%s" % quirkloader.funcre())
_groupre = re.compile(r"\\([0-9]+)")
+def reloadQuirkFunctions():
+ quirkloader.load()
+ global _functionre
+ _functionre = re.compile(r"%s" % quirkloader.funcre())
+
def lexer(string, objlist):
"""objlist is a list: [(objecttype, re),...] list is in order of preference"""
stringlist = [string]
@@ -247,8 +254,8 @@ def splitMessage(msg, format="ctag"):
if len(okmsg) > 0:
output.append(okmsg)
return output
-
-
+
+
def addTimeInitial(string, grammar):
endofi = string.find(":")
@@ -289,7 +296,7 @@ def timeDifference(td):
elif atd < timedelta(0,3600):
if minutes == 1:
timetext = "%d MINUTE %s" % (minutes, when)
- else:
+ else:
timetext = "%d MINUTES %s" % (minutes, when)
elif atd < timedelta(0,3600*100):
if hours == 1 and leftoverminutes == 0:
@@ -300,14 +307,6 @@ def timeDifference(td):
timetext = "%d HOURS %s" % (hours, when)
return timetext
-def upperrep(text):
- return text.upper()
-def lowerrep(text):
- return text.lower()
-def scramblerep(text):
- return "".join(random.sample(text, len(text)))
-def reverserep(text):
- return text[::-1]
def nonerep(text):
return text
@@ -339,8 +338,7 @@ def parseRegexpFunctions(to):
parsed = parseLeaf(nonerep, None)
current = parsed
curi = 0
- functiondict = {"upper(": upperrep, "lower(": lowerrep,
- "scramble(": scramblerep, "reverse(": reverserep}
+ functiondict = quirkloader.quirks
while curi < len(to):
tmp = to[curi:]
mo = _functionre.search(tmp)
@@ -364,7 +362,7 @@ def parseRegexpFunctions(to):
current.append(to[curi:])
curi = len(to)
return parsed
-
+
def img2smiley(string):
string = unicode(string)
@@ -374,7 +372,7 @@ def img2smiley(string):
return string
smiledict = {
- ":rancorous:": "pc_rancorous.gif",
+ ":rancorous:": "pc_rancorous.gif",
":apple:": "apple.gif",
":bathearst:": "bathearst.gif",
":cathearst:": "cathearst.png",
@@ -384,7 +382,7 @@ smiledict = {
":blueghost:": "blueslimer.gif",
":slimer:": "slimer.gif",
":candycorn:": "candycorn.gif",
- ":cheer:": "cheer.gif",
+ ":cheer:": "cheer.gif",
":duhjohn:": "confusedjohn.gif",
":datrump:": "datrump.gif",
":facepalm:": "facepalm.gif",
@@ -424,8 +422,79 @@ smiledict = {
":manipulative:": "manipulative.png",
":vigorous:": "vigorous.png",
":perky:": "perky.png",
- ":acceptant:": "acceptant.png",
+ ":acceptant:": "acceptant.gif",
}
reverse_smiley = dict((v,k) for k, v in smiledict.iteritems())
_smilere = re.compile("|".join(smiledict.keys()))
+
+class ThemeException(Exception):
+ def __init__(self, value):
+ self.parameter = value
+ def __str__(self):
+ return repr(self.parameter)
+
+def themeChecker(theme):
+ needs = ["main/size", "main/icon", "main/windowtitle", "main/style", \
+ "main/background-image", "main/menubar/style", "main/menu/menuitem", \
+ "main/menu/style", "main/menu/selected", "main/close/image", \
+ "main/close/loc", "main/minimize/image", "main/minimize/loc", \
+ "main/menu/loc", "main/menus/client/logviewer", \
+ "main/menus/client/addgroup", "main/menus/client/options", \
+ "main/menus/client/exit", "main/menus/client/userlist", \
+ "main/menus/client/memos", "main/menus/client/import", \
+ "main/menus/client/idle", "main/menus/client/reconnect", \
+ "main/menus/client/_name", "main/menus/profile/quirks", \
+ "main/menus/profile/block", "main/menus/profile/color", \
+ "main/menus/profile/switch", "main/menus/profile/_name", \
+ "main/menus/help/about", "main/menus/help/_name", "main/moodlabel/text", \
+ "main/moodlabel/loc", "main/moodlabel/style", "main/moods", \
+ "main/addchum/style", "main/addchum/text", "main/addchum/size", \
+ "main/addchum/loc", "main/pester/text", "main/pester/size", \
+ "main/pester/loc", "main/block/text", "main/block/size", "main/block/loc", \
+ "main/mychumhandle/label/text", "main/mychumhandle/label/loc", \
+ "main/mychumhandle/label/style", "main/mychumhandle/handle/loc", \
+ "main/mychumhandle/handle/size", "main/mychumhandle/handle/style", \
+ "main/mychumhandle/colorswatch/size", "main/mychumhandle/colorswatch/loc", \
+ "main/defaultmood", "main/chums/size", "main/chums/loc", \
+ "main/chums/style", "main/menus/rclickchumlist/pester", \
+ "main/menus/rclickchumlist/removechum", \
+ "main/menus/rclickchumlist/blockchum", "main/menus/rclickchumlist/viewlog", \
+ "main/menus/rclickchumlist/removegroup", \
+ "main/menus/rclickchumlist/renamegroup", \
+ "main/menus/rclickchumlist/movechum", "convo/size", \
+ "convo/tabwindow/style", "convo/tabs/tabstyle", "convo/tabs/style", \
+ "convo/tabs/selectedstyle", "convo/style", "convo/margins", \
+ "convo/chumlabel/text", "convo/chumlabel/style", "convo/chumlabel/align/h", \
+ "convo/chumlabel/align/v", "convo/chumlabel/maxheight", \
+ "convo/chumlabel/minheight", "main/menus/rclickchumlist/quirksoff", \
+ "main/menus/rclickchumlist/addchum", "main/menus/rclickchumlist/blockchum", \
+ "main/menus/rclickchumlist/unblockchum", \
+ "main/menus/rclickchumlist/viewlog", "main/trollslum/size", \
+ "main/trollslum/style", "main/trollslum/label/text", \
+ "main/trollslum/label/style", "main/menus/profile/block", \
+ "main/chums/moods/blocked/icon", "convo/systemMsgColor", \
+ "convo/textarea/style", "convo/text/beganpester", "convo/text/ceasepester", \
+ "convo/text/blocked", "convo/text/unblocked", "convo/text/blockedmsg", \
+ "convo/text/idle", "convo/input/style", "memos/memoicon", \
+ "memos/textarea/style", "memos/systemMsgColor", "convo/text/joinmemo", \
+ "memos/input/style", "main/menus/rclickchumlist/banuser", \
+ "main/menus/rclickchumlist/opuser", "main/menus/rclickchumlist/voiceuser", \
+ "memos/margins", "convo/text/openmemo", "memos/size", "memos/style", \
+ "memos/label/text", "memos/label/style", "memos/label/align/h", \
+ "memos/label/align/v", "memos/label/maxheight", "memos/label/minheight", \
+ "memos/userlist/style", "memos/userlist/width", "memos/time/text/width", \
+ "memos/time/text/style", "memos/time/arrows/left", \
+ "memos/time/arrows/style", "memos/time/buttons/style", \
+ "memos/time/arrows/right", "memos/op/icon", "memos/voice/icon", \
+ "convo/text/closememo", "convo/text/kickedmemo", \
+ "main/chums/userlistcolor", "main/defaultwindow/style", \
+ "main/chums/moods", "main/chums/moods/chummy/icon", "main/menus/help/help", \
+ "main/menus/help/calsprite", "main/menus/help/nickserv", \
+ "main/menus/rclickchumlist/invitechum", "main/menus/client/randen"]
+
+ for n in needs:
+ try:
+ theme[n]
+ except KeyError:
+ raise ThemeException("Missing theme requirement: %s" % (n))
diff --git a/pesterchum b/pesterchum
index a28ee59..f351cb8 100755
--- a/pesterchum
+++ b/pesterchum
@@ -1,3 +1,3 @@
#!/bin/sh
-python2.6 pesterchum.py
\ No newline at end of file
+python2.6 pesterchum.py $@
diff --git a/pesterchum.js b/pesterchum.js
deleted file mode 100644
index 3fbfbf7..0000000
--- a/pesterchum.js
+++ /dev/null
@@ -1 +0,0 @@
-{"hideOfflineChums": false, "time12Format": true, "tabs": true, "showSeconds": false, "server": "irc.mindfang.org", "soundon": true, "showTimeStamps": false, "chums": ["unknownTraveler", "tentacleTherapist", "vaginalEngineer", "mechanicalSpectacle", "carcinoGeneticist", "schlagzeugGator", "gamblingGenocider", "gardenGnostic", "centaursTesticle", "arachnidsGrip", "grimAuxiliatrix", "remoteBloodbath", "nitroZealist", "greenZephyr", "arsenicCatnip", "cuttlefishCuller", "rageInducer", "gallowsCalibrator", "caligulasAquarium", "terminallyCapricious", "illuminatedWax", "aquaMarinist", "elegantDiversion", "moirailBunp", "uroborosUnbound", "androidTechnician", "midnightSparrow", "apocalypseArisen", "anguillaNuntia", "oilslickOrchid", "pretentiousFantasia", "aquaticMarinist", "lyricalKeraunoscopic", "counterRealist", "ectoBiologist", "percipientPedestrian", "asceticClinician", "doctectiveMiracles", "noSense", "ircMonster", "twinArmageddons", "cannabisHero", "jetRocket", "adiosToreador", "turntechGodhead", "magmaExploiter", "hannaSongstress", "endlessVoid", "grayscaleVisionary", "corruptedInsanity", "stupidlyBrilliant", "artsyGyarados", "obliviousCrafter", "sporadicAgent", "subtleChaotician", "nareSolee", "apostateCourier", "nocturnalTherapist", "herpaDerp", "clockworkUtopia", "digitalSamurai", "astronomicalMaster", "slipshodBrisant", "genialDustbuster", "hyperdriveTyphoon", "magnificentMiser", "gentleRuffian", "riskRepeats", "globalsoftPrika", "globalsoftPirka", "devonianCritter", "lethargicSerpent", "laughingShisa", "bluntInstrument", "sunilaSeed", "bluntInstrument", "nickServ", "ghostBinoculars", "alGore", "evacipatedBox", "acrylicEmulator", "prettyGemmaiden", "calSprite", "fairytalePorn", "brooklynRage", "computerCyanide", "karkatVantas", "spacedKataz", "musikaTriple", "fooBar", "lotusEater"], "defaultprofile": "testProfile", "block": []}
\ No newline at end of file
diff --git a/pesterchum.py b/pesterchum.py
index 280ae31..23cad81 100644
--- a/pesterchum.py
+++ b/pesterchum.py
@@ -1,6 +1,8 @@
# pesterchum
+import version
+version.pcVerCalc()
import logging
-import os, sys
+import os, sys, getopt
import os.path
from datetime import *
from string import Template
@@ -10,23 +12,55 @@ import codecs
import re
import socket
import platform
-from PyQt4 import QtGui, QtCore
-import pygame
-from time import strftime
+from time import strftime, time
+
+missing = []
+try:
+ from PyQt4 import QtGui, QtCore
+except ImportError, e:
+ module = str(e)
+ if module[:16] == "No module named ": missing.append(module[16:])
+ else: print e
+try:
+ import pygame
+except ImportError, e:
+ module = str(e)
+ if module[:16] == "No module named ": missing.append(module[16:])
+ else: print e
+if missing:
+ print "ERROR: The following modules are required for Pesterchum to run and are missing on your system:"
+ for m in missing: print "* "+m
+ exit()
+vnum = QtCore.qVersion()
+major = int(vnum[:vnum.find(".")])
+if vnum.find(".", vnum.find(".")+1) != -1:
+ minor = int(vnum[vnum.find(".")+1:vnum.find(".", vnum.find(".")+1)])
+else:
+ minor = int(vnum[vnum.find(".")+1:])
+if not ((major > 4) or (major == 4 and minor >= 6)):
+ print "ERROR: Pesterchum requires Qt version >= 4.6"
+ print "You currently have version " + vnum + ". Please ungrade Qt"
+ exit()
from menus import PesterChooseQuirks, PesterChooseTheme, \
PesterChooseProfile, PesterOptions, PesterUserlist, PesterMemoList, \
- LoadingScreen, AboutPesterchum
+ LoadingScreen, AboutPesterchum, UpdatePesterchum
from dataobjs import PesterProfile, Mood, pesterQuirk, pesterQuirks
-from generic import PesterIcon, RightClickList, MultiTextDialog, PesterList, CaseInsensitiveDict
+from generic import PesterIcon, RightClickList, RightClickTree, MultiTextDialog, PesterList, CaseInsensitiveDict
from convo import PesterTabWindow, PesterText, PesterInput, PesterConvo
-from parsetools import convertTags, addTimeInitial
+from parsetools import convertTags, addTimeInitial, themeChecker, ThemeException
from memos import PesterMemo, MemoTabWindow, TimeTracker
from irc import PesterIRC
from logviewer import PesterLogUserSelect, PesterLogViewer
+from bugreport import BugReporter
+from randomer import RandomHandler
+from updatecheck import MSPAChecker
_datadir = QtGui.QDesktopServices.storageLocation(QtGui.QDesktopServices.DataLocation)+"Pesterchum/"
-canon_handles = ["apocalypseArisen", "arsenicCatnip", "arachnidsGrip", "adiosToreador", "caligulasAquarium", "cuttlefishCuller", "carcinoGeneticist", "centaursTesticle", "grimAuxiliatrix", "gallowsCalibrator", "gardenGnostic", "ectoBiologist", "twinArmageddons", "terminallyCapricious", "turntechGodhead", "tentacleTherapist"]
+canon_handles = ["apocalypseArisen", "arsenicCatnip", "arachnidsGrip", "adiosToreador", \
+ "caligulasAquarium", "cuttlefishCuller", "carcinoGeneticist", "centaursTesticle", \
+ "grimAuxiliatrix", "gallowsCalibrator", "gardenGnostic", "ectoBiologist", \
+ "twinArmageddons", "terminallyCapricious", "turntechGodhead", "tentacleTherapist"]
if sys.platform == "darwin":
if not os.path.exists(_datadir):
@@ -36,6 +70,15 @@ if sys.platform == "darwin":
if not os.path.exists(_datadir+"pesterchum.js"):
f = open(_datadir+"pesterchum.js", 'w')
f.close()
+else:
+ if not os.path.exists("logs"):
+ os.mkdir("logs")
+ if not os.path.exists("profiles"):
+ os.mkdir("profiles")
+ if not os.path.exists("pesterchum.js"):
+ f = open("pesterchum.js", 'w')
+ f.write("{}")
+ f.close()
class waitingMessageHolder(object):
def __init__(self, mainwindow, **msgfuncs):
@@ -67,10 +110,12 @@ class waitingMessageHolder(object):
class NoneSound(object):
def play(self): pass
+ def set_volume(self, v): pass
class PesterLog(object):
- def __init__(self, handle):
+ def __init__(self, handle, parent=None):
global _datadir
+ self.parent = parent
self.handle = handle
self.convos = {}
if sys.platform != "darwin":
@@ -79,13 +124,28 @@ class PesterLog(object):
self.logpath = _datadir+"logs"
def log(self, handle, msg):
+ if self.parent.config.time12Format():
+ time = strftime("[%I:%M")
+ else:
+ time = strftime("[%H:%M")
+ if self.parent.config.showSeconds():
+ time += strftime(":%S] ")
+ else:
+ time += "] "
+ if handle[0] == '#':
+ if not self.parent.config.logMemos() & self.parent.config.LOG: return
+ if not self.parent.config.logMemos() & self.parent.config.STAMP:
+ time = ""
+ else:
+ if not self.parent.config.logPesters() & self.parent.config.LOG: return
+ if not self.parent.config.logPesters() & self.parent.config.STAMP:
+ time = ""
+ if str(handle).upper() == "NICKSERV": return
#watch out for illegal characters
handle = re.sub(r'[<>:"/\\|?*]', "_", handle)
- #time = strftime("[%H:%M:%S] ")
- # no time codes in logs
- bbcodemsg = convertTags(msg, "bbcode")
- html = convertTags(msg, "html")+"
"
- msg = convertTags(msg, "text")
+ bbcodemsg = time + convertTags(msg, "bbcode")
+ html = time + convertTags(msg, "html")+"
"
+ msg = time +convertTags(msg, "text")
modes = {"bbcode": bbcodemsg, "html": html, "text": msg}
if not self.convos.has_key(handle):
time = datetime.now().strftime("%Y-%m-%d.%H.%M")
@@ -120,6 +180,8 @@ class PesterProfileDB(dict):
else:
self.logpath = _datadir+"logs"
+ if not os.path.exists(self.logpath):
+ os.makedirs(self.logpath)
try:
fp = open("%s/chums.js" % (self.logpath), 'r')
chumdict = json.load(fp)
@@ -135,7 +197,14 @@ class PesterProfileDB(dict):
json.dump(chumdict, fp)
fp.close()
- converted = dict([(handle, PesterProfile(handle, color=QtGui.QColor(c['color']), mood=Mood(c['mood']))) for (handle, c) in chumdict.iteritems()])
+ u = []
+ for (handle, c) in chumdict.iteritems():
+ try:
+ g = c['group']
+ u.append((handle, PesterProfile(handle, color=QtGui.QColor(c['color']), mood=Mood(c['mood']), group=g)))
+ except KeyError:
+ u.append((handle, PesterProfile(handle, color=QtGui.QColor(c['color']), mood=Mood(c['mood']))))
+ converted = dict(u)
self.update(converted)
def save(self):
@@ -156,6 +225,17 @@ class PesterProfileDB(dict):
self[handle].color = color
else:
self[handle] = PesterProfile(handle, color)
+ def getGroup(self, handle, default="Chums"):
+ if not self.has_key(handle):
+ return default
+ else:
+ return self[handle].group
+ def setGroup(self, handle, theGroup):
+ if self.has_key(handle):
+ self[handle].group = theGroup
+ else:
+ self[handle] = PesterProfile(handle, group=theGroup)
+ self.save()
def __setitem__(self, key, val):
dict.__setitem__(self, key, val)
self.save()
@@ -231,7 +311,11 @@ class pesterTheme(dict):
return False
class userConfig(object):
- def __init__(self):
+ def __init__(self, parent):
+ self.parent = parent
+ # Use for bit flag log setting
+ self.LOG = 1
+ self.STAMP = 2
if sys.platform != "darwin":
self.filename = "pesterchum.js"
else:
@@ -243,6 +327,29 @@ class userConfig(object):
self.userprofile = userProfile(self.config["defaultprofile"])
else:
self.userprofile = None
+
+ if sys.platform != "darwin":
+ self.logpath = "logs"
+ else:
+ self.logpath = _datadir+"logs"
+
+ if not os.path.exists(self.logpath):
+ os.makedirs(self.logpath)
+ try:
+ fp = open("%s/groups.js" % (self.logpath), 'r')
+ self.groups = json.load(fp)
+ fp.close()
+ except IOError:
+ self.groups = {}
+ fp = open("%s/groups.js" % (self.logpath), 'w')
+ json.dump(self.groups, fp)
+ fp.close()
+ except ValueError:
+ self.groups = {}
+ fp = open("%s/groups.js" % (self.logpath), 'w')
+ json.dump(self.groups, fp)
+ fp.close()
+
def chums(self):
if not self.config.has_key('chums'):
self.set("chums", [])
@@ -268,6 +375,54 @@ class userConfig(object):
if not self.config.has_key('showSeconds'):
self.set("showSeconds", False)
return self.config.get('showSeconds', False)
+ def sortMethod(self):
+ return self.config.get('sortMethod', 0)
+ def useGroups(self):
+ return self.config.get('useGroups', False)
+ def openDefaultGroup(self):
+ groups = self.getGroups()
+ for g in groups:
+ if g[0] == "Chums":
+ return g[1]
+ return True
+ def showEmptyGroups(self):
+ if not self.config.has_key('emptyGroups'):
+ self.set("emptyGroups", False)
+ return self.config.get('emptyGroups', False)
+ def showOnlineNumbers(self):
+ if not self.config.has_key('onlineNumbers'):
+ self.set("onlineNumbers", False)
+ return self.config.get('onlineNumbers', False)
+ def logPesters(self):
+ return self.config.get('logPesters', self.LOG | self.STAMP)
+ def logMemos(self):
+ return self.config.get('logMemos', self.LOG)
+ def disableUserLinks(self):
+ return not self.config.get('userLinks', True)
+ def idleTime(self):
+ return self.config.get('idleTime', 10)
+ def minimizeAction(self):
+ return self.config.get('miniAction', 0)
+ def closeAction(self):
+ return self.config.get('closeAction', 1)
+ def opvoiceMessages(self):
+ return self.config.get('opvMessages', True)
+ def animations(self):
+ return self.config.get('animations', True)
+ def checkForUpdates(self):
+ u = self.config.get('checkUpdates', 0)
+ if type(u) == type(bool()):
+ if u: u = 2
+ else: u = 3
+ return u
+ # Once a day
+ # Once a week
+ # Only on start
+ # Never
+ def lastUCheck(self):
+ return self.config.get('lastUCheck', 0)
+ def checkMSPA(self):
+ return self.config.get('mspa', False)
def addChum(self, chum):
if chum.handle not in self.chums():
fp = open(self.filename) # what if we have two clients open??
@@ -295,14 +450,67 @@ class userConfig(object):
l = self.getBlocklist()
l.pop(l.index(handle))
self.set('block', l)
+ def getGroups(self):
+ if not self.groups.has_key('groups'):
+ self.saveGroups([["Chums", True]])
+ return self.groups.get('groups', [["Chums", True]])
+ def addGroup(self, group, open=True):
+ l = self.getGroups()
+ exists = False
+ for g in l:
+ if g[0] == group:
+ exists = True
+ break
+ if not exists:
+ l.append([group,open])
+ l.sort()
+ self.saveGroups(l)
+ def delGroup(self, group):
+ l = self.getGroups()
+ i = 0
+ for g in l:
+ if g[0] == group: break
+ i = i+1
+ l.pop(i)
+ l.sort()
+ self.saveGroups(l)
+ def expandGroup(self, group, open=True):
+ l = self.getGroups()
+ for g in l:
+ if g[0] == group:
+ g[1] = open
+ break
+ self.saveGroups(l)
+ def saveGroups(self, groups):
+ self.groups['groups'] = groups
+ try:
+ jsonoutput = json.dumps(self.groups)
+ except ValueError, e:
+ raise e
+ fp = open("%s/groups.js" % (self.logpath), 'w')
+ fp.write(jsonoutput)
+ fp.close()
+
def server(self):
+ if hasattr(self.parent, 'serverOverride'):
+ return self.parent.serverOverride
return self.config.get('server', 'irc.mindfang.org')
def port(self):
+ if hasattr(self.parent, 'portOverride'):
+ return self.parent.portOverride
return self.config.get('port', '6667')
def soundOn(self):
if not self.config.has_key('soundon'):
self.set('soundon', True)
return self.config['soundon']
+ def chatSound(self):
+ return self.config.get('chatSound', True)
+ def memoSound(self):
+ return self.config.get('memoSound', True)
+ def nameSound(self):
+ return self.config.get('nameSound', True)
+ def volume(self):
+ return self.config.get('volume', 100)
def set(self, item, setting):
self.config[item] = setting
try:
@@ -345,6 +553,7 @@ class userProfile(object):
self.chat.mood = Mood(self.theme["main/defaultmood"])
self.lastmood = self.chat.mood.value()
self.quirks = pesterQuirks([])
+ self.randoms = False
else:
fp = open("%s/%s.js" % (self.profiledir, user))
self.userprofile = json.load(fp)
@@ -358,6 +567,9 @@ class userProfile(object):
QtGui.QColor(self.userprofile["color"]),
Mood(self.lastmood))
self.quirks = pesterQuirks(self.userprofile["quirks"])
+ if "randoms" not in self.userprofile:
+ self.userprofile["randoms"] = False
+ self.randoms = self.userprofile["randoms"]
def setMood(self, mood):
self.chat.mood = mood
@@ -373,6 +585,10 @@ class userProfile(object):
self.quirks = quirks
self.userprofile["quirks"] = self.quirks.plainList()
self.save()
+ def setRandom(self, random):
+ self.randoms = random
+ self.userprofile["randoms"] = random
+ self.save()
def getLastMood(self):
return self.lastmood
def setLastMood(self, mood):
@@ -407,13 +623,14 @@ class WMButton(QtGui.QPushButton):
def __init__(self, icon, parent=None):
QtGui.QPushButton.__init__(self, icon, "", parent)
self.setIconSize(icon.realsize())
+ self.resize(icon.realsize())
self.setFlat(True)
self.setStyleSheet("QPushButton { padding: 0px; }")
self.setAutoDefault(False)
-class chumListing(QtGui.QListWidgetItem):
+class chumListing(QtGui.QTreeWidgetItem):
def __init__(self, chum, window):
- QtGui.QListWidgetItem.__init__(self, chum.handle)
+ QtGui.QTreeWidgetItem.__init__(self, [chum.handle])
self.mainwindow = window
self.chum = chum
self.handle = chum.handle
@@ -427,33 +644,40 @@ class chumListing(QtGui.QListWidgetItem):
mood = self.chum.mood
self.mood = mood
icon = self.mood.icon(self.mainwindow.theme)
- self.setIcon(icon)
+ self.setIcon(0, icon)
try:
- self.setTextColor(QtGui.QColor(self.mainwindow.theme["main/chums/moods"][self.mood.name()]["color"]))
+ self.setTextColor(0, QtGui.QColor(self.mainwindow.theme["main/chums/moods"][self.mood.name()]["color"]))
except KeyError:
- self.setTextColor(QtGui.QColor(self.mainwindow.theme["main/chums/moods/chummy/color"]))
+ self.setTextColor(0, QtGui.QColor(self.mainwindow.theme["main/chums/moods/chummy/color"]))
def changeTheme(self, theme):
icon = self.mood.icon(theme)
- self.setIcon(icon)
+ self.setIcon(0, icon)
try:
- self.setTextColor(QtGui.QColor(self.mainwindow.theme["main/chums/moods"][self.mood.name()]["color"]))
+ self.setTextColor(0, QtGui.QColor(self.mainwindow.theme["main/chums/moods"][self.mood.name()]["color"]))
except KeyError:
- self.setTextColor(QtGui.QColor(self.mainwindow.theme["main/chums/moods/chummy/color"]))
+ self.setTextColor(0, QtGui.QColor(self.mainwindow.theme["main/chums/moods/chummy/color"]))
def __lt__(self, cl):
h1 = self.handle.lower()
h2 = cl.handle.lower()
return (h1 < h2)
-class chumArea(RightClickList):
+class chumArea(RightClickTree):
def __init__(self, chums, parent=None):
- QtGui.QListWidget.__init__(self, parent)
+ QtGui.QTreeWidget.__init__(self, parent)
self.mainwindow = parent
theme = self.mainwindow.theme
self.chums = chums
+ gTemp = self.mainwindow.config.getGroups()
+ self.groups = [g[0] for g in gTemp]
+ self.openGroups = [g[1] for g in gTemp]
+ self.showAllGroups(True)
if not self.mainwindow.config.hideOfflineChums():
self.showAllChums()
- self.optionsMenu = QtGui.QMenu(self)
+ if not self.mainwindow.config.showEmptyGroups():
+ self.hideEmptyGroups()
+ self.groupMenu = QtGui.QMenu(self)
self.canonMenu = QtGui.QMenu(self)
+ self.optionsMenu = QtGui.QMenu(self)
self.pester = QtGui.QAction(self.mainwindow.theme["main/menus/rclickchumlist/pester"], self)
self.connect(self.pester, QtCore.SIGNAL('triggered()'),
self, QtCore.SLOT('activateChum()'))
@@ -472,29 +696,158 @@ class chumArea(RightClickList):
self.findalts = QtGui.QAction("Find Alts", self)
self.connect(self.findalts, QtCore.SIGNAL('triggered()'),
self, QtCore.SLOT('findAlts()'))
+ self.removegroup = QtGui.QAction(self.mainwindow.theme["main/menus/rclickchumlist/removegroup"], self)
+ self.connect(self.removegroup, QtCore.SIGNAL('triggered()'),
+ self, QtCore.SLOT('removeGroup()'))
+ self.renamegroup = QtGui.QAction(self.mainwindow.theme["main/menus/rclickchumlist/renamegroup"], self)
+ self.connect(self.renamegroup, QtCore.SIGNAL('triggered()'),
+ self, QtCore.SLOT('renameGroup()'))
self.optionsMenu.addAction(self.pester)
self.optionsMenu.addAction(self.logchum)
self.optionsMenu.addAction(self.blockchum)
self.optionsMenu.addAction(self.removechum)
+ self.moveMenu = QtGui.QMenu(self.mainwindow.theme["main/menus/rclickchumlist/movechum"], self)
+ self.optionsMenu.addMenu(self.moveMenu)
self.optionsMenu.addAction(self.reportchum)
+ self.moveGroupMenu()
+
+ self.groupMenu.addAction(self.renamegroup)
+ self.groupMenu.addAction(self.removegroup)
-
self.canonMenu.addAction(self.pester)
self.canonMenu.addAction(self.logchum)
self.canonMenu.addAction(self.blockchum)
self.canonMenu.addAction(self.removechum)
+ self.canonMenu.addMenu(self.moveMenu)
self.canonMenu.addAction(self.reportchum)
self.canonMenu.addAction(self.findalts)
self.initTheme(theme)
- self.sortItems()
+ #self.sortItems()
+ #self.sortItems(1, QtCore.Qt.AscendingOrder)
+ self.setSortingEnabled(False)
+ self.header().hide()
+ self.setDropIndicatorShown(True)
+ self.setIndentation(4)
+ self.setDragEnabled(True)
+ self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
+ self.setAnimated(True)
+ self.setRootIsDecorated(False)
+
+ self.connect(self, QtCore.SIGNAL('itemDoubleClicked(QTreeWidgetItem *, int)'),
+ self, QtCore.SLOT('expandGroup()'))
+
def getOptionsMenu(self):
- currenthandle = self.currentItem().chum.handle
- if currenthandle in canon_handles:
- return self.canonMenu
+ text = str(self.currentItem().text(0))
+ if text.rfind(" (") != -1:
+ text = text[0:text.rfind(" (")]
+ if text == "Chums":
+ return None
+ elif text in self.groups:
+ return self.groupMenu
else:
- return self.optionsMenu
+ currenthandle = self.currentItem().chum.handle
+ if currenthandle in canon_handles:
+ return self.canonMenu
+ else:
+ return self.optionsMenu
+
+ def startDrag(self, dropAction):
+ # create mime data object
+ mime = QtCore.QMimeData()
+ mime.setData('application/x-item', '???')
+ # start drag
+ drag = QtGui.QDrag(self)
+ drag.setMimeData(mime)
+ drag.start(QtCore.Qt.MoveAction)
+
+ def dragMoveEvent(self, event):
+ if event.mimeData().hasFormat("application/x-item"):
+ event.setDropAction(QtCore.Qt.MoveAction)
+ event.accept()
+ else:
+ event.ignore()
+
+ def dragEnterEvent(self, event):
+ if (event.mimeData().hasFormat('application/x-item')):
+ event.accept()
+ else:
+ event.ignore()
+
+ def dropEvent(self, event):
+ if (event.mimeData().hasFormat('application/x-item')):
+ event.acceptProposedAction()
+ else:
+ event.ignore()
+ return
+ thisitem = str(event.source().currentItem().text(0))
+ if thisitem.rfind(" ") != -1:
+ thisitem = thisitem[0:thisitem.rfind(" ")]
+ if thisitem == "Chums" or thisitem in self.groups:
+ droppos = self.itemAt(event.pos())
+ if not droppos: return
+ droppos = str(droppos.text(0))
+ if droppos.rfind(" ") != -1:
+ droppos = droppos[0:droppos.rfind(" ")]
+ if droppos == "Chums" or droppos in self.groups:
+ saveOpen = event.source().currentItem().isExpanded()
+ saveDrop = self.itemAt(event.pos())
+ saveItem = self.takeTopLevelItem(self.indexOfTopLevelItem(event.source().currentItem()))
+ self.insertTopLevelItems(self.indexOfTopLevelItem(saveDrop)+1, [saveItem])
+ if saveOpen:
+ saveItem.setExpanded(True)
+
+ gTemp = []
+ for i in range(self.topLevelItemCount()):
+ text = str(self.topLevelItem(i).text(0))
+ if text.rfind(" (") != -1:
+ text = text[0:text.rfind(" (")]
+ gTemp.append([unicode(text), self.topLevelItem(i).isExpanded()])
+ self.mainwindow.config.saveGroups(gTemp)
+ else:
+ item = self.itemAt(event.pos())
+ if item:
+ text = str(item.text(0))
+ if text.rfind(" (") != -1:
+ text = text[0:text.rfind(" (")]
+ if text == "Chums" or text in self.groups:
+ group = text
+ else:
+ ptext = str(item.parent().text(0))
+ if ptext.rfind(" ") != -1:
+ ptext = ptext[0:ptext.rfind(" ")]
+ group = ptext
+ chumLabel = event.source().currentItem()
+ chumLabel.chum.group = group
+ self.mainwindow.chumdb.setGroup(chumLabel.chum.handle, group)
+ self.takeItem(chumLabel)
+ self.addItem(chumLabel)
+ if self.mainwindow.config.showOnlineNumbers():
+ self.showOnlineNumbers()
+
+ def moveGroupMenu(self):
+ currentGroup = self.currentItem()
+ if currentGroup:
+ if currentGroup.parent():
+ text = str(currentGroup.parent().text(0))
+ else:
+ text = str(currentGroup.text(0))
+ if text.rfind(" (") != -1:
+ text = text[0:text.rfind(" (")]
+ currentGroup = text
+ self.moveMenu.clear()
+ actGroup = QtGui.QActionGroup(self)
+
+ groups = self.groups[:]
+ for gtext in groups:
+ if gtext == currentGroup:
+ continue
+ movegroup = self.moveMenu.addAction(gtext)
+ actGroup.addAction(movegroup)
+ self.connect(actGroup, QtCore.SIGNAL('triggered(QAction *)'),
+ self, QtCore.SLOT('moveToGroup(QAction *)'))
+
def addChum(self, chum):
if len([c for c in self.chums if c.handle == chum.handle]) != 0:
return
@@ -503,29 +856,163 @@ class chumArea(RightClickList):
chum.mood.name() == "offline"):
chumLabel = chumListing(chum, self.mainwindow)
self.addItem(chumLabel)
- self.sortItems()
+ #self.topLevelItem(0).addChild(chumLabel)
+ #self.topLevelItem(0).sortChildren(0, QtCore.Qt.AscendingOrder)
def getChums(self, handle):
- chums = self.findItems(handle, QtCore.Qt.MatchFlags(0))
+ chums = self.findItems(handle, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive)
return chums
def showAllChums(self):
for c in self.chums:
chandle = c.handle
- if not self.findItems(chandle, QtCore.Qt.MatchFlags(0)):
+ if not len(self.findItems(chandle, QtCore.Qt.MatchContains | QtCore.Qt.MatchRecursive)):
chumLabel = chumListing(c, self.mainwindow)
self.addItem(chumLabel)
- self.sortItems()
+ self.sort()
def hideOfflineChums(self):
+ for j in range(self.topLevelItemCount()):
+ i = 0
+ listing = self.topLevelItem(j).child(i)
+ while listing is not None:
+ if listing.chum.mood.name() == "offline":
+ self.topLevelItem(j).takeChild(i)
+ else:
+ i += 1
+ listing = self.topLevelItem(j).child(i)
+ self.sort()
+ def showAllGroups(self, first=False):
+ if first:
+ for i,g in enumerate(self.groups):
+ child_1 = QtGui.QTreeWidgetItem(["%s" % (g)])
+ self.addTopLevelItem(child_1)
+ if self.openGroups[i]:
+ child_1.setExpanded(True)
+ return
+ curgroups = []
+ for i in range(self.topLevelItemCount()):
+ text = str(self.topLevelItem(i).text(0))
+ if text.rfind(" (") != -1:
+ text = text[0:text.rfind(" (")]
+ curgroups.append(text)
+ for i,g in enumerate(self.groups):
+ if g not in curgroups:
+ child_1 = QtGui.QTreeWidgetItem(["%s" % (g)])
+ j = 0
+ for h in self.groups:
+ if h == g:
+ self.insertTopLevelItem(j, child_1)
+ break
+ if h in curgroups:
+ j += 1
+ if self.openGroups[i]:
+ child_1.setExpanded(True)
+ if self.mainwindow.config.showOnlineNumbers():
+ self.showOnlineNumbers()
+ def showOnlineNumbers(self):
+ if hasattr(self, 'groups'):
+ self.hideOnlineNumbers()
+ totals = {'Chums': 0}
+ online = {'Chums': 0}
+ for g in self.groups:
+ totals[str(g)] = 0
+ online[str(g)] = 0
+ for c in self.chums:
+ yes = c.mood.name() != "offline"
+ if c.group == "Chums":
+ totals[str(c.group)] = totals[str(c.group)]+1
+ if yes:
+ online[str(c.group)] = online[str(c.group)]+1
+ elif c.group in totals:
+ totals[str(c.group)] = totals[str(c.group)]+1
+ if yes:
+ online[str(c.group)] = online[str(c.group)]+1
+ else:
+ totals["Chums"] = totals["Chums"]+1
+ if yes:
+ online["Chums"] = online["Chums"]+1
+ for i in range(self.topLevelItemCount()):
+ text = str(self.topLevelItem(i).text(0))
+ if text.rfind(" (") != -1:
+ text = text[0:text.rfind(" (")]
+ self.topLevelItem(i).setText(0, "%s (%i/%i)" % (text, online[text], totals[text]))
+ def hideOnlineNumbers(self):
+ for i in range(self.topLevelItemCount()):
+ text = str(self.topLevelItem(i).text(0))
+ if text.rfind(" (") != -1:
+ text = text[0:text.rfind(" (")]
+ self.topLevelItem(i).setText(0, "%s" % (text))
+ def hideEmptyGroups(self):
i = 0
- listing = self.item(i)
+ listing = self.topLevelItem(i)
while listing is not None:
- if listing.chum.mood.name() == "offline":
- self.takeItem(i)
+ if listing.childCount() == 0:
+ self.takeTopLevelItem(i)
else:
i += 1
- listing = self.item(i)
- self.sortItems()
+ listing = self.topLevelItem(i)
+ @QtCore.pyqtSlot()
+ def expandGroup(self):
+ item = self.currentItem()
+ text = str(item.text(0))
+ if text.rfind(" (") != -1:
+ text = text[0:text.rfind(" (")]
+
+ if text in self.groups:
+ expand = item.isExpanded()
+ self.mainwindow.config.expandGroup(text, not expand)
+ def addItem(self, chumLabel):
+ if hasattr(self, 'groups'):
+ if chumLabel.chum.group not in self.groups:
+ chumLabel.chum.group = "Chums"
+ if "Chums" not in self.groups:
+ self.mainwindow.config.addGroup("Chums")
+ curgroups = []
+ for i in range(self.topLevelItemCount()):
+ text = str(self.topLevelItem(i).text(0))
+ if text.rfind(" (") != -1:
+ text = text[0:text.rfind(" (")]
+ curgroups.append(text)
+ if not self.findItems(chumLabel.handle, QtCore.Qt.MatchContains | QtCore.Qt.MatchRecursive):
+ if chumLabel.chum.group not in curgroups:
+ child_1 = QtGui.QTreeWidgetItem(["%s" % (chumLabel.chum.group)])
+ i = 0
+ for g in self.groups:
+ if g == chumLabel.chum.group:
+ self.insertTopLevelItem(i, child_1)
+ break
+ if g in curgroups:
+ i += 1
+ if self.openGroups[self.groups.index("%s" % (chumLabel.chum.group))]:
+ child_1.setExpanded(True)
+ for i in range(self.topLevelItemCount()):
+ text = str(self.topLevelItem(i).text(0))
+ if text.rfind(" (") != -1:
+ text = text[0:text.rfind(" (")]
+ if text == chumLabel.chum.group:
+ break
+ self.topLevelItem(i).addChild(chumLabel)
+ self.sort()
+ if self.mainwindow.config.showOnlineNumbers():
+ self.showOnlineNumbers()
+ else: # usually means this is now the trollslum
+ if not self.findItems(chumLabel.handle, QtCore.Qt.MatchContains | QtCore.Qt.MatchRecursive):
+ self.topLevelItem(0).addChild(chumLabel)
+ self.topLevelItem(0).sortChildren(0, QtCore.Qt.AscendingOrder)
+ def takeItem(self, chumLabel):
+ r = None
+ if not hasattr(chumLabel, 'chum'):
+ return r
+ for i in range(self.topLevelItemCount()):
+ for j in range(self.topLevelItem(i).childCount()):
+ if self.topLevelItem(i).child(j).text(0) == chumLabel.chum.handle:
+ r = self.topLevelItem(i).takeChild(j)
+ break
+ if not self.mainwindow.config.showEmptyGroups():
+ self.hideEmptyGroups()
+ if self.mainwindow.config.showOnlineNumbers():
+ self.showOnlineNumbers()
+ return r
def updateMood(self, handle, mood):
hideoff = self.mainwindow.config.hideOfflineChums()
chums = self.getChums(handle)
@@ -536,16 +1023,26 @@ class chumArea(RightClickList):
handle in [p.handle for p in self.chums]:
newLabel = chumListing([p for p in self.chums if p.handle == handle][0], self.mainwindow)
self.addItem(newLabel)
- self.sortItems()
+ #self.sortItems()
chums = [newLabel]
elif mood.name() == "offline" and \
len(chums) > 0:
for c in chums:
- self.takeItem(self.row(c))
+ if (hasattr(c, 'mood')):
+ c.setMood(mood)
+ self.takeItem(c)
chums = []
for c in chums:
- oldmood = c.mood
- c.setMood(mood)
+ if (hasattr(c, 'mood')):
+ oldmood = c.mood
+ c.setMood(mood)
+ if self.mainwindow.config.sortMethod() == 1:
+ for i in range(self.topLevelItemCount()):
+ saveCurrent = self.currentItem()
+ self.moodSort(i)
+ self.setCurrentItem(saveCurrent)
+ if self.mainwindow.config.showOnlineNumbers():
+ self.showOnlineNumbers()
return oldmood
def updateColor(self, handle, color):
chums = self.findItems(handle, QtCore.Qt.MatchFlags(0))
@@ -562,14 +1059,47 @@ class chumArea(RightClickList):
self.removechum.setText(theme["main/menus/rclickchumlist/removechum"])
self.blockchum.setText(theme["main/menus/rclickchumlist/blockchum"])
self.logchum.setText(theme["main/menus/rclickchumlist/viewlog"])
+ self.removegroup.setText(theme["main/menus/rclickchumlist/removegroup"])
+ self.renamegroup.setText(theme["main/menus/rclickchumlist/renamegroup"])
+ self.moveMenu.setTitle(theme["main/menus/rclickchumlist/movechum"])
def changeTheme(self, theme):
self.initTheme(theme)
- chumlistings = [self.item(i) for i in range(0, self.count())]
+ chumlistings = []
+ for i in range(self.topLevelItemCount()):
+ for j in range(self.topLevelItem(i).childCount()):
+ chumlistings.append(self.topLevelItem(i).child(j))
+ #chumlistings = [self.item(i) for i in range(0, self.count())]
for c in chumlistings:
c.changeTheme(theme)
+
+ def count(self):
+ c = 0
+ for i in range(self.topLevelItemCount()):
+ c = c + self.topLevelItem(i).childCount()
+ return c
+
+ def sort(self):
+ if self.mainwindow.config.sortMethod() == 1:
+ for i in range(self.topLevelItemCount()):
+ self.moodSort(i)
+ else:
+ for i in range(self.topLevelItemCount()):
+ self.topLevelItem(i).sortChildren(0, QtCore.Qt.AscendingOrder)
+ def moodSort(self, group):
+ scrollPos = self.verticalScrollBar().sliderPosition()
+ chums = []
+ listing = self.topLevelItem(group).child(0)
+ while listing is not None:
+ chums.append(self.topLevelItem(group).takeChild(0))
+ listing = self.topLevelItem(group).child(0)
+ chums.sort(key=lambda x: ((999 if x.chum.mood.value() == 2 else x.chum.mood.value()), x.chum.handle), reverse=False)
+ for c in chums:
+ self.topLevelItem(group).addChild(c)
+ self.verticalScrollBar().setSliderPosition(scrollPos)
+
@QtCore.pyqtSlot()
def activateChum(self):
- self.itemActivated.emit(self.currentItem())
+ self.itemActivated.emit(self.currentItem(), 0)
@QtCore.pyqtSlot()
def removeChum(self, handle = None):
if handle:
@@ -581,7 +1111,7 @@ class chumArea(RightClickList):
currentChum = self.currentItem().chum
self.chums = [c for c in self.chums if c.handle != currentChum.handle]
self.removeChumSignal.emit(self.currentItem().chum.handle)
- oldlist = self.takeItem(self.currentRow())
+ oldlist = self.takeItem(self.currentItem())
del oldlist
@QtCore.pyqtSlot()
def blockChum(self):
@@ -603,7 +1133,7 @@ class chumArea(RightClickList):
self.mainwindow.sendMessage.emit("ALT %s" % (currentChum.chum.handle) , "calSprite")
@QtCore.pyqtSlot()
def openChumLogs(self):
- currentChum = self.currentItem().text()
+ currentChum = self.currentItem().text(0)
if not currentChum:
return
self.pesterlogviewer = PesterLogViewer(currentChum, self.mainwindow.config, self.mainwindow.theme, self.mainwindow)
@@ -618,20 +1148,67 @@ class chumArea(RightClickList):
self.pesterlogviewer = None
@QtCore.pyqtSlot()
def renameGroup(self):
- (gname, ok) = QtGui.QInputDialog.getText(self, "Rename Group", "Enter a new name for the group:")
- if ok:
- pass
- #rename group
-
+ if not hasattr(self, 'renamegroupdialog'):
+ self.renamegroupdialog = None
+ if not self.renamegroupdialog:
+ (gname, ok) = QtGui.QInputDialog.getText(self, "Rename Group", "Enter a new name for the group:")
+ if ok:
+ gname = unicode(gname)
+ currentGroup = self.currentItem()
+ if not currentGroup:
+ return
+ index = self.indexOfTopLevelItem(currentGroup)
+ if index != -1:
+ expanded = currentGroup.isExpanded()
+ text = str(currentGroup.text(0))
+ if text.rfind(" (") != -1:
+ text = text[0:text.rfind(" (")]
+ self.mainwindow.config.delGroup(text)
+ self.mainwindow.config.addGroup(gname, expanded)
+ gTemp = self.mainwindow.config.getGroups()
+ self.groups = [g[0] for g in gTemp]
+ self.openGroups = [g[1] for g in gTemp]
+ for i in range(currentGroup.childCount()):
+ currentGroup.child(i).chum.group = gname
+ self.mainwindow.chumdb.setGroup(currentGroup.child(i).chum.handle, gname)
+ currentGroup.setText(0, gname)
+ if self.mainwindow.config.showOnlineNumbers():
+ self.showOnlineNumbers()
+ self.renamegroupdialog = None
@QtCore.pyqtSlot()
def removeGroup(self):
- pass
- #remove group
-
+ currentGroup = self.currentItem()
+ if not currentGroup:
+ return
+ text = str(currentGroup.text(0))
+ if text.rfind(" (") != -1:
+ text = text[0:text.rfind(" (")]
+ self.mainwindow.config.delGroup(text)
+ gTemp = self.mainwindow.config.getGroups()
+ self.groups = [g[0] for g in gTemp]
+ self.openGroups = [g[1] for g in gTemp]
+ for i in range(self.topLevelItemCount()):
+ if self.topLevelItem(i).text(0) == currentGroup.text(0):
+ break
+ while self.topLevelItem(i) and self.topLevelItem(i).child(0):
+ chumLabel = self.topLevelItem(i).child(0)
+ chumLabel.chum.group = "Chums"
+ self.mainwindow.chumdb.setGroup(chumLabel.chum.handle, "Chums")
+ self.takeItem(chumLabel)
+ self.addItem(chumLabel)
+ self.takeTopLevelItem(i)
@QtCore.pyqtSlot(QtGui.QAction)
def moveToGroup(self, item):
- pass
- #move to group
+ if not item:
+ return
+ group = str(item.text())
+ chumLabel = self.currentItem()
+ if not chumLabel:
+ return
+ chumLabel.chum.group = group
+ self.mainwindow.chumdb.setGroup(chumLabel.chum.handle, group)
+ self.takeItem(chumLabel)
+ self.addItem(chumLabel)
removeChumSignal = QtCore.pyqtSignal(QtCore.QString)
blockChumSignal = QtCore.pyqtSignal(QtCore.QString)
@@ -643,19 +1220,34 @@ class trollSlum(chumArea):
theme = self.mainwindow.theme
self.setStyleSheet(theme["main/trollslum/chumroll/style"])
self.chums = trolls
+ child_1 = QtGui.QTreeWidgetItem([""])
+ self.addTopLevelItem(child_1)
+ child_1.setExpanded(True)
for c in self.chums:
chandle = c.handle
if not self.findItems(chandle, QtCore.Qt.MatchFlags(0)):
chumLabel = chumListing(c, self.mainwindow)
self.addItem(chumLabel)
+ self.setSortingEnabled(False)
+ self.header().hide()
+ self.setDropIndicatorShown(False)
+ self.setIndentation(0)
+
self.optionsMenu = QtGui.QMenu(self)
self.unblockchum = QtGui.QAction(self.mainwindow.theme["main/menus/rclickchumlist/unblockchum"], self)
self.connect(self.unblockchum, QtCore.SIGNAL('triggered()'),
self, QtCore.SIGNAL('unblockChumSignal()'))
self.optionsMenu.addAction(self.unblockchum)
- self.sortItems()
+ #self.sortItems()
+ def contextMenuEvent(self, event):
+ #fuckin Qt
+ if event.reason() == QtGui.QContextMenuEvent.Mouse:
+ listing = self.itemAt(event.pos())
+ self.setCurrentItem(listing)
+ if self.currentItem().text(0) != "":
+ self.optionsMenu.popup(event.globalPos())
def changeTheme(self, theme):
self.setStyleSheet(theme["main/trollslum/chumroll/style"])
self.removechum.setText(theme["main/menus/rclickchumlist/removechum"])
@@ -718,7 +1310,7 @@ class TrollSlumWindow(QtGui.QFrame):
@QtCore.pyqtSlot()
def removeCurrentTroll(self):
currentListing = self.trollslum.currentItem()
- if not currentListing:
+ if not currentListing or not hasattr(currentListing, 'chum'):
return
self.unblockChumSignal.emit(currentListing.chum.handle)
@QtCore.pyqtSlot()
@@ -845,26 +1437,45 @@ class MovingWindow(QtGui.QFrame):
class PesterWindow(MovingWindow):
- def __init__(self, parent=None):
- MovingWindow.__init__(self, parent,
- (QtCore.Qt.CustomizeWindowHint |
+ def __init__(self, options, parent=None):
+ MovingWindow.__init__(self, parent,
+ (QtCore.Qt.CustomizeWindowHint |
QtCore.Qt.FramelessWindowHint))
self.convos = CaseInsensitiveDict()
self.memos = CaseInsensitiveDict()
self.tabconvo = None
self.tabmemo = None
+ if "advanced" in options:
+ self.advanced = options["advanced"]
+ else: self.advanced = False
+ if "server" in options:
+ self.serverOverride = options["server"]
+ if "port" in options:
+ self.portOverride = options["port"]
self.setAutoFillBackground(True)
self.setObjectName("main")
- self.config = userConfig()
+ self.config = userConfig(self)
if self.config.defaultprofile():
self.userprofile = userProfile(self.config.defaultprofile())
self.theme = self.userprofile.getTheme()
else:
self.userprofile = userProfile(PesterProfile("pesterClient%d" % (random.randint(100,999)), QtGui.QColor("black"), Mood(0)))
self.theme = self.userprofile.getTheme()
+ self.modes = ""
- self.chatlog = PesterLog(self.profile().handle)
+ self.randhandler = RandomHandler(self)
+
+ try:
+ themeChecker(self.theme)
+ except ThemeException, (inst):
+ print "Caught: "+inst.parameter
+ themeWarning = QtGui.QMessageBox(self)
+ themeWarning.setText("Theme Error: %s" % (inst))
+ themeWarning.exec_()
+ self.theme = pesterTheme("pesterchum")
+
+ self.chatlog = PesterLog(self.profile().handle, self)
self.move(100, 100)
@@ -872,6 +1483,13 @@ class PesterWindow(MovingWindow):
self.logv = logv
self.connect(logv, QtCore.SIGNAL('triggered()'),
self, QtCore.SLOT('openLogv()'))
+ grps = QtGui.QAction(self.theme["main/menus/client/addgroup"], self)
+ self.grps = grps
+ self.connect(grps, QtCore.SIGNAL('triggered()'),
+ self, QtCore.SLOT('addGroupWindow()'))
+ self.rand = QtGui.QAction(self.theme["main/menus/client/randen"], self)
+ self.connect(self.rand, QtCore.SIGNAL('triggered()'),
+ self.randhandler, QtCore.SLOT('getEncounter()'))
opts = QtGui.QAction(self.theme["main/menus/client/options"], self)
self.opts = opts
self.connect(opts, QtCore.SIGNAL('triggered()'),
@@ -900,22 +1518,22 @@ class PesterWindow(MovingWindow):
self, QtCore.SIGNAL('reconnectIRC()'))
self.menu = QtGui.QMenuBar(self)
+ self.menu.setNativeMenuBar(False)
filemenu = self.menu.addMenu(self.theme["main/menus/client/_name"])
self.filemenu = filemenu
filemenu.addAction(opts)
filemenu.addAction(memoaction)
filemenu.addAction(logv)
+ if self.randhandler.running:
+ filemenu.addAction(self.rand)
filemenu.addAction(userlistaction)
filemenu.addAction(self.idleaction)
+ filemenu.addAction(grps)
filemenu.addAction(self.importaction)
filemenu.addAction(self.reconnectAction)
filemenu.addAction(exitaction)
- changetheme = QtGui.QAction(self.theme["main/menus/profile/theme"], self)
- self.changetheme = changetheme
- self.connect(changetheme, QtCore.SIGNAL('triggered()'),
- self, QtCore.SLOT('pickTheme()'))
changequirks = QtGui.QAction(self.theme["main/menus/profile/quirks"], self)
self.changequirks = changequirks
self.connect(changequirks, QtCore.SIGNAL('triggered()'),
@@ -937,33 +1555,38 @@ class PesterWindow(MovingWindow):
profilemenu = self.menu.addMenu(self.theme["main/menus/profile/_name"])
self.profilemenu = profilemenu
- profilemenu.addAction(changetheme)
profilemenu.addAction(changequirks)
profilemenu.addAction(loadslum)
profilemenu.addAction(changecoloraction)
profilemenu.addAction(switch)
+ self.helpAction = QtGui.QAction(self.theme["main/menus/help/help"], self)
+ self.connect(self.helpAction, QtCore.SIGNAL('triggered()'),
+ self, QtCore.SLOT('launchHelp()'))
+ self.botAction = QtGui.QAction(self.theme["main/menus/help/calsprite"], self)
+ self.connect(self.botAction, QtCore.SIGNAL('triggered()'),
+ self, QtCore.SLOT('loadCalsprite()'))
+ self.nickServAction = QtGui.QAction(self.theme["main/menus/help/nickserv"], self)
+ self.connect(self.nickServAction, QtCore.SIGNAL('triggered()'),
+ self, QtCore.SLOT('loadNickServ()'))
self.aboutAction = QtGui.QAction(self.theme["main/menus/help/about"], self)
self.connect(self.aboutAction, QtCore.SIGNAL('triggered()'),
self, QtCore.SLOT('aboutPesterchum()'))
- self.botAction = QtGui.QAction("CALSPRITE", self)
- self.connect(self.botAction, QtCore.SIGNAL('triggered()'),
- self, QtCore.SLOT('loadCalsprite()'))
- self.helpAction = QtGui.QAction("HELP", self)
- self.connect(self.helpAction, QtCore.SIGNAL('triggered()'),
- self, QtCore.SLOT('launchHelp()'))
+ self.reportBugAction = QtGui.QAction("REPORT BUG", self)
+ self.connect(self.reportBugAction, QtCore.SIGNAL('triggered()'),
+ self, QtCore.SLOT('reportBug()'))
helpmenu = self.menu.addMenu(self.theme["main/menus/help/_name"])
self.helpmenu = helpmenu
self.helpmenu.addAction(self.helpAction)
self.helpmenu.addAction(self.botAction)
+ self.helpmenu.addAction(self.nickServAction)
self.helpmenu.addAction(self.aboutAction)
+ self.helpmenu.addAction(self.reportBugAction)
self.closeButton = WMButton(PesterIcon(self.theme["main/close/image"]), self)
- self.connect(self.closeButton, QtCore.SIGNAL('clicked()'),
- self, QtCore.SLOT('closeToTray()'))
+ self.setButtonAction(self.closeButton, self.config.closeAction(), -1)
self.miniButton = WMButton(PesterIcon(self.theme["main/minimize/image"]), self)
- self.connect(self.miniButton, QtCore.SIGNAL('clicked()'),
- self, QtCore.SLOT('showMinimized()'))
+ self.setButtonAction(self.miniButton, self.config.minimizeAction(), -1)
self.namesdb = CaseInsensitiveDict()
self.chumdb = PesterProfileDB()
@@ -971,7 +1594,7 @@ class PesterWindow(MovingWindow):
chums = [PesterProfile(c, chumdb=self.chumdb) for c in set(self.config.chums())]
self.chumList = chumArea(chums, self)
self.connect(self.chumList,
- QtCore.SIGNAL('itemActivated(QListWidgetItem *)'),
+ QtCore.SIGNAL('itemActivated(QTreeWidgetItem *, int)'),
self,
QtCore.SLOT('pesterSelectedChum()'))
self.connect(self.chumList,
@@ -1010,7 +1633,7 @@ class PesterWindow(MovingWindow):
self.waitingMessages = waitingMessageHolder(self)
self.autoidle = False
- self.idlethreshold = 600
+ self.idlethreshold = 60*self.config.idleTime()
self.idletimer = QtCore.QTimer(self)
self.idleposition = QtGui.QCursor.pos()
self.idletime = 0
@@ -1021,9 +1644,52 @@ class PesterWindow(MovingWindow):
if not self.config.defaultprofile():
self.changeProfile()
+ QtCore.QTimer.singleShot(1000, self, QtCore.SLOT('mspacheck()'))
+
+ self.connect(self, QtCore.SIGNAL('pcUpdate(QString, QString)'),
+ self, QtCore.SLOT('updateMsg(QString, QString)'))
+
+ self.pingtimer = QtCore.QTimer()
+ self.connect(self.pingtimer, QtCore.SIGNAL('timeout()'),
+ self, QtCore.SLOT('checkPing()'))
+ self.lastping = int(time())
+ self.pingtimer.start(1000*10)
+
+ @QtCore.pyqtSlot()
+ def mspacheck(self):
+ checker = MSPAChecker(self)
+
+ @QtCore.pyqtSlot(QtCore.QString, QtCore.QString)
+ def updateMsg(self, ver, url):
+ if not hasattr(self, 'updatemenu'):
+ self.updatemenu = None
+ if not self.updatemenu:
+ self.updatemenu = UpdatePesterchum(ver, url, self)
+ self.connect(self.updatemenu, QtCore.SIGNAL('accepted()'),
+ self, QtCore.SLOT('updatePC()'))
+ self.connect(self.updatemenu, QtCore.SIGNAL('rejected()'),
+ self, QtCore.SLOT('noUpdatePC()'))
+ self.updatemenu.show()
+ self.updatemenu.raise_()
+ self.updatemenu.activateWindow()
+
+ @QtCore.pyqtSlot()
+ def updatePC(self):
+ QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.updatemenu.url, QtCore.QUrl.TolerantMode))
+ self.updatemenu = None
+ @QtCore.pyqtSlot()
+ def noUpdatePC(self):
+ self.updatemenu = None
+
+ @QtCore.pyqtSlot()
+ def checkPing(self):
+ curtime = int(time())
+ if curtime - self.lastping > 300:
+ self.pingServer.emit()
+
def profile(self):
return self.userprofile.chat
- def closeConversations(self):
+ def closeConversations(self, switch=False):
if not hasattr(self, 'tabconvo'):
self.tabconvo = None
if self.tabconvo:
@@ -1032,10 +1698,17 @@ class PesterWindow(MovingWindow):
for c in self.convos.values():
c.close()
if self.tabmemo:
- self.tabmemo.close()
+ if not switch:
+ self.tabmemo.close()
+ else:
+ for m in self.tabmemo.convos:
+ self.tabmemo.convos[m].sendtime()
else:
for m in self.memos.values():
- m.close()
+ if not switch:
+ m.close()
+ else:
+ m.sendtime()
def paintEvent(self, event):
palette = QtGui.QPalette()
palette.setBrush(QtGui.QPalette.Window, QtGui.QBrush(self.backgroundImage))
@@ -1072,10 +1745,11 @@ class PesterWindow(MovingWindow):
convo.addMessage(msg, False)
# play sound here
if self.config.soundOn():
- if msg in ["PESTERCHUM:CEASE", "PESTERCHUM:BLOCK"]:
- self.ceasesound.play()
- else:
- self.alarm.play()
+ if self.config.chatSound():
+ if msg in ["PESTERCHUM:CEASE", "PESTERCHUM:BLOCK"]:
+ self.ceasesound.play()
+ else:
+ self.alarm.play()
def newMemoMsg(self, chan, handle, msg):
if not self.memos.has_key(chan):
# silently ignore in case we forgot to /part
@@ -1089,9 +1763,22 @@ class PesterWindow(MovingWindow):
memo.times[handle] = time
if msg[0:3] != "/me" and msg[0:13] != "PESTERCHUM:ME":
msg = addTimeInitial(msg, memo.times[handle].getGrammar())
+ if handle == "ChanServ":
+ systemColor = QtGui.QColor(self.theme["memos/systemMsgColor"])
+ msg = "%s" % (systemColor.name(), msg)
memo.addMessage(msg, handle)
if self.config.soundOn():
- self.alarm.play()
+ if self.config.nameSound():
+ initials = self.userprofile.chat.initials()
+ search = r"\b[%s%s][%s%s]\b" % (initials[0].lower(), initials[0], initials[1].lower(), initials[1])
+ if re.search(search, convertTags(msg, "text")):
+ self.namesound.play()
+ return
+ if self.config.memoSound():
+ if re.search(r"\bhonk\b", convertTags(msg, "text"), re.I):
+ self.honksound.play()
+ else:
+ self.memosound.play()
def changeColor(self, handle, color):
# pesterconvo and chumlist
@@ -1133,7 +1820,19 @@ class PesterWindow(MovingWindow):
self.connect(convoWindow, QtCore.SIGNAL('windowClosed(QString)'),
self, QtCore.SLOT('closeConvo(QString)'))
self.convos[chum.handle] = convoWindow
- self.newConvoStarted.emit(QtCore.QString(chum.handle), initiated)
+ if str(chum.handle).upper() == "NICKSERV" or \
+ str(chum.handle).upper() == "CHANSERV" or \
+ str(chum.handle).upper() == "MEMOSERV" or \
+ str(chum.handle).upper() == "OPERSERV" or \
+ str(chum.handle).upper() == "HELPSERV":
+ convoWindow.toggleQuirks(True)
+ convoWindow.quirksOff.setChecked(True)
+ else:
+ if str(chum.handle).upper() == "CALSPRITE" or \
+ str(chum.handle).upper() == "RANDOMENCOUNTER":
+ convoWindow.toggleQuirks(True)
+ convoWindow.quirksOff.setChecked(True)
+ self.newConvoStarted.emit(QtCore.QString(chum.handle), initiated)
convoWindow.show()
def createTabWindow(self):
@@ -1145,7 +1844,7 @@ class PesterWindow(MovingWindow):
self.connect(self.tabmemo, QtCore.SIGNAL('windowClosed()'),
self, QtCore.SLOT('memoTabsClosed()'))
- def newMemo(self, channel, timestr, secret=False):
+ def newMemo(self, channel, timestr, secret=False, invite=False):
if channel == "#pesterchum":
return
if self.memos.has_key(channel):
@@ -1160,12 +1859,16 @@ class PesterWindow(MovingWindow):
else:
memoWindow = PesterMemo(channel, timestr, self, None)
# connect signals
+ self.connect(self, QtCore.SIGNAL('inviteOnlyChan(QString)'),
+ memoWindow, QtCore.SLOT('closeInviteOnly(QString)'))
self.connect(memoWindow, QtCore.SIGNAL('messageSent(QString, QString)'),
self, QtCore.SIGNAL('sendMessage(QString, QString)'))
self.connect(memoWindow, QtCore.SIGNAL('windowClosed(QString)'),
self, QtCore.SLOT('closeMemo(QString)'))
self.connect(self, QtCore.SIGNAL('namesUpdated()'),
memoWindow, QtCore.SLOT('namesUpdated()'))
+ self.connect(self, QtCore.SIGNAL('modesUpdated(QString, QString)'),
+ memoWindow, QtCore.SLOT('modesUpdated(QString, QString)'))
self.connect(self,
QtCore.SIGNAL('userPresentSignal(QString, QString, QString)'),
memoWindow, QtCore.SLOT('userPresentChange(QString, QString, QString)'))
@@ -1176,6 +1879,8 @@ class PesterWindow(MovingWindow):
if self.secret:
self.secret = True
self.setChannelMode.emit(channel, "+s", "")
+ if invite:
+ self.setChannelMode.emit(channel, "+i", "")
memoWindow.sendTimeInfo()
memoWindow.show()
@@ -1209,14 +1914,18 @@ class PesterWindow(MovingWindow):
newcloseicon = PesterIcon(theme["main/close/image"])
self.closeButton.setIcon(newcloseicon)
self.closeButton.setIconSize(newcloseicon.realsize())
+ self.closeButton.resize(newcloseicon.realsize())
self.closeButton.move(*theme["main/close/loc"])
newminiicon = PesterIcon(theme["main/minimize/image"])
self.miniButton.setIcon(newminiicon)
self.miniButton.setIconSize(newminiicon.realsize())
+ self.miniButton.resize(newminiicon.realsize())
self.miniButton.move(*theme["main/minimize/loc"])
# menus
self.menu.move(*theme["main/menu/loc"])
self.logv.setText(theme["main/menus/client/logviewer"])
+ self.grps.setText(theme["main/menus/client/addgroup"])
+ self.rand.setText(self.theme["main/menus/client/randen"])
self.opts.setText(theme["main/menus/client/options"])
self.exitaction.setText(theme["main/menus/client/exit"])
self.userlistaction.setText(theme["main/menus/client/userlist"])
@@ -1225,7 +1934,6 @@ class PesterWindow(MovingWindow):
self.idleaction.setText(theme["main/menus/client/idle"])
self.reconnectAction.setText(theme["main/menus/client/reconnect"])
self.filemenu.setTitle(theme["main/menus/client/_name"])
- self.changetheme.setText(theme["main/menus/profile/theme"])
self.changequirks.setText(theme["main/menus/profile/quirks"])
self.loadslum.setText(theme["main/menus/profile/block"])
self.changecoloraction.setText(theme["main/menus/profile/color"])
@@ -1302,16 +2010,41 @@ class PesterWindow(MovingWindow):
# sounds
if not pygame.mixer:
self.alarm = NoneSound()
+ self.memosound = NoneSound()
self.ceasesound = NoneSound()
else:
try:
self.alarm = pygame.mixer.Sound(theme["main/sounds/alertsound"])
+ self.memosound = pygame.mixer.Sound(theme["main/sounds/memosound"])
+ self.namesound = pygame.mixer.Sound("themes/namealarm.wav")
self.ceasesound = pygame.mixer.Sound(theme["main/sounds/ceasesound"])
+ self.honksound = pygame.mixer.Sound("themes/honk.wav")
except Exception, e:
self.alarm = NoneSound()
+ self.memosound = NoneSound()
+ self.namesound = NoneSound()
self.ceasesound = NoneSound()
+ self.honksound = NoneSound()
+
+ def setVolume(self, vol):
+ vol = vol/100.0
+ print vol
+ self.alarm.set_volume(vol)
+ self.memosound.set_volume(vol)
+ self.namesound.set_volume(vol)
+ self.ceasesound.set_volume(vol)
+ self.honksound.set_volume(vol)
def changeTheme(self, theme):
+ # check theme
+ try:
+ themeChecker(theme)
+ except ThemeException, (inst):
+ themeWarning = QtGui.QMessageBox(self)
+ themeWarning.setText("Theme Error: %s" % (inst))
+ themeWarning.exec_()
+ theme = pesterTheme("pesterchum")
+ return
self.theme = theme
# do self
self.initTheme(theme)
@@ -1371,7 +2104,12 @@ class PesterWindow(MovingWindow):
def pesterSelectedChum(self):
curChum = self.chumList.currentItem()
if curChum:
- self.newConversationWindow(curChum)
+ text = str(curChum.text(0))
+ if text.rfind(" (") != -1:
+ text = text[0:text.rfind(" (")]
+ if text not in self.chumList.groups and \
+ text != "Chums":
+ self.newConversationWindow(curChum)
@QtCore.pyqtSlot(QtGui.QListWidgetItem)
def newConversationWindow(self, chumlisting):
# check chumdb
@@ -1424,6 +2162,29 @@ class PesterWindow(MovingWindow):
def deliverMemo(self, chan, handle, msg):
(c, h, m) = (unicode(chan), unicode(handle), unicode(msg))
self.newMemoMsg(c,h,m)
+ @QtCore.pyqtSlot(QtCore.QString, QtCore.QString)
+ def deliverNotice(self, handle, msg):
+ h = unicode(handle)
+ m = unicode(msg)
+ if h == self.randhandler.randNick:
+ self.randhandler.incoming(msg)
+ elif self.convos.has_key(h):
+ self.newMessage(h, m)
+ @QtCore.pyqtSlot(QtCore.QString, QtCore.QString)
+ def deliverInvite(self, handle, channel):
+ msgbox = QtGui.QMessageBox()
+ msgbox.setText("You're invited!")
+ msgbox.setInformativeText("%s has invited you to the memo: %s\nWould you like to join them?" % (handle, channel))
+ msgbox.setStandardButtons(QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel)
+ ret = msgbox.exec_()
+ if ret == QtGui.QMessageBox.Ok:
+ self.newMemo(unicode(channel), "+0:00")
+ @QtCore.pyqtSlot(QtCore.QString)
+ def chanInviteOnly(self, channel):
+ self.inviteOnlyChan.emit(channel)
+ @QtCore.pyqtSlot(QtCore.QString, QtCore.QString)
+ def cannotSendToChan(self, channel, msg):
+ self.deliverMemo(channel, "ChanServ", msg)
@QtCore.pyqtSlot(QtCore.QString, QtCore.QString, QtCore.QString)
def timeCommand(self, chan, handle, command):
(c, h, cmd) = (unicode(chan), unicode(handle), unicode(command))
@@ -1503,7 +2264,7 @@ class PesterWindow(MovingWindow):
@QtCore.pyqtSlot(QtCore.QString)
def removeChum(self, chumlisting):
self.config.removeChum(chumlisting)
- def reportChum(self, handle):
+ def reportChum(self, handle):
(reason, ok) = QtGui.QInputDialog.getText(self, "Report User", "Enter the reason you are reporting this user (optional):")
if ok:
self.sendMessage.emit("REPORT %s %s" % (handle, reason) , "calSprite")
@@ -1636,18 +2397,16 @@ class PesterWindow(MovingWindow):
selectedmemo = self.memochooser.selectedmemo()
time = unicode(self.memochooser.timeinput.text())
secret = self.memochooser.secretChannel.isChecked()
+ invite = self.memochooser.inviteChannel.isChecked()
if newmemo:
channel = "#"+unicode(newmemo).replace(" ", "_")
channel = re.sub(r"[^A-Za-z0-9#_]", "", channel)
- self.newMemo(channel, time, secret=secret)
+ self.newMemo(channel, time, secret=secret, invite=invite)
elif selectedmemo:
channel = "#"+unicode(selectedmemo.target)
self.newMemo(channel, time)
self.memochooser = None
@QtCore.pyqtSlot()
- def memoChooserClose(self):
- self.memochooser = None
- @QtCore.pyqtSlot()
def memoChooserClose(self):
self.memochooser = None
@@ -1700,11 +2459,21 @@ class PesterWindow(MovingWindow):
self.quirkmenu.activateWindow()
@QtCore.pyqtSlot()
def updateQuirks(self):
+ for i in range(self.quirkmenu.quirkList.topLevelItemCount()):
+ curgroup = unicode(self.quirkmenu.quirkList.topLevelItem(i).text(0))
+ for j in range(self.quirkmenu.quirkList.topLevelItem(i).childCount()):
+ item = self.quirkmenu.quirkList.topLevelItem(i).child(j)
+ item.quirk.quirk["on"] = item.quirk.on = (item.checkState(0) == QtCore.Qt.Checked)
+ item.quirk.quirk["group"] = item.quirk.group = curgroup
quirks = pesterQuirks(self.quirkmenu.quirks())
self.userprofile.setQuirks(quirks)
+ if hasattr(self.quirkmenu, 'quirktester') and self.quirkmenu.quirktester:
+ self.quirkmenu.quirktester.close()
self.quirkmenu = None
@QtCore.pyqtSlot()
def closeQuirks(self):
+ if hasattr(self.quirkmenu, 'quirktester') and self.quirkmenu.quirktester:
+ self.quirkmenu.quirktester.close()
self.quirkmenu = None
@QtCore.pyqtSlot()
def openLogv(self):
@@ -1723,6 +2492,35 @@ class PesterWindow(MovingWindow):
def closeLogUsers(self):
self.logusermenu.close()
self.logusermenu = None
+
+ @QtCore.pyqtSlot()
+ def addGroupWindow(self):
+ if not hasattr(self, 'addgroupdialog'):
+ self.addgroupdialog = None
+ if not self.addgroupdialog:
+ (gname, ok) = QtGui.QInputDialog.getText(self, "Add Group", "Enter a name for the new group:")
+ if ok:
+ gname = unicode(gname)
+ if re.search("[^A-Za-z0-9_\s]", gname) is not None:
+ msgbox = QtGui.QMessageBox()
+ msgbox.setInformativeText("THIS IS NOT A VALID GROUP NAME")
+ msgbox.setStandardButtons(QtGui.QMessageBox.Ok)
+ ret = msgbox.exec_()
+ self.addgroupdialog = None
+ return
+ self.config.addGroup(gname)
+ gTemp = self.config.getGroups()
+ self.chumList.groups = [g[0] for g in gTemp]
+ self.chumList.openGroups = [g[1] for g in gTemp]
+ self.chumList.moveGroupMenu()
+ self.chumList.showAllGroups()
+ if not self.config.showEmptyGroups():
+ self.chumList.hideEmptyGroups()
+ if self.config.showOnlineNumbers():
+ self.chumList.showOnlineNumbers()
+
+ self.addgroupdialog = None
+
@QtCore.pyqtSlot()
def openOpts(self):
if not hasattr(self, 'optionmenu'):
@@ -1791,9 +2589,32 @@ class PesterWindow(MovingWindow):
elif chumsetting and not curchum:
self.chumList.hideOfflineChums()
self.config.set("hideOfflineChums", chumsetting)
+ # sorting method
+ sortsetting = self.optionmenu.sortBox.currentIndex()
+ cursort = self.config.sortMethod()
+ self.config.set("sortMethod", sortsetting)
+ if sortsetting != cursort:
+ self.chumList.sort()
# sound
soundsetting = self.optionmenu.soundcheck.isChecked()
self.config.set("soundon", soundsetting)
+ chatsoundsetting = self.optionmenu.chatsoundcheck.isChecked()
+ curchatsound = self.config.chatSound()
+ if chatsoundsetting != curchatsound:
+ self.config.set('chatSound', chatsoundsetting)
+ memosoundsetting = self.optionmenu.memosoundcheck.isChecked()
+ curmemosound = self.config.memoSound()
+ if memosoundsetting != curmemosound:
+ self.config.set('memoSound', memosoundsetting)
+ namesoundsetting = self.optionmenu.namesoundcheck.isChecked()
+ curnamesound = self.config.nameSound()
+ if namesoundsetting != curnamesound:
+ self.config.set('nameSound', namesoundsetting)
+ volumesetting = self.optionmenu.volume.value()
+ curvolume = self.config.volume()
+ if volumesetting != curvolume:
+ self.config.set('volume', volumesetting)
+ self.setVolume(volumesetting)
# timestamps
timestampsetting = self.optionmenu.timestampcheck.isChecked()
self.config.set("showTimeStamps", timestampsetting)
@@ -1804,11 +2625,121 @@ class PesterWindow(MovingWindow):
self.config.set("time12Format", False)
secondssetting = self.optionmenu.secondscheck.isChecked()
self.config.set("showSeconds", secondssetting)
+ # groups
+ #groupssetting = self.optionmenu.groupscheck.isChecked()
+ #self.config.set("useGroups", groupssetting)
+ emptygroupssetting = self.optionmenu.showemptycheck.isChecked()
+ curemptygroup = self.config.showEmptyGroups()
+ if curemptygroup and not emptygroupssetting:
+ self.chumList.hideEmptyGroups()
+ elif emptygroupssetting and not curemptygroup:
+ self.chumList.showAllGroups()
+ self.config.set("emptyGroups", emptygroupssetting)
+ # online numbers
+ onlinenumsetting = self.optionmenu.showonlinenumbers.isChecked()
+ curonlinenum = self.config.showOnlineNumbers()
+ if onlinenumsetting and not curonlinenum:
+ self.chumList.showOnlineNumbers()
+ elif curonlinenum and not onlinenumsetting:
+ self.chumList.hideOnlineNumbers()
+ self.config.set("onlineNumbers", onlinenumsetting)
+ # logging
+ logpesterssetting = 0
+ if self.optionmenu.logpesterscheck.isChecked():
+ logpesterssetting = logpesterssetting | self.config.LOG
+ if self.optionmenu.stamppestercheck.isChecked():
+ logpesterssetting = logpesterssetting | self.config.STAMP
+ curlogpesters = self.config.logPesters()
+ if logpesterssetting != curlogpesters:
+ self.config.set('logPesters', logpesterssetting)
+ logmemossetting = 0
+ if self.optionmenu.logmemoscheck.isChecked():
+ logmemossetting = logmemossetting | self.config.LOG
+ if self.optionmenu.stampmemocheck.isChecked():
+ logmemossetting = logmemossetting | self.config.STAMP
+ curlogmemos = self.config.logMemos()
+ if logmemossetting != curlogmemos:
+ self.config.set('logMemos', logmemossetting)
+ # memo and user links
+ linkssetting = self.optionmenu.userlinkscheck.isChecked()
+ curlinks = self.config.disableUserLinks()
+ if linkssetting != curlinks:
+ self.config.set('userLinks', not linkssetting)
+ # idle time
+ idlesetting = self.optionmenu.idleBox.value()
+ curidle = self.config.idleTime()
+ if idlesetting != curidle:
+ self.config.set('idleTime', idlesetting)
+ self.idlethreshold = 60*idlesetting
+ # theme
+ self.themeSelected()
+ # randoms
+ if self.randhandler.running:
+ self.randhandler.setRandomer(self.optionmenu.randomscheck.isChecked())
+ # button actions
+ minisetting = self.optionmenu.miniBox.currentIndex()
+ curmini = self.config.minimizeAction()
+ if minisetting != curmini:
+ self.config.set('miniAction', minisetting)
+ self.setButtonAction(self.miniButton, minisetting, curmini)
+ closesetting = self.optionmenu.closeBox.currentIndex()
+ curclose = self.config.closeAction()
+ if closesetting != curclose:
+ self.config.set('closeAction', closesetting)
+ self.setButtonAction(self.closeButton, closesetting, curclose)
+ # op and voice messages
+ opvmesssetting = self.optionmenu.memomessagecheck.isChecked()
+ curopvmess = self.config.opvoiceMessages()
+ if opvmesssetting != curopvmess:
+ self.config.set('opvMessages', opvmesssetting)
+ # animated smiles
+ animatesetting = self.optionmenu.animationscheck.isChecked()
+ curanimate = self.config.animations()
+ if animatesetting != curanimate:
+ self.config.set('animations', animatesetting)
+ self.animationSetting.emit(animatesetting)
+ # update checked
+ updatechecksetting = self.optionmenu.updateBox.currentIndex()
+ curupdatecheck = self.config.checkForUpdates()
+ if updatechecksetting != curupdatecheck:
+ self.config.set('checkUpdates', updatechecksetting)
+ # mspa update check
+ mspachecksetting = self.optionmenu.mspaCheck.isChecked()
+ curmspacheck = self.config.checkMSPA()
+ if mspachecksetting != curmspacheck:
+ self.config.set('mspa', mspachecksetting)
+ # advanced
+ ## user mode
+ if self.advanced:
+ newmodes = self.optionmenu.modechange.text()
+ if newmodes:
+ self.setChannelMode.emit(self.profile().handle, newmodes, "")
self.optionmenu = None
+ def setButtonAction(self, button, setting, old):
+ if old == 0: # minimize to taskbar
+ self.disconnect(button, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('showMinimized()'));
+ elif old == 1: # minimize to tray
+ self.disconnect(button, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('closeToTray()'));
+ elif old == 2: # quit
+ self.disconnect(button, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('close()'));
+
+ if setting == 0: # minimize to taskbar
+ self.connect(button, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('showMinimized()'));
+ elif setting == 1: # minimize to tray
+ self.connect(button, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('closeToTray()'));
+ elif setting == 2: # quit
+ self.connect(button, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('close()'));
+
@QtCore.pyqtSlot()
def themeSelected(self):
- themename = unicode(self.choosetheme.themeBox.currentText())
+ themename = unicode(self.optionmenu.themeBox.currentText())
if themename != self.theme.name:
try:
self.changeTheme(pesterTheme(themename))
@@ -1845,13 +2776,11 @@ class PesterWindow(MovingWindow):
self.changeTheme(self.userprofile.getTheme())
self.chatlog.close()
- self.chatlog = PesterLog(handle)
+ self.chatlog = PesterLog(handle, self)
# is default?
if self.chooseprofile.defaultcheck.isChecked():
self.config.set("defaultprofile", self.userprofile.chat.handle)
- # this may have to be fixed
- self.closeConversations()
if hasattr(self, 'trollslum') and self.trollslum:
self.trollslum.close()
self.chooseprofile = None
@@ -1914,8 +2843,18 @@ class PesterWindow(MovingWindow):
def loadCalsprite(self):
self.newConversation("calSprite")
@QtCore.pyqtSlot()
+ def loadNickServ(self):
+ self.newConversation("nickServ")
+ @QtCore.pyqtSlot()
def launchHelp(self):
QtGui.QDesktopServices.openUrl(QtCore.QUrl("http://nova.xzibition.com/~illuminatedwax/help.html", QtCore.QUrl.TolerantMode))
+ @QtCore.pyqtSlot()
+ def reportBug(self):
+ if hasattr(self, 'bugreportwindow') and self.bugreportwindow:
+ return
+ self.bugreportwindow = BugReporter(self)
+ self.bugreportwindow.exec_()
+ self.bugreportwindow = None
@QtCore.pyqtSlot(QtCore.QString, QtCore.QString)
def nickCollision(self, handle, tmphandle):
@@ -1947,17 +2886,28 @@ class PesterWindow(MovingWindow):
# show context menu i guess
#self.showTrayContext.emit()
+ @QtCore.pyqtSlot()
+ def tooManyPeeps(self):
+ msg = QtGui.QMessageBox(self)
+ msg.setText("D: TOO MANY PEOPLE!!!")
+ msg.setInformativeText("The server has hit max capacity. Please try again later.")
+ msg.show()
+
+ pcUpdate = QtCore.pyqtSignal(QtCore.QString, QtCore.QString)
closeToTraySignal = QtCore.pyqtSignal()
newConvoStarted = QtCore.pyqtSignal(QtCore.QString, bool, name="newConvoStarted")
sendMessage = QtCore.pyqtSignal(QtCore.QString, QtCore.QString)
+ sendNotice = QtCore.pyqtSignal(QtCore.QString, QtCore.QString)
convoClosed = QtCore.pyqtSignal(QtCore.QString)
profileChanged = QtCore.pyqtSignal()
+ animationSetting = QtCore.pyqtSignal(bool)
moodRequest = QtCore.pyqtSignal(PesterProfile)
moodsRequest = QtCore.pyqtSignal(PesterList)
moodUpdated = QtCore.pyqtSignal()
requestChannelList = QtCore.pyqtSignal()
requestNames = QtCore.pyqtSignal(QtCore.QString)
namesUpdated = QtCore.pyqtSignal()
+ modesUpdated = QtCore.pyqtSignal(QtCore.QString, QtCore.QString)
userPresentSignal = QtCore.pyqtSignal(QtCore.QString,QtCore.QString,QtCore.QString)
mycolorUpdated = QtCore.pyqtSignal()
trayIconSignal = QtCore.pyqtSignal(int)
@@ -1967,8 +2917,13 @@ class PesterWindow(MovingWindow):
joinChannel = QtCore.pyqtSignal(QtCore.QString)
leftChannel = QtCore.pyqtSignal(QtCore.QString)
setChannelMode = QtCore.pyqtSignal(QtCore.QString, QtCore.QString, QtCore.QString)
+ channelNames = QtCore.pyqtSignal(QtCore.QString)
+ inviteChum = QtCore.pyqtSignal(QtCore.QString, QtCore.QString)
+ inviteOnlyChan = QtCore.pyqtSignal(QtCore.QString)
closeSignal = QtCore.pyqtSignal()
reconnectIRC = QtCore.pyqtSignal()
+ gainAttention = QtCore.pyqtSignal(QtGui.QWidget)
+ pingServer = QtCore.pyqtSignal()
class PesterTray(QtGui.QSystemTrayIcon):
def __init__(self, icon, mainwindow, parent):
@@ -1989,7 +2944,10 @@ class MainProgram(QtCore.QObject):
def __init__(self):
QtCore.QObject.__init__(self)
self.app = QtGui.QApplication(sys.argv)
- self.app.setApplicationName("Pesterchum 3.14");
+ self.app.setApplicationName("Pesterchum 3.14")
+
+ options = self.oppts(sys.argv[1:])
+
if pygame.mixer:
# we could set the frequency higher but i love how cheesy it sounds
try:
@@ -1998,7 +2956,7 @@ class MainProgram(QtCore.QObject):
print "Warning: No sound! %s" % (e)
else:
print "Warning: No sound!"
- self.widget = PesterWindow()
+ self.widget = PesterWindow(options)
self.widget.show()
self.trayicon = PesterTray(PesterIcon(self.widget.theme["main/icon"]), self.widget, self.app)
@@ -2048,8 +3006,56 @@ class MainProgram(QtCore.QObject):
self.irc = PesterIRC(self.widget.config, self.widget)
self.connectWidgets(self.irc, self.widget)
+ self.connect(self.widget, QtCore.SIGNAL('gainAttention(QWidget*)'),
+ self, QtCore.SLOT('alertWindow(QWidget*)'))
+
+ # 0 Once a day
+ # 1 Once a week
+ # 2 Only on start
+ # 3 Never
+ check = self.widget.config.checkForUpdates()
+ if check == 2:
+ self.runUpdateSlot()
+ elif check == 0:
+ seconds = 60 * 60 * 24
+ if int(time()) - self.widget.config.lastUCheck() < seconds:
+ seconds -= int(time()) - self.widget.config.lastUCheck()
+ if seconds < 0: seconds = 0
+ QtCore.QTimer.singleShot(1000*seconds, self, QtCore.SLOT('runUpdateSlot()'))
+ elif check == 1:
+ seconds = 60 * 60 * 24 * 7
+ if int(time()) - self.widget.config.lastUCheck() < seconds:
+ seconds -= int(time()) - self.widget.config.lastUCheck()
+ if seconds < 0: seconds = 0
+ QtCore.QTimer.singleShot(1000*seconds, self, QtCore.SLOT('runUpdateSlot()'))
+
+ @QtCore.pyqtSlot()
+ def runUpdateSlot(self):
+ import Queue
+ import threading
+ q = Queue.Queue(1)
+ s = threading.Thread(target=version.updateCheck, args=(q,0)) # the 0 is to stop
+ w = threading.Thread(target=self.showUpdate, args=(q,0)) # stupid syntax errors
+ w.start()
+ s.start()
+ self.widget.config.set('lastUCheck', int(time()))
+ check = self.widget.config.checkForUpdates()
+ if check == 0:
+ seconds = 60 * 60 * 24
+ elif check == 1:
+ seconds = 60 * 60 * 24 * 7
+ else:
+ return
+ QtCore.QTimer.singleShot(1000*seconds, self, QtCore.SLOT('runUpdateSlot()'))
+
+ @QtCore.pyqtSlot(QtGui.QWidget)
+ def alertWindow(self, widget):
+ self.app.alert(widget)
+
widget2irc = [('sendMessage(QString, QString)',
'sendMessage(QString, QString)'),
+ ('sendNotice(QString, QString)',
+ 'sendNotice(QString, QString)'),
('newConvoStarted(QString, bool)',
'startConvo(QString, bool)'),
('convoClosed(QString)',
@@ -2072,6 +3078,12 @@ class MainProgram(QtCore.QObject):
'kickUser(QString, QString)'),
('setChannelMode(QString, QString, QString)',
'setChannelMode(QString, QString, QString)'),
+ ('channelNames(QString)',
+ 'channelNames(QString)'),
+ ('inviteChum(QString, QString)',
+ 'inviteChum(QString, QString)'),
+ ('pingServer()',
+ 'pingServer()'),
('reconnectIRC()', 'reconnectIRC()')
]
# IRC --> Main window
@@ -2084,6 +3096,10 @@ class MainProgram(QtCore.QObject):
'deliverMessage(QString, QString)'),
('memoReceived(QString, QString, QString)',
'deliverMemo(QString, QString, QString)'),
+ ('noticeReceived(QString, QString)',
+ 'deliverNotice(QString, QString)'),
+ ('inviteReceived(QString, QString)',
+ 'deliverInvite(QString, QString)'),
('nickCollision(QString, QString)',
'nickCollision(QString, QString)'),
('myHandleChanged(QString)',
@@ -2095,7 +3111,15 @@ class MainProgram(QtCore.QObject):
('channelListReceived(PyQt_PyObject)',
'updateChannelList(PyQt_PyObject)'),
('timeCommand(QString, QString, QString)',
- 'timeCommand(QString, QString, QString)')
+ 'timeCommand(QString, QString, QString)'),
+ ('chanInviteOnly(QString)',
+ 'chanInviteOnly(QString)'),
+ ('modesUpdated(QString, QString)',
+ 'modesUpdated(QString, QString)'),
+ ('cannotSendToChan(QString, QString)',
+ 'cannotSendToChan(QString, QString)'),
+ ('tooManyPeeps()',
+ 'tooManyPeeps()')
]
def connectWidgets(self, irc, widget):
self.connect(irc, QtCore.SIGNAL('finished()'),
@@ -2120,6 +3144,12 @@ class MainProgram(QtCore.QObject):
self.disconnect(self.irc, QtCore.SIGNAL('finished()'),
self, QtCore.SLOT('restartIRC()'))
+ def showUpdate(self, q,num):
+ new_url = q.get()
+ if new_url[0]:
+ self.widget.pcUpdate.emit(new_url[0], new_url[1])
+ q.task_done()
+
def showLoading(self, widget, msg="CONN3CT1NG"):
self.widget.show()
if hasattr(self.widget, 'loadingscreen') and widget.loadingscreen:
@@ -2145,6 +3175,12 @@ class MainProgram(QtCore.QObject):
if status == QtGui.QDialog.Rejected:
sys.exit(0)
else:
+ if self.widget.tabmemo:
+ for c in self.widget.tabmemo.convos:
+ self.irc.joinChannel(c)
+ else:
+ for c in self.widget.memos.values():
+ self.irc.joinChannel(c.channel)
return True
@QtCore.pyqtSlot()
@@ -2187,6 +3223,21 @@ class MainProgram(QtCore.QObject):
self.reconnectok = True
self.showLoading(self.widget, "F41L3D: %s" % stop)
+ def oppts(self, argv):
+ options = {}
+ try:
+ opts, args = getopt.getopt(argv, "s:p:", ["server=", "port=", "advanced"])
+ except getopt.GetoptError:
+ return options
+ for opt, arg in opts:
+ if opt in ("-s", "--server"):
+ options["server"] = arg
+ elif opt in ("-p", "--port"):
+ options["port"] = arg
+ elif opt in ("--advanced"):
+ options["advanced"] = True
+ return options
+
def run(self):
self.irc.start()
self.reconnectok = False
diff --git a/pesterchum_debug.py b/pesterchum_debug.py
new file mode 100644
index 0000000..b2f5d1f
--- /dev/null
+++ b/pesterchum_debug.py
@@ -0,0 +1,9 @@
+# runs pesterchum but appends stdout/err to log file
+import subprocess
+import datetime
+f = open("debug.log", 'a')
+d = datetime.datetime.now()
+f.write("=== PESTERCHUM DEBUG %s ===\n" % d.strftime("%m-%d-%y %H-%M"))
+p = subprocess.Popen("pesterchum.exe", stdout=f, stderr=subprocess.STDOUT)
+p.wait()
+f.close()
diff --git a/pyquirks.py b/pyquirks.py
new file mode 100644
index 0000000..c056d3a
--- /dev/null
+++ b/pyquirks.py
@@ -0,0 +1,67 @@
+import os, sys, imp, re
+from PyQt4 import QtGui, QtCore
+
+class PythonQuirks(object):
+ def __init__(self):
+ self.home = os.getcwd()
+ self.quirks = {}
+ self.last = {}
+ self.load()
+
+ def load(self):
+ self.last = self.quirks.copy()
+ self.quirks.clear()
+ filenames = []
+ if not os.path.exists(os.path.join(self.home, 'quirks')):
+ os.mkdir(os.path.join(self.home, 'quirks'))
+ for fn in os.listdir(os.path.join(self.home, 'quirks')):
+ if fn.endswith('.py') and not fn.startswith('_'):
+ filenames.append(os.path.join(self.home, 'quirks', fn))
+
+ modules = []
+ for filename in filenames:
+ name = os.path.basename(filename)[:-3]
+ try: module = imp.load_source(name, filename)
+ except Exception, e:
+ print "Error loading %s: %s (in pyquirks.py)" % (name, e)
+ msgbox = QtGui.QMessageBox()
+ msgbox.setWindowTitle("Error!")
+ msgbox.setText("Error loading %s: %s (in pyquirks.py)" % (name, e))
+ msgbox.exec_()
+ else:
+ if hasattr(module, 'setup'):
+ module.setup()
+ self.register(vars(module))
+ modules.append(name)
+ for k in self.last:
+ if k in self.quirks:
+ if self.last[k] == self.quirks[k]:
+ del self.quirks[k]
+
+ if self.quirks:
+ print 'Registered quirks:', '), '.join(self.quirks) + ")"
+ else:print "Warning: Couldn't find any python quirks"
+
+ def register(self, variables):
+ for name, obj in variables.iteritems():
+ if hasattr(obj, 'command'):
+ try:
+ if not isinstance(obj("test"), basestring):
+ raise Exception
+ except:
+ print "Quirk malformed: %s" % (obj.command)
+ msgbox = QtGui.QMessageBox()
+ msgbox.setWindowTitle("Error!")
+ msgbox.setText("Quirk malformed: %s" % (obj.command))
+ msgbox.exec_()
+ else:
+ self.quirks[obj.command+"("] = obj
+
+ def funcre(self):
+ if not self.quirks:
+ return r""
+ f = r"("
+ for q in self.quirks:
+ f = f + q[:-1]+r"\(|"
+ f = f + r"\)|\\[0-9]+)"
+ return f
diff --git a/quirks/defaults.py b/quirks/defaults.py
new file mode 100644
index 0000000..7508adc
--- /dev/null
+++ b/quirks/defaults.py
@@ -0,0 +1,17 @@
+import random
+
+def upperrep(text):
+ return text.upper()
+upperrep.command = "upper"
+
+def lowerrep(text):
+ return text.lower()
+lowerrep.command = "lower"
+
+def scramblerep(text):
+ return "".join(random.sample(text, len(text)))
+scramblerep.command = "scramble"
+
+def reverserep(text):
+ return text[::-1]
+reverserep.command = "reverse"
diff --git a/randomer.py b/randomer.py
new file mode 100644
index 0000000..119b039
--- /dev/null
+++ b/randomer.py
@@ -0,0 +1,54 @@
+from PyQt4 import QtGui, QtCore
+
+class RandomHandler(QtCore.QObject):
+ def __init__(self, parent):
+ QtCore.QObject.__init__(self, parent)
+ self.randNick = "randomEncounter"
+ self.mainwindow = parent
+ self.queue = []
+ # Make True when Lex's new randomEncounter bot (C++) is online
+ self.running = False
+
+ def getRandomer(self):
+ self.queue.append("?")
+ self.mainwindow.sendNotice.emit("?", self.randNick)
+
+ def setRandomer(self, r):
+ if r: code = "+"
+ else: code = "-"
+ self.queue.append(code)
+ self.mainwindow.sendNotice.emit(code, self.randNick)
+
+ @QtCore.pyqtSlot()
+ def getEncounter(self):
+ self.queue.append("!")
+ self.mainwindow.sendNotice.emit("!", self.randNick)
+
+ def incoming(self, msg):
+ l = msg.split("=")
+ code = l[0][0]
+ if code not in self.queue:
+ return # Ignore if we didn't request this
+ self.queue.remove(code)
+ if code == "?":
+ if l[1][0] == "y":
+ self.mainwindow.userprofile.setRandom(True)
+ elif l[1][0] == "n":
+ self.mainwindow.userprofile.setRandom(False)
+ elif code in ["+","-"]:
+ if l[1][0] == "k":
+ if code == "+":
+ self.mainwindow.userprofile.setRandom(True)
+ else:
+ self.mainwindow.userprofile.setRandom(False)
+ elif code == "!":
+ if l[1] == "x":
+ from PyQt4 import QtGui
+ msgbox = QtGui.QMessageBox()
+ msgbox.setText("Unable to fetch you a random encounter!")
+ msgbox.setInformativeText("Try again later :(")
+ msgbox.exec_()
+ return
+ name = unicode(l[1])
+ print name
+ self.mainwindow.newConversation(name)
diff --git a/readme.txt b/readme.txt
deleted file mode 100644
index 6304041..0000000
--- a/readme.txt
+++ /dev/null
@@ -1,665 +0,0 @@
-Welcome to Pesterchum 3.14.1!
-
-WHAT'S NEW?
------------
-* Quirks now have a lower(), scramble(), and reverse() function!
-* Timestamps - check your Config!
-* Logviewer - View logs right in Pesterchum!
-* Quirk ordering - order your quirks so they work right!
-* # of users in a memo - You can now see how many users are in a memo.
-* @links to users - typing @ before user's name creates a link
- that will pester them!
-* Support for REPORT and ALT to calSprite built in -
- If someone is bothering you, or a canon handle is idle, or
- for whatever reason, right click their name and go to "Report"
- to report them to a moderator.
- If you want to talk to an alt canon handle, just right click
- the username!
- if you have an alt handle, register it with calSprite!
-
-Here's some tips to help you get started:
-
-- You can import your old Pesterchum contacts by going to
-CLIENT->IMPORT and opening your pesterchum.cfg file. This is usually
-in the 2.5 base directory or in Tinychum's data folder.
-- Some themes can be confusing if you haven't used the program
-already! Some hints:
-
-Trollian: Moods are set by clicking the timelines, and you
-can add chums by clicking "Chumproll." Moods correspond to the troll
-that would most likely exhibit them. You can go offline by hitting the
-"Timelines" menu bar.
-
-Gold: Add chums by hitting the two chumpeoples in the upper left
-corner. Go offline by clicking the "CHUMHANDLE:" label.
-
-Enamel: Add chums by hitting the "CHUMROLL" label. Go offline by
-clicking the upper left hand corner.
-
-- Right-click is your friend! There are useful right click
-options on the chumroll, by clicking the chumhandle in a conversation,
-online userlist, or the list of memo browsers.
-
-Cool features:
-- Importing from old PC. It can already do your chumlist, soon it will
-import your quirks from 2.5 and TC as well!
-- Profile switching. Instantly switch profiles, loading your color and
-quirks with it.
-- Theme switching and creation. So far this comes with a few official
-themes! But you can also make your own: just make a new directory in
-the themes folder with the proper images and style.js file. The
-style.js file will be documented soon, but feel free to poke at it.
-- Memos. Memos that are a lot more like the ones in the comic and
-allow you to appear at multiple times in one chat.
-- Quirks: Prefix, suffix, simple replace, regexp replace (like in
-2.5), random replacement, and an auto-mispeller :P
-- Chum groups. Organize your chums into collapsible groups for easy
-management.
-- Block/user list
-- Add/block chums directly from a conversation, the userlist, or memo
-userlist.
-- Timestamps saved in logs and shown in conversations if wanted.
-- Logging. Logs are output in bbcode (for easy forum posting), html,
-and plain text.
-- Logviewer for easy log reading inside Pesterchum
-- Idling. You can set yourself idle manually, and the computer will
-set it for you after 10 minutes.
-- Improved /me. Any letters immediately following /me will be
-processed correctly. e.g. /me'd rather be fishing -> -- ghostDunk'd
-[GD'D] rather be fishing --
-- Hyperlinks! Now if someone types http://whatever it will turn into a
-link you can just click and follow. No more copy/paste.
-- Memo links. Link your friends to your memos.
-- Smilies. We've added about 30-40 smilies from the forums. There is a
-list later on in this readme.
-- Submit quotes directly to the Pesterchum QDB!
-
-FA%
----
-Q: Norton says it has a virus and then deletes it!
-A: Read this helpful Norton FAQ:
-
-Alright, here's a guide to by-passing Norton:
-First, to download Pesterchum:
-1: Make sure you're on a Moderator account. Moreso for the Norton steps than these ones.
-2: Download the .zip file, not the .exe file.
-3: Unzip the .zip file onto memory. Pesterchum should now be installed.
-
-Now, to by-pass Norton:
-1: Make sure you're still on a moderator account.
-2: Open up Norton.
-3: Click on 'Settings' up in the upperright hand corner.
-4: Click on 'Anitivirus', off to the upper left. It has a small image of a needle or something similar off to it's side.
-5: There's a word that reads 'SONAR protection' halfway to the bottomleft. Off to it's right, there's a bar that's half green. Click on the bar.
-6: It will warn you about turning off SONAR. Have it set to turn back on when the system restarts.
-7: If done properly, the background for the main page of Norton(what you saw on steps 2-3) has turned an apocaliptic red. Feel free to close Norton now. Keep in mind to stay off suspicious online sites now.
-8: Open up Pesterchum, and let the chummy convos begin.
-
-When finished:
-First, Log off of Pesterchum. LOG OFF, NOT CLOSE IT.
-Then, you can either shut off your comp, and Norton will re-enable SONAR, or you can repeat steps 1-5, except turning the red bar green. If done right, Norton will be it's happy color again.
-Keep in mind that you must repeat all of this(other than the download) every time you want to get on Pesterchum.
-
-Hope this is helpful!
-
-(This guide brought to you by the slightly combined efforts of empireomega and Xanaomin)
-
-Q: I can't connect because my school/university/network/stolen wifi is blocking my connection! OR I can't seem to connect to the server at all and I'm not running any firewalls!
-A: Edit your pesterchum.js file. Open it up in notepad or something, and then edit the beginning so it looks like this:
-
-{"port": "1413", ....
-
-where the .... is the rest of the gobbledygook there.
-
-Q: The mood buttons on Pesterchum 6.0 don't match up to what it sets your mood to! What gives?
-A: The mood names are just there to look canon. It is intentional.
-
-Q: I'm appearing as offline to 2.5 users/other users appear the wrong
-mood? What's happeninggggg
-A: The 2.5 people decided to change the mood protocol. When I made
-this program, I decided to go with Tinychat's original protocol (and
-extend it). So some moods will appear wrong between 2.5
-users. (*COUGH*tell them to switch to 3.14*COUGH*)
-
-Q: Pesterchum 2.5 users don't get my /me messages correctly!
-A: That's because they implemented the /me command differently.
-
-Q: Can we resize the main window?
-A: No. This is done so we can offer more flexible UI creation. It's a
-lot easier to make themes that look canon this way.
-
-Q: Can we have different chum rolls for different users?
-A: No. Instead what we're going to do in a later update is make chum
-groups to organize people in your list.
-
-Q: Can we delete profiles?
-A: Yes. Go to the profiles directory and delete the corresponding
-username file.
-
-Q: You should make it so you can ban specific time frames in memos.
-A: This was too complicated to implement, and I don't have the UI
-quite figured out. This will probably go in a future update.
-
-
-DOCUMENTATION
--------------
-
-STARTING
---------
-
-If this is your first time running Pesterchum 3.14, you need to create
-a new profile. Just type in your chum handle in the box and click the
-color swatch to pick your color. Check the "default" checkbox to make
-this your default profile.
-
-BASIC PESTERING
----------------
-To begin pestering, first click the "ADD CHUM" button and type in
-their pester handle. The handle must be all lower case except for one
-capital letter. Once you've added that person, they will appear on
-your chumroll. You can double click to begin pestering them, or
-right-click to bring up a menu where you can pester them, block them,
-or remove them from your chumroll. (Or you can select them and hit
-"enter" OR hit the "PESTER" button.)
-
-Once you begin pestering somebody (or they begin pestering you), it
-will bring up the conversation window. Here you can type to your
-chum. Also remember that if you right-click on the area just above the
-Pesterlog, it will bring up a list of options: Quirks Off will turn
-your quirks off, Add Chum will add this chum to your list, and Block
-will block them. (Those last two options are useful if you are being
-pestered by someone you don't have on your list yet!)
-
-While pestering your chum, here are some useful features:
-
-* Type /me to create a system message. "/me facepalms." will generate:
--- ghostDunk [GD] facepalms. --
- You can also append 's after /me like so: "/me's computer exploded."
--- ghostDunk's [GD'S] computer exploded. --
- In fact, any characters you type after a /me before the space will
- be added: "/meing is the Ghost Nation's official pastime."
--- ghostDunking [GDING] is the Ghost Nation's official pastime. --
-
-* Color tags! If you feel the need to talk about The Green Sun or add
- some appleberry blast to your conversation, just use color
- tags. These work like in TC 1.5: colored text. But in
- PC 3.14, you can use type your color in a lot of different ways:
- - You can use the familiar r,g,b method:
- "The Green Sun"
- - You can use HTML tags:
- "DURR I'M KARKAT AND I'M A HUGE IDIOT"
- - You can even use plain color names:
- "D4V3 TH1S 1S SO D3C4D3NT"
- (list: http://www.w3schools.com/css/css_colornames.asp)
- - You don't even have to add the if you are lazy. Just use a
- new color tag whenever you want to change colors and PC 3.14 will
- add the extra tags for you.
-
-* URLS (anything with http:// in front of it) will automatically be
- detected and made into a link you can CLIPK.
-
-* You can also link people to memos by typing "#" and the name of the
- menu like so: #R41NBOW_RUMPUS_P4RTYTOWN
- Clicking the link will open up the memo select menu.
-
-* Smilies! There are a list of smilies at the end of this document;
- they are based on the MSPA Forum smilies. They don't animate, though
- :(
-
-* Don't worry about your quirks screwing up any of the above: PC will
- apply your quirks AFTER it figures out color codes, links, smilies, etc.
-
-* Pressing the up arrow will cycle through a history of your comments,
- so if you want to retype something, you can pull it up.
-
-* You can submit directly to the Pesterchum Quote Database! If you
- have a particualarly awesome conversation, you can submit it to the
- database by simply highlighting the good part of the conversation,
- right clicking it and choosing "Submit to Pesterchum QDB!"
-
-MEMOS
------
-One of the most interesting features to make was the memos, and make
-them as close to the comic as I could without actually inventing time
-travel. So here is the TIME TUTORIAL:
-
-Joining: When you go CLIENT->MEMOS, you'll see a list of memos pop up
--- those are memos people already have open. To join one, just
-highlight one of them. If you want to make a new memo, just type it in
-the input. If you'd like to make it secret, so that it doesn't appear
-in the list, check "HIDDEN CHANNEL". Then, choose what timeframe you
-want to appear to be in. So if you wanted to be in the future, you
-could move the slider to the right. You can also enter the time
-manually. Then hit JOIN.
-
-Explaining time: Time in memos, unlike Homestuck, will not be relative
-to your position. That is, if you choose 4:13 in the future, you will
-not see someone who has set their time as "current" (or "0") in the
-past: you will see them as "current" and yourself as "future." This is
-because we do not have time travel! Memo time setting is basically an
-RP mechanic: you are pretending to be from the future! It will also
-help keep everyone straight: everyone will see the same thing!
-
-The time slider: The slider shows your current position in the time
-stream. If you want to change your time frame, simply move the slider
-(or type a time in) and hit GO. This will open a new time frame, and
-the next time you type a message, the memo will show that you've
-responded to it in that time frame. You can now switch between your
-time frames simply by clicking the arrows in the right hand
-corner. (THIS COMES IN HANDY IF YOU WANT TO ARGUE WITH YOURSELF.) You
-can have any number of open time frames, and the program will number
-them in the order in which you open them (like in the comic). You can
-have one of your time frames cease responding to the memo by hitting
-"CLOSE." If you open that time frame again, the program will remember
-the number it originally gave it. If you want to be mysteeeeeeeerious,
-you can type in "?" and you will appear as ???.
-
-The memo viewer list: To the right is a list of people currently
-browsing the memo. A shade icon next to their name means they are the
-"operator" of the memo: meaning they can kick ("ban") people from the
-memo and make other people operators as well. A "ban" is not permanent
-(like in the comic), and the program will ask if you want to reconnect
-to the memo. You kick and op people by right clicking their name in
-the window. You can also add them to your chumroll!
-
-Inviting people to your memo: You can link to a memo by simply typing
-"#nameofmemo" in any conversation or memo window. So you can say:
-
-CG: NOW YOU, ME, AND EGBERT NEED TO HAVE A CHAT.
-CG: CLICK IT.
-CG: #FRUITYRUMPUSASSHOLEFACTORY
-
-and it will appear as a link that you can click, which will open the
-memo chooser window.
-
-CLIENT MENU
------------
-
-OPTIONS:
-Tabbed Conversations: Turns tabbed conversations on and off. Don't
-worry if you do this in the middle of a conversation, PC will save
-them for you.
-
-Sounds On: Uncheck to shut it the fuck up.
-
-Hide Offline Chums: Turning this option on will hide all offline chums
-off your chumroll.
-
-Show Empty Groups: Turning this option of will show empty groups.
-
-Time Stamps: Turning this on will show timestamps in your chats.
-
-12/24 hour: Formatting for timestamps. Whether you want them in 12 or
-24 hour time.
-
-Show Seconds: Turning this on will show the seconds in your timestamps.
-
-MEMOS: Opens the Memo list as above.
-
-USERLIST: Shows a list of all the users that are currently logged onto
-Pesterchum. Right-click their names and select "ADD CHUM" to add them
-to your chum roll!
-
-IDLE: Make yourself an idle chum. You will appear as idle until you
-uncheck this box, or if you *actually* go idle (stop using the
-computer) for 10 minutes and then come back.
-
-IMPORT: Imports your old Pesterchum 2.0, 2.5 and Tinychum chum
-rolls. This will also import your old quirks from Pesterchum 2.5.
-
-RECONNECT: Forces PC to reconnect to the server.
-
-EXIT: noooooooooooooooooooooooo
-
-PROFILE MENU
-------------
-
-THEME: Select a new theme! Be warned that switching themes will change
-the user interface, so just... look out for that I guess!
-
-QUIRKS: Opens the quirks menu. More on that below!
-
-TROLLSLUM: Opens up the window where you can view people you've
-blocked. You can add and remove people to the list from here as well.
-
-COLOR: Change your text color here!
-
-SWITCH: Switch your profile! You can have any number of profiles, and
-PC will save your color, quirks, and theme for that profile. Chumrolls
-and block lists are the same for all profiles. Feel free to have
-multiple instances of PC running on two or more handles!
-
-CALSPRITE
----------
-calSprite is the bot that helps moderate canon handle usage! Simply pester
-calSprite with the world "HELP" (turn your quirks off!) and you
-will get instructions on how to use calSprite!
-
-QUIRKS
-------
-There are six kinds of quirks! I'll teach you how to use them all!
-(In this section, I will use quotes ("") around things so it's clearer
-to see exactly what to type! Don't include these quotes when using
-these examples!
-
-Also, note that your quirks will not work until you save them by
-hitting "OK" on the Quirk window.
-
-Prefix/Suffix: This will put text before or after everything you
-say. So for example, we can use prefixes to emulate part of Nepeta or
-Equius' quirks:
-
-PREFIX: ":33 < "
-You type: "*ac twitches her friendly whiskers at ct*"
-Result:
-AC: :33 < *ac twitches her friendly whiskers at ct*
-
-PREFIX: "D --> "
-You type: "Hi"
-Result:
-CT: D --> Hi
-
-Suffixes work the same way, but at the end of the message:
-SUFFIX: "!!!"
-You type: hey there
-Result:
-GD: hey there!!!
-
-Remember that it doesn't automatically add a space! You'll need to add
-it in (see CT and AC examples again!)
-
-Simple Replace:
-This will simply take a set of characters and replace them with other
-characters. Let's add a quirk to our Nepeta:
-
-Replace: "ee" With: "33"
-You type: "*ac saunters from her dark cave a little bit sleepy from
-the recent kill*"
-Result:
-AC: :33 < *ac saunters from her dark cave a little bit sl33py from the
-recent kill*
-
-Let's add two to Equius:
-Replace: "loo" With: "100"
-Replace: "x" With "%"
-You type: "look"
-Result:
-CT: D --> 100k
-You type: "What are you expecting to accomplish with this"
-Result:
-CT: D --> What are you e%pecting to accomplish with this
-
-Aradia:
-Replace: "o" With: "0"
-You type: "and the reward would be within our reach"
-Result:
-AA: and the reward w0uld be within 0ur reach
-
-Notice that it is CASE SENSITIVE. So in the above case, if you typed
-"ABSCOND", it would not replace the "O".
-
-Sollux:
-Replace: "i" With: "ii"
-Replace: "s" With: "2"
-
-Eridan:
-Replace: "v" With: "vv"
-Replace: "w" With: "ww"
-
-Feferi:
-Replace: "h" with: ")("
-Replace: "H" with: ")("
-Replace: "E" with: "-E"
-
-Regexp Replace:
-
-This is a more complex kind of replacement. Regexp stands for "regular
-expression", a kind of programming language (yes, it is a language)
-used to find and replace text. PC 3.14 also includes a function to
-handle capitalization (upper()). If you want to learn it on your own,
-I suggest you start with the Python tutorial
-(http://docs.python.org/howto/regex.html) since PC 3.14 uses Python's
-regexps. Check out V2.5's tutorial too, as that is a pretty good start
-as well.
-
-Let's start with Karkat. Regexps are just like your every day find and
-replace: they search for a string that matches what you want to
-replace, and replaces it with... the replacement.
-
-Regexp: "(.)" Replace with: "upper(\1)"
-
-Three concepts here. Let's look at the regexp. "(.)" has two things
-going on. The first is that ".". In regexp speak, "." is the wildcard:
-it will match *any* character -- and just one.
-
-The parentheses tell the regexp to *save* what's inside them so you
-can put it back when you replace. That's what the "\1" is for -- it
-means, "put the match inside parentheses #1 here". You can have any
-number of parentheses.
-
-"upper()" is a function special to PC 3.14 -- it will uppercase
-anything inside the parentheses. So in this case, upper will uppercase
-"\1" -- which, as you recall is what we found inside the
-parentheses. Which was *every* character. So to sum up, it replaces
-every character with an uppercase version of that character. WHICH
-MAKES YOU TALK LIKE THIS.
-
-Let's look at Terezi next.
-
-Regexp: "[aA]" Replace with: "4"
-Regexp: "[iI]" Replace with: "1"
-Regexp: "[eE]" Replace with: "3"
-Regexp: "(.)" Replace with: "upper(\1)"
-
-We already know what the last line does. But what's up with those
-brackets? What's their deal? Basically, in regular expressions,
-brackets indicate a list of matching characters. So, basically any
-single character within the brackets will be matched. In this case,
-either "a" or "A" will be matched and replaced with "4," and likewise,
-"i" and "I" will be replaced with "1", and "e" and "E" will be
-replaced with "3."
-
-Just like there is an "upper()" function, there is also a "lower()"
-function. It acts just like "upper()" but instead makes everything
-inside the parentheses lowercase. This allows you to do things like:
-
-Regexp: "(.)" Replace with: "lower(\1)"
-You type: "I AM YELLING"
-Result:
-GD: i am yelling
-
-Along with the upper and lower functions is a "scramble()" function.
-The purpose of this function is to randomly scramble anything inside
-the parentheses.
-
-Regexp: "(\w)(\w*)(\w)" Replace with: "\1scramble(\2)\3"
-You type: "hello there"
-Result:
-GD: hlelo trhee
-
-This particular regular expression scrambles all of the letters in
-the middle of a word. Notice that the "h" and "o" at the beginning
-and end of hello remain in place while the other letters are scrambled.
-
-You should also know that "^" is a special character in brackets. If
-placed immediately after the opening bracket (like "[^"), then the
-brackets instead match every character *except* the ones in the
-brackets. So, for example, if you wanted to have a quirk where you
-capitalized all your letters *except* o, you'd do this:
-
-Regexp: "([^o])" Replace with: "upper(\1)"
-You type: "hello there"
-Result:
-GD: HELLo THERE
-
-You can also specify a *range* of characters inside the brackets, by
-using the "-" character. [a-z] will match any lowercase letter. You
-can combine them, too: [a-z0-9] will match any digit and lowercase letter.
-
-There are also different shortcuts for character types:
-
-\d matches any digit; same as [0-9]
-\D matches any non-digit; same as [^0-9]
-\s matches any spaces
-\S matches any non-space
-\w matches any alphanumeric character; same as [a-zA-Z0-9_]
-\W matches any non-alphanumeric character; same as [^a-zA-Z0-9_]
-
-You can include this inside brackets, too.
-
-There's also a special character, \b. What \b does is make sure that
-you are at the beginning or end of a word. So with that knowledge,
-let's try Kanaya:
-
-Regexp: \b(\w) Replace with: upper(\1)
-You type: "i suggest you come to terms with it"
-Result:
-GA: I Suggest You Come To Terms With It
-
-Another feature of regular expressions is the ability to match
-*repeated* characters. There are three repeat characters: the "*", the
-"+", "?", and "{m,n}". They work by playing them after the character,
-or character type you want to match. (So, you could say "\s+" or ".*")
-
-The "*" character matches ZERO or more of that character. So, for
-example, "f*" would match "f" and "ff" -- and any other character!
-That's right, every character counts as matching it zero times. Yeah,
-it's weird. I suggest you use...
-
-The "+" character matches ONE or more of that character. So, if we
-wanted to have a character that wanted to elongate their s's so that
-they used four 's's every time, like sssso, but didn't want to have
-eight s's when using words with double s's, like pass, we'd do this:
-
-Regexp: "s+" Replace with: "ssss"
-You type: "you shall not pass"
-Result:
-UU: you sssshall not passss
-
-As for the other two, I can't really think of any useful quirks to be
-made with them. But to let you know, "?" matches either 0 or 1 of that
-character, so "trolls?" would match "troll" and "trolls". "{m,n}"
-matches between m and n characters. (If you leave out 'n', any number
-of characters more than m will be matched.) So "s{2,4}" will match
-"ss", "sss", and "ssss" and that's it.
-
-Now with repeating expressions, we can do something like make EVERY
-other WORD capitalized:
-
-Regexp: "(\w+) (\w+)" Replace with: "upper(\1) \2"
-You type: "this is pretty annoying i bet"
-Result:
-GD: THIS is PRETTY annoying I bet
-
-The \1 matches the first word -- which has been matched because the
-word is alphanumeric characters, repeated once or more -- and \2
-matches the second word.
-
-Another operator to use is the "|", which will match more than one set
-of characters. For example, "black|red" will match "black" or
-"red". If you want to match something in the middle of words, you have
-to use parentheses: "(black|red) romance" will match "black romance"
-and "red romance".
-
-Finally, there are the "^" and "$" characters. Yes, we already did the
-"^" character, but this is OUTSIDE of brackets, not INSIDE. "^"
-matches the beginning of a message, and "$" matches the end of it. You
-can use this to make more sophisticated prefix and suffix
-behaviors. For example, if we have a quirk that adds "..." to the end
-of all our messages, we can set it up so it doesn't do that if we put
-punctuation [?!.] at the end. So:
-
-Regexp: "([^?!.])$" Replace with: "\1..."
-
-This will match the end of any message as long as it doesn't have
-"?", "!", or "." at the end. Then it will replace it with whatever the
-last character of the sentence was (remember we're replacing it, so we
-have to put it back!) and add "..." at the end.
-
-Careful with the beginning and ending replaces -- if you use more than
-one, you may not get what you expect because they will ALL be applied,
-one after the other! This is a bug in my opinion, that I plan to fix!
-
-Random replace:
-
-Just like the regexp replace, except that instead of just one thing to
-replace it with, you give it a list. PC will then choose from that
-list randomly. So let's say I want to randomly end my sentences with
-either "bro" or "dog":
-
-Regexp: "$" Replace with: "bro" and "dog"
-
-You can also imitate Araida's random "ribbits" in between words:
-
-Regexp: "\s" Replace with: " ribbit ", " ", " ", " ", " ", " ", etc....
-
-where " " is just a blank space added a bunch of times. (You can see
-how many blank spaces you've added by clicking on the list.) You have
-to add the spaces because each entry has the same chance of being
-selected. (Yes, I know this could be improved.) If you add " ribbit "
-and 9 spaces, " ribbit " will have a 1/10 chance of being picked.
-
-Also note that if you add more than one prefix or more than one
-suffix, it will pick randomly from them instead of adding them both!
-
-Mispeller:
-
-Be careful with thsi one. The mispeller will randomly mispell x% of
-the words you type -- where x is the percentage you set the slider
-to. I have attempted to mimic SBaHJ mispelling style but whoof knows
-what will happen oh god ive created a mosnter
-
-
-SMILIES
--------
-Here's a list of smilies:
-:rancorous:
-:apple:
-:bathearst:
-:cathearst:
-:woeful:
-:pleasant:
-:blueghost:
-:slimer:
-:candycorn:
-:cheer:
-:duhjohn:
-:datrump:
-:facepalm:
-:bonk:
-:mspa:
-:gun:
-:cal:
-:amazedfirman:
-:amazed:
-:chummy:
-:cool:
-:smooth:
-:distraughtfirman
-:distraught:
-:insolent:
-:bemused:
-:3:
-:mystified:
-:pranky:
-:tense:
-:record:
-:squiddle:
-:tab:
-:beetip:
-:flipout:
-:befuddled:
-:pumpkin:
-:trollcool:
-:jadecry:
-:ecstatic:
-:relaxed:
-:discontent:
-:devious:
-:sleek:
-:detestful:
-:mirthful:
-:manipulative:
-:vigorous:
-:perky:
-:acceptant:
diff --git a/themes/enamel/leftarrow.png b/themes/enamel/leftarrow.png
new file mode 100644
index 0000000..4caf00b
Binary files /dev/null and b/themes/enamel/leftarrow.png differ
diff --git a/themes/enamel/rightarrow.png b/themes/enamel/rightarrow.png
new file mode 100644
index 0000000..30a8b70
Binary files /dev/null and b/themes/enamel/rightarrow.png differ
diff --git a/themes/enamel/style.js b/themes/enamel/style.js
index cc0c1aa..3c50325 100644
--- a/themes/enamel/style.js
+++ b/themes/enamel/style.js
@@ -10,7 +10,7 @@
"minimize": { "image": "$path/m.gif",
"loc": [300, 32]},
"menubar": { "style": "font-family: 'Century Gothic'; font-size: 14px; color:#9d9d9d" },
- "menu" : { "style": "font-family: 'Century Gothic'; font-size: 14px; background-color: #fdb302;border:2px solid #ffff00",
+ "menu" : { "style": "font-family: 'Century Gothic'; font-size: 14px; color: #000000; background-color: #fdb302;border:2px solid #ffff00",
"menuitem": "margin-right:25px;",
"selected": "background-color: #ffff00",
"loc": [480,30]
@@ -19,10 +19,12 @@
"options": "Options",
"memos": "Memos",
"logviewer": "Pesterlogs",
+ "randen": "Random Encounter",
"userlist": "Userlist",
+ "addgroup": "Add Group",
"import": "Import",
"reconnect": "Reconnect",
- "idle": "Idle",
+ "idle": "Idle",
"exit": "Exit"},
"profile": {"_name": "Profile",
"switch": "Switch",
@@ -31,7 +33,10 @@
"block": "Trollslum",
"quirks": "Quirks"},
"help": { "_name": "Help",
- "about": "About" },
+ "about": "About",
+ "help": "Help",
+ "calsprite": "CalSprite",
+ "nickserv": "NickServ" },
"rclickchumlist": {"pester": "Pester",
"removechum": "Remove Chum",
"blockchum": "Block",
@@ -39,9 +44,14 @@
"addchum": "Add Chum",
"viewlog": "View Pesterlog",
"unblockchum": "Unblock",
+ "removegroup": "Remove Group",
+ "renamegroup": "Rename Group",
+ "movechum": "Move To",
"banuser": "Ban User",
"opuser": "Make OP",
- "quirksoff": "Quirks Off"
+ "voiceuser": "Give Voice",
+ "quirksoff": "Quirks Off",
+ "invitechum": "Invite Chum"
}
},
"chums": { "style": "text-align: center; border:0px; background-image:url($path/chumbg.png); background-color: #ffe400; background-repeat: no-repeat; color: white; font-family: 'Century Gothic';selection-background-color:#646464; font-size:18px; ",
@@ -55,7 +65,7 @@
"loc": [440, 211],
"size": [289, 275],
"userlistcolor": "black",
- "moods": {
+ "moods": {
"chummy": { "icon": "$path/chummy.gif", "color": "black" },
@@ -63,7 +73,7 @@
"offline": { "icon": "$path/offline.gif", "color": "#9d9d9d"},
-
+
"pleasant": { "icon": "$path/pleasant.gif", "color": "black" },
"distraught": { "icon": "$path/distraught.gif", "color": "black" },
@@ -91,7 +101,7 @@
"devious": { "icon": "$path/devious.gif", "color": "red" },
"sleek": { "icon": "$path/sleek.gif", "color": "red" },
-
+
"detestful": { "icon": "$path/detestful.gif", "color": "red" },
"mirthful": { "icon": "$path/mirthful.gif", "color": "red" },
@@ -110,7 +120,7 @@
}
},
- "trollslum": {
+ "trollslum": {
"style": "background: #fdb302; border:2px solid yellow; font-family: 'Century Gothic'",
"size": [195, 200],
"label": { "text": "TROLLSLUM",
@@ -128,7 +138,7 @@
"text": "" },
"currentMood": [1500, 1500]
},
- "defaultwindow": { "style": "background: #fdb302; font-family:'Century Gothic';font:bold;selection-background-color:#919191; "
+ "defaultwindow": { "style": "background: #fdb302; font-family:'Century Gothic';font:bold;selection-background-color:#919191; "
},
"addchum": { "style": "background: rgba(255, 255, 0, 0%); border:0px; color: rgba(0, 0, 0, 0%);",
"loc": [443,144],
@@ -148,82 +158,82 @@
},
"defaultmood": 0,
"moodlabel": { "style": "",
- "loc": [20, 430],
- "text": ""
- },
+ "loc": [20, 430],
+ "text": ""
+ },
"moods": [
- { "style": "background-image:url($path/mood1.png); border:0px;",
- "selected": "background-image:url($path/mood1c.png); border:0px;",
- "loc": [0, 258],
- "size": [100,110],
- "text": "",
- "icon": "",
- "mood": 0
- },
- { "style": "background-image:url($path/mood2.png); border:0px;",
- "selected": "background-image:url($path/mood2c.png); border:0px;",
- "loc": [106, 258],
- "size": [100, 110],
- "text": "",
- "icon": "",
- "mood": 19
- },
- { "style": "background-image:url($path/mood3.png); border:0px;",
- "selected": "background-image:url($path/mood3c.png); border:0px;",
- "loc": [212, 258],
- "size": [100, 110],
- "text": "",
- "icon": "",
- "mood": 22
- },
- { "style": "background-image:url($path/mood4.png); border:0px;",
- "selected": "background-image:url($path/mood4c.png); border:0px;",
- "loc": [318, 258],
- "size": [100, 110],
- "text": "",
- "icon": "",
- "mood": 4
- },
- { "style": "background-image:url($path/mood5.png); border:0px;",
- "selected": "background-image:url($path/mood5c.png); border:0px;",
- "loc": [0, 382],
- "size": [100, 110],
- "text": "",
- "icon": "",
- "mood": 3
- },
- { "style": "background-image:url($path/mood6.png); border:0px;",
- "selected": "background-image:url($path/mood6c.png); border:0px;",
- "loc": [106, 382],
- "size": [100, 110],
- "text": "",
- "icon": "",
- "mood": 20
- },
- { "style": "background-image:url($path/mood7.png); border:0px;",
- "selected": "background-image:url($path/mood7c.png); border:0px;",
- "loc": [212, 382],
- "size": [100, 110],
- "text": "",
- "icon": "",
- "mood": 5
- },
- { "style": "background-image:url($path/mood8.png); border:0px;",
- "selected": "background-image:url($path/mood8c.png); border:0px;",
- "loc": [318, 382],
- "size": [100, 110],
- "text": "",
- "icon": "",
- "mood": 1
- },
- { "style": "border:0px;",
- "selected": "border:0px;",
- "loc": [0, 0],
- "size": [100, 100],
- "text": "",
- "icon": "",
- "mood": 2
- }
+ { "style": "background-image:url($path/mood1.png); border:0px;",
+ "selected": "background-image:url($path/mood1c.png); border:0px;",
+ "loc": [0, 258],
+ "size": [100,110],
+ "text": "",
+ "icon": "",
+ "mood": 0
+ },
+ { "style": "background-image:url($path/mood2.png); border:0px;",
+ "selected": "background-image:url($path/mood2c.png); border:0px;",
+ "loc": [106, 258],
+ "size": [100, 110],
+ "text": "",
+ "icon": "",
+ "mood": 19
+ },
+ { "style": "background-image:url($path/mood3.png); border:0px;",
+ "selected": "background-image:url($path/mood3c.png); border:0px;",
+ "loc": [212, 258],
+ "size": [100, 110],
+ "text": "",
+ "icon": "",
+ "mood": 22
+ },
+ { "style": "background-image:url($path/mood4.png); border:0px;",
+ "selected": "background-image:url($path/mood4c.png); border:0px;",
+ "loc": [318, 258],
+ "size": [100, 110],
+ "text": "",
+ "icon": "",
+ "mood": 4
+ },
+ { "style": "background-image:url($path/mood5.png); border:0px;",
+ "selected": "background-image:url($path/mood5c.png); border:0px;",
+ "loc": [0, 382],
+ "size": [100, 110],
+ "text": "",
+ "icon": "",
+ "mood": 3
+ },
+ { "style": "background-image:url($path/mood6.png); border:0px;",
+ "selected": "background-image:url($path/mood6c.png); border:0px;",
+ "loc": [106, 382],
+ "size": [100, 110],
+ "text": "",
+ "icon": "",
+ "mood": 20
+ },
+ { "style": "background-image:url($path/mood7.png); border:0px;",
+ "selected": "background-image:url($path/mood7c.png); border:0px;",
+ "loc": [212, 382],
+ "size": [100, 110],
+ "text": "",
+ "icon": "",
+ "mood": 5
+ },
+ { "style": "background-image:url($path/mood8.png); border:0px;",
+ "selected": "background-image:url($path/mood8c.png); border:0px;",
+ "loc": [318, 382],
+ "size": [100, 110],
+ "text": "",
+ "icon": "",
+ "mood": 1
+ },
+ { "style": "border:0px;",
+ "selected": "border:0px;",
+ "loc": [0, 0],
+ "size": [100, 100],
+ "text": "",
+ "icon": "",
+ "mood": 2
+ }
]
},
"convo":
@@ -231,7 +241,7 @@
"tabstyle": "background-color: #fdb302; font-family: 'Century Gothic'",
"scrollbar": { "style" : "padding-top:17px; padding-bottom:17px;width: 18px; background: rgba(255, 255, 0, 0%); border:0px;",
"handle": "border-width: 5px; border-image:url($path/scrollbg.png) 5px; min-height:60px;",
- "downarrow": "height:17px;border:0px solid #c48a00;",
+ "downarrow": "height:17px;border:0px solid #c48a00;",
"darrowstyle": "image:url($path/downarrow.png);",
"uparrow": "height:17px;border:0px solid #c48a00;",
"uarrowstyle": "image:url($path/uparrow.png);"
@@ -259,12 +269,12 @@
"ceasepester": "ceased pestering",
"blocked": "blocked",
"unblocked": "unblocked",
- "blockedmsg": "did not receive message from",
+ "blockedmsg": "did not receive message from",
"openmemo": "opened memo on board",
"joinmemo": "responded to memo",
"closememo": "ceased responding to memo",
"kickedmemo": "You have been banned from this memo!",
- "idle": "is now an idle chum!"
+ "idle": "is now an idle chum!"
},
"systemMsgColor": "#646464"
},
@@ -279,7 +289,7 @@
},
"scrollbar": { "style" : "padding-top:17px; padding-bottom:17px;width: 18px; background: rgba(255, 255, 0, 0%); border:0px;",
"handle": "border-width: 5px; border-image:url($path/scrollbg.png) 5px; min-height:60px;",
- "downarrow": "height:17px;border:0px;",
+ "downarrow": "height:17px;border:0px;",
"darrowstyle": "image:url();",
"uparrow": "height:17px;border:0px;",
"uarrowstyle": "image:url();"
@@ -296,20 +306,21 @@
"userlist": { "width": 150,
"style": "border:2px solid #c48a00; background: white; font-family: 'Century Gothic';selection-background-color:#646464; font-size: 14px; margin-left:0px; margin-right:10px;"
},
- "time": { "text": { "width": 75,
- "style": " border: 2px solid yellow; background: white; font-size: 12px; margin-top: 5px; margin-right: 5px; margin-left: 5px; font-family:'Century Gothic';font:bold;"
+ "time": { "text": { "width": 75,
+ "style": " border: 2px solid yellow; background: white; font-size: 12px; margin-top: 5px; margin-right: 5px; margin-left: 5px; font-family:'Century Gothic';font:bold;"
},
"slider": { "style": "border: 0px;",
"groove": "",
"handle": ""
},
- "buttons": { "style": "color: black; font: bold; border: 2px solid #c48a00; font-size: 12px; background: yellow; margin-top: 5px; margin-right: 5px; margin-left: 5px; padding: 2px; width: 50px;" },
- "arrows": { "left": "$path/leftarrow.png",
+ "buttons": { "style": "color: black; font: bold; border: 2px solid #c48a00; font-size: 12px; background: yellow; margin-top: 5px; margin-right: 5px; margin-left: 5px; padding: 2px; width: 50px;" },
+ "arrows": { "left": "$path/leftarrow.png",
"right": "$path/rightarrow.png",
- "style": " border:0px; margin-top: 5px; margin-right:10px;"
+ "style": " border:0px; margin-top: 5px; margin-right:10px;"
}
},
"systemMsgColor": "#646464",
- "op": { "icon": "$path/smooth.png" }
+ "op": { "icon": "$path/smooth.png" },
+ "voice": { "icon": "$path/voice.png" }
}
-}
\ No newline at end of file
+}
diff --git a/themes/enamel/voice.png b/themes/enamel/voice.png
new file mode 100644
index 0000000..c400a6c
Binary files /dev/null and b/themes/enamel/voice.png differ
diff --git a/themes/gold xl/style.js b/themes/gold xl/style.js
index 7bfc9ef..ba20554 100644
--- a/themes/gold xl/style.js
+++ b/themes/gold xl/style.js
@@ -3,9 +3,9 @@
{ "size": [554, 484],
"background-image": "$path/gbbg.png",
"close": { "image": "$path/x.png",
- "loc": [535, 45]},
+ "loc": [539, 47]},
"minimize": { "image": "$path/m.png",
- "loc": [515, 48]},
+ "loc": [519, 50]},
"menu" : { "loc": [214,44] },
"chums": { "style": "border:0px; background-image:url($path/chumbg.png); background-color: rgb(110,110,110); background-repeat: no-repeat; color: white; font-family: 'Arial';selection-background-color:#646464; font-size:16px; ",
"loc": [207, 153],
@@ -112,4 +112,4 @@
}
]
}
-}
\ No newline at end of file
+}
diff --git a/themes/gold/alarm2.wav b/themes/gold/alarm2.wav
new file mode 100644
index 0000000..5ae54fa
Binary files /dev/null and b/themes/gold/alarm2.wav differ
diff --git a/themes/gold/style.js b/themes/gold/style.js
index c297e37..67105c0 100644
--- a/themes/gold/style.js
+++ b/themes/gold/style.js
@@ -6,25 +6,28 @@
"newmsgicon": "$path/trayicon2.png",
"windowtitle": "PESTERCHUM 7.0",
"close": { "image": "$path/x.png",
- "loc": [315, 26]},
+ "loc": [319, 28]},
"minimize": { "image": "$path/m.png",
- "loc": [300, 32]},
- "menubar": { "style": "font-family: 'Arial'; font:bold; font-size: 12px;" },
- "menu" : { "style": "font-family: 'Arial'; font: bold; font-size: 12px; background-color: #fdb302;border:2px solid #ffff00",
+ "loc": [304, 34]},
+ "menubar": { "style": "font-family: 'Arial'; font:bold; font-size: 12px; color: #000000;" },
+ "menu" : { "style": "font-family: 'Arial'; font: bold; font-size: 12px; color: #000000; background-color: #fdb302;border:2px solid #ffff00",
"menuitem": "margin-right:15px;",
"selected": "background-color: #ffff00",
"loc": [150,22]
},
"sounds": { "alertsound": "$path/alarm.wav",
- "ceasesound": "$path/cease.wav" },
+ "memosound": "$path/alarm2.wav",
+ "ceasesound": "$path/cease.wav" },
"menus": {"client": {"_name": "Client",
"options": "Options",
"memos": "Memos",
"logviewer": "Pesterlogs",
+ "randen": "Random Encounter",
"userlist": "Userlist",
+ "addgroup": "Add Group",
"import": "Import",
- "reconnect": "Reconnect",
- "idle": "Idle",
+ "reconnect": "Reconnect",
+ "idle": "Idle",
"exit": "Exit"},
"profile": {"_name": "Profile",
"switch": "Switch",
@@ -33,7 +36,10 @@
"block": "Trollslum",
"quirks": "Quirks"},
"help": { "_name": "Help",
- "about": "About" },
+ "about": "About",
+ "help": "Help",
+ "calsprite": "CalSprite",
+ "nickserv": "NickServ" },
"rclickchumlist": {"pester": "Pester",
"removechum": "Remove Chum",
"report": "Report",
@@ -41,16 +47,21 @@
"addchum": "Add Chum",
"viewlog": "View Pesterlog",
"unblockchum": "Unblock",
+ "removegroup": "Remove Group",
+ "renamegroup": "Rename Group",
+ "movechum": "Move To",
"banuser": "Ban User",
"opuser": "Make OP",
- "quirksoff": "Quirks Off"
+ "voiceuser": "Give Voice",
+ "quirksoff": "Quirks Off",
+ "invitechum": "Invite Chum"
}
},
"chums": { "style": "border:0px; background-image:url($path/chumbg.png); background-color: rgb(110,110,110); background-repeat: no-repeat; color: white; font-family: 'Arial';selection-background-color:#646464; font-size:14px; ",
"loc": [123, 88],
"size": [190, 65],
"userlistcolor": "white",
- "moods": {
+ "moods": {
"chummy": { "icon": "$path/chummy.png", "color": "white" },
@@ -58,7 +69,7 @@
"offline": { "icon": "$path/offline.png", "color": "#bebebe"},
-
+
"pleasant": { "icon": "$path/pleasant.png", "color": "white" },
"distraught": { "icon": "$path/distraught.png", "color": "white" },
@@ -86,7 +97,7 @@
"devious": { "icon": "$path/devious.png", "color": "red" },
"sleek": { "icon": "$path/sleek.png", "color": "red" },
-
+
"detestful": { "icon": "$path/detestful.png", "color": "red" },
"mirthful": { "icon": "$path/mirthful.png", "color": "red" },
@@ -105,7 +116,7 @@
}
},
- "trollslum": {
+ "trollslum": {
"style": "background: #fdb302; border:2px solid yellow; font-family: 'Arial'",
"size": [195, 200],
"label": { "text": "TROLLSLUM",
@@ -123,7 +134,7 @@
"text": "" },
"currentMood": [129, 176]
},
- "defaultwindow": { "style": "background: #fdb302; font-family:'Arial';font:bold;selection-background-color:#919191; "
+ "defaultwindow": { "style": "background: #fdb302; font-family:'Arial';font:bold;selection-background-color:#919191; "
},
"addchum": { "style": "background: rgba(255, 255, 0, 0%); border:0px; color: rgba(0, 0, 0, 0%);",
"loc": [25,0],
@@ -143,90 +154,90 @@
},
"defaultmood": 0,
"moodlabel": { "style": "",
- "loc": [20, 430],
- "text": "MOODS"
- },
+ "loc": [20, 430],
+ "text": "MOODS"
+ },
"moods": [
- { "style": "border:0px;",
- "selected": "background-image:url($path/moodcheck1.png); border:0px;",
- "loc": [13, 204],
- "size": [101, 27],
- "text": "",
- "icon": "",
- "mood": 0
- },
- { "style": "border:0px;",
- "selected": "background-image:url($path/moodcheck2.png); border:0px;",
- "loc": [13, 231],
- "size": [101, 27],
- "text": "",
- "icon": "",
- "mood": 19
- },
- { "style": "border:0px;",
- "selected": "background-image:url($path/moodcheck3.png); border:0px;",
- "loc": [13, 258],
- "size": [101, 27],
- "text": "",
- "icon": "",
- "mood": 20
- },
- { "style": "border:0px;",
- "selected": "background-image:url($path/moodcheck4.png); border:0px;",
- "loc": [116, 204],
- "size": [101, 27],
- "text": "",
- "icon": "",
- "mood": 21
- },
- { "style": "border:0px;",
- "selected": "background-image:url($path/moodcheck5.png); border:0px;",
- "loc": [116, 231],
- "size": [101, 27],
- "text": "",
- "icon": "",
- "mood": 22
- },
- { "style": "border:0px;",
- "selected": "background-image:url($path/moodcheck6.png); border:0px;",
- "loc": [116, 258],
- "size": [101, 27],
- "text": "",
- "icon": "",
- "mood": 5
- },
- { "style": "border:0px;",
- "selected": "background-image:url($path/moodcheck7.png); border:0px;",
- "loc": [219, 204],
- "size": [101, 27],
- "text": "",
- "icon": "",
- "mood": 6
- },
- { "style": "border:0px;",
- "selected": "background-image:url($path/moodcheck8.png); border:0px;",
- "loc": [219, 231],
- "size": [101, 27],
- "text": "",
- "icon": "",
- "mood": 3
- },
- { "style": "border:0px;",
- "selected": "background-image:url($path/moodcheck9.png); border:0px;",
- "loc": [219, 258],
- "size": [101, 27],
- "text": "",
- "icon": "",
- "mood": 1
- },
- { "style": "border:0px;",
- "selected": "border:0px;",
- "loc": [13, 175],
- "size": [101, 27],
- "text": "",
- "icon": "",
- "mood": 2
- }
+ { "style": "border:0px;",
+ "selected": "background-image:url($path/moodcheck1.png); border:0px;",
+ "loc": [13, 204],
+ "size": [101, 27],
+ "text": "",
+ "icon": "",
+ "mood": 0
+ },
+ { "style": "border:0px;",
+ "selected": "background-image:url($path/moodcheck2.png); border:0px;",
+ "loc": [13, 231],
+ "size": [101, 27],
+ "text": "",
+ "icon": "",
+ "mood": 19
+ },
+ { "style": "border:0px;",
+ "selected": "background-image:url($path/moodcheck3.png); border:0px;",
+ "loc": [13, 258],
+ "size": [101, 27],
+ "text": "",
+ "icon": "",
+ "mood": 20
+ },
+ { "style": "border:0px;",
+ "selected": "background-image:url($path/moodcheck4.png); border:0px;",
+ "loc": [116, 204],
+ "size": [101, 27],
+ "text": "",
+ "icon": "",
+ "mood": 21
+ },
+ { "style": "border:0px;",
+ "selected": "background-image:url($path/moodcheck5.png); border:0px;",
+ "loc": [116, 231],
+ "size": [101, 27],
+ "text": "",
+ "icon": "",
+ "mood": 22
+ },
+ { "style": "border:0px;",
+ "selected": "background-image:url($path/moodcheck6.png); border:0px;",
+ "loc": [116, 258],
+ "size": [101, 27],
+ "text": "",
+ "icon": "",
+ "mood": 5
+ },
+ { "style": "border:0px;",
+ "selected": "background-image:url($path/moodcheck7.png); border:0px;",
+ "loc": [219, 204],
+ "size": [101, 27],
+ "text": "",
+ "icon": "",
+ "mood": 6
+ },
+ { "style": "border:0px;",
+ "selected": "background-image:url($path/moodcheck8.png); border:0px;",
+ "loc": [219, 231],
+ "size": [101, 27],
+ "text": "",
+ "icon": "",
+ "mood": 3
+ },
+ { "style": "border:0px;",
+ "selected": "background-image:url($path/moodcheck9.png); border:0px;",
+ "loc": [219, 258],
+ "size": [101, 27],
+ "text": "",
+ "icon": "",
+ "mood": 1
+ },
+ { "style": "border:0px;",
+ "selected": "border:0px;",
+ "loc": [13, 175],
+ "size": [101, 27],
+ "text": "",
+ "icon": "",
+ "mood": 2
+ }
]
},
"convo":
@@ -234,7 +245,7 @@
"tabstyle": "background-color: #fdb302; font-family: 'Arial'",
"scrollbar": { "style" : "padding-top:17px; padding-bottom:17px;width: 18px; background: rgba(255, 255, 0, 0%); border:0px;",
"handle": "background-color:#c48a00;min-height:20px;",
- "downarrow": "height:17px;border:0px solid #c48a00;",
+ "downarrow": "height:17px;border:0px solid #c48a00;",
"darrowstyle": "image:url($path/downarrow.png);",
"uparrow": "height:17px;border:0px solid #c48a00;",
"uarrowstyle": "image:url($path/uparrow.png);"
@@ -264,12 +275,12 @@
"ceasepester": "ceased pestering",
"blocked": "blocked",
"unblocked": "unblocked",
- "blockedmsg": "did not receive message from",
+ "blockedmsg": "did not receive message from",
"openmemo": "opened memo on board",
"joinmemo": "responded to memo",
"closememo": "ceased responding to memo",
"kickedmemo": "You have been banned from this memo!",
- "idle": "is now an idle chum!"
+ "idle": "is now an idle chum!"
},
"systemMsgColor": "#646464"
},
@@ -285,7 +296,7 @@
},
"scrollbar": { "style" : "padding-top:17px; padding-bottom:17px;width: 18px; background: rgba(255, 255, 0, 0%); border:0px;",
"handle": "background-color:#c48a00;min-height:20px;",
- "downarrow": "height:17px;border:0px solid #c48a00;",
+ "downarrow": "height:17px;border:0px solid #c48a00;",
"darrowstyle": "image:url($path/downarrow.png);",
"uparrow": "height:17px;border:0px solid #c48a00;",
"uarrowstyle": "image:url($path/uparrow.png);"
@@ -302,20 +313,21 @@
"userlist": { "width": 150,
"style": "border:2px solid #c48a00; background: white; font-family: 'Arial';selection-background-color:#646464; font-size: 14px; margin-left:0px; margin-right:10px;"
},
- "time": { "text": { "width": 75,
- "style": " border: 2px solid yellow; background: white; font-size: 12px; margin-top: 5px; margin-right: 5px; margin-left: 5px; font-family:'Arial';font:bold;"
+ "time": { "text": { "width": 75,
+ "style": " border: 2px solid yellow; background: white; font-size: 12px; margin-top: 5px; margin-right: 5px; margin-left: 5px; font-family:'Arial';font:bold;"
},
"slider": { "style": "border: 0px;",
"groove": "",
"handle": ""
},
- "buttons": { "style": "color: black; font: bold; border: 2px solid #c48a00; font-size: 12px; background: yellow; margin-top: 5px; margin-right: 5px; margin-left: 5px; padding: 2px; width: 50px;" },
- "arrows": { "left": "$path/leftarrow.png",
+ "buttons": { "style": "color: black; font: bold; border: 2px solid #c48a00; font-size: 12px; background: yellow; margin-top: 5px; margin-right: 5px; margin-left: 5px; padding: 2px; width: 50px;" },
+ "arrows": { "left": "$path/leftarrow.png",
"right": "$path/rightarrow.png",
- "style": " border:0px; margin-top: 5px; margin-right:10px;"
+ "style": " border:0px; margin-top: 5px; margin-right:10px;"
}
},
"systemMsgColor": "#646464",
- "op": { "icon": "$path/smooth.png" }
+ "op": { "icon": "$path/smooth.png" },
+ "voice": { "icon": "$path/voice.png" }
}
-}
\ No newline at end of file
+}
diff --git a/themes/gold/voice.png b/themes/gold/voice.png
new file mode 100644
index 0000000..c400a6c
Binary files /dev/null and b/themes/gold/voice.png differ
diff --git a/themes/honk.wav b/themes/honk.wav
new file mode 100644
index 0000000..7142ac8
Binary files /dev/null and b/themes/honk.wav differ
diff --git a/themes/namealarm.wav b/themes/namealarm.wav
new file mode 100644
index 0000000..10e6ad0
Binary files /dev/null and b/themes/namealarm.wav differ
diff --git a/themes/pesterchum/alarm2.wav b/themes/pesterchum/alarm2.wav
new file mode 100644
index 0000000..5ae54fa
Binary files /dev/null and b/themes/pesterchum/alarm2.wav differ
diff --git a/themes/pesterchum/style.js b/themes/pesterchum/style.js
index 35127de..550680e 100644
--- a/themes/pesterchum/style.js
+++ b/themes/pesterchum/style.js
@@ -6,25 +6,28 @@
"newmsgicon": "$path/trayicon2.png",
"windowtitle": "PESTERCHUM 6.0",
"close": { "image": "$path/x.png",
- "loc": [210, 2]},
+ "loc": [214, 4]},
"minimize": { "image": "$path/m.png",
- "loc": [194, 8]},
- "menubar": { "style": "font-family: 'Courier'; font:bold; font-size: 12px;" },
- "menu" : { "style": "font-family: 'Courier'; font: bold; font-size: 12px; background-color: #fdb302;border:2px solid #ffff00",
+ "loc": [198, 10]},
+ "menubar": { "style": "font-family: 'Courier'; font:bold; font-size: 12px; color: black;" },
+ "menu" : { "style": "font-family: 'Courier'; font: bold; font-size: 12px; color: black; background-color: #fdb302;border:2px solid #ffff00",
"menuitem": "margin-right:10px;",
"selected": "background-color: #ffff00",
"loc": [10,0]
},
"sounds": { "alertsound": "$path/alarm.wav",
- "ceasesound": "$path/cease.wav" },
+ "memosound": "$path/alarm2.wav",
+ "ceasesound": "$path/cease.wav" },
"menus": {"client": {"_name": "CLIENT",
"options": "OPTIONS",
"memos": "MEMOS",
"logviewer": "PESTERLOGS",
+ "randen": "RANDOM ENCOUNTER",
"userlist": "USERLIST",
+ "addgroup": "ADD GROUP",
"import": "IMPORT",
"reconnect": "RECONNECT",
- "idle": "IDLE",
+ "idle": "IDLE",
"exit": "EXIT"},
"profile": {"_name": "PROFILE",
"switch": "SWITCH",
@@ -33,7 +36,10 @@
"block": "TROLLSLUM",
"quirks": "QUIRKS"},
"help": { "_name": "HELP",
- "about": "ABOUT" },
+ "about": "ABOUT",
+ "help": "HELP",
+ "calsprite": "CALSPRITE",
+ "nickserv": "NICKSERV" },
"rclickchumlist": {"pester": "PESTER",
"removechum": "REMOVE CHUM",
"report": "REPORT",
@@ -41,16 +47,21 @@
"addchum": "ADD CHUM",
"viewlog": "VIEW PESTERLOG",
"unblockchum": "UNBLOCK",
+ "removegroup": "REMOVE GROUP",
+ "renamegroup": "RENAME GROUP",
+ "movechum": "MOVE TO",
"banuser": "BAN USER",
"opuser": "MAKE OP",
- "quirksoff": "QUIRKS OFF"
+ "voiceuser": "GIVE VOICE",
+ "quirksoff": "QUIRKS OFF",
+ "invitechum": "INVITE CHUM"
}
},
"chums": { "style": "border:2px solid yellow; background-color: black;color: white;font: bold;font-family: 'Courier';selection-background-color:#646464; ",
"loc": [12, 117],
"size": [209, 82],
"userlistcolor": "white",
- "moods": {
+ "moods": {
"chummy": { "icon": "$path/chummy.png", "color": "white" },
@@ -58,7 +69,7 @@
"offline": { "icon": "$path/offline.png", "color": "#646464"},
-
+
"pleasant": { "icon": "$path/pleasant.png", "color": "white" },
"distraught": { "icon": "$path/distraught.png", "color": "white" },
@@ -86,7 +97,7 @@
"devious": { "icon": "$path/devious.png", "color": "red" },
"sleek": { "icon": "$path/sleek.png", "color": "red" },
-
+
"detestful": { "icon": "$path/detestful.png", "color": "red" },
"mirthful": { "icon": "$path/mirthful.png", "color": "red" },
@@ -105,7 +116,7 @@
}
},
- "trollslum": {
+ "trollslum": {
"style": "background: #fdb302; border:2px solid yellow; font-family: 'Courier'",
"size": [195, 200],
"label": { "text": "TROLLSLUM",
@@ -123,7 +134,7 @@
"text": "" },
"currentMood": [18, 249]
},
- "defaultwindow": { "style": "background: #fdb302; font-family:'Courier';font:bold;selection-background-color:#919191; "
+ "defaultwindow": { "style": "background: #fdb302; font-family:'Courier';font:bold;selection-background-color:#919191; "
},
"addchum": { "style": "background: rgba(255, 255, 0, 0%); border:2px solid #c48a00; font: bold; color: rgba(0, 0, 0, 0%); font-family:'Courier';",
"pressed" : "background: rgb(255, 255, 255, 30%);",
@@ -137,7 +148,7 @@
"size": [71, 22],
"text": ""
},
- "block": { "style": "background: rgba(255, 255, 0, 0%); border:2px solid #c48a00; font: bold; color: rgba(255, 255, 0, 0%); font-family:'Courier';",
+ "block": { "style": "background: rgba(255, 255, 0, 0%); border:2px solid #c48a00; font: bold; color: rgba(255, 255, 0, 0%); font-family:'Courier';",
"pressed" : "background: rgb(255, 255, 255, 30%);",
"loc": [81,202],
"size": [71, 22],
@@ -145,73 +156,73 @@
},
"defaultmood": 0,
"moodlabel": { "style": "",
- "loc": [20, 430],
- "text": "MOODS"
- },
+ "loc": [20, 430],
+ "text": "MOODS"
+ },
"moods": [
- { "style": "text-align:left; border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
- "selected": "text-align:left; background-image:url($path/moodcheck1.png); border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
- "loc": [12, 288],
- "size": [104, 22],
- "text": "CHUMMY",
- "icon": "$path/chummy.png",
- "mood": 0
- },
- { "style": "text-align:left; border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
- "selected": "text-align:left; background-image:url($path/moodcheck2.png); border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
- "loc": [12, 308],
- "size": [104, 22],
- "text": "PALSY",
- "icon": "$path/chummy.png",
- "mood": 3
- },
- { "style": "text-align:left; border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
- "selected": "text-align:left; background-image:url($path/moodcheck3.png); border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
- "loc": [12, 328],
- "size": [104, 22],
- "text": "CHIPPER",
- "icon": "$path/chummy.png",
- "mood": 4
- },
- { "style": "text-align:left; border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
- "selected": "text-align:left; background-image:url($path/moodcheck2.png); border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
- "loc": [117, 288],
- "size": [104, 22],
- "text": "BULLY",
- "icon": "$path/chummy.png",
- "mood": 5
- },
- { "style": "text-align:left; border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
- "selected": "text-align:left; background-image:url($path/moodcheck2.png); border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
- "loc": [117, 308],
- "size": [104, 22],
- "text": "PEPPY",
- "icon": "$path/chummy.png",
- "mood": 6
- },
- { "style": "text-align:left; border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
- "selected": "text-align:left; background-image:url($path/moodcheck4.png); border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
- "loc": [117, 328],
- "size": [104, 22],
- "text": "RANCOROUS",
- "icon": "$path/rancorous.png",
- "mood": 1
- },
- { "style": "text-align:left; border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
- "selected": "text-align:left; background-image:url($path/moodcheck5.png); border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
- "loc": [12, 348],
- "size": [209, 22],
- "text": "ABSCOND",
- "icon": "",
- "mood": 2
- }
+ { "style": "text-align:left; border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
+ "selected": "text-align:left; background-image:url($path/moodcheck1.png); border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
+ "loc": [12, 288],
+ "size": [104, 22],
+ "text": "CHUMMY",
+ "icon": "$path/chummy.png",
+ "mood": 0
+ },
+ { "style": "text-align:left; border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
+ "selected": "text-align:left; background-image:url($path/moodcheck2.png); border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
+ "loc": [12, 308],
+ "size": [104, 22],
+ "text": "PALSY",
+ "icon": "$path/chummy.png",
+ "mood": 3
+ },
+ { "style": "text-align:left; border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
+ "selected": "text-align:left; background-image:url($path/moodcheck3.png); border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
+ "loc": [12, 328],
+ "size": [104, 22],
+ "text": "CHIPPER",
+ "icon": "$path/chummy.png",
+ "mood": 4
+ },
+ { "style": "text-align:left; border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
+ "selected": "text-align:left; background-image:url($path/moodcheck2.png); border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
+ "loc": [117, 288],
+ "size": [104, 22],
+ "text": "BULLY",
+ "icon": "$path/chummy.png",
+ "mood": 5
+ },
+ { "style": "text-align:left; border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
+ "selected": "text-align:left; background-image:url($path/moodcheck2.png); border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
+ "loc": [117, 308],
+ "size": [104, 22],
+ "text": "PEPPY",
+ "icon": "$path/chummy.png",
+ "mood": 6
+ },
+ { "style": "text-align:left; border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
+ "selected": "text-align:left; background-image:url($path/moodcheck4.png); border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
+ "loc": [117, 328],
+ "size": [104, 22],
+ "text": "RANCOROUS",
+ "icon": "$path/rancorous.png",
+ "mood": 1
+ },
+ { "style": "text-align:left; border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
+ "selected": "text-align:left; background-image:url($path/moodcheck5.png); border:2px solid #c48a00; padding: 5px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
+ "loc": [12, 348],
+ "size": [209, 22],
+ "text": "ABSCOND",
+ "icon": "",
+ "mood": 2
+ }
]
},
"convo":
{"style": "background-color: #fdb302;background-image:url($path/convobg.png);background-repeat: no-repeat; border:2px solid yellow; font-family: 'Courier'",
"scrollbar": { "style" : "padding-top:17px; padding-bottom:17px;width: 18px; background: white; border:2px solid #c48a00;",
"handle": "background-color:#c48a00;min-height:20px;",
- "downarrow": "height:17px;border:0px solid #c48a00;",
+ "downarrow": "height:17px;border:0px solid #c48a00;",
"darrowstyle": "image:url($path/downarrow.png);",
"uparrow": "height:17px;border:0px solid #c48a00;",
"uarrowstyle": "image:url($path/uparrow.png);"
@@ -231,7 +242,7 @@
"style": "background: white; border:2px solid #c48a00;margin-top:5px; margin-right:10px; margin-left:10px; font-size: 12px;font-family: 'Courier'"
},
"tabwindow" : {
- "style": "background-color:#fdb302;border:0px"
+ "style": "background-color:#fdb302;border:0px"
},
"tabs": {
"style": "background-color: #7f7f7f; font-family: 'Courier';font:bold;font-size:12px;min-height:25px;",
@@ -244,12 +255,12 @@
"ceasepester": "ceased pestering",
"blocked": "blocked",
"unblocked": "unblocked",
- "blockedmsg": "did not receive message from",
+ "blockedmsg": "did not receive message from",
"openmemo": "opened memo on board",
"joinmemo": "responded to memo",
"closememo": "ceased responding to memo",
"kickedmemo": "You have been banned from this memo!",
- "idle": "is now an idle chum!"
+ "idle": "is now an idle chum!"
},
"systemMsgColor": "#646464"
},
@@ -265,7 +276,7 @@
},
"scrollbar": { "style" : "padding-top:17px; padding-bottom:17px;width: 18px; background: rgba(255, 255, 0, 0%); border:0px;",
"handle": "background-color:#c48a00;min-height:20px;",
- "downarrow": "height:17px;border:0px solid #c48a00;",
+ "downarrow": "height:17px;border:0px solid #c48a00;",
"darrowstyle": "image:url($path/downarrow.png);",
"uparrow": "height:17px;border:0px solid #c48a00;",
"uarrowstyle": "image:url($path/uparrow.png);"
@@ -282,20 +293,21 @@
"userlist": { "width": 150,
"style": "border:2px solid #c48a00; background: white;font: bold;font-family: 'Courier';selection-background-color:#646464; font-size: 12px; margin-left:0px; margin-right:10px;"
},
- "time": { "text": { "width": 75,
- "style": " border: 2px solid yellow; background: white; font-size: 12px; margin-top: 5px; margin-right: 5px; margin-left: 5px; font-family:'Courier';font:bold;"
+ "time": { "text": { "width": 75,
+ "style": " border: 2px solid yellow; background: white; font-size: 12px; margin-top: 5px; margin-right: 5px; margin-left: 5px; font-family:'Courier';font:bold;"
},
"slider": { "style": "border: 0px;",
"groove": "",
"handle": ""
},
- "buttons": { "style": "color: black; font: bold; border: 2px solid #c48a00; font: bold; font-size: 12px; background: yellow; margin-top: 5px; margin-right: 5px; margin-left: 5px; padding: 2px; width: 50px;" },
- "arrows": { "left": "$path/leftarrow.png",
+ "buttons": { "style": "color: black; font: bold; border: 2px solid #c48a00; font: bold; font-size: 12px; background: yellow; margin-top: 5px; margin-right: 5px; margin-left: 5px; padding: 2px; width: 50px;" },
+ "arrows": { "left": "$path/leftarrow.png",
"right": "$path/rightarrow.png",
- "style": " border:0px; margin-top: 5px; margin-right:10px;"
+ "style": " border:0px; margin-top: 5px; margin-right:10px;"
}
},
"systemMsgColor": "#646464",
- "op": { "icon": "$path/op.png" }
+ "op": { "icon": "$path/op.png" },
+ "voice": { "icon": "$path/voice.png" }
}
-}
\ No newline at end of file
+}
diff --git a/themes/pesterchum/voice.png b/themes/pesterchum/voice.png
new file mode 100644
index 0000000..c400a6c
Binary files /dev/null and b/themes/pesterchum/voice.png differ
diff --git a/themes/pesterchum2.5/alarm2.wav b/themes/pesterchum2.5/alarm2.wav
new file mode 100644
index 0000000..5ae54fa
Binary files /dev/null and b/themes/pesterchum2.5/alarm2.wav differ
diff --git a/themes/pesterchum2.5/style.js b/themes/pesterchum2.5/style.js
index 7e9ec98..83c3863 100644
--- a/themes/pesterchum2.5/style.js
+++ b/themes/pesterchum2.5/style.js
@@ -6,26 +6,26 @@
"icon": "$path/trayicon.png",
"newmsgicon": "$path/trayicon2.png",
"windowtitle": "PESTERCHUM",
- "menu" : { "style": "font-family: 'Courier'; font: bold; font-size: 14px; background-color: #fdb302;border:2px solid #ffff00",
- "menuitem": "font-size:14px;" },
- "menubar": { "style": "font-family: 'Courier'; font:bold; font-size: 14px;" },
+ "menu" : { "style": "font-family: 'Courier'; font: bold; font-size: 14px; color: #000000; background-color: #fdb302;border:2px solid #ffff00",
+ "menuitem": "font-size:14px;" },
+ "menubar": { "style": "font-family: 'Courier'; font:bold; font-size: 14px; color: #000000;" },
"close": { "image": "$path/x.png",
- "loc": [282, 4]},
+ "loc": [286, 6]},
"minimize": { "image": "$path/m.png",
- "loc": [264, 10]},
+ "loc": [268, 12]},
"chums": { "style": "border:2px solid yellow; background-color: black;color: white;font: bold;font-size:14px;font-family: 'Courier';selection-background-color:#646464; ",
- "loc": [15, 70],
- "size": [270, 300]
+ "loc": [15, 70],
+ "size": [270, 300]
},
- "mychumhandle": { "label":
+ "mychumhandle": { "label":
{ "text": "CHUMHANDLE:",
"loc": [12,415],
- "style": "color: black ;font:bold; font-family: 'Courier';"
+ "style": "color: black ;font:bold; font-family: 'Courier';"
},
"handle": { "loc": [15,435],
"size": [240, 25],
- "style": "background-color: black; padding: 3px; padding-left: 25px; color:white; font-family:'Courier'; font:bold; text-align:left; border:2px solid #ffff00;"
- },
+ "style": "background-color: black; padding: 3px; padding-left: 25px; color:white; font-family:'Courier'; font:bold; text-align:left; border:2px solid #ffff00;"
+ },
"colorswatch": { "loc": [255,435],
"size": [30,25],
"text": "C" },
@@ -47,72 +47,72 @@
"text": "BLOCK"
},
"moodlabel": { "style": "font:bold;font-family:'Courier';color:black;",
- "loc": [12, 466],
- "text": "MOOD:"
- },
+ "loc": [12, 466],
+ "text": "MOOD:"
+ },
"moods": [
- { "style": "text-align:left; background:#ffff00;border:2px solid #c48a00;color: black; font-family:'Courier'; font:bold; padding-left:3px;",
- "selected": "text-align:left; background:white; border:2px solid #c48a00; color: black; font-family:'Courier'; font:bold; padding-left:3px;",
- "loc": [15, 485],
- "size": [135, 30],
- "text": "CHUMMY",
- "icon": "$path/chummy.png",
- "mood": 0
- },
+ { "style": "text-align:left; background:#ffff00;border:2px solid #c48a00;color: black; font-family:'Courier'; font:bold; padding-left:3px;",
+ "selected": "text-align:left; background:white; border:2px solid #c48a00; color: black; font-family:'Courier'; font:bold; padding-left:3px;",
+ "loc": [15, 485],
+ "size": [135, 30],
+ "text": "CHUMMY",
+ "icon": "$path/chummy.png",
+ "mood": 0
+ },
- { "style": "text-align:left; background:#ffff00;border:2px solid #c48a00;color: black; font-family:'Courier'; font:bold;padding-left:3px;",
- "selected": "text-align:left; background:white; border:2px solid #c48a00; color: black; font-family:'Courier'; font:bold;padding-left:3px;",
- "loc": [15, 513],
- "size": [135, 30],
- "text": "PLEASANT",
- "icon": "$path/pleasant.png",
- "mood": 3
- },
+ { "style": "text-align:left; background:#ffff00;border:2px solid #c48a00;color: black; font-family:'Courier'; font:bold;padding-left:3px;",
+ "selected": "text-align:left; background:white; border:2px solid #c48a00; color: black; font-family:'Courier'; font:bold;padding-left:3px;",
+ "loc": [15, 513],
+ "size": [135, 30],
+ "text": "PLEASANT",
+ "icon": "$path/pleasant.png",
+ "mood": 3
+ },
- { "style": "text-align:left; background:#ffff00;border:2px solid #c48a00;color: black; font-family:'Courier'; font:bold;padding-left:3px;",
- "selected": "text-align:left; background:white; border:2px solid #c48a00; color: black; font-family:'Courier'; font:bold;padding-left:3px;",
- "loc": [15, 541],
- "size": [135, 30],
- "text": "DISTRAUGHT",
- "icon": "$path/distraught.png",
- "mood": 4
- },
+ { "style": "text-align:left; background:#ffff00;border:2px solid #c48a00;color: black; font-family:'Courier'; font:bold;padding-left:3px;",
+ "selected": "text-align:left; background:white; border:2px solid #c48a00; color: black; font-family:'Courier'; font:bold;padding-left:3px;",
+ "loc": [15, 541],
+ "size": [135, 30],
+ "text": "DISTRAUGHT",
+ "icon": "$path/distraught.png",
+ "mood": 4
+ },
- { "style": "text-align:left; background:#ffff00;border:2px solid #c48a00;color: black; font-family:'Courier'; font:bold;padding-left:3px;",
- "selected": "text-align:left; background:white; border:2px solid #c48a00; color: black; font-family:'Courier'; font:bold;padding-left:3px;",
- "loc": [148, 485],
- "size": [135, 30],
- "text": "PRANKY",
- "icon": "$path/pranky.png",
- "mood": 5
- },
+ { "style": "text-align:left; background:#ffff00;border:2px solid #c48a00;color: black; font-family:'Courier'; font:bold;padding-left:3px;",
+ "selected": "text-align:left; background:white; border:2px solid #c48a00; color: black; font-family:'Courier'; font:bold;padding-left:3px;",
+ "loc": [148, 485],
+ "size": [135, 30],
+ "text": "PRANKY",
+ "icon": "$path/pranky.png",
+ "mood": 5
+ },
- { "style": "text-align:left; background:#ffff00;border:2px solid #c48a00;color: black; font-family:'Courier'; font:bold;padding-left:3px;",
- "selected": "text-align:left; background:white; border:2px solid #c48a00; color: black; font-family:'Courier'; font:bold;padding-left:3px;",
- "loc": [148, 513],
- "size": [135, 30],
- "text": "SMOOTH",
- "icon": "$path/smooth.png",
- "mood": 6
- },
+ { "style": "text-align:left; background:#ffff00;border:2px solid #c48a00;color: black; font-family:'Courier'; font:bold;padding-left:3px;",
+ "selected": "text-align:left; background:white; border:2px solid #c48a00; color: black; font-family:'Courier'; font:bold;padding-left:3px;",
+ "loc": [148, 513],
+ "size": [135, 30],
+ "text": "SMOOTH",
+ "icon": "$path/smooth.png",
+ "mood": 6
+ },
- { "style": "text-align:left; background:#f00000;border:2px solid #c48a00;color: black; font-family:'Courier'; font:bold;padding-left:3px;",
- "selected": "text-align:left; background:white; border:2px solid #c48a00; color: red; font-family:'Courier'; font:bold;padding-left:3px;",
- "loc": [148, 541],
- "size": [135, 30],
- "text": "RANCOROUS",
- "icon": "$path/rancorous.png",
- "mood": 1
- },
+ { "style": "text-align:left; background:#f00000;border:2px solid #c48a00;color: black; font-family:'Courier'; font:bold;padding-left:3px;",
+ "selected": "text-align:left; background:white; border:2px solid #c48a00; color: red; font-family:'Courier'; font:bold;padding-left:3px;",
+ "loc": [148, 541],
+ "size": [135, 30],
+ "text": "RANCOROUS",
+ "icon": "$path/rancorous.png",
+ "mood": 1
+ },
- { "style": "text-align:center; border:2px solid #c48a00; background:black;color: white; font-family:'Courier'; font:bold;padding-left:3px;",
- "selected": "text-align:left; background:white; border:2px solid #c48a00; padding: 5px;color: black; font-family:'Courier'; font:bold;padding-left:3px;",
- "loc": [15, 569],
- "size": [270, 30],
- "text": "ABSCOND",
- "icon": "$path/offline.png",
- "mood": 2
- }
+ { "style": "text-align:center; border:2px solid #c48a00; background:black;color: white; font-family:'Courier'; font:bold;padding-left:3px;",
+ "selected": "text-align:left; background:white; border:2px solid #c48a00; padding: 5px;color: black; font-family:'Courier'; font:bold;padding-left:3px;",
+ "loc": [15, 569],
+ "size": [270, 30],
+ "text": "ABSCOND",
+ "icon": "$path/offline.png",
+ "mood": 2
+ }
]
},
"convo": {
@@ -127,4 +127,4 @@
},
"memos":
{ "size": [600,425] }
-}
\ No newline at end of file
+}
diff --git a/themes/pesterchum2.5/voice.png b/themes/pesterchum2.5/voice.png
new file mode 100644
index 0000000..c400a6c
Binary files /dev/null and b/themes/pesterchum2.5/voice.png differ
diff --git a/themes/trollian/moodcheck1.png b/themes/trollian/moodcheck1.png
index 121b5d9..828faab 100644
Binary files a/themes/trollian/moodcheck1.png and b/themes/trollian/moodcheck1.png differ
diff --git a/themes/trollian/moodcheck10.png b/themes/trollian/moodcheck10.png
index 381b192..d88bfb2 100644
Binary files a/themes/trollian/moodcheck10.png and b/themes/trollian/moodcheck10.png differ
diff --git a/themes/trollian/moodcheck11.png b/themes/trollian/moodcheck11.png
index acf46b9..c0c343f 100644
Binary files a/themes/trollian/moodcheck11.png and b/themes/trollian/moodcheck11.png differ
diff --git a/themes/trollian/moodcheck12.png b/themes/trollian/moodcheck12.png
index 8b19362..1f8fdff 100644
Binary files a/themes/trollian/moodcheck12.png and b/themes/trollian/moodcheck12.png differ
diff --git a/themes/trollian/moodcheck2.png b/themes/trollian/moodcheck2.png
index eba1b5e..244edb3 100644
Binary files a/themes/trollian/moodcheck2.png and b/themes/trollian/moodcheck2.png differ
diff --git a/themes/trollian/moodcheck3.png b/themes/trollian/moodcheck3.png
index a9fec2e..d7e4d22 100644
Binary files a/themes/trollian/moodcheck3.png and b/themes/trollian/moodcheck3.png differ
diff --git a/themes/trollian/moodcheck4.png b/themes/trollian/moodcheck4.png
index 02acef5..6b54dbe 100644
Binary files a/themes/trollian/moodcheck4.png and b/themes/trollian/moodcheck4.png differ
diff --git a/themes/trollian/moodcheck5.png b/themes/trollian/moodcheck5.png
index d5855bc..098c211 100644
Binary files a/themes/trollian/moodcheck5.png and b/themes/trollian/moodcheck5.png differ
diff --git a/themes/trollian/moodcheck6.png b/themes/trollian/moodcheck6.png
index eacc11f..589015b 100644
Binary files a/themes/trollian/moodcheck6.png and b/themes/trollian/moodcheck6.png differ
diff --git a/themes/trollian/moodcheck7.png b/themes/trollian/moodcheck7.png
index 53275b8..74510a5 100644
Binary files a/themes/trollian/moodcheck7.png and b/themes/trollian/moodcheck7.png differ
diff --git a/themes/trollian/moodcheck8.png b/themes/trollian/moodcheck8.png
index b014b2a..1ea6e1e 100644
Binary files a/themes/trollian/moodcheck8.png and b/themes/trollian/moodcheck8.png differ
diff --git a/themes/trollian/moodcheck9.png b/themes/trollian/moodcheck9.png
index 97cab63..49c514a 100644
Binary files a/themes/trollian/moodcheck9.png and b/themes/trollian/moodcheck9.png differ
diff --git a/themes/trollian/style.js b/themes/trollian/style.js
index ac22821..6a1f1c5 100644
--- a/themes/trollian/style.js
+++ b/themes/trollian/style.js
@@ -6,23 +6,27 @@
"newmsgicon": "$path/trayicon2.png",
"windowtitle": "TROLLIAN",
"close": { "image": "$path/x.png",
- "loc": [635, 2]},
+ "loc": [639, 4]},
"minimize": { "image": "$path/m.png",
- "loc": [621, 8]},
+ "loc": [625, 10]},
"menubar": { "style": "font-family: 'Arial'; font-size: 11px; color: rgba(0,0,0,0);" },
- "menu" : { "style": "font-family: 'Arial'; font-size: 11px; background-color: #c2c2c2; border:1px solid #545454;",
+ "menu" : { "style": "font-family: 'Arial'; font-size: 11px; color: #000000; background-color: #c2c2c2; border:1px solid #545454;",
"selected": "background-color: #545454",
"menuitem": "margin-right:14px;",
"loc": [14,90]
},
- "sounds": { "alertsound": "$path/alarm.wav" },
+ "sounds": { "alertsound": "$path/alarm.wav",
+ "memosound": "$path/alarm2.wav"},
"menus": {"client": {"_name": "Trollian",
"options": "Options",
"memos": "Memos",
"logviewer": "Pesterlogs",
+ "randen": "Random Encounter",
"userlist": "Fresh Targets",
+ "addgroup": "Add Group",
"import": "import U2;",
- "idle": "Idle",
+ "reconnect": "Reconnect",
+ "idle": "Idle",
"exit": "Abscond"},
"profile": {"_name": "View",
"switch": "Trolltag",
@@ -31,7 +35,10 @@
"block": "Chumpdump",
"quirks": "Annoying" },
"help": { "_name": "Help",
- "about": "About" },
+ "about": "About",
+ "help": "Help",
+ "calsprite": "CalSprite",
+ "nickserv": "NickServ" },
"rclickchumlist": {"pester": "Troll",
"removechum": "Trash",
"report": "Remove",
@@ -39,9 +46,14 @@
"addchum": "Add Chump",
"viewlog": "View Pesterlog",
"unblockchum": "Mercy",
+ "removegroup": "Remove Group",
+ "renamegroup": "Rename Group",
+ "movechum": "Move To",
"banuser": "Ban",
"opuser": "Promote",
- "quirksoff": "Quirks Off" }
+ "voiceuser": "Let Speak",
+ "quirksoff": "Quirks Off",
+ "invitechum": "Invite Chump" }
},
"chums": { "style": "font-size: 12px; background: white; border:0px; font-family: 'Arial';selection-background-color:rgb(200,200,200); ",
"scrollbar": { "style" : "background-color:#c2c2c2;",
@@ -55,50 +67,50 @@
"size": [171, 357],
"userlistcolor": "black",
"moods": {
-
+
"chummy": { "icon": "$path/chummy.png", "color": "#63ea00" },
-
+
"rancorous": { "icon": "$path/rancorous.png", "color": "#7f7f7f" },
-
+
"offline": { "icon": "$path/offline.png", "color": "black"},
-
-
+
+
"pleasant": { "icon": "$path/pleasant.png", "color": "#d69df8" },
-
+
"distraught": { "icon": "$path/distraught.png", "color": "#706eba" },
-
+
"pranky": { "icon": "$path/pranky.png", "color": "blue" },
-
-
+
+
"smooth": { "icon": "$path/smooth.png", "color": "red" },
-
-
+
+
"ecstatic": { "icon": "$path/ecstatic.png", "color": "#99004d" },
-
+
"relaxed": { "icon": "$path/relaxed.png", "color": "#078446" },
-
+
"discontent": { "icon": "$path/discontent.png", "color": "#a75403" },
-
+
"devious": { "icon": "$path/devious.png", "color": "#008282" },
-
+
"sleek": { "icon": "$path/sleek.png", "color": "#a1a100" },
-
+
"detestful": { "icon": "$path/detestful.png", "color": "#6a006a" },
-
+
"mirthful": { "icon": "$path/mirthful.png", "color": "#450077" },
-
+
"manipulative": { "icon": "$path/manipulative.png", "color": "#004182" },
-
+
"vigorous": { "icon": "$path/vigorous.png", "color": "#0021cb" },
-
+
"perky": { "icon": "$path/perky.png", "color": "#406600" },
-
+
"acceptant": { "icon": "$path/acceptant.png", "color": "#a10000" },
-
+
"protective": { "icon": "$path/protective.png", "color": "white" },
-
+
"blocked": { "icon": "$path/blocked.png", "color": "black" }
-
+
}
},
"trollslum": {
@@ -143,101 +155,101 @@
"moods": [
{ "style": "border:0px;",
"selected": "background-image:url($path/moodcheck1.png); border:0px;",
- "loc": [25, 141],
- "size": [20, 270],
+ "loc": [16, 141],
+ "size": [38, 270],
"text": "",
"icon": "",
"mood": 17
},
{ "style": "border:0px;",
"selected": "background-image:url($path/moodcheck2.png); border:0px;",
- "loc": [60, 141],
- "size": [20, 270],
+ "loc": [51, 141],
+ "size": [38, 270],
"text": "",
"icon": "",
"mood": 9
},
{ "style": "border:0px;",
"selected": "background-image:url($path/moodcheck3.png); border:0px;",
- "loc": [95, 141],
- "size": [20, 270],
+ "loc": [86, 141],
+ "size": [38, 270],
"text": "",
"icon": "",
"mood": 11
},
{ "style": "border:0px;",
"selected": "background-image:url($path/moodcheck4.png); border:0px;",
- "loc": [130, 141],
- "size": [20, 270],
+ "loc": [121, 141],
+ "size": [38, 270],
"text": "",
"icon": "",
"mood": 1
},
{ "style": "border:0px;",
"selected": "background-image:url($path/moodcheck5.png); border:0px;",
- "loc": [165, 141],
- "size": [20, 270],
+ "loc": [156, 141],
+ "size": [38, 270],
"text": "",
"icon": "",
"mood": 16
},
{ "style": "border:0px;",
"selected": "background-image:url($path/moodcheck6.png); border:0px;",
- "loc": [200, 141],
- "size": [20, 270],
+ "loc": [191, 141],
+ "size": [38, 270],
"text": "",
"icon": "",
"mood": 8
},
{ "style": "border:0px;",
"selected": "background-image:url($path/moodcheck7.png); border:0px;",
- "loc": [235, 141],
- "size": [20, 270],
+ "loc": [226, 141],
+ "size": [38, 270],
"text": "",
"icon": "",
"mood": 10
},
{ "style": "border:0px;",
"selected": "background-image:url($path/moodcheck8.png); border:0px;",
- "loc": [270, 141],
- "size": [20, 270],
+ "loc": [261, 141],
+ "size": [38, 270],
"text": "",
"icon": "",
"mood": 14
},
{ "style": "border:0px;",
"selected": "background-image:url($path/moodcheck9.png); border:0px;",
- "loc": [305, 141],
- "size": [20, 270],
+ "loc": [296, 141],
+ "size": [38, 270],
"text": "",
"icon": "",
"mood": 15
},
{ "style": "border:0px;",
"selected": "background-image:url($path/moodcheck10.png); border:0px;",
- "loc": [340, 141],
- "size": [20, 270],
+ "loc": [331, 141],
+ "size": [38, 270],
"text": "",
"icon": "",
"mood": 13
},
{ "style": "border:0px;",
"selected": "background-image:url($path/moodcheck11.png); border:0px;",
- "loc": [375, 141],
- "size": [20, 270],
+ "loc": [366, 141],
+ "size": [38, 270],
"text": "",
"icon": "",
"mood": 12
},
{ "style": "border:0px;",
"selected": "background-image:url($path/moodcheck12.png); border:0px;",
- "loc": [410, 141],
- "size": [20, 270],
+ "loc": [401, 141],
+ "size": [38, 270],
"text": "",
"icon": "",
"mood": 7
},
-
+
{ "style": "border:0px;color: rgba(0, 0, 0, 0%);",
"selected": "border:0px; color: rgba(0, 0, 0, 0%);",
"loc": [12, 117],
@@ -264,10 +276,10 @@
"style": "background: white; border:2px solid #c2c2c2; font-size: 14px;"
},
"input": {
- "style": "background: white;margin-top:5px; border:1px solid #c2c2c2; margin-right: 54px; font-size: 12px; height: 19px;"
+ "style": "background: white;margin-top:5px; border:1px solid #c2c2c2; margin-right: 54px; font-size: 12px;"
},
"tabwindow" : {
- "style": "background: rgb(190, 19, 4); font-family: 'Arial'"
+ "style": "background: rgb(190, 19, 4); font-family: 'Arial'"
},
"tabs": {
"style": "",
@@ -307,26 +319,27 @@
"style": "background: white; border:2px solid #c2c2c2; font-size: 12px;"
},
"input": {
- "style": "background: white;margin-top:5px; border:1px solid #c2c2c2; font-size: 12px; height: 19px; margin-bottom: 5px; "
+ "style": "background: white;margin-top:5px; border:1px solid #c2c2c2; font-size: 12px; margin-bottom: 5px; "
},
"margins": {"top": 22, "bottom": 10, "left": 9, "right": 4 },
"userlist": { "width": 125,
"style": "font-size: 12px; background: white; margin-left: 5px; margin-bottom: 5px; border:2px solid #c2c2c2; padding: 5px; font-family: 'Arial';selection-background-color:rgb(200,200,200);"
},
- "time": { "text": { "width": 75,
- "style": "color: black; font:bold; border:1px solid #c2c2c2; background: white; height: 19px;"
+ "time": { "text": { "width": 75,
+ "style": "color: black; font:bold; border:1px solid #c2c2c2; background: white; height: 19px;"
},
"slider": { "style": " border:1px solid #c2c2c2;",
"groove": "border-image:url($path/timeslider.png);",
"handle": "image:url($path/acceptant.png);"
},
- "buttons": { "style": "border:1px solid #a68168; height: 17px; width: 50px; color: #cd8f9d; font-family: 'Arial'; background: rgb(190, 19, 4); margin-left: 2px;" },
- "arrows": { "left": "$path/leftarrow.png",
+ "buttons": { "style": "border:1px solid #a68168; height: 17px; width: 50px; color: #cd8f9d; font-family: 'Arial'; background: rgb(190, 19, 4); margin-left: 2px;" },
+ "arrows": { "left": "$path/leftarrow.png",
"right": "$path/rightarrow.png",
"style": "width: 19px; height: 19px; border:0px; margin-left: 2px;"
}
},
"systemMsgColor": "#646464",
- "op": { "icon": "$path/op.png" }
+ "op": { "icon": "$path/op.png" },
+ "voice": { "icon": "$path/voice.png" }
}
-}
\ No newline at end of file
+}
diff --git a/themes/trollian/voice.png b/themes/trollian/voice.png
new file mode 100644
index 0000000..c400a6c
Binary files /dev/null and b/themes/trollian/voice.png differ
diff --git a/themes/trollian2.5/alarm2.wav b/themes/trollian2.5/alarm2.wav
new file mode 100644
index 0000000..5ae54fa
Binary files /dev/null and b/themes/trollian2.5/alarm2.wav differ
diff --git a/themes/trollian2.5/style.js b/themes/trollian2.5/style.js
index bcd3288..93d46d4 100644
--- a/themes/trollian2.5/style.js
+++ b/themes/trollian2.5/style.js
@@ -5,15 +5,15 @@
"icon": "$path/trayicon.png",
"newmsgicon": "$path/trayicon2.png",
"windowtitle": "TROLLIAN",
- "menu" : { "style": "font-family: 'Arial'; font: bold; font-size: 14px; background-color: #e5000f;border:2px solid #c20f00" },
- "menubar": { "style": "font-family: 'Arial'; font:bold; font-size: 14px;" },
+ "menu" : { "style": "font-family: 'Arial'; font: bold; font-size: 14px; color: #000000; background-color: #e5000f;border:2px solid #c20f00" },
+ "menubar": { "style": "font-family: 'Arial'; font:bold; font-size: 14px; color: #000000;" },
"close": { "image": "$path/x.png",
- "loc": [280, 2]},
+ "loc": [272, 0]},
"minimize": { "image": "$path/m.png",
- "loc": [260, 8]},
+ "loc": [248, 2]},
"defaultwindow": { "style": "background: #e5000f; font-family:'Arial';font:bold;selection-background-color:#919191; " },
"chums": { "style": "border:2px solid #ffa4a4; background-color: black;color: white;font: bold;font-size:14px;font-family: 'Arial';selection-background-color:#646464; ",
- "moods": {
+ "moods": {
"chummy": { "icon": "$path/chummy.png", "color": "white" },
@@ -21,7 +21,7 @@
"offline": { "icon": "$path/offline.png", "color": "#646464"},
-
+
"pleasant": { "icon": "$path/pleasant.png", "color": "white" },
"distraught": { "icon": "$path/distraught.png", "color": "white" },
@@ -49,7 +49,7 @@
"devious": { "icon": "$path/devious.png", "color": "red" },
"sleek": { "icon": "$path/sleek.png", "color": "red" },
-
+
"detestful": { "icon": "$path/detestful.png", "color": "red" },
"mirthful": { "icon": "$path/mirthful.png", "color": "red" },
@@ -65,13 +65,13 @@
"protective": { "icon": "$path/protective.png", "color": "#00ff00" },
"blocked": { "icon": "$path/blocked.png", "color": "black" }
- }
- },
- "mychumhandle": { "label":
+ }
+ },
+ "mychumhandle": { "label":
{ "text": "TROLLTAG:",
- "style": "color: black ;font:bold; font-family: 'Arial';"
+ "style": "color: black ;font:bold; font-family: 'Arial';"
},
- "handle": { "style": "background-color: black; padding: 3px; padding-left: 25px; color:white; font-family:'Arial'; font:bold; text-align:left; border:2px solid #ffa4a4;" }
+ "handle": { "style": "background-color: black; padding: 3px; padding-left: 25px; color:white; font-family:'Arial'; font:bold; text-align:left; border:2px solid #ffa4a4;" }
},
"addchum": { "style": "background: #ffa4a4; border:2px solid #780000; font: bold; color: black; font-family:'Arial';"
},
@@ -80,103 +80,103 @@
"block": { "style": "background: #ffa4a4; border:2px solid #780000; font: bold; color: black; font-family:'Arial';"
},
"moodlabel": { "style": "font:bold;font-family:'Arial';color:black;"
- },
+ },
"defaultmood": 1,
"moods": [
- { "style": "text-align:left; background:#ffa4a4;border:2px solid #780000;color: black; font-family:'Arial'; font:bold; padding-left:3px;",
- "selected": "text-align:left; background:white; border:2px solid #780000; color: black; font-family:'Arial'; font:bold; padding-left:3px;",
- "loc": [15, 485],
- "size": [135, 30],
- "text": "ECSTATIC",
- "icon": "$path/estatic.png",
- "mood": 7
- },
+ { "style": "text-align:left; background:#ffa4a4;border:2px solid #780000;color: black; font-family:'Arial'; font:bold; padding-left:3px;",
+ "selected": "text-align:left; background:white; border:2px solid #780000; color: black; font-family:'Arial'; font:bold; padding-left:3px;",
+ "loc": [15, 485],
+ "size": [135, 30],
+ "text": "ECSTATIC",
+ "icon": "$path/estatic.png",
+ "mood": 7
+ },
- { "style": "text-align:left; background:#ffa4a4;border:2px solid #780000;color: black; font-family:'Arial'; font:bold; padding-left:3px;",
- "selected": "text-align:left; background:white; border:2px solid #780000; color: black; font-family:'Arial'; font:bold; padding-left:3px;",
- "loc": [15, 513],
- "size": [135, 30],
- "text": "RELAXED",
- "icon": "$path/relaxed.png",
- "mood": 8
- },
+ { "style": "text-align:left; background:#ffa4a4;border:2px solid #780000;color: black; font-family:'Arial'; font:bold; padding-left:3px;",
+ "selected": "text-align:left; background:white; border:2px solid #780000; color: black; font-family:'Arial'; font:bold; padding-left:3px;",
+ "loc": [15, 513],
+ "size": [135, 30],
+ "text": "RELAXED",
+ "icon": "$path/relaxed.png",
+ "mood": 8
+ },
- { "style": "text-align:left; background:#ffa4a4;border:2px solid #780000;color: black; font-family:'Arial'; font:bold; padding-left:3px;",
- "selected": "text-align:left; background:white; border:2px solid #780000; color: black; font-family:'Arial'; font:bold; padding-left:3px;",
- "loc": [15, 541],
- "size": [135, 30],
- "text": "DISCONTENT",
- "icon": "$path/discontent.png",
- "mood": 9
- },
+ { "style": "text-align:left; background:#ffa4a4;border:2px solid #780000;color: black; font-family:'Arial'; font:bold; padding-left:3px;",
+ "selected": "text-align:left; background:white; border:2px solid #780000; color: black; font-family:'Arial'; font:bold; padding-left:3px;",
+ "loc": [15, 541],
+ "size": [135, 30],
+ "text": "DISCONTENT",
+ "icon": "$path/discontent.png",
+ "mood": 9
+ },
- { "style": "text-align:left; background:#ffa4a4;border:2px solid #780000;color: black; font-family:'Arial'; font:bold; padding-left:3px;",
- "selected": "text-align:left; background:white; border:2px solid #780000; color: black; font-family:'Arial'; font:bold; padding-left:3px;",
- "loc": [148, 485],
- "size": [135, 30],
- "text": "DEVIOUS",
- "icon": "$path/devious.png",
- "mood": 10
- },
+ { "style": "text-align:left; background:#ffa4a4;border:2px solid #780000;color: black; font-family:'Arial'; font:bold; padding-left:3px;",
+ "selected": "text-align:left; background:white; border:2px solid #780000; color: black; font-family:'Arial'; font:bold; padding-left:3px;",
+ "loc": [148, 485],
+ "size": [135, 30],
+ "text": "DEVIOUS",
+ "icon": "$path/devious.png",
+ "mood": 10
+ },
- { "style": "text-align:left; background:#ffa4a4;border:2px solid #780000;color: black; font-family:'Arial'; font:bold; padding-left:3px;",
- "selected": "text-align:left; background:white; border:2px solid #780000; color: black; font-family:'Arial'; font:bold; padding-left:3px;",
- "loc": [148, 513],
- "size": [135, 30],
- "text": "SLEEK",
- "icon": "$path/sleek.png",
- "mood": 11
- },
+ { "style": "text-align:left; background:#ffa4a4;border:2px solid #780000;color: black; font-family:'Arial'; font:bold; padding-left:3px;",
+ "selected": "text-align:left; background:white; border:2px solid #780000; color: black; font-family:'Arial'; font:bold; padding-left:3px;",
+ "loc": [148, 513],
+ "size": [135, 30],
+ "text": "SLEEK",
+ "icon": "$path/sleek.png",
+ "mood": 11
+ },
- { "style": "text-align:left; background:#ffa4a4;border:2px solid #780000;color: black; font-family:'Arial'; font:bold; padding-left:3px;",
- "selected": "text-align:left; background:white; border:2px solid #780000; color: black; font-family:'Arial'; font:bold; padding-left:3px;",
- "loc": [148, 541],
- "size": [135, 30],
- "text": "DETESTFUL",
- "icon": "$path/detestful.png",
- "mood": 12
- },
+ { "style": "text-align:left; background:#ffa4a4;border:2px solid #780000;color: black; font-family:'Arial'; font:bold; padding-left:3px;",
+ "selected": "text-align:left; background:white; border:2px solid #780000; color: black; font-family:'Arial'; font:bold; padding-left:3px;",
+ "loc": [148, 541],
+ "size": [135, 30],
+ "text": "DETESTFUL",
+ "icon": "$path/detestful.png",
+ "mood": 12
+ },
- { "style": "text-align:center; border:2px solid #780000; background:black;color: white; font-family:'Arial'; font:bold;padding-left:3px;",
- "selected": "text-align:left; background:white; border:2px solid #780000; padding: 5px;color: black; font-family:'Arial'; font:bold;padding-left:3px;",
- "loc": [15, 569],
- "size": [270, 30],
- "text": "ABSCOND",
- "icon": "$path/offline.png",
- "mood": 2
- }
+ { "style": "text-align:center; border:2px solid #780000; background:black;color: white; font-family:'Arial'; font:bold;padding-left:3px;",
+ "selected": "text-align:left; background:white; border:2px solid #780000; padding: 5px;color: black; font-family:'Arial'; font:bold;padding-left:3px;",
+ "loc": [15, 569],
+ "size": [270, 30],
+ "text": "ABSCOND",
+ "icon": "$path/offline.png",
+ "mood": 2
+ }
]
},
"convo": {
"style": "background-color: #e5000f;border:2px solid #780000; font-family: 'Arial';",
"chumlabel": { "style": "margin-bottom: 21px;background: #ffa4a4; color: black; border:0px; font-size: 14px;",
- "text" : ":: trolling: $handle ::" },
- "textarea": {
- "style": "background: white; font-size: 14px;font:bold; border:2px solid #ffa4a4;text-align:center; margin-right:10px; margin-left:10px;font-family: 'Arial'"
- },
+ "text" : ":: trolling: $handle ::" },
+ "textarea": {
+ "style": "background: white; font-size: 14px;font:bold; border:2px solid #ffa4a4;text-align:center; margin-right:10px; margin-left:10px;font-family: 'Arial'"
+ },
"input": { "style": "background: white; border:2px solid #ffa4a4;margin-top:5px; margin-right:10px; margin-left:10px; font-size: 12px;" },
- "tabwindow" : {
- "style": ""
- },
- "tabs": {
- "style": "",
- "selectedstyle": "",
- "newmsgcolor": "#ff0000"
- },
- "scrollbar": null
+ "tabwindow" : {
+ "style": ""
+ },
+ "tabs": {
+ "style": "",
+ "selectedstyle": "",
+ "newmsgcolor": "#ff0000"
+ },
+ "scrollbar": null
},
"memos":
{ "size": [600,425],
"style": "background-color: #e5000f;border:2px solid #780000; font-family: 'Arial';",
"label": { "style": "margin-bottom: 21px;background: #ffa4a4; color: white; border:0px; font-size: 14px;"
- },
+ },
"textarea": {
- "style": "background: white; font-size: 14px;font:bold; border:2px solid #ffa4a4;text-align:center; margin-right:10px; margin-left:10px;font-family: 'Arial'"
+ "style": "background: white; font-size: 14px;font:bold; border:2px solid #ffa4a4;text-align:center; margin-right:10px; margin-left:10px;font-family: 'Arial'"
},
"userlist": { "style": "border:2px solid #780000; background: white;font: bold;font-family: 'Courier';selection-background-color:#646464; font-size: 12px; margin-left:0px; margin-right:10px;"
},
"input": { "style": "background: white; border:2px solid #ffa4a4;margin-top:5px; margin-right:10px; margin-left:10px; font-size: 12px;" },
- "time": { "text": { "style": " border: 2px solid #780000; background: white; font-size: 12px; margin-top: 5px; margin-right: 5px; margin-left: 5px; font-family:'Arial';font:bold;"
+ "time": { "text": { "style": " border: 2px solid #780000; background: white; font-size: 12px; margin-top: 5px; margin-right: 5px; margin-left: 5px; font-family:'Arial';font:bold;"
},
"buttons": { "style": "color: black; font: bold; border: 2px solid #780000; font: bold; font-size: 12px; background: #e5000f; margin-top: 5px; margin-right: 5px; margin-left: 5px; padding: 2px; width: 50px;" }
},
@@ -189,4 +189,4 @@
},
"scrollbar": null
}
-}
\ No newline at end of file
+}
diff --git a/themes/trollian2.5/voice.png b/themes/trollian2.5/voice.png
new file mode 100644
index 0000000..c400a6c
Binary files /dev/null and b/themes/trollian2.5/voice.png differ
diff --git a/themes/typewriter/alarm2.wav b/themes/typewriter/alarm2.wav
new file mode 100644
index 0000000..5ae54fa
Binary files /dev/null and b/themes/typewriter/alarm2.wav differ
diff --git a/themes/typewriter/style.js b/themes/typewriter/style.js
index 13306f0..2169dae 100644
--- a/themes/typewriter/style.js
+++ b/themes/typewriter/style.js
@@ -6,25 +6,28 @@
"newmsgicon": "$path/trayicon2.png",
"windowtitle": "Typewriter",
"close": { "image": "$path/x.png",
- "loc": [264, 164]},
+ "loc": [266, 164]},
"minimize": { "image": "$path/m.png",
- "loc": [239, 168]},
+ "loc": [240, 169]},
"menubar": { "style": "font-family: 'Courier'; font:bold; font-size: 12px; color: rgba(0,0,0,0);" },
- "menu" : { "style": "font-family: 'Courier'; font: bold; font-size: 12px; background-color: white;border:2px solid black;",
+ "menu" : { "style": "font-family: 'Courier'; font: bold; font-size: 12px; color: #000000; background-color: white;border:2px solid black;",
"menuitem": "margin-right:30px;",
"selected": "background-color: black",
"loc": [43,220]
},
"sounds": { "alertsound": "$path/alarm.wav",
- "ceasesound": "$path/cease.wav" },
+ "memosound": "$path/alarm2.wav",
+ "ceasesound": "$path/cease.wav" },
"menus": {"client": {"_name": "Typewriter",
"options": "Preferences",
"memos": "Bulletin Boards",
"logviewer": "Pesterlogs",
+ "randen": "Random Encounter",
"userlist": "Userlist",
+ "addgroup": "Add Group",
"import": "Import",
- "idle": "Idle",
- "reconnect": "Reconnect",
+ "idle": "Idle",
+ "reconnect": "Reconnect",
"exit": "Cease"},
"profile": {"_name": "Ink",
"switch": "Alias",
@@ -33,7 +36,10 @@
"block": "Ruffians",
"quirks": "Quirks"},
"help": { "_name": "Assistance",
- "about": "About" },
+ "about": "About",
+ "help": "Assistance",
+ "calsprite": "CalSprite",
+ "nickserv": "NickServ" },
"rclickchumlist": {"pester": "Converse",
"removechum": "Erase User",
"report": "Report User",
@@ -41,16 +47,21 @@
"addchum": "Add User",
"viewlog": "View Pesterlog",
"unblockchum": "Forgive",
+ "removegroup": "Remove Group",
+ "renamegroup": "Rename Group",
+ "movechum": "Move To",
"banuser": "Expel User",
"opuser": "Promote",
- "quirksoff": "Quirks Off"
+ "voiceuser": "Let Speak",
+ "quirksoff": "Quirks Off",
+ "invitechum": "Invite User"
}
},
"chums": { "style": "border:0px; background-color: white; font: bold;font-family: 'Courier';selection-background-color: black; ",
"loc": [70, 20],
"size": [175,100],
"userlistcolor": "black",
- "moods": {
+ "moods": {
"chummy": { "icon": "$path/chummy.png", "color": "black" },
@@ -58,7 +69,7 @@
"offline": { "icon": "$path/offline.png", "color": "#646464"},
-
+
"pleasant": { "icon": "$path/pleasant.png", "color": "black" },
"distraught": { "icon": "$path/distraught.png", "color": "black" },
@@ -86,7 +97,7 @@
"devious": { "icon": "$path/devious.png", "color": "red" },
"sleek": { "icon": "$path/sleek.png", "color": "red" },
-
+
"detestful": { "icon": "$path/detestful.png", "color": "red" },
"mirthful": { "icon": "$path/mirthful.png", "color": "red" },
@@ -105,7 +116,7 @@
}
},
- "trollslum": {
+ "trollslum": {
"style": "background: #bebebe; border:2px solid black; font-family: 'Courier'",
"size": [195, 200],
"label": { "text": "Ruffians",
@@ -123,7 +134,7 @@
"text": "" },
"currentMood": [0, 0]
},
- "defaultwindow": { "style": "background: #bebebe; font-family:'Courier';font:bold;selection-background-color: black; "
+ "defaultwindow": { "style": "background: #bebebe; font-family:'Courier';font:bold;selection-background-color: black; "
},
"addchum": { "style": "background: rgba(255, 255, 0, 0%); border:0px solid #c48a00; font: bold; color: rgba(0, 0, 0, 0%); font-family:'Courier';",
"pressed" : "background: rgb(255, 255, 255, 30%);",
@@ -137,7 +148,7 @@
"size": [70, 15],
"text": ""
},
- "block": { "style": "background: rgba(255, 255, 0, 0%); border:2px solid #c48a00; font: bold; color: rgba(255, 255, 0, 0%); font-family:'Courier';",
+ "block": { "style": "background: rgba(255, 255, 0, 0%); border:2px solid #c48a00; font: bold; color: rgba(255, 255, 0, 0%); font-family:'Courier';",
"pressed" : "background: rgb(255, 255, 255, 30%);",
"loc": [0,0],
"size": [0, 0],
@@ -145,26 +156,26 @@
},
"defaultmood": 18,
"moodlabel": { "style": "",
- "loc": [20, 430],
- "text": "MOODS"
- },
+ "loc": [20, 430],
+ "text": "MOODS"
+ },
"moods": [
- { "style": "text-align:left; border:0px solid #c48a00; padding: 0px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
- "selected": "text-align:left; background-image:url($path/moodcheck1.png); border:0px solid #c48a00; padding: 0px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
- "loc": [95, 323],
- "size": [62, 9],
- "text": "",
- "icon": "",
- "mood": 18
- },
- { "style": "text-align:left; border:0px solid #c48a00; padding: 0px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
- "selected": "text-align:left; background-image:url($path/moodcheck2.png); border:0px solid #c48a00; padding: 0px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
- "loc": [165, 323],
- "size": [70, 9],
- "text": "",
- "icon": "",
- "mood": 2
- }
+ { "style": "text-align:left; border:0px solid #c48a00; padding: 0px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
+ "selected": "text-align:left; background-image:url($path/moodcheck1.png); border:0px solid #c48a00; padding: 0px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
+ "loc": [95, 323],
+ "size": [62, 9],
+ "text": "",
+ "icon": "",
+ "mood": 18
+ },
+ { "style": "text-align:left; border:0px solid #c48a00; padding: 0px;color: rgba(0, 0, 0, 0%); font-family:'Courier'",
+ "selected": "text-align:left; background-image:url($path/moodcheck2.png); border:0px solid #c48a00; padding: 0px;color: rgba(0, 0, 0, 0%); font-family:'Courier';",
+ "loc": [165, 323],
+ "size": [70, 9],
+ "text": "",
+ "icon": "",
+ "mood": 2
+ }
]
},
"convo":
@@ -195,12 +206,12 @@
"ceasepester": "ceased pestering",
"blocked": "blocked",
"unblocked": "unblocked",
- "blockedmsg": "did not receive message from",
+ "blockedmsg": "did not receive message from",
"openmemo": "opened memo on board",
"joinmemo": "responded to memo",
"closememo": "ceased responding to memo",
"kickedmemo": "You have been banned from this memo!",
- "idle": "is now an idle chum!"
+ "idle": "is now an idle chum!"
},
"systemMsgColor": "#646464"
},
@@ -216,7 +227,7 @@
},
"scrollbar": { "style" : "padding-top:17px; padding-bottom:17px;width: 18px; background: rgba(255, 255, 0, 0%); border:0px;",
"handle": "background-color:black;min-height:20px;",
- "downarrow": "height:17px;border:0px;",
+ "downarrow": "height:17px;border:0px;",
"darrowstyle": "image:url($path/downarrow.png);",
"uparrow": "height:17px;border:0px;",
"uarrowstyle": "image:url($path/uparrow.png);"
@@ -233,20 +244,21 @@
"userlist": { "width": 150,
"style": "border:2px solid black; background: white;font: bold;font-family: 'Courier';selection-background-color:black; font-size: 12px; margin-left:0px; margin-right:10px;"
},
- "time": { "text": { "width": 75,
- "style": " border: 2px solid black; background: white; font-size: 12px; margin-top: 5px; margin-right: 5px; margin-left: 5px; font-family:'Courier';font:bold;"
+ "time": { "text": { "width": 75,
+ "style": " border: 2px solid black; background: white; font-size: 12px; margin-top: 5px; margin-right: 5px; margin-left: 5px; font-family:'Courier';font:bold;"
},
"slider": { "style": "border: 0px;",
"groove": "",
"handle": ""
},
- "buttons": { "style": "color: black; font: bold; border: 2px solid black; font: bold; font-size: 12px; background: white; margin-top: 5px; margin-right: 5px; margin-left: 5px; padding: 2px; width: 50px;" },
- "arrows": { "left": "$path/leftarrow.png",
+ "buttons": { "style": "color: black; font: bold; border: 2px solid black; font: bold; font-size: 12px; background: white; margin-top: 5px; margin-right: 5px; margin-left: 5px; padding: 2px; width: 50px;" },
+ "arrows": { "left": "$path/leftarrow.png",
"right": "$path/rightarrow.png",
- "style": " border:0px; margin-top: 5px; margin-right:10px;"
+ "style": " border:0px; margin-top: 5px; margin-right:10px;"
}
},
"systemMsgColor": "#646464",
- "op": { "icon": "$path/protective.png" }
+ "op": { "icon": "$path/protective.png" },
+ "voice": { "icon": "$path/voice.png" }
}
-}
\ No newline at end of file
+}
diff --git a/themes/typewriter/voice.png b/themes/typewriter/voice.png
new file mode 100644
index 0000000..82a53ae
Binary files /dev/null and b/themes/typewriter/voice.png differ
diff --git a/trollquirks.mkdn b/trollquirks.mkdn
new file mode 100644
index 0000000..f4e4ad1
--- /dev/null
+++ b/trollquirks.mkdn
@@ -0,0 +1,170 @@
+Troll Quirks
+============
+
+Karkat
+------
+
+REGEXP: "(.)"
+REPLACED WITH: "upper(\1)"
+
+
+Aradia
+------
+
+REGEXP: "[oO]"
+REPLACE WITH: "0"
+
+
+#####After Prototyping
+
+RANDOM REGEXP: "\s"
+REPLACED WITH: " ribbit ", " ", " ", " ", " ", " ", etc....
+
+
+Tavros
+------
+
+REGEXP: "\b(\w)(\w*)"
+REPLACED WITH: "lower(\1)upper(\2)"
+
+
+REGEXP: "\.?"
+WITH: ","
+
+
+Sollux
+------
+#####Pre-blind
+
+REGEXP: "[iI]"
+REPLACE WITH: "\1\1"
+
+
+REGEXP: "[sS]"
+REPLACE WITH: "2"
+
+
+REGEXP: "\btoo?\b"
+REPLACE WITH: "two"
+
+
+#####Blind
+
+REGEXP: "[oO]"
+REPLACE WITH: "0"
+
+
+Nepeta
+------
+
+PREFIX: ":33 < "
+
+
+REGEXP: "[eE][eE]"
+REPLACE WITH: "33"
+
+
+Kanaya
+------
+
+REGEXP: "\b(\w)"
+REPLACE WITH: "upper(\1)
+
+
+Terezi
+------
+
+REGEXP: "[aA]"
+REPLACE WITH: "4"
+
+
+REGEXP: "[iI]"
+REPLACE WITH: "1"
+
+
+REGEXP: "[eE]"
+REPLACE WITH: "3"
+
+
+REGEXP: "(.)"
+REPLACE WITH: "upper(\1)"
+
+
+Vriska
+------
+
+REGEXP: "[bB]"
+REPLACE WITH: "8"
+
+
+RANDOM REGEXP: "([aeiouAEIOU])"
+REPLACE WITH: "\1\1\1\1\1\1\1\1", "\1", "\1", "\1", "\1", "\1", etc........
+
+
+RANDOM REGEXP: "([\.\?,!]$)"
+REPLACE WITH: "\1\1\1\1\1\1\1\1", "\1", "\1", "\1", "\1", "\1", etc........
+
+
+REPLACE: ":"
+WITH: "::::"
+
+
+Keep in mind that the RANDOM REGEXP ones require the extra "\\1"s to be added in order to not happen all the time. If you want those quirks to happen less often, add more "\\1".
+
+Equius
+------
+
+PREFIX: "D --> "
+
+
+REGEXP: "[lL][oO][oO]"
+REPLACE WITH: "100"
+
+
+REGEXP: "[xX]"
+REPLACE WITH: "%"
+
+
+REGEXP: "(\b[sS][tT][rR][oO][nN][gG]\w*)"
+REPLACE WITH: "upper(\1)"
+
+
+REGEXP: "[oO][oO]"
+REPLACE WITH: "00"
+
+
+Gamzee
+------
+#####Version 1: "HoNk HoNk"
+
+REGEXP: "([a-zA-Z])([a-zA-Z]?)"
+REPLACED WITH: "upper(\1)\2"
+
+
+#####Version 2: "HoNk hOnK"
+
+REGEXP: "([\w\s])([\w\s]?)"
+REPLACED WITH: "upper(\1)\2"
+
+
+Eridan
+------
+
+REGEXP: "([vVwW])"
+REPLACE WITH: "\1\1"
+
+
+REGEXP: "ing\b"
+REPLACE WITH: "in"
+
+
+Feferi
+------
+
+REGEXP: [hH]
+REPLACE WITH: ")("
+
+
+REPLACE: "E"
+WITH: "-E"
+
\ No newline at end of file
diff --git a/tutorial/convo_1.png b/tutorial/convo_1.png
deleted file mode 100644
index a367e21..0000000
Binary files a/tutorial/convo_1.png and /dev/null differ
diff --git a/tutorial/convo_2.png b/tutorial/convo_2.png
deleted file mode 100644
index 7821c72..0000000
Binary files a/tutorial/convo_2.png and /dev/null differ
diff --git a/tutorial/enamel_ss.png b/tutorial/enamel_ss.png
deleted file mode 100644
index 37d6b14..0000000
Binary files a/tutorial/enamel_ss.png and /dev/null differ
diff --git a/tutorial/gold_ss.png b/tutorial/gold_ss.png
deleted file mode 100644
index 04d0222..0000000
Binary files a/tutorial/gold_ss.png and /dev/null differ
diff --git a/tutorial/gold_xl_ss.png b/tutorial/gold_xl_ss.png
deleted file mode 100644
index 4ade5ac..0000000
Binary files a/tutorial/gold_xl_ss.png and /dev/null differ
diff --git a/tutorial/mainmenu.png b/tutorial/mainmenu.png
deleted file mode 100644
index 5cc705b..0000000
Binary files a/tutorial/mainmenu.png and /dev/null differ
diff --git a/tutorial/memo.png b/tutorial/memo.png
deleted file mode 100644
index 3670287..0000000
Binary files a/tutorial/memo.png and /dev/null differ
diff --git a/tutorial/memo_1.png b/tutorial/memo_1.png
deleted file mode 100644
index d9d5f82..0000000
Binary files a/tutorial/memo_1.png and /dev/null differ
diff --git a/tutorial/memo_10.png b/tutorial/memo_10.png
deleted file mode 100644
index 71b21c9..0000000
Binary files a/tutorial/memo_10.png and /dev/null differ
diff --git a/tutorial/memo_11.png b/tutorial/memo_11.png
deleted file mode 100644
index 7930dbf..0000000
Binary files a/tutorial/memo_11.png and /dev/null differ
diff --git a/tutorial/memo_12.png b/tutorial/memo_12.png
deleted file mode 100644
index bf12b26..0000000
Binary files a/tutorial/memo_12.png and /dev/null differ
diff --git a/tutorial/memo_13.png b/tutorial/memo_13.png
deleted file mode 100644
index bbd10b8..0000000
Binary files a/tutorial/memo_13.png and /dev/null differ
diff --git a/tutorial/memo_2.png b/tutorial/memo_2.png
deleted file mode 100644
index b033505..0000000
Binary files a/tutorial/memo_2.png and /dev/null differ
diff --git a/tutorial/memo_3.png b/tutorial/memo_3.png
deleted file mode 100644
index 4e3d03a..0000000
Binary files a/tutorial/memo_3.png and /dev/null differ
diff --git a/tutorial/memo_4.png b/tutorial/memo_4.png
deleted file mode 100644
index c2328fb..0000000
Binary files a/tutorial/memo_4.png and /dev/null differ
diff --git a/tutorial/memo_5.png b/tutorial/memo_5.png
deleted file mode 100644
index e5cb332..0000000
Binary files a/tutorial/memo_5.png and /dev/null differ
diff --git a/tutorial/memo_6.png b/tutorial/memo_6.png
deleted file mode 100644
index 5d50208..0000000
Binary files a/tutorial/memo_6.png and /dev/null differ
diff --git a/tutorial/memo_7.png b/tutorial/memo_7.png
deleted file mode 100644
index 5e61b48..0000000
Binary files a/tutorial/memo_7.png and /dev/null differ
diff --git a/tutorial/memo_8.png b/tutorial/memo_8.png
deleted file mode 100644
index f5eaf4b..0000000
Binary files a/tutorial/memo_8.png and /dev/null differ
diff --git a/tutorial/memo_useroptions.png b/tutorial/memo_useroptions.png
deleted file mode 100644
index a2ae819..0000000
Binary files a/tutorial/memo_useroptions.png and /dev/null differ
diff --git a/tutorial/mispeller.png b/tutorial/mispeller.png
deleted file mode 100644
index 01e2099..0000000
Binary files a/tutorial/mispeller.png and /dev/null differ
diff --git a/tutorial/options.png b/tutorial/options.png
deleted file mode 100644
index 0de05de..0000000
Binary files a/tutorial/options.png and /dev/null differ
diff --git a/tutorial/pesterchum2.5_ss.png b/tutorial/pesterchum2.5_ss.png
deleted file mode 100644
index ac2e94b..0000000
Binary files a/tutorial/pesterchum2.5_ss.png and /dev/null differ
diff --git a/tutorial/pesterchum_ss.png b/tutorial/pesterchum_ss.png
deleted file mode 100644
index 319ccde..0000000
Binary files a/tutorial/pesterchum_ss.png and /dev/null differ
diff --git a/tutorial/prefix.png b/tutorial/prefix.png
deleted file mode 100644
index c327527..0000000
Binary files a/tutorial/prefix.png and /dev/null differ
diff --git a/tutorial/quirkmenu.png b/tutorial/quirkmenu.png
deleted file mode 100644
index e3afc28..0000000
Binary files a/tutorial/quirkmenu.png and /dev/null differ
diff --git a/tutorial/quirks_terezi.png b/tutorial/quirks_terezi.png
deleted file mode 100644
index cd46be6..0000000
Binary files a/tutorial/quirks_terezi.png and /dev/null differ
diff --git a/tutorial/random_replace_caps.png b/tutorial/random_replace_caps.png
deleted file mode 100644
index 6f6afc1..0000000
Binary files a/tutorial/random_replace_caps.png and /dev/null differ
diff --git a/tutorial/random_replace_simple.png b/tutorial/random_replace_simple.png
deleted file mode 100644
index cd1c578..0000000
Binary files a/tutorial/random_replace_simple.png and /dev/null differ
diff --git a/tutorial/regexp_a4.png b/tutorial/regexp_a4.png
deleted file mode 100644
index ac364b2..0000000
Binary files a/tutorial/regexp_a4.png and /dev/null differ
diff --git a/tutorial/regexp_caps.png b/tutorial/regexp_caps.png
deleted file mode 100644
index d5b807a..0000000
Binary files a/tutorial/regexp_caps.png and /dev/null differ
diff --git a/tutorial/simple_ee33.png b/tutorial/simple_ee33.png
deleted file mode 100644
index 817d444..0000000
Binary files a/tutorial/simple_ee33.png and /dev/null differ
diff --git a/tutorial/switch_profile.png b/tutorial/switch_profile.png
deleted file mode 100644
index baecf42..0000000
Binary files a/tutorial/switch_profile.png and /dev/null differ
diff --git a/tutorial/trollian2.5_ss.png b/tutorial/trollian2.5_ss.png
deleted file mode 100644
index 60b2e74..0000000
Binary files a/tutorial/trollian2.5_ss.png and /dev/null differ
diff --git a/tutorial/trollian_ss.png b/tutorial/trollian_ss.png
deleted file mode 100644
index 6713db5..0000000
Binary files a/tutorial/trollian_ss.png and /dev/null differ
diff --git a/tutorial/trollslum.png b/tutorial/trollslum.png
deleted file mode 100644
index 514071b..0000000
Binary files a/tutorial/trollslum.png and /dev/null differ
diff --git a/tutorial/typewriter_ss.png b/tutorial/typewriter_ss.png
deleted file mode 100644
index abdc242..0000000
Binary files a/tutorial/typewriter_ss.png and /dev/null differ
diff --git a/tutorial/userlist.png b/tutorial/userlist.png
deleted file mode 100644
index 6d38da3..0000000
Binary files a/tutorial/userlist.png and /dev/null differ
diff --git a/updatecheck.py b/updatecheck.py
new file mode 100644
index 0000000..73e014b
--- /dev/null
+++ b/updatecheck.py
@@ -0,0 +1,118 @@
+# Adapted from Eco-Mono's F5Stuck RSS Client
+
+import feedparser
+import pickle
+import os
+from time import mktime
+from PyQt4 import QtCore, QtGui
+
+class MSPAChecker(QtGui.QWidget):
+ def __init__(self, parent=None):
+ QtCore.QObject.__init__(self, parent)
+ self.mainwindow = parent
+ self.refreshRate = 30 # seconds
+ self.status = None
+ self.timer = QtCore.QTimer(self)
+ self.connect(self.timer, QtCore.SIGNAL('timeout()'),
+ self, QtCore.SLOT('check_site()'))
+ self.check_site()
+ self.timer.start(1000*self.refreshRate)
+
+ def save_state(self):
+ try:
+ current_status = open("status_new.pkl","w")
+ pickle.dump(self.status, current_status)
+ current_status.close()
+ try:
+ os.rename("status.pkl","status_old.pkl")
+ except:
+ pass
+ try:
+ os.rename("status_new.pkl","status.pkl")
+ except:
+ if os.path.exists("status_old.pkl"):
+ os.rename("status_old.pkl","status.pkl")
+ raise
+ if os.path.exists("status_old.pkl"):
+ os.remove("status_old.pkl")
+ except Exception, e:
+ print e
+ msg = QtGui.QMessageBox(self)
+ msg.setText("Problems writing save file.")
+ msg.show()
+
+ @QtCore.pyqtSlot()
+ def check_site(self):
+ if not self.mainwindow.config.checkMSPA():
+ return
+ rss = None
+ must_save = False
+ try:
+ rss = feedparser.parse("http://www.mspaintadventures.com/rss/rss.xml")
+ except:
+ return
+ if len(rss.entries) == 0:
+ return
+ entries = sorted(rss.entries,key=(lambda x: mktime(x.updated_parsed)))
+ if self.status == None:
+ self.status = {}
+ self.status['last_visited'] = {'pubdate':mktime(entries[-1].updated_parsed),'link':entries[-1].link}
+ self.status['last_seen'] = {'pubdate':mktime(entries[-1].updated_parsed),'link':entries[-1].link}
+ must_save = True
+ elif mktime(entries[-1].updated_parsed) > self.status['last_seen']['pubdate']:
+ #This is the first time the app itself has noticed this update.
+ self.status['last_seen'] = {'pubdate':mktime(entries[-1].updated_parsed),'link':entries[-1].link}
+ must_save = True
+ if self.status['last_seen']['pubdate'] > self.status['last_visited']['pubdate']:
+ self.mspa = MSPAUpdateWindow(self.parent())
+ self.connect(self.mspa, QtCore.SIGNAL('accepted()'),
+ self, QtCore.SLOT('visit_site()'))
+ self.connect(self.mspa, QtCore.SIGNAL('rejected()'),
+ self, QtCore.SLOT('nothing()'))
+ self.mspa.show()
+ else:
+ #print "No new updates :("
+ pass
+ if must_save:
+ self.save_state()
+
+ @QtCore.pyqtSlot()
+ def visit_site(self):
+ print self.status['last_visited']['link']
+ QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.status['last_visited']['link'], QtCore.QUrl.TolerantMode))
+ if self.status['last_seen']['pubdate'] > self.status['last_visited']['pubdate']:
+ #Visited for the first time. Untrip the icon and remember that we saw it.
+ self.status['last_visited'] = self.status['last_seen']
+ self.save_state()
+ self.mspa = None
+ @QtCore.pyqtSlot()
+ def nothing(self):
+ self.mspa = None
+
+class MSPAUpdateWindow(QtGui.QDialog):
+ def __init__(self, parent=None):
+ QtGui.QDialog.__init__(self, parent)
+ self.mainwindow = parent
+ self.setStyleSheet(self.mainwindow.theme["main/defaultwindow/style"])
+ self.setWindowTitle("MSPA Update!")
+ self.setModal(False)
+
+ self.title = QtGui.QLabel("You have an unread MSPA update! :o)")
+
+ layout_0 = QtGui.QVBoxLayout()
+ layout_0.addWidget(self.title)
+
+ self.ok = QtGui.QPushButton("GO READ NOW!", self)
+ self.ok.setDefault(True)
+ self.connect(self.ok, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('accept()'))
+ self.cancel = QtGui.QPushButton("LATER", self)
+ self.connect(self.cancel, QtCore.SIGNAL('clicked()'),
+ self, QtCore.SLOT('reject()'))
+ layout_2 = QtGui.QHBoxLayout()
+ layout_2.addWidget(self.cancel)
+ layout_2.addWidget(self.ok)
+
+ layout_0.addLayout(layout_2)
+
+ self.setLayout(layout_0)
diff --git a/version.py b/version.py
new file mode 100644
index 0000000..dc790ee
--- /dev/null
+++ b/version.py
@@ -0,0 +1,57 @@
+import urllib
+import re
+import time
+
+USER_TYPE = "dev"
+
+_pcMajor = "3.41"
+_pcMinor = "0"
+_pcStatus = "B" # A = alpha
+ # B = beta
+ # RC = release candidate
+ # None = public release
+_pcRevision = "5"
+_pcVersion = ""
+
+def pcVerCalc():
+ global _pcVersion
+ if _pcStatus:
+ _pcVersion = "%s.%s-%s%s" % (_pcMajor, _pcMinor, _pcStatus, _pcRevision)
+ else:
+ _pcVersion = "%s.%s.%s" % (_pcMajor, _pcMinor, _pcRevision)
+
+def verStrToNum(ver):
+ w = re.match("(\d+\.?\d+)\.(\d+)-?([A-Za-z]{0,2})\.?(\d*):(\S+)", ver)
+ if not w:
+ print "Update check Failure: 3"; return
+ full = ver[:ver.find(":")]
+ return full,w.group(1),w.group(2),w.group(3),w.group(4),w.group(5)
+
+def updateCheck(q,num):
+ time.sleep(3)
+ data = urllib.urlencode({"type" : USER_TYPE})
+ try:
+ f = urllib.urlopen("http://distantsphere.com/pesterchum.php?" + data)
+ except:
+ print "Update check Failure: 1"; return q.put((False,1))
+ newest = f.read()
+ f.close()
+ if not newest or newest[0] == "<":
+ print "Update check Failure: 2"; return q.put((False,2))
+ try:
+ (full, major, minor, status, revision, url) = verStrToNum(newest)
+ except TypeError:
+ return q.put((False,3))
+ print full
+ if major <= _pcMajor:
+ if minor <= _pcMinor:
+ if status:
+ if status <= _pcStatus:
+ if revision <= _pcRevision:
+ return q.put((False,0))
+ else:
+ if not _pcStatus:
+ if revision <= _pcRevision:
+ return q.put((False,0))
+ print "A new version of Pesterchum is avaliable!"
+ q.put((full,url))