| 1 | """ |
|---|
| 2 | GtkSourceEditor.cobra |
|---|
| 3 | by Todd A. |
|---|
| 4 | |
|---|
| 5 | This sample program relies on GtkSourceView# which is only known |
|---|
| 6 | to run on Linux, not Mac or Windows. |
|---|
| 7 | |
|---|
| 8 | pkg-config is commonly used with GTK libraries hence it is used |
|---|
| 9 | for properly referencing gtk-sharp and gtksourceview. |
|---|
| 10 | Install these packages using your system's package manager. |
|---|
| 11 | """ |
|---|
| 12 | |
|---|
| 13 | @args sharpgtk-sharp-2.0,gtksourceview2-sharp |
|---|
| 14 | |
|---|
| 15 | use Gtk |
|---|
| 16 | use GtkSourceView |
|---|
| 17 | use Pango |
|---|
| 18 | |
|---|
| 19 | |
|---|
| 20 | class Editor |
|---|
| 21 | |
|---|
| 22 | var _untitledCount = 0 |
|---|
| 23 | var _defaultTitle = 'Untitled' |
|---|
| 24 | var _filename = '' |
|---|
| 25 | var _modifiedBuffer = false |
|---|
| 26 | var _onDisk = false |
|---|
| 27 | |
|---|
| 28 | var _config = { |
|---|
| 29 | 'lineNumbers': true, |
|---|
| 30 | 'tabWidth': 4, |
|---|
| 31 | 'wrapLines': true, |
|---|
| 32 | 'maxFontSize': 24, |
|---|
| 33 | 'minFontSize': 6, |
|---|
| 34 | 'fontFamily': 'monospace', |
|---|
| 35 | } |
|---|
| 36 | |
|---|
| 37 | # The _ui is a dictionary of common parts of the editor that may need to be referenced |
|---|
| 38 | # throughout various parts of the code. |
|---|
| 39 | var _ui = { |
|---|
| 40 | 'window': nil, |
|---|
| 41 | 'sourceView': nil, |
|---|
| 42 | } |
|---|
| 43 | |
|---|
| 44 | get modifiedBuffer from var |
|---|
| 45 | """ |
|---|
| 46 | Keeps track of whether or not the current buffer was modified |
|---|
| 47 | and will need saving. |
|---|
| 48 | """ |
|---|
| 49 | |
|---|
| 50 | get onDisk from var |
|---|
| 51 | """ |
|---|
| 52 | Indicates whether or not the current buffer was loaded from, or already saved to, |
|---|
| 53 | a file that is on some non-volatile storage. |
|---|
| 54 | """ |
|---|
| 55 | |
|---|
| 56 | get source as String? |
|---|
| 57 | sourceBuffer = _ui['sourceBuffer'] to SourceBuffer |
|---|
| 58 | return sourceBuffer.text |
|---|
| 59 | |
|---|
| 60 | def main |
|---|
| 61 | Application.init |
|---|
| 62 | |
|---|
| 63 | accelGroup = AccelGroup() |
|---|
| 64 | window = Window('') |
|---|
| 65 | listen window.deleteEvent, ref .exitHandler |
|---|
| 66 | window.setDefaultSize(800, 600) |
|---|
| 67 | window.addAccelGroup(accelGroup) |
|---|
| 68 | _ui['window'] = window |
|---|
| 69 | _ui['accelGroup'] = accelGroup |
|---|
| 70 | |
|---|
| 71 | menubar = _buildMenubar |
|---|
| 72 | editor = _buildEditor |
|---|
| 73 | status = _buildStatus |
|---|
| 74 | |
|---|
| 75 | mainbox = VBox(false, 10) |
|---|
| 76 | |
|---|
| 77 | mainbox.packStart(menubar, false, false, 0) |
|---|
| 78 | mainbox.packStart(editor, true, true, 0) |
|---|
| 79 | mainbox.packStart(status, false, false, 0) |
|---|
| 80 | |
|---|
| 81 | window.add(mainbox) |
|---|
| 82 | window.showAll |
|---|
| 83 | |
|---|
| 84 | .updateTitle(nil) |
|---|
| 85 | |
|---|
| 86 | Application.run |
|---|
| 87 | |
|---|
| 88 | def menuNewHandler(sender, e) |
|---|
| 89 | if .modifiedBuffer |
|---|
| 90 | responseType = .promptToSave |
|---|
| 91 | if responseType == ResponseType.Yes, .saveBuffer(.onDisk) |
|---|
| 92 | else if responseType == ResponseType.Cancel, return |
|---|
| 93 | |
|---|
| 94 | .updateTitle(nil) |
|---|
| 95 | _onDisk = false |
|---|
| 96 | _modifiedBuffer = false |
|---|
| 97 | sourceBuffer = _ui['sourceBuffer'] to SourceBuffer |
|---|
| 98 | sourceBuffer.clear |
|---|
| 99 | |
|---|
| 100 | def menuOpenHandler(sender, e) |
|---|
| 101 | if .modifiedBuffer |
|---|
| 102 | responseType = .promptToSave |
|---|
| 103 | if responseType == ResponseType.Yes, .menuSaveHandler(sender, e) |
|---|
| 104 | else if responseType == ResponseType.Cancel, return |
|---|
| 105 | |
|---|
| 106 | fileChooser = FileChooserDialog('Select a file for opening', _ui['window'] to Window, _ |
|---|
| 107 | FileChooserAction.Open, 'Cancel', ResponseType.Cancel, 'Open', ResponseType.Accept) |
|---|
| 108 | |
|---|
| 109 | if fileChooser.run == (ResponseType.Accept to int) |
|---|
| 110 | filename = fileChooser.filename |
|---|
| 111 | using reader = StreamReader(File.openRead(filename)) |
|---|
| 112 | source = reader.readToEnd |
|---|
| 113 | sourceBuffer = _ui['sourceBuffer'] to SourceBuffer |
|---|
| 114 | sourceBuffer.text = source |
|---|
| 115 | |
|---|
| 116 | _filename = filename to ! |
|---|
| 117 | _onDisk = true |
|---|
| 118 | .updateTitle(filename) |
|---|
| 119 | |
|---|
| 120 | # Though the buffer is modified it is for a file that's just opened hence for |
|---|
| 121 | # editor purposes it is not modified. |
|---|
| 122 | _modifiedBuffer = false |
|---|
| 123 | |
|---|
| 124 | fileChooser.destroy |
|---|
| 125 | |
|---|
| 126 | def saveBuffer(onDisk as bool) |
|---|
| 127 | saved = false |
|---|
| 128 | if onDisk |
|---|
| 129 | .writeBuffer(_filename, .source) |
|---|
| 130 | saved = true |
|---|
| 131 | else |
|---|
| 132 | filename = .saveToFile |
|---|
| 133 | if filename |
|---|
| 134 | .writeBuffer(filename, .source) |
|---|
| 135 | _onDisk = true |
|---|
| 136 | .updateTitle(filename) |
|---|
| 137 | saved = true |
|---|
| 138 | |
|---|
| 139 | if saved, .updateStatus('File successfully saved') |
|---|
| 140 | |
|---|
| 141 | |
|---|
| 142 | def menuSaveHandler(sender, e) |
|---|
| 143 | if not .modifiedBuffer, return |
|---|
| 144 | .saveBuffer(.onDisk) |
|---|
| 145 | |
|---|
| 146 | def menuSaveAsHandler(sender, e) |
|---|
| 147 | .saveBuffer(false) |
|---|
| 148 | |
|---|
| 149 | def menuPrintHandler(sender, e) |
|---|
| 150 | printDialog = PrintUnixDialog('Print the current file', _ui['window'] to Window) |
|---|
| 151 | printDialog.run |
|---|
| 152 | printDialog.destroy |
|---|
| 153 | |
|---|
| 154 | def menuExitHandler(sender, e) |
|---|
| 155 | quit = true |
|---|
| 156 | if .modifiedBuffer |
|---|
| 157 | responseType = .promptToSave |
|---|
| 158 | if responseType == ResponseType.Cancel, quit = false |
|---|
| 159 | else if responseType == ResponseType.Yes, .saveBuffer(.onDisk) |
|---|
| 160 | if quit, Application.quit |
|---|
| 161 | |
|---|
| 162 | def exitHandler(sender, e as DeleteEventArgs) |
|---|
| 163 | quit = true |
|---|
| 164 | if .modifiedBuffer |
|---|
| 165 | responseType = .promptToSave |
|---|
| 166 | if responseType == ResponseType.Cancel, quit = false |
|---|
| 167 | else if responseType == ResponseType.Yes, .saveBuffer(.onDisk) |
|---|
| 168 | e.retVal = not quit |
|---|
| 169 | if quit, Application.quit |
|---|
| 170 | |
|---|
| 171 | def menuUndoHandler(sender, e) |
|---|
| 172 | sourceBuffer = _ui['sourceBuffer'] to SourceBuffer |
|---|
| 173 | sourceBuffer.undo |
|---|
| 174 | |
|---|
| 175 | def menuRedoHandler(sender, e) |
|---|
| 176 | sourceBuffer = _ui['sourceBuffer'] to SourceBuffer |
|---|
| 177 | sourceBuffer.redo |
|---|
| 178 | |
|---|
| 179 | def menuCutHandler(sender, e) |
|---|
| 180 | sourceBuffer = _ui['sourceBuffer'] to SourceBuffer |
|---|
| 181 | clipboard = Clipboard.get(Gdk.Selection.clipboard) |
|---|
| 182 | sourceBuffer.cutClipboard(clipboard, true) |
|---|
| 183 | |
|---|
| 184 | def menuCopyHandler(sender, e) |
|---|
| 185 | sourceBuffer = _ui['sourceBuffer'] to SourceBuffer |
|---|
| 186 | clipboard = Clipboard.get(Gdk.Selection.clipboard) |
|---|
| 187 | sourceBuffer.copyClipboard(clipboard) |
|---|
| 188 | |
|---|
| 189 | def menuPasteHandler(sender, e) |
|---|
| 190 | sourceBuffer = _ui['sourceBuffer'] to SourceBuffer |
|---|
| 191 | clipboard = Clipboard.get(Gdk.Selection.clipboard) |
|---|
| 192 | sourceBuffer.pasteClipboard(clipboard) |
|---|
| 193 | |
|---|
| 194 | def menuDeleteHandler(sender, e) |
|---|
| 195 | sourceBuffer = _ui['sourceBuffer'] to SourceBuffer |
|---|
| 196 | sourceBuffer.deleteSelection(true, true) |
|---|
| 197 | |
|---|
| 198 | def menuIncreaseFontSizeHandler(sender, e) |
|---|
| 199 | .changeFontSize(1) |
|---|
| 200 | .updateStatus('Increased font size') |
|---|
| 201 | |
|---|
| 202 | def menuDecreaseFontSizeHandler(sender, e) |
|---|
| 203 | .changeFontSize(-1) |
|---|
| 204 | .updateStatus('Decreased font size') |
|---|
| 205 | |
|---|
| 206 | def menuResetFontSizeHandler(sender, e) |
|---|
| 207 | sourceView = _ui['sourceView'] to SourceView |
|---|
| 208 | fontDesc = _ui['defaultFontDescription'] to FontDescription |
|---|
| 209 | sourceView.modifyFont(fontDesc) |
|---|
| 210 | |
|---|
| 211 | def menuAboutHandler(sender, e) |
|---|
| 212 | aboutDialog = MessageDialog(_ui['window'] to Window, DialogFlags.DestroyWithParent, _ |
|---|
| 213 | MessageType.Info, ButtonsType.Close, 'Copyright © 2010 Cobra Language. All rights reserved.') |
|---|
| 214 | aboutDialog.run |
|---|
| 215 | aboutDialog.destroy |
|---|
| 216 | |
|---|
| 217 | def menuSelectAllHandler(sender, e) |
|---|
| 218 | sourceBuffer = _ui['sourceBuffer'] |
|---|
| 219 | sourceBuffer.selectRange(sourceBuffer.startIter, sourceBuffer.endIter) |
|---|
| 220 | |
|---|
| 221 | def menuPassHandler(sender, e as EventArgs) |
|---|
| 222 | pass |
|---|
| 223 | |
|---|
| 224 | def bufferModifiedHandler(sender, e as EventArgs) |
|---|
| 225 | if not _modifiedBuffer, .updateStatus('') |
|---|
| 226 | _modifiedBuffer = true |
|---|
| 227 | .refresh |
|---|
| 228 | |
|---|
| 229 | def writeBuffer(filename as String?, contents as String?) |
|---|
| 230 | if filename and contents |
|---|
| 231 | using writer = StreamWriter(filename) |
|---|
| 232 | writer.write(contents) |
|---|
| 233 | _modifiedBuffer = false |
|---|
| 234 | |
|---|
| 235 | def saveToFile as String? |
|---|
| 236 | fileChooser = FileChooserDialog('Save file', _ui['window'] to Window, _ |
|---|
| 237 | FileChooserAction.Save, 'Cancel', ResponseType.Cancel, 'Save', ResponseType.Accept) |
|---|
| 238 | fileChooser.modal = true |
|---|
| 239 | |
|---|
| 240 | if fileChooser.run == (ResponseType.Accept to int) |
|---|
| 241 | filename = fileChooser.filename |
|---|
| 242 | fileChooser.destroy |
|---|
| 243 | return filename |
|---|
| 244 | |
|---|
| 245 | fileChooser.destroy |
|---|
| 246 | return nil |
|---|
| 247 | |
|---|
| 248 | def promptToSave as ResponseType |
|---|
| 249 | dialog = MessageDialog(_ui['window'] to Window, DialogFlags.Modal, MessageType.Question, _ |
|---|
| 250 | ButtonsType.None, 'Would you like to save the changes in ') |
|---|
| 251 | dialog.addButton('Yes', ResponseType.Yes) |
|---|
| 252 | dialog.addButton('No', ResponseType.No) |
|---|
| 253 | dialog.addButton('Cancel', ResponseType.Cancel) |
|---|
| 254 | responseType = dialog.run |
|---|
| 255 | dialog.destroy |
|---|
| 256 | return responseType to ResponseType |
|---|
| 257 | |
|---|
| 258 | def changeFontSize(delta as int) |
|---|
| 259 | sourceView = _ui['sourceView'] to SourceView |
|---|
| 260 | context = sourceView.pangoContext |
|---|
| 261 | fontDesc = context.fontDescription |
|---|
| 262 | scale = Pango.Scale.pangoScale |
|---|
| 263 | curSize = fontDesc.size / scale |
|---|
| 264 | curSize += delta |
|---|
| 265 | minSize = _config['minFontSize'] |
|---|
| 266 | maxSize = _config['maxFontSize'] |
|---|
| 267 | if curSize >= minSize and curSize <= maxSize |
|---|
| 268 | fontDesc.size = (curSize * scale) to int |
|---|
| 269 | sourceView.modifyFont(fontDesc) |
|---|
| 270 | |
|---|
| 271 | def refresh |
|---|
| 272 | """ |
|---|
| 273 | Update the state of UI elements based on the current editor state. |
|---|
| 274 | """ |
|---|
| 275 | sourceBuffer = _ui['sourceBuffer'] to SourceBuffer |
|---|
| 276 | redoItem = _ui['menu[Stock.redo]'] to MenuItem |
|---|
| 277 | undoItem = _ui['menu[Stock.undo]'] to MenuItem |
|---|
| 278 | |
|---|
| 279 | redoItem.sensitive = sourceBuffer.canRedo |
|---|
| 280 | undoItem.sensitive = sourceBuffer.canUndo |
|---|
| 281 | |
|---|
| 282 | def updateTitle(title as String?) |
|---|
| 283 | window = _ui['window'] to Window |
|---|
| 284 | if title and title.length |
|---|
| 285 | window.title = title |
|---|
| 286 | else |
|---|
| 287 | _untitledCount += 1 |
|---|
| 288 | _filename = '[_defaultTitle] [_untitledCount]' |
|---|
| 289 | window.title = _filename |
|---|
| 290 | |
|---|
| 291 | def updateStatus(message as String) |
|---|
| 292 | """ |
|---|
| 293 | Removes any message previously displayed in the status bar then display |
|---|
| 294 | a new one. |
|---|
| 295 | """ |
|---|
| 296 | statusBar = _ui['statusBar'] to Statusbar |
|---|
| 297 | statusBar.pop(1) |
|---|
| 298 | statusBar.push(1, message) |
|---|
| 299 | |
|---|
| 300 | def _buildMenubar as Widget |
|---|
| 301 | container = MenuBar() |
|---|
| 302 | |
|---|
| 303 | menuItems as Dictionary<of String, dynamic> = { |
|---|
| 304 | 'File': [ |
|---|
| 305 | [Stock.new, EventHandler(ref .menuNewHandler)], |
|---|
| 306 | [Stock.open, EventHandler(ref .menuOpenHandler)], |
|---|
| 307 | [Stock.save, EventHandler(ref .menuSaveHandler)], |
|---|
| 308 | [Stock.saveAs, EventHandler(ref .menuSaveAsHandler)], |
|---|
| 309 | ['----', nil], |
|---|
| 310 | [Stock.print, EventHandler(ref .menuPrintHandler)], |
|---|
| 311 | ['----', nil], |
|---|
| 312 | [Stock.quit, EventHandler(ref .menuExitHandler)] |
|---|
| 313 | ], |
|---|
| 314 | |
|---|
| 315 | 'Edit': [ |
|---|
| 316 | [Stock.undo, EventHandler(ref .menuUndoHandler), StateType.Insensitive], |
|---|
| 317 | [Stock.redo, EventHandler(ref .menuRedoHandler), StateType.Insensitive], |
|---|
| 318 | ['----', nil], |
|---|
| 319 | [Stock.cut, EventHandler(ref .menuCutHandler)], |
|---|
| 320 | [Stock.copy, EventHandler(ref .menuCopyHandler)], |
|---|
| 321 | [Stock.paste, EventHandler(ref .menuPasteHandler)], |
|---|
| 322 | [Stock.delete, EventHandler(ref .menuDeleteHandler)], |
|---|
| 323 | ['----', nil], |
|---|
| 324 | [Stock.selectAll, EventHandler(ref .menuSelectAllHandler)], |
|---|
| 325 | ], |
|---|
| 326 | |
|---|
| 327 | 'Format': [ |
|---|
| 328 | { |
|---|
| 329 | 'FontSize': [ |
|---|
| 330 | [Stock.zoomIn, EventHandler(ref .menuIncreaseFontSizeHandler)], |
|---|
| 331 | [Stock.zoomOut, EventHandler(ref .menuDecreaseFontSizeHandler)], |
|---|
| 332 | ['----', nil], |
|---|
| 333 | [Stock.zoom100, EventHandler(ref .menuResetFontSizeHandler)], |
|---|
| 334 | ] to System.Object, |
|---|
| 335 | } |
|---|
| 336 | ], |
|---|
| 337 | |
|---|
| 338 | 'Help': [ |
|---|
| 339 | [Stock.about, EventHandler(ref .menuAboutHandler)], |
|---|
| 340 | ], |
|---|
| 341 | } |
|---|
| 342 | |
|---|
| 343 | _buildMenu(container, menuItems) |
|---|
| 344 | |
|---|
| 345 | return container |
|---|
| 346 | |
|---|
| 347 | def _buildMenu(container as Container, items as Dictionary<of String, System.Object>) |
|---|
| 348 | accelGroup = _ui['accelGroup'] to AccelGroup |
|---|
| 349 | for label, item in items |
|---|
| 350 | menuItem = MenuItem(label) |
|---|
| 351 | menu = Menu() |
|---|
| 352 | for subItem in item to dynamic |
|---|
| 353 | if subItem inherits List<of System.Object> |
|---|
| 354 | stockId, handler = subItem |
|---|
| 355 | subMenuItem = ImageMenuItem(stockId to String, accelGroup) |
|---|
| 356 | if subItem.count > 2 |
|---|
| 357 | for i in 2 : subItem.count |
|---|
| 358 | if subItem[i] inherits StateType and subItem[i] to StateType == StateType.Insensitive, subMenuItem.sensitive = true |
|---|
| 359 | |
|---|
| 360 | _ui['menu[stockId]'] = subMenuItem |
|---|
| 361 | |
|---|
| 362 | menu.append(subMenuItem) |
|---|
| 363 | |
|---|
| 364 | if handler inherits EventHandler |
|---|
| 365 | listen subMenuItem.activated, handler |
|---|
| 366 | else if subItem inherits List<of String> |
|---|
| 367 | menu.append(SeparatorMenuItem()) |
|---|
| 368 | else if subItem inherits Dictionary<of String, System.Object> |
|---|
| 369 | _buildMenu(menu, subItem) |
|---|
| 370 | menuItem.submenu = menu |
|---|
| 371 | container.add(menuItem) |
|---|
| 372 | |
|---|
| 373 | def _buildEditor as Widget |
|---|
| 374 | container = ScrolledWindow() |
|---|
| 375 | container.hscrollbarPolicy = PolicyType.Never |
|---|
| 376 | container.vscrollbarPolicy = PolicyType.Automatic |
|---|
| 377 | |
|---|
| 378 | fontDesc = FontDescription() |
|---|
| 379 | fontDesc.family = 'monospace' |
|---|
| 380 | _ui['defaultFontDescription'] = fontDesc |
|---|
| 381 | |
|---|
| 382 | languageManager = SourceLanguageManager.default |
|---|
| 383 | language = languageManager.getLanguage('cobra') |
|---|
| 384 | buffer = SourceBuffer(language) |
|---|
| 385 | listen buffer.changed, ref .bufferModifiedHandler |
|---|
| 386 | sourceView = SourceView(buffer) |
|---|
| 387 | sourceView.showLineNumbers = _config['lineNumbers'] to bool |
|---|
| 388 | sourceView.wrapMode = Gtk.WrapMode.Word |
|---|
| 389 | sourceView.modifyFont(fontDesc) |
|---|
| 390 | |
|---|
| 391 | _ui['sourceBuffer'] = buffer |
|---|
| 392 | _ui['sourceView'] = sourceView |
|---|
| 393 | _ui['fontDescription'] = fontDesc |
|---|
| 394 | |
|---|
| 395 | container.add(sourceView) |
|---|
| 396 | |
|---|
| 397 | return container |
|---|
| 398 | |
|---|
| 399 | def _buildStatus as Widget |
|---|
| 400 | container = Statusbar() |
|---|
| 401 | _ui['statusBar'] = container |
|---|
| 402 | return container |
|---|
| 403 | |
|---|
| 404 | def _saveFileAs |
|---|
| 405 | fileChooser = FileChooserDialog('Save file as...', _ui['window'] to Window, FileChooserAction.Save, _ |
|---|
| 406 | "Cancel", ResponseType.Cancel, "Save", ResponseType.Accept) |
|---|
| 407 | |
|---|
| 408 | if fileChooser.run == (ResponseType.Accept to int) |
|---|
| 409 | # Save the file |
|---|
| 410 | pass |
|---|
| 411 | fileChooser.destroy |
|---|