-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.py
266 lines (213 loc) · 10.5 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
import kivy
import sys
import os
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
# to use buttons:
from kivy.uix.button import Button
from kivy.uix.screenmanager import ScreenManager, Screen
import client
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.uix.scrollview import ScrollView
kivy.require("1.10.1")
class ConnectPage(GridLayout):
# runs on initialization
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.cols = 2 # used for our grid
with open("prev_details.txt","r") as f:
d = f.read().split(",")
prev_ip = d[0]
prev_port = d[1]
prev_username = d[2]
self.add_widget(Label(text='IP:')) # widget #1, top left
self.ip = TextInput(text=prev_ip, multiline=False) # defining self.ip...
self.add_widget(self.ip) # widget #2, top right
self.add_widget(Label(text='Port:'))
self.port = TextInput(text=prev_port, multiline=False)
self.add_widget(self.port)
self.add_widget(Label(text='Username:'))
self.username = TextInput(text=prev_username, multiline=False)
self.add_widget(self.username)
# add our button.
self.join = Button(text="Join")
self.join.bind(on_press=self.join_button)
self.add_widget(Label()) # just take up the spot.
self.add_widget(self.join)
def join_button(self, instance):
port = self.port.text
ip = self.ip.text
username = self.username.text
with open("prev_details.txt","w") as f:
f.write(f"{ip},{port},{username}")
#print(f"Joining {ip}:{port} as {username}")
# Create info string, update InfoPage with a message and show it
info = f"Joining {ip}:{port} as {username}"
chat_app.info_page.update_info(info)
chat_app.screen_manager.current = 'Info'
Clock.schedule_once(self.connect, 1)
# Connects to the server
# (second parameter is the time after which this function had been called,
# we don't care about it, but kivy sends it, so we have to receive it)
def connect(self, _):
# Get information for sockets client
port = int(self.port.text)
ip = self.ip.text
username = self.username.text
if not client.connect(ip, port, username, show_error):
return
# Create chat page and activate it
chat_app.create_chat_page()
chat_app.screen_manager.current = 'Chat'
# This class is an improved version of Label
# Kivy does not provide scrollable label, so we need to create one
class ScrollableLabel(ScrollView):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# ScrollView does not allow us to add more than one widget, so we need to trick it
# by creating a layout and placing two widgets inside it
# Layout is going to have one collumn and and size_hint_y set to None,
# so height wo't default to any size (we are going to set it on our own)
self.layout = GridLayout(cols=1, size_hint_y=None)
self.add_widget(self.layout)
# Now we need two wodgets - Label for chat history and 'artificial' widget below
# so we can scroll to it every new message and keep new messages visible
# We want to enable markup, so we can set colors for example
self.chat_history = Label(size_hint_y=None, markup=True)
self.scroll_to_point = Label()
# We add them to our layout
self.layout.add_widget(self.chat_history)
self.layout.add_widget(self.scroll_to_point)
# Methos called externally to add new message to the chat history
def update_chat_history(self, message):
# First add new line and message itself
self.chat_history.text += '\n' + message
# Set layout height to whatever height of chat history text is + 15 pixels
# (adds a bit of space at teh bottom)
# Set chat history label to whatever height of chat history text is
# Set width of chat history text to 98 of the label width (adds small margins)
self.layout.height = self.chat_history.texture_size[1] + 15
self.chat_history.height = self.chat_history.texture_size[1]
self.chat_history.text_size = (self.chat_history.width * 0.98, None)
# As we are updating above, text height, so also label and layout height are going to be bigger
# than the area we have for this widget. ScrollView is going to add a scroll, but won't
# scroll to the botton, nor there is a method that can do that.
# That's why we want additional, empty wodget below whole text - just to be able to scroll to it,
# so scroll to the bottom of the layout
self.scroll_to(self.scroll_to_point)
def update_chat_history_layout(self, _=None):
self.layout.height=self.chat_history.texture_size[1]+15
self.chat_history.height=self.chat_history.texture_size[1]
self.chat_history.text_size=(self.chat_history.width*0.98,None)
class ChatPage(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# We are going to use 1 column and 2 rows
self.cols = 1
self.rows = 2
# First row is going to be occupied by our scrollable label
# We want it be take 90% of app height
self.history = ScrollableLabel(height=Window.size[1]*0.9, size_hint_y=None)
self.add_widget(self.history)
# In the second row, we want to have input fields and Send button
# Input field should take 80% of window width
# We also want to bind button click to send_message method
self.new_message = TextInput(width=Window.size[0]*0.8, size_hint_x=None, multiline=False)
self.send = Button(text="Send")
self.send.bind(on_press=self.send_message)
# To be able to add 2 widgets into a layout with just one collumn, we use additional layout,
# add widgets there, then add this layout to main layout as second row
bottom_line = GridLayout(cols=2)
bottom_line.add_widget(self.new_message)
bottom_line.add_widget(self.send)
self.add_widget(bottom_line)
Window.bind(on_key_down=self.on_key_down)
Clock.schedule_once(self.focus_text_input,1)
client.start_listening(self.incoming_message,show_error)
self.bind(size=self.adjust_fields)
def adjust_fields(self,*_):
if Window.size[1]*0.1<50:
new_height=Window.size[1]-50
else:
new_height=Window.size[1]*0.9
self.history.height=new_height
if Window.size[0]*0.2<160:
new_width=Window.size[0]-160
else:
new_width=Window.size[0]*0.8
self.new_message.width=new_width
Clock.schedule_once(self.history.update_chat_history_layout,0.01)
def on_key_down(self,instance,keyboard,keycode,text,modifiers):
if keycode==40:
self.send_message(None)
# Gets called when either Send button or Enter key is being pressed
# (kivy passes button object here as well, but we don;t care about it)
def send_message(self, _):
message=self.new_message.text
self.new_message.text=""
if message:
self.history.update_chat_history(f"[color=dd2020]{chat_app.connect_page.username.text}[/color] > {message}")
client.send(message)
Clock.schedule_once(self.focus_text_input,0.1)
def focus_text_input(self, _):
self.new_message.focus=True
def incoming_message(self,username,message):
self.history.update_chat_history(f"[color=20dd20]{username}[/color] > {message}")
# Simple information/error page
class InfoPage(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Just one column
self.cols = 1
# And one label with bigger font and centered text
self.message = Label(halign="center", valign="middle", font_size=30)
# By default every widget returns it's side as [100, 100], it gets finally resized,
# but we have to listen for size change to get a new one
# more: https://github.com/kivy/kivy/issues/1044
self.message.bind(width=self.update_text_width)
# Add text widget to the layout
self.add_widget(self.message)
# Called with a message, to update message text in widget
def update_info(self, message):
self.message.text = message
# Called on label width update, so we can set text width properly - to 90% of label width
def update_text_width(self, *_):
self.message.text_size = (self.message.width * 0.9, None)
class EpicApp(App):
def build(self):
# We are going to use screen manager, so we can add multiple screens
# and switch between them
self.screen_manager = ScreenManager()
# Initial, connection screen (we use passed in name to activate screen)
# First create a page, then a new screen, add page to screen and screen to screen manager
self.connect_page = ConnectPage()
screen = Screen(name='Connect')
screen.add_widget(self.connect_page)
self.screen_manager.add_widget(screen)
# Info page
self.info_page = InfoPage()
screen = Screen(name='Info')
screen.add_widget(self.info_page)
self.screen_manager.add_widget(screen)
return self.screen_manager
# We cannot create chat screen with other screens, as it;s init method will start listening
# for incoming connections, but at this stage connection is not being made yet, so we
# call this method later
def create_chat_page(self):
self.chat_page = ChatPage()
screen = Screen(name='Chat')
screen.add_widget(self.chat_page)
self.screen_manager.add_widget(screen)
# Error callback function, used by sockets client
# Updates info page with an error message, shows message and schedules exit in 10 seconds
# time.sleep() won't work here - will block Kivy and page with error message won't show up
def show_error(message):
chat_app.info_page.update_info(message)
chat_app.screen_manager.current = 'Info'
Clock.schedule_once(sys.exit, 10)
if __name__ == "__main__":
chat_app = EpicApp()
chat_app.run()