In the movies, when it is time to detonate the explosives, the person setting off the KABOOM has a sweet remote control with either a two part switch or with two separate switches. Damn, I love a good movie explosion.
The two part switch allows the user to:
- activate the explosive
- blow shit up
Because the switch has two components, activate and detonate, it is uncommon for there to be accidental explosions. This is a damn fine safety measure and when adding the ability to block an identi.ca user with heybuddy, I wanted a similar safety precaution; blocking someone by accident should not be possible. So that is what I did.
To accomplish this, it was necessary to do the following
- create a disabled button that brings the hammer down!
- create a checkbox that will activate the button
In GTK parlance, to disable a button, one needs to set the button sensitivity to false. Since I am using Python, this is accomplished thusly
To show this idea as a small working bit of code, I wrote a quick Python-GTK app that almost does the bare minimum.
Enter the Python
import gtk
class application:
def __init__(self):
#I want to keep track of click counts
self.click_count=0
'''build the UI'''
#make the window
winder = gtk.Window(gtk.WINDOW_TOPLEVEL)
winder.connect("destroy",self.quit)
#winder.set_default_size(300,300)
winder.set_title("Super Trigger")
#make some boxes
vbox = gtk.VBox()
hbox = gtk.HBox()
#make some buttons
self.button = gtk.Button("Click Me")
self.checkbutton = gtk.CheckButton(label="Enable Button")
#add the buttons to the hbox
hbox.pack_start(self.button,False,False)
hbox.pack_start(self.checkbutton,False,False)
#add the hbox to the vbox
vbox.pack_start(hbox,False,False)
#create a Label
self.label = gtk.Label()
#add the label to the vbox
vbox.pack_start(self.label)
#add the vbox to the window
winder.add(vbox)
#disable the button
self.button.set_sensitive(False)
#connect the buttons
self.checkbutton.connect('toggled', self.toggle_clicked )
self.button.connect('clicked', self.button_clicked)
#show it all!
winder.show_all()
def toggle_clicked(self, widget):
#is the checkbutton active?
active = widget.get_active()
self.button.set_sensitive(active)
def button_clicked(self,widget):
#increment the click count
self.click_count+=1
#make a string
string = "clicks: %d" %(self.click_count)
#set the label to the string
self.label.set_text(string)
def run(self):
gtk.main()
def quit(self,widget=None):
gtk.main_quit()
if __name__=="__main__":
a = application()
a.run()
It may not be the best solution, but it is a solution and it works.
Well that's it for now. Quit reading, and go write some code.
Starting with version 2.18, the GTK library allowed for adding links to Labels, and this is exactly how I handled adding clickable URLs and names in heybuddy. Unfortunately, the version of GTK on Maemo devices is too old to use links in Labels.
Another unfortunate problem with links in Labels is that no GTK theme seems to have set a custom link color and all themes are stuck with the default bright blue link color. Egads! It sure would be nice if the user could set the color of links in heybuddy.
After searching and searching, I couldn't find an example of how to set a custom color for links in a GTK Label so I first needed to write some code to experiment with. The end result was a very simple example of exactly what I needed: a custom link color in a GTK Label.
Enter the Python
import gtk
w = gtk.Window()
l = gtk.Label()
m = "go to <a href='http://www.jezra.net'>jezra.net</a>"
l.set_markup(m)
style='''
style "label" {
GtkLabel::link-color="#ff6600"
}
widget_class "*GtkLabel" style "label"
'''
gtk.rc_parse_string(style)
w.add(l)
w.show_all()
gtk.main()
shazaam! Custom color, baby!
Now quit reading, and go make things colorful.
Quite a few of my coding projects have a large text field where the user is expected to do a lot of typing. Actually, it would appear that none of my projects have a large text field. I suppose I should make markdowner and scripsi official jezra.net projects. Anyway, both of the previously mentioned apps have a large text field where the user types and types and types( hey, they are text editing programs ).
- scripsi is a special purpose dual paned text editor written in Vala
- markdowner is a special purpose text editor written in Python
Both applications use the GTK+ toolkit to create the user interface.
The Problem
When a GTK TextView widget is used to create a large text input area, if the user's cursor travels past the viewable area of the TextView, which happens when typing to the bottom of the screen, the cursor is below the visible area and the user will need to stop typing and scroll the TextView until it is visible. If the user were a typing ninja then I suppose they wouldn't care about typing without knowing what text is showing up in the TextView. Well, I am not a typing ninja.
The Solution
There is probably a more elegant solution to the problem, but I couldn't find it so you are stuck with what I figured out, and this is my solution:
When the text changes, get the coordinates of the cursor and scroll to those coordinates. This will ensure that the cursor is always visible to the user. How would you like to see a working example in Vala?
Enter the Vala
using Gtk;
public static void main( string[] args) {
//initialize gtk
Gtk.init(ref args);
//make a window
Window window = new Window();
//make a textbuffer
TextBuffer textBuffer = new TextBuffer(null);
//make a textview with our buffer
TextView textView= new TextView.with_buffer(textBuffer);
//let the textview wrap at word or char
textView.set_wrap_mode(WrapMode.WORD_CHAR);
//we need to know when the text buffer changes
textBuffer.changed.connect( ()=>{
//make a textiterator
TextIter iter = TextIter();
//what is the cursors x coordinate?
int x = textView.virtual_cursor_x;
//what is the cursors y coordinate?
int y = textView.virtual_cursor_y;
//get the text iter at the cursor's coordinates
textView.get_iter_at_location(out iter,x,y);
//scroll to the cursor's text iter
textView.scroll_to_iter(iter,0,false,0,0);
});
//make a scrolledwindow to hold the textviw
ScrolledWindow scroll = new ScrolledWindow (null, null);
//set the scrollwindow's scrolling policy
scroll.set_policy (PolicyType.AUTOMATIC, PolicyType.AUTOMATIC);
//add the textview to the scollwindow
scroll.add (textView);
//add the scrollwindow to our window
window.add(scroll);
//show all the widgets
window.show_all();
//run the gtk main loop
Gtk.main();
}
Save the above code in a file named "cursor.vala" and compile with
Now I need to port the code to Python and implement the change in markdowner.
Now quit reading, and go fix a problem with a dirty hack.
Also, I'm still working on that FOSScon blog post; depending on how things go, you might even see it today!
Recently, while writing code for heybuddy, I needed to:
- display a tray icon
- hide the window when "close window" is selected
- show the window when the tray icon is clicked
Although getting this done was easy enough to do, finding out how to do it was a bit of a pain, so I thought it would be best if I shared my Python/GTK test code and save future developers from some pain.
Enter the Python
#!/usr/bin/env python import gtk class Win (gtk.Window): def __init__(self): gtk.Window.__init__(self) self.set_title("Close to tray") self.connect("delete-event", self.delete_event) #create a virtical box to hold some widgets vbox = gtk.VBox(False) #make a quit button quitbutton = gtk.Button("Quit") quitbutton.connect("clicked",self.quit) #add the quitbutton to the vbox vbox.pack_start(quitbutton) #create a label to say *something* and add to the vbox label=gtk.Label("This is a test of 'close to tray' functionality") vbox.pack_start(label) #add the vbox to the Window self.add(vbox) #show all of the stuff self.show_all() #make a status icon self.statusicon = gtk.status_icon_new_from_stock(gtk.STOCK_GOTO_TOP) self.statusicon.connect('activate', self.status_clicked ) self.statusicon.set_tooltip("the window is visible") #start the gtk main loop gtk.main() def quit(self,button): #quit the gtk main loop gtk.main_quit() def delete_event(self,window,event): #don't delete; hide instead self.hide_on_delete() self.statusicon.set_tooltip("the window is hidden") return True def status_clicked(self,status): #unhide the window self.show_all() self.statusicon.set_tooltip("the window is visible") if __name__=="__main__": win = Win()
The basics of what needs to be done:
- connect the Window's "delete-event" signal to a function
- in the function, have the window "hide_on_delete()"
- return True ( if True is not returned, the Window's children will be destroyed and everything will go to crap )
So there you have it, an easy way to "close to tray" or whatever you want to call it.
Now quit reading, and go ease a developer's pain.
Do you know what else deals with the relationship of elements in a grid? MineSweeper!
MineSweeper happens to be one of my favorite computer games. At this point, it should be noted that there are few implementations of minesweeper for Linux. Of the two versions that I found, one requires Gnome and one Required KDE. Seriously? Why does a simple game need to require Gnome or KDE libs? Oh well, I guess I'll just have to write my own.
Enter the Vala:
/* Copyright 2010 jezra lickter licensed GPL version 3 http://www.gnu.org/licenses/gpl-3.0.html compile with: valac --pkg gtk+-2.0 main.vala -o sweeper */ using Gtk; using Gdk; public class Cell:EventBox { signal void flip_cell(); signal void flag_cell(); public int row; public int col; private Button button; private Label label; const string ONE= "blue"; const string TWO= "dark green"; const string THREE= "red"; const string FOUR= "purple"; const string FIVE= "orange"; const string SIX= "yellow"; private Color red; construct { button = new Button(); //box = new Box(); } public Cell(int r, int c) { Color.parse("red",out red); row=r; col=c; //create the cell button.set_relief(ReliefStyle.HALF); button.event.connect( (obj,event)=> { parse_event(event); } ); //box.pack_start(button, true, true, 0); add(button); label=new Label(null); set_size_request(30,30); //add(box); } public bool parse_event(Gdk.Event event) { if(event.button.type == Gdk.EventType.BUTTON_PRESS) { if (event.button.button==1) // && event.button.button<=3) { flip_cell(); }else if( event.button.button==3 ) { flag_cell(); } return true; }else{ return false; } } public void flag() { button.set_label("*"); } public void end_flag() { if(button.get_label()!="*") { show_label("*"); } } public void show_label(string str) { string color; switch(str) { case "1": color = ONE; break; case "2": color = TWO; break; case "3": color = THREE; break; case "4": color = FOUR; break; case "5": color = FIVE; break; case "6": color = SIX; break; /*case "7": color = SEVEN; break; */ default: color = "black"; break; } label.set_markup("<span foreground=\"%s\">%s</span>".printf(color,str) ); remove(button); add(label); label.show(); } public void show_baddy() { modify_bg(StateType.NORMAL, red ); label.set_markup("<span background=\"red\" font_weight=\"heavy\">*</span>"); remove(button); add(label); //box.pack_start(label,true,true,0); label.show(); } public void disable() { button.set_state(Gtk.StateType.INSENSITIVE); } } public class Application:Gtk.Window { MainLoop loop; bool[,] mineArray; bool[,] flippedArray; private Button startButton; private ComboBox levelCombo; private Table cellTable; private int rows {get;set;} private int cols {get;set;} private int mines; private Cell[,] cells; private VBox vbox; private bool game_playing; private bool game_needs_new_table; //define the small/medium/large values const int small_rows=6; const int small_cols=10; const int small_mines=10; const int medium_rows=10; const int medium_cols=16; const int medium_mines=26; const int large_rows=12; const int large_cols=20; const int large_mines=40; private int required_flips; private int flip_count; construct { type=Gtk.WindowType.TOPLEVEL; set_resizable(false); game_playing=false; game_needs_new_table=false; //we default to small rows = small_rows; cols = small_cols; mines = small_mines; } public Application() { destroy.connect(quit); vbox = new VBox(false,0); HBox hbox = new HBox(false,0); startButton = new Button.with_label("Start"); levelCombo = new ComboBox.text(); levelCombo.append_text("Small"); levelCombo.append_text("Medium"); levelCombo.append_text("Large"); //levelCombo.append_text("Custom"); levelCombo.set_active(0); levelCombo.changed.connect( level_changed ); Label dif = new Label("Size:"); startButton.clicked.connect( start_game ); hbox.pack_start(startButton,false,false,0); hbox.pack_start(dif,false,false,0); hbox.pack_start(levelCombo,false,false,0); hbox.set_spacing(5); vbox.pack_start(hbox,false,false,0); add(vbox); show_all(); //create the first table new_table(); cellTable.set_sensitive(false); } private void level_changed() { string val = levelCombo.get_active_text(); switch(val) { case "Small": rows = small_rows; cols = small_cols; mines = small_mines; break; case "Medium": rows = medium_rows; cols = medium_cols; mines = medium_mines; break; case "Large": rows = large_rows; cols = large_cols; mines = large_mines; break; } game_needs_new_table=true; start_game(); //we should probably resize the window } public void start_game() { if(game_needs_new_table) { new_table(); } game_playing=true; //disable the start button startButton.set_sensitive(false); cellTable.set_sensitive(true); } public void end_game() { //disable unflipped cells for(int r=0; r<rows; r++) { for(int c=0; c<cols; c++) { if(!flippedArray[r,c]) { //if this is a mine, mark it as such if(mineArray[r,c]) { cells[r,c].end_flag(); } cells[r,c].disable(); } } } game_needs_new_table=true; //enable the start button game_playing = false; startButton.set_sensitive(true); } public void new_table() { vbox.remove(cellTable); cellTable = make_table(); vbox.pack_start( cellTable,true,true,0 ); cellTable.show_all(); } private Table make_table() { //reset the required flips required_flips = rows*cols - mines; //reset the flip_count flip_count=0; mineArray = new bool[rows,cols]; flippedArray = new bool[rows,cols]; cells = new Cell[rows,cols]; Table table = new Table(rows,cols,true); for( int r=0;r<rows;r++ ) { for ( int c=0; c<cols; c++ ) { flippedArray[r,c]=false; mineArray[r,c]=false; cells[r,c] = new Cell(r,c); cells[r,c].flip_cell.connect( (cell)=>{ flip_cell(cell); } ); cells[r,c].flag_cell.connect( (cell)=>{ flag_cell(cell); } ); //add the cell to the table lrtb table.attach(cells[r,c],c,c+1,r,r+1,AttachOptions.FILL ,AttachOptions.FILL ,0,0); } } //make the mines int minecount=0; int randomrow; int randomcol; Rand rand = new Rand(); while(minecount<mines) { //pick a random row randomrow= rand.int_range(0,rows); //pick a random col randomcol = rand.int_range(0,cols); if(!mineArray[randomrow,randomcol]) { mineArray[randomrow,randomcol]=true; minecount++; } } return table; } public void flag_cell( Cell c ) { c.flag(); } private void flip_cell(Cell c) { //consider this flipped flippedArray[c.row,c.col]=true; if(mineArray[c.row,c.col]) { c.show_baddy(); end_game(); }else{ detect_neighboring_mines(c.row,c.col); flip_count+=1; if(flip_count==required_flips) { //we did it! end_game(); } } } private void detect_neighboring_mines(int r, int c) { string label=""; int mine_count = 0; int r_min = r-1; int r_max = r+1; int c_min = c-1; int c_max = c+1; if(r==0) { r_min=r; } else if (r == rows-1) { r_max=r; } if ( c==0 ) { c_min=c; } else if (c == cols-1) { c_max=c; } int tr = r_min; int tc = c_min; while(tr <= r_max) { tc = c_min; while(tc <= c_max) { if ( tr!=r || tc!=c ) { if ( mineArray[tr,tc]) { mine_count+=1; } } tc+=1; } tr+=1; } if(mine_count>0) { label = mine_count.to_string(); }else{ expand_from_empty_cell(r,c); } cells[r,c].show_label( label ); } private void expand_from_empty_cell(int r, int c) { int r_min = r-1; int r_max = r+1; int c_min = c-1; int c_max = c+1; if(r==0) { r_min=r; } else if (r == rows-1) { r_max=r; } if ( c==0 ) { c_min=c; } else if (c == cols-1) { c_max=c; } int tr = r_min; int tc = c_min; while(tr <= r_max) { tc = c_min; while(tc <= c_max) { if ( tr!=r || tc!=c ) { if ( !flippedArray[tr,tc]) { //check this cell flip_cell(cells[tr,tc]); //detect_neighboring_mines(tr,tc); } } tc+=1; } tr+=1; } } public void run() { loop = new MainLoop(); loop.run(); } public void quit() { loop.quit(); } } public static void main( string[] args) { Gtk.init(ref args); Application app = new Application(); app.run(); }
YUCK! That is some ugly code. Normally, when I write some code, I will first write the steps I want the code to take as comments and I will then write the actual code of the app. Obviously, that was not the case this time. The passion took me.
Requirements
GTK
GDK
Vala
Compiling
valac --pkg gtk+-2.0 main.vala -o sweeper
Screenshots


The game is over when all of the non-mine squares have been selected. Yes, I know that there is no timer, or flag counter and honestly, I don't care; I just want to play.
Now stop reading, and go sweep something.
At least for newbies like me. Thanks for sharing the code!
I really like the language, but currently I'm more focussed on others. Vala still is in an early stage I think, and breaks source compability too often for me :(
- Try to find another terminal app that does what I need
- Edit the Sakura soure code to add the maximize and undecorate option
- Code my own app
I elected for "code my own app" and I called it muterm because it is a (m)aximized (u)ndecorated (term)inal emulator. This would also be a good introduction for anyone interested in adding a terminal emulator to a Vala project.
Here is my main.vala code:
using Gtk; using Vte; private class Term { private Term() { //start with a gtk window Window w; //we also need a vte terminal Terminal term; //create our new window w = new Window(Gtk.WindowType.TOPLEVEL); //create the new terminal term = new Terminal(); //connect exiting on the terminal with quiting the app term.child_exited.connect ( (t)=> { Gtk.main_quit(); } ); //fork a command to run when the terminal is started term.fork_command(null,null,null,null, true, true,true); //add the terminal to the window w.add(term); //maximize the window w.maximize(); //undecorate the window w.set_decorated(false); //try to set the icon try{ w.set_icon_from_file("/usr/share/pixmaps/gnome-term.png"); }catch(Error er) { //we don't really need to print this error stdout.printf(er.message); } //show our window and all children of our window w.show_all(); } private void run() { //start the gtk mainloop Gtk.main(); } private static void main(string[] args) { Term t; Gtk.init(ref args); t = new Term(); t.run(); } }
The code is compiled with:
valac --pkg gtk+-2.0 --pkg vte main.vala -o muterm
It took me a while to realize that I didn't need to have values for all of the Vte.Terminal.fork_command() function. Howerver, I should allow passing a directory as an arguement and have muterm start in that directory as the `pwd`. Copying and pasting text in the terminal would also be a great idea. Maybe, we'll see what the future holds.
I'd show a screenshot, but really, how different do terminals really look?
term.set_backspace_binding(TerminalEraseBinding.ASCII_BACKSPACE);