MainFrame.py 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262
  1. # -*- coding: utf-8 -*-
  2. #
  3. # author: oldj
  4. # blog: http://oldj.net
  5. # email: [email protected]
  6. #
  7. import os
  8. import sys
  9. import glob
  10. import simplejson as json
  11. import wx
  12. from wx import stc
  13. import ui
  14. import urllib
  15. import re
  16. import traceback
  17. import random
  18. import Queue
  19. import time
  20. from Hosts import Hosts
  21. from TaskbarIcon import TaskBarIcon
  22. from BackThreads import BackThreads
  23. import common_operations as co
  24. import lang
  25. sys_type = co.getSystemType()
  26. if sys_type == "linux":
  27. # Linux
  28. try:
  29. import pynotify
  30. pynotify.init("SwitchHosts!")
  31. except ImportError:
  32. pynotify = None
  33. elif sys_type == "mac":
  34. # Mac
  35. import gntp.notifier
  36. growl = gntp.notifier.GrowlNotifier(
  37. applicationName="SwitchHosts!",
  38. notifications=["New Updates", "New Messages"],
  39. defaultNotifications=["New Messages"],
  40. hostname="127.0.0.1", # Defaults to localhost
  41. # password="" # Defaults to a blank password
  42. )
  43. try:
  44. growl.register()
  45. has_growl = True
  46. except Exception:
  47. has_growl = False
  48. class MainFrame(ui.Frame):
  49. def __init__(self, mainjob, instance_name,
  50. parent=None, id=wx.ID_ANY, title=None, pos=wx.DefaultPosition,
  51. size=wx.DefaultSize,
  52. style=wx.DEFAULT_FRAME_STYLE,
  53. version=None, working_path=None,
  54. taskbar_icon=None,
  55. ):
  56. u""""""
  57. self.mainjob = mainjob
  58. self.instance_name = instance_name
  59. self.version = version
  60. self.default_title = "SwitchHosts! %s" % self.version
  61. self.sudo_password = ""
  62. self.is_running = True
  63. ui.Frame.__init__(self, parent, id,
  64. title or self.default_title, pos, size, style)
  65. self.taskbar_icon = taskbar_icon or TaskBarIcon(self)
  66. if taskbar_icon:
  67. self.taskbar_icon.setMainFrame(self)
  68. self.latest_stable_version = "0"
  69. self.__sys_hosts_path = None
  70. self.local_encoding = co.getLocalEncoding()
  71. self.sys_type = co.getSystemType()
  72. if working_path:
  73. working_path = working_path.decode(self.local_encoding)
  74. self.working_path = working_path
  75. self.configs_path = os.path.join(self.working_path, "configs.json")
  76. self.hosts_path = os.path.join(self.working_path, "hosts")
  77. if not os.path.isdir(self.hosts_path):
  78. os.makedirs(self.hosts_path)
  79. self.active_fn = os.path.join(self.working_path, ".active")
  80. self.task_qu = Queue.Queue(4096)
  81. self.startBackThreads(2)
  82. self.makeHostsContextMenu()
  83. self.init2()
  84. self.initBind()
  85. # self.task_qu.put(self.chkActive)
  86. def init2(self):
  87. self.showing_rnd_id = random.random()
  88. self.is_switching_text = False
  89. self.current_using_hosts = None
  90. self.current_showing_hosts = None
  91. self.current_tree_hosts = None
  92. self.current_dragging_hosts = None
  93. self.current_tree_item = None # 当前选中的树无素
  94. self.origin_hostses = []
  95. self.common_hostses = []
  96. self.hostses = []
  97. self.fn_common_hosts = "COMMON.hosts"
  98. self.configs = {}
  99. self.loadConfigs()
  100. common_host_file_path = os.path.join(self.hosts_path, self.fn_common_hosts)
  101. if not os.path.isfile(common_host_file_path):
  102. common_file = open(common_host_file_path, "w")
  103. common_file.write("# common")
  104. common_file.close()
  105. hosts = Hosts(path=common_host_file_path, is_common=True)
  106. self.addHosts(hosts)
  107. self.getSystemHosts()
  108. self.scanSavedHosts()
  109. if not os.path.isdir(self.hosts_path):
  110. os.makedirs(self.hosts_path)
  111. def initBind(self):
  112. u"""初始化时绑定事件"""
  113. self.Bind(wx.EVT_CLOSE, self.OnClose)
  114. self.Bind(wx.EVT_MENU, self.OnExit, id=wx.ID_EXIT)
  115. self.Bind(wx.EVT_MENU, self.OnAbout, id=wx.ID_ABOUT)
  116. self.Bind(wx.EVT_MENU, self.OnHomepage, self.m_menuItem_homepage)
  117. self.Bind(wx.EVT_MENU, self.OnFeedback, self.m_menuItem_feedback)
  118. self.Bind(wx.EVT_MENU, self.OnChkUpdate, self.m_menuItem_chkUpdate)
  119. self.Bind(wx.EVT_MENU, self.OnNew, self.m_menuItem_new)
  120. self.Bind(wx.EVT_MENU, self.OnDel, id=wx.ID_DELETE)
  121. self.Bind(wx.EVT_MENU, self.OnApply, id=wx.ID_APPLY)
  122. self.Bind(wx.EVT_MENU, self.OnEdit, id=wx.ID_EDIT)
  123. self.Bind(wx.EVT_MENU, self.OnRefresh, id=wx.ID_REFRESH)
  124. self.Bind(wx.EVT_MENU, self.OnExport, self.m_menuItem_export)
  125. self.Bind(wx.EVT_MENU, self.OnImport, self.m_menuItem_import)
  126. self.Bind(wx.EVT_MENU, self.OnDonate, self.m_menuItem_donate)
  127. self.Bind(wx.EVT_BUTTON, self.OnNew, self.m_btn_add)
  128. self.Bind(wx.EVT_BUTTON, self.OnApply, id=wx.ID_APPLY)
  129. self.Bind(wx.EVT_BUTTON, self.OnDel, id=wx.ID_DELETE)
  130. self.Bind(wx.EVT_BUTTON, self.OnRefresh, id=wx.ID_REFRESH)
  131. self.Bind(wx.EVT_BUTTON, self.OnEdit, id=wx.ID_EDIT)
  132. self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnTreeSelectionChange, self.m_tree)
  133. self.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.OnTreeRClick, self.m_tree)
  134. self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.OnTreeActive, self.m_tree)
  135. self.Bind(wx.EVT_TREE_END_LABEL_EDIT, self.OnRenameEnd, self.m_tree)
  136. self.Bind(wx.EVT_TREE_BEGIN_DRAG, self.OnTreeBeginDrag, self.m_tree)
  137. self.Bind(wx.EVT_TREE_END_DRAG, self.OnTreeEndDrag, self.m_tree)
  138. self.Bind(stc.EVT_STC_CHANGE, self.OnHostsChange, id=self.ID_HOSTS_TEXT)
  139. def startBackThreads(self, count=1):
  140. self.back_threads = []
  141. for i in xrange(count):
  142. t = BackThreads(task_qu=self.task_qu)
  143. t.start()
  144. self.back_threads.append(t)
  145. def stopBackThreads(self):
  146. for t in self.back_threads:
  147. t.stop()
  148. def makeHostsContextMenu(self):
  149. self.hosts_item_menu = wx.Menu()
  150. self.hosts_item_menu.Append(wx.ID_APPLY, u"切换到当前hosts")
  151. self.hosts_item_menu.Append(wx.ID_EDIT, u"编辑")
  152. self.hosts_item_menu.AppendMenu(-1, u"图标", self.makeSubIconMenu())
  153. self.hosts_item_menu.AppendSeparator()
  154. self.hosts_item_menu.Append(wx.ID_REFRESH, u"刷新")
  155. self.hosts_item_menu.Append(wx.ID_DELETE, u"删除")
  156. def makeSubIconMenu(self):
  157. u"""生成图标子菜单"""
  158. menu = wx.Menu()
  159. def _f(i):
  160. return lambda e: self.setHostsIcon(e, i)
  161. icons_length = len(co.ICONS)
  162. for i in range(icons_length):
  163. item_id = wx.NewId()
  164. mitem = wx.MenuItem(menu, item_id, u"图标#%d" % (i + 1))
  165. mitem.SetBitmap(co.GetMondrianBitmap(i))
  166. menu.AppendItem(mitem)
  167. self.Bind(wx.EVT_MENU, _f(i), id=item_id)
  168. return menu
  169. def setHostsIcon(self, event=None, i=0):
  170. u"""图标子菜单,点击动作的响应函数"""
  171. hosts = self.current_showing_hosts
  172. if not hosts:
  173. return
  174. hosts.icon_idx = i
  175. self.updateHostsIcon(hosts)
  176. hosts.save()
  177. def scanSavedHosts(self):
  178. u"""扫描目前保存的各个hosts"""
  179. fns = glob.glob(os.path.join(self.hosts_path, "*.hosts"))
  180. fns = [os.path.split(fn)[1] for fn in fns]
  181. if self.fn_common_hosts in fns:
  182. fns.remove(self.fn_common_hosts)
  183. cfg_hostses = self.configs.get("hostses", [])
  184. # 移除不存在的 hosts
  185. tmp_hosts = []
  186. for fn in cfg_hostses:
  187. if fn in fns:
  188. tmp_hosts.append(fn)
  189. cfg_hostses = tmp_hosts
  190. # 添加新的 hosts
  191. for fn in fns:
  192. if fn not in cfg_hostses:
  193. cfg_hostses.append(fn)
  194. self.configs["hostses"] = cfg_hostses
  195. self.saveConfigs()
  196. for fn in self.configs["hostses"]:
  197. path = os.path.join(self.hosts_path, fn)
  198. hosts = Hosts(path)
  199. if hosts.content:
  200. pass
  201. self.addHosts(hosts)
  202. def setHostsDir(self):
  203. pass
  204. @property
  205. def sys_hosts_path(self):
  206. u"""取得系统 hosts 文件的路径"""
  207. if not self.__sys_hosts_path:
  208. if os.name == "nt":
  209. systemroot = os.environ.get("SYSTEMROOT", "C:\\Windows")
  210. path = "%s\\System32\\drivers\\etc\\hosts" % systemroot
  211. else:
  212. path = "/etc/hosts"
  213. self.__sys_hosts_path = path if os.path.isfile(path) else None
  214. return self.__sys_hosts_path
  215. def getSystemHosts(self):
  216. path = self.sys_hosts_path
  217. if path:
  218. hosts = Hosts(path=path, title=lang.trans("origin_hosts"), is_origin=True)
  219. self.origin_hostses = [hosts]
  220. self.addHosts(hosts)
  221. self.highLightHosts(hosts)
  222. self.updateBtnStatus(hosts)
  223. def showHosts(self, hosts):
  224. self.showing_rnd_id = random.random()
  225. content = hosts.content if not hosts.is_loading else "loading..."
  226. self.is_switching_text = True
  227. self.m_textCtrl_content.SetReadOnly(False)
  228. self.m_textCtrl_content.SetValue(content)
  229. self.m_textCtrl_content.SetReadOnly(not self.getHostsAttr(hosts, "is_content_edit_able"))
  230. self.is_switching_text = False
  231. if self.current_showing_hosts:
  232. self.m_tree.SetItemBackgroundColour(self.current_showing_hosts.tree_item_id, None)
  233. self.m_tree.SetItemBackgroundColour(hosts.tree_item_id, "#ccccff")
  234. self.current_showing_hosts = hosts
  235. def tryToShowHosts(self, hosts):
  236. if hosts == self.current_showing_hosts:
  237. self.showHosts(hosts)
  238. def tryToSaveBySudoPassword(self, hosts, common_hosts):
  239. if not self.sudo_password:
  240. # 尝试获取sudo密码
  241. pswd = None
  242. dlg = wx.PasswordEntryDialog(None, u"请输入sudo密码:", u"需要管理员权限",
  243. style=wx.OK|wx.CANCEL
  244. )
  245. if dlg.ShowModal() == wx.ID_OK:
  246. pswd = dlg.GetValue().strip()
  247. dlg.Destroy()
  248. if not pswd:
  249. return False
  250. self.sudo_password = pswd
  251. #尝试通过sudo密码保存
  252. try:
  253. hosts.save(path=self.sys_hosts_path, common=common_hosts,
  254. sudo_password=self.sudo_password)
  255. return True
  256. except Exception:
  257. print(traceback.format_exc())
  258. return False
  259. def useHosts(self, hosts):
  260. if hosts.is_loading:
  261. wx.MessageBox(u"当前 hosts 内容正在下载中,请稍后再试...")
  262. return
  263. msg = None
  264. is_success = False
  265. common_hosts = None
  266. try:
  267. for common_hosts in self.common_hostses:
  268. if common_hosts.is_common:
  269. break
  270. hosts.save(path=self.sys_hosts_path, common=common_hosts)
  271. is_success = True
  272. except Exception:
  273. err = traceback.format_exc()
  274. co.log(err)
  275. if "Permission denied:" in err:
  276. if sys_type in ("linux", "mac") and self.tryToSaveBySudoPassword(
  277. hosts, common_hosts
  278. ):
  279. is_success = True
  280. else:
  281. msg = u"切换 hosts 失败!\n没有修改 '%s' 的权限!" % self.sys_hosts_path
  282. else:
  283. msg = u"切换 hosts 失败!\n\n%s" % err
  284. if msg and self.current_showing_hosts:
  285. wx.MessageBox(msg, caption=u"出错啦!")
  286. return
  287. if is_success:
  288. if len(self.origin_hostses) > 0:
  289. self.origin_hostses[0].icon_idx = hosts.icon_idx
  290. self.notify(msg=u"hosts 已切换为「%s」。" % hosts.title, title=u"hosts 切换成功")
  291. self.tryToFlushDNS()
  292. self.highLightHosts(hosts)
  293. def tryToFlushDNS(self):
  294. u"""尝试更新 DNS 缓存
  295. @see http://cnzhx.net/blog/how-to-flush-dns-cache-in-linux-windows-mac/
  296. """
  297. try:
  298. if self.sys_type == "mac":
  299. cmd = "dscacheutil -flushcache"
  300. os.popen(cmd)
  301. elif self.sys_type == "win":
  302. cmd = "ipconfig /flushdns"
  303. os.popen(cmd)
  304. elif self.sys_type == "linux":
  305. cmd = "service nscd restart"
  306. os.popen(cmd)
  307. except Exception:
  308. pass
  309. def highLightHosts(self, hosts):
  310. u"""将切换的host文件高亮显示"""
  311. self.m_tree.SelectItem(hosts.tree_item_id)
  312. if self.current_using_hosts:
  313. self.m_tree.SetItemBold(self.current_using_hosts.tree_item_id, bold=False)
  314. self.m_tree.SetItemBold(hosts.tree_item_id)
  315. self.showHosts(hosts)
  316. self.current_using_hosts = hosts
  317. self.updateIcon()
  318. def updateIcon(self):
  319. co.log("update icon")
  320. if self.current_using_hosts:
  321. if len(self.origin_hostses) > 0:
  322. self.updateHostsIcon(self.origin_hostses[0])
  323. self.SetIcon(co.GetMondrianIcon(self.current_using_hosts.icon_idx))
  324. self.taskbar_icon.updateIcon()
  325. def addHosts(self, hosts, show_after_add=False):
  326. if hosts.is_origin:
  327. tree = self.m_tree_origin
  328. list_hosts = self.origin_hostses
  329. elif hosts.is_online:
  330. tree = self.m_tree_online
  331. list_hosts = self.hostses
  332. elif hosts.is_common:
  333. tree = self.m_tree_common
  334. list_hosts = self.common_hostses
  335. else:
  336. tree = self.m_tree_local
  337. list_hosts = self.hostses
  338. if hosts.is_origin:
  339. hosts.tree_item_id = self.m_tree_origin
  340. elif hosts.is_common:
  341. hosts.tree_item_id = self.m_tree_common
  342. list_hosts.append(hosts)
  343. else:
  344. list_hosts.append(hosts)
  345. hosts.tree_item_id = self.m_tree.AppendItem(tree, hosts.title)
  346. self.updateHostsIcon(hosts)
  347. self.m_tree.Expand(tree)
  348. if show_after_add:
  349. self.m_tree.SelectItem(hosts.tree_item_id)
  350. def updateHostsIcon(self, hosts):
  351. icon_idx = hosts.icon_idx
  352. if type(icon_idx) not in (int, long) or icon_idx < 0:
  353. icon_idx = 0
  354. elif icon_idx >= len(self.ico_colors_idx):
  355. icon_idx = len(self.ico_colors_idx) - 1
  356. self.m_tree.SetItemImage(
  357. hosts.tree_item_id, self.ico_colors_idx[icon_idx], wx.TreeItemIcon_Normal
  358. )
  359. # if hosts == self.current_using_hosts:
  360. # self.updateIcon()
  361. def delHosts(self, hosts):
  362. if not hosts:
  363. return False
  364. if hosts.is_origin:
  365. wx.MessageBox(u"初始 hosts 不能删除哦~", caption=u"出错啦!")
  366. return False
  367. if hosts == self.current_using_hosts:
  368. wx.MessageBox(u"这个 hosts 方案正在使用,不能删除哦~", caption=u"出错啦!")
  369. return False
  370. dlg = wx.MessageDialog(None, u"确定要删除 hosts '%s'?" % hosts.title, u"删除 hosts",
  371. wx.YES_NO | wx.ICON_QUESTION
  372. )
  373. ret_code = dlg.ShowModal()
  374. if ret_code != wx.ID_YES:
  375. dlg.Destroy()
  376. return False
  377. dlg.Destroy()
  378. try:
  379. hosts.remove()
  380. except Exception:
  381. err = traceback.format_exc()
  382. wx.MessageBox(err, caption=u"出错啦!")
  383. return False
  384. self.m_tree.Delete(hosts.tree_item_id)
  385. self.hostses.remove(hosts)
  386. cfg_hostses = self.configs.get("hostses")
  387. if cfg_hostses and hosts.title in cfg_hostses:
  388. cfg_hostses.remove(hosts.title)
  389. return True
  390. def export(self, path):
  391. u"""将当前所有设置以及方案导出为一个文件"""
  392. data = {
  393. "version": self.version,
  394. "configs": self.configs,
  395. }
  396. hosts_files = []
  397. for hosts in self.hostses:
  398. hosts_files.append({
  399. "filename": hosts.filename,
  400. "content": hosts.full_content,
  401. })
  402. data["hosts_files"] = hosts_files
  403. try:
  404. self.writeFile(path, json.dumps(data))
  405. except Exception:
  406. wx.MessageBox(u"导出失败!\n\n%s" % traceback.format_exc(), caption=u"出错啦!")
  407. return
  408. wx.MessageBox(u"导出完成!")
  409. def importHosts(self, content):
  410. u"""导入"""
  411. try:
  412. data = json.loads(content)
  413. except Exception:
  414. wx.MessageBox(u"档案解析出错了!", caption=u"导入失败")
  415. return
  416. if type(data) != dict:
  417. wx.MessageBox(u"档案格式有误!", caption=u"导入失败")
  418. return
  419. configs = data.get("configs")
  420. hosts_files = data.get("hosts_files")
  421. if type(configs) != dict or type(hosts_files) not in (list, tuple):
  422. wx.MessageBox(u"档案数据有误!", caption=u"导入失败")
  423. return
  424. # 删除现有 hosts 文件
  425. current_files = glob.glob(os.path.join(self.hosts_path, "*.hosts"))
  426. for fn in current_files:
  427. try:
  428. os.remove(fn)
  429. except Exception:
  430. wx.MessageBox(u"删除 '%s' 时失败!\n\n%s" % (fn, traceback.format_exc()),
  431. caption=u"导入失败")
  432. return
  433. # 写入新 hosts 文件
  434. for hf in hosts_files:
  435. if type(hf) != dict or "filename" not in hf or "content" not in hf:
  436. continue
  437. fn = hf["filename"].strip()
  438. if not fn or not fn.lower().endswith(".hosts"):
  439. continue
  440. try:
  441. self.writeFile(os.path.join(self.hosts_path, fn), hf["content"].strip().encode("utf-8"))
  442. except Exception:
  443. wx.MessageBox(u"写入 '%s' 时失败!\n\n%s" % (fn, traceback.format_exc()),
  444. caption=u"导入失败")
  445. return
  446. # 更新 configs
  447. # self.configs = {}
  448. try:
  449. self.writeFile(self.configs_path, json.dumps(configs).encode("utf-8"))
  450. except Exception:
  451. wx.MessageBox(u"写入 '%s' 时失败!\n\n%s" % (self.configs_path, traceback.format_exc()),
  452. caption=u"导入失败")
  453. return
  454. # self.clearTree()
  455. # self.init2()
  456. wx.MessageBox(u"导入成功!")
  457. self.restart()
  458. def restart(self):
  459. u"""重启主界面程序"""
  460. self.mainjob.toRestart(None)
  461. # self.mainjob.toRestart(self.taskbar_icon)
  462. self.stopBackThreads()
  463. self.taskbar_icon.Destroy()
  464. self.Destroy()
  465. def clearTree(self):
  466. for hosts in self.all_hostses:
  467. self.m_tree.Delete(hosts.tree_item_id)
  468. def notify(self, msg="", title=u"消息"):
  469. def macGrowlNotify(msg, title):
  470. try:
  471. growl.notify(
  472. noteType="New Messages",
  473. title=title,
  474. description=msg,
  475. sticky=False,
  476. priority=1,
  477. )
  478. except Exception:
  479. pass
  480. if self.sys_type == "mac":
  481. # Mac 系统
  482. if has_growl:
  483. macGrowlNotify(msg, title)
  484. elif self.sys_type == "linux":
  485. # linux 系统
  486. pynotify.Notification(title, msg).show()
  487. else:
  488. try:
  489. import ToasterBox as TB
  490. except ImportError:
  491. TB = None
  492. sw, sh = wx.GetDisplaySize()
  493. width, height = 210, 50
  494. px = sw - 230
  495. py = sh - 100
  496. tb = TB.ToasterBox(self)
  497. tb.SetPopupText(msg)
  498. tb.SetPopupSize((width, height))
  499. tb.SetPopupPosition((px, py))
  500. tb.Play()
  501. self.SetFocus()
  502. def updateConfigs(self, configs):
  503. keys = ("hostses",)
  504. for k in keys:
  505. if k in configs:
  506. self.configs[k] = configs[k]
  507. # 校验配置有效性
  508. if type(self.configs.get("hostses")) != list:
  509. self.configs["hostses"] = []
  510. def loadConfigs(self):
  511. if os.path.isfile(self.configs_path):
  512. try:
  513. configs = json.loads(open(self.configs_path, "rb").read())
  514. except Exception:
  515. wx.MessageBox("读取配置信息失败!", caption=u"出错啦!")
  516. return
  517. if type(configs) != dict:
  518. wx.MessageBox("配置信息格式有误!", caption=u"出错啦!")
  519. return
  520. self.updateConfigs(configs)
  521. self.saveConfigs()
  522. def saveConfigs(self):
  523. try:
  524. self.writeFile(self.configs_path, json.dumps(self.configs))
  525. except Exception:
  526. wx.MessageBox("保存配置信息失败!\n\n%s" % traceback.format_exc(), caption=u"出错啦!")
  527. def eachHosts(self, func):
  528. for hosts in self.hostses:
  529. func(hosts)
  530. @property
  531. def all_hostses(self):
  532. return self.origin_hostses + self.hostses
  533. @property
  534. def local_hostses(self):
  535. return [hosts for hosts in self.hostses if not hosts.is_online]
  536. @property
  537. def online_hostses(self):
  538. return [hosts for hosts in self.hostses if hosts.is_online]
  539. def makeNewHostsFileName(self):
  540. u"""生成一个新的 hosts 文件名"""
  541. fns = glob.glob(os.path.join(self.hosts_path, "*.hosts"))
  542. fns = [os.path.split(fn)[1] for fn in fns]
  543. for i in xrange(1024):
  544. fn = "%d.hosts" % i
  545. if fn not in fns:
  546. break
  547. else:
  548. return None
  549. return fn
  550. def saveHosts(self, hosts):
  551. try:
  552. if hosts.save():
  553. co.log("saved.")
  554. return True
  555. except Exception:
  556. err = traceback.format_exc()
  557. if "Permission denied:" in err:
  558. msg = u"没有修改 '%s' 的权限!" % hosts.path
  559. else:
  560. msg = u"保存 hosts 失败!\n\n%s" % err
  561. wx.MessageBox(msg, caption=u"出错啦!")
  562. return False
  563. def showDetailEditor(self, hosts=None, default_is_online=False):
  564. u"""显示详情编辑窗口"""
  565. dlg = ui.Dlg_addHosts(self)
  566. if hosts:
  567. # 初始化值
  568. dlg.m_radioBtn_local.SetValue(not hosts.is_online)
  569. dlg.m_radioBtn_online.SetValue(hosts.is_online)
  570. dlg.m_radioBtn_local.Enable(False)
  571. dlg.m_radioBtn_online.Enable(False)
  572. dlg.m_textCtrl_title.SetValue(hosts.title)
  573. if hosts.url:
  574. dlg.m_textCtrl_url.SetValue(hosts.url)
  575. dlg.m_textCtrl_url.Enable(True)
  576. else:
  577. dlg.m_radioBtn_local.SetValue(not default_is_online)
  578. dlg.m_radioBtn_online.SetValue(default_is_online)
  579. dlg.m_textCtrl_url.Enabled = default_is_online
  580. if dlg.ShowModal() != wx.ID_OK:
  581. dlg.Destroy()
  582. return
  583. dlg.Destroy()
  584. is_online = dlg.m_radioBtn_online.GetValue()
  585. title = dlg.m_textCtrl_title.GetValue().strip()
  586. url = dlg.m_textCtrl_url.GetValue().strip()
  587. if not title:
  588. wx.MessageBox(u"方案名不能为空!", caption=u"出错啦!")
  589. return
  590. for h in self.hostses:
  591. if h != hosts and h.title == title:
  592. wx.MessageBox(u"已经有名为 '%s' 的方案了!" % title, caption=u"出错啦!")
  593. return
  594. if not hosts:
  595. # 新建 hosts
  596. fn = self.makeNewHostsFileName()
  597. if not fn:
  598. wx.MessageBox(u"hosts 文件数超出限制,无法再创建新 hosts 了!", caption=u"出错啦!")
  599. return
  600. path = os.path.join(self.hosts_path, fn)
  601. hosts = Hosts(path, title=title, url=url if is_online else None)
  602. hosts.content = u"# %s" % title
  603. if hosts.is_online:
  604. self.getHostsContent(hosts)
  605. self.addHosts(hosts, show_after_add=True)
  606. else:
  607. # 修改 hosts
  608. hosts.is_online = is_online
  609. hosts.title = title
  610. hosts.url = url if is_online else None
  611. self.updateHostsTitle(hosts)
  612. self.saveHosts(hosts)
  613. def getHostsContent(self, hosts):
  614. hosts.is_loading = True
  615. def tryToDestroy(obj):
  616. # mac 下,progress_dlg 销毁时总是会异常退出...
  617. if sys_type != "mac":
  618. try:
  619. obj.Destroy()
  620. except Exception:
  621. print(traceback.format_exc())
  622. if hosts.is_online:
  623. progress_dlg = wx.ProgressDialog(u"加载中",
  624. u"正在加载「%s」...\nURL: %s" % (hosts.title, hosts.url), 100,
  625. style=wx.PD_AUTO_HIDE
  626. )
  627. self.task_qu.put(lambda : [
  628. wx.CallAfter(progress_dlg.Update, 10),
  629. hosts.getContent(force=True, progress_dlg=progress_dlg),
  630. wx.CallAfter(progress_dlg.Update, 80),
  631. wx.CallAfter(self.tryToShowHosts, hosts),
  632. wx.CallAfter(progress_dlg.Update, 90),
  633. wx.CallAfter(self.saveHosts, hosts),
  634. wx.CallAfter(progress_dlg.Update, 100),
  635. # wx.CallAfter(lambda : progress_dlg.Destroy() and self.SetFocus()),
  636. wx.CallAfter(lambda : tryToDestroy(progress_dlg)),
  637. wx.CallAfter(self.SetFocus),
  638. ])
  639. else:
  640. self.task_qu.put(lambda : [
  641. hosts.getContent(force=True),
  642. wx.CallAfter(self.tryToShowHosts, hosts),
  643. wx.CallAfter(self.saveHosts, hosts),
  644. ])
  645. self.tryToShowHosts(hosts)
  646. def updateHostsTitle(self, hosts):
  647. u"""更新hosts的名称"""
  648. self.m_tree.SetItemText(hosts.tree_item_id, hosts.title)
  649. def getHostsFromTreeByEvent(self, event):
  650. item = event.GetItem()
  651. self.current_tree_item = item
  652. if item in (self.m_tree_online, self.m_tree_local, self.m_tree_root):
  653. pass
  654. elif self.current_using_hosts and item == self.current_using_hosts.tree_item_id:
  655. return self.current_using_hosts
  656. else:
  657. for hosts in self.all_hostses:
  658. if item == hosts.tree_item_id:
  659. return hosts
  660. for hosts in self.common_hostses:
  661. if item == hosts.tree_item_id:
  662. return hosts
  663. return None
  664. def getLatestStableVersion(self, alert=False):
  665. url = "https://github.com/oldj/SwitchHosts/blob/master/README.md"
  666. ver = None
  667. try:
  668. c = urllib.urlopen(url).read()
  669. # wx.CallAfter(progress_dlg.Update, 50)
  670. v = re.search(r"\bLatest Stable:\s?(?P<version>[\d\.]+)\b", c)
  671. if v:
  672. ver = v.group("version")
  673. self.latest_stable_version = ver
  674. co.log("last_stable_version: %s" % ver)
  675. except Exception:
  676. pass
  677. if not alert:
  678. return
  679. def _msg():
  680. if not ver:
  681. wx.MessageBox(u"未能取得最新版本号!", caption=u"出错啦!")
  682. else:
  683. cmpv = co.compareVersion(self.version, self.latest_stable_version)
  684. try:
  685. if cmpv >= 0:
  686. wx.MessageBox(u"当前已是最新版本!")
  687. else:
  688. if wx.MessageBox(
  689. u"更新的稳定版 %s 已经发布,现在立刻查看吗?" % self.latest_stable_version,
  690. u"发现新版本!",
  691. wx.YES_NO | wx.ICON_INFORMATION
  692. ) == wx.YES:
  693. self.openHomepage()
  694. except Exception:
  695. co.debugErr()
  696. pass
  697. wx.CallAfter(_msg)
  698. def getHostsAttr(self, hosts, key=None):
  699. attrs = {
  700. "is_refresh_able": hosts and hosts in self.all_hostses or hosts in self.common_hostses,
  701. "is_delete_able": hosts and hosts in self.hostses,
  702. "is_info_edit_able": hosts and not hosts.is_loading and hosts in self.hostses,
  703. "is_content_edit_able": hosts and not hosts.is_loading and
  704. (hosts in self.hostses or hosts in self.common_hostses),
  705. "is_apply_able": not hosts.is_common and not hosts.is_origin,
  706. }
  707. for k in attrs:
  708. attrs[k] = True if attrs[k] else False
  709. return attrs.get(key, False) if key else attrs
  710. def updateBtnStatus(self, hosts):
  711. hosts_attrs = self.getHostsAttr(hosts)
  712. # 更新下方按钮状态
  713. self.m_btn_refresh.Enable(hosts_attrs["is_refresh_able"])
  714. self.m_btn_del.Enable(hosts_attrs["is_delete_able"])
  715. self.m_btn_edit_info.Enable(hosts_attrs["is_info_edit_able"])
  716. self.m_btn_apply.Enable(hosts_attrs["is_apply_able"])
  717. # 更新右键菜单项状态
  718. self.hosts_item_menu.Enable(wx.ID_EDIT, hosts_attrs["is_info_edit_able"])
  719. self.hosts_item_menu.Enable(wx.ID_DELETE, hosts_attrs["is_delete_able"])
  720. self.hosts_item_menu.Enable(wx.ID_REFRESH, hosts_attrs["is_refresh_able"])
  721. self.hosts_item_menu.Enable(wx.ID_APPLY, hosts_attrs["is_apply_able"])
  722. def writeFile(self, path, content, mode="w"):
  723. try:
  724. path = path.encode(self.local_encoding)
  725. except Exception:
  726. co.debugErr()
  727. open(path, mode).write(content)
  728. def openHomepage(self):
  729. u"""打开项目主页"""
  730. url= "http://oldj.github.io/SwitchHosts/"
  731. wx.LaunchDefaultBrowser(url)
  732. def OnHomepage(self, event):
  733. self.openHomepage()
  734. def openFeedbackPage(self):
  735. u"""打开反馈主页"""
  736. url = "https://github.com/oldj/SwitchHosts/issues?direction=desc&sort=created&state=open"
  737. wx.LaunchDefaultBrowser(url)
  738. def OnFeedback(self, event):
  739. self.openFeedbackPage()
  740. def OnHostsChange(self, event):
  741. if self.is_switching_text:
  742. return
  743. self.current_showing_hosts.content = self.m_textCtrl_content.GetText().strip()
  744. self.saveHosts(self.current_showing_hosts)
  745. def OnChkUpdate(self, event):
  746. self.task_qu.put(lambda : [
  747. self.getLatestStableVersion(alert=True),
  748. ])
  749. def OnExit(self, event):
  750. self.is_running = False
  751. self.stopBackThreads()
  752. self.taskbar_icon.Destroy()
  753. self.Destroy()
  754. # 退出时删除进程锁文件
  755. lock_fn = os.path.join(self.working_path, self.instance_name) \
  756. if self.instance_name else None
  757. if lock_fn and os.path.isfile(lock_fn):
  758. os.remove(lock_fn)
  759. # sys.exit()
  760. def OnAbout(self, event):
  761. dlg = ui.AboutBox(version=self.version, latest_stable_version=self.latest_stable_version)
  762. dlg.ShowModal()
  763. dlg.Destroy()
  764. def OnTreeSelectionChange(self, event):
  765. u"""当点击左边树状结构的节点的时候触发"""
  766. hosts = self.getHostsFromTreeByEvent(event)
  767. if not hosts:
  768. return
  769. self.current_tree_hosts = hosts
  770. self.updateBtnStatus(hosts)
  771. if not hosts or (hosts not in self.hostses and hosts not in self.origin_hostses and hosts not in self.common_hostses):
  772. return event.Veto()
  773. if hosts and hosts != self.current_showing_hosts:
  774. if hosts.is_origin:
  775. # 重新读取系统 hosts 值
  776. hosts.getContent()
  777. self.showHosts(hosts)
  778. def OnTreeRClick(self, event):
  779. u"""在树节点上单击右键,展示右键菜单"""
  780. hosts = self.getHostsFromTreeByEvent(event)
  781. if hosts:
  782. self.OnTreeSelectionChange(event)
  783. self.m_tree.PopupMenu(self.hosts_item_menu, event.GetPoint())
  784. def OnTreeMenu(self, event):
  785. co.log("tree menu...")
  786. def OnTreeActive(self, event):
  787. u"""双击树的节点时候触发"""
  788. hosts = self.getHostsFromTreeByEvent(event)
  789. if hosts:
  790. if hosts.is_common or hosts.is_origin:
  791. return
  792. self.useHosts(hosts)
  793. def OnApply(self, event):
  794. u"""点击切换Hosts时候,触发该函数"""
  795. if self.current_showing_hosts and self.current_showing_hosts.is_common:
  796. return
  797. if self.current_showing_hosts:
  798. self.useHosts(self.current_showing_hosts)
  799. def OnDel(self, event):
  800. if self.delHosts(self.current_tree_hosts):
  801. self.current_showing_hosts = None
  802. def OnNew(self, event):
  803. is_online = False
  804. hosts = self.current_showing_hosts
  805. if hosts.is_online or self.current_tree_item == self.m_tree_online:
  806. is_online = True
  807. self.showDetailEditor(default_is_online=is_online)
  808. def OnEdit(self, event):
  809. self.showDetailEditor(hosts=self.current_showing_hosts)
  810. def OnRename(self, event):
  811. hosts = self.current_showing_hosts
  812. if not hosts:
  813. return
  814. if hosts in self.origin_hostses:
  815. wx.MessageBox(u"%s不能改名!" % lang.trans("origin_hosts"), caption=u"出错啦!")
  816. return
  817. self.m_tree.EditLabel(hosts.tree_item_id)
  818. def OnRenameEnd(self, event):
  819. hosts = self.current_showing_hosts
  820. if not hosts:
  821. return
  822. title = event.GetLabel().strip()
  823. if title and hosts.title != title:
  824. hosts.title = title
  825. hosts.save()
  826. else:
  827. event.Veto()
  828. def OnRefresh(self, event):
  829. hosts = self.current_showing_hosts
  830. self.getHostsContent(hosts)
  831. def OnExport(self, event):
  832. if wx.MessageBox(
  833. u"您可以将现在的 hosts 档案导出并共享给其他 SwitchHosts! 用户。\n\n" +
  834. u"注意,只有“%s”和“%s”中的 hosts 会被导出!" % (
  835. lang.trans("local_hosts"), lang.trans("online_hosts")),
  836. caption=u"导出档案",
  837. style=wx.OK | wx.CANCEL,
  838. ) != wx.OK:
  839. return
  840. wildcard = u"SwicthHosts! 档案 (*.swh)|*.swh"
  841. dlg = wx.FileDialog(self, u"导出为...", os.getcwd(), "hosts.swh", wildcard, wx.SAVE)
  842. if dlg.ShowModal() == wx.ID_OK:
  843. self.export(dlg.GetPath())
  844. dlg.Destroy()
  845. def OnImport(self, event):
  846. dlg = ui.Dlg_Import(self)
  847. if dlg.ShowModal() == wx.ID_OK:
  848. path = dlg.m_filePicker.GetPath()
  849. url = dlg.m_textCtrl_url.GetValue()
  850. content = None
  851. if dlg.m_notebook.GetSelection() != 1:
  852. # 本地
  853. if os.path.isfile(path):
  854. content = open(path).read()
  855. else:
  856. wx.MessageBox(u"%s 不是有效的文件路径!" % path, caption=u"出错啦!")
  857. else:
  858. # 在线
  859. if co.httpExists(url):
  860. content = urllib.urlopen(url).read()
  861. else:
  862. wx.MessageBox(u"URL %s 无法访问!" % url, caption=u"出错啦!")
  863. if content and wx.MessageBox(u"导入档案会替换现有设置及数据,确定要导入吗?",
  864. caption=u"警告",
  865. style=wx.OK | wx.CANCEL) == wx.OK:
  866. self.importHosts(content)
  867. dlg.Destroy()
  868. def OnDonate(self, event):
  869. wx.LaunchDefaultBrowser("https://me.alipay.com/oldj")
  870. def OnTreeBeginDrag(self, event):
  871. item = event.GetItem()
  872. hosts = self.getHostsFromTreeByEvent(event)
  873. if not hosts or hosts.is_origin or hosts.is_common:
  874. event.Veto()
  875. return
  876. co.log("drag start..")
  877. self.current_dragging_hosts = hosts
  878. self.__dragging_item = item
  879. event.Allow()
  880. self.m_tree.Bind(wx.EVT_MOTION, self._drag_OnMotion)
  881. self.m_tree.Bind(wx.EVT_LEFT_UP, self._drag_OnMouseLeftUp)
  882. def _drag_OnMotion(self, event):
  883. event.Skip()
  884. def _drag_OnMouseLeftUp(self, event):
  885. co.log("mouse left up..")
  886. self.m_tree.Unbind(wx.EVT_MOTION)
  887. self.m_tree.Unbind(wx.EVT_LEFT_UP)
  888. event.Skip()
  889. def OnTreeEndDrag(self, event):
  890. co.log("drag end..")
  891. target_item = event.GetItem()
  892. target_hosts = self.getHostsFromTreeByEvent(event)
  893. source_item = self.__dragging_item
  894. source_hosts = self.current_dragging_hosts
  895. self.__dragging_item = None
  896. self.current_dragging_hosts = None
  897. def getHostsIdx(hosts):
  898. idx = 0
  899. for h in self.hostses:
  900. if h == hosts:
  901. break
  902. if h.is_online == hosts.is_online:
  903. idx += 1
  904. return idx
  905. is_dragged = False
  906. if target_hosts and target_hosts != source_hosts and \
  907. source_hosts.is_online == target_hosts.is_online:
  908. # 拖到目标 hosts 上了
  909. parent = self.m_tree.GetItemParent(target_item)
  910. added_item_id = self.m_tree.InsertItemBefore(parent, getHostsIdx(target_hosts),
  911. source_hosts.title
  912. )
  913. source_hosts.tree_item_id = added_item_id
  914. # self.updateHostsTitle(source_hosts)
  915. self.updateHostsIcon(source_hosts)
  916. if source_hosts == self.current_using_hosts:
  917. self.highLightHosts(source_hosts)
  918. self.hostses.remove(source_hosts)
  919. self.hostses.insert(self.hostses.index(target_hosts), source_hosts)
  920. is_dragged = True
  921. elif target_item == self.m_tree_local and not source_hosts.is_online:
  922. # 拖到本地树上了
  923. pass
  924. elif target_item == self.m_tree_online and source_hosts.is_online:
  925. # 拖到在线树上了
  926. pass
  927. if is_dragged:
  928. self.updateConfigs({
  929. "hostses": [hosts.filename for hosts in self.hostses],
  930. })
  931. self.saveConfigs()
  932. self.m_tree.Delete(source_item)
  933. self.m_tree.SelectItem(source_hosts.tree_item_id)
  934. def OnActiveApp(self, event):
  935. """Called when the doc icon is clicked, and ???"""
  936. print("---")
  937. # self.GetTopWindow().Raise()
  938. self.Raise()
  939. def chkActive(self):
  940. u"""循环查看工作目录下是否有 .active 文件,有则激活主窗口"""
  941. if self.is_running and os.path.isfile(self.active_fn):
  942. print("active..")
  943. os.remove(self.active_fn)
  944. # print(dir(self.mainjob.app))
  945. self.Raise()
  946. wx.TopLevelWindow.RequestUserAttention(self)
  947. # self.mainjob.app.SetTopWindow(self)
  948. time.sleep(0.5)
  949. # wx.CallAfter(self.chkActive)
  950. if self.is_running:
  951. self.task_qu.put(self.chkActive)