Kivy GUI application

Assignment 1

Create a Graphical User Interface (GUI) program using Python 3 and the Kivy toolkit, as described in the following information and accompanying screencast. This assignment will help you build skills using classes and GUIs as well as giving you more practice using techniques like selection, repetition, exceptions, lists, file I/O and functions. Some requirements have in-text help references, like [0], that refer to the resources list near the bottom. Everything you need to know to complete this assignment can be found in the subject materials. 

NB: DO NOT USE GLOBAL VARIABLES

Program Overview:

Ensure that your program GUI has the following features, as demonstrated in the screenshots and accompanying screencast:

  • the left side of the screen contains a drop-down “spinner” for the user to choose the song sorting, and text entry fields for inputting information for a new song
  • the right side contains buttons for the songs, colour-coded based on whether they are learned or not
  • the status bar at the top of the right side shows the number of songs learned and still to learn
  • the status bar at the bottom of the right side shows messages about the state of the program, including updating when a song is clicked on
  • the user can add a new song by entering text in the input fields and clicking “Add Song”
  • the exact style (including colours) is up to you, but ensure that all functionality is readily accessible with your chosen GUI style

Program Functionality Details:

  • Complete the main program in a Kivy App subclass in main.py. There will be no main() function, but rather your program will run() the Kivy app.
  • The program should start by loading the same CSV file of songs as with your first assignment. This must be done with a method of your main app class and will save the songs as Song instances in a SongList instance.
  • The songs file must be saved when the program ends, updating any changes made with the app by the user.

Adding:

  • All song fields are required. If a field is left blank, the bottom status bar should display “All fields must be completed” when “Add Song” is clicked.
  • The pages field must be a valid integer. If this is invalid, the status bar should display “Please enter a valid number”.
  • Pressing the Tab key should move between the text fields.
  • When the user successfully adds a song, the fields should be cleared and the song should appear in the songs list on the right.
  • When the user clicks the “Clear” button, all text in the input fields and the status bar should be cleared.

Classes

One of the most important parts of this assignment is to learn how to use classes to create reusable data types that simplify and modularise your program. You should write and then test each method of each class – one at a time.

The starter code that you get in your repository when you start your work with GitHub includes two files (test_song.py and test_songlist.py) with incomplete code for testing your classes.

  • Complete the Song class in song.py. This should be a simple class with the required attributes for a song and the standard methods: __init__ (constructor), __str__ (used when displaying song details in the status message).
  • Complete the SongList class in songlist.py. It should contain a single attribute: a list of Song objects, and at least the following methods:

oget song by title – take in a title (string) and return the Song object with thattitle;

oadd song – add a single Song object to the song list attributeoget number of required songs

oget number of learned songs

oload songs (from csv file into Song objects in the list)osave songs (from song list into csv file)

osort (by the key passed in, then by title)

GUI Requirements:

The functionality can be achieved with a variety of GUI styles and colour schemes. You are welcome to customise the GUI, but it should do everything required and match any constraints specified.

Git/GitHub:

You must use Git version control with your project stored in the private repository on GitHub that will be created when you accept the GitHub classroom invitation above.

Solution

 main.py

 #!/usr/bin/env python3

“””

Name:

Date:

Brief Project Description:

GitHub URL:

“””

fromkivy.app import App

fromkivy.lang.builder import Builder

fromkivy.uix.widget import Widget

fromkivy.uix.boxlayout import BoxLayout

fromkivy.uix.button import Button

from song import Song

fromsonglist import SongList

LEARNED_COLOR = [0.75, 0.75, 0.25, 1]

TO_LEARN_COLOR = [0.25, 0.75, 0.65, 1]

classSongButton(Button):

“””Custom Button class for song.

Args:

song (Song): Represented song

“””

def __init__(self, song):

super().__init__()

self.text = str(song)

if(song.learned):

self.background_color = LEARNED_COLOR

else:

self.background_color = TO_LEARN_COLOR

classSongsToLearn(BoxLayout):

“””Layout containing all Widgets.”””

song_list = SongList()

def __init__(self):

super().__init__()

SongsToLearn.song_list.load_songs(‘songs.csv’)

def _valid_int(self, s):

“””Check if integer is in valid format.”””

try:

int(s)

if(len(s) == 4):

return True

return False

exceptValueError:

return False

def _update_learn_status_bar(self):

“””Update status bar in upper right corner.”””

self.ids.learn_status_bar.text = “To learn: %s. Learned: %s” % (self.song_list.get_number_learned(), self.song_list.get_number_required())

def _update_message_status_bar(self, text):

“””Update status bar in lower right corner.”””

self.ids.message_status_bar.text = text

def _clear_song_list(self):

“””Remove all song butttons.”””

children = list(self.ids.songs.children)

forsong_button in children:

self.ids.songs.remove_widget(song_button)

def _create_song_button(self, song):

“””Create new button containing song.”””

song_button = SongButton(song)

song_button.bind(on_press=self._learned_song)

returnsong_button

defupdate_song_list(self):

“””Insert buttons containing songs.”””

songs = SongsToLearn.song_list.songs

for song in songs:

self.ids.songs.add_widget(self._create_song_button(song))

self._update_learn_status_bar()

def _learned_song(self, instance):

“””Mark song as learned or unlearned.”””

title = instance.text.split(“\””)[1]

song = self.song_list.get_song(title)

self.song_list.has_learned_song(song, not song.learned)

self._clear_song_list()

self.update_song_list()

if(song.learned):

self._update_message_status_bar(“You have learned ” + title)

else:

self._update_message_status_bar(“You have to learn ” + title)

defadd_song(self):

“””Add new song to list.”””

title = self.ids.title_input.text

artist = self.ids.artist_input.text

year = self.ids.year_input.text

if(len(title) == 0 or len(artist) == 0 or len(year) == 0):

self._update_message_status_bar(“All fields must be completed”)

return

if(not self._valid_int(year)):

self._update_message_status_bar(“Please enter a valid number”)

return

new_song = Song(title, artist, year, False)

self.song_list.add_song(new_song)

new_song_button = self._create_song_button(new_song)

self.ids.songs.add_widget(new_song_button)

self._update_learn_status_bar()

self.clear()

self.sort_songs()

def clear(self):

“””Clear all input fields and status bar.”””

self.ids.title_input.text = “”

self.ids.artist_input.text = “”

self.ids.year_input.text = “”

self.ids.message_status_bar.text = “”

defsort_songs(self):

“””Sort song list by key.”””

self.song_list.sort(self.ids.sort_spinner.text)

self._clear_song_list()

self.update_song_list()

defsave_songs(self):

“””Save songs to csv file.”””

self.song_list.save_songs()

classSongsToLearnApp(App):

“””Main app”””

def __init__(self):

super().__init__()

self.songs_to_learn = None

def build(self):

Builder.load_file(‘app.kv’)

self.songs_to_learn = SongsToLearn()

self.songs_to_learn.update_song_list()

self.songs_to_learn.sort_songs()

returnself.songs_to_learn

defon_stop(self):

self.songs_to_learn.save_songs()

SongsToLearnApp().run()

 song.py

class Song():

“””Object representing song

Args:

title: Song title.

artist: Song artist.

year: Year when song was recorded.

learned: Has song been learned.

“””

def __init__(self, title=””, artist=””, year=””, learned=False):

self.title = title

self.artist = artist

self.year = year

self.learned = learned

defmark_learned(self):

“””Mark song as learned”””

self.learned = True

defmark_to_learn(self):

“””Mark song as needed to learn”””

self.learned = False

def __str__(self):

return “\”%s\” by %s (%s) %s” % (self.title, self.artist, self.year, “(learned)” if self.learned else “”)

def __repr__(self):

returnrepr((self.title, self.artist, self.year, self.learned))

 songlist.py

 from song import Song

from operator import attrgetter

classSongList():

“””List of songs”””

def __init__(self):

self.songs = list()

defget_song(self, title):

“””Return song based on title”””

for song in self.songs:

ifsong.title == title:

return song

defadd_song(self, song):

“””Add new song to list”””

self.songs.append(song)

defget_number_required (self):

“””Return number of songs which have to be learned”””

number_of_required = 0

for song in self.songs:

if(song.learned == False):

number_of_required += 1

returnnumber_of_required

defget_number_learned(self):

“””Return number of song which have been learned”””

number_of_learned = 0

for song in self.songs:

if(song.learned == True):

number_of_learned += 1

returnnumber_of_learned

defload_songs(self, filename):

“””Load songs from csv file”””

f = open(filename, ‘r’)

for song in f:

title, artist, year, learned = song.split(‘,’)

learned = True if learned.strip() == ‘y’ else False

self.add_song(Song(title, artist, year, learned))

defsave_songs(self):

“””Save songs to csv file”””

f = open(‘songs.csv’, ‘w’)

for song in self.songs:

f.write(“%s,%s,%s,%s\n” % (song.title, song.artist, song.year, “y” if song.learned else “n”))

def sort(self, key):

“””Sort songs based on key”””

self.songs = sorted(self.songs, key=lambda song: song.title)

if(key == “Artist”):

self.songs = sorted(self.songs, key=lambda song: song.artist)

elif(key == “Year”):

self.songs = sorted(self.songs, key=lambda song: song.year)

elif(key == “Learned”):

self.songs = sorted(self.songs, key=lambda song: song.learned)

defhas_learned_song(self, song, learned):

“””Mark song as learned or unlearned”””

index = self.songs.index(song)

if(learned):

self.songs[index].mark_learned()

else:

self.songs[index].mark_to_learn()

def __str__(self):

return “[” + ‘, ‘.join(str(song) for song in self.songs) + “]” 

test_song.py 

from song import Song

# test empty song (defaults)

song = Song()

print(song)

assert song.artist == “”

assert song.title == “”

assert song.year == “”

assert not song.learned

# test initial-value song

song2 = Song(“Amazing Grace”, “John Newton”, “1779”, True)

print(song2)

assert song2.title == “Amazing Grace”

assert song2.artist == “John Newton”

assert song2.year == “1779”

assert song2.learned

# testmark_learned()

song3 = Song(“Do I Wanna Know”, “Arctic Monkeys”, “2013”, False)

print(song3)

song3.mark_learned()

assert song3.learned

# testmark_to_learn()

song4 = Song(“Tighten Up”, “The Black Keys”, “2010”, True)

print(song4)

song4.mark_to_learn()

assert not song4.learned 

test_songlist.py

fromsonglist import SongList

from song import Song

# test empty SongList

song_list = SongList()

print(song_list)

print(len(song_list.songs))

assertlen(song_list.songs) == 0

# test loading songs

song_list.load_songs(‘songs.csv’)

print(song_list)

assertlen(song_list.songs) > 0  # assuming CSV file is not empty

# test sorting songs

song_list.sort(“Artist”)

print(song_list)

# test adding a new Song

list_size = len(song_list.songs)

new_song = Song(“Do I Wanna Know”, “Arctic Monkeys”, “2013”, False)

song_list.add_song(new_song)

print(song_list)

assertlen(song_list.songs) == list_size+1

# testget_song()

song = song_list.get_song(“Do I Wanna Know”)

print(song)

assertsong.artist == new_song.artist

assertsong.title == new_song.title

assertsong.year == new_song.year

assertsong.learned == new_song.learned

# test getting the number of required and learned songs (separately)

list_size = len(song_list.songs)

no_of_requred = song_list.get_number_required()

no_of_learned = song_list.get_number_learned()

print(“Number of required: %s, number of learned: %s, total songs: %s” % (no_of_requred, no_of_learned, list_size))

assert (no_of_learned + no_of_requred) == list_size

# test saving songs (check CSV file manually to see results)

song_list.save_songs()